liu
20 小时以前 1f7c36424e8f48a5917ac6c77e2e47979c469a28
Merge remote-tracking branch 'origin/dev_business' into dev_business
已添加22个文件
已修改31个文件
1730 ■■■■■ 文件已修改
.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 | 历史
.gitignore 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/projectSql/ruoyi-vue-pro.sql 111 ●●●● 补丁 | 查看 | 原始文档 | 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-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackRespVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackSubmitReqVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | 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/controller/admin/pro/workorder/MesProWorkOrderController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderSaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/feedback/MesProFeedbackDO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | 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/dataobject/pro/workorder/MesProWorkOrderDO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/mps/MesProMpsMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | 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/dal/mysql/pro/task/MesProTaskMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/common/bpm/MesBpmProcessService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/common/bpm/MesBpmProcessServiceImpl.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectServiceImpl.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/listener/MesProFeedbackStatusListener.java 74 ●●●●● 补丁 | 查看 | 原始文档 | 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 | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-wtxc.yaml 222 ●●●●● 补丁 | 查看 | 原始文档 | 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 | âŒ ä¸æ”¯æŒ |
.gitignore
@@ -58,3 +58,6 @@
.claude/settings.local.json
.claude/worktrees/
.claude/scheduled_tasks.json
graphify-out/
.agents/
docs/projectSql/ruoyi-vue-pro.sql
@@ -7935,38 +7935,39 @@
DROP TABLE IF EXISTS `mes_pro_feedback`;
CREATE TABLE `mes_pro_feedback`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '报工单编号',
  `type` int NULL DEFAULT NULL COMMENT '报工类型',
  `channel` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '报工途径',
  `feedback_time` datetime NULL DEFAULT NULL COMMENT '报工时间',
  `workstation_id` bigint NULL DEFAULT NULL COMMENT '工作站编号',
  `route_id` bigint NULL DEFAULT NULL COMMENT '工艺路线编号',
  `process_id` bigint NULL DEFAULT NULL COMMENT '工序编号',
  `work_order_id` bigint NULL DEFAULT NULL COMMENT '生产工单编号',
  `task_id` bigint NULL DEFAULT NULL COMMENT '生产任务编号',
  `item_id` bigint NULL DEFAULT NULL COMMENT '产品物料编号',
  `expire_date` datetime NULL DEFAULT NULL COMMENT '过期日期',
  `lot_number` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '生产批号',
  `scheduled_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '排产数量',
  `feedback_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '本次报工数量',
  `qualified_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '合格品数量',
  `unqualified_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '不良品数量',
  `uncheck_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '待检测数量',
  `labor_scrap_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '工废数量',
  `material_scrap_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '料废数量',
  `other_scrap_quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '其他废品数量',
  `feedback_user_id` bigint NULL DEFAULT NULL COMMENT '报工用户编号',
  `approve_user_id` bigint NULL DEFAULT NULL COMMENT '审核用户编号',
  `status` 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 '创建者',
  `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '报工单编号',
  `type` int DEFAULT NULL COMMENT '报工类型',
  `channel` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '报工途径',
  `feedback_time` datetime DEFAULT NULL COMMENT '报工时间',
  `workstation_id` bigint DEFAULT NULL COMMENT '工作站编号',
  `route_id` bigint DEFAULT NULL COMMENT '工艺路线编号',
  `process_id` bigint DEFAULT NULL COMMENT '工序编号',
  `work_order_id` bigint DEFAULT NULL COMMENT '生产工单编号',
  `task_id` bigint DEFAULT NULL COMMENT '生产任务编号',
  `item_id` bigint DEFAULT NULL COMMENT '产品物料编号',
  `expire_date` datetime DEFAULT NULL COMMENT '过期日期',
  `lot_number` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '生产批号',
  `scheduled_quantity` decimal(24,6) DEFAULT NULL COMMENT '排产数量',
  `feedback_quantity` decimal(24,6) DEFAULT NULL COMMENT '本次报工数量',
  `qualified_quantity` decimal(24,6) DEFAULT NULL COMMENT '合格品数量',
  `unqualified_quantity` decimal(24,6) DEFAULT NULL COMMENT '不良品数量',
  `uncheck_quantity` decimal(24,6) DEFAULT NULL COMMENT '待检测数量',
  `labor_scrap_quantity` decimal(24,6) DEFAULT NULL COMMENT '工废数量',
  `material_scrap_quantity` decimal(24,6) DEFAULT NULL COMMENT '料废数量',
  `other_scrap_quantity` decimal(24,6) DEFAULT NULL COMMENT '其他废品数量',
  `feedback_user_id` bigint DEFAULT NULL COMMENT '报工用户编号',
  `approve_user_id` bigint DEFAULT NULL COMMENT '审核用户编号',
  `status` int DEFAULT NULL COMMENT '状态',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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 NULL DEFAULT '' 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
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES ç”Ÿäº§æŠ¥å·¥' ROW_FORMAT = Dynamic;
  `process_instance_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM æµç¨‹å®žä¾‹ç¼–号',
  `process_definition_key` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM æµç¨‹å®šä¹‰ Key',
  PRIMARY KEY (`id`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=79 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='MES ç”Ÿäº§æŠ¥å·¥';
-- ----------------------------
-- Records of mes_pro_feedback
-- ----------------------------
@@ -8222,34 +8223,38 @@
DROP TABLE IF EXISTS `mes_pro_work_order`;
CREATE TABLE `mes_pro_work_order`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '工单编码',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '工单名称',
  `type` int NULL DEFAULT NULL COMMENT '工单类型',
  `order_source_type` int NULL DEFAULT NULL COMMENT '来源类型',
  `order_source_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '来源单据编号',
  `product_id` bigint NULL DEFAULT NULL COMMENT '产品编号',
  `quantity` decimal(24, 6) NULL DEFAULT NULL COMMENT '生产数量',
  `quantity_produced` decimal(24, 6) NULL DEFAULT NULL COMMENT '已生产数量',
  `quantity_changed` decimal(24, 6) NULL DEFAULT NULL COMMENT '调整数量',
  `quantity_scheduled` decimal(24, 6) NULL DEFAULT NULL COMMENT '已排产数量',
  `client_id` bigint NULL DEFAULT NULL COMMENT '客户编号',
  `vendor_id` bigint NULL DEFAULT NULL COMMENT '供应商编号',
  `batch_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '批次号',
  `request_date` datetime NULL DEFAULT NULL COMMENT '需求日期',
  `parent_id` bigint NULL DEFAULT NULL COMMENT '父工单编号',
  `finish_date` datetime NULL DEFAULT NULL COMMENT '完成时间',
  `cancel_date` datetime NULL DEFAULT NULL COMMENT '取消时间',
  `status` int NULL DEFAULT NULL COMMENT '工单状态',
  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'BPM æµç¨‹å®žä¾‹ç¼–号',
  `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 '创建者',
  `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工单编码',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工单名称',
  `type` int DEFAULT NULL COMMENT '工单类型',
  `order_source_type` int DEFAULT NULL COMMENT '来源类型',
  `order_source_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源单据编号',
  `product_id` bigint DEFAULT NULL COMMENT '产品编号',
  `quantity` decimal(24,6) DEFAULT NULL COMMENT '生产数量',
  `quantity_produced` decimal(24,6) DEFAULT NULL COMMENT '已生产数量',
  `quantity_changed` decimal(24,6) DEFAULT NULL COMMENT '调整数量',
  `quantity_scheduled` decimal(24,6) DEFAULT NULL COMMENT '已排产数量',
  `client_id` bigint DEFAULT NULL COMMENT '客户编号',
  `vendor_id` bigint DEFAULT NULL COMMENT '供应商编号',
  `batch_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '批次号',
  `route_id` bigint DEFAULT NULL COMMENT '工艺路线编号',
  `auto_finish_flag` tinyint(1) DEFAULT '1' COMMENT '是否自动完成',
  `request_date` datetime DEFAULT NULL COMMENT '需求日期',
  `parent_id` bigint DEFAULT NULL COMMENT '父工单编号',
  `finish_date` datetime DEFAULT NULL COMMENT '完成时间',
  `cancel_date` datetime DEFAULT NULL COMMENT '取消时间',
  `status` int DEFAULT NULL COMMENT '工单状态',
  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM æµç¨‹å®žä¾‹ç¼–号',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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 NULL DEFAULT '' 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 '是否删除',
  `is_feedback_approve` tinyint(1) DEFAULT NULL COMMENT '是否开启报工审批 (1: å¼€å¯, 0/NULL: å…³é—­)',
  `process_definition_key` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM æµç¨‹å®šä¹‰ Key',
  `feedback_role` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '报工角色 (多个角色以逗号分隔)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES ç”Ÿäº§å·¥å•' ROW_FORMAT = Dynamic;
) ENGINE=InnoDB AUTO_INCREMENT=62 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='MES ç”Ÿäº§å·¥å•';
-- ----------------------------
-- Records of mes_pro_work_order
-- ----------------------------
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-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
@@ -20,6 +20,7 @@
    public static final String INFO_KEY_NICKNAME = "nickname";
    public static final String INFO_KEY_DEPT_ID = "deptId";
    public static final String INFO_KEY_ROLE_CODES = "roleCodes";
    /**
     * ç”¨æˆ·ç¼–号
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
@@ -114,6 +114,29 @@
    }
    /**
     * èŽ·å¾—å½“å‰ç”¨æˆ·çš„è§’è‰²ç¼–ç åˆ—è¡¨ï¼Œä»Žä¸Šä¸‹æ–‡ä¸­
     *
     * @return è§’色编码列表
     */
    public static java.util.List<String> getLoginUserRoleCodeList() {
        LoginUser loginUser = getLoginUser();
        if (loginUser == null || loginUser.getInfo() == null) {
            return Collections.emptyList();
        }
        String roleCodes = loginUser.getInfo().get(LoginUser.INFO_KEY_ROLE_CODES);
        return StrUtil.isNotEmpty(roleCodes) ? StrUtil.split(roleCodes, ",") : Collections.emptyList();
    }
    /**
     * åˆ¤æ–­å½“前用户是否为超级管理员
     *
     * @return æ˜¯å¦ä¸ºè¶…级管理员
     */
    public static boolean isSuperAdmin() {
        return getLoginUserRoleCodeList().contains("super_admin");
    }
    /**
     * è®¾ç½®å½“前用户
     *
     * @param loginUser ç™»å½•用户
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java
@@ -17,6 +17,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackQuickCreateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSubmitReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
@@ -176,10 +177,9 @@
    @PutMapping("/submit")
    @Operation(summary = "提交报工")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:pro-feedback:update')")
    public CommonResult<Boolean> submitFeedback(@RequestParam("id") Long id) {
        feedbackService.submitFeedback(id);
    public CommonResult<Boolean> submitFeedback(@Valid @RequestBody MesProFeedbackSubmitReqVO reqVO) {
        feedbackService.submitFeedback(reqVO);
        return success(true);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackRespVO.java
@@ -169,4 +169,12 @@
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号")
    @ExcelProperty("BPM æµç¨‹å®žä¾‹ç¼–号")
    private String processInstanceId;
    @Schema(description = "BPM æµç¨‹å®šä¹‰ Key")
    @ExcelProperty("BPM æµç¨‹å®šä¹‰ Key")
    private String processDefinitionKey;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackSaveReqVO.java
@@ -87,4 +87,10 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号")
    private String processInstanceId;
    @Schema(description = "BPM æµç¨‹å®šä¹‰ Key")
    private String processDefinitionKey;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackSubmitReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES ç”Ÿäº§æŠ¥å·¥æäº¤ Request VO")
@Data
public class MesProFeedbackSubmitReqVO {
    @Schema(description = "报工单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "报工单ID不能为空")
    private Long id;
    @Schema(description = "生产日期")
    private LocalDateTime produceDate;
    @Schema(description = "过期日期")
    private LocalDateTime expireDate;
    @Schema(description = "入库日期")
    private LocalDateTime receiptDate;
    @Schema(description = "供应商ID")
    private Long vendorId;
    @Schema(description = "客户ID")
    private Long clientId;
    @Schema(description = "采购订单编号")
    private String purchaseOrderCode;
    @Schema(description = "销售订单编号")
    private String salesOrderCode;
    @Schema(description = "工具ID")
    private Long toolId;
    @Schema(description = "模具ID")
    private Long moldId;
    @Schema(description = "生产批号")
    private String lotNumber;
    @Schema(description = "质量状态")
    private Integer qualityStatus;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
@@ -94,6 +94,8 @@
    private MesProTaskPauseRecordMapper pauseRecordMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private cn.iocoder.yudao.module.system.api.permission.PermissionApi permissionApi;
    @PostMapping("/create")
    @Operation(summary = "创建生产任务")
@@ -190,6 +192,11 @@
    @Operation(summary = "获得待报工台账分页", description = "已排产且仍有待报工数量的任务(状态=进行中)")
    @PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
    public CommonResult<PageResult<MesProTaskRespVO>> getPendingFeedbackPage(@Valid MesProTaskPageReqVO pageReqVO) {
        Long loginUserId = cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId();
        // å¦‚果当前登录人不是超级管理员,则仅查询分配给自己的生产工单
        if (loginUserId != null && !permissionApi.hasAnyRoles(loginUserId, cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum.SUPER_ADMIN.getCode())) {
            pageReqVO.setAssigneeUserId(loginUserId);
        }
        PageResult<MesProTaskDO> pageResult = taskService.getPendingFeedbackPage(pageReqVO);
        List<MesProTaskRespVO> respVOList = buildTaskRespVOList(pageResult.getList());
        
@@ -349,6 +356,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 +395,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(assigneeUserNames);
            }
            return vo;
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskPageReqVO.java
@@ -49,4 +49,7 @@
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] createTime;
    @Schema(description = "处理人用户编号", example = "1024")
    private Long assigneeUserId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java
@@ -134,4 +134,12 @@
    @Schema(description = "停工历史记录")
    private List<MesProTaskPauseRecordRespVO> pauseRecords;
    @Schema(description = "指定报工人员 ID é›†åˆ")
    private List<Long> assigneeUserIds;
    @Schema(description = "指定报工人员姓名集合")
    private List<String> assigneeUserNames;
    @Schema(description = "是否报工审批")
    private Boolean feedbackApprove;
}
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/controller/admin/pro/workorder/MesProWorkOrderController.java
@@ -113,6 +113,13 @@
        return success(new PageResult<>(buildWorkOrderRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/approve-process-list")
    @Operation(summary = "获取审批报工的流程定义列表")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
    public CommonResult<List<Map<String, Object>>> getWorkOrderApproveProcessDefinitionList() {
        return success(workOrderService.getWorkOrderApproveProcessDefinitionList());
    }
    @GetMapping("/schedule-page")
    @Operation(summary = "获得生产工单调度状态分页")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderRespVO.java
@@ -145,4 +145,16 @@
    @ExcelProperty("生产状态")
    private Integer productionStatus;
    @Schema(description = "是否报工审批")
    @ExcelProperty("是否报工审批")
    private Boolean feedbackApprove;
    @Schema(description = "BPM æµç¨‹å®šä¹‰ Key")
    @ExcelProperty("BPM æµç¨‹å®šä¹‰ Key")
    private String processDefinitionKey;
    @Schema(description = "报工角色 (多个角色逗号分隔)")
    @ExcelProperty("报工角色")
    private String feedbackRole;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderSaveReqVO.java
@@ -70,4 +70,13 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "是否报工审批")
    private Boolean feedbackApprove;
    @Schema(description = "BPM æµç¨‹å®šä¹‰ Key")
    private String processDefinitionKey;
    @Schema(description = "报工角色 (多个角色逗号分隔)")
    private String feedbackRole;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/feedback/MesProFeedbackDO.java
@@ -160,4 +160,14 @@
     */
    private String remark;
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     */
    private String processInstanceId;
    /**
     * BPM æµç¨‹å®šä¹‰ Key
     */
    private String processDefinitionKey;
}
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/dataobject/pro/workorder/MesProWorkOrderDO.java
@@ -8,6 +8,7 @@
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.math.BigDecimal;
@@ -148,4 +149,21 @@
     */
    private String remark;
    /**
     * æ˜¯å¦æŠ¥å·¥å®¡æ‰¹
     */
    @com.baomidou.mybatisplus.annotation.TableField("is_feedback_approve")
    private Boolean feedbackApprove;
    /**
     * BPM æµç¨‹å®šä¹‰ Key
     */
    private String processDefinitionKey;
    /**
     * æŠ¥å·¥è§’色 (多个角色逗号分隔)
     */
    private String feedbackRole;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/mps/MesProMpsMapper.java
