2026-08-03 f0c9b593769315001591cb4a759650a76b71cb8f
feat(crm): 合同导出功能优化及状态中文展示

- 新增 CrmAuditStatusEnum.getNameByStatus 方法实现状态码到中文名称转换
- 在合同导出接口中计算未回款金额并转换审批状态为中文名称
- 修改 CrmContractRespVO 类增加审核状态名称字段和未回款金额字段
- 调整 Excel 导出字段顺序并更新字段标题为中文描述
- 从 MDM 物料控制器中移除导入相关功能代码
- 删除 MES 模块中物料导入相关的 VO 类和服务方法
- 优化 MDM 物料下拉列表返回单位名称等关联字段信息
- 从 SRM 错误码中移除物料导入空数据相关定义
- 在 SRM 模块中新增招标流程精细化状态校验错误码
- 实现 SRM 定标和评标服务中的状态流转校验逻辑
- 添加 SRM 投标截止时间检查防止逾期提交
- 在供应商分页查询中增加排除已审批选项功能
已修改19个文件
已添加8个文件
已删除2个文件
1008 ■■■■ 文件已修改
.playwright-mcp/page-2026-08-03T09-16-38-621Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-03T09-20-27-740Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-03T09-20-36-226Z.yml 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-03T09-22-30-620Z.yml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-03T09-22-42-679Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-03T09-22-54-018Z.yml 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmAuditStatusEnum.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemImportExcelVO.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemImportRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | 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/md/item/MesMdItemService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/vo/SrmSupplierPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderProjectController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectPartialUpdateReqVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/mysql/SrmSupplierMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/mysql/SrmTenderBidMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/LogRecordConstants.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/TenderStatusEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidEvaluationServiceImpl.java 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-03T09-16-38-621Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-03T09-20-27-740Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-03T09-20-36-226Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,154 @@
- generic [ref=f1e7]:
  - complementary [ref=f1e8]:
    - generic [ref=f1e9]:
      - link "工业AI智造平台 å·¥ä¸šAI智造平台" [ref=f1e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工业AI智造平台" [ref=f1e18]
        - generic [ref=f1e19]: å·¥ä¸šAI智造平台
      - menu [ref=f1e22]:
        - menuitem "首页" [ref=f1e23] [cursor=pointer]
        - listitem [ref=f1e30]:
          - generic [ref=f1e31] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f1e39]:
          - generic [ref=f1e40] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f1e46]:
          - generic [ref=f1e47] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f1e53]:
          - generic [ref=f1e54] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f1e61]:
          - generic [ref=f1e62] [cursor=pointer]: ä»“储物流
        - listitem [ref=f1e70]:
          - generic [ref=f1e71] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f1e78]:
          - generic [ref=f1e79] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f1e85]:
          - generic [ref=f1e86] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=f1e92]:
          - generic [ref=f1e93] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f1e99]:
          - generic [ref=f1e100] [cursor=pointer]: å”®åŽç®¡ç†
        - listitem [ref=f1e106]:
          - generic [ref=f1e107] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f1e113]:
          - generic [ref=f1e114] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f1e120]:
          - generic [ref=f1e121] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=f1e134]:
    - generic [ref=f1e135]:
      - banner [ref=f1e136]:
        - button [ref=f1e137] [cursor=pointer]
        - button [ref=f1e140] [cursor=pointer]
        - navigation "breadcrumb" [ref=f1e145]:
          - list [ref=f1e146]:
            - listitem [ref=f1e147]:
              - link [disabled] [ref=f1e148]
        - generic [ref=f1e155]:
          - generic [ref=f1e157] [cursor=pointer]:
            - generic [ref=f1e161]: æœç´¢
            - generic [ref=f1e162]: Ctrl K
          - button [ref=f1e164] [cursor=pointer]
          - button "dark" [ref=f1e169] [cursor=pointer]
          - button [ref=f1e178] [cursor=pointer]
          - button [ref=f1e184] [cursor=pointer]
          - button [ref=f1e187] [cursor=pointer]
          - button [ref=f1e193] [cursor=pointer]:
            - button [ref=f1e195]
          - button "工作台" [ref=f1e199] [cursor=pointer]
          - button [ref=f1e203] [cursor=pointer]:
            - img "芋道源码" [ref=f1e208]
      - generic [ref=f1e210]:
        - generic [ref=f1e211]: é¦–页
        - button [ref=f1e233] [cursor=pointer]
    - main [ref=f1e251]:
      - generic [ref=f1e254]:
        - generic [ref=f1e255]:
          - generic [ref=f1e256]:
            - heading "下午好,欢迎回来" [level=1] [ref=f1e257]
            - paragraph [ref=f1e258]: 2026å¹´8月3日 æ˜ŸæœŸä¸€
          - generic [ref=f1e259]: ç³»ç»Ÿè¿è¡Œä¸­
        - generic [ref=f1e264]:
          - generic [ref=f1e273] [cursor=pointer]:
            - generic [ref=f1e274]: ç”Ÿäº§è®¢å•
            - generic [ref=f1e275]:
              - generic [ref=f1e276]: "25"
              - generic [ref=f1e278]: è¿›è¡Œä¸­
            - generic [ref=f1e279]:
              - text: å¾…排产 15
              - generic [ref=f1e281]: "|"
              - text: å·²å®Œæˆ 5
          - generic [ref=f1e293] [cursor=pointer]:
            - generic [ref=f1e294]: ä»Šæ—¥äº§é‡
            - generic [ref=f1e295]:
              - generic [ref=f1e296]: "0"
              - generic [ref=f1e298]: ä»¶
            - generic [ref=f1e299]: æ˜¨æ—¥ 0 ä»¶
          - generic [ref=f1e310] [cursor=pointer]:
            - generic [ref=f1e311]: è´¨é‡åˆæ ¼çއ
            - generic [ref=f1e312]:
              - generic [ref=f1e313]: "0.0"
              - generic [ref=f1e315]: "%"
            - generic [ref=f1e316]: åˆæ ¼ 0 | ä¸è‰¯ 0
          - generic [ref=f1e328] [cursor=pointer]:
            - generic [ref=f1e329]: è®¾å¤‡è¿è¡Œçއ
            - generic [ref=f1e330]:
              - generic [ref=f1e331]: "100.0"
              - generic [ref=f1e333]: "%"
            - generic [ref=f1e334]:
              - generic [ref=f1e335]: åœæœº 0
              - generic [ref=f1e336]: "|"
              - generic [ref=f1e337]: ç»´æŠ¤ 0
        - generic [ref=f1e340]:
          - generic [ref=f1e350] [cursor=pointer]:
            - generic [ref=f1e351]: é‡‡è´­è®¢å•
            - generic [ref=f1e352]:
              - generic [ref=f1e353]: "14"
              - generic [ref=f1e355]: ç¬”
            - generic [ref=f1e356]: ERP é‡‡è´­ç®¡ç†
          - generic [ref=f1e367] [cursor=pointer]:
            - generic [ref=f1e368]: é”€å”®è®¢å•
            - generic [ref=f1e369]:
              - generic [ref=f1e370]: "22"
              - generic [ref=f1e372]: ç¬”
            - generic [ref=f1e373]: ERP é”€å”®ç®¡ç†
          - generic [ref=f1e385] [cursor=pointer]:
            - generic [ref=f1e386]: ç‰©æ–™åº“å­˜
            - generic [ref=f1e387]:
              - generic [ref=f1e388]: "13"
              - generic [ref=f1e390]: æ¡
            - generic [ref=f1e391]: MES åº“存台账
          - generic [ref=f1e403] [cursor=pointer]:
            - generic [ref=f1e404]: æˆ‘的待办
            - generic [ref=f1e405]:
              - generic [ref=f1e406]: "1"
              - generic [ref=f1e408]: é¡¹
            - generic [ref=f1e409]: BPM å¾…审批
        - generic [ref=f1e413]:
          - heading "AI æ™ºèƒ½åˆ†æž" [level=3] [ref=f1e414]
          - generic [ref=f1e415]: 25 ä¸ªå·¥å•可分析
        - generic [ref=f1e416]:
          - heading "生产趋势(近7天)" [level=3] [ref=f1e419]
          - generic [ref=f1e424]:
            - generic [ref=f1e425]:
              - generic [ref=f1e431] [cursor=pointer]:
                - generic [ref=f1e432]: å®‰ç¯å‘¼å«
                - generic [ref=f1e433]:
                  - generic [ref=f1e434]: "0"
                  - generic [ref=f1e436]: æœªå¤„ç½®
              - generic [ref=f1e442] [cursor=pointer]:
                - generic [ref=f1e443]: ç»´ä¿®å·¥å•
                - generic [ref=f1e444]:
                  - generic [ref=f1e445]: "0"
                  - generic [ref=f1e447]: å¾…处理
            - heading "工单状态分布" [level=4] [ref=f1e449]
        - generic [ref=f1e457]:
          - heading "快捷操作" [level=3] [ref=f1e458]
          - generic [ref=f1e459]:
            - generic [ref=f1e460] [cursor=pointer]: ç”Ÿäº§å·¥å•
            - generic [ref=f1e463] [cursor=pointer]: ç”Ÿäº§æŠ¥å·¥
            - generic [ref=f1e466] [cursor=pointer]: è´¨æ£€ç®¡ç†
            - generic [ref=f1e469] [cursor=pointer]: è®¾å¤‡ç‚¹æ£€
            - generic [ref=f1e472] [cursor=pointer]: ç”Ÿäº§æŽ’程
            - generic [ref=f1e475] [cursor=pointer]: å®‰ç¯å‘¼å«
            - generic [ref=f1e478] [cursor=pointer]: ç‰©æ–™åº“å­˜
            - generic [ref=f1e481] [cursor=pointer]: å·¥è‰ºè·¯çº¿
            - generic [ref=f1e484] [cursor=pointer]: æ•°æ®æŠ¥è¡¨
