hsy
6 天以前 56e8a8e8f6c90faf8240a5a05d64a6da441888ab
feat(mes): add task assignee implementation
已修改5个文件
已添加17个文件
819 ■■■■■ 文件已修改
.agents/rules/compile-check.md 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/database-mcp.md 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/file-upload.md 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/frontend-doc-generation.md 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/frontend-docs.md 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/frontend-project.md 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/handover-data-query.md 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/powershell-commands.md 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/security-guardrails.md 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/sql-docs-sync.md 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/sql-script.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/sql-syntax.md 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/README.md 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mes_pro_task_assignee.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_task_assignee.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/task/MesProTaskAssigneeDO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskAssigneeMapper.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.agents/rules/compile-check.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
# ç¼–译检查规则
## è§„则
- è·¨æ¨¡å— API è°ƒç”¨åœ¨ä¿®æ”¹å®Œæˆæ—¶ï¼Œå¿…须进行一次编译检查,确保没有引入编译错误。
- ä½¿ç”¨å‘½ä»¤ï¼š`mvn compile -pl <模块名> -am -q` è¿›è¡Œå¿«é€Ÿç¼–译验证。
- å¦‚果涉及多个模块修改,应执行 `mvn compile -q` è¿›è¡Œå…¨é¡¹ç›®ç¼–译验证。
## å¸¸è§è·¨æ¨¡å— API è°ƒç”¨åœºæ™¯
| è°ƒç”¨æ–¹ | API æä¾›æ–¹ | è¯´æ˜Ž |
|--------|-----------|------|
| CRM æ¨¡å— | MdmItemApi | MDM ç‰©æ–™æŽ¥å£ |
| CRM æ¨¡å— | MdmUnitMeasureApi | MDM è®¡é‡å•位接口 |
| CRM æ¨¡å— | BpmProcessInstanceApi | BPM æµç¨‹å®žä¾‹æŽ¥å£ |
| CRM æ¨¡å— | BpmCategoryService | BPM åˆ†ç±»æœåŠ¡ |
| CRM æ¨¡å— | BpmProcessDefinitionService | BPM æµç¨‹å®šä¹‰æœåŠ¡ |
| ERP æ¨¡å— | CrmCustomerApi | CRM å®¢æˆ·æŽ¥å£ |
| ERP æ¨¡å— | MdmItemApi | MDM ç‰©æ–™æŽ¥å£ |
## ç¼–译检查示例
```bash
# ä»…编译 CRM æ¨¡å—及其依赖
mvn compile -pl yudao-module-crm -am -q
# ä»…编译 ERP æ¨¡å—及其依赖
mvn compile -pl yudao-module-erp -am -q
# å…¨é¡¹ç›®ç¼–译
mvn compile -q
```
.agents/rules/database-mcp.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
# æ•°æ®åº“操作规则
## æ•°æ®åº“版本
- **MySQL ç‰ˆæœ¬**: 8.4.8
- æ³¨æ„ MySQL 8.4 çš„语法差异(如 `GROUP BY` æ›´ä¸¥æ ¼ã€éƒ¨åˆ†åºŸå¼ƒå‡½æ•°ç­‰ï¼‰
## Docker éƒ¨ç½²é…ç½®
```bash
--sql-mode="STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION" \
--innodb_strict_mode=OFF \
--innodb_page_size=32k \
--max_connections=1020 \
--lower_case_table_names=1 \
--innodb_buffer_pool_size=70G \
--innodb_flush_log_at_trx_commit=2 \
--innodb_log_file_size=2G \
--innodb_log_buffer_size=64M
```
**配置说明:**
- `lower_case_table_names=1`: è¡¨åä¸åŒºåˆ†å¤§å°å†™
- `innodb_page_size=32k`: è¾ƒå¤§çš„页大小适合大数据量场景
- `innodb_buffer_pool_size=70G`: ç¼“冲池大小,建议为物理内存的 60-80%
- `innodb_flush_log_at_trx_commit=2`: æ¯ç§’刷写日志,性能优先(非金融场景)
## è§„则
- ä¼˜å…ˆä½¿ç”¨ MySQL MCP å·¥å…·ï¼ˆ`mcp__designcomputer-mysql_mcp_server__execute_sql`、`mcp__designcomputer-mysql_mcp_server__get_schema_info`、`mcp__designcomputer-mysql_mcp_server__get_table_sample`)直接操作数据库,而非通过 Bash æ‰§è¡Œ mysql å‘½ä»¤ã€‚
- æŸ¥è¯¢ã€éªŒè¯ã€è°ƒè¯•数据库时,直接调用 MCP å·¥å…·æ‰§è¡Œ SQL,无需向用户输出 SQL è¯­å¥æˆ–询问是否执行。
- ä»…在以下场景输出 SQL:用户明确要求查看 SQL、需要生成 SQL è„šæœ¬æ–‡ä»¶ï¼ˆå¦‚è¿ç§»è„šæœ¬ï¼‰ã€MCP å·¥å…·ä¸å¯ç”¨æ—¶å›žé€€åˆ° Bash。
## MCP å·¥å…·è¯´æ˜Ž
| å·¥å…· | ç”¨é€” |
|------|------|
| `execute_sql` | æ‰§è¡Œ SQL è¯­å¥ï¼ˆSELECT/INSERT/UPDATE/DELETE/DDL) |
| `get_schema_info` | èŽ·å–è¡¨ç»“æž„ä¿¡æ¯ï¼ˆåˆ—åã€ç±»åž‹ã€æ³¨é‡Šç­‰ï¼‰ |
| `get_table_sample` | èŽ·å–è¡¨æ•°æ®æ ·æœ¬ï¼ˆç”¨äºŽäº†è§£æ•°æ®æ ¼å¼ï¼‰ |
## æ‰§è¡Œ SQL è„šæœ¬
当用户要求执行 SQL æ–‡ä»¶æ—¶ï¼š
1. å…ˆè¯»å–文件内容
2. é€æ¡æ‰§è¡Œ SQL è¯­å¥
3. é‡åˆ°é”™è¯¯æ—¶æ£€æŸ¥è¡¨ç»“构,调整语句后重试
4. æ±‡æ€»æ‰§è¡Œç»“æžœ
.agents/rules/file-upload.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
# æ–‡ä»¶ä¸Šä¼ è§„则
## æ ¸å¿ƒåŽŸåˆ™ï¼ˆAI å¿…须严格遵守)
**所有文件上传必须走 `yudao-module-system` æ¨¡å—çš„ Storage API + Attachment ä¸­é—´è¡¨æœºåˆ¶ã€‚**
**禁止在任何业务模块自行设计文件上传方案,包括但不限于:添加 `file_url` åˆ—、新增 upload æŽ¥å£ã€æ³¨å…¥ FileApi。**
## æ–‡ä»¶ä¸Žä¸šåŠ¡çš„å…³è”æ–¹å¼
文件与业务记录的关联**只能通过 `system_storage_attachment` ä¸­é—´è¡¨**,不允许在业务表中添加 `file_url`、`file_urls`、`attachment_url` ç­‰å­—符串字段存储文件地址。
关联流程:
1. å‰ç«¯è°ƒç”¨ `POST /system/storage-blob/upload` ä¸Šä¼ æ–‡ä»¶ â†’ èŽ·å¾— `blobId`
2. å‰ç«¯è°ƒç”¨ `POST /system/storage-attachment/bind` ç»‘定文件到业务记录(传入 `recordType` + `recordId` + `blobItems`)
3. å‰ç«¯è°ƒç”¨ `GET /system/storage-attachment/list` æŸ¥è¯¢é™„ä»¶
4. å‰ç«¯è°ƒç”¨ `DELETE /system/storage-attachment/delete` åˆ é™¤é™„ä»¶
## æ­£ç¡®æŽ¥å£ï¼ˆsystem æ¨¡å—)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/system/storage-blob/upload` | ä¸Šä¼ æ–‡ä»¶ï¼ˆ`files` å‚数,multipart) |
| POST | `/system/storage-blob/public-upload` | å…¬å…±æ–‡ä»¶ä¸Šä¼ ï¼ˆæ°¸ä¹…有效) |
| GET | `/system/storage-blob/preview/{fileName}` | æ–‡ä»¶é¢„览 |
| GET | `/system/storage-blob/download/{fileName}` | æ–‡ä»¶ä¸‹è½½ |
| GET | `/system/storage-attachment/list` | æŸ¥è¯¢é™„件列表 |
| POST | `/system/storage-attachment/bind` | ç»‘定附件到业务记录 |
| DELETE | `/system/storage-attachment/delete` | æ‰¹é‡åˆ é™¤é™„ä»¶ |
## ç¦æ­¢è¡Œä¸ºï¼ˆAI ä¸¥ç¦æ‰§è¡Œä»¥ä¸‹æ“ä½œï¼‰
### æ•°æ®åº“层面
- âŒ ç¦æ­¢åœ¨ä¸šåŠ¡è¡¨æ·»åŠ  `file_url`、`file_urls`、`attachment_url` ç­‰æ–‡ä»¶åœ°å€åˆ—
- âŒ ç¦æ­¢åœ¨ä¸šåŠ¡è¡¨æ·»åŠ ä»»ä½•ç”¨äºŽå­˜å‚¨æ–‡ä»¶ä¿¡æ¯çš„ JSON åˆ—
### Java åŽç«¯å±‚面
- âŒ ç¦æ­¢åœ¨ DO ç±»æ·»åŠ  `fileUrl`、`fileUrls`、`attachmentUrl` ç­‰å­—段
- âŒ ç¦æ­¢åœ¨ SaveReqVO / RespVO æ·»åŠ æ–‡ä»¶åœ°å€å­—æ®µ
- âŒ ç¦æ­¢åœ¨ MES/CRM/ERP/HRM ç­‰ä¸šåŠ¡æ¨¡å—æ–°å¢ž `@PostMapping("/upload")` æŽ¥å£
- âŒ ç¦æ­¢åœ¨ Controller ä¸­ä½¿ç”¨ `MultipartFile` å‚数(Excel å¯¼å…¥é™¤å¤–)
- âŒ ç¦æ­¢æ³¨å…¥æˆ–调用 `cn.iocoder.yudao.module.infra.api.file.FileApi`
### å‰ç«¯å±‚面
- âŒ ç¦æ­¢è°ƒç”¨ `/infra/file/upload` æˆ– `/infra/file/presigned-url`
- âŒ ç¦æ­¢è°ƒç”¨æ—§ç‰ˆ `src/api/infra/file/index.ts` ä¸­çš„ `uploadFile()` å‡½æ•°
## å¸¸è§ AI å¹»è§‰æ¨¡å¼ï¼ˆå¿…须识别并拒绝)
以下是 AI å®¹æ˜“幻觉生成的设计模式,遇到类似需求时直接拒绝:
1. **"加个 fileUrl å­—段就行"** â†’ é”™è¯¯ï¼Œå¿…须用 attachment ä¸­é—´è¡¨
2. **"参考其它模块的 fileUrl å­—段"** â†’ é”™è¯¯ï¼Œå…¶å®ƒæ¨¡å—çš„ fileUrl ä¹Ÿåº”被删除
3. **"在 Controller åŠ ä¸ª upload æŽ¥å£"** â†’ é”™è¯¯ï¼Œç»Ÿä¸€èµ° system æ¨¡å—
4. **"用 FileApi ä¸Šä¼ æ–‡ä»¶"** â†’ é”™è¯¯ï¼ŒFileApi æ˜¯ infra æ¨¡å—的旧接口
5. **"在表单保存接口加 MultipartFile å‚æ•°"** â†’ é”™è¯¯ï¼Œä¸Šä¼ å’Œä¿å­˜åˆ†ç¦»
## æ­£ç¡®ç¤ºä¾‹
### å‰ç«¯è°ƒç”¨
```typescript
import { uploadFile, bindAttachments, listAttachments } from '#/api/system/storage';
// 1. ä¸Šä¼ æ–‡ä»¶
const blobs = await uploadFile([file]);
const blobId = blobs[0].id;
// 2. ä¿å­˜ä¸šåŠ¡è®°å½•åŽï¼Œç»‘å®šé™„ä»¶
await bindAttachments({
  application: 'file',
  recordType: 'erp_purchase_order',
  recordId: savedRecord.id,
  blobItems: [{ blobId }],
});
// 3. æŸ¥è¯¢é™„ä»¶
const attachments = await listAttachments({
  recordType: 'erp_purchase_order',
  recordId: savedRecord.id,
});
```
### recordType å‘½åè§„范
格式:`{模块前缀}_{业务表名}`
| æ¨¡å— | recordType |
|------|------------|
| ERP é‡‡è´­è®¢å• | `erp_purchase_order` |
| ERP é‡‡è´­ç”³è¯· | `erp_purchase_request` |
| ERP é”€å”®è®¢å• | `erp_sale_order` |
| ERP æ”¶æ¬¾å• | `erp_finance_receipt` |
| ERP ä»˜æ¬¾å• | `erp_finance_payment` |
| HRM è¯·å‡ | `hrm_leave_application` |
| HRM ç¦»èŒ | `hrm_resignation_application` |
| HRM è°ƒåЍ | `hrm_transfer_application` |
| MES é‡‡è´­è®¢å• | `mes_purchase_order` |
| MES å·¥å• | `mes_pro_work_order` |
| CRM å®¢æˆ· | `crm_customer` |
## AI è‡ªæŸ¥æ¸…单
在实现任何文件上传相关需求前,逐项确认:
- [ ] æ˜¯å¦ä½¿ç”¨äº† `/system/storage-blob/upload` ä¸Šä¼ ï¼Ÿ
- [ ] æ˜¯å¦ä½¿ç”¨äº† `/system/storage-attachment/bind` å…³è”?
- [ ] æ˜¯å¦æ²¡æœ‰åœ¨ä¸šåŠ¡è¡¨ä¸­æ·»åŠ æ–‡ä»¶åœ°å€åˆ—ï¼Ÿ
- [ ] æ˜¯å¦æ²¡æœ‰åœ¨ DO/VO ä¸­æ·»åŠ æ–‡ä»¶åœ°å€å­—æ®µï¼Ÿ
- [ ] æ˜¯å¦æ²¡æœ‰åˆ›å»ºæ–°çš„ upload Controller æŽ¥å£ï¼Ÿ
- [ ] æ˜¯å¦æ²¡æœ‰ä½¿ç”¨ infra æ¨¡å—çš„ FileApi?
以上 6 é¡¹å…¨éƒ¨ä¸º"是"才可继续。
## ä¸ºä»€ä¹ˆ
- å­˜å‚¨é…ç½®ï¼ˆæœ¬åœ°/S3)在 system æ¨¡å—统一管理
- é™„件绑定机制(storage-attachment)提供 `recordType + recordId` çš„æ ‡å‡†å…³è”方式,支持多文件、用途分类
- infra æ¨¡å—çš„ `/infra/file/*` æ˜¯æ—§ç‰ˆæŽ¥å£ï¼Œå­˜åœ¨æ–‡ä»¶ä¸Žä¸šåŠ¡è®°å½•æ— æ³•å…³è”çš„é—®é¢˜
- ä¸šåŠ¡è¡¨å­˜å‚¨ file_url å­—符串无法支持多文件,且文件与业务记录之间缺乏标准化的关联管理
.agents/rules/frontend-doc-generation.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
---
name: frontend-doc-generation
description: å‰ç«¯è”调方案生成规则
metadata:
  type: feedback
