# 采购订单管理系统 ## 项目概述 采购订单管理系统用于管理企业的采购流程,包括采购订单、送货通知、收货等功能。系统主要实现了ERP系统与SRM系统之间的采购数据比对功能,帮助企业及时发现并处理两个系统之间的数据差异。 ## 技术架构 - Spring Boot框架 - MyBatis Plus ORM框架 - Oracle数据库 - 前后端分离架构 ## 主要功能模块 1. 采购订单管理 2. 送货通知管理 3. 收货管理 4. 退货管理 5. 供应商管理 6. ERP与SRM系统数据比对 7. 钉钉消息通知 ## 核心数据实体 - **采购订单明细(PurchaseOrderDetail)**: 存储SRM系统采购订单数据 - **ERP采购订单数据(MesRohInData)**: 存储ERP系统采购订单数据 - **采购订单比对结果(PurchaseOrderCompare)**: 存储两系统数据比对结果 ## 核心功能实现 ### 数据同步与比对流程 1. 从SRM系统API获取采购订单明细数据 2. 根据单号和项次查询ERP系统中对应的采购订单数据 3. 计算两个系统中的待收数量差异 4. 将比对结果保存到数据库 ### 使用注意事项 - API调用频率不低于2小时 - 请求时间范围不大于24小时 - 数据类型转换需注意:ERP系统(Long)与SRM系统(Integer) ## REST API接口 ### 钉钉消息发送接口 #### 接口信息 - **URL**: `POST /api/dingtalk/sendMessage` - **功能**: 发送钉钉消息通知 - **描述**: 根据检验单号发送不合格检验单的钉钉通知消息 #### 请求参数 ```json { "releaseNo": "检验单号" } ``` **参数说明**: - `releaseNo` (String, 必填): 检验单号,用于查询对应的钉钉消息内容 #### 响应结果 ```json { "code": 200, "message": null, "successful": 0, "data": "接收成功" } ``` **响应字段说明**: - `code` (Integer): 响应状态码,200表示成功,500表示失败 - `message` (String): 错误信息,成功时为null - `successful` (Integer): 成功标识,0表示成功,1表示失败 - `data` (String): 响应数据,成功时为"接收成功",失败时为"接收失败" #### 使用示例 **请求示例**: ```bash curl -X POST http://localhost:9095/api/dingtalk/sendMessage \ -H "Content-Type: application/json" \ -d '{"releaseNo": "IQC202501270001"}' ``` **成功响应示例**: ```json { "code": 200, "message": null, "successful": 0, "data": "接收成功" } ``` **失败响应示例**: ```json { "code": 500, "message": "检验单号不能为空", "successful": 1, "data": "接收失败" } ``` #### 错误处理 接口会处理以下错误情况: 1. **参数为空**: 检验单号为空或null时返回错误信息 2. **数据不存在**: 检验单号对应的钉钉消息内容不存在时返回失败 3. **发送失败**: 钉钉API调用失败时返回失败信息 4. **系统异常**: 其他系统异常时返回异常信息 #### 注意事项 1. 确保检验单号在`DINGTALK_MSG`表中有对应的记录 2. 确保钉钉应用配置正确且有发送权限 3. 接口调用频率建议不超过每分钟1次 4. 建议在生产环境中添加接口访问权限控制 #### 详细文档 更多详细的API使用说明、错误处理、最佳实践等内容,请参考 [API_DOCUMENTATION.md](./API_DOCUMENTATION.md) 文件。 ## 数据库表结构 项目中包含以下主要数据表: 1. `PURCHASE_ORDER_DETAIL` - 存储SRM系统采购订单明细数据 2. `MES_ROH_IN_DATA` - 存储ERP系统采购订单数据 3. `PURCHASE_ORDER_COMPARE` - 存储ERP与SRM系统数据比对结果 ### 表结构说明 #### 采购订单明细表(PURCHASE_ORDER_DETAIL)的Oracle表结构 ```sql CREATE TABLE PURCHASE_ORDER_DETAIL ( ID NUMBER(19) PRIMARY KEY, PRODUCT_CODE VARCHAR2(50), PRODUCT_NAME VARCHAR2(100), PRODUCT_SCALE VARCHAR2(200), INNER_VENDOR_CODE VARCHAR2(50), INNER_VENDOR_NAME VARCHAR2(100), PROFIT_CENTER_CODE VARCHAR2(50), PROFIT_CENTER_NAME VARCHAR2(100), PURCHASE_TYPE NUMBER(2), PO_ERP_NO VARCHAR2(50), LINE_NO VARCHAR2(50), PO_LINE_NO_SHOW VARCHAR2(50), ERP_PURCHASE_DATE NUMBER(19), ORDER_STATUS VARCHAR2(2), PO_LINE_STATUS NUMBER(2), PURCHASE_UNIT_CODE VARCHAR2(20), PURCHASE_UNIT_NAME VARCHAR2(20), TOTAL_ANSWER_QTY NUMBER(10), TOTAL_DELIVERY_QTY NUMBER(10), TOTAL_RECEIVE_QTY NUMBER(10), TOTAL_RETURN_QTY NUMBER(10), PO_WAIT_DELIVERY_QTY NUMBER(10), SYS_WAIT_DELIVERY_QTY NUMBER(10), RETURN_WAIT_DELIVERY_QTY NUMBER(10), EXPECTED_DATE NUMBER(19), NOTICE_QTY NUMBER(10), NOTICE_UN_DELIVERY_QTY NUMBER(10), TOTAL_REPORT_FINISH_QTY NUMBER(10), ISSUED_SETS NUMBER(10), RECEIVE_STATUS NUMBER(2), VALID_FLAG NUMBER(1), EXTEND_N01 VARCHAR2(200), EXTEND_N02 VARCHAR2(200), EXTEND_N03 VARCHAR2(200), EXTEND_N04 VARCHAR2(200), EXTEND_N05 VARCHAR2(200), EXTEND_N06 VARCHAR2(200), EXTEND_N07 VARCHAR2(200), EXTEND_N08 VARCHAR2(200), EXTEND_N09 VARCHAR2(200), EXTEND_N10 VARCHAR2(200), EXTEND_N11 VARCHAR2(200), EXTEND_N12 VARCHAR2(200) ); ``` #### 采购订单比对表(PURCHASE_ORDER_COMPARE)的Oracle表结构 ```sql CREATE TABLE PURCHASE_ORDER_COMPARE ( ID NUMBER(19) PRIMARY KEY, BILL_NO VARCHAR2(50), ORDER_LINE_ID VARCHAR2(50), LINE_NO VARCHAR2(50), ERP_PURCHASE_QTY NUMBER(10), ERP_RECEIVED_QTY NUMBER(10), ERP_WAIT_RECEIVE_QTY NUMBER(10), SRM_PURCHASE_QTY NUMBER(10), SRM_RECEIVED_QTY NUMBER(10), SRM_WAIT_RECEIVE_QTY NUMBER(10), DIFF_FLAG NUMBER(1), DIFF_QTY NUMBER(10), PRODUCT_CODE VARCHAR2(50), PRODUCT_NAME VARCHAR2(100), CREATE_TIME DATE, UPDATE_TIME DATE ); ``` ### 序列 用于主键ID生成的序列: ```sql CREATE SEQUENCE SEQ_PURCHASE_ORDER_DETAIL START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; CREATE SEQUENCE SEQ_PURCHASE_ORDER_COMPARE START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; ``` ## 项目维护 如需修改或添加字段,请在相应的实体类文件中进行修改,并保持与数据库表结构同步。 ### 数据库维护 如需修改数据库表结构,请按照以下步骤: 1. 修改实体类文件中的字段定义 2. 修改对应的SQL文件中的表结构定义 3. 执行SQL语句更新数据库表结构 ## 性能优化 系统在处理大量数据时可能会遇到内存占用过高的问题,特别是在执行以下操作时: 1. 采购订单数据同步(`syncPurchaseOrderDetails`方法) 2. 送货单数据处理 ### 已实施的优化措施 1. **分批处理数据**:将大量数据分成小批次进行处理,减少一次性内存占用 2. **及时释放对象引用**:处理完毕后将不再使用的对象引用设为null,帮助GC回收内存 3. **优化SQL查询**:避免一次性加载大量数据到内存 ### JVM调优建议 在启动应用时,可以通过以下JVM参数优化内存使用: ```bash java -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar XkyCollection.jar ``` 参数说明: - `-Xms512m`:初始堆内存大小为512MB - `-Xmx1024m`:最大堆内存大小为1024MB(根据服务器实际可用内存调整) - `-XX:MetaspaceSize=128m`:初始元空间大小为128MB - `-XX:MaxMetaspaceSize=256m`:最大元空间大小为256MB - `-XX:+UseG1GC`:使用G1垃圾收集器,适合大内存应用 - `-XX:MaxGCPauseMillis=200`:最大GC暂停时间目标为200毫秒 ### 监控建议 1. 使用JConsole或VisualVM等工具监控应用内存使用情况 2. 关注GC日志,分析内存使用模式 3. 在生产环境中,可以添加以下参数开启GC日志: ``` -Xloggc:/path/to/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps ``` ## 常见问题排查 ### 内存溢出(OutOfMemoryError) 如果遇到内存溢出问题: 1. 检查是否有大量数据一次性加载到内存 2. 确认是否有内存泄漏(对象创建后未被释放) 3. 增加JVM堆内存大小 4. 考虑使用分页查询或流式处理大数据量 ### 服务卡顿 如果服务出现卡顿: 1. 检查是否有长时间运行的事务 2. 确认数据库连接是否正常释放 3. 查看GC日志,确认是否频繁发生Full GC 4. 优化数据库查询,添加适当的索引 ## 最佳实践 1. 避免在高峰期执行大量数据同步操作 2. 对于定时任务,选择在系统负载较低的时段执行 3. 使用适当的批处理大小(建议100-500条记录) 4. 定期清理不再需要的历史数据 ## 异步任务处理 系统使用异步任务处理机制来执行耗时操作,避免阻塞主线程和定时任务调度线程。主要包括以下几个部分: ### 线程池配置 系统配置了两个专用线程池: 1. **采购订单同步线程池 (purchaseTaskExecutor)** - 核心线程数:1(确保同一时间只有一个采购同步任务在执行) - 最大线程数:2 - 队列容量:5 - 拒绝策略:CallerRunsPolicy(调用者线程执行) 2. **通用异步任务线程池 (taskExecutor)** - 核心线程数:5 - 最大线程数:10 - 队列容量:25 - 拒绝策略:CallerRunsPolicy(调用者线程执行) ### 定时任务优化 所有定时任务都使用异步执行方式,防止互相阻塞: 1. **采购订单同步任务**:每天12:05执行一次,使用专用线程池 2. **设备实时数据获取**:每5分钟执行一次,使用通用线程池 3. **补偿逻辑**:每5分钟执行多次,使用通用线程池 4. **钉钉数据同步**:每53分钟执行一次,使用通用线程池 ### 任务执行状态管理 使用 AtomicBoolean 标记任务执行状态,避免同一任务重复执行: ```java private final AtomicBoolean isRunning = new AtomicBoolean(false); // 任务开始前检查 if(!isRunning. compareAndSet(false,true)){ log. info("上一次任务还在执行中,跳过本次执行"); return; } // 任务结束后重置状态 finally{ isRunning. set(false); } ``` ### 异步任务执行流程 1. 定时器触发任务 2. 检查任务是否已在运行,如已运行则跳过 3. 将任务提交到相应的线程池异步执行 4. 定时器立即返回,不等待任务完成 5. 任务在线程池中执行完毕后重置状态标记 这种机制确保了即使某个任务执行时间较长,也不会影响其他定时任务的正常执行。 ## 最新优化更新 在最近的优化中,我们进一步改进了系统性能和稳定性: 1. **送货单数据处理优化** - 实现了 XkyService.GetSaveDetail() 方法的分批处理 - 每批处理10条送货单数据,减少内存占用 - 增强了异常处理,单条数据异常不会影响整批处理 2. **异步任务处理增强** - 为 DeliveryNoticeService 添加了 @Async 注解的异步处理方法 - 实现了 processAsyncBatch 方法,支持并行处理多批数据 - 优化了日志记录,便于问题排查 3. **错误处理改进** - 所有关键方法都添加了详细的日志记录 - 实现了更细粒度的异常捕获和处理 - 防止单个任务失败导致整个流程中断 ### 异步执行流程示例 以下是一个典型的异步执行流程: ``` 主线程: 开始处理100条数据 主线程: 将数据分为5批,每批20条 主线程: 提交批次1到异步线程池 主线程: 提交批次2到异步线程池 主线程: 提交批次3到异步线程池 主线程: 提交批次4到异步线程池 主线程: 提交批次5到异步线程池 主线程: 全部数据处理提交完成 异步线程1: 开始处理批次1 异步线程2: 开始处理批次2 异步线程3: 开始处理批次3 异步线程1: 批次1处理完成 异步线程1: 开始处理批次4 异步线程2: 批次2处理完成 异步线程2: 开始处理批次5 异步线程3: 批次3处理完成 异步线程1: 批次4处理完成 异步线程2: 批次5处理完成 ``` 这种方式确保了主线程不会被长时间阻塞,同时充分利用了系统资源进行并行处理。 ## 钉钉消息通知功能 系统集成了钉钉消息通知功能,用于在重要事件发生时向指定用户发送通知。主要包括以下几个部分: ### 功能概述 1. **通知场景**: - 采购订单数据比对完成后,通知相关人员查看结果 - 数据异常时的预警通知 - 系统重要操作的确认通知 - 不合格检验单审批情况通知 2. **通知方式**: - 个人工作通知:直接发送给指定用户 - 群机器人通知:发送到指定的钉钉群 ### 数据结构 钉钉用户信息存储在 `DINGTALK_INFO` 表中,表结构如下: | 字段名 | 类型 | 说明 | |------------------|---------|--------------------| | id | Long | 主键 | | sid | Long | 职工ID | | phone | String | 电话号码 | | dingtalk_id | String | 钉钉用户ID | | is_send_dingtalk | Integer | 是否发送钉钉通知(1:是, 0:否) | 不合格检验单信息存储在 `DINGTALK_MSG` 表中,表结构如下: | 字段名 | 类型 | 说明 | |---------------|--------|-------| | release_no | String | 检验单号 | | supp_name | String | 供应商名称 | | create_date | Date | 来料日期 | | project_codes | String | 项目代码 | | item_no | String | 料号 | | fname | String | 审核人 | | fng_handle | String | 处理方式 | ### 消息发送流程 1. **获取通知用户**: - 从 `DINGTALK_INFO` 表中筛选 `is_send_dingtalk` 为1的用户 - 如果用户的钉钉ID为空,则通过钉钉API根据手机号获取钉钉ID - 更新数据库中的钉钉ID 2. **发送通知**: - 汇总所有有效的钉钉用户ID - 根据检验单号查询 `DINGTALK_MSG` 表获取消息内容 - 调用钉钉开放API发送工作通知 ### 配置说明 钉钉应用配置信息存储在 `DataAcquisitionConfiguration` 中: ```java // 钉钉应用Key public static final String TALK_APP_KEY = "your_app_key"; // 钉钉应用Secret public static final String TALK_APP_SECRET = "your_app_secret"; // 钉钉自定义机器人Token public static final String CUSTOM_ROBOT_TOKEN = "your_robot_token"; ``` ### 使用方法 要发送钉钉通知,可以调用 `DingtalkInfoService` 的 `sendMessage` 方法: ```java @Autowired private DingtalkInfoService dingtalkInfoService; // 发送不合格检验单通知 boolean result = dingtalkInfoService.sendMessage("检验单号"); ``` ### 定时任务 系统配置了定时任务自动发送不合格检验单通知: ```java /** * 定时发送不合格检验单钉钉通知 * 每小时检查一次是否有新的不合格检验单 */ @Scheduled(cron = "0 0 */1 * * ?") public void sendInspectionNotification() { // 任务实现... } ``` ### 消息格式 不合格检验单通知的格式如下: ``` 供应商[xxx] 来料日期[yyyy-MM-dd] 项目[xxx] 料号[xxx]的不合格检验单被[xxx]审批为[xxx],请查收! ``` ### 注意事项 1. 钉钉消息发送频率有限制,请勿过于频繁发送 2. 确保应用有发送工作通知的权限 3. 用户手机号必须与钉钉注册手机号一致 4. 建议将重要通知同时通过多种渠道发送(如邮件、短信等) 5. 检验单通知在`DINGTALK_MSG`表中必须有对应的记录