.playwright-mcp/page-2026-08-03T09-22-30-620Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
- generic [ref=f2e3]:
  - generic [ref=f2e6]:
    - heading "无法访问此网站" [level=1] [ref=f2e7]
    - paragraph [ref=f2e8]:
      - strong [ref=f2e9]: localhost
      - text: æ‹’绝了我们的连接请求。
    - generic [ref=f2e10]:
      - paragraph [ref=f2e11]: è¯·è¯•试以下办法:
      - list [ref=f2e12]:
        - listitem [ref=f2e13]: æ£€æŸ¥ç½‘络连接
        - listitem [ref=f2e14]:
          - link "检查代理服务器和防火墙" [ref=f2e15] [cursor=pointer]:
            - /url: "#buttons"
    - generic [ref=f2e16]: ERR_CONNECTION_REFUSED
  - generic [ref=f2e17]:
    - button "重新加载" [ref=f2e19] [cursor=pointer]
    - button "详情" [ref=f2e20] [cursor=pointer]
.playwright-mcp/page-2026-08-03T09-22-42-679Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-03T09-22-54-018Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
- generic [ref=f3e7]:
  - complementary [ref=f3e8]:
    - generic [ref=f3e9]:
      - link "工业AI智造平台 å·¥ä¸šAI智造平台" [ref=f3e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工业AI智造平台" [ref=f3e18]
        - generic [ref=f3e19]: å·¥ä¸šAI智造平台
      - menu [ref=f3e22]:
        - menuitem "首页" [ref=f3e23] [cursor=pointer]
        - listitem [ref=f3e30]:
          - generic [ref=f3e31] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f3e39]:
          - generic [ref=f3e40] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f3e46]:
          - generic [ref=f3e47] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f3e53]:
          - generic [ref=f3e54] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f3e61]:
          - generic [ref=f3e62] [cursor=pointer]: ä»“储物流
        - listitem [ref=f3e70]:
          - generic [ref=f3e71] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f3e78]:
          - generic [ref=f3e79] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f3e85]:
          - generic [ref=f3e86] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=f3e92]:
          - generic [ref=f3e93] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f3e99]:
          - generic [ref=f3e100] [cursor=pointer]: å”®åŽç®¡ç†
        - listitem [ref=f3e106]:
          - generic [ref=f3e107] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f3e113]:
          - generic [ref=f3e114] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f3e120]:
          - generic [ref=f3e121] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=f3e134]:
    - generic [ref=f3e135]:
      - banner [ref=f3e136]:
        - button [ref=f3e137] [cursor=pointer]
        - button [ref=f3e140] [cursor=pointer]
        - navigation "breadcrumb" [ref=f3e145]:
          - list [ref=f3e146]:
            - listitem [ref=f3e147]:
              - link [disabled] [ref=f3e148]
        - generic [ref=f3e155]:
          - generic [ref=f3e157] [cursor=pointer]:
            - generic [ref=f3e161]: æœç´¢
            - generic [ref=f3e162]: Ctrl K
          - button [ref=f3e164] [cursor=pointer]
          - button "dark" [ref=f3e169] [cursor=pointer]
          - button [ref=f3e178] [cursor=pointer]
          - button [ref=f3e184] [cursor=pointer]
          - button [ref=f3e187] [cursor=pointer]
          - button [ref=f3e193] [cursor=pointer]:
            - button [ref=f3e195]
          - button "工作台" [ref=f3e199] [cursor=pointer]
          - button [ref=f3e203] [cursor=pointer]:
            - img "芋道源码" [ref=f3e208]
      - generic [ref=f3e210]:
        - generic [ref=f3e211]: é¦–页
        - button [ref=f3e233] [cursor=pointer]
    - main [ref=f3e251]:
      - generic [ref=f3e254]:
        - generic [ref=f3e255]:
          - generic [ref=f3e256]:
            - heading "下午好,欢迎回来" [level=1] [ref=f3e257]
            - paragraph [ref=f3e258]: 2026å¹´8月3日 æ˜ŸæœŸä¸€
          - generic [ref=f3e259]: ç³»ç»Ÿè¿è¡Œä¸­
        - generic [ref=f3e264]:
          - generic [ref=f3e273] [cursor=pointer]:
            - generic [ref=f3e274]: ç”Ÿäº§è®¢å•
            - generic [ref=f3e275]:
              - generic [ref=f3e276]: "25"
              - generic [ref=f3e278]: è¿›è¡Œä¸­
            - generic [ref=f3e279]:
              - text: å¾…排产 15
              - generic [ref=f3e281]: "|"
              - text: å·²å®Œæˆ 5
          - generic [ref=f3e293] [cursor=pointer]:
            - generic [ref=f3e294]: ä»Šæ—¥äº§é‡
            - generic [ref=f3e295]:
              - generic [ref=f3e296]: "0"
              - generic [ref=f3e298]: ä»¶
            - generic [ref=f3e299]: æ˜¨æ—¥ 0 ä»¶
          - generic [ref=f3e310] [cursor=pointer]:
            - generic [ref=f3e311]: è´¨é‡åˆæ ¼çއ
            - generic [ref=f3e312]:
              - generic [ref=f3e313]: "0.0"
              - generic [ref=f3e315]: "%"
            - generic [ref=f3e316]: åˆæ ¼ 0 | ä¸è‰¯ 0
          - generic [ref=f3e328] [cursor=pointer]:
            - generic [ref=f3e329]: è®¾å¤‡è¿è¡Œçއ
            - generic [ref=f3e330]:
              - generic [ref=f3e331]: "100.0"
              - generic [ref=f3e333]: "%"
            - generic [ref=f3e334]:
              - generic [ref=f3e335]: åœæœº 0
              - generic [ref=f3e336]: "|"
              - generic [ref=f3e337]: ç»´æŠ¤ 0
        - generic [ref=f3e340]:
          - generic [ref=f3e350] [cursor=pointer]:
            - generic [ref=f3e351]: é‡‡è´­è®¢å•
            - generic [ref=f3e352]:
              - generic [ref=f3e353]: "14"
              - generic [ref=f3e355]: ç¬”
            - generic [ref=f3e356]: ERP é‡‡è´­ç®¡ç†
          - generic [ref=f3e367] [cursor=pointer]:
            - generic [ref=f3e368]: é”€å”®è®¢å•
            - generic [ref=f3e369]:
              - generic [ref=f3e370]: "22"
              - generic [ref=f3e372]: ç¬”
            - generic [ref=f3e373]: ERP é”€å”®ç®¡ç†
          - generic [ref=f3e385] [cursor=pointer]:
            - generic [ref=f3e386]: ç‰©æ–™åº“å­˜
            - generic [ref=f3e387]:
              - generic [ref=f3e388]: "13"
              - generic [ref=f3e390]: æ¡
            - generic [ref=f3e391]: MES åº“存台账
          - generic [ref=f3e403] [cursor=pointer]:
            - generic [ref=f3e404]: æˆ‘的待办
            - generic [ref=f3e405]:
              - generic [ref=f3e406]: "1"
              - generic [ref=f3e408]: é¡¹
            - generic [ref=f3e409]: BPM å¾…审批
        - generic [ref=f3e412]:
          - generic [ref=f3e413]:
            - heading "AI æ™ºèƒ½åˆ†æž" [level=3] [ref=f3e414]
            - generic [ref=f3e415]: 25 ä¸ªå·¥å•可分析
          - generic [ref=f3e416]:
            - generic [ref=f3e424] [cursor=pointer]:
              - generic [ref=f3e425]: AI ç‰©æ–™é¢„测
              - generic [ref=f3e426]: é¢„测物料需求与短缺风险
            - generic [ref=f3e435] [cursor=pointer]:
              - generic [ref=f3e436]: AI æ—¶é•¿é¢„测
              - generic [ref=f3e437]: é¢„测各工序生产耗时
            - generic [ref=f3e444] [cursor=pointer]:
              - generic [ref=f3e445]: AI é£Žé™©é¢„测
              - generic [ref=f3e446]: è¯†åˆ«æ½œåœ¨ç”Ÿäº§é£Žé™©å¹¶ç»™å‡ºå»ºè®®
            - generic [ref=f3e456] [cursor=pointer]:
              - generic [ref=f3e457]: AI äº¤ä»˜è¯„ä¼°
              - generic [ref=f3e458]: è¯„估订单交付准时率与延期因素
        - generic [ref=f3e461]:
          - heading "生产趋势(近7天)" [level=3] [ref=f3e464]
          - generic [ref=f3e469]:
            - generic [ref=f3e470]:
              - generic [ref=f3e476] [cursor=pointer]:
                - generic [ref=f3e477]: å®‰ç¯å‘¼å«
                - generic [ref=f3e478]:
                  - generic [ref=f3e479]: "0"
                  - generic [ref=f3e481]: æœªå¤„ç½®
              - generic [ref=f3e487] [cursor=pointer]:
                - generic [ref=f3e488]: ç»´ä¿®å·¥å•
                - generic [ref=f3e489]:
                  - generic [ref=f3e490]: "0"
                  - generic [ref=f3e492]: å¾…处理
            - heading "工单状态分布" [level=4] [ref=f3e494]
        - generic [ref=f3e502]:
          - heading "快捷操作" [level=3] [ref=f3e503]
          - generic [ref=f3e504]:
            - generic [ref=f3e505] [cursor=pointer]: ç”Ÿäº§å·¥å•
            - generic [ref=f3e512] [cursor=pointer]: ç”Ÿäº§æŠ¥å·¥
            - generic [ref=f3e519] [cursor=pointer]: è´¨æ£€ç®¡ç†
            - generic [ref=f3e527] [cursor=pointer]: è®¾å¤‡ç‚¹æ£€
            - generic [ref=f3e534] [cursor=pointer]: ç”Ÿäº§æŽ’程
            - generic [ref=f3e541] [cursor=pointer]: å®‰ç¯å‘¼å«
            - generic [ref=f3e546] [cursor=pointer]: ç‰©æ–™åº“å­˜
            - generic [ref=f3e554] [cursor=pointer]: å·¥è‰ºè·¯çº¿
            - generic [ref=f3e562] [cursor=pointer]: æ•°æ®æŠ¥è¡¨
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -19,6 +19,7 @@
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
@@ -159,9 +160,18 @@
    public void exportContractExcel(@Valid CrmContractPageReqVO exportReqVO,
                                    HttpServletResponse response) throws IOException {
        PageResult<CrmContractDO> pageResult = contractService.getContractPage(exportReqVO, getLoginUserId());
        List<CrmContractRespVO> list = BeanUtils.toBean(pageResult.getList(), CrmContractRespVO.class);
        // è®¡ç®—未回款金额、转换审批状态为中文
        list.forEach(vo -> {
            if (vo.getTotalPrice() != null && vo.getTotalReceivablePrice() != null) {
                vo.setUnReceivablePrice(vo.getTotalPrice().subtract(vo.getTotalReceivablePrice()));
            }
            if (vo.getAuditStatus() != null) {
                vo.setAuditStatusName(CrmAuditStatusEnum.getNameByStatus(vo.getAuditStatus()));
            }
        });
        // å¯¼å‡º Excel
        ExcelUtils.write(response, "合同.xls", "数据", CrmContractRespVO.class,
                BeanUtils.toBean(pageResult.getList(), CrmContractRespVO.class));
        ExcelUtils.write(response, "合同.xls", "数据", CrmContractRespVO.class, list);
    }
    @PutMapping("/transfer")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