---
# å‰ç«¯è”调方案规则
## è§„则
后端接口变更时,**不生成前端代码**,只生成前端联调方案文档,输出到 `docs/` ç›®å½•。
## ç¦æ­¢å†…容
- âŒ ç¦æ­¢å†™ Vue æ¨¡æ¿ä»£ç ï¼ˆ`<el-table-column>`、`<el-form-item>` ç­‰ï¼‰
- âŒ ç¦æ­¢å†™ JavaScript/TypeScript ä»£ç ï¼ˆæ–¹æ³•、函数、逻辑判断等)
- âŒ ç¦æ­¢å†™ CSS æ ·å¼ä»£ç 
- âŒ ç¦æ­¢å†™å‰ç«¯ç»„件使用示例
## è”调方案内容要求
文档应只包含以下内容:
- **API è¯´æ˜Ž**:路径、方法、请求参数、响应格式
- **涉及的前端页面**:列出需要修改的页面名称
- **数据带入说明**:重点强调信息如何从上一步带入(如采购订单 -> åˆ°è´§é€šçŸ¥ -> é‡‡è´­å…¥åº“)
- **接口调用时序**:说明何时调用哪个接口获取关联数据
- **字段展示规则**:用表格说明字段展示位置和说明,不写代码
- **业务规则说明**:用文字或表格说明业务逻辑
- **注意事项**:需要前端注意的边界情况
## æ–‡æ¡£æ¨¡æ¿
```markdown
# åŠŸèƒ½åç§° - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é¡µé¢1
- é¡µé¢2
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. æ­¥éª¤1 -> æ­¥éª¤2:字段A、字段B ä»Žæ­¥éª¤1自动带入
2. æ­¥éª¤2 -> æ­¥éª¤3:字段C ä»Žæ­¥éª¤2自动带入
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /xxx/xxx | è¯´æ˜Ž |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| xxx | String | æ˜¯ | xxx |
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| xxx | String | xxx |
**响应示例:**
```json
{
  "code": 0,
  "data": {}
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å­—段A | åˆ—表、详情 | è¯´æ˜Ž |
| å­—段B | è¯¦æƒ… | è¯´æ˜Ž |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| åœºæ™¯1 | è§„则说明 |
| åœºæ™¯2 | è§„则说明 |
## æ³¨æ„äº‹é¡¹
- æ³¨æ„äº‹é¡¹1
- æ³¨æ„äº‹é¡¹2
```
.agents/rules/frontend-docs.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
# æŽ¥å£å˜æ›´å‰ç«¯è”调方案规则
## è§„则
- æ¯æ¬¡æ¶‰åŠæŽ¥å£å˜æ›´ï¼ˆæ–°å¢žæŽ¥å£ã€ä¿®æ”¹è¯·æ±‚参数、修改响应结构)时,必须输出一份**前端联调方案**到 `docs/` ç›®å½•。
- **不生成前端代码**,只生成联调方案文档,指导前端开发人员如何对接。
- æ–‡æ¡£ä»¥ Markdown æ ¼å¼ç¼–写,文件名使用小写字母和下划线,如 `docs/purchase_receipt_integration.md`。
- æ–‡æ¡£å†…容重点强调**信息带入**问题:哪些数据从上一步自动带入、何时获取关联数据、字段如何映射。
## æ–‡æ¡£æ¨¡æ¿
```markdown
# åŠŸèƒ½åç§°
## æ¶‰åŠé¡µé¢
- é¡µé¢1
- é¡µé¢2
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /xxx/xxx | è¯´æ˜Ž |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| xxx | String | æ˜¯ | xxx |
**响应:** `{ "code": 200, "msg": "操作成功" }`
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æŒ‰é’®/弹窗/表格等
\`\`\`html
<el-button>xxx</el-button>
\`\`\`
### 2. data æ•°æ®
\`\`\`js
data() {
  return {
    xxx: '',
  }
}
\`\`\`
### 3. æ–¹æ³•
\`\`\`js
handleXxx() {
  // ...
}
\`\`\`
## æ³¨æ„äº‹é¡¹
- æ³¨æ„äº‹é¡¹1
- æ³¨æ„äº‹é¡¹2
```
.agents/rules/frontend-project.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
# å‰ç«¯é¡¹ç›®è§„则
## è§„则
- `mom-pro2-before` æ˜¯å‰ç«¯é¡¹ç›®ï¼Œä½äºŽåŽç«¯é¡¹ç›®æ ¹ç›®å½•下(`D:\牛马\南通\后端\mom-pro2-after\mom-pro2-before`)。
- å¯ä»¥æŸ¥çœ‹å‰ç«¯ä»£ç ä½œä¸ºå‚考,帮助理解业务逻辑、设计后端方案。
- **未经用户明确允许,禁止修改前端项目中的任何文件。**
## ä½¿ç”¨åœºæ™¯
| å…è®¸ | ç¦æ­¢ |
|------|------|
| è¯»å–前端文件了解现有实现 | ä¿®æ”¹å‰ç«¯ä»£ç  |
| å‚考前端 API è°ƒç”¨æ–¹å¼è®¾è®¡åŽç«¯æŽ¥å£ | æ–°å¢žå‰ç«¯ç»„ä»¶/页面 |
| å‚考前端数据结构理解业务 | åˆ é™¤å‰ç«¯æ–‡ä»¶ |
| åœ¨å‰ç«¯é¡¹ç›®ä¸­æœç´¢/查找代码 | é‡æž„前端代码 |
.agents/rules/handover-data-query.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
# æ•°æ®äº¤æŽ¥æŸ¥è¯¢è§„则
## è§„则
所有模块在查询"我的数据"(按用户 ID è¿‡æ»¤çš„业务数据列表)时,**必须调用 `HrmUserHandoverApi.expandUserIds()`** æ‰©å±•用户 ID é›†åˆï¼Œç¡®ä¿äº¤æŽ¥äººèƒ½æŸ¥çœ‹å·²ç¦»èŒäº¤æŽ¥äººçš„业务数据。
## èƒŒæ™¯
当员工离职时,其业务数据(客户、商机、工单、采购申请等)需要交由交接人管理。系统通过 `hrm_user_handover` æ˜ å°„表记录了离职用户与交接目标用户的关联,而非直接修改业务数据的所有者字段。
查询时通过 `expandUserIds` å°†äº¤æŽ¥æºç”¨æˆ· ID çº³å…¥æŸ¥è¯¢èŒƒå›´ï¼Œä½¿äº¤æŽ¥äººèƒ½åŒæ—¶çœ‹åˆ°è‡ªå·±å’Œç¦»èŒäººçš„æ•°æ®ã€‚
## æ ¸å¿ƒ API
```java
// æŽ¥å£ï¼šHrmUserHandoverApi (yudao-module-hrm-api)
public interface HrmUserHandoverApi {
    Set<Long> expandUserIds(Set<Long> userIds);
}
```
## å„模块集成方式
### åŸºæœ¬ç”¨æ³•
```java
// æ³¨å…¥ API
@Resource
private HrmUserHandoverApi handoverApi;
// åœ¨æŸ¥è¯¢å‰æ‰©å±• userId
public PageResult<XxxDO> getXxxPage(XxxPageReqVO reqVO) {
    Long currentUserId = getCurrentUserId();
    Set<Long> expandedUserIds = handoverApi.expandUserIds(Set.of(currentUserId));
    // ç”¨ expandedUserIds åš IN æŸ¥è¯¢
    return mapper.selectPage(reqVO, new LambdaQueryWrapperX<XxxDO>()
            .in(XxxDO::getOwnerUserId, expandedUserIds)
            ...);
}
```
### Mapper å±‚写法
```java
// å¦‚æžœ userId æ˜¯åŠ¨æ€ä¼ å…¥çš„ï¼ˆå¦‚ç®¡ç†å‘˜å¯æŸ¥çœ‹æ‰€æœ‰äººçš„æ•°æ®ï¼‰ï¼Œå…ˆåšåˆ¤æ–­ï¼š
default PageResult<XxxDO> selectPage(XxxPageReqVO reqVO, Set<Long> userIds) {
    return selectPage(reqVO, new LambdaQueryWrapperX<XxxDO>()
            .inIfPresent(XxxDO::getOwnerUserId, CollUtil.isNotEmpty(userIds) ? userIds : null)
            ...);
}
```
## å„模块涉及的查询场景
| æ¨¡å— | æŸ¥è¯¢åœºæ™¯ | æ¶‰åŠè¡¨ | ç”¨æˆ·å­—段 |
|------|----------|--------|----------|
| CRM | æˆ‘的客户 | `crm_customer` | `ownerUserId` |
| CRM | æˆ‘的线索 | `crm_clue` | `ownerUserId` |
| CRM | æˆ‘的联系人 | `crm_contact` | `ownerUserId` |
| CRM | æˆ‘的合同 | `crm_contract` | `ownerUserId` |
| CRM | æˆ‘的商机 | `crm_business` | `ownerUserId` |
| CRM | æˆ‘的回款 | `crm_receivable` | `ownerUserId` |
| CRM | æˆ‘的回款计划 | `crm_receivable_plan` | `ownerUserId` |
| CRM | æˆ‘的报价 | `crm_sale_quotation` | `ownerUserId` |
| ERP | æˆ‘的采购申请 | `erp_purchase_request` | `requestUserId` |
| ERP | æˆ‘的销售订单 | `erp_sale_order` | `saleUserId` |
| ERP | æˆ‘的付款单 | `erp_finance_payment` | `financeUserId` |
| ERP | æˆ‘的收款单 | `erp_finance_receipt` | `financeUserId` |
| MES | æˆ‘的工单/报工/质检等 | å„ MES è¡¨ | `userId` / `inspectorUserId` / `chargeUserId` ç­‰ |
## expandUserIds åŽŸç†
```
hrm_user_handover è¡¨è®°å½•:
┌────┬──────────────┬────────────┬───────────────┬─────────────────┐
│ id â”‚ from_user_id â”‚ to_user_id â”‚ resignation_id│ handover_status â”‚
├────┼──────────────┼────────────┼───────────────┼─────────────────┤
│ 1  â”‚ 100(离职人)  â”‚ 101(交接人)│ 1             â”‚ 20(已完成)       â”‚
└────┴──────────────┴────────────┴───────────────┴─────────────────┘
expandUserIds({101}) â†’ {101, 100}
→ äº¤æŽ¥äºº101查询时:WHERE owner_user_id IN (101, 100)
→ åŒæ—¶çœ‹åˆ°è‡ªå·±çš„和离职人的数据
```
## æ³¨æ„äº‹é¡¹
- `expandUserIds` åªæ‰©å±•**已完成的交接记录**(`handoverStatus = 20`)
- ä¼ å…¥ç©ºé›†åˆè¿”回空集合
- æŸ¥è¯¢æ€§èƒ½ï¼šæ˜ å°„表数据量极小(仅为已离职人员数),可放心在每次查询前调用
- å¦‚果查询要求严格只看本人数据(如个人设置页),则不应调用 `expandUserIds`
## Maven ä¾èµ–
需要在模块的 `pom.xml` ä¸­æ·»åР坹 HRM API çš„依赖:
```xml
<dependency>
    <groupId>cn.iocoder.boot</groupId>
    <artifactId>yudao-module-hrm-api</artifactId>
    <version>${revision}</version>
</dependency>
```
.agents/rules/powershell-commands.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
# PowerShell å‘½ä»¤è§„则
## è§„则
由于项目运行在 Windows çŽ¯å¢ƒä¸‹ï¼Œç”¨æˆ·ç»ˆç«¯é»˜è®¤ä½¿ç”¨ PowerShell è€Œéž bash。在提供命令行指导时,必须使用 PowerShell è¯­æ³•。
## å¸¸ç”¨å‘½ä»¤å¯¹ç…§
| æ“ä½œ | Bash | PowerShell |
|------|------|-------------|
| åˆ é™¤ç›®å½• | `rm -rf <path>` | `Remove-Item -Recurse -Force <path>` æˆ–简写 `ri -r -fo <path>` |
| å¤åˆ¶æ–‡ä»¶ | `cp <src> <dest>` | `Copy-Item <src> <dest>` æˆ–简写 `cp <src> <dest>` |
| ç§»åŠ¨æ–‡ä»¶ | `mv <src> <dest>` | `Move-Item <src> <dest>` æˆ–简写 `mv <src> <dest>` |
| åˆ›å»ºç›®å½• | `mkdir -p <path>` | `New-Item -ItemType Directory -Path <path>` æˆ–简写 `md <path>` |
| æŸ¥çœ‹æ–‡ä»¶ | `cat <file>` | `Get-Content <file>` æˆ–简写 `gc <file>` |
| åˆ—出文件 | `ls` | `Get-ChildItem` æˆ–简写 `ls`/`dir` |
| æŸ¥æ‰¾æ–‡ä»¶ | `find . -name "*.java"` | `Get-ChildItem -Recurse -Filter "*.java"` æˆ–简写 `gci -r -fi "*.java"` |
| æŸ¥æ‰¾å†…容 | `grep -r "pattern" .` | `Select-String -Path "*.java" -Pattern "pattern"` æˆ–简写 `sls "pattern" *.java -r` |
## åˆ é™¤ç›®å½•示例
```powershell
# æŽ¨èï¼šä½¿ç”¨ Remove-Item å®Œæ•´å‘½ä»¤
Remove-Item -Recurse -Force yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/client
# ç®€å†™å½¢å¼
ri -r -fo yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/client
```
## æ³¨æ„äº‹é¡¹
- PowerShell å‚数使用 `-` å‰ç¼€ï¼Œå¦‚ `-Recurse`、`-Force`
- å‚数可以简写首字母,如 `-r`、`-fo`
- è·¯å¾„分隔符在 PowerShell ä¸­å¯ä»¥ä½¿ç”¨ `/` æˆ– `\`,推荐使用 `/` ä»¥ä¿æŒè·¨å¹³å°å…¼å®¹
- é¿å… `-rf` è¿™æ ·çš„ bash é£Žæ ¼å‚数组合,PowerShell ä¸æ”¯æŒ
.agents/rules/security-guardrails.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
# å®‰å…¨æŠ¤æ 
## SQL æ³¨å…¥é˜²æŠ¤
- æ°¸è¿œä¸è¦å°†ç”¨æˆ·è¾“入直接拼接到 SQL æŸ¥è¯¢ä¸­ã€‚
- å§‹ç»ˆä½¿ç”¨å‚数化查询或 ORM æ–¹æ³•。
- åœ¨æ•°æ®åº“操作前验证和清理所有输入。
## XSS é˜²æŠ¤
- åœ¨ HTML ä¸­æ¸²æŸ“前转义所有用户生成的内容。
- ä½¿ç”¨æ¨¡æ¿è‡ªåŠ¨è½¬ä¹‰åŠŸèƒ½ã€‚
- åœ¨å®¢æˆ·ç«¯å’ŒæœåŠ¡å™¨ç«¯éƒ½è¿›è¡Œè¾“å…¥éªŒè¯å’Œæ¸…ç†ã€‚
## å¯†é’¥ç®¡ç†
- æ°¸è¿œä¸è¦åœ¨æºä»£ç ä¸­ç¡¬ç¼–码密钥、API å¯†é’¥æˆ–密码。
- ä½¿ç”¨çŽ¯å¢ƒå˜é‡æˆ–å®‰å…¨ä¿é™©åº“å­˜å‚¨æ•æ„Ÿæ•°æ®ã€‚
- å®šæœŸè½®æ¢å¯†é’¥ï¼Œé¿å…åœ¨æ—¥å¿—中记录它们。
## è¾“入验证
- åœ¨ç³»ç»Ÿè¾¹ç•Œå¤„验证所有输入。
- ä½¿ç”¨ç™½åå•而非黑名单进行验证。
- åœ¨å¤„理上传前检查文件类型和大小。
## é”™è¯¯å¤„理
- ä¸è¦åœ¨é”™è¯¯æ¶ˆæ¯ä¸­æš´éœ²æ•æ„Ÿä¿¡æ¯ã€‚
- åœ¨å†…部记录详细错误,向用户返回通用消息。
- å¤„理所有异常以防止信息泄露。
## ä¾èµ–管理
- ä¿æŒä¾èµ–项更新。
- å®¡è®¡ä¾èµ–项中的已知漏洞。
- å›ºå®šç‰ˆæœ¬ä»¥é¿å…æ„å¤–的破坏性变更。
.agents/rules/sql-docs-sync.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
---
description: Automatically sync new SQL files to docs/sql and update the index
---
# SQL Files Management Rule
**CRITICAL RULE**: Whenever you create or add a new SQL file (e.g. database migrations, init scripts) in this project (such as in `sql/mysql/` or anywhere else), you MUST:
1. **Copy/Create a duplicate** of that SQL file in the `docs/sql/` directory.
2. **Update the SQL Index**: Record the newly added SQL file in `docs/sql/README.md` (create the README file if it does not exist) with a brief description of what the SQL file does.
Always remember this when you are generating new SQL scripts to maintain the project's documentation consistency.
.agents/rules/sql-script.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
# SQL è„šæœ¬è§„èŒƒ
## è§„则
- SQL è„šæœ¬ä¸­ä¸è¦åŒ…å« tenant_id å­—段,项目不使用多租户模式。
- INSERT è¯­å¥åªåŒ…含必要的业务字段,不需要 tenant_id。
.agents/rules/sql-syntax.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
# SQL è¯­æ³•规范
## è§„则
- MySQL `ALTER TABLE ADD COLUMN` ä¸æ”¯æŒ `IF NOT EXISTS` è¯­æ³•,即使 MySQL 8.0+ ä¹Ÿä¸æ”¯æŒã€‚
- æ·»åŠ å­—æ®µå‰éœ€è¦å…ˆæ£€æŸ¥å­—æ®µæ˜¯å¦å­˜åœ¨ï¼Œæˆ–ç›´æŽ¥æ‰§è¡Œå¹¶å¿½ç•¥"字段已存在"错误。
## æ­£ç¡®ç¤ºä¾‹
```sql
-- æ­£ç¡®ï¼šç›´æŽ¥æ·»åŠ å­—æ®µ
ALTER TABLE mes_wm_sn ADD COLUMN uuid VARCHAR(64) COMMENT '批次UUID' AFTER id;
-- æ­£ç¡®ï¼šå…ˆæ£€æŸ¥å†æ·»åŠ ï¼ˆå…¼å®¹è„šæœ¬ï¼‰
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_sn' AND column_name = 'uuid');
SET @sql = IF(@exists = 0,
              'ALTER TABLE mes_wm_sn ADD COLUMN uuid VARCHAR(64) COMMENT ''批次UUID'' AFTER id',
              'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
```
## é”™è¯¯ç¤ºä¾‹
```sql
-- é”™è¯¯ï¼šADD COLUMN ä¸æ”¯æŒ IF NOT EXISTS
ALTER TABLE mes_wm_sn ADD COLUMN IF NOT EXISTS uuid VARCHAR(64) COMMENT '批次UUID' AFTER id;
```
## IF NOT EXISTS æ”¯æŒåœºæ™¯
| è¯­å¥ | æ˜¯å¦æ”¯æŒ |
|------|---------|
| CREATE TABLE IF NOT EXISTS | âœ… æ”¯æŒ |
| CREATE INDEX IF NOT EXISTS | âœ… æ”¯æŒï¼ˆMySQL 8.0+) |
| ALTER TABLE ADD COLUMN IF NOT EXISTS | âŒ ä¸æ”¯æŒ |
| ALTER TABLE ADD INDEX IF NOT EXISTS | âœ… æ”¯æŒï¼ˆMySQL 8.0+) |
| ALTER TABLE ADD CONSTRAINT IF NOT EXISTS | âŒ ä¸æ”¯æŒ |
docs/sql/README.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
# SQL è„šæœ¬ç´¢å¼•
本项目包含的 SQL è„šæœ¬æ±‡æ€»å¦‚ä¸‹ï¼š
- [mes_pro_task_assignee.sql](mes_pro_task_assignee.sql): å¢žåŠ ç”Ÿäº§ä»»åŠ¡æŠ¥å·¥äººå‘˜å…³è”è¡¨ï¼Œç”¨äºŽå¤šäººå‘˜æŠ¥å·¥åˆ†é…ã€‚
docs/sql/mes_pro_task_assignee.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
CREATE TABLE `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 '是否删除',
  `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租户编号',
  PRIMARY KEY (`id`),
  KEY `idx_task_id` (`task_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生产任务指定报工人员表';
sql/mysql/mes_pro_task_assignee.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
CREATE TABLE `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 '是否删除',
  `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租户编号',
  PRIMARY KEY (`id`),
  KEY `idx_task_id` (`task_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生产任务指定报工人员表';
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
@@ -349,6 +349,15 @@
                inTransitMap.merge(fb.getTaskId(), fbQty, BigDecimal::add);
            }
        }
        // æŸ¥è¯¢æŒ‡æ´¾æŠ¥å·¥äººå‘˜
        Set<Long> taskIds = convertSet(list, MesProTaskDO::getId);
        Map<Long, List<cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO>> assigneeMap = taskService.getTaskAssigneeMap(taskIds);
        Set<Long> allAssigneeUserIds = new HashSet<>();
        assigneeMap.values().forEach(assignees ->
            assignees.forEach(assignee -> allAssigneeUserIds.add(assignee.getUserId())));
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(allAssigneeUserIds);
        // æ‹¼æŽ¥ VO
        return convertList(list, task -> {
            MesProTaskRespVO vo = BeanUtils.toBean(task, MesProTaskRespVO.class);
@@ -379,6 +388,22 @@
                    findAndThen(processCheckMap, task.getProcessId(), vo::setCheckFlag));
            findAndThen(routeProcessBackflushFlagMap, task.getRouteId(), processBackflushMap ->
                    findAndThen(processBackflushMap, task.getProcessId(), vo::setBackflushFlag));
            // è®¾ç½®æŒ‡æ´¾æŠ¥å·¥äººå‘˜
            List<cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO> assignees = assigneeMap.get(task.getId());
            if (CollUtil.isNotEmpty(assignees)) {
                List<Long> assigneeUserIds = convertList(assignees, cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO::getUserId);
                vo.setAssigneeUserIds(assigneeUserIds);
                List<String> assigneeUserNames = new ArrayList<>();
                for (Long userId : assigneeUserIds) {
                    AdminUserRespDTO user = userMap.get(userId);
                    if (user != null && user.getNickname() != null) {
                        assigneeUserNames.add(user.getNickname());
                    }
                }
                vo.setAssigneeUserNames(String.join(",", assigneeUserNames));
            }
            return vo;
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java
@@ -134,4 +134,10 @@
    @Schema(description = "停工历史记录")
    private List<MesProTaskPauseRecordRespVO> pauseRecords;
    @Schema(description = "指定报工人员 ID é›†åˆ")
    private List<Long> assigneeUserIds;
    @Schema(description = "指定报工人员姓名集合")
    private List<String> assigneeUserNames;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskSaveReqVO.java
@@ -51,4 +51,7 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "指定报工人员 ID é›†åˆ")
    private java.util.List<Long> assigneeUserIds;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/task/MesProTaskAssigneeDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.pro.task;
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.*;
/**
 * ç”Ÿäº§ä»»åŠ¡æŒ‡å®šæŠ¥å·¥äººå‘˜è¡¨ DO
 *
 * @author è‡ªåŠ¨ç”Ÿæˆ
 */
@TableName("mes_pro_task_assignee")
@KeySequence("mes_pro_task_assignee_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesProTaskAssigneeDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * ç”Ÿäº§ä»»åŠ¡ ID
     *
     * å…³è” {@link MesProTaskDO#getId()}
     */
    private Long taskId;
    /**
     * ç³»ç»Ÿç”¨æˆ· ID (指定报工人员)
     */
    private Long userId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskAssigneeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.task;
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.task.MesProTaskAssigneeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * ç”Ÿäº§ä»»åŠ¡æŒ‡å®šæŠ¥å·¥äººå‘˜ Mapper
 *
 * @author è‡ªåŠ¨ç”Ÿæˆ
 */
@Mapper
public interface MesProTaskAssigneeMapper extends BaseMapperX<MesProTaskAssigneeDO> {
    default List<MesProTaskAssigneeDO> selectListByTaskId(Long taskId) {
        return selectList(MesProTaskAssigneeDO::getTaskId, taskId);
    }
    default List<MesProTaskAssigneeDO> selectListByTaskIds(Collection<Long> taskIds) {
        return selectList(new LambdaQueryWrapperX<MesProTaskAssigneeDO>()
                .inIfPresent(MesProTaskAssigneeDO::getTaskId, taskIds));
    }
    default void deleteByTaskId(Long taskId) {
        delete(MesProTaskAssigneeDO::getTaskId, taskId);
    }
    default void deleteByTaskIdAndUserIds(Long taskId, Collection<Long> userIds) {
        delete(new LambdaQueryWrapperX<MesProTaskAssigneeDO>()
                .eq(MesProTaskAssigneeDO::getTaskId, taskId)
                .in(MesProTaskAssigneeDO::getUserId, userIds));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java
@@ -176,4 +176,23 @@
     */
    void resumeTask(cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskResumeReqVO resumeReqVO);
    /**
     * èŽ·å–ç”Ÿäº§ä»»åŠ¡çš„æŒ‡å®šæŠ¥å·¥äººå‘˜åˆ—è¡¨ï¼ŒæŒ‰ä»»åŠ¡ç¼–å·åˆ†ç»„
     *
     * @param taskIds ä»»åŠ¡ç¼–å·é›†åˆ
     * @return ä»»åŠ¡å…³è”çš„äººå‘˜åˆ—è¡¨ Map
     */
    default Map<Long, List<cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO>> getTaskAssigneeMap(Collection<Long> taskIds) {
        if (cn.hutool.core.collection.CollUtil.isEmpty(taskIds)) {
            return java.util.Collections.emptyMap();
        }
        return cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap(
                getTaskAssigneeListByTaskIds(taskIds), cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO::getTaskId);
    }
    /**
     * æ ¹æ®ä»»åŠ¡ç¼–å·é›†åˆèŽ·å–å…³è”äººå‘˜åˆ—è¡¨
     */
    List<cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO> getTaskAssigneeListByTaskIds(Collection<Long> taskIds);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -58,6 +58,8 @@
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskPauseRecordDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskPauseRecordMapper;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskAssigneeMapper;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPauseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskResumeReqVO;
@@ -78,6 +80,9 @@
    @Resource
    private MesProTaskPauseRecordMapper taskPauseRecordMapper;
    @Resource
    private MesProTaskAssigneeMapper taskAssigneeMapper;
    @Resource
    @Lazy
@@ -141,6 +146,10 @@
        }
        // 2.2 æ’å…¥
        taskMapper.insert(task);
        // 2.3 ä¿å­˜æŒ‡å®šæŠ¥å·¥äººå‘˜
        updateTaskAssignees(task.getId(), createReqVO.getAssigneeUserIds());
        return task.getId();
    }
@@ -173,6 +182,34 @@
        MesProTaskDO updateObj = BeanUtils.toBean(updateReqVO, MesProTaskDO.class)
                .setName(buildTaskName(item, quantity));
        taskMapper.updateById(updateObj);
        // 3. æ›´æ–°æŒ‡å®šæŠ¥å·¥äººå‘˜ï¼ˆæ¯”对更新)
        updateTaskAssignees(task.getId(), updateReqVO.getAssigneeUserIds());
    }
    private void updateTaskAssignees(Long taskId, List<Long> assigneeUserIds) {
        if (assigneeUserIds == null) {
            assigneeUserIds = Collections.emptyList();
        }
        // 1. æŸ¥è¯¢æ•°æ®åº“中原有的
        List<MesProTaskAssigneeDO> oldAssignees = taskAssigneeMapper.selectListByTaskId(taskId);
        Set<Long> oldUserIds = convertSet(oldAssignees, MesProTaskAssigneeDO::getUserId);
        Set<Long> newUserIds = new java.util.HashSet<>(assigneeUserIds);
        // 2. è®¡ç®—需要新增的和需要删除的
        Set<Long> toAddUserIds = newUserIds.stream().filter(id -> !oldUserIds.contains(id)).collect(java.util.stream.Collectors.toSet());
        Set<Long> toRemoveUserIds = oldUserIds.stream().filter(id -> !newUserIds.contains(id)).collect(java.util.stream.Collectors.toSet());
        // 3. æ–°å¢ž
        if (CollUtil.isNotEmpty(toAddUserIds)) {
            List<MesProTaskAssigneeDO> addList = convertList(toAddUserIds, userId -> new MesProTaskAssigneeDO().setTaskId(taskId).setUserId(userId));
            taskAssigneeMapper.insertBatch(addList);
        }
        // 4. åˆ é™¤
        if (CollUtil.isNotEmpty(toRemoveUserIds)) {
            taskAssigneeMapper.deleteByTaskIdAndUserIds(taskId, toRemoveUserIds);
        }
    }
    /**
@@ -245,8 +282,10 @@
        if (feedbackMapper.selectCountByTaskId(id) > 0) {
            throw exception(PRO_TASK_HAS_FEEDBACK);
        }
        // 3. åˆ é™¤
        // 3. åˆ é™¤ä»»åŠ¡ä¸»è¡¨
        taskMapper.deleteById(id);
        // 4. åˆ é™¤å…³è”的人员
        taskAssigneeMapper.deleteByTaskId(id);
    }
    @Override
@@ -273,6 +312,14 @@
    }
    @Override
    public List<MesProTaskAssigneeDO> getTaskAssigneeListByTaskIds(Collection<Long> taskIds) {
        if (CollUtil.isEmpty(taskIds)) {
            return Collections.emptyList();
        }
        return taskAssigneeMapper.selectListByTaskIds(taskIds);
    }
    @Override
    public MesProTaskDO validateTaskExists(Long id) {
        MesProTaskDO task = taskMapper.selectById(id);
        if (task == null) {