已修改36个文件
已添加15个文件
已删除1个文件
2053 ■■■■■ 文件已修改
.gitignore 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/hrm_employee_import.md 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/hrm_employee_post_multiselect.md 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/process_param_bind_feedback_integration.md 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mes_process_param_bind_full.sql 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_employee_post_multi.sql 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_employee_post_multiselect_deploy.sql 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpAuditStatus.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ErpSaleOrderStatusListener.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/workstation/MesMdWorkstationWorkerController.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackRespVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/process/MesProProcessParamController.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/process/vo/param/MesProProcessParamRespVO.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/process/vo/param/MesProProcessParamSaveReqVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/process/MesProProcessParamDO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderParamRecordDetailDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/process/MesProProcessParamMapper.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderParamRecordMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/processparam/MesPdProcessParamServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/process/MesProProcessParamService.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/process/MesProProcessParamServiceImpl.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderParamRecordService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderParamRecordServiceImpl.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/SrmSupplierApplyController.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplyRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplyReviewReqVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/dataobject/SrmSupplierApplyDO.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/SupplierApplyStatusEnum.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyServiceImpl.java 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/listener/SrmSupplierApplyStatusListener.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserUpdateReqDTO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -59,5 +59,19 @@
.claude/worktrees/
.claude/scheduled_tasks.json
######################################################################
# AI / Personal Docs(个人工作文档,不纳入版本控制)
/docs/
/TODO.md
/fix_quartz_and_issues.md
# ç³»ç»Ÿ & æ—¥å¿—
.DS_Store
Thumbs.db
*.log
*.md
graphify-out/
.agents/
.agents/
# æ•°æ®åº“变更脚本(手动执行,不入库)
sql/sale_order_bpm_approval.sql
docs/hrm_employee_import.md
@@ -10,7 +10,7 @@
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. **下载模板** -> å‰ç«¯è°ƒç”¨ `GET /hrm/employee/import-template` ä¸‹è½½ Excel æ¨¡æ¿ï¼Œæ¨¡æ¿åŒ…含示例行供参考。
2. **填写模板** -> ç”¨æˆ·æŒ‰æ¨¡æ¿å¡«å†™å‘˜å·¥ä¿¡æ¯ï¼Œå…¶ä¸­"部门名称/岗位名称/用户角色"三列填**名称**(用户角色多个用中文逗号分隔),由后端自动转换为 ID。
2. **填写模板** -> ç”¨æˆ·æŒ‰æ¨¡æ¿å¡«å†™å‘˜å·¥ä¿¡æ¯ï¼Œå…¶ä¸­"部门名称/岗位名称/用户角色"三列填**名称**(岗位名称、用户角色多个用中文或英文逗号分隔),由后端自动转换为 ID。
3. **上传导入** -> å‰ç«¯è°ƒç”¨ `POST /hrm/employee/import`(multipart è¡¨å•,file + updateSupport)上传文件。
4. **展示结果** -> åŽç«¯è¿”回导入结果(创建成功名单 / æ›´æ–°æˆåŠŸåå• / å¤±è´¥åå•及原因),前端逐条展示。
@@ -30,6 +30,7 @@
- æ€§åˆ«ï¼šå¡«"男"/"女"(字典 `hrm_gender`,值 1/2)
- å‘˜å·¥çŠ¶æ€ï¼šå¡«"在职"/"试用"/"离职"(字典 `hrm_employee_status`,值 1/2/3)
- å²—位名称:填岗位名称,**支持多个**,多个用英文逗号(`,`)或中文逗号(`,`)分隔(如"部门经理,项目经理")
- ç”¨æˆ·è§’色:填角色名称,多个用**中文逗号**分隔(如"普通管理员,普通员工")
### å¯¼å…¥å‘˜å·¥
@@ -75,7 +76,7 @@
| æ€§åˆ« | æ¨¡æ¿åˆ— | å¿…填,默认男 |
| æ‰‹æœºå· | æ¨¡æ¿åˆ— | å¿…填,将作为系统登录账号 |
| éƒ¨é—¨åç§° | æ¨¡æ¿åˆ— | å¿…填,填名称,后端转 ID |
| å²—位名称 | æ¨¡æ¿åˆ— | å¿…填,填名称,后端转 ID |
| å²—位名称 | æ¨¡æ¿åˆ— | å¿…填,填名称,支持多个(用逗号分隔),后端转 ID |
| ç”¨æˆ·è§’色 | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œå¡«åç§°ï¼Œå¤šä¸ªç”¨ä¸­æ–‡é€—号分隔 |
| å…¥èŒæ—¥æœŸ | æ¨¡æ¿åˆ— | å¿…填,格式 YYYY-MM-DD |
| å‘˜å·¥çŠ¶æ€ | æ¨¡æ¿åˆ— | å¿…填,默认在职 |
docs/hrm_employee_post_multiselect.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
# å‘˜å·¥å°è´¦å²—位多选 - å‰ç«¯è”调方案
## èƒŒæ™¯
人力资源 > å‘˜å·¥ç®¡ç†ï¼ˆå‘˜å·¥å°è´¦ï¼‰çš„"岗位"由单选改为**多选**,与系统"用户管理"的多岗位能力保持一致。存储上采用 JSON æ•°ç»„(`post_ids` åˆ—),同"角色"字段的既有模式。
## æ¶‰åŠé¡µé¢
- äººåŠ›èµ„æº > å‘˜å·¥ç®¡ç† åˆ—表页(`src/views/hrm/employee/index.vue`)
- å‘˜å·¥æ–°å¢ž/编辑表单(`src/views/hrm/employee/modules/form.vue`、`data.ts`)
- å‘˜å·¥è¯¦æƒ…(`src/views/hrm/employee/modules/detail.vue`)
- å‘˜å·¥å¯¼å…¥å¼¹çª—(含下载模板、上传文件)
- å‘˜å·¥å¯¼å‡º Excel
## API å˜æ›´è¯´æ˜Ž
| æ–¹æ³• | è·¯å¾„ | å˜æ›´ |
|------|------|------|
| POST | `/hrm/employee/create` | è¯·æ±‚字段 `postId` â†’ `postIds`(数组) |
| PUT | `/hrm/employee/update` | è¯·æ±‚字段 `postId` â†’ `postIds`(数组) |
| GET | `/hrm/employee/get` | å“åº”字段 `postId/postName` â†’ `postIds/postNames` |
| GET | `/hrm/employee/page` | å“åº”字段 `postId/postName` â†’ `postIds/postNames` |
| GET | `/hrm/employee/export-excel` | "岗位名称"列支持多值(逗号拼接) |
| GET | `/hrm/employee/import-template` | "岗位名称"列示例改为多岗位 |
| POST | `/hrm/employee/import` | "岗位名称"列支持填写多个岗位名 |
### 1. æ–°å¢žå‘˜å·¥ `POST /hrm/employee/create`
**请求参数(字段变更):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| postIds | List\<Long\> | æ˜¯ | å²—位ID数组,至少一个(原 `postId` å•值字段已移除) |
```json
{
  "name": "张三",
  "phone": "13800138000",
  "deptId": 103,
  "postIds": [2, 4],
  "roleIds": [1],
  "hireDate": "2026-08-01",
  "employeeStatus": 1,
  "gender": 1
}
```
### 2. æ›´æ–°å‘˜å·¥ `PUT /hrm/employee/update`
同 create,请求体中的 `postIds` ä¸ºå‘˜å·¥æœ€ç»ˆçš„岗位集合(全量覆盖,非增量)。
### 3. å‘˜å·¥è¯¦æƒ… / åˆ†é¡µ `GET /hrm/employee/get`、`GET /hrm/employee/page`
**响应字段变更:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| postIds | List\<Long\> | å²—位ID数组(原 `postId` å·²ç§»é™¤ï¼‰ |
| postNames | String | å²—位名称,多个用英文逗号拼接,如"部门经理,项目经理"(原 `postName` å·²ç§»é™¤ï¼‰ |
```json
{
  "code": 0,
  "data": {
    "id": 6,
    "name": "张三",
    "deptName": "研发部",
    "postIds": [2, 4],
    "postNames": "部门经理,项目经理",
    "roleNames": "普通管理员"
  }
}
```
### 4. å¯¼å‡º Excel `GET /hrm/employee/export-excel`
- "岗位名称"列展示多个岗位名,英文逗号拼接(与分页返回的 `postNames` ä¸€è‡´ï¼‰ã€‚
### 5. å¯¼å…¥æ¨¡æ¿ / å¯¼å…¥ `GET /hrm/employee/import-template`、`POST /hrm/employee/import`
- æ¨¡æ¿"岗位名称"列由单个岗位名改为支持**多个岗位名**,多个用**英文逗号(`,`)或中文逗号(`,`)**分隔。
- æ¨¡æ¿ç¤ºä¾‹è¡Œï¼š`部门经理,项目经理`。
- ä»»ä¸€å²—位名不存在,该行导入失败,原因"岗位【xx】不存在"。
- å²—位名称列不允许为空(岗位必填)。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| postIds | è¡¨å•(新增/编辑)、详情 | è¡¨å•岗位改为多选下拉框,编辑回显勾选多个 |
| postNames | åˆ—表、详情、导出 | å±•示为逗号拼接的多个岗位名 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| è¡¨å•保存 | `postIds` å¿…填,至少一个岗位 |
| ç¼–辑保存 | `postIds` å…¨é‡è¦†ç›–该员工岗位 |
| å¯¼å…¥ | "岗位名称"列多个名称用逗号分隔,任一不存在则整行失败 |
| å·²æœ‰æ•°æ® | å­˜é‡å‘˜å·¥çš„单岗位数据已迁移为 `postIds`,老数据不丢失 |
## æ³¨æ„äº‹é¡¹
- **前端需同步修改**:新增/编辑表单的岗位字段从单选改为多选下拉,提交字段名从 `postId` æ”¹ä¸º `postIds`;列表/详情展示字段从 `postName` æ”¹ä¸º `postNames`。
- **编辑员工时同步更新**关联系统登录用户(用户管理页)的岗位/部门/昵称/角色/邮箱/备注,两边保持一致。
- å²—位下拉数据仍从系统岗位接口获取(与之前一致)。
- å²—位名称解析为精确匹配,不支持模糊。
docs/process_param_bind_feedback_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
# åŸºç¡€å‚数与工序绑定 + æŠ¥å·¥å¡«å†™å‚数字段 - å‰ç«¯è”调方案
## éœ€æ±‚背景
- åŸºç¡€å‚数(工艺参数,菜单「工艺设计 â†’ åŸºç¡€å‚数」)原本无法与「工序」直接绑定。
- æœ¬æ¬¡å®žçŽ°ï¼š**工序主数据可绑定基础参数**;任务无参数模板时,生成任务会按工序绑定的参数自动固化参数快照;**报工时必须填写绑定的参数字段**(必填参数强校验),报工详情/列表回显参数值。
## æ¶‰åŠé¡µé¢
1. å·¥åºè®¾ç½®é¡µï¼ˆ`mes/process-design/process/index`)——新增「工艺参数」Tab,维护工序绑定的参数
2. åŸºç¡€å‚数页(`mes/process-design/parameter/index`)——新增「关联工序」入口,查看该参数被哪些工序使用
3. ç”Ÿäº§ä»»åŠ¡åˆ›å»º/自动排产——任务按工序绑定参数生成参数快照(无模板时)
4. ç”Ÿäº§æŠ¥å·¥æ–°å¢ž/编辑——按任务参数记录渲染参数字段,必填参数校验
5. æŠ¥å·¥è¯¦æƒ…/列表——回显已填写的参数值
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
```
1. å·¥åºè®¾ç½®é¡µ â†’ ã€Œå·¥è‰ºå‚数」Tab â†’ å‹¾é€‰åŸºç¡€å‚数,设置是否必填/排序 â†’ ä¿å­˜åˆ° mes_pro_process_param
2. æŽ’产/手工建任务 â†’ ä»»åŠ¡çš„å·¥åºè‹¥æœªç»‘å®šå‚æ•°æ¨¡æ¿ï¼Œåˆ™ä»Žã€Œå·¥åºç»‘å®šçš„å‚æ•°ã€ç”Ÿæˆä»»åŠ¡å‚æ•°è®°å½•å¿«ç…§
   ï¼ˆå‚数来源优先级:工单工序绑定模板 â†’ è·¯çº¿å·¥åºç»‘定模板 â†’ å·¥åºä¸»æ•°æ®ç»‘定参数 â†’ æ— å‚数记录)
3. ç”Ÿäº§æŠ¥å·¥ â†’ æŒ‰ä»»åŠ¡å‚æ•°è®°å½•æ¸²æŸ“å‚æ•°å­—æ®µï¼ˆå‚æ•°å + å•位 + å¿…填星号)
4. æäº¤æŠ¥å·¥ â†’ å¿…填参数为空则后端拒绝(报错「必填参数【xxx】未填写」)
5. æŠ¥å·¥è¯¦æƒ…/列表 â†’ å›žæ˜¾æœ¬æ¬¡ä»»åŠ¡å‚æ•°å¿«ç…§åŠå·²å¡«å†™çš„å‚æ•°å€¼
```
## API
### ä¸€ã€å·¥åº-工艺参数绑定(新增)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/admin-api/mes/pro/process-param/list-by-process` | æŸ¥è¯¢å·¥åºç»‘定的参数列表(带参数详情/单位) |
| POST | `/admin-api/mes/pro/process-param/update-bindings` | å…¨é‡è¦†ç›–保存工序绑定的参数 |
| GET | `/admin-api/mes/pro/process-param/list-by-param` | åæŸ¥æŸåŸºç¡€å‚数被哪些工序绑定 |
权限:读 `mes:pro-process:query`;写 `mes:pro-process:update`(复用工序权限,无需新菜单)。
**GET /list-by-process è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| processId | Long | æ˜¯ | å·¥åºç¼–号 |
**响应:**
```json
{
  "code": 0,
  "data": [
    {
      "id": 9,
      "processId": 10,
      "processParamId": 7,
      "paramCode": "CS202607150004",
      "paramName": "测试参数_数值",
      "paramType": 1,
      "unitMeasureId": 1,
      "unitMeasureName": "吨",
      "required": true,
      "sort": 1,
      "remark": null
    }
  ]
}
```
**POST /update-bindings è¯·æ±‚参数(全量覆盖):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| processId | Long | æ˜¯ | å·¥åºç¼–号 |
| items | Array | å¦ | ç»‘定的参数列表(传空数组=清空绑定) |
| items[].processParamId | Long | æ˜¯ | åŸºç¡€å‚数编号 |
| items[].required | Boolean | å¦ | æ˜¯å¦å¿…填(可覆盖参数默认值) |
| items[].sort | Integer | å¦ | æŽ’序 |
| items[].remark | String | å¦ | å¤‡æ³¨ |
**GET /list-by-param è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| paramId | Long | æ˜¯ | åŸºç¡€å‚数编号 |
**响应:** `data` ä¸ºå·¥åºåˆ—表(`id`、`code`、`name`)。
### äºŒã€ä»»åŠ¡å‚æ•°è®°å½•ï¼ˆå·²æœ‰æŽ¥å£ï¼Œè¡Œä¸ºå¢žå¼ºï¼‰
任务创建/自动排产时,若任务无参数模板但工序绑定了参数,会生成一条 `templateName=工序绑定参数`、`templateId=null` çš„参数记录。
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/admin-api/mes/pro/work-order-param-record/page-by-work-order?taskId=` | æŒ‰ä»»åŠ¡æŸ¥è¯¢å‚æ•°è®°å½•ï¼ˆå«æ˜Žç»†å¿«ç…§ï¼‰ |
明细快照新增字段:`required`(是否必填)。
### ä¸‰ã€ç”Ÿäº§æŠ¥å·¥ï¼ˆå·²æœ‰æŽ¥å£ï¼Œå¢žå¼ºï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/pro/feedback/create` | åˆ›å»ºæŠ¥å·¥ï¼ˆæäº¤ `paramValues`) |
| PUT | `/admin-api/mes/pro/feedback/update` | ä¿®æ”¹æŠ¥å·¥ |
| GET | `/admin-api/mes/pro/feedback/get` | æŠ¥å·¥è¯¦æƒ…(响应新增 `paramValues`) |
| GET | `/admin-api/mes/pro/feedback/page` | æŠ¥å·¥åˆ†é¡µï¼ˆå“åº”新增 `paramValues`) |
**请求参数新增(已存在):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| paramValues | Array | å¦ | æœ¬æ¬¡æŠ¥å·¥å¡«å†™çš„参数值;任务无参数记录时不传 |
| paramValues[].detailId | Long | æ˜¯ï¼ˆä¼ å…¥æ—¶ï¼‰ | **任务参数记录明细编号**(不是模板明细编号) |
| paramValues[].value | String | æ¡ä»¶å¿…å¡« | å‚数值(必填参数不能为空) |
**响应字段新增(报工详情/列表):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| paramValues | Array | ä»»åŠ¡å‚æ•°å¿«ç…§åŠå·²å¡«å†™çš„å€¼ |
| paramValues[].detailId | Long | ä»»åŠ¡å‚æ•°è®°å½•æ˜Žç»†ç¼–å· |
| paramValues[].paramCode | String | å‚数编码 |
| paramValues[].paramName | String | å‚数名称 |
| paramValues[].paramType | Integer | å‚数类型 |
| paramValues[].unitMeasureName | String | å•位名称 |
| paramValues[].paramValue | String | å‚数值 |
| paramValues[].required | Boolean | æ˜¯å¦å¿…å¡« |
**响应示例(报工详情):**
```json
{
  "code": 0,
  "data": {
    "id": 48,
    "code": "FB_TEST_39",
    "processId": 10,
    "paramValues": [
      { "detailId": 55, "paramCode": "CS202607150004", "paramName": "测试参数_数值", "paramType": 1, "unitMeasureName": "吨", "paramValue": "200", "required": true },
      { "detailId": 56, "paramCode": "CS202607100003", "paramName": "测试参数11", "paramType": 2, "unitMeasureName": "千克", "paramValue": "200", "required": false }
    ]
  }
}
```
## å­—段展示规则
### å·¥åºè®¾ç½®é¡µã€Œå·¥è‰ºå‚数」Tab
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å‚数编码/名称 | Tab è¡¨æ ¼ | å±•示已绑定参数 |
| å‚数类型 | Tab è¡¨æ ¼ | å­—典:1=文本 2=数值 3=布尔 4=日期 |
| å•位 | Tab è¡¨æ ¼ | è®¡é‡å•位名称 |
| æ˜¯å¦å¿…å¡« | Tab è¡¨æ ¼ï¼ˆå¼€å…³ï¼‰ | å¯è¦†ç›–参数默认值 |
| æŽ’序 | Tab è¡¨æ ¼ | æ•°å­—排序 |
| å¤‡æ³¨ | Tab è¡¨æ ¼ | å¤‡æ³¨ |
| æ“ä½œ | Tab è¡¨æ ¼ | æ–°å¢žè¡Œï¼ˆé€‰æ‹©åŸºç¡€å‚数)、删除行 |
| ä¿å­˜ | Tab åº•部 | å…¨é‡è¦†ç›–保存(调用 update-bindings) |
### åŸºç¡€å‚数页「关联工序」
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å·¥åºç¼–码/名称 | å¼¹çª—/抽屉 | è¯¥å‚数被绑定的工序列表(只读) |
### ç”Ÿäº§æŠ¥å·¥è¡¨å•
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å‚数名称 | è¡¨å•行标签 | ä»Žä»»åŠ¡å‚æ•°è®°å½•å¿«ç…§è¯»å– |
| å•位 | è¾“入框后缀 | å¦‚ â„ƒ |
| å¿…å¡« | çº¢è‰²æ˜Ÿå· | required=true æ—¶æ ‡è®° |
| å‚数值 | è¾“入框 | æŒ‰å‚数类型渲染输入控件 |
| å¿…填校验 | æäº¤æ—¶ | å¿…填参数为空,后端返回「必填参数【xxx】未填写」 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å‚数来源优先级 | å·¥å•工序绑定模板 â†’ è·¯çº¿å·¥åºç»‘定模板 â†’ å·¥åºä¸»æ•°æ®ç»‘定参数 â†’ æ— å‚数记录 |
| æ— å‚数记录 | ä»»åŠ¡æ­£å¸¸ç”Ÿæˆã€æŠ¥å·¥ä¸è¦æ±‚å‚æ•°å€¼ |
| æœ‰å‚数记录 | æŠ¥å·¥æŒ‰ä»»åŠ¡å‚æ•°æ˜Žç»†å¡«å†™ï¼›å¿…å¡«å‚æ•°ï¼ˆrequired=true)必须填写 |
| å¿«ç…§æœºåˆ¶ | ä»»åŠ¡ç”Ÿæˆæ—¶å›ºåŒ–å‚æ•°å¿«ç…§ï¼ˆåç§°/类型/单位/必填),后续改工序绑定不影响历史任务 |
| å…¨é‡è¦†ç›– | å·¥åºç»‘定保存为全量覆盖;传空数组=清空绑定 |
| åˆ é™¤åŸºç¡€å‚æ•° | è‡ªåŠ¨æ¸…ç†å…¶å·¥åºç»‘å®šå…³ç³» |
| æŠ¥å·¥å›žæ˜¾ | æŠ¥å·¥è¯¦æƒ…/列表回显任务参数快照及已填值,历史值不因后续变更改变 |
## æ³¨æ„äº‹é¡¹
- `paramValues[].detailId` æ˜¯**任务参数记录明细编号**,不是模板明细编号;前端应先按任务参数记录接口取明细,再以其 id æäº¤ã€‚
- ä»»åŠ¡æ— æ¨¡æ¿ä½†å·¥åºç»‘å®šäº†å‚æ•°æ—¶ï¼Œå‚æ•°è®°å½•çš„ `templateId=null`、`templateName=工序绑定参数`,工单参数记录页按此展示。
- å‚数类型:1=文本 2=数值 3=布尔 4=日期,报工表单按类型渲染输入控件。
- æŠ¥å·¥æäº¤æ—¶å³ä½¿ä¸å¡«å‚数(paramValues ä¸ºç©ºï¼‰ï¼ŒåŽç«¯ä¹Ÿä¼šæ ¡éªŒå¿…填参数,任务绑定了参数时必须填写。
- å·¥åºç»‘定参数接口的读写权限复用工序设置页权限(`mes:pro-process:query/update`)。
- å‰ç«¯é¡µé¢è·¯å¾„与菜单:工序设置 `mes/process-design/process/index`、基础参数 `mes/process-design/parameter/index`。
docs/sql/mes_process_param_bind_full.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,145 @@
-- ============================================================================
-- éœ€æ±‚:基础参数(工艺参数)可与工序绑定,绑定后报工需填写参数字段
-- ä¸€é”®æ•´åˆè„šæœ¬ï¼šå¯ç›´æŽ¥æ‰‹åŠ¨æ‰§è¡Œï¼ˆå«å¹‚ç­‰ä¿æŠ¤ï¼Œé‡å¤æ‰§è¡Œä¸æŠ¥é”™ï¼‰
--
-- ç¬¬ä¸€éƒ¨åˆ†ï¼šåŠŸèƒ½è¿ç§»ï¼ˆå·¥åº-参数绑定)
-- ç¬¬äºŒéƒ¨åˆ†ï¼šæ´»åº“漂移修复(代码已引用但库缺失的列/表,需补齐才能建任务/报工)
-- ============================================================================
-- ============================================================================
-- ç¬¬ä¸€éƒ¨åˆ†ï¼šåŠŸèƒ½è¿ç§»
-- ============================================================================
-- ---------- 1.1 å·¥åº/任务/工单工序 å¢žåŠ  param_template_id(参数模板关联,功能链路前置) ----------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_task' AND column_name = 'param_template_id');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_task ADD COLUMN param_template_id bigint DEFAULT NULL COMMENT ''工艺参数模板编号'' AFTER process_id',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_process' AND column_name = 'param_template_id');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_route_process ADD COLUMN param_template_id bigint DEFAULT NULL COMMENT ''工艺参数模板编号'' AFTER process_id',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order_process' AND column_name = 'param_template_id');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_work_order_process ADD COLUMN param_template_id bigint DEFAULT NULL COMMENT ''工艺参数模板编号'' AFTER process_id',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------- 1.2 æ–°å»º å·¥åº-工艺参数绑定表 ----------
CREATE TABLE IF NOT EXISTS `mes_pro_process_param` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `process_id` bigint NOT NULL COMMENT '工序编号(关联 mes_pro_process)',
  `process_param_id` bigint NOT NULL COMMENT '工艺参数编号(关联 mes_pd_process_param)',
  `required` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否必填',
  `sort` int NULL DEFAULT NULL COMMENT '排序',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_process_id`(`process_id` ASC) USING BTREE,
  INDEX `idx_process_param_id`(`process_param_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES å·¥åº-工艺参数绑定' ROW_FORMAT = DYNAMIC;