@@ -52,9 +52,11 @@
                .orderByDesc(MesProMpsDO::getId);
        if (permittedClientIds != null) {
            if (CollUtil.isEmpty(permittedClientIds)) {
                return PageResult.empty();
                wrapper.isNull(MesProMpsDO::getClientId);
            } else {
                wrapper.and(w -> w.in(MesProMpsDO::getClientId, permittedClientIds)
                        .or().isNull(MesProMpsDO::getClientId));
            }
            wrapper.in(MesProMpsDO::getClientId, permittedClientIds);
        }
        return selectPage(reqVO, wrapper);
    }
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/dal/mysql/pro/task/MesProTaskMapper.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -48,6 +49,12 @@
    default PageResult<MesProTaskDO> selectPendingFeedbackPage(MesProTaskPageReqVO reqVO) {
        MPJLambdaWrapperX<MesProTaskDO> query = new MPJLambdaWrapperX<>();
        query.selectAll(MesProTaskDO.class);
        // å½“需要按 assigneeUserId è¿‡æ»¤æ—¶ï¼ŒINNER JOIN mes_pro_task_assignee
        if (reqVO.getAssigneeUserId() != null) {
            query.innerJoin(MesProTaskAssigneeDO.class, on -> on
                    .eq(MesProTaskAssigneeDO::getTaskId, MesProTaskDO::getId));
            query.eq(MesProTaskAssigneeDO::getUserId, reqVO.getAssigneeUserId());
        }
        // å¾…报工台账:进行中的任务(排除已完成/已取消),且仍有待报工量
        query.in(MesProTaskDO::getStatus, MesProTaskStatusEnum.PREPARE.getStatus(),MesProTaskStatusEnum.PAUSED.getStatus());
        query.apply("(quantity - IFNULL(produced_quantity, 0)) > 0");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -40,6 +40,7 @@
    ErrorCode MD_ITEM_BATCH_CONFIG_NOT_EXISTS = new ErrorCode(1_040_102_100, "物料批次属性配置不存在");
    ErrorCode MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_FLAG = new ErrorCode(1_040_102_101, "批次管理已启用,至少需要配置一个批次属性");
    ErrorCode MD_ITEM_PRODUCT_BOM_REQUIRED = new ErrorCode(1_040_102_102, "产品类物料启用前,必须配置至少一个 BOM ç»„成");
    ErrorCode MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG = new ErrorCode(1_040_102_103, "需要配置一个批次属性,请在审批意见中填写");
    // ========== MES ä»“库管理-批次管理(1-040-717-000) ==========
    ErrorCode WM_BATCH_PRODUCE_DATE_REQUIRED = new ErrorCode(1_040_717_000, "批次配置要求生产日期不能为空");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/common/bpm/MesBpmProcessService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.service.common.bpm;
import java.util.List;
import java.util.Map;
/**
 * MES æ¨¡å—的通用 BPM æµç¨‹å¤„理 Service æŽ¥å£
 */
public interface MesBpmProcessService {
    /**
     * èŽ·å–æŒ‡å®šåˆ†ç±»ä¸‹çš„æ¿€æ´»çŠ¶æ€ã€æœ€æ–°ç‰ˆæœ¬çš„æµç¨‹å®šä¹‰åˆ—è¡¨
     *
     * @param categoryCode æµç¨‹åˆ†ç±»ç¼–码
     * @return æµç¨‹å®šä¹‰åˆ—表,包含 id, key, name
     */
    List<Map<String, Object>> getApproveProcessDefinitionList(String categoryCode);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/common/bpm/MesBpmProcessServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.mes.service.common.bpm;
import cn.hutool.core.collection.CollUtil;
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.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.*;
/**
 * MES æ¨¡å—的通用 BPM æµç¨‹å¤„理 Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class MesBpmProcessServiceImpl implements MesBpmProcessService {
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    public List<Map<String, Object>> getApproveProcessDefinitionList(String categoryCode) {
        // 1. æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨
        List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
                Collections.singletonList(categoryCode));
        if (CollUtil.isEmpty(categories)) {
            log.warn("[getApproveProcessDefinitionList] BPM æµç¨‹åˆ†ç±» {} ä¸å­˜åœ¨", categoryCode);
            return Collections.emptyList();
        }
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(categoryCode);
        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;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectServiceImpl.java
@@ -7,8 +7,7 @@
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.mes.service.common.bpm.MesBpmProcessService;
import cn.iocoder.yudao.module.mes.controller.admin.pd.project.vo.MesPdProjectPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pd.project.vo.MesPdProjectSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO;
@@ -80,10 +79,7 @@
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    private MesBpmProcessService mesBpmProcessService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -238,40 +234,7 @@
    @Override
    public List<Map<String, Object>> getProjectApproveProcessDefinitionList() {
        // 1. æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨
        List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
                Collections.singletonList(BPM_PROCESS_DEFINITION_CATEGORY_CODE));
        if (CollUtil.isEmpty(categories)) {
            throw exception(PD_PROJECT_BPM_CATEGORY_NOT_EXISTS);
        }
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(BPM_PROCESS_DEFINITION_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;
        return mesBpmProcessService.getApproveProcessDefinitionList(BPM_PROCESS_DEFINITION_CATEGORY_CODE);
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java
@@ -81,9 +81,9 @@
    /**
     * æäº¤æŠ¥å·¥ï¼ˆè‰ç¨¿ â†’ å®¡æ‰¹ä¸­ï¼‰
     *
     * @param id ç¼–号
     * @param reqVO æäº¤è¯·æ±‚
     */
    void submitFeedback(Long id);
    void submitFeedback(@Valid MesProFeedbackSubmitReqVO reqVO);
    /**
     * é©³å›žæŠ¥å·¥ï¼ˆå®¡æ‰¹ä¸­ â†’ è‰ç¨¿ï¼‰
@@ -122,6 +122,14 @@
    MesProFeedbackBatchCheckRespVO checkBatchRequirements(Long feedbackId);
    /**
     * å¤„理 BPM å®¡æ‰¹ç»“æžœ
     *
     * @param id æŠ¥å·¥å•ID
     * @param bpmResult å®¡æ‰¹ç»“果状态
     */
    void handleBpmProcessResult(Long id, Integer bpmResult,String reason);
    /**
     * IPQC å®ŒæˆåŽå›žè°ƒï¼šå®ŒæˆæŠ¥å·¥å•并更新任务/工单进度
     *
     * <p>由 IPQC æ£€éªŒå®Œæˆæ—¶è°ƒç”¨ï¼Œå°†æŠ¥å·¥å•状态从待检验改为已完成,
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -4,6 +4,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
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.module.mes.controller.admin.pro.feedback.vo.*;
@@ -62,6 +63,7 @@
 */
@Service
@Validated
@lombok.extern.slf4j.Slf4j
public class MesProFeedbackServiceImpl implements MesProFeedbackService {
    @Resource
@@ -94,6 +96,8 @@
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Resource
    private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi;
    @Override
    public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
@@ -154,13 +158,76 @@
    }
    @Override
    public void submitFeedback(Long id) {
    @Transactional(rollbackFor = Exception.class)
    public void submitFeedback(MesProFeedbackSubmitReqVO reqVO) {
        Long id = reqVO.getId();
        // 1. æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateFeedbackStatusPrepare(id);
        MesProFeedbackDO feedback = validateFeedbackStatusPrepare(id);
        // 2. æ›´æ–°çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­ï¼ˆæŠ¥å·¥äººå’ŒæŠ¥å·¥æ—¶é—´ç”±è¡¨å•ä¿å­˜æ—¶ç¡®å®šï¼Œæäº¤ä¸è¦†ç›–ï¼‰
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
        if (workOrder != null && Boolean.TRUE.equals(workOrder.getFeedbackApprove()) && cn.hutool.core.util.StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) {
            // 2. æ›´æ–°çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­ï¼Œå¹¶å¯åŠ¨æµç¨‹
            String processInstanceId = processInstanceApi.createProcessInstance(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId(),
                    new cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO()
                            .setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
                            .setBusinessKey(String.valueOf(id)));
            feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                    .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
                    .setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
                    .setProcessInstanceId(processInstanceId));
        } else {
            // 2. åˆ¤æ–­è¿™ä¸ªæŠ¥å·¥éœ€ä¸éœ€è¦æ‰¹æ¬¡å·
            MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
            if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()) {
                if (StrUtil.isBlank(reqVO.getLotNumber())) {
                    throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
                }
            }
            // 3. æ›´æ–°çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­ï¼Œç„¶åŽç›´æŽ¥é€šè¿‡å®¡æ‰¹
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()));
            MesProFeedbackApproveReqVO approveReqVO = cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(reqVO, MesProFeedbackApproveReqVO.class);
            approveFeedback(approveReqVO);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void handleBpmProcessResult(Long id, Integer bpmResult,String reason) {
        // 1. æŸ”性校验:获取报工单并判断状态
        MesProFeedbackDO feedback = feedbackMapper.selectById(id);
        if (feedback == null) {
            log.warn("[handleBpmProcessResult] ç”Ÿäº§æŠ¥å·¥({}) ä¸å­˜åœ¨", id);
            return;
        }
        if (!MesProFeedbackStatusEnum.APPROVING.getStatus().equals(feedback.getStatus())) {
            log.warn("[handleBpmProcessResult] ç”Ÿäº§æŠ¥å·¥({}) ä¸å¤„于审批中状态,当前状态:{}", id, feedback.getStatus());
            return;
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果进行处理
        if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            MesProFeedbackApproveReqVO  reqVO = new MesProFeedbackApproveReqVO();
            reqVO.setId(id);
            //  åˆ¤æ–­è¿™ä¸ªæŠ¥å·¥éœ€ä¸éœ€è¦æ‰¹æ¬¡å·
            MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
            if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()){
               if (StrUtil.isBlank(reason)){
                   //  æŠ¥é”™ è¯·åœ¨å®¡æ‰¹æ„è§é‡Œå¡«å†™æ‰¹æ¬¡å·
                   throw  exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
               }else {
                 BeanUtils.copyProperties(mesProFeedbackBatchCheckRespVO,reqVO);
                 reqVO.setLotNumber(reason);
               }
            }
            approveFeedback(reqVO);
        } else if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult) ||
                   cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            rejectFeedback(id);
        }
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/listener/MesProFeedbackStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.mes.service.pro.feedback.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import org.flowable.task.api.history.HistoricTaskInstance;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
/**
 * ç”Ÿäº§æŠ¥å·¥å®¡æ‰¹çŠ¶æ€ç›‘å¬å™¨
 *
 * ç›‘听 BPM æµç¨‹å®žä¾‹çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œæ›´æ–°ç”Ÿäº§æŠ¥å·¥çš„å®¡æ‰¹çŠ¶æ€
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesProFeedbackStatusListener extends BpmProcessInstanceStatusEventListener {
    /**
     * BPM ç”Ÿäº§æŠ¥å·¥å®¡æ‰¹æµç¨‹å®šä¹‰ key
     */
    private static final String BPM_PROCESS_DEFINITION_KEY = "mes-pro-feedback-audit";
    @Resource
    private MesProFeedbackService feedbackService;
    @Resource
    private BpmTaskService bpmTaskService;
    @Override
    protected String getProcessDefinitionKey() {
        return BPM_PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long feedbackId;
        try {
            feedbackId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„生产报工编号", event.getBusinessKey());
            return;
        }
        String reason = event.getReason();
        if (StrUtil.isEmpty(reason) && event.getId() != null) {
            // å½“ event.getReason() ä¸ºç©ºæ—¶ï¼Œå°è¯•从最后一个完成的任务中获取审批意见
            List<HistoricTaskInstance> tasks = bpmTaskService.getTaskListByProcessInstanceId(event.getId(), false);
            if (CollUtil.isNotEmpty(tasks)) {
                HistoricTaskInstance lastTask = tasks.stream()
                        .filter(task -> task.getEndTime() != null)
                        .max(Comparator.comparing(HistoricTaskInstance::getEndTime))
                        .orElse(null);
                if (lastTask != null && lastTask.getTaskLocalVariables() != null) {
                    reason = (String) lastTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON);
                }
            }
        }
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,feedbackId={}, status={},Reason={}", feedbackId, event.getStatus(), reason);
        feedbackService.handleBpmProcessResult(feedbackId, event.getStatus(), reason);
    }
}
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) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderService.java
@@ -67,6 +67,13 @@
    MesProWorkOrderDO getWorkOrder(String code);
    /**
     * èŽ·å–å®¡æ‰¹æŠ¥å·¥çš„æµç¨‹å®šä¹‰åˆ—è¡¨
     *
     * @return æµç¨‹å®šä¹‰åˆ—表,包含 id, key, name
     */
    List<Map<String, Object>> getWorkOrderApproveProcessDefinitionList();
    /**
     * èŽ·å¾—ç”Ÿäº§å·¥å•åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderServiceImpl.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeBizTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.common.bpm.MesBpmProcessService;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderProgressRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
@@ -46,6 +47,11 @@
@Service
@Validated
public class MesProWorkOrderServiceImpl implements MesProWorkOrderService {
    @Resource
    private MesBpmProcessService mesBpmProcessService;
    private static final String BPM_PROCESS_DEFINITION_CATEGORY_CODE = "mes_pro_feedback_audit";
    @Resource
    private MesProWorkOrderMapper workOrderMapper;
@@ -150,6 +156,13 @@
    }
    @Override
    public List<Map<String, Object>> getWorkOrderApproveProcessDefinitionList() {
        return mesBpmProcessService.getApproveProcessDefinitionList(BPM_PROCESS_DEFINITION_CATEGORY_CODE);
    }
    @Override
    public MesProWorkOrderDO getWorkOrder(Long id) {
        return workOrderMapper.selectById(id);
    }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -14,7 +14,10 @@
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.common.SexEnum;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
@@ -33,6 +36,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -49,6 +53,10 @@
    private AdminUserService userService;
    @Resource
    private DeptService deptService;
    @Resource
    private RoleService roleService;
    @Resource
    private PermissionService permissionService;
    @PostMapping("/create")
    @Operation(summary = "新增用户")
@@ -148,6 +156,32 @@
        return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap));
    }
    @GetMapping("/list-by-role-code")
    @Operation(summary = "通过角色标识获取用户精简信息列表", description = "只包含拥有这些角色的用户,主要用于前端的下拉选项。支持逗号分隔的多个角色标识")
    @Parameter(name = "code", description = "角色标识,支持多个,逗号分隔", required = true, example = "admin,user")
    public CommonResult<List<UserSimpleRespVO>> getSimpleUserListByRoleCode(
            @RequestParam("code") String code) {
        if (StrUtil.isBlank(code)) {
            return success(Collections.emptyList());
        }
        List<String> codes = StrUtil.split(code, ",");
        List<RoleDO> roles = roleService.getRoleListByCodes(codes);
        if (CollUtil.isEmpty(roles)) {
            return success(Collections.emptyList());
        }
        Set<Long> userIds = permissionService.getUserRoleIdListByRoleId(convertList(roles, RoleDO::getId));
        if (CollUtil.isEmpty(userIds)) {
            return success(Collections.emptyList());
        }
        List<AdminUserDO> list = userService.getUserList(userIds);
        // æ‹¼æŽ¥æ•°æ®
        Map<Long, DeptDO> deptMap = deptService.getDeptMap(
                convertList(list, AdminUserDO::getDeptId));
        return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap));
    }
    @GetMapping("/get")
    @Operation(summary = "获得用户详情")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java
@@ -32,6 +32,10 @@
        return selectOne(RoleDO::getCode, code);
    }
    default List<RoleDO> selectListByCodes(Collection<String> codes) {
        return selectList(RoleDO::getCode, codes);
    }
    default List<RoleDO> selectListByStatus(@Nullable Collection<Integer> statuses) {
        return selectList(RoleDO::getStatus, statuses);
    }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java
@@ -57,6 +57,12 @@
    @Resource
    @Lazy // æ‡’加载,避免循环依赖
    private AdminUserService adminUserService;
    @Resource
    @Lazy
    private cn.iocoder.yudao.module.system.service.permission.PermissionService permissionService;
    @Resource
    @Lazy
    private cn.iocoder.yudao.module.system.service.permission.RoleService roleService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -226,8 +232,18 @@
        }
        if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
            AdminUserDO user = adminUserService.getUser(userId);
            return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname())
                    .put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(user.getDeptId())).build();
            java.util.Map<String, String> info = new java.util.HashMap<>();
            info.put(LoginUser.INFO_KEY_NICKNAME, user.getNickname());
            info.put(LoginUser.INFO_KEY_DEPT_ID, cn.hutool.core.util.StrUtil.toStringOrNull(user.getDeptId()));
            // Fetch roles and put into info
            java.util.Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(userId);
            if (cn.hutool.core.collection.CollUtil.isNotEmpty(roleIds)) {
                java.util.List<cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO> roles = roleService.getRoleList(roleIds);
                if (cn.hutool.core.collection.CollUtil.isNotEmpty(roles)) {
                    info.put(LoginUser.INFO_KEY_ROLE_CODES, cn.hutool.core.collection.CollUtil.join(roles, ",", cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO::getCode));
                }
            }
            return info;
        } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
            // æ³¨æ„ï¼šç›®å‰ Member æš‚时不读取,可以按需实现
            return Collections.emptyMap();
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java
@@ -65,6 +65,22 @@
    RoleDO getRole(Long id);
    /**
     * èŽ·å¾—è§’è‰²
     *
     * @param code è§’色编码
     * @return è§’色
     */
    RoleDO getRoleByCode(String code);
    /**
     * èŽ·å¾—è§’è‰²åˆ—è¡¨
     *
     * @param codes è§’色编码数组
     * @return è§’色列表
     */
    List<RoleDO> getRoleListByCodes(Collection<String> codes);
    /**
     * èŽ·å¾—è§’è‰²ï¼Œä»Žç¼“å­˜ä¸­
     *
     * @param id è§’色编号
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java
@@ -190,6 +190,19 @@
    }
    @Override
    public RoleDO getRoleByCode(String code) {
        return roleMapper.selectByCode(code);
    }
    @Override
    public List<RoleDO> getRoleListByCodes(Collection<String> codes) {
        if (CollUtil.isEmpty(codes)) {
            return Collections.emptyList();
        }
        return roleMapper.selectListByCodes(codes);
    }
    @Override
    @Cacheable(value = RedisKeyConstants.ROLE, key = "#id",
            unless = "#result == null")
    public RoleDO getRoleFromCache(Long id) {
yudao-server/src/main/resources/application-wtxc.yaml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,222 @@
server:
  port: 48080
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
    # noinspection SpringBootApplicationYaml
    exclude:
      - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # é»˜è®¤ local çŽ¯å¢ƒï¼Œä¸å¼€å¯ Quartz çš„自动配置
      - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Qdrant,手动创建
      - org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Redis,手动创建
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # ç¦ç”¨æœªé…ç½® API Key çš„æ¨¡åž‹è‡ªåŠ¨é…ç½®ï¼ˆåªä¿ç•™é€šä¹‰åƒé—® Embedding çš„æ‰‹åŠ¨é…ç½®ï¼‰
      - org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration
      - org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration
      - org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiEmbeddingConnectionAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiTextEmbeddingAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaApiAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration
      - org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration
  # æ•°æ®æºé…ç½®é¡¹
  datasource:
    druid: # Druid ã€ç›‘控】相关的全局配置
      web-stat-filter:
        enabled: true
      stat-view-servlet:
        enabled: true
        allow: # è®¾ç½®ç™½åå•,不填则允许所有访问
        url-pattern: /druid/*
        login-username: # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-password:
      filter:
        stat:
          enabled: true
          log-slow-sql: true # æ…¢ SQL è®°å½•
          slow-sql-millis: 100
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
    dynamic: # å¤šæ•°æ®æºé…ç½®
      druid: # Druid ã€è¿žæŽ¥æ± ã€‘相关的全局配置
        initial-size: 1 # åˆå§‹è¿žæŽ¥æ•°
        min-idle: 1 # æœ€å°è¿žæŽ¥æ± æ•°é‡
        max-active: 20 # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
        max-wait: 60000 # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´ï¼Œå•ä½ï¼šæ¯«ç§’ï¼ˆ1 åˆ†é’Ÿï¼‰
        time-between-eviction-runs-millis: 60000 # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 åˆ†é’Ÿï¼‰
        min-evictable-idle-time-millis: 600000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(10 åˆ†é’Ÿï¼‰
        max-evictable-idle-time-millis: 1800000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(30 åˆ†é’Ÿï¼‰
        validation-query: SELECT 1 FROM DUAL # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: true # æ˜¯å¦å¼€å¯ PreparedStatement ç¼“å­˜
        max-pool-prepared-statement-per-connection-size: 20 # æ¯ä¸ªè¿žæŽ¥ç¼“存的 PreparedStatement æ•°é‡
      primary: master
      datasource:
        master:
          url: jdbc:mysql://172.17.0.1:9002/mom-wtxc?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          #          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL è¿žæŽ¥çš„示例
          #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle è¿žæŽ¥çš„示例
          #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer è¿žæŽ¥çš„示例
          #          url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM è¿žæŽ¥çš„示例
          #          url: jdbc:kingbase8://127.0.0.1:54321/test  # äººå¤§é‡‘仓 KingbaseES è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          username: root
          password: wtxc@123456..
          #          username: sa # SQL Server è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # SQL Server è¿žæŽ¥çš„示例
          #          username: SYSDBA # DM è¿žæŽ¥çš„示例
          #          password: SYSDBA001 # DM è¿žæŽ¥çš„示例
          #          username: root # OpenGauss è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # OpenGauss è¿žæŽ¥çš„示例
          #          username: root # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          #          password: 123456 # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
#        tdengine: # IoT æ•°æ®åº“(需要 IoT ç‰©è”网再开启噢!)
#          lazy: true # å¼€å¯æ‡’加载,保证启动速度
#          url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro?varcharAsString=true&enableAutoReconnect=true # å‚见:https://t.zsxq.com/qaX9c,开启 TDengine WebSocket è‡ªåŠ¨é‡è¿ž
#          driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
#          username: root
#          password: taosdata
#          druid:
#            validation-query: SELECT SERVER_STATUS() # TDengine æ•°æ®æºçš„æœ‰æ•ˆæ€§æ£€æŸ¥ SQL
  # Redis é…ç½®ã€‚Redisson é»˜è®¤çš„配置足够使用,一般不需要进行调优
  data:
    redis:
      host: 172.17.0.1 # åœ°å€
      port: 6379 # ç«¯å£
      database: 1 # æ•°æ®åº“索引
#      password: dev # å¯†ç ï¼Œå»ºè®®ç”Ÿäº§çŽ¯å¢ƒå¼€å¯
--- #################### å®šæ—¶ä»»åŠ¡ç›¸å…³é…ç½® ####################
# Quartz é…ç½®é¡¹ï¼Œå¯¹åº” QuartzProperties é…ç½®ç±»
spring:
  quartz:
    auto-startup: true # æœ¬åœ°å¼€å‘环境,尽量不要开启 Job
    scheduler-name: schedulerName # Scheduler åå­—。默认为 schedulerName
    job-store-type: jdbc # Job å­˜å‚¨å™¨ç±»åž‹ã€‚默认为 memory è¡¨ç¤ºå†…存,可选 jdbc ä½¿ç”¨æ•°æ®åº“。
    wait-for-jobs-to-complete-on-shutdown: true # åº”用关闭时,是否等待定时任务执行完成。默认为 false ï¼Œå»ºè®®è®¾ç½®ä¸º true
    properties: # æ·»åŠ  Quartz Scheduler é™„加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html æ–‡æ¡£
      org:
        quartz:
          # Scheduler ç›¸å…³é…ç½®
          scheduler:
            instanceName: schedulerName
            instanceId: AUTO # è‡ªåŠ¨ç”Ÿæˆ instance ID
          # JobStore ç›¸å…³é…ç½®
          jobStore:
            # JobStore å®žçŽ°ç±»ã€‚å¯è§åšå®¢ï¼šhttps://blog.csdn.net/weixin_42458219/article/details/122247162
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            isClustered: true # æ˜¯é›†ç¾¤æ¨¡å¼
            clusterCheckinInterval: 15000 # é›†ç¾¤æ£€æŸ¥é¢‘率,单位:毫秒。默认为 15000,即 15 ç§’
            misfireThreshold: 60000 # misfire é˜€å€¼ï¼Œå•位:毫秒。
          # çº¿ç¨‹æ± ç›¸å…³é…ç½®
          threadPool:
            threadCount: 25 # çº¿ç¨‹æ± å¤§å°ã€‚默认为 10 ã€‚
            threadPriority: 5 # çº¿ç¨‹ä¼˜å…ˆçº§
            class: org.quartz.simpl.SimpleThreadPool # çº¿ç¨‹æ± ç±»åž‹
    jdbc: # ä½¿ç”¨ JDBC çš„ JobStore çš„æ—¶å€™ï¼ŒJDBC çš„配置
      initialize-schema: NEVER # æ˜¯å¦è‡ªåŠ¨ä½¿ç”¨ SQL åˆå§‹åŒ– Quartz è¡¨ç»“构。这里设置成 never ï¼Œæˆ‘们手动创建表结构。
--- #################### æœåŠ¡ä¿éšœç›¸å…³é…ç½® ####################
# Lock4j é…ç½®é¡¹
lock4j:
  acquire-timeout: 3000 # èŽ·å–åˆ†å¸ƒå¼é”è¶…æ—¶æ—¶é—´ï¼Œé»˜è®¤ä¸º 3000 æ¯«ç§’
  expire: 30000 # åˆ†å¸ƒå¼é”çš„超时时间,默认为 30000 æ¯«ç§’
--- #################### ç›‘控相关配置 ####################
# Actuator ç›‘控端点的配置项
management:
  endpoints:
    web:
      base-path: /actuator # Actuator æä¾›çš„ API æŽ¥å£çš„æ ¹ç›®å½•。默认为 /actuator
      exposure:
        include: '*' # éœ€è¦å¼€æ”¾çš„端点。默认值只打开 health å’Œ info ä¸¤ä¸ªç«¯ç‚¹ã€‚通过设置 * ï¼Œå¯ä»¥å¼€æ”¾æ‰€æœ‰ç«¯ç‚¹ã€‚
# Spring Boot Admin é…ç½®é¡¹
spring:
  boot:
    admin:
      # Spring Boot Admin Client å®¢æˆ·ç«¯çš„相关配置
      client:
        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # è®¾ç½® Spring Boot Admin Server åœ°å€
        instance:
          service-host-type: IP # æ³¨å†Œå®žä¾‹æ—¶ï¼Œä¼˜å…ˆä½¿ç”¨ IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
        username: admin
        password: admin
      # Spring Boot Admin Server æœåŠ¡ç«¯çš„ç›¸å…³é…ç½®
      context-path: /admin # é…ç½® Spring
      # å…è®¸åµŒå…¥ iframe çš„域名(支持通配符),实际部署时,可以改为 "'self' [你的公网域名]"
      frame-ancestors: "'self' localhost localhost:48080 127.0.0.1 127.0.0.1:48080"
# æ—¥å¿—文件配置
logging:
  file:
    name: logs/${spring.application.name}.log # æ—¥å¿—文件名,相对于项目根目录
  level:
    # é…ç½®è‡ªå·±å†™çš„ MyBatis Mapper æ‰“印日志
    cn.iocoder.yudao.module.bpm.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # é…ç½® ApiErrorLogMapper çš„æ—¥å¿—级别为 info,避免和 GlobalExceptionHandler é‡å¤æ‰“印
    cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper: INFO # é…ç½® JobLogMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper: INFO # é…ç½® FileConfigMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.system.dal.mysql: debug
    cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # é…ç½® SmsChannelMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.member.dal.mysql: debug
    cn.iocoder.yudao.module.crm.dal.mysql: debug
    cn.iocoder.yudao.module.erp.dal.mysql: debug
    cn.iocoder.yudao.module.mes.dal.mysql: debug
    cn.iocoder.yudao.module.ai.dal.mysql: debug
    cn.iocoder.yudao.module.im.dal.mysql: debug
    org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO èŠ‹è‰¿ï¼šå…ˆç¦ç”¨ï¼ŒSpring Boot 3.X å­˜åœ¨éƒ¨åˆ†é”™è¯¯çš„ WARN æç¤º
debug: false
yudao:
  ai:
    vector-store:
      type: milvus # å‘量库类型:milvus
      milvus:
        initialize-schema: true
        database-name: test
        collection-name: knowledge_segment_wtxc
        host: 36.213.90.123
        port: 19530
--- #################### è¶…级管理员相关配置 ####################
# è¶…级管理员配置项,设置当前项目所有自定义的配置
yudao:
  captcha:
    enable: false # æœ¬åœ°çŽ¯å¢ƒï¼Œæš‚æ—¶å…³é—­å›¾ç‰‡éªŒè¯ç ï¼Œæ–¹ä¾¿ç™»å½•ç­‰æŽ¥å£çš„æµ‹è¯•ï¼›
  security:
    mock-enable: true
  access-log: # è®¿é—®æ—¥å¿—的配置项
    enable: false
  demo: false # å…³é—­æ¼”示模式
  storage: # æ–‡ä»¶ä¸Šä¼ é…ç½®
    path: /data/javaWork/mom/file # æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://42.63.71.140:9001 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    expired: 120 # token过期时间(分钟)
    use-limit: 10 # token使用次数上限
    jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWT密钥
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)