@@ -17,121 +17,120 @@
public class CrmContractRespVO {
    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
    @ExcelProperty("合同编号")
    private Long id;
    @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
    @ExcelProperty("合同名称")
    @ExcelProperty(value = "合同名称", index = 1)
    private String name;
    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20230101")
    @ExcelProperty("合同编号")
    @ExcelProperty(value = "合同编号", index = 0)
    private String no;
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
    @ExcelProperty("客户编号")
    private Long customerId;
    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
    @ExcelProperty("客户名称")
    @ExcelProperty(value = "客户名称", index = 2)
    private String customerName;
    @Schema(description = "商机编号", example = "10864")
    @ExcelProperty("商机编号")
    private Long businessId;
    @Schema(description = "商机名称", example = "10864")
    @ExcelProperty("商机名称")
    @ExcelProperty(value = "商机名称", index = 3)
    private String businessName;
    @Schema(description = "最后跟进时间")
    @ExcelProperty("最后跟进时间")
    @ExcelProperty(value = "最后跟进时间", index = 14)
    private LocalDateTime contactLastTime;
    @Schema(description = "负责人的用户编号", example = "25682")
    @ExcelProperty("负责人的用户编号")
    private Long ownerUserId;
    @Schema(description = "负责人名字", example = "25682")
    @ExcelProperty("负责人名字")
    @ExcelProperty(value = "负责人", index = 15)
    private String ownerUserName;
    @Schema(description = "负责人部门")
    @ExcelProperty("负责人部门")
    @ExcelProperty(value = "所属部门", index = 16)
    private String ownerUserDeptName;
    @Schema(description = "工作流编号", example = "1043")
    @ExcelProperty("工作流编号")
    private String processInstanceId;
    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty("审批状态")
    private Integer auditStatus;
    @Schema(description = "合同状态名称", example = "审批中")
    @ExcelProperty(value = "合同状态", index = 20)
    private String auditStatusName;
    @Schema(description = "下单日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("下单日期")
    @ExcelProperty(value = "下单时间", index = 6)
    private LocalDateTime orderDate;
    @Schema(description = "开始时间")
    @ExcelProperty("开始时间")
    @ExcelProperty(value = "合同开始时间", index = 7)
    private LocalDateTime startTime;
    @Schema(description = "结束时间")
    @ExcelProperty("结束时间")
    @ExcelProperty(value = "合同结束时间", index = 8)
    private LocalDateTime endTime;
    @Schema(description = "产品总金额", example = "19510")
    @ExcelProperty("产品总金额")
    private BigDecimal totalProductPrice;
    @Schema(description = "整单折扣")
    @ExcelProperty("整单折扣")
    private BigDecimal discountPercent;
    @Schema(description = "合同金额", example = "5617")
    @ExcelProperty("合同金额")
    @ExcelProperty(value = "合同金额(元)", index = 4)
    private BigDecimal totalPrice;
    @Schema(description = "定金金额,单位:元", example = "1000.00")
    @ExcelProperty("定金金额")
    @ExcelProperty(value = "定金金额(元)", index = 5)
    private BigDecimal depositPrice;
    @Schema(description = "已回款金额", example = "5617")
    @ExcelProperty("已回款金额")
    @ExcelProperty(value = "已回款金额(元)", index = 12)
    private BigDecimal totalReceivablePrice;
    @Schema(description = "未回款金额,等于 åˆåŒé‡‘额 - å·²å›žæ¬¾é‡‘额", example = "1000.00")
    @ExcelProperty(value = "未回款金额(元)", index = 13)
    private BigDecimal unReceivablePrice;
    @Schema(description = "关联 ERP é”€å”®è®¢å•编号", example = "12345")
    private Long orderId;
    @Schema(description = "ERP é”€å”®è®¢å•单号", example = "SO001")
    @ExcelProperty("销售订单单号")
    @ExcelProperty(value = "关联订单", index = 21)
    private String orderNo;
    @Schema(description = "客户签约人编号", example = "18546")
    private Long signContactId;
    @Schema(description = "客户签约人", example = "小豆")
    @ExcelProperty("客户签约人")
    @ExcelProperty(value = "客户签约人", index = 9)
    private String signContactName;
    @Schema(description = "公司签约人", example = "14036")
    private Long signUserId;
    @Schema(description = "公司签约人", example = "小明")
    @ExcelProperty("公司签约人")
    @ExcelProperty(value = "公司签约人", index = 10)
    private String signUserName;
    @Schema(description = "备注", example = "你猜")
    @ExcelProperty("备注")
    @ExcelProperty(value = "备注", index = 11)
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    @ExcelProperty(value = "创建时间", index = 18)
    private LocalDateTime createTime;
    @Schema(description = "创建人", example = "25682")
    @ExcelProperty("创建人")
    private String creator;
    @Schema(description = "创建人名字", example = "test")
    @ExcelProperty("创建人名字")
    @ExcelProperty(value = "创建人", index = 19)
    private String creatorName;
    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("更新时间")
    @ExcelProperty(value = "更新时间", index = 17)
    private LocalDateTime updateTime;
    @Schema(description = "产品列表")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmAuditStatusEnum.java
@@ -27,6 +27,14 @@
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(CrmAuditStatusEnum::getStatus).toArray(Integer[]::new);
    public static String getNameByStatus(Integer status) {
        if (status == null) return null;
        for (CrmAuditStatusEnum e : values()) {
            if (e.getStatus().equals(status)) return e.getName();
        }
        return null;
    }
    @Override
    public Integer[] array() {
        return ARRAYS;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -189,7 +189,17 @@
    @Operation(summary = "获取物料精简列表(启用状态,用于下拉选择)")
    public CommonResult<List<MdmItemRespVO>> getItemSimpleList() {
        List<MdmItemDO> list = itemService.getItemSimpleList();
        return success(BeanUtils.toBean(list, MdmItemRespVO.class));
        List<MdmItemRespVO> respList = BeanUtils.toBean(list, MdmItemRespVO.class);
        // å¡«å……单位名称等关联字段,确保下拉选择后可带出单位等信息
        Map<Long, MdmUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(respList, MdmItemRespVO::getUnitMeasureId));
        respList.forEach(resp -> {
            MdmUnitMeasureDO unit = unitMap.get(resp.getUnitMeasureId());
            if (unit != null) {
                resp.setUnitMeasureName(unit.getName());
            }
        });
        return success(respList);
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemController.java
@@ -8,8 +8,6 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
@@ -29,7 +27,6 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Collections;
@@ -118,32 +115,6 @@
        // å¯¼å‡º Excel
        ExcelUtils.write(response, "物料产品.xls", "数据", MesMdItemRespVO.class,
                buildItemVOList(pageResult.getList()));
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得物料导入模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // æ‰‹åŠ¨åˆ›å»ºå¯¼å‡º demo
        List<MesMdItemImportExcelVO> list = Collections.singletonList(
                MesMdItemImportExcelVO.builder().code("ITEM001").name("螺丝").specification("M6*20")
                        .unitMeasureCode("PCS").itemTypeId(1L).status(0).build()
        );
        // è¾“出
        ExcelUtils.write(response, "物料导入模板.xls", "物料列表", MesMdItemImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入物料")
    @Parameters({
            @Parameter(name = "file", description = "Excel æ–‡ä»¶", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
    })
    @PreAuthorize("@ss.hasPermission('mes:md-item:import')")
    public CommonResult<MesMdItemImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
                                                           @RequestParam(value = "updateSupport", required = false,
                                                                   defaultValue = "false") Boolean updateSupport) throws Exception {
        List<MesMdItemImportExcelVO> list = ExcelUtils.read(file, MesMdItemImportExcelVO.class);
        return success(itemService.importItemList(list, updateSupport));
    }
    // ==================== æ‹¼æŽ¥ VO ====================
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemImportExcelVO.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemImportRespVO.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -33,7 +33,6 @@
    ErrorCode MD_ITEM_NOT_EXISTS = new ErrorCode(1_040_102_000, "物料不存在");
    ErrorCode MD_ITEM_CODE_DUPLICATE = new ErrorCode(1_040_102_001, "物料编码已存在");
    ErrorCode MD_ITEM_NAME_DUPLICATE = new ErrorCode(1_040_102_002, "物料名称已存在");
    ErrorCode MD_ITEM_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_102_003, "导入物料数据不能为空");
    ErrorCode MD_ITEM_BATCH_REQUIRED = new ErrorCode(1_040_102_004, "当前物料启用了批次管理,请选择批次");
    ErrorCode MD_ITEM_IS_DISABLE = new ErrorCode(1_040_102_005, "物料已禁用");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemService.java
@@ -1,8 +1,6 @@
package cn.iocoder.yudao.module.mes.service.md.item;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
@@ -116,15 +114,6 @@
     * @return ç‰©æ–™æ•°é‡
     */
    Long getItemCountByItemTypeId(Long itemTypeId);
    /**
     * æ‰¹é‡å¯¼å…¥ç‰©æ–™
     *
     * @param importItems å¯¼å…¥ç‰©æ–™åˆ—表
     * @param updateSupport æ˜¯å¦æ”¯æŒæ›´æ–°
     * @return å¯¼å…¥ç»“æžœ
     */
    MesMdItemImportRespVO importItemList(List<MesMdItemImportExcelVO> importItems, boolean updateSupport);
    /**
     * åŸºäºŽè®¡é‡å•位编号,获得物料数量
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -2,12 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
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.md.item.vo.MesMdItemImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -17,7 +13,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
import cn.iocoder.yudao.module.mes.enums.md.MesMdItemTypeEnum;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeBizTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
@@ -31,7 +27,6 @@
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -308,94 +303,6 @@
    @Override
    public Long getItemCountByUnitMeasureId(Long unitMeasureId) {
        return itemMapper.selectCountByUnitMeasureId(unitMeasureId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesMdItemImportRespVO importItemList(List<MesMdItemImportExcelVO> importItems, boolean updateSupport) {
        // 1. å‚数校验
        if (CollUtil.isEmpty(importItems)) {
            throw exception(MD_ITEM_IMPORT_LIST_IS_EMPTY);
        }
        // 2. éåŽ†ï¼Œé€ä¸ªåˆ›å»º or æ›´æ–°
        MesMdItemImportRespVO respVO = MesMdItemImportRespVO.builder()
                .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>())
                .failureCodes(new LinkedHashMap<>()).build();
        AtomicInteger index = new AtomicInteger(1);
        importItems.forEach(importItem -> {
            int currentIndex = index.getAndIncrement();
            // 2.1 æ ¡éªŒå­—段
            if (StrUtil.isBlank(importItem.getCode())) {
                // ç©ºç¼–码时自动生成
                importItem.setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.MD_ITEM_CODE.getCode()));
            }
            String key = importItem.getCode();
            if (StrUtil.isBlank(importItem.getName())) {
                respVO.getFailureCodes().put(key, "物料名称不能为空");
                return;
            }
            if (StrUtil.isBlank(importItem.getUnitMeasureCode())) {
                respVO.getFailureCodes().put(key, "单位编码不能为空");
                return;
            }
            // å°†å•位编码转换为单位 ID
            MesMdUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasureByCode(importItem.getUnitMeasureCode());
            if (unitMeasure == null) {
                respVO.getFailureCodes().put(key, "单位编码[" + importItem.getUnitMeasureCode() + "]不存在");
                return;
            }
            if (importItem.getItemTypeId() == null) {
                respVO.getFailureCodes().put(key, "物料分类编号不能为空");
                return;
            }
            // 2.2 æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨
            try {
                validateItemTypeExists(importItem.getItemTypeId());
            } catch (ServiceException ex) {
                respVO.getFailureCodes().put(key, ex.getMessage());
                return;
            }
            // 2.3 åˆ¤æ–­ï¼šåˆ›å»º or æ›´æ–°
            MesMdItemDO existItem = itemMapper.selectByCode(importItem.getCode());
            if (existItem == null) {
                // 2.3.1 åˆ›å»º
                try {
                    validateItemNameUnique(null, importItem.getName());
                } catch (ServiceException ex) {
                    respVO.getFailureCodes().put(key, ex.getMessage());
                    return;
                }
                MesMdItemDO item = BeanUtils.toBean(importItem, MesMdItemDO.class);
                item.setUnitMeasureId(unitMeasure.getId());
                item.setStatus(CommonStatusEnum.DISABLE.getStatus()); // é»˜è®¤ç¦ç”¨ã€‚信息完成后,再启用
                clearStockIfNotSafe(item);
                itemMapper.insert(item);
                // è‡ªåŠ¨ç”Ÿæˆæ¡ç 
                barcodeService.autoGenerateBarcode(BarcodeBizTypeEnum.ITEM.getValue(),
                        item.getId(), item.getCode(), item.getName());
                respVO.getCreateCodes().add(importItem.getCode());
            } else if (updateSupport) {
                // 2.3.2 æ›´æ–°
                try {
                    validateItemNameUnique(existItem.getId(), importItem.getName());
                } catch (ServiceException ex) {
                    respVO.getFailureCodes().put(key, ex.getMessage());
                    return;
                }
                MesMdItemDO updateObj = BeanUtils.toBean(importItem, MesMdItemDO.class);
                updateObj.setId(existItem.getId());
                updateObj.setUnitMeasureId(unitMeasure.getId());
                clearStockIfNotSafe(updateObj);
                itemMapper.updateById(updateObj);
                respVO.getUpdateCodes().add(importItem.getCode());
            } else {
                // ä¸æ”¯æŒæ›´æ–°
                respVO.getFailureCodes().put(key, "物料编码已存在");
            }
        });
        return respVO;
    }
    /**
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/vo/SrmSupplierPageReqVO.java
@@ -33,4 +33,7 @@
    @Schema(description = "仅显示已通过准入审批的供应商")
    private Boolean approved;
    @Schema(description = "排除已通过准入审批的供应商(用于新增准入时避免重复选择)")
    private Boolean excludeApproved;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderProjectController.java
@@ -96,6 +96,14 @@
        return success(true);
    }
    @PutMapping("/update-info")
    @Operation(summary = "部分更新招标信息(仅发布中/投标中可编辑:招标说明和资质要求)")
    @PreAuthorize("@ss.hasPermission('srm:tender-project:update')")
    public CommonResult<Boolean> updateTenderProjectInfo(@Valid @RequestBody SrmTenderProjectPartialUpdateReqVO updateReqVO) {
        tenderProjectService.updateTenderProjectPartial(updateReqVO);
        return success(true);
    }
    // ========== ç‰©æ–™å­è¡¨ ==========
    @PostMapping("/material/create")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectPartialUpdateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.srm.controller.admin.tender.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - SRM æ‹›æ ‡é¡¹ç›®éƒ¨åˆ†æ›´æ–° Request VO(发布中/投标中阶段)")
@Data
public class SrmTenderProjectPartialUpdateReqVO {
    @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "项目ID不能为空")
    private Long id;
    @Schema(description = "招标描述", example = "本次招标采购钢材一批")
    private String tenderDesc;
    @Schema(description = "资质要求", example = "需具备ISO9001认证")
    private String qualificationRequirements;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/mysql/SrmSupplierMapper.java
@@ -26,6 +26,9 @@
        if (Boolean.TRUE.equals(reqVO.getApproved())) {
            wrapper.apply("id IN (SELECT supplier_id FROM srm_supplier_apply WHERE apply_status = 40 AND supplier_id IS NOT NULL AND deleted = 0)");
        }
        if (Boolean.TRUE.equals(reqVO.getExcludeApproved())) {
            wrapper.apply("id NOT IN (SELECT supplier_id FROM srm_supplier_apply WHERE apply_status = 40 AND supplier_id IS NOT NULL AND deleted = 0)");
        }
        return selectPage(reqVO, wrapper);
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/mysql/SrmTenderBidMapper.java
@@ -26,4 +26,9 @@
                .eq(SrmTenderBidDO::getBidNo, bidNo));
    }
    default long countByTenderProjectId(Long tenderProjectId) {
        return selectCount(new LambdaQueryWrapperX<SrmTenderBidDO>()
                .eq(SrmTenderBidDO::getTenderProjectId, tenderProjectId));
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java
@@ -48,6 +48,17 @@
    ErrorCode AWARD_SUPPLIER_NOT_SYNCED = new ErrorCode(1_040_500_014, "供应商未同步至ERP,无法生成采购订单");
    ErrorCode AWARD_QUOTE_NOT_EXISTS = new ErrorCode(1_040_500_015, "中标供应商报价不存在");
    // çŠ¶æ€æ ¡éªŒç²¾ç»†åŒ–é”™è¯¯ç 
    ErrorCode TENDER_MATERIAL_LOCKED = new ErrorCode(1_040_500_022, "物料清单已锁定,不允许增删改");
    ErrorCode TENDER_BID_FORBIDDEN = new ErrorCode(1_040_500_023, "当前状态不允许新增投标");
    ErrorCode TENDER_QUOTE_FORBIDDEN = new ErrorCode(1_040_500_024, "当前状态不允许报价操作");
    ErrorCode TENDER_END_TIME_PASSED = new ErrorCode(1_040_500_025, "投标截止时间已过,不允许新增投标");
    ErrorCode TENDER_PROJECT_LOCKED = new ErrorCode(1_040_500_026, "项目已锁定,当前状态不允许操作");
    ErrorCode TENDER_HAS_BIDS_CANNOT_DELETE = new ErrorCode(1_040_500_027, "已存在投标数据,不允许删除");
    ErrorCode TENDER_BID_FORBIDDEN_WITHDRAW = new ErrorCode(1_040_500_028, "当前状态不允许撤标");
    ErrorCode TENDER_EVALUATION_FORBIDDEN = new ErrorCode(1_040_500_029, "当前状态不允许评标操作");
    ErrorCode TENDER_AWARD_FORBIDDEN = new ErrorCode(1_040_500_030, "当前状态不允许定标操作");
    // ========== é€šç”¨ ==========
    ErrorCode QUOTE_MATERIAL_DUPLICATE = new ErrorCode(1_040_900_001, "该投标已存在相同物料的报价");
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/LogRecordConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.srm.enums;
/**
 * SRM æ“ä½œæ—¥å¿—常量
 */
public interface LogRecordConstants {
    // ======================= SRM_TENDER æ‹›æ ‡é¡¹ç›® =======================
    String LOG_TENDER_TYPE = "SRM æ‹›æ ‡é¡¹ç›®";
    String LOG_TENDER_UPDATE_SUB_TYPE = "更新招标项目";
    String LOG_TENDER_UPDATE_SUCCESS = "更新了招标项目【{{#tenderName}}】,"
            + "招标说明:{{#oldTenderDesc}} â†’ {{#newTenderDesc}},"
            + "资质要求:{{#oldQualification}} â†’ {{#newQualification}}";
    // ======================= SRM_BID æŠ•æ ‡ =======================
    String LOG_BID_TYPE = "SRM æŠ•æ ‡";
    String LOG_BID_CREATE_SUB_TYPE = "创建投标";
    String LOG_BID_CREATE_SUCCESS = "供应商对招标项目【{{#tenderName}}】提交了投标【{{#bidNo}}】";
    String LOG_BID_WITHDRAW_SUB_TYPE = "撤标";
    String LOG_BID_WITHDRAW_SUCCESS = "撤回了投标【{{#bidNo}}】";
    // ======================= SRM_QUOTE æŠ¥ä»· =======================
    String LOG_QUOTE_TYPE = "SRM æŠ¥ä»·";
    String LOG_QUOTE_CREATE_SUB_TYPE = "创建报价";
    String LOG_QUOTE_CREATE_SUCCESS = "提交了投标的供应商报价";
    String LOG_QUOTE_UPDATE_SUB_TYPE = "更新报价";
    String LOG_QUOTE_UPDATE_SUCCESS = "更新了投标的供应商报价";
    // ======================= SRM_EVALUATION è¯„æ ‡ =======================
    String LOG_EVALUATION_TYPE = "SRM è¯„æ ‡";
    String LOG_EVALUATION_CREATE_SUB_TYPE = "创建评标";
    String LOG_EVALUATION_CREATE_SUCCESS = "对招标项目进行了评标评分";
    String LOG_EVALUATION_UPDATE_SUB_TYPE = "更新评标";
    String LOG_EVALUATION_UPDATE_SUCCESS = "更新了评标记录";
    // ======================= SRM_AWARD å®šæ ‡ =======================
    String LOG_AWARD_TYPE = "SRM å®šæ ‡";
    String LOG_AWARD_CREATE_SUB_TYPE = "创建定标";
    String LOG_AWARD_CREATE_SUCCESS = "创建了定标【{{#createReqVO.awardNo}}】";
    String LOG_AWARD_APPROVE_SUB_TYPE = "审批定标";
    String LOG_AWARD_APPROVE_SUCCESS = "审批通过了定标记录";
    String LOG_AWARD_ORDER_SUB_TYPE = "生成采购订单";
    String LOG_AWARD_ORDER_SUCCESS = "定标记录生成了采购订单";
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/TenderStatusEnum.java
@@ -23,4 +23,41 @@
    private final Integer code;
    private final String name;
    // ========== çŠ¶æ€å·¥å…·æ–¹æ³• ==========
    /** DRAFT å…è®¸å…¨å­—段编辑 */
    public static boolean canEditFullProject(Integer status) {
        return DRAFT.getCode().equals(status);
    }
    /** PUBLISHED æˆ– BIDDING å…è®¸éƒ¨åˆ†ç¼–辑(招标说明、资质要求) */
    public static boolean canEditPartialProject(Integer status) {
        return PUBLISHED.getCode().equals(status) || BIDDING.getCode().equals(status);
    }
    /** DRAFT å…è®¸ç¼–辑物料清单 */
    public static boolean canEditMaterial(Integer status) {
        return DRAFT.getCode().equals(status);
    }
    /** PUBLISHED æˆ– BIDDING å…è®¸ä¾›åº”商投标 */
    public static boolean canCreateBid(Integer status) {
        return PUBLISHED.getCode().equals(status) || BIDDING.getCode().equals(status);
    }
    /** PUBLISHED æˆ– BIDDING å…è®¸ä¾›åº”商报价 */
    public static boolean canCreateOrUpdateQuote(Integer status) {
        return PUBLISHED.getCode().equals(status) || BIDDING.getCode().equals(status);
    }
    /** EVALUATING å…è®¸è¯„标操作 */
    public static boolean canEvaluate(Integer status) {
        return EVALUATING.getCode().equals(status);
    }
    /** EVALUATING å…è®¸å®šæ ‡æ“ä½œ */
    public static boolean canAward(Integer status) {
        return EVALUATING.getCode().equals(status);
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.srm.enums.BidStatusEnum;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -25,6 +26,8 @@
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.module.srm.enums.LogRecordConstants.*;
/**
 * SRM å®šæ ‡ç®¡ç† Service å®žçŽ°ç±»
@@ -60,7 +63,14 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_AWARD_TYPE, subType = LOG_AWARD_CREATE_SUB_TYPE,
            bizNo = "{{#createReqVO.awardNo}}", success = LOG_AWARD_CREATE_SUCCESS)
    public Long createAward(SrmBidAwardSaveReqVO createReqVO) {
        // æ£€æŸ¥æ‹›æ ‡é¡¹ç›®çŠ¶æ€ï¼šä»… EVALUATING å…è®¸åˆ›å»ºå®šæ ‡
        SrmTenderProjectDO project = tenderProjectMapper.selectById(createReqVO.getTenderProjectId());
        if (project == null || !TenderStatusEnum.canAward(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_AWARD_FORBIDDEN);
        }
        // æ ¡éªŒæ˜¯å¦å·²å­˜åœ¨å®šæ ‡
        SrmBidAwardDO existing = bidAwardMapper.selectByTenderProjectId(createReqVO.getTenderProjectId());
        if (existing != null) {
@@ -95,6 +105,10 @@
    @Transactional(rollbackFor = Exception.class)
    public void deleteAward(Long id) {
        SrmBidAwardDO award = validateAwardExists(id);
        // ä»…待审批状态可删除
        if (!AwardStatusEnum.PENDING_APPROVAL.getCode().equals(award.getAwardStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_AWARD_FORBIDDEN);
        }
        bidAwardMapper.deleteById(id);
    }
@@ -102,16 +116,18 @@
    @Transactional(rollbackFor = Exception.class)
    public void approveAward(Long id) {
        SrmBidAwardDO award = validateAwardExists(id);
        // æ ¡éªŒæ‹›æ ‡é¡¹ç›®çŠ¶æ€ä¸ºè¯„æ ‡ä¸­
        SrmTenderProjectDO project = tenderProjectMapper.selectById(award.getTenderProjectId());
        if (project == null || !TenderStatusEnum.canAward(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_AWARD_FORBIDDEN);
        }
        award.setAwardStatus(AwardStatusEnum.AWARDED.getCode());
        award.setApprovalTime(LocalDateTime.now());
        bidAwardMapper.updateById(award);
        // æ›´æ–°æ‹›æ ‡é¡¹ç›®çŠ¶æ€ä¸ºå·²å®šæ ‡
        SrmTenderProjectDO project = tenderProjectMapper.selectById(award.getTenderProjectId());
        if (project != null) {
            project.setTenderStatus(TenderStatusEnum.AWARDED.getCode());
            tenderProjectMapper.updateById(project);
        }
        project.setTenderStatus(TenderStatusEnum.AWARDED.getCode());
        tenderProjectMapper.updateById(project);
    }
    @Override
@@ -121,6 +137,11 @@
        SrmBidAwardDO award = validateAwardExists(id);
        if (!AwardStatusEnum.AWARDED.getCode().equals(award.getAwardStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_STATUS_NOT_AWARDED);
        }
        // æ ¡éªŒæ‹›æ ‡é¡¹ç›®çŠ¶æ€ä¸ºå·²å®šæ ‡
        SrmTenderProjectDO project = tenderProjectMapper.selectById(award.getTenderProjectId());
        if (project == null || !TenderStatusEnum.AWARDED.getCode().equals(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_AWARD_FORBIDDEN);
        }
        // 2. æŸ¥ SRM ä¾›åº”商
@@ -201,11 +222,8 @@
        bidAwardMapper.updateById(award);
        // 9. æ›´æ–°æ‹›æ ‡é¡¹ç›®çŠ¶æ€
        SrmTenderProjectDO project = tenderProjectMapper.selectById(award.getTenderProjectId());
        if (project != null) {
            project.setTenderStatus(TenderStatusEnum.CLOSED.getCode());
            tenderProjectMapper.updateById(project);
        }
        project.setTenderStatus(TenderStatusEnum.CLOSED.getCode());
        tenderProjectMapper.updateById(project);
    }
    private SrmBidAwardDO validateAwardExists(Long id) {
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidEvaluationServiceImpl.java
@@ -11,6 +11,7 @@
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -21,6 +22,8 @@
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import static cn.iocoder.yudao.module.srm.enums.LogRecordConstants.*;
/**
 * SRM è¯„标管理 Service å®žçŽ°ç±»
@@ -42,7 +45,20 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_EVALUATION_TYPE, subType = LOG_EVALUATION_CREATE_SUB_TYPE,
            bizNo = "{{#createReqVO.tenderProjectId}}", success = LOG_EVALUATION_CREATE_SUCCESS)
    public Long createEvaluation(SrmBidEvaluationSaveReqVO createReqVO) {
        // æ£€æŸ¥æ‹›æ ‡é¡¹ç›®çŠ¶æ€ï¼šBIDDING æˆ– EVALUATING æ‰å…è®¸è¯„æ ‡
        SrmTenderProjectDO project = tenderProjectMapper.selectById(createReqVO.getTenderProjectId());
        if (project == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NOT_EXISTS);
        }
        Integer status = project.getTenderStatus();
        if (!TenderStatusEnum.BIDDING.getCode().equals(status)
                && !TenderStatusEnum.EVALUATING.getCode().equals(status)) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_EVALUATION_FORBIDDEN);
        }
        SrmBidEvaluationDO evaluation = BeanUtils.toBean(createReqVO, SrmBidEvaluationDO.class);
        // å¼ºåˆ¶ç½®ç©º id,避免前端传入已有 id å¯¼è‡´ä¸»é”®å†²çªï¼Œç”±æ•°æ®åº“序列自动生成
        evaluation.setId(null);
@@ -60,8 +76,7 @@
        evaluation.setEvaluationTime(LocalDateTime.now());
        bidEvaluationMapper.insert(evaluation);
        // é¦–次评标时更新招标项目状态为评标中
        SrmTenderProjectDO project = tenderProjectMapper.selectById(createReqVO.getTenderProjectId());
        if (project != null && TenderStatusEnum.BIDDING.getCode().equals(project.getTenderStatus())) {
        if (TenderStatusEnum.BIDDING.getCode().equals(project.getTenderStatus())) {
            project.setTenderStatus(TenderStatusEnum.EVALUATING.getCode());
            tenderProjectMapper.updateById(project);
        }
@@ -74,6 +89,11 @@
        SrmBidEvaluationDO existing = bidEvaluationMapper.selectById(updateReqVO.getId());
        if (existing == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.EVALUATION_NOT_EXISTS_TENDER);
        }
        // æ£€æŸ¥æ‹›æ ‡é¡¹ç›®çŠ¶æ€ï¼šä»… EVALUATING å…è®¸æ›´æ–°è¯„æ ‡
        SrmTenderProjectDO project = tenderProjectMapper.selectById(existing.getTenderProjectId());
        if (project == null || !TenderStatusEnum.canEvaluate(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_EVALUATION_FORBIDDEN);
        }
        SrmBidEvaluationDO evaluation = BeanUtils.toBean(updateReqVO, SrmBidEvaluationDO.class);
        BigDecimal price = nvl(evaluation.getPriceScore());
@@ -98,6 +118,10 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void calculateRanking(Long tenderProjectId) {
        SrmTenderProjectDO project = tenderProjectMapper.selectById(tenderProjectId);
        if (project == null || !TenderStatusEnum.canEvaluate(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_EVALUATION_FORBIDDEN);
        }
        List<SrmBidEvaluationDO> list = bidEvaluationMapper.selectListByTenderProjectId(tenderProjectId);
        // æŒ‰ç»¼åˆè¯„分降序排名
        list.sort(Comparator.comparing(e -> e.getCompositeScore() != null ? e.getCompositeScore() : BigDecimal.ZERO, Comparator.reverseOrder()));
@@ -113,15 +137,20 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createBidOpen(Long tenderProjectId) {
        // æ ¡éªŒæ‹›æ ‡é¡¹ç›®å­˜åœ¨ä¸”状态为发布中
        SrmTenderProjectDO project = tenderProjectMapper.selectById(tenderProjectId);
        if (project == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NOT_EXISTS);
        }
        if (!TenderStatusEnum.PUBLISHED.getCode().equals(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_PUBLISHED);
        }
        if (bidOpenMapper.selectByTenderProjectId(tenderProjectId) != null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_PUBLISHED);
        }
        // å¼€æ ‡åŽè‡ªåŠ¨æ›´æ–°æ‹›æ ‡é¡¹ç›®çŠ¶æ€ä¸ºæŠ•æ ‡ä¸­
        SrmTenderProjectDO project = tenderProjectMapper.selectById(tenderProjectId);
        if (project != null && TenderStatusEnum.PUBLISHED.getCode().equals(project.getTenderStatus())) {
            project.setTenderStatus(TenderStatusEnum.BIDDING.getCode());
            tenderProjectMapper.updateById(project);
        }
        project.setTenderStatus(TenderStatusEnum.BIDDING.getCode());
        tenderProjectMapper.updateById(project);
        SrmBidOpenDO open = new SrmBidOpenDO();
        open.setTenderProjectId(tenderProjectId);
        open.setOpenTime(LocalDateTime.now());
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderBidSaveReqVO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierQuoteDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderBidDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderProjectDO;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierApplyMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierQuoteMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderBidMapper;
@@ -13,6 +14,8 @@
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -20,6 +23,8 @@
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.module.srm.enums.LogRecordConstants.*;
/**
 * SRM æŠ•标管理 Service å®žçŽ°ç±»
@@ -44,9 +49,15 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_BID_TYPE, subType = LOG_BID_CREATE_SUB_TYPE,
            bizNo = "{{#createReqVO.bidNo}}", success = LOG_BID_CREATE_SUCCESS)
    public Long createBid(SrmTenderBidSaveReqVO createReqVO) {
        // æ ¡éªŒæ‹›æ ‡é¡¹ç›®æ˜¯å¦å­˜åœ¨ä¸”状态正确
        validateTenderStatus(createReqVO.getTenderProjectId());
        // æ ¡éªŒæ‹›æ ‡é¡¹ç›®çŠ¶æ€ï¼ˆPUBLISHED/BIDDING æ‰å…è®¸æŠ•标)
        SrmTenderProjectDO project = validateTenderStatus(createReqVO.getTenderProjectId());
        // æ£€æŸ¥æŠ•标截止时间
        if (project.getEndTime() != null && LocalDateTime.now().isAfter(project.getEndTime())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_END_TIME_PASSED);
        }
        // æ ¡éªŒä¾›åº”商是否已通过准入审批
        if (!supplierApplyMapper.existsApprovedBySupplierId(createReqVO.getSupplierId())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_SUPPLIER_NOT_APPROVED);
@@ -64,6 +75,8 @@
        bid.setBidStatus(BidStatusEnum.REGISTERED.getCode());
        bid.setBidTime(LocalDateTime.now());
        tenderBidMapper.insert(bid);
        LogRecordContext.putVariable("bidNo", bid.getBidNo());
        LogRecordContext.putVariable("tenderName", project.getTenderName());
        return bid.getId();
    }
@@ -79,20 +92,35 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_BID_TYPE, subType = LOG_BID_WITHDRAW_SUB_TYPE,
            bizNo = "{{#id}}", success = LOG_BID_WITHDRAW_SUCCESS)
    public void withdrawBid(Long id) {
        SrmTenderBidDO bid = validateBidExists(id);
        // æ£€æŸ¥æ‹›æ ‡é¡¹ç›®çŠ¶æ€ï¼šä»… PUBLISHED/BIDDING å…è®¸æ’¤æ ‡
        SrmTenderProjectDO project = tenderProjectMapper.selectById(bid.getTenderProjectId());
        if (project == null || !TenderStatusEnum.canCreateOrUpdateQuote(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_BID_FORBIDDEN_WITHDRAW);
        }
        bid.setBidStatus(BidStatusEnum.WITHDRAWN.getCode());
        tenderBidMapper.updateById(bid);
        // åˆ é™¤æŠ¥ä»·
        supplierQuoteMapper.deleteByBidId(id);
        LogRecordContext.putVariable("bidNo", bid.getBidNo());
    }
    // ========== æŠ¥ä»·ç®¡ç† ==========
    @Override
    @LogRecord(type = LOG_QUOTE_TYPE, subType = LOG_QUOTE_CREATE_SUB_TYPE,
            bizNo = "{{#createReqVO.bidId}}", success = LOG_QUOTE_CREATE_SUCCESS)
    public Long createQuote(SrmSupplierQuoteSaveReqVO createReqVO) {
        // æ ¡éªŒæŠ•标状态(撤标后不允许报价)
        SrmTenderBidDO bid = validateBidNotWithdrawn(createReqVO.getBidId());
        // æ ¡éªŒæ‹›æ ‡é¡¹ç›®çŠ¶æ€ï¼šä»… PUBLISHED/BIDDING å…è®¸æŠ¥ä»·
        SrmTenderProjectDO project = tenderProjectMapper.selectById(bid.getTenderProjectId());
        if (project == null || !TenderStatusEnum.canCreateOrUpdateQuote(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_QUOTE_FORBIDDEN);
        }
        // æ ¡éªŒåŒä¸€æŠ•标下物料不可重复报价
        if (supplierQuoteMapper.existsByBidAndMaterial(createReqVO.getBidId(), createReqVO.getTenderMaterialId())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTE_MATERIAL_DUPLICATE);
@@ -108,7 +136,12 @@
    @Override
    public void updateQuote(SrmSupplierQuoteSaveReqVO updateReqVO) {
        // æ ¡éªŒæŠ•标状态(撤标后不允许报价)
        validateBidNotWithdrawn(updateReqVO.getBidId());
        SrmTenderBidDO bid = validateBidNotWithdrawn(updateReqVO.getBidId());
        // æ ¡éªŒæ‹›æ ‡é¡¹ç›®çŠ¶æ€ï¼šä»… PUBLISHED/BIDDING å…è®¸æŠ¥ä»·
        SrmTenderProjectDO project = tenderProjectMapper.selectById(bid.getTenderProjectId());
        if (project == null || !TenderStatusEnum.canCreateOrUpdateQuote(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_QUOTE_FORBIDDEN);
        }
        // æ ¡éªŒåŒä¸€æŠ•标下物料不可重复报价(排除自身)
        SrmSupplierQuoteDO existing = supplierQuoteMapper.selectById(updateReqVO.getId());
        if (existing != null && !existing.getTenderMaterialId().equals(updateReqVO.getTenderMaterialId())) {
@@ -143,9 +176,14 @@
        return bid;
    }
    private void validateTenderStatus(Long tenderProjectId) {
        if (tenderProjectMapper.selectById(tenderProjectId) == null) {
    private SrmTenderProjectDO validateTenderStatus(Long tenderProjectId) {
        SrmTenderProjectDO project = tenderProjectMapper.selectById(tenderProjectId);
        if (project == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NOT_EXISTS);
        }
        if (!TenderStatusEnum.canCreateBid(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_BID_FORBIDDEN);
        }
        return project;
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectService.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPageReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPartialUpdateReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectSaveReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderMaterialSaveReqVO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO;
@@ -14,6 +15,7 @@
    Long createTenderProject(@Valid SrmTenderProjectSaveReqVO createReqVO);
    void updateTenderProject(@Valid SrmTenderProjectSaveReqVO updateReqVO);
    void updateTenderProjectPartial(@Valid SrmTenderProjectPartialUpdateReqVO updateReqVO);
    void deleteTenderProject(Long id);
    SrmTenderProjectDO getTenderProject(Long id);
    PageResult<SrmTenderProjectDO> getTenderProjectPage(SrmTenderProjectPageReqVO pageReqVO);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java
@@ -4,17 +4,21 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderMaterialSaveReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPageReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPartialUpdateReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectSaveReqVO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmBidAwardDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderProjectDO;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmBidAwardMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderBidMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderMaterialMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderProjectMapper;
import cn.iocoder.yudao.module.srm.enums.AwardStatusEnum;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -23,6 +27,8 @@
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.module.srm.enums.LogRecordConstants.*;
@Service
@Validated
@@ -34,6 +40,8 @@
    private SrmTenderMaterialMapper tenderMaterialMapper;
    @Resource
    private SrmBidAwardMapper bidAwardMapper;
    @Resource
    private SrmTenderBidMapper tenderBidMapper;
    @Override
    public Long createTenderProject(SrmTenderProjectSaveReqVO createReqVO) {
@@ -44,13 +52,42 @@
    }
    @Override
    @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE,
            bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS)
    public void updateTenderProject(SrmTenderProjectSaveReqVO updateReqVO) {
        SrmTenderProjectDO project = validateTenderExists(updateReqVO.getId());
        if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT);
        Integer status = project.getTenderStatus();
        // è‰ç¨¿çŠ¶æ€ï¼šå…¨å­—æ®µå¯ç¼–è¾‘
        if (TenderStatusEnum.canEditFullProject(status)) {
            SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class);
            updateObj.setTenderStatus(status); // çŠ¶æ€ä¸å…è®¸å‰ç«¯ä¼ å…¥è¦†ç›–
            tenderProjectMapper.updateById(updateObj);
            LogRecordContext.putVariable("tenderName", project.getTenderName());
            LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
            LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
            LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements());
            LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements());
            return;
        }
        SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class);
        tenderProjectMapper.updateById(updateObj);
        // å‘布中/投标中:仅允许修改招标说明和资质要求
        if (TenderStatusEnum.canEditPartialProject(status)) {
            SrmTenderProjectDO updateObj = new SrmTenderProjectDO();
            updateObj.setId(updateReqVO.getId());
            updateObj.setTenderDesc(updateReqVO.getTenderDesc());
            updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements());
            tenderProjectMapper.updateById(updateObj);
            LogRecordContext.putVariable("tenderName", project.getTenderName());
            LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
            LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
            LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements());
            LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements());
            return;
        }
        // å…¶ä»–状态:不允许编辑
        throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED);
    }
    @Override
@@ -58,6 +95,10 @@
        SrmTenderProjectDO project = validateTenderExists(id);
        if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT);
        }
        // æ£€æŸ¥æ˜¯å¦å·²æœ‰æŠ•标数据
        if (tenderBidMapper.countByTenderProjectId(id) > 0) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_HAS_BIDS_CANNOT_DELETE);
        }
        tenderMaterialMapper.deleteByTenderProjectId(id);
        tenderProjectMapper.deleteById(id);
@@ -117,6 +158,10 @@
        if (TenderStatusEnum.CLOSED.getCode().equals(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_ALREADY_CLOSED);
        }
        // è‰ç¨¿çŠ¶æ€ä¸å…è®¸ç›´æŽ¥å…³é—­
        if (TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED);
        }
        project.setTenderStatus(TenderStatusEnum.CLOSED.getCode());
        tenderProjectMapper.updateById(project);
    }
@@ -141,22 +186,60 @@
        tenderProjectMapper.updateById(project);
    }
    @Override
    @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE,
            bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS)
    public void updateTenderProjectPartial(SrmTenderProjectPartialUpdateReqVO updateReqVO) {
        SrmTenderProjectDO project = validateTenderExists(updateReqVO.getId());
        if (!TenderStatusEnum.canEditPartialProject(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED);
        }
        SrmTenderProjectDO updateObj = new SrmTenderProjectDO();
        updateObj.setId(updateReqVO.getId());
        updateObj.setTenderDesc(updateReqVO.getTenderDesc());
        updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements());
        tenderProjectMapper.updateById(updateObj);
        LogRecordContext.putVariable("tenderName", project.getTenderName());
        LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
        LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
        LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements());
        LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements());
    }
    // ========== ç‰©æ–™å­è¡¨ ==========
    @Override
    public void createMaterial(SrmTenderMaterialSaveReqVO createReqVO) {
        SrmTenderProjectDO project = validateTenderExists(createReqVO.getTenderProjectId());
        if (!TenderStatusEnum.canEditMaterial(project.getTenderStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED);
        }
        SrmTenderMaterialDO material = BeanUtils.toBean(createReqVO, SrmTenderMaterialDO.class);
        tenderMaterialMapper.insert(material);
    }
    @Override
    public void updateMaterial(SrmTenderMaterialSaveReqVO updateReqVO) {
        SrmTenderMaterialDO existing = tenderMaterialMapper.selectById(updateReqVO.getId());
        if (existing != null) {
            SrmTenderProjectDO project = tenderProjectMapper.selectById(existing.getTenderProjectId());
            if (project != null && !TenderStatusEnum.canEditMaterial(project.getTenderStatus())) {
                throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED);
            }
        }
        SrmTenderMaterialDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderMaterialDO.class);
        tenderMaterialMapper.updateById(updateObj);
    }
    @Override
    public void deleteMaterial(Long id) {
        SrmTenderMaterialDO material = tenderMaterialMapper.selectById(id);
        if (material != null) {
            SrmTenderProjectDO project = tenderProjectMapper.selectById(material.getTenderProjectId());
            if (project != null && !TenderStatusEnum.canEditMaterial(project.getTenderStatus())) {
                throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED);
            }
        }
        tenderMaterialMapper.deleteById(id);
    }