liu
15 小时以前 cdf5605fb5caa0da0f134893bac350785250ea8a
feat(mes): 基础参数与工序绑定,报工填写绑定的参数字段

- 新增 mes_pro_process_param 工序-工艺参数绑定表及 CRUD 接口(按工序查/保存/反查)
- 任务无参数模板时回退到工序绑定参数,生成任务参数快照(含 required)
- 报工必填参数强校验(补事务避免残留草稿)、报工详情/列表回显参数值
- 附带整合 SQL:docs/sql/mes_process_param_bind_full.sql

Co-Authored-By: Claude <noreply@anthropic.com>
已添加9个文件
已修改9个文件
1018 ■■■■■ 文件已修改
docs/process_param_bind_feedback_integration.md 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mes_process_param_bind_full.sql 145 ●●●●● 补丁 | 查看 | 原始文档 | 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 | 历史
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';
-- ============================================================================
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);
        }
    }