-- ---------- 1.3 å‚数记录主表模板字段允许为空(工序绑定参数生成时无模板) ----------
ALTER TABLE mes_pro_work_order_param_record MODIFY COLUMN template_id bigint NULL DEFAULT NULL COMMENT '参数模板编号(工序绑定参数生成时为空)';
ALTER TABLE mes_pro_work_order_param_record MODIFY COLUMN template_name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模板名称(快照)';
-- ---------- 1.4 å‚数记录明细增加 required å¿«ç…§ï¼ˆæŠ¥å·¥å¿…填校验用) ----------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order_param_record_detail' AND column_name = 'required');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_work_order_param_record_detail ADD COLUMN required bit(1) NOT NULL DEFAULT b''0'' COMMENT ''是否必填(快照)'' AFTER param_type',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ============================================================================
-- ç¬¬äºŒéƒ¨åˆ†ï¼šæ´»åº“漂移修复(代码已引用但库缺失;若你的库已有这些列/表可忽略)
-- ============================================================================
-- ---------- 2.1 mes_pro_work_order ç¼ºå¤±åˆ—(报工审批 BPM ç›¸å…³ï¼‰ ----------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order' AND column_name = 'is_feedback_approve');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_work_order ADD COLUMN is_feedback_approve tinyint(1) DEFAULT NULL COMMENT ''是否开启报工审批 (1: å¼€å¯, 0/NULL: å…³é—­)'' AFTER deleted',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order' AND column_name = 'process_definition_key');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_work_order ADD COLUMN process_definition_key varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''BPM æµç¨‹å®šä¹‰ Key'' AFTER is_feedback_approve',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order' AND column_name = 'feedback_role');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_work_order ADD COLUMN feedback_role varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''报工角色 (多个角色以逗号分隔)'' AFTER process_definition_key',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------- 2.2 mes_pro_feedback ç¼ºå¤±åˆ—(报工 BPM ç›¸å…³ï¼‰ ----------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_feedback' AND column_name = 'process_instance_id');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_feedback ADD COLUMN process_instance_id varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''BPM æµç¨‹å®žä¾‹ç¼–号'' AFTER remark',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_pro_feedback' AND column_name = 'process_definition_key');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_pro_feedback ADD COLUMN process_definition_key varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''BPM æµç¨‹å®šä¹‰ Key'' AFTER process_instance_id',
              'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------- 2.3 ç¼ºå¤±è¡¨ï¼šä»»åŠ¡æŒ‡å®šæŠ¥å·¥äººå‘˜ã€ä»»åŠ¡åœå·¥è®°å½•ï¼ˆæ­¤å‰è¿ç§»æœªåº”ç”¨ï¼‰ ----------
CREATE TABLE IF NOT EXISTS `mes_pro_task_assignee` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `task_id` bigint(20) NOT NULL COMMENT '生产任务 ID',
  `user_id` bigint(20) NOT NULL COMMENT '系统用户 ID (指定报工人员)',
  `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`),
  KEY `idx_task_id` (`task_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生产任务指定报工人员表';
CREATE TABLE IF NOT EXISTS `mes_pro_task_pause_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `task_id` bigint(20) NOT NULL COMMENT '任务编号',
  `work_order_id` bigint(20) DEFAULT NULL COMMENT '工单编号',
  `workstation_id` bigint(20) DEFAULT NULL COMMENT '工作站编号',
  `reason` varchar(255) NOT NULL COMMENT '停工原因',
  `pause_time` datetime NOT NULL COMMENT '停工时间',
  `pause_user_id` bigint(20) NOT NULL COMMENT '停工操作人ID',
  `resume_time` datetime DEFAULT NULL COMMENT '复工时间',
  `resume_user_id` bigint(20) DEFAULT NULL COMMENT '复工操作人ID',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_task_id` (`task_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生产任务停工记录表';
-- ============================================================================
-- æ‰§è¡Œå®Œæˆã€‚可执行以下语句核对:
--   SHOW TABLES LIKE 'mes_pro_process_param';
--   SHOW COLUMNS FROM mes_pro_work_order_param_record_detail LIKE 'required';
-- ============================================================================
sql/mysql/hrm_employee_post_multi.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
-- =============================================
-- HRM å‘˜å·¥è¡¨ï¼šå²—位多选改造 SQL
-- 1. æ·»åŠ  post_ids å­—段(JSON æ ¼å¼ï¼Œä¸Ž role_ids å­˜å‚¨æ–¹å¼ä¸€è‡´ï¼‰
-- 2. è¿ç§»å·²æœ‰ post_id æ•°æ®åˆ° post_ids
-- 3. ä¿ç•™ post_id å­—段(兼容旧脚本/外部引用,已废弃不再写入)
-- =============================================
-- 1. æ·»åŠ å²—ä½ID列表字段(JSON格式存储)
ALTER TABLE `hrm_employee` ADD COLUMN `post_ids` varchar(500) DEFAULT NULL COMMENT '岗位ID列表(JSON格式)' AFTER `post_id`;
-- 2. è¿ç§»å·²æœ‰ post_id æ•°æ®åˆ° post_ids
UPDATE `hrm_employee` SET `post_ids` = CONCAT('[', `post_id`, ']') WHERE `post_id` IS NOT NULL AND (`post_ids` IS NULL OR `post_ids` = '');
-- æ‰§è¡Œå®Œæˆæç¤º
SELECT 'HRM å‘˜å·¥è¡¨å²—位多选改造完成!' AS result;
sql/mysql/hrm_employee_post_multiselect_deploy.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
-- =============================================
-- HRM å‘˜å·¥å°è´¦å²—位多选 - æ•°æ®åº“变更汇总脚本
-- é€‚用:test / prod ç­‰éœ€è¦æ‰‹å·¥æ‰§è¡Œçš„环境
-- åŒ…含:
--   1. hrm_employee è¡¨æ–°å¢žå²—位ID列表(post_ids)列,并迁移旧 post_id æ•°æ®
--   2. hrm_salary_structure è¡¨æ–°å¢žä¼‘息日/法定节假日加班工资倍率列(修复员工编辑页薪酬结构下拉报错)
-- æ³¨æ„ï¼šMySQL 8 çš„ ALTER TABLE ADD COLUMN ä¸æ”¯æŒ IF NOT EXISTS;
--       è‹¥æŸåˆ—已存在,对应 ALTER è¯­å¥ä¼šæŠ¥"Duplicate column",忽略该条即可继续。
-- =============================================
-- ---------- 1. hrm_employee:新增岗位ID列表(JSON),并迁移旧 post_id ----------
ALTER TABLE `hrm_employee` ADD COLUMN `post_ids` varchar(500) DEFAULT NULL COMMENT '岗位ID列表(JSON格式)' AFTER `post_id`;
UPDATE `hrm_employee` SET `post_ids` = CONCAT('[', `post_id`, ']') WHERE `post_id` IS NOT NULL AND (`post_ids` IS NULL OR `post_ids` = '');
-- ---------- 2. hrm_salary_structure:新增休息日/法定节假日加班倍率 ----------
ALTER TABLE `hrm_salary_structure` ADD COLUMN `rest_day_overtime_pay_ratio` decimal(4,2) DEFAULT 2.00 COMMENT '休息日加班工资倍率' AFTER `overtime_pay_ratio`;
ALTER TABLE `hrm_salary_structure` ADD COLUMN `legal_holiday_overtime_pay_ratio` decimal(4,2) DEFAULT 3.00 COMMENT '法定节假日加班工资倍率' AFTER `rest_day_overtime_pay_ratio`;
-- ---------- å®Œæˆæç¤º ----------
SELECT 'HRM å‘˜å·¥å°è´¦å²—位多选数据库变更完成!' AS result;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -45,6 +45,7 @@
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -91,13 +92,21 @@
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新销售订单的状态")
    @PostMapping("/submit")
    @Operation(summary = "提交销售订单审批(协同办公)")
    @PreAuthorize("@ss.hasPermission('erp:sale-order:update-status')")
    public CommonResult<Boolean> updateSaleOrderStatus(@RequestParam("id") Long id,
                                                      @RequestParam("status") Integer status) {
        saleOrderService.updateSaleOrderStatus(id, status);
    public CommonResult<Boolean> submitSaleOrder(@RequestParam("id") Long id,
                                                 @RequestParam("processDefinitionKey") String processDefinitionKey) {
        Long userId = getLoginUserId();
        saleOrderService.submitSaleOrder(id, processDefinitionKey, userId);
        return success(true);
    }
    @GetMapping("/approve-process-list")
    @Operation(summary = "获取销售订单审批流程列表")
    @PreAuthorize("@ss.hasPermission('erp:sale-order:query')")
    public CommonResult<List<Map<String, Object>>> getApproveProcessList() {
        return success(saleOrderService.getSaleOrderApproveProcessDefinitionList());
    }
    @PutMapping("/cancel")
@@ -161,6 +170,7 @@
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductUnitId));
        // 3. è½¬æ¢è®¢å•项
        List<ErpSaleOrderRespVO.Item> itemVOList = BeanUtils.toBean(saleOrderItemList, ErpSaleOrderRespVO.Item.class, item -> {
            fillItemPrice(item);
            item.setStockCount(mesWmStockApi.getStockQuantity(item.getProductId()));
            item.setBatchCode(batchCodeMap.get(item.getId()));
            MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem ->
@@ -260,6 +270,7 @@
        return BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class, saleOrder -> {
            saleOrder.setItems(BeanUtils.toBean(saleOrderItemMap.get(saleOrder.getId()), ErpSaleOrderRespVO.Item.class,
                    item -> {
                        fillItemPrice(item);
                        item.setBatchCode(batchCodeMap.get(item.getId()));
                        MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem ->
                                item.setProductName(mdmItem.getName()).setProductBarCode(mdmItem.getBarCode())
@@ -281,4 +292,18 @@
        });
    }
    /**
     * å¡«å……订单项的金额字段
     *
     * DO ä¸­ totalPrice ä¸ºäº§å“é‡‘额(不含税),RespVO éœ€è¦è¡¥å…¨ï¼š
     * - totalProductPrice:产品金额
     * - totalPrice:含税金额(产品金额 + ç¨Žé¢ï¼‰
     */
    private void fillItemPrice(ErpSaleOrderRespVO.Item item) {
        BigDecimal productAmount = item.getTotalPrice() != null ? item.getTotalPrice() : BigDecimal.ZERO;
        BigDecimal tax = item.getTaxPrice() != null ? item.getTaxPrice() : BigDecimal.ZERO;
        item.setTotalProductPrice(productAmount);
        item.setTotalPrice(productAmount.add(tax));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -77,6 +77,9 @@
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号", example = "b00eb97d-2d1c-4cf1-9b4b-2a5bcdc9a3a7")
    private String processInstanceId;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
@@ -148,6 +151,12 @@
        @Schema(description = "税额,单位:元", example = "100.00")
        private BigDecimal taxPrice;
        @Schema(description = "产品金额,单位:元", example = "100.00")
        private BigDecimal totalProductPrice;
        @Schema(description = "含税金额,单位:元", example = "113.00")
        private BigDecimal totalPrice;
        @Schema(description = "备注", example = "随便")
        private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -110,6 +110,15 @@
     */
    private String remark;
    // ========== ååŒåŠžå…¬(BPM)审批 ==========
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     *
     * æäº¤å®¡æ‰¹åˆ°ååŒåŠžå…¬åŽç”Ÿæˆçš„æµç¨‹å®žä¾‹ç¼–å·ï¼Œç”¨äºŽå…³è”å®¡æ‰¹è®°å½•
     */
    private String processInstanceId;
    // ========== é”€å”®å‡ºåº“ ==========
    /**
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpAuditStatus.java
@@ -17,7 +17,8 @@
@Getter
public enum ErpAuditStatus implements ArrayValuable<Integer> {
    PROCESS(10, "未审核"), // æœªå®¡æ ¸/草稿
    DRAFT(0, "未提交"), // æœªæäº¤/草稿,创建后初始状态,提交后进入审批
    PROCESS(10, "未审核"), // æœªå®¡æ ¸/草稿(历史状态,新数据不再使用)
    APPROVING(15, "审批中"), // å®¡æ‰¹ä¸­
    APPROVE(20, "已审核"), // å®¡æ ¸é€šè¿‡
    CANCEL(30, "已作废"), // å·²ä½œåºŸ/已撤销
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -95,6 +95,10 @@
    ErrorCode SALE_ORDER_CANCEL_FAIL = new ErrorCode(1_020_201_014, "作废失败,请刷新页面后重试");
    ErrorCode SALE_ORDER_ENABLE_FAIL_NOT_CANCEL = new ErrorCode(1_020_201_015, "启用失败,销售订单({})未作废,只有已作废的订单才能重新启用");
    ErrorCode SALE_ORDER_ENABLE_FAIL = new ErrorCode(1_020_201_016, "启用失败,请刷新页面后重试");
    ErrorCode SALE_ORDER_SUBMIT_FAIL_STATUS = new ErrorCode(1_020_201_017, "提交审批失败,销售订单({})只有未提交或已驳回状态才能提交");
    ErrorCode SALE_ORDER_SUBMIT_FAIL_ITEM_EMPTY = new ErrorCode(1_020_201_018, "提交审批失败,销售订单({})没有订单明细,无法提交");
    ErrorCode SALE_ORDER_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_020_201_019, "销售订单审核分类不存在,请先在BPM模块创建销售订单审核流程分类");
    ErrorCode SALE_ORDER_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_020_201_020, "销售订单审核分类下没有可用的流程模型,请先创建并发布流程");
    // ========== ERP é”€å”®å‡ºåº“(1-030-202-000) ==========
    ErrorCode SALE_OUT_NOT_EXISTS = new ErrorCode(1_020_202_000, "销售出库单不存在");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java
@@ -35,12 +35,30 @@
    void updateSaleOrder(@Valid ErpSaleOrderSaveReqVO updateReqVO);
    /**
     * æ›´æ–°é”€å”®è®¢å•的状态
     * æäº¤é”€å”®è®¢å•审批(协同办公 BPM)
     *
     * æœªæäº¤æˆ–已驳回的销售订单可提交,提交后进入协同办公审批流程
     *
     * @param id ç¼–号
     * @param status çŠ¶æ€
     * @param processDefinitionKey æµç¨‹å®šä¹‰çš„æ ‡è¯†
     * @param userId æäº¤äººç¼–号
     */
    void updateSaleOrderStatus(Long id, Integer status);
    void submitSaleOrder(Long id, String processDefinitionKey, Long userId);
    /**
     * æ›´æ–°é”€å”®è®¢å•的审批状态(协同办公审批结果回调)
     *
     * @param id ç¼–号
     * @param bpmResult BPM å®¡æ‰¹ç»“果(通过/驳回/取消)
     */
    void updateSaleOrderAuditStatus(Long id, Integer bpmResult);
    /**
     * èŽ·å¾—é”€å”®è®¢å•å®¡æ‰¹æµç¨‹å®šä¹‰åˆ—è¡¨
     *
     * @return æµç¨‹å®šä¹‰åˆ—表(id/key/name)
     */
    List<Map<String, Object>> getSaleOrderApproveProcessDefinitionList();
    /**
     * ä½œåºŸé”€å”®è®¢å•
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -5,6 +5,13 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.contract.CrmContractApi;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
@@ -25,6 +32,7 @@
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.flowable.engine.repository.ProcessDefinition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -33,8 +41,10 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -56,6 +66,15 @@
     * MPS çŠ¶æ€ï¼šå·²å–æ¶ˆï¼ˆå¯¹åº” MES æ¨¡å— MesProMpsStatusEnum.CANCELED çš„值 3)
     */
    private static final Integer MPS_STATUS_CANCELED = 3;
    /**
     * BPM é”€å”®è®¢å•审批分类编码
     */
    private static final String SALE_ORDER_APPROVE_CATEGORY_CODE = "sale_order_approve";
    /**
     * BPM é”€å”®è®¢å•审批流程定义 Key(提交后进入协同办公审批)
     */
    private static final String SALE_ORDER_APPROVE_PROCESS_DEFINITION_KEY = "sale_order_approve";
    @Resource
    private ErpSaleOrderMapper saleOrderMapper;
@@ -83,6 +102,13 @@
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createSaleOrder(ErpSaleOrderSaveReqVO createReqVO) {
@@ -104,9 +130,9 @@
            throw exception(SALE_ORDER_NO_EXISTS);
        }
        // 2.1 æ’入订单
        // 2.1 æ’入订单(创建后为未提交状态,需手动提交审批到协同办公)
        ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
                .setNo(no).setStatus(ErpAuditStatus.DRAFT.getStatus())
                .setDepositPrice(ObjectUtil.defaultIfNull(createReqVO.getDepositPrice(), BigDecimal.ZERO))
                .setOutCount(BigDecimal.ZERO).setOutStatus(ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus()));
        calculateTotalPrice(saleOrder, saleOrderItems);
@@ -125,10 +151,12 @@
    public void updateSaleOrder(ErpSaleOrderSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpSaleOrderDO saleOrder = validateSaleOrderExists(updateReqVO.getId());
        if (ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
        // 1.2 æ ¡éªŒçŠ¶æ€ï¼šåªæœ‰æœªæäº¤æˆ–å·²é©³å›žçš„é”€å”®è®¢å•æ‰èƒ½ä¿®æ”¹
        if (!ErpAuditStatus.DRAFT.getStatus().equals(saleOrder.getStatus())
                && !ErpAuditStatus.REJECT.getStatus().equals(saleOrder.getStatus())) {
            throw exception(SALE_ORDER_UPDATE_FAIL_APPROVE, saleOrder.getNo());
        }
        // 1.2 æ ¡éªŒå®¢æˆ·ï¼ˆä½¿ç”¨ CRM å®¢æˆ· API)
        // 1.3 æ ¡éªŒå®¢æˆ·ï¼ˆä½¿ç”¨ CRM å®¢æˆ· API)
        customerApi.validateCustomer(updateReqVO.getCustomerId());
        // 1.3 æ ¡éªŒç»“算账户
        if (updateReqVO.getAccountId() != null) {
@@ -167,31 +195,117 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateSaleOrderStatus(Long id, Integer status) {
        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
        // 1.1 æ ¡éªŒå­˜åœ¨
    public void submitSaleOrder(Long id, String processDefinitionKey, Long userId) {
        // 1. æ ¡éªŒå­˜åœ¨
        ErpSaleOrderDO saleOrder = validateSaleOrderExists(id);
        // 1.2 æ ¡éªŒçŠ¶æ€
        if (saleOrder.getStatus().equals(status)) {
            throw exception(approve ? SALE_ORDER_APPROVE_FAIL : SALE_ORDER_PROCESS_FAIL);
        // 2. æ ¡éªŒçŠ¶æ€ï¼šåªæœ‰æœªæäº¤æˆ–å·²é©³å›žçš„é”€å”®è®¢å•æ‰èƒ½æäº¤
        if (!ErpAuditStatus.DRAFT.getStatus().equals(saleOrder.getStatus())
                && !ErpAuditStatus.REJECT.getStatus().equals(saleOrder.getStatus())) {
            throw exception(SALE_ORDER_SUBMIT_FAIL_STATUS, saleOrder.getNo());
        }
        // 1.3 å­˜åœ¨é”€å”®å‡ºåº“单,无法反审核
        BigDecimal outCount = saleOrder.getOutCount() != null ? saleOrder.getOutCount() : BigDecimal.ZERO;
        if (!approve && outCount.compareTo(BigDecimal.ZERO) > 0) {
            throw exception(SALE_ORDER_PROCESS_FAIL_EXISTS_OUT);
        // 3. æ ¡éªŒè®¢å•项不能为空
        List<ErpSaleOrderItemDO> items = saleOrderItemMapper.selectListByOrderId(id);
        if (CollUtil.isEmpty(items)) {
            throw exception(SALE_ORDER_SUBMIT_FAIL_ITEM_EMPTY, saleOrder.getNo());
        }
        // 2. æ›´æ–°çŠ¶æ€
        int updateCount = saleOrderMapper.updateByIdAndStatus(id, saleOrder.getStatus(),
                new ErpSaleOrderDO().setStatus(status));
        if (updateCount == 0) {
            throw exception(approve ? SALE_ORDER_APPROVE_FAIL : SALE_ORDER_PROCESS_FAIL);
        // 4. åˆ›å»º BPM æµç¨‹å®žä¾‹ï¼ˆå®¡æ‰¹åœ¨ååŒåŠžå…¬å®Œæˆï¼‰
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 5. æ›´æ–°é”€å”®è®¢å•状态为审批中,记录流程实例编号
        saleOrderMapper.updateById(new ErpSaleOrderDO()
                .setId(id)
                .setStatus(ErpAuditStatus.APPROVING.getStatus())
                .setProcessInstanceId(processInstanceId));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateSaleOrderAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒå­˜åœ¨
        ErpSaleOrderDO saleOrder = validateSaleOrderExists(id);
        if (!ErpAuditStatus.APPROVING.getStatus().equals(saleOrder.getStatus())) {
            log.warn("[updateSaleOrderAuditStatus] é”€å”®è®¢å•({}) ä¸å¤„于审批中状态,跳过", id);
            return;
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        Integer newStatus = convertBpmResultToAuditStatus(bpmResult);
        if (newStatus != null) {
            saleOrderMapper.updateById(new ErpSaleOrderDO()
                    .setId(id).setStatus(newStatus));
        }
        // 3. å®¡æ ¸é€šè¿‡æ—¶ï¼Œä¸ºéœ€è¦ç”Ÿäº§çš„æ˜Žç»†åˆ›å»º MPS
        if (approve) {
        if (ErpAuditStatus.APPROVE.getStatus().equals(newStatus)) {
            createMpsForSaleOrder(id, saleOrder);
        }
    }
    @Override
    public List<Map<String, Object>> getSaleOrderApproveProcessDefinitionList() {
        // 1. æ ¡éªŒåˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
        validateSaleOrderApproveCategoryAndProcessDefinition();
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(SALE_ORDER_APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            return Collections.emptyList();
        }
        // 3. éåŽ†èŽ·å–æµç¨‹å®šä¹‰è¯¦æƒ…ï¼Œè¿‡æ»¤æ¿€æ´»çŠ¶æ€ï¼Œä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
            ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
            if (pd == null || pd.isSuspended()) {
                continue;
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        // 4. è¿”回流程定义列表
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            result.add(item);
        }
        return result;
    }
    /**
     * æ ¡éªŒé”€å”®è®¢å•审批分类是否存在
     */
    private void validateSaleOrderApproveCategoryAndProcessDefinition() {
        // 1. æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨
        List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
                Collections.singletonList(SALE_ORDER_APPROVE_CATEGORY_CODE));
        if (CollUtil.isEmpty(categories)) {
            throw exception(SALE_ORDER_BPM_CATEGORY_NOT_EXISTS);
        }
    }
    /**
     * è½¬æ¢ BPM å®¡æ‰¹ç»“果为审核状态
     */
    private Integer convertBpmResultToAuditStatus(Integer bpmResult) {
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.APPROVE.getStatus();
        } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.REJECT.getStatus();
        } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.CANCEL.getStatus();
        }
        return null;
    }
    /**
@@ -381,13 +495,14 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteSaleOrder(List<Long> ids) {
        // 1. æ ¡éªŒä¸å¤„于已审批
        // 1. æ ¡éªŒçŠ¶æ€ï¼šåªæœ‰æœªæäº¤æˆ–å·²é©³å›žçš„é”€å”®è®¢å•æ‰èƒ½åˆ é™¤
        List<ErpSaleOrderDO> saleOrders = saleOrderMapper.selectByIds(ids);
        if (CollUtil.isEmpty(saleOrders)) {
            return;
        }
        saleOrders.forEach(saleOrder -> {
            if (ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
            if (!ErpAuditStatus.DRAFT.getStatus().equals(saleOrder.getStatus())
                    && !ErpAuditStatus.REJECT.getStatus().equals(saleOrder.getStatus())) {
                throw exception(SALE_ORDER_DELETE_FAIL_APPROVE, saleOrder.getNo());
            }
        });
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ErpSaleOrderStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.erp.service.sale.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * é”€å”®è®¢å•审批状态监听器
 *
 * ç›‘听 BPM æµç¨‹å®žä¾‹çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œæ›´æ–°é”€å”®è®¢å•çš„å®¡æ‰¹çŠ¶æ€ï¼ˆååŒåŠžå…¬å®¡æ‰¹åŽå›žå†™ï¼‰
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class ErpSaleOrderStatusListener extends BpmProcessInstanceStatusEventListener {
    /**
     * é”€å”®è®¢å•审批流程定义 Key
     */
    private static final String PROCESS_DEFINITION_KEY = "sale_order_approve";
    @Resource
    private ErpSaleOrderService saleOrderService;
    @Override
    protected String getProcessDefinitionKey() {
        return PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long orderId;
        try {
            orderId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„销售订单编号", event.getBusinessKey());
            return;
        }
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,orderId={}, status={}", orderId, event.getStatus());
        try {
            saleOrderService.updateSaleOrderAuditStatus(orderId, event.getStatus());
            log.info("[onEvent] é”€å”®è®¢å•({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°å®Œæˆ", orderId);
        } catch (Exception e) {
            log.error("[onEvent] æ›´æ–°é”€å”®è®¢å•审批状态失败,orderId={}", orderId, e);
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java
@@ -115,10 +115,12 @@
        Map<Long, DeptRespDTO> deptMap = CollUtil.isEmpty(deptIds) ? Collections.emptyMap() : deptApi.getDeptMap(deptIds);
        // 2. æ‰¹é‡èŽ·å–å²—ä½ä¿¡æ¯
        Set<Long> postIds = employeeList.stream()
                .map(HrmEmployeeDO::getPostId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Set<Long> postIds = new HashSet<>();
        for (HrmEmployeeDO employee : employeeList) {
            if (StrUtil.isNotBlank(employee.getPostIds())) {
                postIds.addAll(JSONUtil.toList(employee.getPostIds(), Long.class));
            }
        }
        Map<Long, PostRespDTO> postMap = CollUtil.isEmpty(postIds) ? Collections.emptyMap() : postApi.getPostMap(postIds);
        // 3. æ”¶é›†æ‰€æœ‰è§’色ID
@@ -143,9 +145,16 @@
                respVO.setDeptName(deptMap.get(employee.getDeptId()).getName());
            }
            // å¡«å……岗位名称
            if (employee.getPostId() != null && postMap.containsKey(employee.getPostId())) {
                respVO.setPostName(postMap.get(employee.getPostId()).getName());
            // å¡«å……岗位信息
            if (StrUtil.isNotBlank(employee.getPostIds())) {
                List<Long> employeePostIds = JSONUtil.toList(employee.getPostIds(), Long.class);
                respVO.setPostIds(employeePostIds);
                if (CollUtil.isNotEmpty(employeePostIds)) {
                    respVO.setPostNames(employeePostIds.stream()
                            .filter(postMap::containsKey)
                            .map(id -> postMap.get(id).getName())
                            .collect(Collectors.joining(",")));
                }
            }
            // è§£æž roleIds å¹¶å¡«å……角色名称
@@ -195,7 +204,7 @@
        // è¾“出示例数据,方便使用者参考
        List<HrmEmployeeImportExcelVO> list = Arrays.asList(
                HrmEmployeeImportExcelVO.builder().name("张三").gender(1).phone("13800138000")
                        .deptName("研发部").postName("部门经理").roleNames("普通管理员")
                        .deptName("研发部").postName("部门经理,项目经理").roleNames("普通管理员")
                        .hireDate(LocalDate.of(2020, 3, 1)).employeeStatus(1)
                        .idCard("320102199001150011").birthday(LocalDate.of(1990, 1, 15))
                        .nation("汉族").marriageStatus("已婚").age(36).education("本科").politicalStatus("中共党员")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java
@@ -32,12 +32,12 @@
    @ExcelProperty("部门名称")
    private String deptName;
    @Schema(description = "岗位ID")
    private Long postId;
    @Schema(description = "岗位ID列表")
    private List<Long> postIds;
    @Schema(description = "岗位名称")
    @Schema(description = "岗位名称列表")
    @ExcelProperty("岗位名称")
    private String postName;
    private String postNames;
    @Schema(description = "员工姓名")
    @ExcelProperty("员工姓名")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java
@@ -23,9 +23,9 @@
    @NotNull(message = "部门不能为空")
    private Long deptId;
    @Schema(description = "岗位ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "岗位不能为空")
    private Long postId;
    @Schema(description = "岗位ID列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
    @NotEmpty(message = "岗位不能为空")
    private List<Long> postIds;
    @Schema(description = "员工姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
    @NotEmpty(message = "员工姓名不能为空")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java
@@ -45,11 +45,11 @@
    private Long deptId;
    /**
     * å²—位ID
     * å²—位ID列表
     *
     * å…³è” PostDO#getId()
     * å…³è” PostDO#getId(),JSON æ ¼å¼å­˜å‚¨
     */
    private Long postId;
    private String postIds;
    /**
     * å‘˜å·¥ç¼–号
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java
@@ -2,6 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
@@ -255,9 +257,17 @@
        resp.setEmployeeName(employee.getName()); resp.setEmployeeNo(employee.getEmployeeNo());
        resp.setDeptId(employee.getDeptId());
        DeptRespDTO dept = employee.getDeptId() == null ? null : deptApi.getDept(employee.getDeptId());
        PostRespDTO post = employee.getPostId() == null ? null
                : postApi.getPostMap(Collections.singleton(employee.getPostId())).get(employee.getPostId());
        resp.setDeptName(dept == null ? null : dept.getName());
        resp.setPostName(post == null ? null : post.getName());
        // å²—位多选:解析 postIds JSON,拼接岗位名称
        if (StrUtil.isNotBlank(employee.getPostIds())) {
            List<Long> postIds = JSONUtil.toList(employee.getPostIds(), Long.class);
            if (CollUtil.isNotEmpty(postIds)) {
                Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
                resp.setPostName(postIds.stream()
                        .filter(postMap::containsKey)
                        .map(id -> postMap.get(id).getName())
                        .collect(Collectors.joining(",")));
            }
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -33,6 +33,7 @@
import cn.iocoder.yudao.module.system.api.permission.dto.RoleRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserUpdateReqDTO;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -91,7 +92,7 @@
        // 1. æ ¡éªŒéƒ¨é—¨
        deptApi.validateDeptList(Collections.singleton(createReqVO.getDeptId()));
        // 2. æ ¡éªŒå²—位
        postApi.validPostList(Collections.singleton(createReqVO.getPostId()));
        postApi.validPostList(createReqVO.getPostIds());
        // 3. æ ¡éªŒæ‰‹æœºå·æ˜¯å¦å·²å­˜åœ¨
        if (employeeMapper.selectByPhone(createReqVO.getPhone()) != null) {
            throw exception(EMPLOYEE_PHONE_EXISTS);
@@ -101,7 +102,7 @@
        userCreateReqDTO.setUsername(createReqVO.getPhone());
        userCreateReqDTO.setNickname(createReqVO.getName());
        userCreateReqDTO.setDeptId(createReqVO.getDeptId());
        userCreateReqDTO.setPostIds(Collections.singleton(createReqVO.getPostId()));
        userCreateReqDTO.setPostIds(new LinkedHashSet<>(createReqVO.getPostIds()));
        userCreateReqDTO.setMobile(createReqVO.getPhone());
        userCreateReqDTO.setPassword(createReqVO.getPhone());
        userCreateReqDTO.setSex(createReqVO.getGender());
@@ -121,6 +122,10 @@
        // roleIds å­˜å‚¨ä¸º JSON
        if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) {
            employee.setRoleIds(JSONUtil.toJsonStr(createReqVO.getRoleIds()));
        }
        // postIds å­˜å‚¨ä¸º JSON
        if (CollUtil.isNotEmpty(createReqVO.getPostIds())) {
            employee.setPostIds(JSONUtil.toJsonStr(createReqVO.getPostIds()));
        }
        employeeMapper.insert(employee);
        Long employeeId = employee.getId();
@@ -167,7 +172,7 @@
        // æ ¡éªŒéƒ¨é—¨
        deptApi.validateDeptList(Collections.singleton(updateReqVO.getDeptId()));
        // æ ¡éªŒå²—位
        postApi.validPostList(Collections.singleton(updateReqVO.getPostId()));
        postApi.validPostList(updateReqVO.getPostIds());
        // æ ¡éªŒæ‰‹æœºå·æ˜¯å¦å·²å­˜åœ¨ï¼ˆå¦‚果手机号有变更)
        if (!Objects.equals(employee.getPhone(), updateReqVO.getPhone())
                && employeeMapper.selectByPhone(updateReqVO.getPhone()) != null) {
@@ -179,7 +184,24 @@
        if (CollUtil.isNotEmpty(updateReqVO.getRoleIds())) {
            updateObj.setRoleIds(JSONUtil.toJsonStr(updateReqVO.getRoleIds()));
        }
        // postIds å­˜å‚¨ä¸º JSON
        if (CollUtil.isNotEmpty(updateReqVO.getPostIds())) {
            updateObj.setPostIds(JSONUtil.toJsonStr(updateReqVO.getPostIds()));
        }
        employeeMapper.updateById(updateObj);
        // åŒæ­¥æ›´æ–°å…³è”系统用户(岗位/部门/昵称/角色/邮箱/备注),保持与用户管理一致
        if (employee.getUserId() != null) {
            AdminUserUpdateReqDTO userUpdateReqDTO = new AdminUserUpdateReqDTO();
            userUpdateReqDTO.setId(employee.getUserId());
            userUpdateReqDTO.setNickname(updateReqVO.getName());
            userUpdateReqDTO.setDeptId(updateReqVO.getDeptId());
            userUpdateReqDTO.setPostIds(new LinkedHashSet<>(updateReqVO.getPostIds()));
            userUpdateReqDTO.setRoleIds(CollUtil.isEmpty(updateReqVO.getRoleIds()) ? null
                    : new LinkedHashSet<>(updateReqVO.getRoleIds()));
            userUpdateReqDTO.setEmail(updateReqVO.getEmail());
            userUpdateReqDTO.setRemark(updateReqVO.getRemark());
            adminUserApi.updateUser(userUpdateReqDTO);
        }
        Long employeeId = updateReqVO.getId();
        // åŒæ­¥æ•™è‚²ç»åކ
@@ -270,12 +292,16 @@
                respVO.setDeptName(dept.getName());
            }
        }
        // å¡«å……岗位名称
        if (employee.getPostId() != null) {
            Map<Long, PostRespDTO> postMap = postApi.getPostMap(Collections.singleton(employee.getPostId()));
            PostRespDTO post = postMap.get(employee.getPostId());
            if (post != null) {
                respVO.setPostName(post.getName());
        // å¡«å……岗位信息
        if (StrUtil.isNotBlank(employee.getPostIds())) {
            List<Long> postIds = JSONUtil.toList(employee.getPostIds(), Long.class);
            respVO.setPostIds(postIds);
            if (CollUtil.isNotEmpty(postIds)) {
                Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
                respVO.setPostNames(postIds.stream()
                        .filter(postMap::containsKey)
                        .map(postId -> postMap.get(postId).getName())
                        .collect(Collectors.joining(",")));
            }
        }
        // å¡«å……角色名称
@@ -370,10 +396,10 @@
            try {
                // 3.1 åç§°è½¬ ID
                Long deptId = parseDeptId(importEmployee.getDeptName(), deptMap);
                Long postId = parsePostId(importEmployee.getPostName(), postMap);
                Set<Long> postIds = parsePostIds(importEmployee.getPostName(), postMap);
                Set<Long> roleIds = parseRoleIds(importEmployee.getRoleNames(), roleMap);
                // 3.2 æž„建保存请求
                HrmEmployeeSaveReqVO saveReqVO = buildImportSaveReqVO(importEmployee, deptId, postId, roleIds);
                HrmEmployeeSaveReqVO saveReqVO = buildImportSaveReqVO(importEmployee, deptId, postIds, roleIds);
                // 3.3 åˆ¤æ–­å­˜åœ¨æ€§ï¼šæŒ‰æ‰‹æœºå·
                HrmEmployeeDO existEmployee = employeeMapper.selectByPhone(importEmployee.getPhone());
                if (existEmployee == null) {
@@ -398,7 +424,7 @@
    }
    private HrmEmployeeSaveReqVO buildImportSaveReqVO(HrmEmployeeImportExcelVO importEmployee,
                                                      Long deptId, Long postId, Set<Long> roleIds) {
                                                      Long deptId, Set<Long> postIds, Set<Long> roleIds) {
        if (StrUtil.isBlank(importEmployee.getName())) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "员工姓名");
        }
@@ -413,7 +439,7 @@
        reqVO.setGender(importEmployee.getGender() != null ? importEmployee.getGender() : 1); // é»˜è®¤ç”·
        reqVO.setPhone(importEmployee.getPhone());
        reqVO.setDeptId(deptId);
        reqVO.setPostId(postId);
        reqVO.setPostIds(CollUtil.isEmpty(postIds) ? null : new ArrayList<>(postIds));
        reqVO.setRoleIds(CollUtil.isEmpty(roleIds) ? null : new ArrayList<>(roleIds));
        reqVO.setHireDate(importEmployee.getHireDate());
        reqVO.setEmployeeStatus(importEmployee.getEmployeeStatus() != null
@@ -441,15 +467,26 @@
        return dept.getId();
    }
    private Long parsePostId(String postName, Map<String, PostRespDTO> postMap) {
        if (StrUtil.isBlank(postName)) {
    private Set<Long> parsePostIds(String postNames, Map<String, PostRespDTO> postMap) {
        if (StrUtil.isBlank(postNames)) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "岗位名称");
        }
        PostRespDTO post = postMap.get(postName.trim());
        if (post == null) {
            throw exception(EMPLOYEE_POST_NAME_NOT_EXISTS, postName.trim());
        Set<Long> postIds = new LinkedHashSet<>();
        for (String postName : postNames.split("[,,]")) {
            String trimmed = postName.trim();
            if (StrUtil.isBlank(trimmed)) {
                continue;
            }
            PostRespDTO post = postMap.get(trimmed);
            if (post == null) {
                throw exception(EMPLOYEE_POST_NAME_NOT_EXISTS, trimmed);
            }
            postIds.add(post.getId());
        }
        return post.getId();
        if (CollUtil.isEmpty(postIds)) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "岗位名称");
        }
        return postIds;
    }
    private Set<Long> parseRoleIds(String roleNames, Map<String, RoleRespDTO> roleMap) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/workstation/MesMdWorkstationWorkerController.java
@@ -10,6 +10,8 @@
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationWorkerService;
import cn.iocoder.yudao.module.system.api.dept.PostApi;
import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -37,6 +39,9 @@
    @Resource
    private PostApi postApi;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建人力资源")
@@ -72,6 +77,24 @@
        return success(buildWorkstationWorkerRespVOList(list));
    }
    @GetMapping("/list-user-by-workstation")
    @Operation(summary = "获得工作站对应岗位的员工列表")
    @Parameter(name = "workstationId", description = "工作站编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:md-workstation:query')")
    public CommonResult<List<AdminUserRespDTO>> getUserListByWorkstation(
            @RequestParam("workstationId") Long workstationId) {
        List<MesMdWorkstationWorkerDO> list = workstationWorkerService.getWorkstationWorkerListByWorkstationId(workstationId);
        if (CollUtil.isEmpty(list)) {
            return success(Collections.emptyList());
        }
        List<Long> postIds = cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList(list, MesMdWorkstationWorkerDO::getPostId);
        if (CollUtil.isEmpty(postIds)) {
            return success(Collections.emptyList());
        }
        List<AdminUserRespDTO> userList = adminUserApi.getUserListByPostIds(postIds);
        return success(userList);
    }
    // ==================== æ‹¼æŽ¥ VO ====================
    private List<MesMdWorkstationWorkerRespVO> buildWorkstationWorkerRespVOList(List<MesMdWorkstationWorkerDO> list) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java
@@ -59,10 +59,15 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceMapper;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderParamRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderParamRecordDetailDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderParamRecordMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderParamRecordDetailMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -109,6 +114,10 @@
    private MesWmItemConsumeLineMapper itemConsumeLineMapper;
    @Resource
    private MesQcIpqcMapper ipqcMapper;
    @Resource
    private MesProWorkOrderParamRecordMapper paramRecordMapper;
    @Resource
    private MesProWorkOrderParamRecordDetailMapper paramRecordDetailMapper;
    @PostMapping("/create")
    @Operation(summary = "创建生产报工")
@@ -287,6 +296,28 @@
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSetByFlatMap(list, feedback ->
                        Stream.of(feedback.getFeedbackUserId(), feedback.getApproveUserId())));
        // 1.9 ä»»åŠ¡å‚æ•°è®°å½•ï¼šæŠ¥å·¥å›žæ˜¾å‚æ•°å€¼
        Set<Long> taskIds = convertSet(list, MesProFeedbackDO::getTaskId);
        Map<Long, List<MesProWorkOrderParamRecordDetailDO>> taskParamDetailMap = new HashMap<>();
        if (CollUtil.isNotEmpty(taskIds)) {
            List<MesProWorkOrderParamRecordDO> paramRecords = paramRecordMapper.selectListByTaskIds(taskIds);
            if (CollUtil.isNotEmpty(paramRecords)) {
                Map<Long, List<MesProWorkOrderParamRecordDetailDO>> recordDetailMap = paramRecordDetailMapper
                        .selectListByRecordIds(convertSet(paramRecords, MesProWorkOrderParamRecordDO::getId)).stream()
                        .collect(Collectors.groupingBy(MesProWorkOrderParamRecordDetailDO::getRecordId));
                for (MesProWorkOrderParamRecordDO record : paramRecords) {
                    taskParamDetailMap.putIfAbsent(record.getTaskId(),
                            recordDetailMap.getOrDefault(record.getId(), Collections.emptyList()));
                }
            }
        }
        Set<Long> paramUnitIds = new HashSet<>();
        taskParamDetailMap.values().forEach(details -> details.forEach(d -> {
            if (d.getUnitMeasureId() != null) {
                paramUnitIds.add(d.getUnitMeasureId());
            }
        }));
        Map<Long, MesMdUnitMeasureDO> paramUnitMeasureMap = unitMeasureService.getUnitMeasureMap(paramUnitIds);
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesProFeedbackRespVO.class, vo -> {
@@ -320,6 +351,22 @@
                    vo.setFeedbackUserNickname(user.getNickname()));
            findAndThen(userMap, vo.getApproveUserId(), user ->
                    vo.setApproveUserNickname(user.getNickname()));
            // å‚数值回显(任务参数记录快照,含必填标记)
            List<MesProWorkOrderParamRecordDetailDO> taskDetails = taskParamDetailMap.get(vo.getTaskId());
            if (CollUtil.isNotEmpty(taskDetails)) {
                vo.setParamValues(taskDetails.stream().map(detail -> {
                    MesProFeedbackRespVO.ParamValueResp pv = new MesProFeedbackRespVO.ParamValueResp();
                    pv.setDetailId(detail.getId());
                    pv.setParamCode(detail.getParamCode());
                    pv.setParamName(detail.getParamName());
                    pv.setParamType(detail.getParamType());
                    pv.setUnitMeasureName(paramUnitMeasureMap.containsKey(detail.getUnitMeasureId())
                            ? paramUnitMeasureMap.get(detail.getUnitMeasureId()).getName() : null);
                    pv.setParamValue(detail.getParamValue());
                    pv.setRequired(detail.getRequired());
                    return pv;
                }).toList());
            }
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackRespVO.java
@@ -10,6 +10,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES ç”Ÿäº§æŠ¥å·¥ Response VO")
@Data
@@ -177,4 +178,34 @@
    @ExcelProperty("BPM æµç¨‹å®šä¹‰ Key")
    private String processDefinitionKey;
    @Schema(description = "本次报工对应的任务参数值(按任务参数记录快照展示)")
    private List<ParamValueResp> paramValues;
    @Data
    @Schema(description = "报工参数值")
    public static class ParamValueResp {
        @Schema(description = "任务参数记录明细编号", example = "1")
        private Long detailId;
        @Schema(description = "参数编码", example = "PP2024001")
        private String paramCode;
        @Schema(description = "参数名称", example = "温度")
        private String paramName;
        @Schema(description = "参数类型", example = "2")
        private Integer paramType;
        @Schema(description = "计量单位名称", example = "℃")
        private String unitMeasureName;
        @Schema(description = "参数值", example = "200")
        private String paramValue;
        @Schema(description = "是否必填", example = "true")
        private Boolean required;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/process/MesProProcessParamController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.process;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.process.vo.MesProProcessRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.process.vo.param.MesProProcessParamRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.process.vo.param.MesProProcessParamSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.processparam.MesPdProcessParamDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.pd.processparam.MesPdProcessParamService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessParamService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@Tag(name = "管理后台 - MES å·¥åºå·¥è‰ºå‚数绑定")
@RestController
@RequestMapping("/mes/pro/process-param")
@Validated
public class MesProProcessParamController {
    @Resource
    private MesProProcessParamService processParamService;
    @Resource
    private MesPdProcessParamService pdProcessParamService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesProProcessService processService;
    @GetMapping("/list-by-process")
    @Operation(summary = "获得工序绑定的工艺参数列表(按工序编号)")
    @Parameter(name = "processId", description = "工序编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:pro-process:query')")
    public CommonResult<List<MesProProcessParamRespVO>> getProcessParamListByProcessId(@RequestParam("processId") Long processId) {
        List<MesProProcessParamDO> list = processParamService.getProcessParamListByProcessId(processId);
        return success(buildRespVOList(list));
    }
    @PostMapping("/update-bindings")
    @Operation(summary = "全量覆盖保存工序绑定的工艺参数")
    @PreAuthorize("@ss.hasPermission('mes:pro-process:update')")
    public CommonResult<Boolean> updateProcessParams(@Valid @RequestBody MesProProcessParamSaveReqVO reqVO) {
        processParamService.updateProcessParams(reqVO.getProcessId(), reqVO.getItems());
        return success(true);
    }
    @GetMapping("/list-by-param")
    @Operation(summary = "获得工艺参数被绑定的工序列表(按工艺参数编号)")
    @Parameter(name = "paramId", description = "工艺参数编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:pro-process:query')")
    public CommonResult<List<MesProProcessRespVO>> getProcessListByParamId(@RequestParam("paramId") Long paramId) {
        List<MesProProcessParamDO> list = processParamService.getProcessParamListByParamId(paramId);
        if (CollUtil.isEmpty(list)) {
            return success(Collections.emptyList());
        }
        Map<Long, MesProProcessDO> processMap = processService.getProcessMap(
                convertSet(list, MesProProcessParamDO::getProcessId));
        List<MesProProcessRespVO> result = new ArrayList<>(list.size());
        for (MesProProcessParamDO binding : list) {
            MesProProcessDO process = processMap.get(binding.getProcessId());
            if (process != null) {
                result.add(new MesProProcessRespVO()
                        .setId(process.getId()).setCode(process.getCode()).setName(process.getName()));
            }
        }
        return success(result);
    }
    // ==================== æ‹¼æŽ¥ VO ====================
    private List<MesProProcessParamRespVO> buildRespVOList(List<MesProProcessParamDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. æ‰¹é‡èŽ·å–å·¥è‰ºå‚æ•°ä¸Žè®¡é‡å•ä½
        Map<Long, MesPdProcessParamDO> paramMap = pdProcessParamService.getProcessParamMap(
                convertSet(list, MesProProcessParamDO::getProcessParamId));
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(paramMap.values(), MesPdProcessParamDO::getUnitMeasureId));
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesProProcessParamRespVO.class, vo ->
                findAndThen(paramMap, vo.getProcessParamId(), param -> {
                    vo.setParamCode(param.getParamCode()).setParamName(param.getParamName())
                            .setParamType(param.getParamType()).setUnitMeasureId(param.getUnitMeasureId());
                    findAndThen(unitMeasureMap, param.getUnitMeasureId(), unit ->
                            vo.setUnitMeasureName(unit.getName()));
                })
        );
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/process/vo/param/MesProProcessParamRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.process.vo.param;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - MES å·¥åºå·¥è‰ºå‚数绑定 Response VO")
@Data
@ExcelIgnoreUnannotated
public class MesProProcessParamRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @ExcelProperty("编号")
    private Long id;
    @Schema(description = "工序编号", example = "1")
    private Long processId;
    @Schema(description = "工艺参数编号", example = "1")
    private Long processParamId;
    @Schema(description = "参数编码", example = "PP2024001")
    @ExcelProperty("参数编码")
    private String paramCode;
    @Schema(description = "参数名称", example = "温度")
    @ExcelProperty("参数名称")
    private String paramName;
    @Schema(description = "参数类型", example = "2")
    @ExcelProperty(value = "参数类型", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_PD_PROCESS_PARAM_TYPE)
    private Integer paramType;
    @Schema(description = "计量单位编号", example = "1")
    private Long unitMeasureId;
    @Schema(description = "计量单位名称", example = "℃")
    @ExcelProperty("单位")
    private String unitMeasureName;
    @Schema(description = "是否必填", example = "true")
    @ExcelProperty("是否必填")
    private Boolean required;
    @Schema(description = "排序", example = "1")
    private Integer sort;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/process/vo/param/MesProProcessParamSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.process.vo.param;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - MES å·¥åºå·¥è‰ºå‚数绑定新增/修改 Request VO")
@Data
public class MesProProcessParamSaveReqVO {
    @Schema(description = "工序编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "工序编号不能为空")
    private Long processId;
    @Schema(description = "绑定的工艺参数列表(全量覆盖保存)")
    private List<Item> items;
    @Data
    public static class Item {
        @Schema(description = "工艺参数编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
        @NotNull(message = "工艺参数不能为空")
        private Long processParamId;
        @Schema(description = "是否必填(可覆盖参数默认值)", example = "true")
        private Boolean required;
        @Schema(description = "排序", example = "1")
        private Integer sort;
        @Schema(description = "备注", example = "备注")
        private String remark;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/process/MesProProcessParamDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.pro.process;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
 * MES å·¥åº-工艺参数绑定 DO
 *
 * <p>工序主数据与基础参数(工艺参数)的关联关系,用于报工时按工序展示参数字段。
 *
 * @author è¶…级管理员
 */
@TableName("mes_pro_process_param")
@KeySequence("mes_pro_process_param_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesProProcessParamDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å·¥åºç¼–号
     *
     * å…³è” {@link MesProProcessDO#getId()}
     */
    private Long processId;
    /**
     * å·¥è‰ºå‚数编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.pd.processparam.MesPdProcessParamDO#getId()}
     */
    private Long processParamId;
    /**
     * æ˜¯å¦å¿…填(可覆盖参数默认值)
     */
    private Boolean required;
    /**
     * æŽ’序
     */
    private Integer sort;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderParamRecordDetailDO.java
@@ -55,6 +55,10 @@
     */
    private Integer paramType;
    /**
     * æ˜¯å¦å¿…填(快照)
     */
    private Boolean required;
    /**
     * è®¡é‡å•位编号(快照)
     */
    private Long unitMeasureId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/process/MesProProcessParamMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.process;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MES å·¥åº-工艺参数绑定 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesProProcessParamMapper extends BaseMapperX<MesProProcessParamDO> {
    default List<MesProProcessParamDO> selectListByProcessId(Long processId) {
        return selectList(new LambdaQueryWrapperX<MesProProcessParamDO>()
                .eq(MesProProcessParamDO::getProcessId, processId)
                .orderByAsc(MesProProcessParamDO::getSort)
                .orderByAsc(MesProProcessParamDO::getId));
    }
    default List<MesProProcessParamDO> selectListByProcessIds(Collection<Long> processIds) {
        return selectList(new LambdaQueryWrapperX<MesProProcessParamDO>()
                .inIfPresent(MesProProcessParamDO::getProcessId, processIds)
                .orderByAsc(MesProProcessParamDO::getSort)
                .orderByAsc(MesProProcessParamDO::getId));
    }
    default List<MesProProcessParamDO> selectListByProcessParamId(Long processParamId) {
        return selectList(new LambdaQueryWrapperX<MesProProcessParamDO>()
                .eq(MesProProcessParamDO::getProcessParamId, processParamId)
                .orderByAsc(MesProProcessParamDO::getId));
    }
    default void deleteByProcessId(Long processId) {
        delete(new LambdaQueryWrapperX<MesProProcessParamDO>()
                .eq(MesProProcessParamDO::getProcessId, processId));
    }
    default void deleteByProcessParamId(Long processParamId) {
        delete(new LambdaQueryWrapperX<MesProProcessParamDO>()
                .eq(MesProProcessParamDO::getProcessParamId, processParamId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderParamRecordMapper.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderParamRecordDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
@@ -40,4 +41,10 @@
                .orderByDesc(MesProWorkOrderParamRecordDO::getId));
    }
    default List<MesProWorkOrderParamRecordDO> selectListByTaskIds(Collection<Long> taskIds) {
        return selectList(new LambdaQueryWrapperX<MesProWorkOrderParamRecordDO>()
                .in(MesProWorkOrderParamRecordDO::getTaskId, taskIds)
                .orderByAsc(MesProWorkOrderParamRecordDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/processparam/MesPdProcessParamServiceImpl.java
@@ -35,6 +35,10 @@
    @Lazy
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    @Lazy
    private cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessParamService processParamService;
    @Override
    public Long createProcessParam(MesPdProcessParamSaveReqVO createReqVO) {
        // ç”Ÿæˆå‚数编码
@@ -60,6 +64,8 @@
    public void deleteProcessParam(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateProcessParamExists(id);
        // åˆ é™¤å·¥åºç»‘定关系,避免残留脏数据
        processParamService.deleteByParamId(id);
        // åˆ é™¤
        processParamMapper.deleteById(id);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -104,6 +104,7 @@
    private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
        // 1. æ ¡éªŒ
        MesProTaskDO task = validateFeedbackData(createReqVO);
@@ -113,11 +114,13 @@
                .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                .setItemId(task.getItemId());
        feedbackMapper.insert(feedback);
        // 3. ä¿å­˜å‚数值;必填参数校验失败时整体回滚,避免残留草稿
        saveParamValues(task.getId(), createReqVO.getParamValues());
        return feedback.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateFeedbackStatusPrepare(updateReqVO.getId());
@@ -128,11 +131,14 @@
        MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class)
                .setItemId(task.getItemId());
        feedbackMapper.updateById(updateObj);
        // 3. ä¿å­˜å‚数值;必填参数校验失败时整体回滚
        saveParamValues(task.getId(), updateReqVO.getParamValues());
    }
    private void saveParamValues(Long taskId, List<MesProFeedbackSaveReqVO.ParamValue> paramValues) {
        // å³ä½¿å‚数为空也调用更新,以便触发必填参数校验(任务绑定了参数时必须填写)
        if (CollUtil.isEmpty(paramValues)) {
            paramRecordService.updateTaskParamValues(taskId, null, null);
            return;
        }
        paramRecordService.updateTaskParamValues(taskId,
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/process/MesProProcessParamService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.mes.service.pro.process;
import cn.iocoder.yudao.module.mes.controller.admin.pro.process.vo.param.MesProProcessParamSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import java.util.Collection;
import java.util.List;
/**
 * MES å·¥åº-工艺参数绑定 Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesProProcessParamService {
    /**
     * èŽ·å¾—æŸå·¥åºç»‘å®šçš„å·¥è‰ºå‚æ•°åˆ—è¡¨ï¼ˆæŒ‰æŽ’åºï¼‰
     *
     * @param processId å·¥åºç¼–号
     * @return ç»‘定列表
     */
    List<MesProProcessParamDO> getProcessParamListByProcessId(Long processId);
    /**
     * æ‰¹é‡èŽ·å¾—å¤šä¸ªå·¥åºç»‘å®šçš„å·¥è‰ºå‚æ•°
     *
     * @param processIds å·¥åºç¼–号集合
     * @return ç»‘定列表
     */
    List<MesProProcessParamDO> getProcessParamListByProcessIds(Collection<Long> processIds);
    /**
     * èŽ·å¾—æŸå·¥è‰ºå‚æ•°è¢«å“ªäº›å·¥åºç»‘å®šï¼ˆåŸºç¡€å‚æ•°é¡µåæŸ¥å·¥åºï¼‰
     *
     * @param processParamId å·¥è‰ºå‚数编号
     * @return ç»‘定列表
     */
    List<MesProProcessParamDO> getProcessParamListByParamId(Long processParamId);
    /**
     * å…¨é‡è¦†ç›–保存某工序绑定的工艺参数
     *
     * @param processId å·¥åºç¼–号
     * @param items     ç»‘定项列表
     */
    void updateProcessParams(Long processId, List<MesProProcessParamSaveReqVO.Item> items);
    /**
     * åˆ é™¤æŸå·¥è‰ºå‚数的所有工序绑定(删除工艺参数时清理)
     *
     * @param processParamId å·¥è‰ºå‚数编号
     */
    void deleteByParamId(Long processParamId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/process/MesProProcessParamServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.mes.service.pro.process;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.mes.controller.admin.pro.process.vo.param.MesProProcessParamSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.process.MesProProcessParamMapper;
import cn.iocoder.yudao.module.mes.service.pd.processparam.MesPdProcessParamService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
 * MES å·¥åº-工艺参数绑定 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesProProcessParamServiceImpl implements MesProProcessParamService {
    @Resource
    private MesProProcessParamMapper processParamMapper;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesPdProcessParamService pdProcessParamService;
    @Override
    public List<MesProProcessParamDO> getProcessParamListByProcessId(Long processId) {
        return processParamMapper.selectListByProcessId(processId);
    }
    @Override
    public List<MesProProcessParamDO> getProcessParamListByProcessIds(Collection<Long> processIds) {
        if (CollUtil.isEmpty(processIds)) {
            return new ArrayList<>();
        }
        return processParamMapper.selectListByProcessIds(processIds);
    }
    @Override
    public List<MesProProcessParamDO> getProcessParamListByParamId(Long processParamId) {
        return processParamMapper.selectListByProcessParamId(processParamId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProcessParams(Long processId, List<MesProProcessParamSaveReqVO.Item> items) {
        // 1. æ ¡éªŒå·¥åºå­˜åœ¨
        processService.validateProcessExists(processId);
        // 2. æ ¡éªŒå·¥è‰ºå‚数存在
        if (CollUtil.isNotEmpty(items)) {
            for (MesProProcessParamSaveReqVO.Item item : items) {
                pdProcessParamService.validateProcessParamExists(item.getProcessParamId());
            }
        }
        // 3. å…¨é‡è¦†ç›–:先删旧绑定,再插入新绑定
        processParamMapper.deleteByProcessId(processId);
        if (CollUtil.isEmpty(items)) {
            return;
        }
        List<MesProProcessParamDO> list = new ArrayList<>(items.size());
        for (int i = 0; i < items.size(); i++) {
            MesProProcessParamSaveReqVO.Item item = items.get(i);
            list.add(MesProProcessParamDO.builder()
                    .processId(processId)
                    .processParamId(item.getProcessParamId())
                    .required(Boolean.TRUE.equals(item.getRequired()))
                    .sort(item.getSort() != null ? item.getSort() : i + 1)
                    .remark(item.getRemark())
                    .build());
        }
        processParamMapper.insertBatch(list);
    }
    @Override
    public void deleteByParamId(Long processParamId) {
        processParamMapper.deleteByProcessParamId(processParamId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -12,6 +12,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationMachineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
@@ -24,6 +25,7 @@
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationMachineService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessParamService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
@@ -111,6 +113,9 @@
    @Lazy
    private MesProWorkOrderParamRecordService workOrderParamRecordService;
    @Resource
    @Lazy
    private MesProProcessParamService processParamService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private MesMdWorkstationMachineService workstationMachineService;
@@ -150,14 +155,28 @@
        }
        // 2.2 æ’å…¥
        taskMapper.insert(task);
        if (task.getParamTemplateId() != null) {
            workOrderParamRecordService.createTaskParamRecord(task.getId(), task.getParamTemplateId());
        }
        // 2.2.1 å‚数快照:优先使用模板;无模板时回退到工序主数据绑定的工艺参数
        createTaskParamRecordWithFallback(task);
        // 2.3 ä¿å­˜æŒ‡å®šæŠ¥å·¥äººå‘˜
        updateTaskAssignees(task.getId(), createReqVO.getAssigneeUserIds());
        return task.getId();
    }
    /**
     * ä¸ºä»»åŠ¡å›ºåŒ–å‚æ•°å¿«ç…§ï¼šä¼˜å…ˆä½¿ç”¨è·¯çº¿/工单工序绑定的参数模板;无模板时回退到工序主数据绑定的工艺参数
     */
    private void createTaskParamRecordWithFallback(MesProTaskDO task) {
        if (task.getParamTemplateId() != null) {
            workOrderParamRecordService.createTaskParamRecord(task.getId(), task.getParamTemplateId());
            return;
        }
        // æ— æ¨¡æ¿ï¼šä»Žå·¥åºä¸»æ•°æ®è§£æžç»‘定的工艺参数
        List<MesProProcessParamDO> processParams = processParamService.getProcessParamListByProcessId(task.getProcessId());
        if (CollUtil.isNotEmpty(processParams)) {
            workOrderParamRecordService.createTaskParamRecordFromProcessParams(task.getId(), processParams);
        }
    }
    @Override
@@ -498,9 +517,8 @@
                    .setClientId(workOrder.getClientId())
                    .setColorCode(rp.getColorCode());
            taskMapper.insert(task);
            if (task.getParamTemplateId() != null) {
                workOrderParamRecordService.createTaskParamRecord(task.getId(), task.getParamTemplateId());
            }
            // å‚数快照:优先使用模板;无模板时回退到工序主数据绑定的工艺参数
            createTaskParamRecordWithFallback(task);
            taskIds.add(task.getId());
        }
        // 7. æ›´æ–°å·¥å•已排产数量
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderParamRecordService.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.paramrecord.MesProWorkOrderParamRecordPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.paramrecord.MesProWorkOrderParamRecordSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderParamRecordDO;
import jakarta.validation.Valid;
@@ -30,6 +31,15 @@
    Long createTaskParamRecord(Long taskId, Long templateId);
    /**
     * æ ¹æ®å·¥åºç»‘定的工艺参数为生产任务固化参数快照(无模板时的回退路径)
     *
     * @param taskId        ä»»åŠ¡ç¼–å·
     * @param processParams å·¥åºç»‘定的工艺参数列表
     * @return è®°å½•编号
     */
    Long createTaskParamRecordFromProcessParams(Long taskId, List<MesProProcessParamDO> processParams);
    /**
     * æ›´æ–°å·¥å•参数记录(仅更新填写值,不换模板)
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderParamRecordServiceImpl.java
@@ -5,14 +5,17 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.paramrecord.MesProWorkOrderParamRecordPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.paramrecord.MesProWorkOrderParamRecordSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.processparam.MesPdProcessParamDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.processparamtemplate.MesPdProcessParamTemplateDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.processparamtemplate.MesPdProcessParamTemplateDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderParamRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderParamRecordDetailDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderParamRecordDetailMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderParamRecordMapper;
import cn.iocoder.yudao.module.mes.service.pd.processparam.MesPdProcessParamService;
import cn.iocoder.yudao.module.mes.service.pd.processparamtemplate.MesPdProcessParamTemplateService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
@@ -22,6 +25,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -46,6 +50,8 @@
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesPdProcessParamTemplateService templateService;
    @Resource
    private MesPdProcessParamService pdProcessParamService;
    @Autowired
    private MesProTaskService mesProTaskService;
    @Autowired
@@ -80,10 +86,57 @@
                .paramCode(detail.getParamCode())
                .paramName(detail.getParamName())
                .paramType(detail.getParamType())
                .required(detail.getRequired())
                .unitMeasureId(detail.getUnitMeasureId())
                .paramValue(null)
                .remark(detail.getRemark())
                .build()).toList());
        return record.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createTaskParamRecordFromProcessParams(Long taskId, List<MesProProcessParamDO> processParams) {
        MesProTaskDO task = mesProTaskService.getTask(taskId);
        if (task == null || CollUtil.isEmpty(processParams)) {
            return null;
        }
        // 1. æ‰¹é‡èŽ·å–å·¥è‰ºå‚æ•°å®šä¹‰ï¼Œè¿‡æ»¤å·²åˆ é™¤çš„å‚æ•°
        Map<Long, MesPdProcessParamDO> paramMap = pdProcessParamService.getProcessParamMap(
                processParams.stream().map(MesProProcessParamDO::getProcessParamId).collect(Collectors.toSet()));
        List<MesProProcessParamDO> valid = processParams.stream()
                .filter(pp -> paramMap.containsKey(pp.getProcessParamId()))
                .toList();
        if (CollUtil.isEmpty(valid)) {
            return null;
        }
        // 2. æ’入主表(无模板,template ç›¸å…³å­—段为空)
        MesProWorkOrderDO workOrder = mesProWorkOrderService.getWorkOrder(task.getWorkOrderId());
        MesProWorkOrderParamRecordDO record = MesProWorkOrderParamRecordDO.builder()
                .workOrderId(workOrder != null ? workOrder.getId() : task.getWorkOrderId())
                .taskId(taskId)
                .templateId(null)
                .templateCode(null)
                .templateName("工序绑定参数")
                .recordNo("TASK-" + taskId)
                .remark("生产任务按工序绑定参数生成的参数快照")
                .build();
        paramRecordMapper.insert(record);
        // 3. æ’入明细(快照参数定义,required ä¼˜å…ˆå–绑定值,其次取参数默认值)
        paramRecordDetailMapper.insertBatch(valid.stream().map(pp -> {
            MesPdProcessParamDO param = paramMap.get(pp.getProcessParamId());
            return MesProWorkOrderParamRecordDetailDO.builder()
                    .recordId(record.getId())
                    .templateDetailId(null)
                    .paramCode(param.getParamCode())
                    .paramName(param.getParamName())
                    .paramType(param.getParamType())
                    .required(pp.getRequired() != null ? pp.getRequired() : param.getRequired())
                    .unitMeasureId(param.getUnitMeasureId())
                    .paramValue(null)
                    .remark(pp.getRemark())
                    .build();
        }).toList());
        return record.getId();
    }
@@ -139,6 +192,7 @@
                            .paramCode(templateDetail.getParamCode())
                            .paramName(templateDetail.getParamName())
                            .paramType(templateDetail.getParamType())
                            .required(templateDetail.getRequired())
                            .unitMeasureId(templateDetail.getUnitMeasureId())
                            .paramValue(item.getParamValue())
                            .build();
@@ -194,6 +248,7 @@
                            .paramCode(templateDetail.getParamCode())
                            .paramName(templateDetail.getParamName())
                            .paramType(templateDetail.getParamType())
                            .required(templateDetail.getRequired())
                            .unitMeasureId(templateDetail.getUnitMeasureId())
                            .paramValue(item.getParamValue())
                            .build();
@@ -230,22 +285,38 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateTaskParamValues(Long taskId, List<Long> detailIds, List<String> values) {
        if (CollUtil.isEmpty(detailIds)) {
            return;
        }
        MesProWorkOrderParamRecordDO record = paramRecordMapper.selectFirstByTaskId(taskId);
        if (record == null) {
            return;
            return; // ä»»åŠ¡æ— å‚æ•°è®°å½•ï¼ˆæœªç»‘å®šå‚æ•°ï¼‰ï¼Œæ— éœ€æ ¡éªŒ
        }
        List<MesProWorkOrderParamRecordDetailDO> details = paramRecordDetailMapper.selectListByRecordId(record.getId());
        if (CollUtil.isEmpty(details)) {
            return;
        }
        // 1. æž„建提交明细映射(detailId -> å‚数值)
        Map<Long, String> valueMap = new HashMap<>();
        if (CollUtil.isNotEmpty(detailIds)) {
            for (int i = 0; i < detailIds.size(); i++) {
                valueMap.put(detailIds.get(i), values != null && values.size() > i ? values.get(i) : null);
            }
        }
        // 2. æ ¡éªŒå¿…填参数都有值(绑定参数或模板快照中的 required)
        Map<Long, MesProWorkOrderParamRecordDetailDO> detailMap = details.stream()
                .collect(Collectors.toMap(MesProWorkOrderParamRecordDetailDO::getId, d -> d));
        for (int i = 0; i < detailIds.size(); i++) {
            MesProWorkOrderParamRecordDetailDO detail = detailMap.get(detailIds.get(i));
        for (MesProWorkOrderParamRecordDetailDO detail : details) {
            String value = valueMap.get(detail.getId());
            if (Boolean.TRUE.equals(detail.getRequired())
                    && (value == null || value.isBlank())) {
                throw exception(PRO_WORK_ORDER_PARAM_RECORD_REQUIRED_PARAM_MISSING, detail.getParamName());
            }
        }
        // 3. æ›´æ–°æäº¤çš„参数值
        for (Map.Entry<Long, String> entry : valueMap.entrySet()) {
            MesProWorkOrderParamRecordDetailDO detail = detailMap.get(entry.getKey());
            if (detail == null) {
                throw exception(PRO_WORK_ORDER_PARAM_RECORD_DETAIL_NOT_IN_TEMPLATE);
            }
            detail.setParamValue(values != null && values.size() > i ? values.get(i) : null);
            detail.setParamValue(entry.getValue());
            paramRecordDetailMapper.updateById(detail);
        }
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/SrmSupplierApplyController.java
@@ -15,7 +15,11 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - SRM ä¾›åº”商准入")
@RestController
@@ -66,45 +70,19 @@
    }
    @PutMapping("/submit")
    @Operation(summary = "提交申请")
    @Parameter(name = "id", description = "编号", required = true)
    @Operation(summary = "提交准入申请审批(启动协同办公流程)")
    @PreAuthorize("@ss.hasPermission('srm:supplier-apply:update')")
    public CommonResult<Boolean> submitApply(@RequestParam("id") Long id) {
        applyService.submitApply(id);
    public CommonResult<Boolean> submitApply(@RequestParam("id") Long id,
                                             @RequestParam("processDefinitionKey") String processDefinitionKey) {
        applyService.submitApply(id, processDefinitionKey, getLoginUserId());
        return success(true);
    }
    @PutMapping("/review-purchase")
    @Operation(summary = "采购审核")
    @PreAuthorize("@ss.hasPermission('srm:supplier-apply:update')")
    public CommonResult<Boolean> reviewPurchase(@Valid @RequestBody SrmSupplierApplyReviewReqVO reqVO) {
        applyService.reviewPurchase(reqVO.getId(), reqVO.getOpinion(), reqVO.getPassed());
        return success(true);
    }
    @PutMapping("/review-quality")
    @Operation(summary = "质量审核")
    @PreAuthorize("@ss.hasPermission('srm:supplier-apply:update')")
    public CommonResult<Boolean> reviewQuality(@Valid @RequestBody SrmSupplierApplyReviewReqVO reqVO) {
        applyService.reviewQuality(reqVO.getId(), reqVO.getOpinion(), reqVO.getPassed());
        return success(true);
    }
    @PutMapping("/review-finance")
    @Operation(summary = "财务审核")
    @PreAuthorize("@ss.hasPermission('srm:supplier-apply:update')")
    public CommonResult<Boolean> reviewFinance(@Valid @RequestBody SrmSupplierApplyReviewReqVO reqVO) {
        applyService.reviewFinance(reqVO.getId(), reqVO.getOpinion(), reqVO.getPassed());
        return success(true);
    }
    @PutMapping("/approve")
    @Operation(summary = "通过后自动创建供应商")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('srm:supplier-apply:update')")
    public CommonResult<Boolean> approve(@RequestParam("id") Long id) {
        applyService.approve(id);
        return success(true);
    @GetMapping("/approve-process-list")
    @Operation(summary = "获取准入申请审批流程列表")
    @PreAuthorize("@ss.hasPermission('srm:supplier-apply:query')")
    public CommonResult<List<Map<String, Object>>> getApproveProcessList() {
        return success(applyService.getSupplierApplyApproveProcessDefinitionList());
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplyRespVO.java
@@ -70,6 +70,9 @@
    @Schema(description = "申请状态", example = "0")
    private Integer applyStatus;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号", example = "1a2b3c4d")
    private String processInstanceId;
    @Schema(description = "采购审核人ID", example = "2")
    private Long purchaseReviewerId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplyReviewReqVO.java
ÎļþÒÑɾ³ý
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/dataobject/SrmSupplierApplyDO.java
@@ -44,6 +44,7 @@
    private String applyUserName;
    private LocalDateTime applyTime;
    private Integer applyStatus;
    private String processInstanceId;
    private Long purchaseReviewerId;
    private LocalDateTime purchaseReviewTime;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java
@@ -21,6 +21,8 @@
    ErrorCode APPLY_STATUS_NOT_PENDING = new ErrorCode(1_040_200_002, "当前状态不允许审核");
    ErrorCode APPLY_SUPPLIER_EXISTS = new ErrorCode(1_040_200_003, "该供应商已存在准入记录");
    ErrorCode APPLY_SUPPLIER_NAME_REQUIRED = new ErrorCode(1_040_200_004, "未选择SRM供应商时,供应商名称不能为空");
    ErrorCode APPLY_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_040_200_005, "供应商准入审批分类不存在,请先在BPM模块创建供应商准入审批流程分类");
    ErrorCode APPLY_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_040_200_006, "供应商准入审批分类下没有可用的流程模型,请先创建并发布流程");
    // ========== ä¾›åº”商资质 1_040_300_000 ==========
    ErrorCode CERTIFICATE_NOT_EXISTS = new ErrorCode(1_040_300_000, "资质证书不存在");
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/SupplierApplyStatusEnum.java
@@ -4,16 +4,19 @@
/**
 * ä¾›åº”商准入申请状态枚举
 *
 * æŽ¥å…¥ååŒåŠžå…¬(BPM)后,SRM ä¾§åªæ„ŸçŸ¥æµç¨‹æœ€ç»ˆç»“æžœ,当前审批进度在协同办公查看。
 * åŽ†å²çŠ¶æ€(待采购审核 10 / å¾…质量审核 20 / å¾…财务审核 30)已废弃,
 * ç”± SQL è„šæœ¬ç»Ÿä¸€è¿ç§»:历史审批中单据重置为「待提交」,重新提交走协同办公。
 */
@Getter
public enum SupplierApplyStatusEnum {
    DRAFT(0, "待提交"),
    PENDING_PURCHASE(10, "待采购审核"),
    PENDING_QUALITY(20, "待质量审核"),
    PENDING_FINANCE(30, "待财务审核"),
    PROCESS(10, "审批中"),
    CANCEL(30, "已取消"),
    APPROVED(40, "已通过"),
    REJECTED(50, "已拒绝");
    REJECTED(50, "已驳回");
    SupplierApplyStatusEnum(Integer code, String name) {
        this.code = code;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyService.java
@@ -6,6 +6,9 @@
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierApplyDO;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
/**
 * SRM ä¾›åº”商准入申请 Service æŽ¥å£
 */
@@ -37,32 +40,34 @@
    PageResult<SrmSupplierApplyDO> getApplyPage(SrmSupplierApplyPageReqVO pageReqVO);
    /**
     * æäº¤ç”³è¯·ï¼ˆå¾…采购审核)
     * æäº¤ç”³è¯·ï¼ˆå¯åŠ¨ååŒåŠžå…¬æµç¨‹ï¼ŒçŠ¶æ€ç½®ä¸ºå®¡æ‰¹ä¸­ï¼‰
     *
     * @param id                  ç”³è¯·ç¼–号
     * @param processDefinitionKey ååŒåŠžå…¬æµç¨‹å®šä¹‰ Key
     * @param userId              æäº¤äººç¼–号
     */
    void submitApply(Long id);
    void submitApply(Long id, String processDefinitionKey, Long userId);
    /**
     * é‡‡è´­å®¡æ ¸
     * èŽ·å–ä¾›åº”å•†å‡†å…¥å®¡æ‰¹æµç¨‹åˆ—è¡¨ï¼ˆååŒåŠžå…¬å·²å‘å¸ƒä¸”æ¿€æ´»çš„æµç¨‹ï¼‰
     */
    void reviewPurchase(Long id, String opinion, Boolean passed);
    List<Map<String, Object>> getSupplierApplyApproveProcessDefinitionList();
    /**
     * è´¨é‡å®¡æ ¸
     * ååŒåŠžå…¬æµç¨‹ç»“æžœå›žå†™
     *
     * @param id       ç”³è¯·ç¼–号
     * @param bpmResult æµç¨‹ç»“果({@link cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum})
     */
    void reviewQuality(Long id, String opinion, Boolean passed);
    void updateSupplierApplyAuditStatus(Long id, Integer bpmResult);
    /**
     * è´¢åŠ¡å®¡æ ¸
     */
    void reviewFinance(Long id, String opinion, Boolean passed);
    /**
     * é€šè¿‡åŽè‡ªåŠ¨åˆ›å»ºä¾›åº”å•†
     * å®¡æ‰¹é€šè¿‡åŽè‡ªåŠ¨åˆ›å»ºä¾›åº”å•†
     */
    void approve(Long id);
    /**
     * ç¡®ä¿ä¾›åº”商已准入:未准入则自动创建准入申请并置为已通过
     * ç¡®ä¿ä¾›åº”商已准入:未准入则自动创建准入申请并置为已通过(定标自动准入,不走协同办公)
     *
     * @param supplierId ä¾›åº”商编号
     */
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyServiceImpl.java
@@ -1,9 +1,17 @@
package cn.iocoder.yudao.module.srm.service.apply;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.srm.controller.admin.apply.vo.SrmSupplierApplyPageReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.apply.vo.SrmSupplierApplySaveReqVO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierApplyDO;
@@ -13,24 +21,47 @@
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.SupplierApplyStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * SRM ä¾›åº”商准入申请 Service å®žçŽ°ç±»
 *
 * æŽ¥å…¥ååŒåŠžå…¬(BPM)后,采购/质量/财务审核在协同办公流程中完成,
 * SRM ä¾§åªè´Ÿè´£å‘起流程并接收流程结束事件回写状态。
 */
@Service
@Validated
@Slf4j
public class SrmSupplierApplyServiceImpl implements SrmSupplierApplyService {
    /**
     * BPM ä¾›åº”商准入审批分类编码
     */
    private static final String SUPPLIER_APPLY_APPROVE_CATEGORY_CODE = "supplier_apply_approve";
    @Resource
    private SrmSupplierApplyMapper supplierApplyMapper;
    @Resource
    private SrmSupplierMapper supplierMapper;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    public Long createApply(SrmSupplierApplySaveReqVO createReqVO) {
@@ -54,7 +85,9 @@
    @Override
    public void updateApply(SrmSupplierApplySaveReqVO updateReqVO) {
        SrmSupplierApplyDO apply = validateApplyExists(updateReqVO.getId());
        if (!SupplierApplyStatusEnum.DRAFT.getCode().equals(apply.getApplyStatus())) {
        // ä»…待提交/已驳回可编辑
        if (!SupplierApplyStatusEnum.DRAFT.getCode().equals(apply.getApplyStatus())
                && !SupplierApplyStatusEnum.REJECTED.getCode().equals(apply.getApplyStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.APPLY_STATUS_NOT_DRAFT);
        }
        // æ ¡éªŒï¼šå¿…须选择 SRM ä¾›åº”商或手动填写供应商名称
@@ -72,7 +105,12 @@
    @Override
    public void deleteApply(Long id) {
        validateApplyExists(id);
        SrmSupplierApplyDO apply = validateApplyExists(id);
        // ä»…待提交/已驳回可删除
        if (!SupplierApplyStatusEnum.DRAFT.getCode().equals(apply.getApplyStatus())
                && !SupplierApplyStatusEnum.REJECTED.getCode().equals(apply.getApplyStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.APPLY_STATUS_NOT_DRAFT);
        }
        supplierApplyMapper.deleteById(id);
    }
@@ -88,59 +126,83 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitApply(Long id) {
    public void submitApply(Long id, String processDefinitionKey, Long userId) {
        // 1. æ ¡éªŒå­˜åœ¨ + å¾…提交/已驳回状态
        SrmSupplierApplyDO apply = validateApplyExists(id);
        if (!SupplierApplyStatusEnum.DRAFT.getCode().equals(apply.getApplyStatus())) {
        if (!SupplierApplyStatusEnum.DRAFT.getCode().equals(apply.getApplyStatus())
                && !SupplierApplyStatusEnum.REJECTED.getCode().equals(apply.getApplyStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.APPLY_STATUS_NOT_DRAFT);
        }
        apply.setApplyStatus(SupplierApplyStatusEnum.PENDING_PURCHASE.getCode());
        apply.setApplyTime(LocalDateTime.now());
        supplierApplyMapper.updateById(apply);
        // 2. åˆ›å»º BPM æµç¨‹å®žä¾‹ï¼ˆé‡‡è´­å®¡æ ¸â†’质量审核→财务审核,流程与审批人在协同办公配置)
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 3. æ›´æ–°ç”³è¯·çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­ï¼Œå›žå­˜æµç¨‹å®žä¾‹ç¼–å·
        supplierApplyMapper.updateById(new SrmSupplierApplyDO()
                .setId(id)
                .setApplyStatus(SupplierApplyStatusEnum.PROCESS.getCode())
                .setProcessInstanceId(processInstanceId));
    }
    @Override
    public List<Map<String, Object>> getSupplierApplyApproveProcessDefinitionList() {
        // 1. æ ¡éªŒåˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
        validateSupplierApplyApproveCategoryAndProcessDefinition();
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(SUPPLIER_APPLY_APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            return Collections.emptyList();
        }
        // 3. éåŽ†èŽ·å–æµç¨‹å®šä¹‰è¯¦æƒ…ï¼Œè¿‡æ»¤æ¿€æ´»çŠ¶æ€ï¼Œä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
            ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
            if (pd == null || pd.isSuspended()) {
                continue;
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        // 4. è¿”回流程定义列表
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            result.add(item);
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void reviewPurchase(Long id, String opinion, Boolean passed) {
    public void updateSupplierApplyAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒå­˜åœ¨
        SrmSupplierApplyDO apply = validateApplyExists(id);
        if (!SupplierApplyStatusEnum.PENDING_PURCHASE.getCode().equals(apply.getApplyStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.APPLY_STATUS_NOT_PENDING);
        if (!SupplierApplyStatusEnum.PROCESS.getCode().equals(apply.getApplyStatus())) {
            log.warn("[updateSupplierApplyAuditStatus] å‡†å…¥ç”³è¯·({}) ä¸å¤„于审批中状态", id);
            return;
        }
        apply.setPurchaseReviewOpinion(opinion);
        apply.setPurchaseReviewTime(LocalDateTime.now());
        apply.setApplyStatus(Boolean.TRUE.equals(passed)
                ? SupplierApplyStatusEnum.PENDING_QUALITY.getCode()
                : SupplierApplyStatusEnum.REJECTED.getCode());
        supplierApplyMapper.updateById(apply);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void reviewQuality(Long id, String opinion, Boolean passed) {
        SrmSupplierApplyDO apply = validateApplyExists(id);
        if (!SupplierApplyStatusEnum.PENDING_QUALITY.getCode().equals(apply.getApplyStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.APPLY_STATUS_NOT_PENDING);
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        Integer newStatus = convertBpmResultToStatus(bpmResult);
        if (newStatus != null) {
            supplierApplyMapper.updateById(new SrmSupplierApplyDO()
                    .setId(id).setApplyStatus(newStatus));
        }
        apply.setQualityReviewOpinion(opinion);
        apply.setQualityReviewTime(LocalDateTime.now());
        apply.setApplyStatus(Boolean.TRUE.equals(passed)
                ? SupplierApplyStatusEnum.PENDING_FINANCE.getCode()
                : SupplierApplyStatusEnum.REJECTED.getCode());
        supplierApplyMapper.updateById(apply);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void reviewFinance(Long id, String opinion, Boolean passed) {
        SrmSupplierApplyDO apply = validateApplyExists(id);
        if (!SupplierApplyStatusEnum.PENDING_FINANCE.getCode().equals(apply.getApplyStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.APPLY_STATUS_NOT_PENDING);
        // 3. å®¡æ‰¹é€šè¿‡ï¼šè‡ªåŠ¨åˆ›å»º/关联供应商档案
        if (SupplierApplyStatusEnum.APPROVED.getCode().equals(newStatus)) {
            approve(id);
        }
        apply.setFinanceReviewOpinion(opinion);
        apply.setFinanceReviewTime(LocalDateTime.now());
        apply.setApplyStatus(Boolean.TRUE.equals(passed)
                ? SupplierApplyStatusEnum.APPROVED.getCode()
                : SupplierApplyStatusEnum.REJECTED.getCode());
        supplierApplyMapper.updateById(apply);
    }
    @Override
@@ -158,7 +220,7 @@
        if (supplierApplyMapper.existsApprovedBySupplierId(supplierId)) {
            return;
        }
        // è‡ªåŠ¨åˆ›å»ºå‡†å…¥ç”³è¯·å¹¶ç›´æŽ¥ç½®ä¸ºå·²é€šè¿‡
        // è‡ªåŠ¨åˆ›å»ºå‡†å…¥ç”³è¯·å¹¶ç›´æŽ¥ç½®ä¸ºå·²é€šè¿‡ï¼ˆå®šæ ‡è‡ªåŠ¨å‡†å…¥ï¼Œä¸èµ°ååŒåŠžå…¬ï¼‰
        SrmSupplierApplyDO apply = new SrmSupplierApplyDO();
        apply.setApplyNo(generateApplyNo());
        apply.setSupplierId(supplierId);
@@ -221,6 +283,31 @@
    }
    /**
     * æ ¡éªŒä¾›åº”商准入审批分类存在
     */
    private void validateSupplierApplyApproveCategoryAndProcessDefinition() {
        List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
                Collections.singletonList(SUPPLIER_APPLY_APPROVE_CATEGORY_CODE));
        if (CollUtil.isEmpty(categories)) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.APPLY_BPM_CATEGORY_NOT_EXISTS);
        }
    }
    /**
     * è½¬æ¢ BPM å®¡æ‰¹ç»“果为业务状态
     */
    private Integer convertBpmResultToStatus(Integer bpmResult) {
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return SupplierApplyStatusEnum.APPROVED.getCode();
        } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return SupplierApplyStatusEnum.REJECTED.getCode();
        } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return SupplierApplyStatusEnum.CANCEL.getCode();
        }
        return null;
    }
    /**
     * ç”Ÿæˆç”³è¯·ç¼–号
     */
    private String generateApplyNo() {
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/listener/SrmSupplierApplyStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.srm.service.apply.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.srm.service.apply.SrmSupplierApplyService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * ä¾›åº”商准入审批状态监听器
 *
 * ç›‘听 BPM æµç¨‹å®žä¾‹çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œæ›´æ–°å‡†å…¥ç”³è¯·çš„å®¡æ‰¹çŠ¶æ€ï¼ˆååŒåŠžå…¬å®¡æ‰¹åŽå›žå†™ï¼‰
 */
@Component
@Slf4j
public class SrmSupplierApplyStatusListener extends BpmProcessInstanceStatusEventListener {
    /**
     * ä¾›åº”商准入审批流程定义 Key
     */
    private static final String PROCESS_DEFINITION_KEY = "supplier_apply_approve";
    @Resource
    private SrmSupplierApplyService supplierApplyService;
    @Override
    protected String getProcessDefinitionKey() {
        return PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long applyId;
        try {
            applyId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„准入申请编号", event.getBusinessKey());
            return;
        }
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,applyId={}, status={}", applyId, event.getStatus());
        try {
            supplierApplyService.updateSupplierApplyAuditStatus(applyId, event.getStatus());
            log.info("[onEvent] å‡†å…¥ç”³è¯·({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°å®Œæˆ", applyId);
        } catch (Exception e) {
            log.error("[onEvent] æ›´æ–°å‡†å…¥ç”³è¯·å®¡æ‰¹çŠ¶æ€å¤±è´¥ï¼ŒapplyId={}", applyId, e);
        }
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserUpdateReqDTO;
import java.util.Collection;
import java.util.Collections;
@@ -25,6 +26,13 @@
    Long createUser(AdminUserCreateReqDTO createReqDTO);
    /**
     * æ›´æ–°ç”¨æˆ·ï¼ˆä¾›å…¶ä»–模块同步更新系统用户部分信息,如岗位/部门/昵称等)
     *
     * @param updateReqDTO ç”¨æˆ·æ›´æ–°ä¿¡æ¯
     */
    void updateUser(AdminUserUpdateReqDTO updateReqDTO);
    /**
     * é€šè¿‡ç”¨æˆ· ID æŸ¥è¯¢ç”¨æˆ·
     *
     * @param id ç”¨æˆ·ID
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserUpdateReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
@@ -53,6 +54,19 @@
    }
    @Override
    public void updateUser(AdminUserUpdateReqDTO updateReqDTO) {
        UserSaveReqVO updateReqVO = new UserSaveReqVO();
        updateReqVO.setId(updateReqDTO.getId());
        updateReqVO.setNickname(updateReqDTO.getNickname());
        updateReqVO.setDeptId(updateReqDTO.getDeptId());
        updateReqVO.setPostIds(updateReqDTO.getPostIds());
        updateReqVO.setRoleIds(updateReqDTO.getRoleIds());
        updateReqVO.setEmail(updateReqDTO.getEmail());
        updateReqVO.setRemark(updateReqDTO.getRemark());
        userService.updateUser(updateReqVO);
    }
    @Override
    @DataPermission(enable = false) // å¿½ç•¥æ•°æ®æƒé™ï¼Œé¿å…å› ä¸ºè¿‡æ»¤ï¼Œå¯¼è‡´æ— æ³•查询用户。类似:https://github.com/YunaiV/ruoyi-vue-pro/issues/1051
    public AdminUserRespDTO getUser(Long id) {
        AdminUserDO user = userService.getUser(id);
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserUpdateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.system.api.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Set;
/**
 * Admin ç”¨æˆ·æ›´æ–° Request DTO
 *
 * ä¾›å…¶ä»–模块同步更新系统用户的部分信息(如 HRM å‘˜å·¥ç¼–辑时同步岗位/部门等)
 */
@Schema(description = "管理后台 - ç”¨æˆ·æ›´æ–° Request DTO")
@Data
public class AdminUserUpdateReqDTO {
    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "用户编号不能为空")
    private Long id;
    @Schema(description = "用户昵称", example = "张三")
    private String nickname;
    @Schema(description = "部门编号", example = "1")
    private Long deptId;
    @Schema(description = "岗位编号数组", example = "[1, 2]")
    private Set<Long> postIds;
    @Schema(description = "角色编号数组", example = "[1, 2]")
    private Set<Long> roleIds;
    @Schema(description = "用户邮箱", example = "zhangsan@example.com")
    private String email;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -42,6 +42,8 @@
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_POST_NOT_EMPTY;
@Tag(name = "管理后台 - ç”¨æˆ·")
@RestController
@@ -62,6 +64,10 @@
    @Operation(summary = "新增用户")
    @PreAuthorize("@ss.hasPermission('system:user:create')")
    public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) {
        // æ ¡éªŒå²—位必填
        if (CollUtil.isEmpty(reqVO.getPostIds())) {
            throw exception(USER_POST_NOT_EMPTY);
        }
        Long id = userService.createUser(reqVO);
        return success(id);
    }
@@ -70,6 +76,10 @@
    @Operation(summary = "修改用户")
    @PreAuthorize("@ss.hasPermission('system:user:update')")
    public CommonResult<Boolean> updateUser(@Valid @RequestBody UserSaveReqVO reqVO) {
        // æ ¡éªŒå²—位必填
        if (CollUtil.isEmpty(reqVO.getPostIds())) {
            throw exception(USER_POST_NOT_EMPTY);
        }
        userService.updateUser(reqVO);
        return success(true);
    }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -42,6 +42,7 @@
    ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_002_003_004, "导入用户数据不能为空!");
    ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1_002_003_005, "用户密码校验失败");
    ErrorCode USER_IS_DISABLE = new ErrorCode(1_002_003_006, "名字为【{}】的用户已被禁用");
    ErrorCode USER_POST_NOT_EMPTY = new ErrorCode(1_002_003_007, "岗位不能为空");
    ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})!");
    ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空");
    ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册");