# mjava-minglei 明磊业务集成服务。项目基于 Spring Boot 2.7 构建,用于对接钉钉、宜搭、员工花名册以及明磊外部 T100 接口。 本文档说明当前工程的模块边界、配置项、REST 接口、ERP 推送规则和本地构建方式。文档中的凭证均使用占位符,真实密钥必须通过环境变量或部署配置注入。 ## 1. 技术栈 | 技术 | 说明 | | ------------------ | -------------------------- | | Java | JDK 8 | | Spring Boot | 2.7.18 | | Spring MVC | REST Controller、参数绑定、异常处理 | | Spring Validation | 请求参数校验 | | Maven | 构建和依赖管理 | | `com.malk:mjava` | 钉钉、宜搭客户端和基础模型,当前版本 `0.0.3` | | Jackson / Fastjson | JSON 绑定、序列化和表单字段组装 | | RestTemplate | 调用外部 T100 HTTP 接口 | | Logback + SLF4J | 应用日志、错误日志和请求响应日志 | | JUnit 5 + Mockito | 单元测试 | ## 2. 工程结构 ```text src/main/java/com/malk/minglei |-- MjavaMingleiApplication.java |-- config | |-- DingTalkRosterConfiguration.java | |-- EmployeeRosterConfiguration.java | |-- EmployeeRosterProperties.java | |-- HttpRequestResponseLoggingFilter.java | |-- MingleiPortConfiguration.java | |-- MingleiPortProperties.java | |-- MjavaYidaConfiguration.java | `-- YidaApplicationsProperties.java |-- controller | |-- ApprovalTransferDepartmentController.java | |-- EmployeeRosterController.java | |-- ErpYidaPushController.java | |-- GlobalExceptionHandler.java | |-- MingleiPortController.java | `-- YidaProcessController.java |-- dto `-- service |-- approval |-- employee |-- port |-- yida `-- impl |-- approval |-- employee |-- port `-- yida ``` ### 2.1 分层职责 | 层级 | 职责 | | ------------------ | ----------------------------------------------------- | | `controller` | 暴露 HTTP 接口,处理请求校验和响应包装 | | `dto` | 请求体、响应体和业务数据模型 | | `service.approval` | 钉钉审批实例查询、表单解析,以及通过宜搭审批矩阵(按"部门"字段匹配)解析转入部门各级别负责人与部门负责人 | | `service.employee` | 员工花名册同步、查询和钉钉人员数据处理 | | `service.port` | 明磊外部 T100 接口转发 | | `service.yida` | 宜搭流程、宜搭表单查询、ERP 推送分发 | | `service.impl.*` | 各业务实现、处理器和内部辅助类 | 新增能力时优先放入现有业务域。只有出现清晰的新业务边界、独立生命周期或独立外部依赖时,才新增新的 `Service` 分类。 ## 3. 配置说明 配置文件位于 `src/main/resources/application.yml`。生产环境不要使用文件中的默认示例值,应通过环境变量覆盖。 | 配置项 | 环境变量 | 用途 | | ------------------------------------------------------ | ------------------------------------------------------ | --------------------------------------------- | | `server.port` | `SERVER_PORT` | HTTP 服务端口 | | `spring.profiles.active` | `SPRING_PROFILES_ACTIVE` | Spring Profile | | `dingtalk.agent-id` | `DINGTALK_AGENT_ID` | 钉钉应用 AgentId | | `dingtalk.app-key` | `DINGTALK_APP_KEY` | 钉钉应用 Key | | `dingtalk.app-secret` | `DINGTALK_APP_SECRET` | 钉钉应用 Secret | | `minglei.yida.coordination-office.app-type` | `YIDA_COORDINATION_OFFICE_APP_TYPE` | 协同办公宜搭应用编码 | | `minglei.yida.coordination-office.system-token` | `YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN` | 协同办公宜搭应用 System Token | | `minglei.yida.master-data.app-type` | `YIDA_MASTER_DATA_APP_TYPE` | 主数据宜搭应用编码 | | `minglei.yida.master-data.system-token` | `YIDA_MASTER_DATA_SYSTEM_TOKEN` | 主数据宜搭应用 System Token | | `minglei.port.supplier-price-approval-url` | `MINGLEI_SUPPLIER_PRICE_APPROVAL_URL` | 外部 T100 接口地址 | | `minglei.port.supplier-price-approval-key` | `MINGLEI_SUPPLIER_PRICE_APPROVAL_KEY` | 外部 T100 接口接入密钥 | | `minglei.banking.base-url` | `MINGLEI_BANKING_BASE_URL` | 银企直连地址,默认沙盒环境 | | `minglei.banking.app-id` | `MINGLEI_BANKING_APP_ID` | NexBank 应用标识 | | `minglei.banking.app-secret` | `MINGLEI_BANKING_APP_SECRET` | NexBank API Key | | `minglei.banking.customer-code` | `MINGLEI_BANKING_CUSTOMER_CODE` | NexBank 客户编号 | | `minglei.banking.default-from-account-no` | `MINGLEI_BANKING_DEFAULT_FROM_ACCOUNT_NO` | 默认付款账号;支付请求未传 `fromAccountNo` 时使用 | | `minglei.banking.default-from-account-name` | `MINGLEI_BANKING_DEFAULT_FROM_ACCOUNT_NAME` | 默认付款账户名称;支付请求未传 `fromAccountName` 时使用 | | `minglei.banking.intra-bank-name` | `MINGLEI_BANKING_INTRA_BANK_NAME` | 本行名称;匹配收款银行时自动发送 `crossBank=0` | | `minglei.banking.payment-encryption-enabled` | `MINGLEI_BANKING_PAYMENT_ENCRYPTION_ENABLED` | 单笔支付是否启用 SM2/SM4 加密,默认 `true` | | `minglei.banking.xencio-public-key` | `MINGLEI_BANKING_XENCIO_PUBLIC_KEY` | NexBank 的 SM2 十六进制公钥,仅支付制单需要 | | `minglei.banking.enterprise-private-key` | `MINGLEI_BANKING_ENTERPRISE_PRIVATE_KEY` | 企业 SM2 十六进制私钥,仅支付制单需要 | | `minglei.employee-roster.datasource.url` | `MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_URL` | 员工花名册 MySQL JDBC 地址 | | `minglei.employee-roster.datasource.username` | `MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_USERNAME` | 员工花名册数据库用户名 | | `minglei.employee-roster.datasource.password` | `MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_PASSWORD` | 员工花名册数据库密码 | | `minglei.employee-roster.datasource.driver-class-name` | `MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_DRIVER_CLASS_NAME` | JDBC 驱动类 | | `minglei.dictionary.datasource.url` | `MINGLEI_DICTIONARY_DATASOURCE_URL` | 数据字典 Oracle JDBC 地址 | | `minglei.dictionary.datasource.username` | `MINGLEI_DICTIONARY_DATASOURCE_USERNAME` | 数据字典 Oracle 用户名 | | `minglei.dictionary.datasource.password` | `MINGLEI_DICTIONARY_DATASOURCE_PASSWORD` | 数据字典 Oracle 密码 | | `minglei.dictionary.datasource.driver-class-name` | `MINGLEI_DICTIONARY_DATASOURCE_DRIVER_CLASS_NAME` | Oracle JDBC 驱动类,默认 `oracle.jdbc.OracleDriver` | | `logging.file.path` | `LOGGING_FILE_PATH` | 日志输出目录 | `MjavaYidaConfiguration` 注册两套宜搭客户端: | Bean | 用途 | | ------------------------------ | ----------------- | | `coordinationOfficeYidaClient` | 协同办公应用,主要用于发起流程 | | `masterDataYidaClient` | 主数据应用,主要用于查询主数据表单 | ## 4. REST 接口 ### 4.1 宜搭流程发起 ```http POST /api/yida/process-instances Content-Type: application/json ``` 请求示例: ```json { "type": "cpmp402", "userId": "41603502201288023", "dataTitle": "包材核价单", "fields": { "mainData": { "rq": "2026-09-08", "bm": "ML07000000", "tjr": "3075", "gs": "ML", "t100id": "ML", "ent": "87", "t100bm": "ML07000000" }, "detailData": { "detailData": [ { "hjdh": "MLCH326090800001", "gysbh": "113009", "gysmc": "供应商名称", "t100id": "ML", "ent": "87", "yy": "zh_CN" } ] } } } ``` 成功响应为 HTTP `201 Created`: ```json { "code": 200, "isSuccess": true, "result": { "processInstanceId": "process-instance-id" } } ``` ### 4.2 宜搭主数据表单查询 分页查询: ```http GET /api/yida/minglei/forms/instances?page=1&size=20 ``` 精确查询: ```http POST /api/yida/minglei/forms/instances/query Content-Type: application/json ``` 请求示例: ```json { "dept_id": "123456", "user_id": "dingTalkUserId", "dept_name": "明磊锂能技术部" } ``` 精确查询优先级为 `dept_id`、`user_id`、`dept_name`。传入 `user_id` 时,服务端会先查询人员所属部门,再按部门 ID 查询主数据。响应固定包含 `employeeField_mszdqrl8`、`employeeField_mszdqrl9`、`employeeField_mszdqrla`、`employeeField_mszdqrlb`、`employeeField_mualkf6a`(部门负责人)和 `employeeField_mszdqrl3`;未匹配到审批矩阵记录或字段未配置时,对应字段值为 `null`。 ### 4.3 部门成员查询 ```http POST /api/yida/minglei/departments/users Content-Type: application/json ``` 请求示例: ```json { "dept_Id": "123456,234567", "type": 0, "userIds": ["user-1", "user-2"] } ``` `userIds` 为可选的用户 ID 列表。未传或传入空数组时不影响部门成员查询结果;传入非空列表时,列表中的用户 ID 会按传入顺序追加在部门成员列表之后,并与部门成员及列表内其他 ID 一并去重。 `dept_Id` 支持单个部门 ID,或多个以英文逗号分隔的部门 ID;返回结果为各部门查询范围内人员的去重合集。 `type` 取值: | 值 | 说明 | | --- | ------------ | | `0` | 查询指定部门和所有子部门 | | `1` | 仅查询指定部门 | ### 4.4 员工花名册 本地表 `employee_roster` 的 `is_resigned` 字段以 `1` 表示在职、`0` 表示离职。同步读取到钉钉通讯录用户的 `active=false` 时,会将该人员更新为离职;花名册按钉钉用户 ID 或工号的查询会自动排除离职人员。 每次同步完成后,服务会按 `username` 检查在职重名人员。存在重名时,服务从主数据宜搭表单 `FORM-66F0B2216DD64580834D75815BFEF24138F7` 的首条记录读取成员字段 `employeeField_mv94lty1`,并向其中配置的人事人员发送钉钉工作通知。 同步指定部门: ```http POST /api/yida/minglei/employee-roster/sync?departmentId=1&type=0 ``` 全量同步: ```http POST /api/yida/minglei/employee-roster/sync-full ``` 按钉钉用户 ID 查询花名册: ```http POST /api/yida/minglei/employee-roster/getUserRoster Content-Type: application/json ``` ### 4.5 数据字典 完整调用说明见 [数据字典接口文档](DATA-DICTIONARY-API.md)。 按字典编码查询已注册的数据字典,筛选条件通过 URL 查询参数传入。当前支持: | 字典编码 | 查询对象 | 返回字段 | 筛选条件 | | ------------------------ | -------------------------------- | ------------------------------- | ------------------------------- | | `erp-supplier` | `DSDATA.erp_apmt440_pmaauc_v` | 视图全部字段 | 必填:`t100id`、`ent`;可选:`pmaal004` | | `erp-packaging-b9-window-view` | `DSDATA.imaf_t`、`DSDATA.imaal_t` | `imaf001`、`imaal003`、`imaal004` | 必填:`t100id`、`ent`;可选:`keyword` | | `erp-packaging-material` | `DSDATA.pmaauc_t` | `pmaauc001`、`pmaauc002`、`tp` | 必填:`ent`;可选:`keyword` | | `erp-item-view` | `DSDATA.erp_apmt440_imaauc_v` | 视图全部字段 | 必填:`ent`;可选:`keyword`(匹配 `imaa001`、`imaal003`) | | `erp-sales-order` | `DSDATA.xmda_t`、`DSDATA.xmdc_t`、`DSDATA.xmdd_t`、`DSDATA.ooff_t` | 销售订单、客户、料件、未交数量和开票信息 | 必填:`t100id` | | `erp-production-order` | `DSDATA.xmda_t`、`DSDATA.xmdc_t`、`DSDATA.xmdd_t`、`DSDATA.ooff_t` | 生产令、客户、料件、数量和开票信息 | 必填:`t100id`、`qx` | | `erp-supplier-disabled` | `DSDATA.ERP_APMT440_PMAAUC_N` | 禁用供应商视图全部字段 | 必填:`t100id`、`ent`;可选:`keyword`(匹配 `pmaal004`) | | `erp-active-item` | `DSDATA.imaa_t`、`DSDATA.imaf_t`、`DSDATA.imaal_t` | `imaa001`、`imaal003`、`imaal004` | 可选:`keyword`(匹配 `imaa001`、`imaal003`) | | `erp-template-item` | `DSDATA.imaa_t`、`DSDATA.imaal_t` | `imaa001`、`imaal003`、`imaal004` | 可选:`keyword`(匹配 `imaa001`、`imaal003`) | | `erp-template-orders` | `DSDATA.xmda_t`、`DSDATA.xmdc_t` | 范本料号关联订单及料件明细 | 必填:`bgfblh`;可选:`keyword`(匹配 `xmdadocno`) | | `erp-last-price` | `DSDATA.pmdi_t`、`DSDATA.pmdj_t` | `pmdj011` | 必填:`main.pmdi004`、`detail_1.ljbh` | | `erp-outsourced-purchase-order-window` | `DSDATA.pmdp_t`、`DSDATA.imaal_t` | `pmdpseq`、`pmdp003`、`pmdp001`、`imaal003`、`imaal004`、`pmdp023` | 必填:`formtable_main_158_dt1_wwcgdh` | | `erp-outsourced-bom` | `DSDATA.pmdp_t`、`DSDATA.sfba_t`、`DSDATA.imaal_t` | `sfba006`、`imaal003`、`imaal004`、`sfba013` | 必填:`formtable_main_158_dt1_wwcgdh`、`formtable_main_158_dt1_cgxc` | 供应商视图: ```http GET /api/dictionary/erp-supplier?t100id=ML&ent=明磊&pmaal004=供应商 ``` 明磊包材B9开窗视图: ```http GET /api/dictionary/erp-packaging-b9-window-view?t100id=ML&ent=明磊&keyword=B9开窗 ``` 包材材质: ```http GET /api/dictionary/erp-packaging-material?ent=明磊&keyword=彩盒 ``` ERP 料件视图: ```http GET /api/dictionary/erp-item-view?ent=明磊&keyword=电池 ``` 销售订单开窗: ```http GET /api/dictionary/erp-sales-order?t100id=ML ``` 生产令开窗: ```http GET /api/dictionary/erp-production-order?t100id=ML&qx=OWNER-A,OWNER-B ``` ERP 供应商禁用视图: ```http GET /api/dictionary/erp-supplier-disabled?t100id=ML&ent=明磊&keyword=供应商 ``` T100 有效料号: ```http GET /api/dictionary/erp-active-item?keyword=B9 ``` 范本料号开窗: ```http GET /api/dictionary/erp-template-item?keyword=B9 ``` 范本料号开窗对应所有订单: ```http GET /api/dictionary/erp-template-orders?bgfblh=B9&keyword=SO-001 ``` 取料号上次价格: ```http GET /api/dictionary/erp-last-price?main.pmdi004=SUP-001&detail_1.ljbh=B9-001 ``` 委外采购单开窗: ```http GET /api/dictionary/erp-outsourced-purchase-order-window?formtable_main_158_dt1_wwcgdh=WWCG-001 ``` 委外采购对应工单备料: ```http GET /api/dictionary/erp-outsourced-bom?formtable_main_158_dt1_wwcgdh=WWCG-001&formtable_main_158_dt1_cgxc=10 ``` `erp-supplier` 的 `pmaal004` 按供应商名称模糊匹配。`erp-packaging-b9-window-view` 使用单一 `keyword` 参数,后端会将其分别与 `imaf001`(料件编号)、`imaal003`(中文品名)和 `imaal004`(规格)进行模糊匹配,任一字段命中即可返回。明磊包材B9开窗视图仅返回中文料件信息,排除 `imaf016` 为 `TY` 或 `LTY` 的记录,并只包含编码以 `B9`、`VB9` 或 `J` 开头的料件。 `erp-supplier`、`erp-packaging-b9-window-view` 和 `erp-supplier-disabled` 的 `t100id`、`ent` 均为必填筛选条件;`erp-packaging-material` 和 `erp-item-view` 仅需传入 `ent`。销售订单开窗仅需传入 `t100id`,固定查询公司 `86` 中已审核、存在未交数量且不属于指定排除客户的订单。生产令开窗需传入 `t100id` 与 `qx`,其中 `qx` 为逗号分隔的拥有者权限值,固定查询公司 `86` 中已审核的订单。范本料号关联订单需传入 `bgfblh`,服务端按该料号前缀匹配 ML 据点未作废订单。取料号上次价格需同时传入 `main.pmdi004`(主表料号)和 `detail_1.ljbh`(子表料件编号),服务端返回有效采购价格单中最新确认时间的 `pmdj011`。有效料号和范本料号均可选 `keyword`,同时模糊匹配 `imaa001`(料号)和 `imaal003`(中文品名);范本料号关联订单可选 `keyword` 模糊匹配 `xmdadocno`(订单号);ERP 供应商禁用视图可选 `keyword` 模糊匹配 `pmaal004`(供应商名称)。包材材质可用单一 `keyword` 对 `pmaauc001`(包材编号)、`pmaauc002`(包材名称)和 `tp`(中文材质类型)进行模糊匹配,任一字段命中即可返回;ERP 料件视图的 `keyword` 会同时模糊匹配 `imaa001`(料号)和 `imaal003`(中文品名)。委外采购单开窗仅需传入 `formtable_main_158_dt1_wwcgdh`;委外采购对应工单备料需同时传入采购单号 `formtable_main_158_dt1_wwcgdh` 和采购项次 `formtable_main_158_dt1_cgxc`。两者 SQL 均固定查询公司 `86`、据点 `ML`。`ent=明磊` 映射为 ERP 公司编号 `86`,`ent=越南` 映射为 ERP 公司编号 `60`,其他公司名称会返回 `400`。十三个字典接口不接收 `page` 和 `pageSize`,服务端固定返回符合条件的前 `100` 条记录。包材材质的 `tp` 字段会将 `pmaauc016` 转换为 `说明书`、`彩盒`、`纸箱` 或 `标贴`。 成功响应的 `result` 统一为: ```json { "page": 1, "pageSize": 100, "total": 238, "records": [] } ``` 数据字典实现约定:固定 SQL、字典编码和允许的筛选参数统一维护在 `DictionaryQueryServiceImpl`,Controller 只处理 HTTP 参数与响应封装;SQL 必须使用 `NamedParameterJdbcTemplate` 绑定参数,不得拼接外部输入;新增字典时在同一 ServiceImpl 中增加路由分支、参数校验、单元测试和本节文档。未注册的字典编码或未声明的筛选条件会返回 `400`。 ### 4.6 审批转入部门主管查询 ```http POST /api/dingtalk/approval/transfer-department/managers Content-Type: application/json ``` 请求示例: ```json { "processInstanceId": "YuRRlAutRFi4CcsrPkqvQg05031789886060" } ``` 服务端处理链路: 1. 调用钉钉审批开放接口 `GET https://api.dingtalk.com/v1.0/workflow/processInstances`,通过 `x-acs-dingtalk-access-token` 请求头传入应用凭证。 2. 从 `result.formComponentValues` 中查找 `name` 为 `转入部门` 的表单组件;未匹配到时回退匹配 `bizAlias` 为 `transferDepartment` 的组件。 3. 解析该组件的 `extValue`,取第一个元素的 `id` 作为转入部门 ID,`deptName` 或 `name` 作为部门名称。`extValue` 中的 `id` 可能是字符串或数字,服务端统一按字符串处理。 4. 复用工程内 `MingleiDocumentService.queryDepartmentManagersByDepartmentId`(宜搭审批矩阵表单 `FORM-8F527F696C5B4AF7AB7A53979527FBA23USO`)按"部门"选择字段 `departmentSelectField_mszdqrks` 内嵌的部门 ID 精确匹配,查询各级别负责人,不再调用钉钉通讯录部门详情接口。 > 注意:第 4 步改为按"部门"选择字段匹配(而非原先的"部门编号"文本字段 `textField_msztwhs2`),且返回结果新增"部门负责人"字段(审批矩阵 `employeeField_mualkf6a`)。 响应中各级别负责人按字段分开返回,分别对应审批矩阵表单中的 `employeeField_mszdqrl8`(一级部门主管)、`employeeField_mszdqrl9`(二级部门主管)、`employeeField_mszdqrla`(三级部门主管)、`employeeField_mszdqrlb`(四级部门主管)、`employeeField_mualkf6a`(部门负责人)与 `employeeField_mszdqrl3`(部门分管领导)。 成功响应: ```json { "code": 200, "isSuccess": true, "result": { "departmentId": "972414534", "departmentName": "合作伙伴", "firstLevelManagerUserId": "manager-1", "secondLevelManagerUserId": "manager-2", "thirdLevelManagerUserId": "manager-3", "fourthLevelManagerUserId": "manager-4", "departmentHeadUserId": "manager-head", "departmentLeaderUserId": "manager-5" } } ``` 各部门负责人字段为对应级别审批人的钉钉 `userid`,审批矩阵未配置该级别负责人时该字段为 `null`。`getAllManagerUserIds()` 按一级至四级部门主管、部门负责人、部门分管领导的顺序返回所有非空的负责人 ID。 | 异常场景 | HTTP 状态码 | 说明 | | ----------------------- | -------- | -------------------------------------------------------------- | | `processInstanceId` 为空 | `400` | 请求参数不合法 | | 审批实例无表单数据或未包含转入部门字段 | `400` | 表单结构与预期不一致 | | 转入部门 `extValue` 缺少部门 ID | `400` | 无法解析部门 ID | | 审批实例接口调用失败或宜搭审批矩阵查询失败 | `502` | 返回 `DINGTALK_APPROVAL_CALL_FAILED` 或 `YIDA_CALL_FAILED` 及上游错误码 | | 宜搭应用或钉钉凭证配置缺失 | `412` | 返回配置缺失信息 | ### 4.7 外部 T100 接口转发 ```http POST /api/minglei/ports/supplier-price-approvals Content-Type: application/json ``` 宜搭连接器调用时传表单 ID 和流程实例 ID。服务端按 `formId` 选择字段映射规则,再使用钉钉审批实例接口读取表单字段、转换为 T100 核价单,并补齐 T100 协议外层的 `key`、`host`、`service`、`datakey`、`type`,以及生成 17 位毫秒时间戳。不要在连接器请求体中传入表头、明细、密钥或 T100 外层字段。 ```json { "formId": "FORM-468B6FB2D5BF490A8BC6DBD6FAF9BCCA28R9", "processInstanceId": "宜搭流程实例 ID" } ``` 接口按 `formId` 选择对应字段映射,当前已注册表单如下: | 表单 | `formId` | 明细字段映射 | | --- | --- | --- | | 供应商价格审批单(核价) | `FORM-468B6FB2D5BF490A8BC6DBD6FAF9BCCA28R9` | `tableField_mszqctqd`:料件编号、核价、备注 | | 供应商价格审批单(暂估) | `FORM-F906C997A4A44DC8B9180198C0B2E55F6W0S` | `tableField_msh186sv`:料件编号、材料价格、备注 | | 供应商价格审批单-核价(子公司) | `FORM-C09818E73B1640A9AA9F6D7F55C47612EFV8` | `tableField_mszqctqd`:料件编号、核价、备注 | | 供应商价格审批单-暂估(子公司) | `FORM-EC46C7109182497796AA0B70F3848576UYBU` | `tableField_msh186sv`:料件编号、暂估单价、备注 | 四个表单都会将单据编号、委外否、单号、报价单日期、核价供应商、生效日期、失效日期、使用部门、申请人映射至 T100 表头;明细的料件编号、价格、备注分别映射至 `pmdj002`、`pmdj011`、`pmdj030`。未注册的 `formId` 会返回参数错误,不会访问宜搭或 T100。`pmdiud001` 固定传 `N`,`pmdistus` 固定传 `I`。外部 T100 接口地址与密钥分别由 `MINGLEI_SUPPLIER_PRICE_APPROVAL_URL`、`MINGLEI_SUPPLIER_PRICE_APPROVAL_KEY` 配置。 ### 4.8 ERP 推送宜搭 ```http POST /api/erp/yida/push Content-Type: application/json ``` 请求示例: ```json { "type": "axmt140", "mainData": { "rq": "2026-09-08", "bm": "123456", "tjr": "3075", "gs": "ML" }, "detailData": { "detailData1": [ { "xmfyseq": "10", "xmfy002": "DN-001", "xmfy003": "2026-09-08", "xmfy004": "CUS-001", "pmaal004": "客户名称" } ] } } ``` 成功响应使用 `McR.success()`: ```json { "success": true, "code": "200", "message": "SUCCESS" } ``` ## 5. ERP 推送规则 ### 5.1 支持的类型 `type` 会去除首尾空格并转为大写后匹配,调用方传入大小写不敏感。 | `type` 或别名 | 处理器 | 单据或流程 | 表单 UUID | 流程编码 | | ------------------------------------------- | -------------------------------------- | ----------- | ------------------------------------------- | ----------------------------------------------- | | `cpmp402` | `Cpmp402YidaProcessHandler` | 包材核价单 | `FORM-A3D4889187E54697A59590BE54F350E7AOO5` | `TPROC--98G66QB1NTF8E1R1LI0OK4D6JI9437HC92YSM1` | | `FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K` | `MlWorkOrderCraftPriceFormPushHandler` | ML-工单工艺工价审定 | `FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K` | `TPROC--WG966BA1VWF8Q1K4OGF8H9KW47RR3ZREH0YSM2` | | `axmt500` | `ApproveOrderFormPushHandler` | 客户订单审批 | `FORM-8454FA17732746058A2D90B1C559CC19G5SR` | `TPROC--9VD66QA18AJ8317KK3NXM54W1HIT30C7N28TM7` | | `axmt140` | `CustomerReleaseFormPushHandler` | 客户放行审批 | `FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C` | `TPROC--6TG66381N8G8Y7L9GBEEY7QVXTUY2LBBGZ7TMU` | | `cint301` | `MiscReceiptIssueFormPushHandler` | 杂收杂发审批 | `FORM-4B08ADABCDAA45788C043729A855E03ED76G` | `TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0` | | `asft800` | `Asft800FormPushHandler` | 包装工单变更审批流程 | `FORM-40D6FBBBF4A94864A2AF9535AAC7924CRBH2` | `TPROC--U2I66F719HF854X6PIGK5A2AP9VC2LODB7YSM1` | | `aqct300` | `Aqct300FormPushHandler` | 检验情况审批流程 | `FORM-38C8592E7F5F45B3A388DA3FEFBA67C2GCEV` | `TPROC--LX766PC193N85EWUJ9ZLACNJNPSW3HBRG78TM1` | | `axmt510` | `Axmt510FormPushHandler` | 订单变更申请流程 | `FORM-8F318292B70D4ED39080EE0C631DC74BG3MD` | `TPROC--BHC66W71Q1N87Q0YFD7FVBNYXRJ62OIA378TM4` | | `apmt100` | `Apmt100FormPushHandler` | 供应商导入流程 | `FORM-9EE14CA14D8D4AEE8102944E669E6B0CFUKS` | `TPROC--75G66PD15YG82766OFSWD8BS4ZJ33YPRFOZSM2` | ### 5.2 `mainData` 规则 | 字段 | 说明 | | -------- | ----------------------------- | | `rq` | 申请日期,通常为 `yyyy-MM-dd` 或毫秒时间戳 | | `bm` | 钉钉部门 ID | | `tjr` | 提交人工号,用于查询员工花名册并解析钉钉 `userId` | | `gs` | 公司或组织编码 | | `t100id` | T100 业务标识 | | `ent` | 地区或企业编号 | | `t100bm` | T100 部门 | 顶层 `bm`、`gs`、`tjr` 非空时,会覆盖写入 `mainData`。服务端会将 `mainData` 中的通用字段补入各明细行,便于表单处理器复用。 ### 5.3 `detailData` 规则 `detailData` 外层必须是对象,内部至少包含一个数组字段。数组字段名支持: - `detailData` - `detailData1` - `detailData2` - 其他满足 `detailData\d*` 格式的字段 不同单据的约定: | 类型 | 明细字段 | | ------------------------------------------- | --------------------------------------------- | | `cpmp402` | `detailData` | | `FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K` | `detailData` | | `axmt500` | `detailData1`、`detailData2`,兼容历史 `detailData` | | `axmt140` | 优先 `detailData1`,缺失时回退到 `detailData` | | `cint301` | `detailData1` | | `asft800` | `detailData1` | | `aqct300` | `detailData1`~`detailData4` | | `axmt510` | `detailData1`~`detailData3` | | `apmt100` | 无明细,传入空对象 `{}` | 明细对象中可以直接传入宜搭 `fieldId`。服务端会保留包含 `Field_` 的字段值,用于补充默认字段映射。 ### 5.4 发起人解析 1. `ErpYidaPushServiceImpl` 优先使用顶层 `tjr`,其次使用 `mainData.tjr`。 2. 服务端通过员工花名册查询工号对应的 `dingtalkId`。 3. `dingtalkId` 会作为 `originatorUserId`、`userId` 和 `applicant` 写入请求上下文或明细行。 4. 匹配到 `YidaFormPushHandler` 时,提交人工号允许为空,具体处理器可以使用默认发起人兜底。 5. 未匹配到表单处理器并走通用 `YidaProcessService` 时,提交人工号不能为空。 ### 5.5 `asft800` 包装工单变更审批流程 调用 `POST /api/erp/yida/push` 时传入 `type: "asft800"`。表头使用 ERP 原始字段,明细必须放在 `detailData.detailData1`。 ```json { "type": "asft800", "bm": "123456789", "gs": "87", "tjr": "EMP0001", "mainData": { "rq": "2026-09-16", "bm": "123456789", "gs": "87", "tjr": "EMP0001", "t100id": "EMP0001", "ent": "87", "t100bm": "PACK", "sfkaent": "87", "sfkasite": "ML", "sfkadocno": "WO-20260916-001", "sfka900": "2", "sfka902": "2026/09/16", "sfka057": "包装工单", "sfka010": "ITEM-001", "imaal003": "锂电池包装组件", "imaal004": "标准规格", "sfka012": 100.5, "sfka006": "SOURCE-001", "sfkaud009": "MO-001", "sfka906": "变更物料及数量", "ooagud004": "dingtalk-user-id" }, "detailData": { "detailData1": [ { "sfkgent": "87", "sfkgdocno": "WO-20260916-001", "sfkg900": "2", "sfkgseq": "10", "sfkg901": "变更", "sfkg006": "MAT-002", "imaal003": "新物料名称", "imaal004": "新物料规格", "sfkg013": 101.5, "sfkg014": "PCS", "sfba006": "MAT-001", "o_imaal003": "原物料名称", "o_imaal004": "原物料规格", "sfba013": 100.0 } ] } } ``` `tjr` 是员工工号,服务端会从员工花名册解析对应钉钉用户作为发起人;未传时,处理器使用 `mainData.ooagud004` 作为钉钉用户 ID。`bm` 为钉钉部门 ID,`sfka902` 支持 `yyyy-MM-dd` 或 `yyyy/MM/dd` 格式。 ### 5.6 `aqct300` 检验情况审批流程 调用 `POST /api/erp/yida/push` 时传入 `type: "aqct300"`。表头使用 ERP 原始字段;明细分别放入 `detailData1`(检验项目)、`detailData2`(缺点原因)、`detailData3`(计量检验)、`detailData4`(来源明细)。 当前仅发起宜搭流程,不做 ERP 回写。以下字段宜搭无对应控件,推送时丢弃:`qcba005`(交易对象编号)、`qcba010`(料件编号)、`qcba027`(不良数)、`detailData3.qcbd002`(检验位置)。`qcba000` 需直接传宜搭选项值:`IQC` / `FQC` / `Inventory QC`。 ```json { "type": "aqct300", "bm": "123456789", "gs": "87", "tjr": "EMP0001", "mainData": { "rq": "2026-09-21", "bm": "123456789", "gs": "87", "tjr": "EMP0001", "qcbaent": "87", "qcbadocno": "QC-20260921-001", "qcbadocdt": "2026/09/21", "qcba000": "IQC", "qcba008": 100, "qcba009": "PCS", "imaal003": "料件品名", "imaal004": "规格", "qcba012": "产品特征", "qcba016": "EA", "qcba017": 80, "qcba022": "PASS", "qcba023": 75, "qcba024": "张三", "qcba034": "LOT-001" }, "detailData": { "detailData1": [ { "qcbdent": "87", "qcbddocno": "QC-20260921-001", "qcbdseq": "10", "qcbd001": "DIM", "oocql004": "尺寸检验", "qcbd009": 20, "qcbd010": 1, "qcbd011": "NG", "qcbd021": 1 } ], "detailData2": [ { "qcbeent": "87", "qcbedocno": "QC-20260921-001", "qcbeseq": "10", "qcbe001": "划伤", "qcbe002": 2, "qcbe003": "备注", "qcbe004": 2 } ], "detailData3": [ { "qcbdent": "87", "qcbddocno": "QC-20260921-001", "qcbdseq": "10", "qcbd013": 10.5, "qcbd015": 9.5, "qcbg002": 10.1, "qcbd017": "mm", "qcbg003": "Y", "qcbd001": "SIZE", "oocql004": "检验标准", "qcbd019": "备注" } ], "detailData4": [ { "qcbaucseq": "1", "qcbauc014": "PO", "qcbauc001": "PO-001", "qcbauc002": "10", "qcbauc003": "REF-001", "qcbauc004": "20", "qcbauc005": "SUP-001", "qcbauc008": 100, "qcbauc009": "PCS", "qcbauc010": "ITEM-001", "imaal003": "料件品名", "imaal004": "规格" } ] } } ``` `tjr` 为员工工号,服务端从花名册解析钉钉用户作为发起人与申请人;`bm` 为钉钉部门 ID;`qcbadocdt` / `rq` 支持 `yyyy-MM-dd` 或 `yyyy/MM/dd`。 ### 5.7 `axmt510` 订单变更申请流程 调用 `POST /api/erp/yida/push` 时传入 `type: "axmt510"`。表头使用 ERP 原始字段;明细分别放入 `detailData1`(订单变更单身)、`detailData2`(交期明细)、`detailData3`(配件明细)。 当前仅发起宜搭流程,不做 ERP 回写。表头「变更事件」、附件与已到物料相关字段无对应 ERP 字段,推送时不填。子表1 `xmegkk` 映射到「变更事件」选项:`数量变更` / `交期变更` / `其他变更`。 ```json { "type": "axmt510", "bm": "123456789", "gs": "87", "tjr": "EMP0001", "mainData": { "rq": "2026-09-23", "bm": "123456789", "gs": "87", "tjr": "EMP0001", "xmeeent": "87", "xmeedocno": "SO-20260923-001", "xmee900": "1", "xmeedocdt": "2026/09/20", "xmee001": 2, "xmee002": "dingtalk-sales-user", "xmee003": "dingtalk-dept-id", "xmee004": "C001", "pmaal004": "客户简称", "xmee013": "Y", "xmee042": 12800.5, "xmee901": "2", "xmee902": "2026/09/23", "xmee903": "R01", "xmee904": "交期调整" }, "detailData": { "detailData1": [ { "xmegent": "87", "xmegdocno": "SO-20260923-001", "xmegseq": "10", "xmeg900": "1", "xmeg001": "ITEM-001", "xmeg002": "feature-a", "imaal003": "品名", "imaal004": "规格", "xmdc007": 100, "xmdc012": "2026/09/30", "xmdc015": 10.5, "xmeg006": "PCS", "xmeg007": 120, "xmeg012": "2026/10/15", "xmeg015": 11, "xmeg046": 1100, "xmeg047": 1243, "xmeg048": 143, "xmeg049": "RC01", "xmeg053": "HC01", "xmeg901": "2", "xmegud010": "MO-001", "xmegkk": "交期变更" } ], "detailData2": [ { "xmehent": "87", "xmehdocno": "SO-20260923-001", "xmehseq": "10", "xmehseq1": "1", "xmehseq2": "1", "xmeh900": "1", "xmeh001": "ITEM-001", "xmeh004": "PCS", "xmeh005": 120, "xmeh009": "1", "xmeh011": "2026/10/15" } ], "detailData3": [ { "xmfwucent": "87", "xmfwucdocno": "SO-20260923-001", "xmfwucseq": "10", "xmfwucseq1": "1", "xmfwuc001": "PART-001", "imaal003": "配件品名", "imaal004": "配件规格", "xmfwucud001": "feat-p", "xmfwuc006": 2, "xmfwuc007": 1, "xmfwuc008": "PCS", "xmfwuc009": 240, "xmfwuc900": "1", "xmfwuc901": "2" } ] } } ``` `tjr` 为员工工号;`xmee002` / `xmee003` 需传钉钉用户 ID / 部门 ID。日期字段支持 `yyyy-MM-dd` 或 `yyyy/MM/dd`。流程标题为 `订单变更申请流程-{订单单号}-{变更序}`。 ### 5.8 `apmt100` 供应商导入流程 调用 `POST /api/erp/yida/push` 时传入 `type: "apmt100"`。该流程无明细,`detailData` 必须为 `{}`。 ```json { "type": "apmt100", "bm": "123456789", "gs": "ML", "tjr": "EMP0001", "mainData": { "rq": "2026-09-16", "sqlx": "NEW", "gysmc": "供应商名称", "gysbm": "SUP-001", "sh": "91330100TEST", "gczczj": "1000000.50", "clsj": "2026/09/16", "zysccp": "锂电池包装组件", "gcgm": "120", "lcbh": "PM-001", "smzq": "ACTIVE", "pmbb033all": "CNY", "pmbb034all": "VAT", "pmbb053all": "NET30", "pmbb033": "CNY", "pmbb034": "VAT", "pmbb053": "NET30", "pmbb037": "NET30", "gst100": "ML", "lxr": "张三", "gsdz": "浙江省杭州市", "lxfs": "13800000000" }, "detailData": {} } ``` `lcbh` 用于生成流程标题;若其为空,可使用 `docno`。`clsj` 支持 `yyyy-MM-dd` 或 `yyyy/MM/dd`。所有 `pmbb***all` 字段映射到集团惯用数据,未带 `all` 后缀的 `pmbb***` 映射到据点惯用数据。 ## 6. 日志和异常 ### 6.1 日志 Logback 输出: - 控制台日志。 - `${LOGGING_FILE_PATH}/application.log`。 - `${LOGGING_FILE_PATH}/error.log`。 - `archive` 目录下的滚动归档日志。 `HttpRequestResponseLoggingFilter` 会记录 HTTP 方法、URI、状态码、耗时、请求体和响应体。单个载荷最大记录 10KB,并对 `password`、`secret`、`token`、`authorization`、`accessToken`、`systemToken`、`key` 等字段脱敏。 ### 6.2 异常响应 | 场景 | HTTP 状态码 | 说明 | | -------------- | -------- | ----------------------------- | | 请求体格式错误或参数校验失败 | `400` | 请求不合法 | | 配置缺失 | `412` | 必要配置未提供 | | 宜搭或钉钉上游业务错误 | `502` | 返回上游错误码和错误信息 | | 外部 T100 调用失败 | `502` | 返回 `MINGLEI_PORT_CALL_FAILED` | ## 7. 构建和运行 环境要求: - JDK 8 - Maven 3.6+ - 可访问项目依赖仓库和内部 `malk` 依赖 本地构建: ```powershell mvn clean package ``` 本地运行: ```powershell $env:SERVER_PORT = "8080" $env:DINGTALK_APP_KEY = "" $env:DINGTALK_APP_SECRET = "" $env:YIDA_COORDINATION_OFFICE_APP_TYPE = "" $env:YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN = "" $env:YIDA_MASTER_DATA_APP_TYPE = "" $env:YIDA_MASTER_DATA_SYSTEM_TOKEN = "" mvn spring-boot:run ``` 运行打包产物: ```powershell java -jar target\mjava-minglei-1.0.0-SNAPSHOT.jar ``` ## 8. 测试 执行完整测试: ```powershell mvn clean test ``` 当前测试覆盖: - Spring Boot 上下文加载。 - Controller 请求和响应结构。 - 员工花名册同步、查询和字段提取。 - 宜搭流程处理器分发。 - ERP 推送到不同宜搭表单处理器的字段映射。 - 外部 T100 请求组装和响应转发。 - 钉钉审批实例详情调用、转入部门解析和宜搭审批矩阵(按"部门"字段匹配)各级别部门负责人与部门负责人提取。 当前验证结果: ```text Tests run: 52, Failures: 0, Errors: 0, Skipped: 1 ``` ## 9. 扩展规范 ### 9.1 新增宜搭流程处理器 新增可由 `/api/yida/process-instances` 触发的通用流程时: 1. 新建 `XxxYidaProcessHandler`。 2. 实现 `YidaProcessHandler`。 3. 在 `getType()` 返回业务路由编码。 4. 在处理器中维护表单 UUID、流程编码和字段映射。 5. 补充对应单元测试。 6. 更新 README 的支持类型说明。 ### 9.2 新增 ERP 表单推送处理器 新增可由 `/api/erp/yida/push` 路由的表单流程时: 1. 新建 `XxxFormPushHandler`。 2. 实现 `YidaFormPushHandler`。 3. 在 `getTypes()` 返回主 `type` 和必要别名。 4. 在处理器中维护表单 UUID、流程编码和字段映射。 5. 在 `src/test/java` 中补充字段映射和异常测试。 6. 更新 README 的 ERP 推送类型表。 ### 9.3 字段映射原则 - 业务 `type` 是路由键,不等于表单 UUID。 - 应用凭证、表单 UUID、流程编码由服务端维护,调用方不传。 - 表单处理器优先使用 ERP 原始字段名映射。 - 调用方确需补充时,可以传入宜搭 `fieldId` 字段。 - 不要在日志、文档或测试数据中写入真实密钥、Token、Cookie 或数据库密码。 ## 10. 运维注意事项 - 部署账号必须对 `LOGGING_FILE_PATH` 有写权限。 - 宜搭和钉钉接口所需的网络连通性、权限范围、组织数据权限由部署环境保证。 - 当前 Controller 未内置业务鉴权。如需访问控制,应在网关或统一认证层配置。 - 发生宜搭配置不匹配时,确认应用编码、System Token 和处理器选择的客户端属于同一应用。 - 员工花名册依赖数据库连接和数据完整性。若 `mainData.tjr` 无法解析,优先检查花名册同步状态和员工 `dingtalkId`。