liyong
5 天以前 f90d9f53beb844265c4153159aadb0baf81bafdb
Merge remote-tracking branch 'origin/dev_business' into dev_business

# Conflicts:
# yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java
已修改50个文件
已添加76个文件
6374 ■■■■■ 文件已修改
.mcp.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
.mcp.json.tmp 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-40-35-404Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-42-11-724Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-42-36-804Z.yml 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-43-35-225Z.yml 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-47-24-223Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-47-38-983Z.yml 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-47-57-125Z.yml 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-51-00-932Z.yml 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-56-04-077Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T06-56-14-335Z.yml 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T07-10-45-745Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T07-12-31-548Z.yml 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T07-14-24-445Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T07-14-44-587Z.yml 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-07-29T07-14-58-476Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/aftersales_frontend_integration.md 636 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/crm_customer_view_all.md 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/document_audit_project_filter_fix.md 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/machinery_new_fields_frontend.md 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/pro_archive_integration.md 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/pro_task_calculate_duration.md 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/aftersales_init.sql 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pd_menu_init.sql 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales-api/pom.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApi.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleReturnRespDTO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleTicketRespDTO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/pom.xml 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApiImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/AfterSaleReturnController.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnPageReqVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnRespVO.java 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnSaveReqVO.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/permission/AfterSalePermissionDO.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/repair/AfterSaleRepairDO.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnDO.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnItemDO.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketItemDO.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/permission/AfterSalePermissionMapper.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/repair/AfterSaleRepairMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnItemMapper.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnMapper.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketItemMapper.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/RedisKeyConstants.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleBizTypeEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleIssueTypeEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRepairStatusEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleReturnStatusEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSalePermissionLevelEnum.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSaleSceneTypeEnum.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionService.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionServiceImpl.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/bo/AfterSalePermissionCreateReqBO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairService.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnService.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/util/AfterSalePermissionUtils.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmSceneTypeEnum.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/MesDvMachineryController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryImportExcelVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryRespVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachinerySaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/documentaudit/vo/MesPdDocumentAuditPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/machinery/MesDvMachineryMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pd/documentaudit/MesPdDocumentAuditMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | 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/dv/machinery/MesDvMachineryServiceImpl.java 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/documentaudit/MesPdDocumentAuditServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidAwardController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidEvaluationController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderBidController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmSupplierQuoteRespVO.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 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidEvaluationService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidEvaluationServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.mcp.json
@@ -1,7 +1,7 @@
{
  "mcpServers": {
    "idea": {
      "url": "http://127.0.0.1:64342/stream",
      "url": "http://127.0.0.1:64417/stream",
      "type": "http"
    }
  }
.mcp.json.tmp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
{
  "mcpServers": {
    "idea": {
      "url": "http://127.0.0.1:64417/stream",
      "type": "http"
    }
  }
}
.playwright-mcp/page-2026-07-29T06-40-35-404Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
.playwright-mcp/page-2026-07-29T06-42-11-724Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
.playwright-mcp/page-2026-07-29T06-42-36-804Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
- generic [ref=e7]:
  - complementary [ref=e8]:
    - generic [ref=e9]:
      - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工厂数字化MOM系统" [ref=e18]
        - generic [ref=e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
      - menu [ref=e22]:
        - listitem [ref=e23]:
          - generic [ref=e24] [cursor=pointer]: æ¦‚览
        - listitem [ref=e34]:
          - generic [ref=e35] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=e43]:
          - generic [ref=e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=e50]:
          - generic [ref=e51] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=e57]:
          - generic [ref=e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=e65]:
          - generic [ref=e66] [cursor=pointer]: ä»“储物流
        - listitem [ref=e74]:
          - generic [ref=e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=e82]:
          - generic [ref=e83] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=e89]:
          - generic [ref=e90] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=e96]:
          - generic [ref=e97] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=e103]:
          - generic [ref=e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=e110]:
          - generic [ref=e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=e117]:
          - generic [ref=e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=e131]:
    - generic [ref=e132]:
      - banner [ref=e133]:
        - button [ref=e134] [cursor=pointer]
        - button [ref=e137] [cursor=pointer]
        - navigation "breadcrumb" [ref=e142]:
          - list [ref=e143]:
            - listitem [ref=e144]:
              - link [ref=e145] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=e152]
            - listitem [ref=e155]:
              - link "招标详情" [disabled] [ref=e156]
        - generic [ref=e159]:
          - generic [ref=e161] [cursor=pointer]:
            - generic [ref=e165]: æœç´¢
            - generic [ref=e166]: Ctrl K
          - button [ref=e168] [cursor=pointer]
          - button "dark" [ref=e173] [cursor=pointer]
          - button [ref=e182] [cursor=pointer]
          - button [ref=e188] [cursor=pointer]
          - button [ref=e191] [cursor=pointer]
          - button [ref=e197] [cursor=pointer]:
            - button [ref=e199]
          - button "工作台" [ref=e203] [cursor=pointer]
          - button "源码" [ref=e207] [cursor=pointer]
      - generic [ref=e214]:
        - generic [ref=e219]:
          - generic [ref=e220] [cursor=pointer]: åˆ†æžé¡µ
          - generic [ref=e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
        - button [ref=e254] [cursor=pointer]
    - main [ref=e272]:
      - generic [ref=e275]:
        - generic [ref=e276]:
          - button [ref=e277] [cursor=pointer]
          - generic [ref=e280]: æµ‹è¯•项目
          - generic [ref=e281]: æµ‹è¯•编号
          - generic [ref=e282]: è¯„标中
          - generic [ref=e283]: å…¬å¼€æ‹›æ ‡
          - button "刷新" [ref=e284] [cursor=pointer]
        - generic [ref=e290]:
          - generic:
            - generic "草稿"
            - generic "发布"
            - generic "投标中"
            - generic "评标中"
            - generic "已定标"
          - generic [ref=e291]:
            - generic [ref=e292]:
              - generic [ref=e293]: é¢„算金额
              - generic [ref=e294]: 15,000 å…ƒ
            - generic [ref=e295]:
              - generic [ref=e296]: è´Ÿè´£äºº
              - generic [ref=e297]: "-"
            - generic [ref=e298]:
              - generic [ref=e299]: é‡‡è´­ç»„织
              - generic [ref=e300]: "-"
            - generic [ref=e301]:
              - generic [ref=e302]: å¼€æ ‡æ—¶é—´
              - generic [ref=e303]: 2026-07-29 11:20:11
            - generic [ref=e304]:
              - generic [ref=e305]: å¼€å§‹æ—¶é—´
              - generic [ref=e306]: 2026-07-29 11:13:02
            - generic [ref=e307]:
              - generic [ref=e308]: æˆªæ­¢æ—¶é—´
              - generic [ref=e309]: 2026-07-29 19:13:06
        - generic [ref=e313]:
          - tablist [ref=e314]:
            - generic [ref=e316]:
              - tab [ref=e318] [cursor=pointer]
              - tab [ref=e323] [cursor=pointer]
              - tab [ref=e328] [cursor=pointer]
              - tab [ref=e333] [cursor=pointer]
              - tab [ref=e338] [cursor=pointer]
              - tab [active] [selected] [ref=e343] [cursor=pointer]
          - tabpanel "定标" [ref=e404]:
            - generic [ref=e405]:
              - generic "尚未定标"
.playwright-mcp/page-2026-07-29T06-43-35-225Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
- generic [ref=e7]:
  - complementary [ref=e8]:
    - generic [ref=e9]:
      - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工厂数字化MOM系统" [ref=e18]
        - generic [ref=e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
      - menu [ref=e22]:
        - listitem [ref=e23]:
          - generic [ref=e24] [cursor=pointer]: æ¦‚览
        - listitem [ref=e34]:
          - generic [ref=e35] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=e43]:
          - generic [ref=e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=e50]:
          - generic [ref=e51] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=e57]:
          - generic [ref=e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=e65]:
          - generic [ref=e66] [cursor=pointer]: ä»“储物流
        - listitem [ref=e74]:
          - generic [ref=e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=e82]:
          - generic [ref=e83] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=e89]:
          - generic [ref=e90] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=e96]:
          - generic [ref=e97] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=e103]:
          - generic [ref=e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=e110]:
          - generic [ref=e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=e117]:
          - generic [ref=e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=e131]:
    - generic [ref=e132]:
      - banner [ref=e133]:
        - button [ref=e134] [cursor=pointer]
        - button [ref=e137] [cursor=pointer]
        - navigation "breadcrumb" [ref=e142]:
          - list [ref=e143]:
            - listitem [ref=e144]:
              - link [ref=e145] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=e152]
            - listitem [ref=e155]:
              - link "招标详情" [disabled] [ref=e156]
        - generic [ref=e159]:
          - generic [ref=e161] [cursor=pointer]:
            - generic [ref=e165]: æœç´¢
            - generic [ref=e166]: Ctrl K
          - button [ref=e168] [cursor=pointer]
          - button "dark" [ref=e173] [cursor=pointer]
          - button [ref=e182] [cursor=pointer]
          - button [ref=e188] [cursor=pointer]
          - button [ref=e191] [cursor=pointer]
          - button [ref=e197] [cursor=pointer]:
            - button [ref=e199]
          - button "工作台" [ref=e203] [cursor=pointer]
          - button "源码" [ref=e207] [cursor=pointer]
      - generic [ref=e214]:
        - generic [ref=e219]:
          - generic [ref=e220] [cursor=pointer]: åˆ†æžé¡µ
          - generic [ref=e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
        - button [ref=e254] [cursor=pointer]
    - main [ref=e272]:
      - generic [ref=e275]:
        - generic [ref=e276]:
          - button [ref=e277] [cursor=pointer]
          - generic [ref=e280]: æµ‹è¯•项目
          - generic [ref=e281]: æµ‹è¯•编号
          - generic [ref=e282]: è¯„标中
          - generic [ref=e283]: å…¬å¼€æ‹›æ ‡
          - button "刷新" [ref=e284] [cursor=pointer]
        - generic [ref=e290]:
          - generic:
            - generic "草稿"
            - generic "发布"
            - generic "投标中"
            - generic "评标中"
            - generic "已定标"
          - generic [ref=e291]:
            - generic [ref=e292]:
              - generic [ref=e293]: é¢„算金额
              - generic [ref=e294]: 15,000 å…ƒ
            - generic [ref=e295]:
              - generic [ref=e296]: è´Ÿè´£äºº
              - generic [ref=e297]: "-"
            - generic [ref=e298]:
              - generic [ref=e299]: é‡‡è´­ç»„织
              - generic [ref=e300]: "-"
            - generic [ref=e301]:
              - generic [ref=e302]: å¼€æ ‡æ—¶é—´
              - generic [ref=e303]: 2026-07-29 11:20:11
            - generic [ref=e304]:
              - generic [ref=e305]: å¼€å§‹æ—¶é—´
              - generic [ref=e306]: 2026-07-29 11:13:02
            - generic [ref=e307]:
              - generic [ref=e308]: æˆªæ­¢æ—¶é—´
              - generic [ref=e309]: 2026-07-29 19:13:06
        - generic [ref=e313]:
          - tablist [ref=e314]:
            - generic [ref=e316]:
              - tab [ref=e318] [cursor=pointer]
              - tab [ref=e323] [cursor=pointer]
              - tab [ref=e328] [cursor=pointer]
              - tab [active] [selected] [ref=e333] [cursor=pointer]
              - tab [ref=e338] [cursor=pointer]
              - tab [ref=e343] [cursor=pointer]
          - tabpanel "开标" [ref=e406]:
            - generic [ref=e407]:
              - generic "尚未开标"
.playwright-mcp/page-2026-07-29T06-47-24-223Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
.playwright-mcp/page-2026-07-29T06-47-38-983Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
- generic [ref=f1e7]:
  - complementary [ref=f1e8]:
    - generic [ref=f1e9]:
      - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=f1e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工厂数字化MOM系统" [ref=f1e18]
        - generic [ref=f1e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
      - menu [ref=f1e22]:
        - listitem [ref=f1e23]:
          - generic [ref=f1e24] [cursor=pointer]: æ¦‚览
        - listitem [ref=f1e34]:
          - generic [ref=f1e35] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f1e43]:
          - generic [ref=f1e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f1e50]:
          - generic [ref=f1e51] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f1e57]:
          - generic [ref=f1e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f1e65]:
          - generic [ref=f1e66] [cursor=pointer]: ä»“储物流
        - listitem [ref=f1e74]:
          - generic [ref=f1e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f1e82]:
          - generic [ref=f1e83] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f1e89]:
          - generic [ref=f1e90] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=f1e96]:
          - generic [ref=f1e97] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f1e103]:
          - generic [ref=f1e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f1e110]:
          - generic [ref=f1e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f1e117]:
          - generic [ref=f1e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=f1e131]:
    - generic [ref=f1e132]:
      - banner [ref=f1e133]:
        - button [ref=f1e134] [cursor=pointer]
        - button [ref=f1e137] [cursor=pointer]
        - navigation "breadcrumb" [ref=f1e142]:
          - list [ref=f1e143]:
            - listitem [ref=f1e144]:
              - link [ref=f1e145] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f1e152]
            - listitem [ref=f1e155]:
              - link "招标详情" [disabled] [ref=f1e156]
        - generic [ref=f1e159]:
          - generic [ref=f1e161] [cursor=pointer]:
            - generic [ref=f1e165]: æœç´¢
            - generic [ref=f1e166]: Ctrl K
          - button [ref=f1e168] [cursor=pointer]
          - button "dark" [ref=f1e173] [cursor=pointer]
          - button [ref=f1e182] [cursor=pointer]
          - button [ref=f1e188] [cursor=pointer]
          - button [ref=f1e191] [cursor=pointer]
          - button [ref=f1e197] [cursor=pointer]:
            - button [ref=f1e199]
          - button "工作台" [ref=f1e203] [cursor=pointer]
          - button "源码" [ref=f1e207] [cursor=pointer]
      - generic [ref=f1e214]:
        - generic [ref=f1e219]:
          - generic [ref=f1e220] [cursor=pointer]: åˆ†æžé¡µ
          - generic [ref=f1e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
        - button [ref=f1e254] [cursor=pointer]
    - main [ref=f1e272]:
      - generic [ref=f1e275]:
        - generic [ref=f1e276]:
          - button [ref=f1e277] [cursor=pointer]
          - generic [ref=f1e280]: æµ‹è¯•项目
          - generic [ref=f1e281]: æµ‹è¯•编号
          - generic [ref=f1e282]: è¯„标中
          - generic [ref=f1e283]: å…¬å¼€æ‹›æ ‡
          - button "刷新" [ref=f1e284] [cursor=pointer]
        - generic [ref=f1e290]:
          - generic:
            - generic "草稿"
            - generic "发布"
            - generic "投标中"
            - generic "评标中"
            - generic "已定标"
          - generic [ref=f1e291]:
            - generic [ref=f1e292]:
              - generic [ref=f1e293]: é¢„算金额
              - generic [ref=f1e294]: 15,000 å…ƒ
            - generic [ref=f1e295]:
              - generic [ref=f1e296]: è´Ÿè´£äºº
              - generic [ref=f1e297]: "-"
            - generic [ref=f1e298]:
              - generic [ref=f1e299]: é‡‡è´­ç»„织
              - generic [ref=f1e300]: "-"
            - generic [ref=f1e301]:
              - generic [ref=f1e302]: å¼€æ ‡æ—¶é—´
              - generic [ref=f1e303]: 2026-07-29 11:20:11
            - generic [ref=f1e304]:
              - generic [ref=f1e305]: å¼€å§‹æ—¶é—´
              - generic [ref=f1e306]: 2026-07-29 11:13:02
            - generic [ref=f1e307]:
              - generic [ref=f1e308]: æˆªæ­¢æ—¶é—´
              - generic [ref=f1e309]: 2026-07-29 19:13:06
        - generic [ref=f1e313]:
          - tablist [ref=f1e314]:
            - generic [ref=f1e316]:
              - tab [ref=f1e318] [cursor=pointer]
              - tab [ref=f1e323] [cursor=pointer]
              - tab [ref=f1e328] [cursor=pointer]
              - tab [ref=f1e333] [cursor=pointer]
              - tab [ref=f1e338] [cursor=pointer]
              - tab [active] [selected] [ref=f1e343] [cursor=pointer]
          - tabpanel "定标" [ref=f1e404]:
            - generic [ref=f1e405]:
              - generic "尚未定标"
              - button "创建定标" [ref=f1e406] [cursor=pointer]
.playwright-mcp/page-2026-07-29T06-47-57-125Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,152 @@
- generic [ref=f1e4]:
  - generic [ref=f1e7]:
    - complementary [ref=f1e8]:
      - generic [ref=f1e9]:
        - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=f1e15] [cursor=pointer]:
          - /url: javascript:void 0
          - img "工厂数字化MOM系统" [ref=f1e18]
          - generic [ref=f1e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
        - menu [ref=f1e22]:
          - listitem [ref=f1e23]:
            - generic [ref=f1e24] [cursor=pointer]: æ¦‚览
          - listitem [ref=f1e34]:
            - generic [ref=f1e35] [cursor=pointer]: åŸºç¡€æ•°æ®
          - listitem [ref=f1e43]:
            - generic [ref=f1e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
          - listitem [ref=f1e50]:
            - generic [ref=f1e51] [cursor=pointer]: è¥é”€ç®¡ç†
          - listitem [ref=f1e57]:
            - generic [ref=f1e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
          - listitem [ref=f1e65]:
            - generic [ref=f1e66] [cursor=pointer]: ä»“储物流
          - listitem [ref=f1e74]:
            - generic [ref=f1e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
          - listitem [ref=f1e82]:
            - generic [ref=f1e83] [cursor=pointer]: è´¨é‡ç®¡ç†
          - listitem [ref=f1e89]:
            - generic [ref=f1e90] [cursor=pointer]: ååŒåŠžå…¬
          - listitem [ref=f1e96]:
            - generic [ref=f1e97] [cursor=pointer]: äººåŠ›èµ„æº
          - listitem [ref=f1e103]:
            - generic [ref=f1e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
          - listitem [ref=f1e110]:
            - generic [ref=f1e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
          - listitem [ref=f1e117]:
            - generic [ref=f1e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
    - generic [ref=f1e131]:
      - generic [ref=f1e132]:
        - banner [ref=f1e133]:
          - button [ref=f1e134] [cursor=pointer]
          - button [ref=f1e137] [cursor=pointer]
          - navigation "breadcrumb" [ref=f1e142]:
            - list [ref=f1e143]:
              - listitem [ref=f1e144]:
                - link [ref=f1e145] [cursor=pointer]:
                  - /url: javascript:void 0
                - listitem [ref=f1e152]
              - listitem [ref=f1e155]:
                - link "招标详情" [disabled] [ref=f1e156]
          - generic [ref=f1e159]:
            - generic [ref=f1e161] [cursor=pointer]:
              - generic [ref=f1e165]: æœç´¢
              - generic [ref=f1e166]: Ctrl K
            - button [ref=f1e168] [cursor=pointer]
            - button "dark" [ref=f1e173] [cursor=pointer]
            - button [ref=f1e182] [cursor=pointer]
            - button [ref=f1e188] [cursor=pointer]
            - button [ref=f1e191] [cursor=pointer]
            - button [ref=f1e197] [cursor=pointer]:
              - button [ref=f1e199]
            - button "工作台" [ref=f1e203] [cursor=pointer]
            - button "源码" [ref=f1e207] [cursor=pointer]
        - generic [ref=f1e214]:
          - generic [ref=f1e219]:
            - generic [ref=f1e220] [cursor=pointer]: åˆ†æžé¡µ
            - generic [ref=f1e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
          - button [ref=f1e254] [cursor=pointer]
      - main [ref=f1e272]:
        - generic [ref=f1e275]:
          - generic [ref=f1e276]:
            - button [ref=f1e277] [cursor=pointer]
            - generic [ref=f1e280]: æµ‹è¯•项目
            - generic [ref=f1e281]: æµ‹è¯•编号
            - generic [ref=f1e282]: è¯„标中
            - generic [ref=f1e283]: å…¬å¼€æ‹›æ ‡
            - button "刷新" [ref=f1e284] [cursor=pointer]
          - generic [ref=f1e290]:
            - generic:
              - generic "草稿"
              - generic "发布"
              - generic "投标中"
              - generic "评标中"
              - generic "已定标"
            - generic [ref=f1e291]:
              - generic [ref=f1e292]:
                - generic [ref=f1e293]: é¢„算金额
                - generic [ref=f1e294]: 15,000 å…ƒ
              - generic [ref=f1e295]:
                - generic [ref=f1e296]: è´Ÿè´£äºº
                - generic [ref=f1e297]: "-"
              - generic [ref=f1e298]:
                - generic [ref=f1e299]: é‡‡è´­ç»„织
                - generic [ref=f1e300]: "-"
              - generic [ref=f1e301]:
                - generic [ref=f1e302]: å¼€æ ‡æ—¶é—´
                - generic [ref=f1e303]: 2026-07-29 11:20:11
              - generic [ref=f1e304]:
                - generic [ref=f1e305]: å¼€å§‹æ—¶é—´
                - generic [ref=f1e306]: 2026-07-29 11:13:02
              - generic [ref=f1e307]:
                - generic [ref=f1e308]: æˆªæ­¢æ—¶é—´
                - generic [ref=f1e309]: 2026-07-29 19:13:06
          - generic [ref=f1e313]:
            - tablist [ref=f1e314]:
              - generic [ref=f1e316]:
                - tab [ref=f1e318] [cursor=pointer]
                - tab [ref=f1e323] [cursor=pointer]
                - tab [ref=f1e328] [cursor=pointer]
                - tab [ref=f1e333] [cursor=pointer]
                - tab [ref=f1e338] [cursor=pointer]
                - tab [selected] [ref=f1e343] [cursor=pointer]
            - tabpanel "定标" [ref=f1e404]:
              - generic [ref=f1e405]:
                - generic "尚未定标"
                - button "创建定标" [active] [ref=f1e406] [cursor=pointer]
  - dialog "创建定标" [ref=f1e412]:
    - document:
      - generic [ref=f1e413]:
        - button "Close" [ref=f1e414] [cursor=pointer]:
          - img "close" [ref=f1e416]
        - generic [ref=f1e419]: åˆ›å»ºå®šæ ‡
        - generic [ref=f1e422]:
          - generic [ref=f1e424]:
            - generic "定标编号" [ref=f1e426]: "* å®šæ ‡ç¼–号"
            - textbox "如 AWARD2024010001" [ref=f1e430]
          - generic [ref=f1e431]:
            - generic [ref=f1e434]:
              - generic "供应商" [ref=f1e436]: "* ä¾›åº”商"
              - generic [ref=f1e441]:
                - combobox [ref=f1e443]
                - generic "0" [ref=f1e444]
            - generic [ref=f1e447]:
              - generic "投标" [ref=f1e449]: "* æŠ•æ ‡"
              - generic [ref=f1e454]:
                - combobox [ref=f1e456]
                - generic "0" [ref=f1e457]
          - generic [ref=f1e459]:
            - generic "定标金额" [ref=f1e461]: "* å®šæ ‡é‡‘额"
            - generic [ref=f1e466]:
              - generic [ref=f1e467]:
                - generic [ref=f1e468]:
                  - button "Increase Value" [ref=f1e469] [cursor=pointer]:
                    - img "up" [ref=f1e470]
                  - button "Decrease Value" [ref=f1e473] [cursor=pointer]:
                    - img "down" [ref=f1e474]
                - spinbutton "定标金额" [ref=f1e478]
              - generic [ref=f1e479]: å…ƒ
          - generic [ref=f1e481]:
            - generic "备注" [ref=f1e483]
            - textbox "如:综合评标第一名" [ref=f1e487]
        - generic [ref=f1e488]:
          - button "取 æ¶ˆ" [ref=f1e489] [cursor=pointer]
          - button "ç¡® å®š" [ref=f1e491] [cursor=pointer]
.playwright-mcp/page-2026-07-29T06-51-00-932Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,152 @@
- generic [ref=f1e4]:
  - generic [ref=f1e7]:
    - complementary [ref=f1e8]:
      - generic [ref=f1e9]:
        - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=f1e15] [cursor=pointer]:
          - /url: javascript:void 0
          - img "工厂数字化MOM系统" [ref=f1e18]
          - generic [ref=f1e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
        - menu [ref=f1e22]:
          - listitem [ref=f1e23]:
            - generic [ref=f1e24] [cursor=pointer]: æ¦‚览
          - listitem [ref=f1e34]:
            - generic [ref=f1e35] [cursor=pointer]: åŸºç¡€æ•°æ®
          - listitem [ref=f1e43]:
            - generic [ref=f1e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
          - listitem [ref=f1e50]:
            - generic [ref=f1e51] [cursor=pointer]: è¥é”€ç®¡ç†
          - listitem [ref=f1e57]:
            - generic [ref=f1e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
          - listitem [ref=f1e65]:
            - generic [ref=f1e66] [cursor=pointer]: ä»“储物流
          - listitem [ref=f1e74]:
            - generic [ref=f1e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
          - listitem [ref=f1e82]:
            - generic [ref=f1e83] [cursor=pointer]: è´¨é‡ç®¡ç†
          - listitem [ref=f1e89]:
            - generic [ref=f1e90] [cursor=pointer]: ååŒåŠžå…¬
          - listitem [ref=f1e96]:
            - generic [ref=f1e97] [cursor=pointer]: äººåŠ›èµ„æº
          - listitem [ref=f1e103]:
            - generic [ref=f1e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
          - listitem [ref=f1e110]:
            - generic [ref=f1e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
          - listitem [ref=f1e117]:
            - generic [ref=f1e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
    - generic [ref=f1e131]:
      - generic [ref=f1e132]:
        - banner [ref=f1e133]:
          - button [ref=f1e134] [cursor=pointer]
          - button [ref=f1e137] [cursor=pointer]
          - navigation "breadcrumb" [ref=f1e142]:
            - list [ref=f1e143]:
              - listitem [ref=f1e144]:
                - link [ref=f1e145] [cursor=pointer]:
                  - /url: javascript:void 0
                - listitem [ref=f1e152]
              - listitem [ref=f1e155]:
                - link "招标详情" [disabled] [ref=f1e156]
          - generic [ref=f1e159]:
            - generic [ref=f1e161] [cursor=pointer]:
              - generic [ref=f1e165]: æœç´¢
              - generic [ref=f1e166]: Ctrl K
            - button [ref=f1e168] [cursor=pointer]
            - button "dark" [ref=f1e173] [cursor=pointer]
            - button [ref=f1e182] [cursor=pointer]
            - button [ref=f1e188] [cursor=pointer]
            - button [ref=f1e191] [cursor=pointer]
            - button [ref=f1e197] [cursor=pointer]:
              - button [ref=f1e199]
            - button "工作台" [ref=f1e203] [cursor=pointer]
            - button "源码" [ref=f1e207] [cursor=pointer]
        - generic [ref=f1e214]:
          - generic [ref=f1e219]:
            - generic [ref=f1e220] [cursor=pointer]: åˆ†æžé¡µ
            - generic [ref=f1e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
          - button [ref=f1e254] [cursor=pointer]
      - main [ref=f1e272]:
        - generic [ref=f1e275]:
          - generic [ref=f1e276]:
            - button [ref=f1e277] [cursor=pointer]
            - generic [ref=f1e280]: æµ‹è¯•项目
            - generic [ref=f1e281]: æµ‹è¯•编号
            - generic [ref=f1e282]: è¯„标中
            - generic [ref=f1e283]: å…¬å¼€æ‹›æ ‡
            - button "刷新" [ref=f1e284] [cursor=pointer]
          - generic [ref=f1e290]:
            - generic:
              - generic "草稿"
              - generic "发布"
              - generic "投标中"
              - generic "评标中"
              - generic "已定标"
            - generic [ref=f1e291]:
              - generic [ref=f1e292]:
                - generic [ref=f1e293]: é¢„算金额
                - generic [ref=f1e294]: 15,000 å…ƒ
              - generic [ref=f1e295]:
                - generic [ref=f1e296]: è´Ÿè´£äºº
                - generic [ref=f1e297]: "-"
              - generic [ref=f1e298]:
                - generic [ref=f1e299]: é‡‡è´­ç»„织
                - generic [ref=f1e300]: "-"
              - generic [ref=f1e301]:
                - generic [ref=f1e302]: å¼€æ ‡æ—¶é—´
                - generic [ref=f1e303]: 2026-07-29 11:20:11
              - generic [ref=f1e304]:
                - generic [ref=f1e305]: å¼€å§‹æ—¶é—´
                - generic [ref=f1e306]: 2026-07-29 11:13:02
              - generic [ref=f1e307]:
                - generic [ref=f1e308]: æˆªæ­¢æ—¶é—´
                - generic [ref=f1e309]: 2026-07-29 19:13:06
          - generic [ref=f1e313]:
            - tablist [ref=f1e314]:
              - generic [ref=f1e316]:
                - tab [ref=f1e318] [cursor=pointer]
                - tab [ref=f1e323] [cursor=pointer]
                - tab [ref=f1e328] [cursor=pointer]
                - tab [ref=f1e333] [cursor=pointer]
                - tab [ref=f1e338] [cursor=pointer]
                - tab [selected] [ref=f1e343] [cursor=pointer]
            - tabpanel "定标" [ref=f1e404]:
              - generic [ref=f1e405]:
                - generic "尚未定标"
                - button "创建定标" [ref=f1e406] [cursor=pointer]
  - dialog "创建定标" [ref=f1e412]:
    - document:
      - generic [ref=f1e413]:
        - button "Close" [ref=f1e414] [cursor=pointer]:
          - img "close" [ref=f1e416]
        - generic [ref=f1e419]: åˆ›å»ºå®šæ ‡
        - generic [ref=f1e422]:
          - generic [ref=f1e424]:
            - generic "定标编号" [ref=f1e426]: "* å®šæ ‡ç¼–号"
            - textbox "如 AWARD2024010001" [ref=f1e430]
          - generic [ref=f1e431]:
            - generic [ref=f1e434]:
              - generic "供应商" [ref=f1e436]: "* ä¾›åº”商"
              - generic [ref=f1e441]:
                - combobox [ref=f1e443]
                - generic "0" [ref=f1e444]
            - generic [ref=f1e447]:
              - generic "投标" [ref=f1e449]: "* æŠ•æ ‡"
              - generic [ref=f1e454]:
                - combobox [ref=f1e456]
                - generic "0" [ref=f1e457]
          - generic [ref=f1e459]:
            - generic "定标金额" [ref=f1e461]: "* å®šæ ‡é‡‘额"
            - generic [ref=f1e466]:
              - generic [ref=f1e467]:
                - generic [ref=f1e468]:
                  - button "Increase Value" [ref=f1e469] [cursor=pointer]:
                    - img "up" [ref=f1e470]
                  - button "Decrease Value" [ref=f1e473] [cursor=pointer]:
                    - img "down" [ref=f1e474]
                - spinbutton "定标金额" [ref=f1e478]
              - generic [ref=f1e479]: å…ƒ
          - generic [ref=f1e481]:
            - generic "备注" [ref=f1e483]
            - textbox "如:综合评标第一名" [ref=f1e487]
        - generic [ref=f1e488]:
          - button "取 æ¶ˆ" [ref=f1e489] [cursor=pointer]
          - button "ç¡® å®š" [ref=f1e491] [cursor=pointer]
.playwright-mcp/page-2026-07-29T06-56-04-077Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
.playwright-mcp/page-2026-07-29T06-56-14-335Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
- generic [ref=f2e7]:
  - complementary [ref=f2e8]:
    - generic [ref=f2e9]:
      - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=f2e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工厂数字化MOM系统" [ref=f2e18]
        - generic [ref=f2e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
      - menu [ref=f2e22]:
        - listitem [ref=f2e23]:
          - generic [ref=f2e24] [cursor=pointer]: æ¦‚览
        - listitem [ref=f2e34]:
          - generic [ref=f2e35] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f2e43]:
          - generic [ref=f2e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f2e50]:
          - generic [ref=f2e51] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f2e57]:
          - generic [ref=f2e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f2e65]:
          - generic [ref=f2e66] [cursor=pointer]: ä»“储物流
        - listitem [ref=f2e74]:
          - generic [ref=f2e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f2e82]:
          - generic [ref=f2e83] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f2e89]:
          - generic [ref=f2e90] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=f2e96]:
          - generic [ref=f2e97] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f2e103]:
          - generic [ref=f2e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f2e110]:
          - generic [ref=f2e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f2e117]:
          - generic [ref=f2e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=f2e131]:
    - generic [ref=f2e132]:
      - banner [ref=f2e133]:
        - button [ref=f2e134] [cursor=pointer]
        - button [ref=f2e137] [cursor=pointer]
        - navigation "breadcrumb" [ref=f2e142]:
          - list [ref=f2e143]:
            - listitem [ref=f2e144]:
              - link [ref=f2e145] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f2e152]
            - listitem [ref=f2e155]:
              - link "招标详情" [disabled] [ref=f2e156]
        - generic [ref=f2e159]:
          - generic [ref=f2e161] [cursor=pointer]:
            - generic [ref=f2e165]: æœç´¢
            - generic [ref=f2e166]: Ctrl K
          - button [ref=f2e168] [cursor=pointer]
          - button "dark" [ref=f2e173] [cursor=pointer]
          - button [ref=f2e182] [cursor=pointer]
          - button [ref=f2e188] [cursor=pointer]
          - button [ref=f2e191] [cursor=pointer]
          - button [ref=f2e197] [cursor=pointer]:
            - button [ref=f2e199]
          - button "工作台" [ref=f2e203] [cursor=pointer]
          - button "源码" [ref=f2e207] [cursor=pointer]
      - generic [ref=f2e214]:
        - generic [ref=f2e219]:
          - generic [ref=f2e220] [cursor=pointer]: åˆ†æžé¡µ
          - generic [ref=f2e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
        - button [ref=f2e254] [cursor=pointer]
    - main [ref=f2e272]:
      - generic [ref=f2e275]:
        - generic [ref=f2e276]:
          - button [ref=f2e277] [cursor=pointer]
          - generic [ref=f2e280]: æµ‹è¯•项目
          - generic [ref=f2e281]: æµ‹è¯•编号
          - generic [ref=f2e282]: å·²å®šæ ‡
          - generic [ref=f2e283]: å…¬å¼€æ‹›æ ‡
          - button "刷新" [ref=f2e284] [cursor=pointer]
        - generic [ref=f2e290]:
          - generic:
            - generic "草稿"
            - generic "发布"
            - generic "投标中"
            - generic "评标中"
            - generic "已定标"
          - generic [ref=f2e291]:
            - generic [ref=f2e292]:
              - generic [ref=f2e293]: é¢„算金额
              - generic [ref=f2e294]: 15,000 å…ƒ
            - generic [ref=f2e295]:
              - generic [ref=f2e296]: è´Ÿè´£äºº
              - generic [ref=f2e297]: "-"
            - generic [ref=f2e298]:
              - generic [ref=f2e299]: é‡‡è´­ç»„织
              - generic [ref=f2e300]: "-"
            - generic [ref=f2e301]:
              - generic [ref=f2e302]: å¼€æ ‡æ—¶é—´
              - generic [ref=f2e303]: 2026-07-29 11:20:11
            - generic [ref=f2e304]:
              - generic [ref=f2e305]: å¼€å§‹æ—¶é—´
              - generic [ref=f2e306]: 2026-07-29 11:13:02
            - generic [ref=f2e307]:
              - generic [ref=f2e308]: æˆªæ­¢æ—¶é—´
              - generic [ref=f2e309]: 2026-07-29 19:13:06
        - generic [ref=f2e313]:
          - tablist [ref=f2e314]:
            - generic [ref=f2e316]:
              - tab [ref=f2e318] [cursor=pointer]
              - tab [ref=f2e323] [cursor=pointer]
              - tab [ref=f2e328] [cursor=pointer]
              - tab [ref=f2e333] [cursor=pointer]
              - tab [ref=f2e338] [cursor=pointer]
              - tab [active] [selected] [ref=f2e343] [cursor=pointer]
          - tabpanel "定标" [ref=f2e350]:
            - generic [ref=f2e351]:
              - generic [ref=f2e353]:
                - generic [ref=f2e354]: å·²å®šè¡¨
                - generic [ref=f2e355]: "12"
                - generic [ref=f2e357]: 10,000 å…ƒ
                - text: 2026-07-29 14:52:43
                - generic [ref=f2e358]: å·²å®šæ ‡
                - text: å¾…生成综合评定
              - generic [ref=f2e361]:
                - generic "定标完成"
                - button "生成采购订单" [ref=f2e362] [cursor=pointer]
                - button "删除定标" [ref=f2e366] [cursor=pointer]
.playwright-mcp/page-2026-07-29T07-10-45-745Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
.playwright-mcp/page-2026-07-29T07-12-31-548Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
- generic [ref=f3e7]:
  - complementary [ref=f3e8]:
    - generic [ref=f3e9]:
      - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=f3e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工厂数字化MOM系统" [ref=f3e18]
        - generic [ref=f3e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
      - menu [ref=f3e22]:
        - listitem [ref=f3e23]:
          - generic [ref=f3e24] [cursor=pointer]: æ¦‚览
        - listitem [ref=f3e34]:
          - generic [ref=f3e35] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f3e43]:
          - generic [ref=f3e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f3e50]:
          - generic [ref=f3e51] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f3e57]:
          - generic [ref=f3e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f3e65]:
          - generic [ref=f3e66] [cursor=pointer]: ä»“储物流
        - listitem [ref=f3e74]:
          - generic [ref=f3e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f3e82]:
          - generic [ref=f3e83] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f3e89]:
          - generic [ref=f3e90] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=f3e96]:
          - generic [ref=f3e97] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f3e103]:
          - generic [ref=f3e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f3e110]:
          - generic [ref=f3e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f3e117]:
          - generic [ref=f3e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=f3e131]:
    - generic [ref=f3e132]:
      - banner [ref=f3e133]:
        - button [ref=f3e134] [cursor=pointer]
        - button [ref=f3e137] [cursor=pointer]
        - navigation "breadcrumb" [ref=f3e142]:
          - list [ref=f3e143]:
            - listitem [ref=f3e144]:
              - link [ref=f3e145] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f3e152]
            - listitem [ref=f3e155]:
              - link "招标详情" [disabled] [ref=f3e156]
        - generic [ref=f3e159]:
          - generic [ref=f3e161] [cursor=pointer]:
            - generic [ref=f3e165]: æœç´¢
            - generic [ref=f3e166]: Ctrl K
          - button [ref=f3e168] [cursor=pointer]
          - button "dark" [ref=f3e173] [cursor=pointer]
          - button [ref=f3e182] [cursor=pointer]
          - button [ref=f3e188] [cursor=pointer]
          - button [ref=f3e191] [cursor=pointer]
          - button [ref=f3e197] [cursor=pointer]:
            - button [ref=f3e199]
          - button "工作台" [ref=f3e203] [cursor=pointer]
          - button "源码" [ref=f3e207] [cursor=pointer]
      - generic [ref=f3e214]:
        - generic [ref=f3e219]:
          - generic [ref=f3e220] [cursor=pointer]: åˆ†æžé¡µ
          - generic [ref=f3e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
        - button [ref=f3e254] [cursor=pointer]
    - main [ref=f3e272]:
      - generic [ref=f3e275]:
        - generic [ref=f3e276]:
          - button [ref=f3e277] [cursor=pointer]
          - generic [ref=f3e280]: æµ‹è¯•项目
          - generic [ref=f3e281]: æµ‹è¯•编号
          - generic [ref=f3e282]: å·²å®šæ ‡
          - generic [ref=f3e283]: å…¬å¼€æ‹›æ ‡
          - button "刷新" [ref=f3e284] [cursor=pointer]
        - generic [ref=f3e290]:
          - generic:
            - generic "草稿"
            - generic "发布"
            - generic "投标中"
            - generic "评标中"
            - generic "已定标"
          - generic [ref=f3e291]:
            - generic [ref=f3e292]:
              - generic [ref=f3e293]: é¢„算金额
              - generic [ref=f3e294]: 15,000 å…ƒ
            - generic [ref=f3e295]:
              - generic [ref=f3e296]: è´Ÿè´£äºº
              - generic [ref=f3e297]: "-"
            - generic [ref=f3e298]:
              - generic [ref=f3e299]: é‡‡è´­ç»„织
              - generic [ref=f3e300]: "-"
            - generic [ref=f3e301]:
              - generic [ref=f3e302]: å¼€æ ‡æ—¶é—´
              - generic [ref=f3e303]: 2026-07-29 11:20:11
            - generic [ref=f3e304]:
              - generic [ref=f3e305]: å¼€å§‹æ—¶é—´
              - generic [ref=f3e306]: 2026-07-29 11:13:02
            - generic [ref=f3e307]:
              - generic [ref=f3e308]: æˆªæ­¢æ—¶é—´
              - generic [ref=f3e309]: 2026-07-29 19:13:06
        - generic [ref=f3e313]:
          - tablist [ref=f3e314]:
            - generic [ref=f3e316]:
              - tab [ref=f3e318] [cursor=pointer]
              - tab [ref=f3e323] [cursor=pointer]
              - tab [ref=f3e328] [cursor=pointer]
              - tab [ref=f3e333] [cursor=pointer]
              - tab [ref=f3e338] [cursor=pointer]
              - tab [active] [selected] [ref=f3e343] [cursor=pointer]
          - tabpanel "定标" [ref=f3e404]:
            - generic [ref=f3e405]:
              - generic [ref=f3e407]:
                - generic [ref=f3e408]: å·²å®šè¡¨
                - generic [ref=f3e409]: "12"
                - generic [ref=f3e411]: 10,000 å…ƒ
                - text: 2026-07-29 14:52:43
                - generic [ref=f3e412]: å·²ç”Ÿæˆè®¢å•
                - text: 18综合评定
              - generic [ref=f3e415]:
                - generic "订单已生成"
                - button "删除定标" [ref=f3e416] [cursor=pointer]
.playwright-mcp/page-2026-07-29T07-14-24-445Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
.playwright-mcp/page-2026-07-29T07-14-44-587Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
- generic [ref=f4e7]:
  - complementary [ref=f4e8]:
    - generic [ref=f4e9]:
      - link "工厂数字化MOM系统 å·¥åŽ‚æ•°å­—åŒ–MOM系统" [ref=f4e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工厂数字化MOM系统" [ref=f4e18]
        - generic [ref=f4e19]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
      - menu [ref=f4e22]:
        - listitem [ref=f4e23]:
          - generic [ref=f4e24] [cursor=pointer]: æ¦‚览
        - listitem [ref=f4e34]:
          - generic [ref=f4e35] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f4e43]:
          - generic [ref=f4e44] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f4e50]:
          - generic [ref=f4e51] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f4e57]:
          - generic [ref=f4e58] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f4e65]:
          - generic [ref=f4e66] [cursor=pointer]: ä»“储物流
        - listitem [ref=f4e74]:
          - generic [ref=f4e75] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f4e82]:
          - generic [ref=f4e83] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f4e89]:
          - generic [ref=f4e90] [cursor=pointer]: ååŒåŠžå…¬
        - listitem [ref=f4e96]:
          - generic [ref=f4e97] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f4e103]:
          - generic [ref=f4e104] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f4e110]:
          - generic [ref=f4e111] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f4e117]:
          - generic [ref=f4e118] [cursor=pointer]: IM å³æ—¶é€šè®¯
  - generic [ref=f4e131]:
    - generic [ref=f4e132]:
      - banner [ref=f4e133]:
        - button [ref=f4e134] [cursor=pointer]
        - button [ref=f4e137] [cursor=pointer]
        - navigation "breadcrumb" [ref=f4e142]:
          - list [ref=f4e143]:
            - listitem [ref=f4e144]:
              - link [ref=f4e145] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f4e152]
            - listitem [ref=f4e155]:
              - link "招标详情" [disabled] [ref=f4e156]
        - generic [ref=f4e159]:
          - generic [ref=f4e161] [cursor=pointer]:
            - generic [ref=f4e165]: æœç´¢
            - generic [ref=f4e166]: Ctrl K
          - button [ref=f4e168] [cursor=pointer]
          - button "dark" [ref=f4e173] [cursor=pointer]
          - button [ref=f4e182] [cursor=pointer]
          - button [ref=f4e188] [cursor=pointer]
          - button [ref=f4e191] [cursor=pointer]
          - button [ref=f4e197] [cursor=pointer]:
            - button [ref=f4e199]
          - button "工作台" [ref=f4e203] [cursor=pointer]
          - button "源码" [ref=f4e207] [cursor=pointer]
      - generic [ref=f4e214]:
        - generic [ref=f4e219]:
          - generic [ref=f4e220] [cursor=pointer]: åˆ†æžé¡µ
          - generic [ref=f4e238] [cursor=pointer]: æ‹›æ ‡è¯¦æƒ…
        - button [ref=f4e254] [cursor=pointer]
    - main [ref=f4e272]:
      - generic [ref=f4e275]:
        - generic [ref=f4e276]:
          - button [ref=f4e277] [cursor=pointer]
          - generic [ref=f4e280]: æµ‹è¯•项目
          - generic [ref=f4e281]: æµ‹è¯•编号
          - generic [ref=f4e282]: å·²å®šæ ‡
          - generic [ref=f4e283]: å…¬å¼€æ‹›æ ‡
          - button "刷新" [ref=f4e284] [cursor=pointer]
        - generic [ref=f4e290]:
          - generic:
            - generic "草稿"
            - generic "发布"
            - generic "投标中"
            - generic "评标中"
            - generic "已定标"
          - generic [ref=f4e291]:
            - generic [ref=f4e292]:
              - generic [ref=f4e293]: é¢„算金额
              - generic [ref=f4e294]: 15,000 å…ƒ
            - generic [ref=f4e295]:
              - generic [ref=f4e296]: è´Ÿè´£äºº
              - generic [ref=f4e297]: "-"
            - generic [ref=f4e298]:
              - generic [ref=f4e299]: é‡‡è´­ç»„织
              - generic [ref=f4e300]: "-"
            - generic [ref=f4e301]:
              - generic [ref=f4e302]: å¼€æ ‡æ—¶é—´
              - generic [ref=f4e303]: 2026-07-29 11:20:11
            - generic [ref=f4e304]:
              - generic [ref=f4e305]: å¼€å§‹æ—¶é—´
              - generic [ref=f4e306]: 2026-07-29 11:13:02
            - generic [ref=f4e307]:
              - generic [ref=f4e308]: æˆªæ­¢æ—¶é—´
              - generic [ref=f4e309]: 2026-07-29 19:13:06
        - generic [ref=f4e313]:
          - tablist [ref=f4e314]:
            - generic [ref=f4e316]:
              - tab [ref=f4e318] [cursor=pointer]
              - tab [ref=f4e323] [cursor=pointer]
              - tab [ref=f4e328] [cursor=pointer]
              - tab [ref=f4e333] [cursor=pointer]
              - tab [ref=f4e338] [cursor=pointer]
              - tab [selected] [ref=f4e343] [cursor=pointer]
          - tabpanel "定标" [active] [ref=f4e404]:
            - generic [ref=f4e405]:
              - generic [ref=f4e407]:
                - generic [ref=f4e408]: AWARD2024070001
                - generic [ref=f4e409]: "12"
                - generic [ref=f4e411]: 10,000 å…ƒ
                - text: 2026-07-29 14:52:43
                - generic [ref=f4e412]: å·²ç”Ÿæˆè®¢å•
                - text: 18综合评定
              - generic [ref=f4e415]:
                - generic "订单已生成"
                - button "删除定标" [ref=f4e416] [cursor=pointer]
.playwright-mcp/page-2026-07-29T07-14-58-476Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥åŽ‚æ•°å­—åŒ–MOM系统
docs/aftersales_frontend_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,636 @@
# å”®åŽå·¥å•模块 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢ï¼ˆå…¨éƒ¨æ–°å»ºï¼‰
| é¡µé¢ | è·¯ç”±å»ºè®® | è¯´æ˜Ž |
|------|---------|------|
| å”®åŽå·¥å•列表 | `/aftersales/ticket` | å·¥å•列表、搜索、状态筛选 |
| å”®åŽå·¥å•详情/编辑 | `/aftersales/ticket/detail?id=:id` | æ–°å»º/编辑工单、查看详情 |
| é€€è´§ç”³è¯·åˆ—表 | `/aftersales/return` | é€€è´§ç”³è¯·åˆ—表、搜索 |
| é€€è´§ç”³è¯·è¯¦æƒ… | `/aftersales/return/detail?id=:id` | é€€è´§è¯¦æƒ…、审核操作 |
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
### æµç¨‹ä¸€ï¼šä¸€èˆ¬é—®é¢˜å·¥å•
```
创建工单 â†’ åˆ†é…å¤„理人 â†’ é—®é¢˜åˆ¤å®šï¼ˆä¸€èˆ¬é—®é¢˜ï¼‰â†’ å¤„理 â†’ å®Œç»“ â†’ å…³é—­
```
### æµç¨‹äºŒï¼šé€€è´§é—®é¢˜å·¥å•
```
创建工单 â†’ åˆ†é…å¤„理人 â†’ é—®é¢˜åˆ¤å®šï¼ˆé€€è´§é—®é¢˜ï¼‰
  â†’ åˆ›å»ºé€€è´§ç”³è¯· â†’ é”€å”®å®¡æ ¸ â†’ [MES退货流程自动执行] â†’ é€€æ¬¾/换货完成 â†’ å®Œç»“ â†’ å…³é—­
```
### æµç¨‹ä¸‰ï¼šç»´ä¿®é—®é¢˜å·¥å•
```
创建工单 â†’ åˆ†é…å¤„理人 â†’ é—®é¢˜åˆ¤å®šï¼ˆç»´ä¿®é—®é¢˜ï¼‰
  â†’ åˆ›å»ºç»´ä¿®è®°å½• â†’ ç»´ä¿®å®Œæˆ â†’ å®Œç»“ â†’ å…³é—­
```
### æ•°æ®å¸¦å…¥å…³ç³»
| æ­¥éª¤ | æ¥æº | å¸¦å…¥å­—段 |
|------|------|----------|
| åˆ›å»ºé€€è´§ç”³è¯· | å·¥å•详情 | `ticketId`、`customerId`、`saleOrderId`、工单明细行(`ticketItemId`、`itemId`、`itemCode`、`itemName`) |
| é€€è´§ç”³è¯·å®¡æ‰¹ | é€€è´§ç”³è¯· | `id`(通过 URL å‚数或按钮传入) |
**重点说明:**
- åˆ›å»ºé€€è´§ç”³è¯·æ—¶ï¼Œä»Žå·¥å•详情页的明细行中筛选 `needReturn = true` çš„行作为退货明细
- é€€è´§æ˜Žç»†ä¸­çš„ `ticketItemId` å¿…须对应工单明细行的 `id`
- ç‰©æ–™ä¿¡æ¯ï¼ˆ`itemId`、`itemCode`、`itemName`)从工单明细行带入,不允许修改
- é”€å”®å•价(`unitPrice`)需要从销售订单行查询获取,前端可通过已有的 ERP é”€å”®è®¢å•接口查询
---
## API è¯´æ˜Ž
### ä¸€ã€å”®åŽå·¥å•
| æ–¹æ³• | è·¯å¾„ | æƒé™ | è¯´æ˜Ž |
|------|------|------|------|
| POST | `/aftersales/ticket/create` | `aftersales:ticket:create` | åˆ›å»ºå·¥å• |
| PUT | `/aftersales/ticket/update` | `aftersales:ticket:update` | æ›´æ–°å·¥å•(仅草稿状态可更新) |
| DELETE | `/aftersales/ticket/delete` | `aftersales:ticket:delete` | åˆ é™¤å·¥å•(仅草稿/已取消状态可删除) |
| GET | `/aftersales/ticket/get` | `aftersales:ticket:query` | èŽ·å–å·¥å•è¯¦æƒ… |
| GET | `/aftersales/ticket/page` | `aftersales:ticket:query` | åˆ†é¡µæŸ¥è¯¢å·¥å• |
| POST | `/aftersales/ticket/assign` | `aftersales:ticket:update` | åˆ†é…å¤„理人 |
| POST | `/aftersales/ticket/classify` | `aftersales:ticket:update` | é—®é¢˜åˆ¤å®š |
| PUT | `/aftersales/ticket/resolve` | `aftersales:ticket:update` | å®Œç»“工单 |
| PUT | `/aftersales/ticket/close` | `aftersales:ticket:update` | å…³é—­å·¥å• |
| PUT | `/aftersales/ticket/cancel` | `aftersales:ticket:update` | å–消工单 |
#### 1.1 åˆ›å»ºå·¥å• `POST /aftersales/ticket/create`
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| name | String | æ˜¯ | å·¥å•主题 |
| customerId | Long | æ˜¯ | å®¢æˆ·ç¼–号 |
| contactId | Long | å¦ | è”系人编号 |
| saleOrderId | Long | å¦ | é”€å”®è®¢å•编号 |
| saleOrderNo | String | å¦ | é”€å”®è®¢å•号(冗余,与 saleOrderId é…å¥—传入) |
| contractId | Long | å¦ | åˆåŒç¼–号 |
| issueDescription | String | å¦ | é—®é¢˜æè¿° |
| remark | String | å¦ | å¤‡æ³¨ |
| items | List\<Item\> | å¦ | æ˜Žç»†è¡Œåˆ—表 |
**Item ç»“构:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| saleOrderItemId | Long | å¦ | é”€å”®è®¢å•行编号 |
| itemId | Long | æ˜¯ | MDM ç‰©æ–™ç¼–号 |
| itemCode | String | å¦ | ç‰©æ–™ç¼–码 |
| itemName | String | å¦ | ç‰©æ–™åç§° |
| batchId | Long | å¦ | ç”Ÿäº§æ‰¹æ¬¡ç¼–号 |
| batchCode | String | å¦ | ç”Ÿäº§æ‰¹æ¬¡å· |
| saleQuantity | BigDecimal | å¦ | é”€å”®æ•°é‡ |
| returnQuantity | BigDecimal | å¦ | é€€è´§æ•°é‡ |
| needRepair | Boolean | å¦ | æ˜¯å¦éœ€è¦ç»´ä¿® |
| needReturn | Boolean | å¦ | æ˜¯å¦éœ€è¦é€€è´§ |
| remark | String | å¦ | å¤‡æ³¨ |
**请求示例:**
```json
{
  "name": "产品外观缺陷投诉",
  "customerId": 1,
  "contactId": 2,
  "saleOrderId": 100,
  "saleOrderNo": "SO202401010001",
  "contractId": 50,
  "issueDescription": "收到货后发现产品表面有明显划痕",
  "remark": "客户要求尽快处理",
  "items": [
    {
      "saleOrderItemId": 10,
      "itemId": 200,
      "itemCode": "MAT-001",
      "itemName": "电子元器件",
      "batchId": 5,
      "batchCode": "BATCH-2024-001",
      "saleQuantity": 100.000,
      "returnQuantity": 10.000,
      "needRepair": false,
      "needReturn": true,
      "remark": "外观缺陷"
    }
  ]
}
```
**响应:** `{ "code": 0, "data": 1 }` â€” data ä¸ºæ–°å»ºå·¥å• ID
#### 1.2 æ›´æ–°å·¥å• `PUT /aftersales/ticket/update`
请求参数与创建相同,额外需要 `id` å­—段。仅草稿状态可更新。
#### 1.3 åˆ é™¤å·¥å• `DELETE /aftersales/ticket/delete`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¥å•编号(Query å‚数) |
**响应:** `{ "code": 0, "data": true }`
#### 1.4 èŽ·å–å·¥å•è¯¦æƒ… `GET /aftersales/ticket/get`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¥å•编号(Query å‚数) |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | å·¥å•编号 |
| no | String | å·¥å•单号(格式:AS + yyyyMMdd + 6位序号) |
| name | String | å·¥å•主题 |
| customerId | Long | å®¢æˆ·ç¼–号 |
| customerName | String | å®¢æˆ·åç§° |
| contactId | Long | è”系人编号 |
| contactName | String | è”系人名称 |
| saleOrderId | Long | é”€å”®è®¢å•编号 |
| saleOrderNo | String | é”€å”®è®¢å•号 |
| contractId | Long | åˆåŒç¼–号 |
| issueType | Integer | é—®é¢˜ç±»åž‹ï¼š1-一般问题 2-维修问题 3-退货问题 |
| issueCategory | String | é—®é¢˜åˆ†ç±» |
| issueDescription | String | é—®é¢˜æè¿° |
| severityLevel | Integer | ä¸¥é‡ç¨‹åº¦ï¼š0-轻微 1-一般 2-严重 3-致命 |
| status | Integer | çŠ¶æ€ï¼ˆè§çŠ¶æ€æžšä¸¾ï¼‰ |
| ownerUserId | Long | è´Ÿè´£äººç”¨æˆ·ç¼–号 |
| ownerUserName | String | è´Ÿè´£äººåç§° |
| handlerUserId | Long | å¤„理人用户编号 |
| handlerUserName | String | å¤„理人名称 |
| processInstanceId | String | BPM å·¥ä½œæµå®žä¾‹ç¼–号 |
| returnId | Long | å…³è”退货申请编号 |
| repairId | Long | å…³è”维修记录编号 |
| resolvedTime | DateTime | è§£å†³æ—¶é—´ |
| closedTime | DateTime | å…³é—­æ—¶é—´ |
| remark | String | å¤‡æ³¨ |
| creator | String | åˆ›å»ºè€… |
| createTime | DateTime | åˆ›å»ºæ—¶é—´ |
| updater | String | æ›´æ–°è€… |
| updateTime | DateTime | æ›´æ–°æ—¶é—´ |
| items | List\<Item\> | æ˜Žç»†è¡Œåˆ—表 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "no": "AS20260730000001",
    "name": "产品外观缺陷投诉",
    "customerId": 1,
    "customerName": "XX科技有限公司",
    "contactId": 2,
    "contactName": "张三",
    "saleOrderId": 100,
    "saleOrderNo": "SO202401010001",
    "contractId": 50,
    "issueType": 3,
    "issueCategory": "外观缺陷",
    "issueDescription": "收到货后发现产品表面有明显划痕",
    "severityLevel": 1,
    "status": 30,
    "ownerUserId": 1,
    "ownerUserName": "管理员",
    "handlerUserId": 2,
    "handlerUserName": "售后专员",
    "returnId": 1,
    "items": [
      {
        "id": 1,
        "ticketId": 1,
        "saleOrderItemId": 10,
        "itemId": 200,
        "itemCode": "MAT-001",
        "itemName": "电子元器件",
        "batchId": 5,
        "batchCode": "BATCH-2024-001",
        "saleQuantity": 100.000,
        "returnQuantity": 10.000,
        "needRepair": false,
        "needReturn": true
      }
    ]
  }
}
```
#### 1.5 åˆ†é¡µæŸ¥è¯¢å·¥å• `GET /aftersales/ticket/page`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| pageNo | Integer | å¦ | é¡µç ï¼Œé»˜è®¤ 1 |
| pageSize | Integer | å¦ | æ¯é¡µå¤§å°ï¼Œé»˜è®¤ 10 |
| no | String | å¦ | å·¥å•单号 |
| name | String | å¦ | å·¥å•主题 |
| customerId | Long | å¦ | å®¢æˆ·ç¼–号 |
| saleOrderId | Long | å¦ | é”€å”®è®¢å•编号 |
| status | Integer | å¦ | å·¥å•状态 |
| issueType | Integer | å¦ | é—®é¢˜ç±»åž‹ |
| sceneType | Integer | å¦ | åœºæ™¯ç±»åž‹ï¼š1-我负责的 2-我参与的 3-下属负责的 4-全部 |
**响应:**
```json
{
  "code": 0,
  "data": {
    "list": [ /* AfterSaleTicketRespVO æ•°ç»„ */ ],
    "total": 100
  }
}
```
**注意:** åˆ†é¡µåˆ—表响应中的每条记录结构与详情接口一致(含 `items` å’Œå…³è”名称字段),但列表场景下 `items` å¯ä¸ºç©ºã€‚
#### 1.6 åˆ†é…å¤„理人 `POST /aftersales/ticket/assign`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¥å•编号(Query å‚数) |
| handlerUserId | Long | æ˜¯ | å¤„理人用户编号(Query å‚数) |
**业务规则:** ä»…草稿、处理中状态可分配。分配后状态自动变为"处理中"。
#### 1.7 é—®é¢˜åˆ¤å®š `POST /aftersales/ticket/classify`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¥å•编号(Query å‚数) |
| issueType | Integer | æ˜¯ | é—®é¢˜ç±»åž‹ï¼š1-一般问题 2-维修问题 3-退货问题 |
| issueCategory | String | å¦ | é—®é¢˜åˆ†ç±»ï¼ˆå¦‚"外观缺陷"、"功能故障"等) |
| severityLevel | Integer | å¦ | ä¸¥é‡ç¨‹åº¦ï¼š0-轻微 1-一般 2-严重 3-致命 |
**业务规则:** ä»…处理中状态可判定。判定为退货问题后,应引导用户创建退货申请。
#### 1.8 å®Œç»“工单 `PUT /aftersales/ticket/resolve`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¥å•编号(Query å‚数) |
**业务规则:** ä»…处理中/待退货/待退款状态可完结。
#### 1.9 å…³é—­å·¥å• `PUT /aftersales/ticket/close`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¥å•编号(Query å‚数) |
**业务规则:** ä»…已完结状态可关闭。
#### 1.10 å–消工单 `PUT /aftersales/ticket/cancel`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¥å•编号(Query å‚数) |
**业务规则:** ä»…草稿、处理中状态可取消。
---
### äºŒã€å”®åŽé€€è´§ç”³è¯·
| æ–¹æ³• | è·¯å¾„ | æƒé™ | è¯´æ˜Ž |
|------|------|------|------|
| POST | `/aftersales/return/create` | `aftersales:return:create` | åˆ›å»ºé€€è´§ç”³è¯· |
| PUT | `/aftersales/return/approve` | `aftersales:return:approve` | å®¡æ ¸é€šè¿‡ï¼ˆè§¦å‘ MES é€€è´§å•) |
| GET | `/aftersales/return/get` | `aftersales:return:query` | èŽ·å–é€€è´§è¯¦æƒ… |
| GET | `/aftersales/return/page` | `aftersales:return:query` | åˆ†é¡µæŸ¥è¯¢é€€è´§ç”³è¯· |
#### 2.1 åˆ›å»ºé€€è´§ç”³è¯· `POST /aftersales/return/create`
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| ticketId | Long | æ˜¯ | å…³è”工单编号 |
| customerId | Long | æ˜¯ | å®¢æˆ·ç¼–号(从工单带入) |
| saleOrderId | Long | å¦ | å…³è”销售订单编号(从工单带入) |
| returnType | Integer | æ˜¯ | é€€è´§ç±»åž‹ï¼š1-退款退货 2-换货 3-仅退款 |
| returnReason | String | æ˜¯ | é€€è´§åŽŸå›  |
| defectCategory | String | å¦ | ç¼ºé™·åˆ†ç±» |
| remark | String | å¦ | å¤‡æ³¨ |
| items | List\<Item\> | æ˜¯ | é€€è´§æ˜Žç»†è¡Œåˆ—表 |
**Item ç»“构:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| ticketItemId | Long | æ˜¯ | å·¥å•明细行编号(关联 after_sale_ticket_item.id) |
| itemId | Long | æ˜¯ | MDM ç‰©æ–™ç¼–号(从工单明细行带入) |
| itemCode | String | å¦ | ç‰©æ–™ç¼–码 |
| itemName | String | å¦ | ç‰©æ–™åç§° |
| batchId | Long | å¦ | æ‰¹æ¬¡ç¼–号 |
| batchCode | String | å¦ | æ‰¹æ¬¡å· |
| returnQuantity | BigDecimal | æ˜¯ | é€€è´§æ•°é‡ |
| unitPrice | BigDecimal | æ˜¯ | é”€å”®å•价(需从销售订单行查询获取) |
| returnPrice | BigDecimal | å¦ | é€€è´§é‡‘额(= returnQuantity Ã— unitPrice) |
| remark | String | å¦ | å¤‡æ³¨ |
**请求示例:**
```json
{
  "ticketId": 1,
  "customerId": 1,
  "saleOrderId": 100,
  "returnType": 1,
  "returnReason": "产品外观缺陷",
  "defectCategory": "外观缺陷",
  "remark": "客户要求退货退款",
  "items": [
    {
      "ticketItemId": 1,
      "itemId": 200,
      "itemCode": "MAT-001",
      "itemName": "电子元器件",
      "batchId": 5,
      "batchCode": "BATCH-2024-001",
      "returnQuantity": 10.000,
      "unitPrice": 100.00,
      "returnPrice": 1000.00
    }
  ]
}
```
**响应:** `{ "code": 0, "data": 1 }` â€” data ä¸ºæ–°å»ºé€€è´§ç”³è¯· ID
**业务规则:**
- åˆ›å»ºé€€è´§ç”³è¯·åŽï¼Œå…³è”工单状态自动变为"待退货(30)"
- `returnPrice` åŽç«¯ä¼šè‡ªåŠ¨è®¡ç®—ï¼ˆ`returnQuantity Ã— unitPrice` æ±‚和),前端可不传
#### 2.2 å®¡æ ¸é€šè¿‡ `PUT /aftersales/return/approve`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é€€è´§ç”³è¯·ç¼–号(Query å‚数) |
**业务规则:**
- ä»…草稿状态(0)可审核
- å®¡æ ¸é€šè¿‡åŽï¼ŒåŽç«¯è‡ªåŠ¨è°ƒç”¨ MES åˆ›å»ºé”€å”®é€€è´§å•,状态变为"审核通过-已同步MES(20)"
- åŽç»­ MES é€€è´§æµç¨‹ï¼ˆè´¨æ£€â†’入库→退款)由后端回调自动完成,前端无需操作
#### 2.3 èŽ·å–é€€è´§è¯¦æƒ… `GET /aftersales/return/get`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é€€è´§ç”³è¯·ç¼–号(Query å‚数) |
**响应字段(新增/重要字段):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | é€€è´§ç”³è¯·ç¼–号 |
| no | String | é€€è´§å•号(格式:RT + yyyyMMdd + 6位序号) |
| ticketId | Long | å…³è”工单编号 |
| customerId | Long | å®¢æˆ·ç¼–号 |
| customerName | String | å®¢æˆ·åç§° |
| saleOrderId | Long | é”€å”®è®¢å•编号 |
| returnType | Integer | é€€è´§ç±»åž‹ |
| returnReason | String | é€€è´§åŽŸå›  |
| defectCategory | String | ç¼ºé™·åˆ†ç±» |
| totalReturnPrice | BigDecimal | åº”退总金额 |
| actualRefundPrice | BigDecimal | å®žé€€é‡‘额 |
| status | Integer | çŠ¶æ€ï¼ˆè§é€€è´§çŠ¶æ€æžšä¸¾ï¼‰ |
| auditUserId | Long | å®¡æ ¸äºº |
| auditTime | DateTime | å®¡æ ¸æ—¶é—´ |
| mesReturnId | Long | MES退货单编号 |
| mesReturnCode | String | MES退货单号 |
| qualityResult | String | è´¨æ£€ç»“论 |
| qualityReportUrl | String | è´¨æ£€æŠ¥å‘Šé™„ä»¶URL |
| returnInboundTime | DateTime | é€€è´§å…¥åº“完成时间 |
| refundTime | DateTime | é€€æ¬¾å®Œæˆæ—¶é—´ |
| items | List\<Item\> | é€€è´§æ˜Žç»†è¡Œ |
#### 2.4 åˆ†é¡µæŸ¥è¯¢é€€è´§ç”³è¯· `GET /aftersales/return/page`
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| pageNo | Integer | å¦ | é¡µç  |
| pageSize | Integer | å¦ | æ¯é¡µå¤§å° |
| no | String | å¦ | é€€è´§å•号 |
| ticketId | Long | å¦ | å…³è”工单编号 |
| customerId | Long | å¦ | å®¢æˆ·ç¼–号 |
| status | Integer | å¦ | é€€è´§çŠ¶æ€ |
| sceneType | Integer | å¦ | åœºæ™¯ç±»åž‹ |
---
## æžšä¸¾å€¼é€ŸæŸ¥
### å·¥å•状态 (AfterSaleTicketStatusEnum)
| å€¼ | åç§° | è¯´æ˜Ž |
|----|------|------|
| 0 | è‰ç¨¿ | å¯ç¼–辑、可删除、可取消 |
| 10 | å¤„理中 | å¯åˆ†é…ã€å¯åˆ¤å®šã€å¯å–消 |
| 30 | å¾…退货 | é€€è´§æµç¨‹è¿›è¡Œä¸­ |
| 40 | å¾…退款/换货 | ç­‰å¾…退款或换货完成 |
| 50 | å·²å®Œç»“ | å¯å…³é—­ |
| 60 | å·²å…³é—­ | ç»ˆæ€ |
| 99 | å·²å–消 | ç»ˆæ€ |
### é—®é¢˜ç±»åž‹ (AfterSaleIssueTypeEnum)
| å€¼ | åç§° | åŽç»­æ“ä½œ |
|----|------|----------|
| 0 | æœªåˆ¤å®š | åˆ›å»ºå·¥å•时的初始值 |
| 1 | ä¸€èˆ¬é—®é¢˜ | ç›´æŽ¥å¤„理→完结 |
| 2 | ç»´ä¿®é—®é¢˜ | åˆ›å»ºç»´ä¿®è®°å½•→维修完成→完结 |
| 3 | é€€è´§é—®é¢˜ | åˆ›å»ºé€€è´§ç”³è¯·â†’审核→MES退货→完结 |
### é€€è´§çŠ¶æ€ (AfterSaleReturnStatusEnum)
| å€¼ | åç§° | è¯´æ˜Ž |
|----|------|------|
| 0 | è‰ç¨¿ | å¯å®¡æ ¸ |
| 10 | é”€å”®å®¡æ ¸ä¸­ | â€” |
| 20 | å®¡æ ¸é€šè¿‡-已同步MES | ç­‰å¾…MES处理 |
| 30 | æ£€éªŒä¸­ | MES质检进行中 |
| 40 | æ£€éªŒå®Œæˆ | ç­‰å¾…入库 |
| 50 | å·²å…¥åº“ | ç­‰å¾…退款 |
| 60 | é€€æ¬¾/换货完成 | â€” |
| 70 | å·²å…³é—­ | ç»ˆæ€ |
| 99 | å·²å–消 | ç»ˆæ€ |
### é€€è´§ç±»åž‹
| å€¼ | åç§° |
|----|------|
| 1 | é€€æ¬¾é€€è´§ |
| 2 | æ¢è´§ |
| 3 | ä»…退款 |
### ä¸¥é‡ç¨‹åº¦
| å€¼ | åç§° | å»ºè®®é¢œè‰² |
|----|------|----------|
| 0 | è½»å¾® | ç»¿è‰² |
| 1 | ä¸€èˆ¬ | è“è‰² |
| 2 | ä¸¥é‡ | æ©™è‰² |
| 3 | è‡´å‘½ | çº¢è‰² |
### åœºæ™¯ç±»åž‹ (sceneType)
| å€¼ | åç§° | è¯´æ˜Ž |
|----|------|------|
| 1 | æˆ‘负责的 | å½“前用户为负责人 |
| 2 | æˆ‘参与的 | å½“前用户为处理人或被授权 |
| 3 | ä¸‹å±žè´Ÿè´£çš„ | ä¸‹å±žä¸ºè´Ÿè´£äºº |
| 4 | å…¨éƒ¨ | ç®¡ç†å‘˜æŸ¥çœ‹æ‰€æœ‰ |
---
## å­—段展示规则
### å·¥å•列表页
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| no | åˆ—表 | å•号,可点击进入详情 |
| name | åˆ—表 | å·¥å•主题 |
| customerName | åˆ—表 | å®¢æˆ·åç§° |
| saleOrderNo | åˆ—表 | é”€å”®è®¢å•号 |
| issueType | åˆ—表 | é—®é¢˜ç±»åž‹ï¼Œç”¨æ ‡ç­¾åŒºåˆ†é¢œè‰² |
| severityLevel | åˆ—表 | ä¸¥é‡ç¨‹åº¦ï¼Œç”¨æ ‡ç­¾åŒºåˆ†é¢œè‰² |
| status | åˆ—表 | çŠ¶æ€ï¼Œç”¨æ ‡ç­¾åŒºåˆ†é¢œè‰² |
| ownerUserName | åˆ—表 | è´Ÿè´£äºº |
| handlerUserName | åˆ—表 | å¤„理人 |
| createTime | åˆ—表 | åˆ›å»ºæ—¶é—´ |
| â€” | æœç´¢åŒº | å•号、主题、客户、订单、状态、问题类型 |
| â€” | æœç´¢åŒº | åœºæ™¯ç±»åž‹ Tab:我负责的/我参与的/下属负责的/全部 |
### å·¥å•详情页
| å­—段 | å±•示区域 | è¯´æ˜Ž |
|------|----------|------|
| no | å¤´éƒ¨ | å•号 |
| name | å¤´éƒ¨ | å·¥å•主题 |
| status | å¤´éƒ¨ | çŠ¶æ€æ ‡ç­¾ + çŠ¶æ€æµè½¬æŒ‰é’® |
| customerId / customerName | åŸºæœ¬ä¿¡æ¯ | å®¢æˆ·é€‰æ‹©å™¨ï¼ˆåˆ›å»ºæ—¶å¯é€‰ï¼Œåˆ›å»ºåŽåªè¯»ï¼‰ |
| contactId / contactName | åŸºæœ¬ä¿¡æ¯ | è”系人选择器 |
| saleOrderId / saleOrderNo | åŸºæœ¬ä¿¡æ¯ | é”€å”®è®¢å•选择器 |
| contractId | åŸºæœ¬ä¿¡æ¯ | åˆåŒé€‰æ‹©å™¨ |
| issueType | åŸºæœ¬ä¿¡æ¯ | é—®é¢˜ç±»åž‹ï¼ˆåˆ¤å®šåŽæ˜¾ç¤ºï¼‰ |
| issueCategory | åŸºæœ¬ä¿¡æ¯ | é—®é¢˜åˆ†ç±»ï¼ˆåˆ¤å®šåŽæ˜¾ç¤ºï¼‰ |
| issueDescription | åŸºæœ¬ä¿¡æ¯ | é—®é¢˜æè¿° |
| severityLevel | åŸºæœ¬ä¿¡æ¯ | ä¸¥é‡ç¨‹åº¦ï¼ˆåˆ¤å®šåŽæ˜¾ç¤ºï¼‰ |
| ownerUserId / ownerUserName | åŸºæœ¬ä¿¡æ¯ | è´Ÿè´£äººï¼ˆåˆ›å»ºæ—¶è‡ªåŠ¨è®¾ä¸ºå½“å‰ç”¨æˆ·ï¼‰ |
| handlerUserId / handlerUserName | åŸºæœ¬ä¿¡æ¯ | å¤„理人(分配后显示) |
| returnId | åŸºæœ¬ä¿¡æ¯ | å…³è”退货申请(可点击跳转) |
| repairId | åŸºæœ¬ä¿¡æ¯ | å…³è”维修记录(可点击跳转) |
| resolvedTime | åŸºæœ¬ä¿¡æ¯ | è§£å†³æ—¶é—´ï¼ˆå®Œç»“后显示) |
| closedTime | åŸºæœ¬ä¿¡æ¯ | å…³é—­æ—¶é—´ï¼ˆå…³é—­åŽæ˜¾ç¤ºï¼‰ |
| remark | åŸºæœ¬ä¿¡æ¯ | å¤‡æ³¨ |
| items | æ˜Žç»†è¡¨æ ¼ | ç‰©æ–™ã€æ‰¹æ¬¡ã€é”€å”®æ•°é‡ã€é€€è´§æ•°é‡ã€æ˜¯å¦ç»´ä¿®ã€æ˜¯å¦é€€è´§ |
| createTime / creator | åº•部信息 | åˆ›å»ºä¿¡æ¯ |
| updateTime / updater | åº•部信息 | æ›´æ–°ä¿¡æ¯ |
### é€€è´§ç”³è¯·åˆ—表页
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| no | åˆ—表 | é€€è´§å•号 |
| ticketId | åˆ—表 | å…³è”工单编号(可点击跳转) |
| customerName | åˆ—表 | å®¢æˆ·åç§° |
| returnType | åˆ—表 | é€€è´§ç±»åž‹æ ‡ç­¾ |
| totalReturnPrice | åˆ—表 | åº”退总金额 |
| actualRefundPrice | åˆ—表 | å®žé€€é‡‘额 |
| status | åˆ—表 | çŠ¶æ€æ ‡ç­¾ |
| mesReturnCode | åˆ—表 | MES退货单号 |
| qualityResult | åˆ—表 | è´¨æ£€ç»“论 |
| createTime | åˆ—表 | åˆ›å»ºæ—¶é—´ |
### é€€è´§ç”³è¯·è¯¦æƒ…页
| å­—段 | å±•示区域 | è¯´æ˜Ž |
|------|----------|------|
| no | å¤´éƒ¨ | é€€è´§å•号 |
| status | å¤´éƒ¨ | çŠ¶æ€æ ‡ç­¾ + å®¡æ ¸æŒ‰é’®ï¼ˆè‰ç¨¿çŠ¶æ€æ˜¾ç¤ºï¼‰ |
| ticketId | åŸºæœ¬ä¿¡æ¯ | å…³è”工单(可点击跳转) |
| customerName | åŸºæœ¬ä¿¡æ¯ | å®¢æˆ·åç§° |
| saleOrderId | åŸºæœ¬ä¿¡æ¯ | é”€å”®è®¢å• |
| returnType | åŸºæœ¬ä¿¡æ¯ | é€€è´§ç±»åž‹ |
| returnReason | åŸºæœ¬ä¿¡æ¯ | é€€è´§åŽŸå›  |
| defectCategory | åŸºæœ¬ä¿¡æ¯ | ç¼ºé™·åˆ†ç±» |
| totalReturnPrice | åŸºæœ¬ä¿¡æ¯ | åº”退总金额 |
| actualRefundPrice | åŸºæœ¬ä¿¡æ¯ | å®žé€€é‡‘额 |
| mesReturnId / mesReturnCode | MES信息 | MES退货单 |
| qualityResult | MES信息 | è´¨æ£€ç»“论 |
| qualityReportUrl | MES信息 | è´¨æ£€æŠ¥å‘Šé“¾æŽ¥ |
| returnInboundTime | MES信息 | å…¥åº“æ—¶é—´ |
| refundTime | é€€æ¬¾ä¿¡æ¯ | é€€æ¬¾æ—¶é—´ |
| items | æ˜Žç»†è¡¨æ ¼ | ç‰©æ–™ã€æ‰¹æ¬¡ã€é€€è´§æ•°é‡ã€å•价、退货金额 |
| createTime / creator | åº•部信息 | åˆ›å»ºä¿¡æ¯ |
---
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
### å·¥å•操作按钮显隐规则
| æ“ä½œ | è‰ç¨¿(0) | å¤„理中(10) | å¾…退货(30) | å¾…退款(40) | å·²å®Œç»“(50) | å·²å…³é—­(60) | å·²å–消(99) |
|------|---------|------------|------------|------------|------------|------------|------------|
| ç¼–辑 | âœ… | âŒ | âŒ | âŒ | âŒ | âŒ | âŒ |
| åˆ é™¤ | âœ… | âŒ | âŒ | âŒ | âŒ | âŒ | âœ… |
| åˆ†é…å¤„理人 | âœ… | âœ… | âŒ | âŒ | âŒ | âŒ | âŒ |
| é—®é¢˜åˆ¤å®š | âŒ | âœ… | âŒ | âŒ | âŒ | âŒ | âŒ |
| åˆ›å»ºé€€è´§ç”³è¯· | âŒ | âœ…(判定为退货后) | âŒ | âŒ | âŒ | âŒ | âŒ |
| å®Œç»“ | âŒ | âœ… | âœ… | âœ… | âŒ | âŒ | âŒ |
| å…³é—­ | âŒ | âŒ | âŒ | âŒ | âœ… | âŒ | âŒ |
| å–消 | âœ… | âœ… | âŒ | âŒ | âŒ | âŒ | âŒ |
### é€€è´§ç”³è¯·æ“ä½œæŒ‰é’®æ˜¾éšè§„则
| æ“ä½œ | è‰ç¨¿(0) | å®¡æ ¸ä¸­(10) | å·²åŒæ­¥MES(20) | æ£€éªŒä¸­(30) | æ£€éªŒå®Œæˆ(40) | å·²å…¥åº“(50) | é€€æ¬¾å®Œæˆ(60) |
|------|---------|------------|---------------|------------|--------------|------------|--------------|
| å®¡æ ¸é€šè¿‡ | âœ… | âŒ | âŒ | âŒ | âŒ | âŒ | âŒ |
| æŸ¥çœ‹è¯¦æƒ… | âœ… | âœ… | âœ… | âœ… | âœ… | âœ… | âœ… |
### åˆ†é¡µæŸ¥è¯¢æ•°æ®æƒé™è¯´æ˜Ž
- **sceneType=1(我负责的)**:显示当前用户为 `ownerUserId` çš„工单 + æ•°æ®äº¤æŽ¥çš„工单
- **sceneType=2(我参与的)**:显示当前用户有读/写权限的工单
- **sceneType=3(下属负责的)**:显示当前用户的下属负责的工单
- **sceneType=4(全部)**:需要管理员权限(`aftersales:admin`)
---
## æ–‡ä»¶ä¸Šä¼ 
售后模块使用统一的文件上传机制,不自行实现上传接口。
**上传流程:**
1. è°ƒç”¨ `POST /system/storage-blob/upload` ä¸Šä¼ æ–‡ä»¶ â†’ èŽ·å¾— `blobId`
2. è°ƒç”¨ `POST /system/storage-attachment/bind` ç»‘定到业务记录
**recordType å–值:**
| ä¸šåŠ¡å¯¹è±¡ | recordType |
|----------|------------|
| å”®åŽå·¥å• | `aftersales_ticket` |
| å”®åŽé€€è´§ç”³è¯· | `aftersales_return` |
**查询附件:** `GET /system/storage-attachment/list?recordType=aftersales_ticket&recordId=1`
---
## æ³¨æ„äº‹é¡¹
1. **明细行更新策略**:更新工单时,明细行采用"先删后增"策略,前端必须传入完整的明细行列表(包括未修改的行),否则会导致明细行丢失。
2. **单号生成**:工单单号(AS前缀)和退货单号(RT前缀)由后端自动生成,前端不需要传入。
3. **状态流转**:所有状态变更操作(分配、判定、完结、关闭、取消、审核)后端都会校验当前状态是否允许,前端应根据按钮显隐规则控制操作入口。
4. **退货审核后自动同步 MES**:审核通过退货申请后,后端自动调用 MES åˆ›å»ºé”€å”®é€€è´§å•,后续 MES æµç¨‹ï¼ˆè´¨æ£€â†’入库→退款)由回调自动推进状态,前端只需轮询刷新查看最新状态。
5. **数据带入**:创建退货申请时,必须从工单详情页跳转并带入 `ticketId`、`customerId`、`saleOrderId` ä»¥åŠå·¥å•明细行中标记为"需要退货"的行。
6. **权限控制**:所有接口都有 `@PreAuthorize` æƒé™æ ¡éªŒï¼Œå‰ç«¯èœå•和按钮需要对应配置权限标识。
7. **路由权限**:需在 `src/router/routes/modules/` ä¸‹æ–°å»º `aftersales.ts` è·¯ç”±æ¨¡å—文件,并在菜单中注册"售后服务"一级菜单。
8. **API æ¨¡å—**:需在 `src/api/` ä¸‹æ–°å»º `aftersales/` ç›®å½•,创建 `ticket.ts` å’Œ `return.ts` ä¸¤ä¸ª API æ–‡ä»¶ã€‚
docs/crm_customer_view_all.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
# æŸ¥çœ‹å…¨éƒ¨å®¢æˆ·æƒé™ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯ç”±è·¯å¾„ | è¯´æ˜Ž |
|------|----------|------|
| å®¢æˆ·ç®¡ç†åˆ—表 | `/crm/customer` | å®¢æˆ·åˆ—表页,新增"全部"Tab |
---
## API
### å·²æœ‰æŽ¥å£ï¼šå®¢æˆ·åˆ†é¡µæŸ¥è¯¢ï¼ˆæ— éœ€æ–°å¢žæŽ¥å£ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/crm/customer/page` | å®¢æˆ·åˆ†é¡µæŸ¥è¯¢ |
**已有请求参数(`CrmCustomerPageReqVO`):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| sceneType | Integer | å¦ | åœºæ™¯ç±»åž‹ï¼š1-我负责的 / 2-我参与的 / 3-下属负责的 / **4-全部(新增)** |
| name | String | å¦ | å®¢æˆ·åç§°ï¼ˆæ¨¡ç³Šï¼‰ |
| mobile | String | å¦ | æ‰‹æœº |
| industryId | Integer | å¦ | æ‰€å±žè¡Œä¸š |
| level | Integer | å¦ | å®¢æˆ·ç­‰çº§ |
| source | Integer | å¦ | å®¢æˆ·æ¥æº |
| pool | Boolean | å¦ | æ˜¯å¦ä¸ºå…¬æµ·æ•°æ® |
| followUpStatus | Boolean | å¦ | è·Ÿè¿›çŠ¶æ€ |
**响应无变化**,`CrmCustomerRespVO` å·²åŒ…含:
- `ownerUserName` â€” é”€å”®äººå‘˜åç§°
- `ownerUserDeptName` â€” é”€å”®äººå‘˜éƒ¨é—¨åç§°
---
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
### åœºæ™¯ç±»åž‹æ‰©å±•
| sceneType å€¼ | åœºæ™¯åç§° | æ•°æ®èŒƒå›´ | æƒé™è¦æ±‚ |
|--------------|----------|----------|----------|
| 1 | æˆ‘负责的 | `owner_user_id = å½“前用户` | `crm:customer:query` |
| 2 | æˆ‘参与的 | å›¢é˜Ÿæˆå‘˜ï¼ˆéžè´Ÿè´£äººï¼‰ | `crm:customer:query` |
| 3 | ä¸‹å±žè´Ÿè´£çš„ | ä¸‹å±žä¸ºè´Ÿè´£äºº | `crm:customer:query` |
| **4(新增)** | **全部** | **所有客户(无过滤)** | **`crm:customer:view-all` æˆ– `crm_admin` è§’色** |
### æƒé™é™çº§è§„则
当用户以 `sceneType=4` è¯·æ±‚数据时:
- ç”¨æˆ·æ‹¥æœ‰ `crm:customer:view-all` æƒé™ **或** `crm_admin` è§’色 â†’ è¿”回全部客户
- ç”¨æˆ·**没有**该权限 â†’ è‡ªåŠ¨é™çº§ä¸º"我负责的"(`sceneType=1`),仅返回自己的客户
### æƒé™æ ‡è¯†
| æƒé™æ ‡è¯† | è¯´æ˜Ž | æ‰€å±žèœå• |
|----------|------|----------|
| `crm:customer:view-all` | æŸ¥çœ‹å…¨éƒ¨å®¢æˆ· | CRM å®¢æˆ·å…¬æµ· |
该权限已添加到数据库 `system_menu` è¡¨ï¼Œä½äºŽã€Œå®¢æˆ·å…¬æµ·ã€èœå•下(id=602583),角色分配后即可生效。
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. å®¢æˆ·åˆ—表页 Tab åˆ‡æ¢
在现有的场景类型 Tab(我负责的/我参与的/下属负责的)后面,新增"全部"Tab(`sceneType=4`)。
**展示规则**:
- ä»…当用户拥有 `crm:customer:view-all` æƒé™æˆ– `crm_admin` è§’色时,才显示"全部"Tab
- åˆ¤æ–­æ–¹å¼ï¼šè°ƒç”¨æƒé™åˆ¤æ–­æŽ¥å£æˆ–从用户权限列表中检查
### 2. æƒé™åˆ¤æ–­
前端需要判断当前用户是否有 `crm:customer:view-all` æƒé™ï¼š
- æ–¹å¼ä¸€ï¼šé€šè¿‡ Vue æŒ‡ä»¤ `v-hasPermi="['crm:customer:view-all']"` æŽ§åˆ¶ Tab æ˜¾éš
- æ–¹å¼äºŒï¼šé€šè¿‡èŽ·å–ç”¨æˆ·æƒé™åˆ—è¡¨åˆ¤æ–­
---
## æ³¨æ„äº‹é¡¹
- "全部"Tab ä»…控制数据可见范围,不改变其他功能权限(如编辑、删除仍需对应权限)
- å¯¼å‡º Excel æ—¶åŒæ ·æ”¯æŒ `sceneType=4`,拥有权限的用户可导出全部客户数据
- æƒé™æ•°æ®å·²æ’入数据库(`system_menu` id=602583),无需前端额外处理权限注册
- è¯¥æƒé™ä¸Ž `crm_admin` è§’色的区别:`crm_admin` æ‹¥æœ‰æ‰€æœ‰ CRM ç®¡ç†æƒé™ï¼Œè€Œ `view-all` ä»…控制客户列表的查看范围
docs/document_audit_project_filter_fix.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
# é¡¹ç›®è¯¦æƒ… - èµ„料审核查询修复(前端联调方案)
## é—®é¢˜
项目详情页"资料审核" Tab æŸ¥è¯¢ `/mes/pd/document-audit/page` æ—¶æœªä¼ å…¥ `projectId`,导致返回了所有项目的审核记录,而非当前项目的。
## æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯ç”± | è¯´æ˜Ž |
|------|------|------|
| é¡¹ç›®è¯¦æƒ… - èµ„料审核 Tab | `/mes/pd/project/detail/{id}` | å¾…修复 |
---
## API å˜æ›´
`GET /mes/pd/document-audit/page` â€” æ–°å¢žè¯·æ±‚参数:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| projectId | Long | å¦ | é¡¹ç›® ID,传入后仅返回该项目下资料的审核记录 |
**响应结构不变。**
---
## å‰ç«¯ä¿®æ”¹ç‚¹
**文件**: `src/views/mes/pd/project/modules/document-audit-list.vue`
该组件已通过 `props.projectId` æŽ¥æ”¶äº†é¡¹ç›® ID,但未传给接口。在 `proxyConfig.ajax.query` ä¸­åŠ ä¸Šå³å¯ï¼š
```typescript
// ç¬¬ 62-67 è¡Œï¼ŒåŽŸæ¥çš„ï¼š
query: async ({ page }, formValues) => {
  return await getPdDocumentAuditPage({
    pageNo: page.currentPage,
    pageSize: page.pageSize,
    ...formValues,
  });
},
// æ”¹ä¸ºï¼š
query: async ({ page }, formValues) => {
  return await getPdDocumentAuditPage({
    pageNo: page.currentPage,
    pageSize: page.pageSize,
    projectId: props.projectId,
    ...formValues,
  });
},
```
---
## æ³¨æ„äº‹é¡¹
- è¯¥ç»„件在项目状态为 `PENDING_AUDIT`(待审核)时展示,通过 props æŽ¥æ”¶ `projectId`
- æ”¹åŠ¨ä»…éœ€ä¸€è¡Œï¼Œæ— éœ€ä¿®æ”¹ API å°è£…函数或类型定义
docs/machinery_new_fields_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
# è®¾å¤‡å°è´¦æ–°å¢žå­—段 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯ç”±è·¯å¾„ | è¯´æ˜Ž |
|------|----------|------|
| è®¾å¤‡å°è´¦åˆ—表 | `/mes/dv-machinery` | åˆ—表新增筛选条件、表格列 |
| è®¾å¤‡å°è´¦è¡¨å• | `/mes/dv-machinery` (弹窗) | æ–°å¢ž/编辑表单中增加 3 ä¸ªå­—段 |
| è®¾å¤‡å°è´¦å¯¼å…¥ | åŒä¸Šï¼ˆå¯¼å…¥å¼¹çª—) | Excel æ¨¡æ¿æ–°å¢ž 3 åˆ— |
---
## æ–°å¢žå­—段说明
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| `productionEfficiency` | BigDecimal | å¦ | ç”Ÿäº§æ•ˆçŽ‡ï¼Œå•ä½ï¼šæ¯å°æ—¶ç”Ÿäº§æ•°é‡ï¼Œç”¨äºŽä¸‹æŽ’äº§è‡ªåŠ¨è®¡ç®—ç”Ÿäº§æ—¶é—´ |
| `conveyorBelt` | Integer | å¦ | ä¼ é€å±¥å¸¦æ•°é‡ï¼Œä¾‹ï¼š2 ä»£è¡¨åŒå€æ•ˆçއ |
| `workstationId` | Long | å¦ | å·¥ä½œç«™ç¼–号,用于将设备绑定到指定工作站 |
---
## API
### å·²æœ‰æŽ¥å£ï¼ˆæ— éœ€æ–°å¢žï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/mes/dv/machinery/create` | åˆ›å»ºè®¾å¤‡ |
| PUT | `/mes/dv/machinery/update` | æ›´æ–°è®¾å¤‡ |
| GET | `/mes/dv/machinery/get?id={id}` | èŽ·å–è¯¦æƒ… |
| GET | `/mes/dv/machinery/page` | åˆ†é¡µæŸ¥è¯¢ |
| GET | `/mes/dv/machinery/simple-list` | ç²¾ç®€åˆ—表(下拉选项) |
| POST | `/mes/dv/machinery/import` | å¯¼å…¥è®¾å¤‡ |
| GET | `/mes/dv/machinery/get-import-template` | ä¸‹è½½å¯¼å…¥æ¨¡æ¿ |
### è¯·æ±‚参数 - åˆ›å»º/更新(`MesDvMachinerySaveReqVO`)
**新增字段:**
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| productionEfficiency | BigDecimal | å¦ | ç”Ÿäº§æ•ˆçŽ‡ï¼ˆæ¯åˆ†é’Ÿï¼‰ |
| conveyorBelt | Integer | å¦ | ä¼ é€å±¥å¸¦æ•°é‡ |
| workstationId | Long | å¦ | å·¥ä½œç«™ç¼–号 |
### åˆ†é¡µæŸ¥è¯¢å‚数(`MesDvMachineryPageReqVO`)
**新增筛选字段:**
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| workstationId | Long | å¦ | æŒ‰å·¥ä½œç«™ç­›é€‰ |
### å“åº”(`MesDvMachineryRespVO`)
**新增字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| productionEfficiency | BigDecimal | ç”Ÿäº§æ•ˆçއ |
| conveyorBelt | Integer | ä¼ é€å±¥å¸¦ |
| workstationId | Long | å·¥ä½œç«™ç¼–号 |
| workstationName | String | å·¥ä½œç«™åç§° |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "name": "CNC åŠ å·¥ä¸­å¿ƒ",
    "code": "EQ-001",
    "productionEfficiency": 10.5,
    "conveyorBelt": 2,
    "workstationId": 300,
    "workstationName": "一号工作站",
    ...
  }
}
```
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. åˆ—表页 - ç­›é€‰é¡¹
在列表搜索栏新增"工作站"下拉筛选框,调用 `/mes/md-workstation/simple-list` èŽ·å–é€‰é¡¹ã€‚
### 2. åˆ—表页 - è¡¨æ ¼åˆ—
| æ–°å¢žåˆ— | ç»‘定字段 | è¯´æ˜Ž |
|--------|----------|------|
| ç”Ÿäº§æ•ˆçއ | `productionEfficiency` | æ˜¾ç¤ºæ•°å€¼ |
| ä¼ é€å±¥å¸¦ | `conveyorBelt` | æ˜¾ç¤ºæ•°å€¼ |
| å·¥ä½œç«™ | `workstationName` | æ˜¾ç¤ºå·¥ä½œç«™åç§° |
### 3. è¡¨å•页 - æ–°å¢ž/编辑
在表单中新增 3 ä¸ªå­—段:
| å­—段 | ç»„ä»¶ | è¯´æ˜Ž |
|------|------|------|
| ç”Ÿäº§æ•ˆçއ | `<el-input-number>` | æ”¯æŒå°æ•°ï¼Œå•位提示"每小时" |
| ä¼ é€å±¥å¸¦ | `<el-input-number>` | æ­£æ•´æ•°ï¼Œæç¤º"2=双倍效率" |
| å·¥ä½œç«™ | `<el-select>` | ä¸‹æ‹‰é€‰æ‹©ï¼Œè°ƒç”¨å·¥ä½œç«™ç²¾ç®€åˆ—表接口 |
### 4. å¯¼å…¥æ¨¡æ¿
导入 Excel æ¨¡æ¿æ–°å¢ž 3 åˆ—(可选列):
| åˆ—名 | è¯´æ˜Ž |
|------|------|
| ç”Ÿäº§æ•ˆçއ(每小时) | æ•°å­—,可选 |
| ä¼ é€å±¥å¸¦ | æ•´æ•°ï¼Œå¯é€‰ |
| å·¥ä½œç«™ç¼–码 | å·¥ä½œç«™ç¼–码,可选,填了则校验是否存在 |
---
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| è§„则 | è¯´æ˜Ž |
|------|------|
| ç”Ÿäº§æ•ˆçŽ‡å•ä½ | å›ºå®šä¸º"每小时生产数量",前端直接展示"每小时" |
| ä¼ é€å±¥å¸¦å«ä¹‰ | æ•°å€¼ 2 ä»£è¡¨åŒå€æ•ˆçŽ‡ï¼ŒæŽ’äº§æ—¶ `conveyorBelt Ã— productionEfficiency` å¾—到最终产能 |
| å·¥ä½œç«™ç»‘定 | ä¸€å°è®¾å¤‡å¯ç»‘定一个工作站(非强制),下拉数据源为 `/mes/md-workstation/simple-list` |
| å¯¼å…¥æ—¶å·¥ä½œç«™ | å¡«å…¥å·¥ä½œç«™ç¼–码(非 ID),系统自动转换,编码不存在则导入失败 |
---
## æ³¨æ„äº‹é¡¹
- 3 ä¸ªæ–°å­—段均为可选,不影响现有数据
- ç”Ÿäº§æ•ˆçŽ‡è¾“å…¥å»ºè®®é™åˆ¶æœ€å°å€¼ 0,保留 2 ä½å°æ•°
- ä¼ é€å±¥å¸¦ä»…允许正整数
- å·¥ä½œç«™ä¸‹æ‹‰ä½¿ç”¨å·²æœ‰æŽ¥å£ `/mes/md-workstation/simple-list`,无需新增
- Excel å¯¼å…¥æ—¶å·¥ä½œç«™åˆ—填编码而非 ID
- å¯¼å…¥æ¨¡æ¿åŽç«¯æŽ¥å£ `/mes/dv/machinery/get-import-template` ä¹Ÿä¼šè¿”回新列
docs/pro_archive_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,154 @@
# ç”Ÿäº§æµç¨‹ - æŸ¥çœ‹äº§å“è®¾è®¡å½’档及附件(前端联调方案)
## æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯ç”±è·¯å¾„ | è¯´æ˜Ž |
|------|----------|------|
| ç”Ÿäº§å·¥å•列表/详情 | `/mes/pro-workorder` | æ–°å¢ž"查看归档"按钮 |
| ç”Ÿäº§å·¥å•中心 | `/mes/workorder-center` | æ–°å¢ž"查看归档"按钮 |
| ç”Ÿäº§ä»»åŠ¡åˆ—è¡¨/详情 | `/mes/pro-task` | æ–°å¢ž"查看归档"按钮 |
| å¾…报工台账 | `/mes/pro-task` (pending-feedback) | æ–°å¢ž"查看归档"按钮 |
| ç”Ÿäº§æŠ¥å·¥åˆ—表/详情 | `/mes/pro-feedback` | æ–°å¢ž"查看归档"按钮 |
---
## API(已有接口,无需新增)
### å½’档分页列表
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/mes/pd/archive/page` | æŒ‰äº§å“ç¼–码筛选归档列表 |
**请求参数(关键):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| productCode | String | æ˜¯ | äº§å“ç¼–码,从当前记录的 `itemCode` æˆ– `productCode` ä¼ å…¥ |
| pageNo | Integer | å¦ | é¡µç  |
| pageSize | Integer | å¦ | æ¯é¡µæ¡æ•° |
### å½’档详情(含附件)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/mes/pd/archive/detail-by-product` | æŒ‰äº§å“ç¼–码获取归档详情和附件 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| productCode | String | æ˜¯ | äº§å“ç¼–码 |
**响应结构:**
```json
{
  "code": 0,
  "data": [
    {
      "project": {
        "id": 1,
        "taskCode": "PD2024001",
        "taskName": "新产品设计任务",
        "productCode": "P001",
        "productName": "注塑产品A",
        "version": "V1.0",
        "status": 4,
        "chiefDesigner": 1,
        "chiefDesignerNickname": "张三"
      },
      "archives": [
        {
          "id": 1,
          "archiveCode": "ARC2024001",
          "archiver": 1,
          "archiverNickname": "王五",
          "archiveStatus": 1,
          "archiveTime": "2024-01-15T10:30:00"
        }
      ],
      "documents": [
        {
          "id": 1,
          "documentType": "1",
          "documentName": "产品设计图",
          "documentVersion": "V1.0",
          "fileName": "design.pdf",
          "attachmentList": [
            {
              "id": "blob-id-xxx",
              "name": "设计图纸.pdf",
              "url": "/system/storage-blob/preview/xxx"
            }
          ],
          "uploadUserId": 1,
          "uploadUserNickname": "赵六",
          "uploadTime": "2024-01-10T09:00:00"
        }
      ]
    }
  ]
}
```
---
## æ•°æ®ä¼ é€’
生产流程各环节的记录中已包含产品编码,直接传给归档接口:
| æ¥æºé¡µé¢ | å­—段名 | ä¼ ç»™å½’档接口 |
|----------|--------|-------------|
| ç”Ÿäº§å·¥å• | `productCode` | `?productCode={productCode}` |
| ç”Ÿäº§ä»»åŠ¡ | `itemCode` | `?productCode={itemCode}` |
| ç”Ÿäº§æŠ¥å·¥ | `itemCode` | `?productCode={itemCode}` |
| å¾…报工台账 | `itemCode` | `?productCode={itemCode}` |
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æŸ¥çœ‹å½’档按钮
在生产工单、任务、报工列表的操作列,以及详情页,新增"设计归档"按钮。
点击后弹出抽屉或弹窗,调用 `/mes/pd/archive/detail-by-product?productCode={code}` èŽ·å–å½’æ¡£è¯¦æƒ…ã€‚
### 2. å¼¹çª—内容展示
以 Tab æˆ–分区展示三部分内容:
| åŒºåŸŸ | å†…容 | æ•°æ®æ¥æº |
|------|------|----------|
| é¡¹ç›®ä¿¡æ¯ | ä»»åŠ¡ç¼–ç ã€ä»»åŠ¡åç§°ã€äº§å“ç¼–ç /名称、版本、主设计师、状态 | `response[].project` |
| å½’档记录 | è¡¨æ ¼ï¼šå½’档编号、归档人、归档状态、归档时间 | `response[].archives` |
| è®¾è®¡èµ„æ–™ | è¡¨æ ¼ï¼šèµ„料类型、名称、版本、文件名,附件可点击下载/预览 | `response[].documents` |
### 3. é™„件处理
设计资料中的 `attachmentList` ä¸ºé™„件列表,其中 `url` å¯ç›´æŽ¥ç”¨äºŽæ–‡ä»¶é¢„览/下载。
文件预览 URL æ ¼å¼ï¼š
- é¢„览:`/system/storage-blob/preview/{fileName}`
- ä¸‹è½½ï¼š`/system/storage-blob/download/{fileName}`
---
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| è§„则 | è¯´æ˜Ž |
|------|------|
| äº§å“æœªå½’æ¡£ | æŽ¥å£è¿”回空数组,弹窗提示"该产品暂无设计归档" |
| å¤šä¸ªé¡¹ç›® | åŒä¸€äº§å“å¯èƒ½æœ‰å¤šä¸ªè®¾è®¡é¡¹ç›®ï¼ŒæŒ‰é¡¹ç›®åˆ†ç»„展示 |
| é™„件权限 | é™„件预览/下载走 system æ¨¡å— Storage API,无需额外鉴权 |
| ç©ºé™„ä»¶ | `attachmentList` å¯èƒ½ä¸ºç©ºæ•°ç»„,前端判断后不显示下载链接 |
---
## æ³¨æ„äº‹é¡¹
- åˆ—表页操作列宽度有限,建议按钮文字简写为"归档"
- `detail-by-product` æŽ¥å£è¿”回数组,一个产品可能对应多个设计项目
- é™„件预览使用 `window.open()` æ‰“开预览 URL
- æŽ¥å£å·²å­˜åœ¨ï¼Œå‰ç«¯ç›´æŽ¥è°ƒç”¨ `src/api/mes/pd/archive/index.ts` ä¸­çš„封装方法
docs/pro_task_calculate_duration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
# ç”Ÿäº§ä»»åŠ¡ - é€‰å·¥ä½œç«™è‡ªåŠ¨è®¡ç®—ç”Ÿäº§æ—¶é•¿ï¼ˆå‰ç«¯è”è°ƒæ–¹æ¡ˆï¼‰
## æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯ç”±è·¯å¾„ | è¯´æ˜Ž |
|------|----------|------|
| ç”Ÿäº§ä»»åŠ¡è¡¨å• | `/mes/pro-task` (新增/编辑弹窗) | é€‰æ‹©å·¥ä½œç«™åŽè‡ªåŠ¨è°ƒç”¨æŽ¥å£è®¡ç®—ç”Ÿäº§æ—¶é•¿ |
---
## API
### æ–°å¢žæŽ¥å£ï¼šè®¡ç®—生产时长
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/mes/pro/task/calculate-duration` | æ ¹æ®å·¥ä½œç«™å’Œæ•°é‡è®¡ç®—生产时长 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| workstationId | Long | æ˜¯ | å·¥ä½œç«™ç¼–号 |
| quantity | BigDecimal | æ˜¯ | æŽ’产数量 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "duration": 3
  },
  "msg": ""
}
```
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| duration | Integer | ç”Ÿäº§æ—¶é•¿ï¼ˆå°æ—¶ï¼‰ï¼Œæ— ç»‘定设备时返回 0 |
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. è§¦å‘时机
在新增/编辑生产任务的表单中,当用户**选择工作站**或**修改排产数量**后,自动调用计算接口。
### 2. å­—段回填
接口返回的 `duration` å€¼è‡ªåŠ¨å¡«å…¥è¡¨å•çš„"生产时长"字段,无需用户手动计算。
### 3. è¾¹ç•Œæƒ…况
| åœºæ™¯ | è¡¨çް |
|------|------|
| æœªé€‰å·¥ä½œç«™ | ä¸è°ƒç”¨æŽ¥å£ |
| æœªå¡«æ•°é‡ | ä¸è°ƒç”¨æŽ¥å£ |
| å·¥ä½œç«™æœªç»‘定设备 | è¿”回 `duration=0`,前端可提示"该工作站未绑定设备" |
| è®¾å¤‡æœªé…ç½®ç”Ÿäº§æ•ˆçއ | è¿”回 `duration=0`,前端可提示"绑定设备未配置生产效率" |
---
## è®¡ç®—逻辑说明
| æ­¥éª¤ | è¯´æ˜Ž |
|------|------|
| 1 | æŸ¥è¯¢å·¥ä½œç«™ç»‘定的所有设备 |
| 2 | æ€»æ•ˆçއ = Î£(每台设备的 `productionEfficiency` Ã— `conveyorBelt`) |
| 3 | ä¼ é€å±¥å¸¦ä¸º 0 æˆ–空时默认取 1 |
| 4 | ç”Ÿäº§æ—¶é•¿ï¼ˆå°æ—¶ï¼‰= å‘上取整(排产数量 / æ€»æ•ˆçއ) |
---
## æ³¨æ„äº‹é¡¹
- è°ƒç”¨æŽ¥å£å‰éœ€ç¡®ä¿ `workstationId` å’Œ `quantity` å‡æœ‰å€¼ï¼Œé¿å…æ— æ•ˆè¯·æ±‚
- æŽ¥å£è¿”回 0 æ—¶ï¼Œå»ºè®®ç»™å‡ºå‹å¥½æç¤ºï¼Œè€Œéžé™é»˜
- ç”¨æˆ·ä»å¯æ‰‹åŠ¨ä¿®æ”¹è®¡ç®—åŽçš„ç”Ÿäº§æ—¶é•¿
- è¯¥æŽ¥å£ä»…用于前端辅助计算,不影响后端创建/更新任务的业务逻辑
docs/sql/aftersales_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
-- ============================================================
-- å”®åŽå·¥å•模块 DDL
-- ============================================================
-- 1. å”®åŽå·¥å•主表
DROP TABLE IF EXISTS `after_sale_ticket`;
CREATE TABLE `after_sale_ticket` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '工单编号',
  `no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '工单单号',
  `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '工单主题/标题',
  `customer_id` bigint NOT NULL COMMENT '客户编号,关联 crm_customer.id',
  `contact_id` bigint DEFAULT NULL COMMENT '联系人编号,关联 crm_contact.id',
  `sale_order_id` bigint DEFAULT NULL COMMENT '销售订单编号,关联 erp_sale_order.id',
  `sale_order_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '销售订单号(冗余)',
  `contract_id` bigint DEFAULT NULL COMMENT '合同编号,关联 crm_contract.id',
  `issue_type` tinyint NOT NULL DEFAULT 0 COMMENT '问题类型: 0-未判定 1-一般问题 2-维修问题 3-退货问题',
  `issue_category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '问题分类',
  `issue_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '问题描述',
  `severity_level` tinyint DEFAULT 0 COMMENT '严重程度: 0-轻微 1-一般 2-严重 3-致命',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态: 0-草稿 10-处理中 30-待退货 40-待退款/换货 50-已完结 60-已关闭 99-已取消',
  `owner_user_id` bigint NOT NULL COMMENT '负责人用户编号',
  `handler_user_id` bigint DEFAULT NULL COMMENT '处理人用户编号',
  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM工作流实例编号',
  `return_id` bigint DEFAULT NULL COMMENT '关联退货申请编号',
  `repair_id` bigint DEFAULT NULL COMMENT '关联维修记录编号',
  `resolved_time` datetime DEFAULT NULL COMMENT '解决时间',
  `closed_time` datetime DEFAULT NULL COMMENT '关闭时间',
  `remark` varchar(500) 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 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,
  UNIQUE KEY `uk_no` (`no`),
  KEY `idx_customer_id` (`customer_id`),
  KEY `idx_sale_order_id` (`sale_order_id`),
  KEY `idx_owner_user_id` (`owner_user_id`),
  KEY `idx_status` (`status`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='售后工单主表';
-- 2. å”®åŽå·¥å•明细行表
DROP TABLE IF EXISTS `after_sale_ticket_item`;
CREATE TABLE `after_sale_ticket_item` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `ticket_id` bigint NOT NULL COMMENT '工单编号,关联 after_sale_ticket.id',
  `sale_order_item_id` bigint DEFAULT NULL COMMENT '销售订单行编号',
  `item_id` bigint NOT NULL COMMENT 'MDM物料编号',
  `item_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '物料编码(冗余)',
  `item_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '物料名称(冗余)',
  `batch_id` bigint DEFAULT NULL COMMENT '生产批次编号',
  `batch_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '生产批次号',
  `sale_quantity` decimal(10,3) NOT NULL DEFAULT 0.000 COMMENT '销售数量',
  `return_quantity` decimal(10,3) DEFAULT 0.000 COMMENT '退货数量',
  `need_repair` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否需要维修',
  `need_return` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否需要退货',
  `remark` varchar(500) 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 DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_ticket_id` (`ticket_id`),
  KEY `idx_item_id` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='售后工单明细行表';
-- 3. é€€è´§ç”³è¯·è¡¨
DROP TABLE IF EXISTS `after_sale_return`;
CREATE TABLE `after_sale_return` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '退货申请编号',
  `no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '退货单号',
  `ticket_id` bigint NOT NULL COMMENT '关联工单编号,关联 after_sale_ticket.id',
  `customer_id` bigint NOT NULL COMMENT '客户编号',
  `sale_order_id` bigint DEFAULT NULL COMMENT '关联销售订单编号',
  `return_type` tinyint NOT NULL COMMENT '退货类型: 1-退款退货 2-换货 3-仅退款',
  `return_reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '退货原因',
  `defect_category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '缺陷分类',
  `total_return_price` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '应退总金额',
  `actual_refund_price` decimal(12,2) DEFAULT 0.00 COMMENT '实退金额',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态: 0-草稿 10-销售审核中 20-审核通过已同步MES 30-检验中 40-检验完成 50-已入库 60-退款/换货完成 70-已关闭 99-已取消',
  `audit_user_id` bigint DEFAULT NULL COMMENT '审核人',
  `audit_time` datetime DEFAULT NULL COMMENT '审核时间',
  `mes_return_id` bigint DEFAULT NULL COMMENT 'MES退货单编号,关联 mes_wm_return_sales.id',
  `mes_return_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'MES退货单号(冗余)',
  `quality_result` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '质检结论: åˆæ ¼/特采/退货/报废',
  `quality_report_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '质检报告附件URL',
  `return_inbound_time` datetime DEFAULT NULL COMMENT '退货入库完成时间',
  `refund_time` datetime DEFAULT NULL COMMENT '退款/换货完成时间',
  `remark` varchar(500) 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 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,
  UNIQUE KEY `uk_no` (`no`),
  KEY `idx_ticket_id` (`ticket_id`),
  KEY `idx_mes_return_id` (`mes_return_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='售后退货申请表';
-- 4. é€€è´§æ˜Žç»†è¡Œè¡¨
DROP TABLE IF EXISTS `after_sale_return_item`;
CREATE TABLE `after_sale_return_item` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `return_id` bigint NOT NULL COMMENT '退货申请编号,关联 after_sale_return.id',
  `ticket_item_id` bigint NOT NULL COMMENT '工单明细行编号,关联 after_sale_ticket_item.id',
  `item_id` bigint NOT NULL COMMENT 'MDM物料编号',
  `item_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '物料编码(冗余)',
  `item_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '物料名称(冗余)',
  `batch_id` bigint DEFAULT NULL COMMENT '批次编号',
  `batch_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '批次号',
  `return_quantity` decimal(10,3) NOT NULL COMMENT '退货数量',
  `unit_price` decimal(12,2) NOT NULL COMMENT '销售单价',
  `return_price` decimal(12,2) NOT NULL COMMENT '退货金额',
  `mes_return_line_id` bigint DEFAULT NULL COMMENT 'MES退货单行编号',
  `remark` varchar(500) 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 DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_return_id` (`return_id`),
  KEY `idx_ticket_item_id` (`ticket_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='售后退货明细行表';
-- 5. ç»´ä¿®è®°å½•表
DROP TABLE IF EXISTS `after_sale_repair`;
CREATE TABLE `after_sale_repair` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '维修记录编号',
  `ticket_id` bigint NOT NULL COMMENT '关联工单编号,关联 after_sale_ticket.id',
  `repair_type` tinyint NOT NULL COMMENT '维修类型: 1-现场维修 2-返厂维修 3-远程指导',
  `fault_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '故障描述',
  `repair_action` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '维修措施',
  `repair_result` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '维修结果: å·²ä¿®å¤/无法修复/部分修复',
  `repair_cost` decimal(10,2) DEFAULT 0.00 COMMENT '维修费用',
  `repair_user_id` bigint DEFAULT NULL COMMENT '维修人',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态: 0-待处理 10-维修中 20-已完成 30-已关闭',
  `start_time` datetime DEFAULT NULL COMMENT '开始维修时间',
  `finish_time` datetime DEFAULT NULL COMMENT '完成维修时间',
  `remark` varchar(500) 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 DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_ticket_id` (`ticket_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='售后维修记录表';
-- 6. å”®åŽæ•°æ®æƒé™è¡¨
DROP TABLE IF EXISTS `after_sale_permission`;
CREATE TABLE `after_sale_permission` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `biz_type` tinyint NOT NULL COMMENT '数据类型: 1-售后工单 2-退货申请 3-维修记录',
  `biz_id` bigint NOT NULL COMMENT '数据编号',
  `user_id` bigint NOT NULL COMMENT '用户编号',
  `level` tinyint NOT NULL COMMENT '权限级别: 1-负责人 2-只读 3-读写',
  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_biz` (`biz_type`, `biz_id`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='售后数据权限表';
pom.xml
@@ -38,6 +38,9 @@
        <!-- SRM ä¾›åº”商关系管理模块 -->
        <module>yudao-module-srm-api</module>
        <module>yudao-module-srm</module>
        <!-- å”®åŽå·¥å•模块 -->
        <module>yudao-module-aftersales-api</module>
        <module>yudao-module-aftersales</module>
        <!-- è¯·å‚考 https://doc.iocoder.cn/ai/build/ æ–‡æ¡£ï¼Œå®Œæˆ AI æ¨¡å—的启动!!! -->
<!--        <module>yudao-module-ai</module>-->
    </modules>
sql/mysql/mes_pd_menu_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
-- =============================================
-- MES PD äº§å“è®¾è®¡æ¨¡å—菜单初始化
-- åœ¨ MES ç³»ç»Ÿ(5100) ä¸‹æ–°å¢ž"产品设计"二级目录
-- è®¾è®¡é¡¹ç›®ã€è®¾è®¡å½’档两个页面
-- =============================================
-- ä¿®æ­£åŽŸæœ‰"产品设计"(6809) å‘½åä¸º"工艺设计",避免冲突(其内容为工艺设计:工序设置、工艺流程等)
UPDATE `system_menu` SET `name` = '工艺设计', `updater` = '1', `update_time` = NOW() WHERE `id` = 6809 AND `name` = '产品设计';
-- äº§å“è®¾è®¡ï¼ˆäºŒçº§ç›®å½•,放在 MES ç³»ç»Ÿä¸‹ï¼Œsort=25 ä½äºŽå·¥è‰ºè®¾è®¡ä¹‹åŽï¼‰
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES (602571, '产品设计', '', 1, 25, 5100, 'pd', 'ep:design', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- è®¾è®¡é¡¹ç›®ï¼ˆä¸‰çº§èœå•)
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES (602572, '设计项目', '', 2, 1, 602571, 'project', 'ep:document', 'mes/pd/project/index', 'MesPdProject', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- è®¾è®¡é¡¹ç›® - æŒ‰é’®æƒé™
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(602573, '设计项目查询', 'mes:pd-project:query', 3, 1, 602572, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602574, '设计项目创建', 'mes:pd-project:create', 3, 2, 602572, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602575, '设计项目更新', 'mes:pd-project:update', 3, 3, 602572, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602576, '设计项目删除', 'mes:pd-project:delete', 3, 4, 602572, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602577, '设计项目导出', 'mes:pd-project:export', 3, 5, 602572, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- è®¾è®¡å½’档(三级菜单)
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES (602578, '设计归档', '', 2, 2, 602571, 'archive', 'ep:folder', 'mes/pd/archive/index', 'MesPdArchive', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- è®¾è®¡å½’æ¡£ - æŒ‰é’®æƒé™
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(602579, '设计归档查询', 'mes:pd-archive:query', 3, 1, 602578, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602580, '设计归档更新', 'mes:pd-archive:update', 3, 2, 602578, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602581, '设计归档删除', 'mes:pd-archive:delete', 3, 3, 602578, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602582, '设计归档导出', 'mes:pd-archive:export', 3, 4, 602578, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
yudao-module-aftersales-api/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-aftersales-api</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        å”®åŽå·¥å•模块的 API æŽ¥å£å®šä¹‰ï¼Œä¾›å…¶ä»–模块引用
    </description>
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>
        <!-- Swagger æ³¨è§£ -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.aftersales.api;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleReturnRespDTO;
import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleTicketRespDTO;
import java.math.BigDecimal;
/**
 * å”®åŽå·¥å• API æŽ¥å£ï¼ˆä¾› MES/ERP è·¨æ¨¡å—调用)
 */
public interface AfterSaleTicketApi {
    String PREFIX = "/rpc-api/aftersales/ticket";
    /**
     * MES é€€è´§è´¨æ£€å®ŒæˆåŽå›žè°ƒ
     *
     * @param returnId      é€€è´§ç”³è¯·ç¼–号
     * @param qualityResult è´¨æ£€ç»“论
     * @param reportUrl     è´¨æ£€æŠ¥å‘Š URL
     */
    CommonResult<Boolean> onReturnQualityFinished(Long returnId, String qualityResult, String reportUrl);
    /**
     * MES é€€è´§å…¥åº“完成后回调
     *
     * @param returnId é€€è´§ç”³è¯·ç¼–号
     */
    CommonResult<Boolean> onReturnInboundFinished(Long returnId);
    /**
     * ERP é€€æ¬¾å®ŒæˆåŽå›žè°ƒ
     *
     * @param returnId           é€€è´§ç”³è¯·ç¼–号
     * @param actualRefundPrice  å®žé€€é‡‘额
     */
    CommonResult<Boolean> onRefundCompleted(Long returnId, BigDecimal actualRefundPrice);
    /**
     * æŸ¥è¯¢å”®åŽå·¥å•(供 MES/ERP æŸ¥è¯¢å…³è”信息)
     *
     * @param id å·¥å•编号
     */
    CommonResult<AfterSaleTicketRespDTO> getTicket(Long id);
    /**
     * æŸ¥è¯¢é€€è´§ç”³è¯·ï¼ˆä¾› MES/ERP æŸ¥è¯¢å…³è”信息)
     *
     * @param id é€€è´§ç”³è¯·ç¼–号
     */
    CommonResult<AfterSaleReturnRespDTO> getReturn(Long id);
}
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleReturnRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.aftersales.api.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å”®åŽé€€è´§ç”³è¯·å“åº” DTO(供 RPC è°ƒç”¨ä½¿ç”¨ï¼‰
 */
@Data
public class AfterSaleReturnRespDTO {
    private Long id;
    private String no;
    private Long ticketId;
    private Long customerId;
    private Long saleOrderId;
    private Integer returnType;
    private String returnReason;
    private BigDecimal totalReturnPrice;
    private BigDecimal actualRefundPrice;
    private Integer status;
    private Long mesReturnId;
    private String mesReturnCode;
    private String qualityResult;
    private String qualityReportUrl;
    private LocalDateTime returnInboundTime;
    private LocalDateTime refundTime;
    private LocalDateTime createTime;
}
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleTicketRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.aftersales.api.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * å”®åŽå·¥å•响应 DTO(供 RPC è°ƒç”¨ä½¿ç”¨ï¼‰
 */
@Data
public class AfterSaleTicketRespDTO {
    private Long id;
    private String no;
    private String name;
    private Long customerId;
    private Long saleOrderId;
    private String saleOrderNo;
    private Integer issueType;
    private String issueCategory;
    private String issueDescription;
    private Integer severityLevel;
    private Integer status;
    private Long ownerUserId;
    private Long handlerUserId;
    private Long returnId;
    private Long repairId;
    private LocalDateTime resolvedTime;
    private LocalDateTime createTime;
}
yudao-module-aftersales/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-aftersales</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        aftersales åŒ…下,售后服务管理(After-sales Service Management)。
        ä¾‹å¦‚说:售后工单、退货申请、维修记录等等
    </description>
    <dependencies>
        <!-- å”®åŽ API æ¨¡å— -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-aftersales-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- CRM API æ¨¡å— -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-crm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-infra</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-bpm</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mdm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ERP é”€å”®è®¢å• API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-erp-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- MES API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mes-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- HRM API(数据交接) -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-hrm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ä¸šåŠ¡ç»„ä»¶ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
        </dependency>
        <!-- Web ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-security</artifactId>
        </dependency>
        <!-- DB ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
        </dependency>
        <!-- å·¥å…·ç±»ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-excel</artifactId>
        </dependency>
        <!-- Test æµ‹è¯•相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.aftersales.api;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleReturnRespDTO;
import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleTicketRespDTO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.service.returnobj.AfterSaleReturnService;
import cn.iocoder.yudao.module.aftersales.service.ticket.AfterSaleTicketService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å”®åŽå·¥å• API å®žçŽ°ç±»ï¼ˆä¾› MES/ERP è·¨æ¨¡å—调用)
 */
@RestController
public class AfterSaleTicketApiImpl implements AfterSaleTicketApi {
    @Resource
    private AfterSaleTicketService ticketService;
    @Resource
    private AfterSaleReturnService returnService;
    @Override
    public CommonResult<Boolean> onReturnQualityFinished(Long returnId, String qualityResult, String reportUrl) {
        returnService.onQualityFinished(returnId, qualityResult, reportUrl);
        return success(true);
    }
    @Override
    public CommonResult<Boolean> onReturnInboundFinished(Long returnId) {
        returnService.onInboundFinished(returnId);
        return success(true);
    }
    @Override
    public CommonResult<Boolean> onRefundCompleted(Long returnId, BigDecimal actualRefundPrice) {
        returnService.onRefundCompleted(returnId, actualRefundPrice);
        return success(true);
    }
    @Override
    public CommonResult<AfterSaleTicketRespDTO> getTicket(Long id) {
        AfterSaleTicketDO ticket = ticketService.getTicket(id);
        return success(BeanUtils.toBean(ticket, AfterSaleTicketRespDTO.class));
    }
    @Override
    public CommonResult<AfterSaleReturnRespDTO> getReturn(Long id) {
        AfterSaleReturnDO returnDO = returnService.getReturn(id);
        return success(BeanUtils.toBean(returnDO, AfterSaleReturnRespDTO.class));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/AfterSaleReturnController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnRespVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
import cn.iocoder.yudao.module.aftersales.service.returnobj.AfterSaleReturnService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - å”®åŽé€€è´§ç”³è¯·")
@RestController
@RequestMapping("/aftersales/return")
@Validated
public class AfterSaleReturnController {
    @Resource
    private AfterSaleReturnService returnService;
    @Resource
    private CrmCustomerApi customerApi;
    @PostMapping("/create")
    @Operation(summary = "创建退货申请")
    @PreAuthorize("@ss.hasPermission('aftersales:return:create')")
    public CommonResult<Long> createReturn(@Valid @RequestBody AfterSaleReturnSaveReqVO createReqVO) {
        return success(returnService.createReturn(createReqVO, getLoginUserId()));
    }
    @PutMapping("/approve")
    @Operation(summary = "审核通过退货申请")
    @PreAuthorize("@ss.hasPermission('aftersales:return:approve')")
    public CommonResult<Boolean> approveReturn(@RequestParam("id") Long id) {
        returnService.approveReturn(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得退货申请")
    @Parameter(name = "id", description = "退货申请编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:return:query')")
    public CommonResult<AfterSaleReturnRespVO> getReturn(@RequestParam("id") Long id) {
        AfterSaleReturnDO returnDO = returnService.getReturn(id);
        List<AfterSaleReturnItemDO> items = returnService.getReturnItemListByReturnId(id);
        List<AfterSaleReturnRespVO> enrichedList = buildReturnDetailList(List.of(returnDO));
        AfterSaleReturnRespVO respVO = enrichedList.get(0);
        respVO.setItems(BeanUtils.toBean(items, AfterSaleReturnRespVO.Item.class));
        return success(respVO);
    }
    @GetMapping("/page")
    @Operation(summary = "获得退货申请分页")
    @PreAuthorize("@ss.hasPermission('aftersales:return:query')")
    public CommonResult<PageResult<AfterSaleReturnRespVO>> getReturnPage(@Valid AfterSaleReturnPageReqVO pageReqVO) {
        PageResult<AfterSaleReturnDO> pageResult = returnService.getReturnPage(pageReqVO, getLoginUserId());
        List<AfterSaleReturnRespVO> list = buildReturnDetailList(pageResult.getList());
        return success(new PageResult<>(list, pageResult.getTotal()));
    }
    // ========== å†…部方法:批量拼装关联名称 ==========
    private List<AfterSaleReturnRespVO> buildReturnDetailList(List<AfterSaleReturnDO> returnList) {
        if (CollUtil.isEmpty(returnList)) {
            return Collections.emptyList();
        }
        // æ‰¹é‡åŠ è½½å®¢æˆ·åç§°
        Set<Long> customerIds = convertSet(returnList, AfterSaleReturnDO::getCustomerId);
        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(customerIds);
        // æ‹¼è£…名称字段
        return BeanUtils.toBean(returnList, AfterSaleReturnRespVO.class, vo -> {
            CrmCustomerRespDTO customer = customerMap.get(vo.getCustomerId());
            if (customer != null) vo.setCustomerName(customer.getName());
        });
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
 * å”®åŽé€€è´§ç”³è¯·åˆ†é¡µæŸ¥è¯¢ VO
 */
@Schema(description = "售后退货申请 - åˆ†é¡µæŸ¥è¯¢ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AfterSaleReturnPageReqVO extends PageParam {
    @Schema(description = "退货单号", example = "RT20240101000001")
    private String no;
    @Schema(description = "关联工单编号", example = "1")
    private Long ticketId;
    @Schema(description = "客户编号", example = "1")
    private Long customerId;
    @Schema(description = "退货状态", example = "10")
    private Integer status;
    @Schema(description = "场景类型", example = "1")
    private Integer sceneType;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
 * å”®åŽé€€è´§ç”³è¯·å“åº” VO
 */
@Schema(description = "售后退货申请 - å“åº” VO")
@Data
public class AfterSaleReturnRespVO {
    @Schema(description = "退货申请编号", example = "1")
    private Long id;
    @Schema(description = "退货单号", example = "RT20240101000001")
    private String no;
    @Schema(description = "关联工单编号", example = "1")
    private Long ticketId;
    @Schema(description = "客户编号", example = "1")
    private Long customerId;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "关联销售订单编号", example = "1")
    private Long saleOrderId;
    @Schema(description = "退货类型: 1-退款退货 2-换货 3-仅退款", example = "1")
    private Integer returnType;
    @Schema(description = "退货原因", example = "产品外观缺陷")
    private String returnReason;
    @Schema(description = "缺陷分类", example = "外观缺陷")
    private String defectCategory;
    @Schema(description = "应退总金额", example = "1000.00")
    private BigDecimal totalReturnPrice;
    @Schema(description = "实退金额", example = "950.00")
    private BigDecimal actualRefundPrice;
    @Schema(description = "状态", example = "10")
    private Integer status;
    @Schema(description = "审核人", example = "1")
    private Long auditUserId;
    @Schema(description = "审核时间")
    private LocalDateTime auditTime;
    @Schema(description = "MES退货单编号", example = "1")
    private Long mesReturnId;
    @Schema(description = "MES退货单号", example = "MES-RT-001")
    private String mesReturnCode;
    @Schema(description = "质检结论", example = "合格")
    private String qualityResult;
    @Schema(description = "质检报告附件URL")
    private String qualityReportUrl;
    @Schema(description = "退货入库完成时间")
    private LocalDateTime returnInboundTime;
    @Schema(description = "退款/换货完成时间")
    private LocalDateTime refundTime;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建者")
    private String creator;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "更新者")
    private String updater;
    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
    @Schema(description = "退货明细行列表")
    private List<Item> items;
    @Schema(description = "退货明细行")
    @Data
    public static class Item {
        @Schema(description = "编号", example = "1")
        private Long id;
        @Schema(description = "退货申请编号", example = "1")
        private Long returnId;
        @Schema(description = "工单明细行编号", example = "1")
        private Long ticketItemId;
        @Schema(description = "MDM物料编号", example = "1")
        private Long itemId;
        @Schema(description = "物料编码", example = "MAT-001")
        private String itemCode;
        @Schema(description = "物料名称", example = "电子元器件")
        private String itemName;
        @Schema(description = "批次编号", example = "1")
        private Long batchId;
        @Schema(description = "批次号", example = "BATCH-2024-001")
        private String batchCode;
        @Schema(description = "退货数量", example = "10.000")
        private BigDecimal returnQuantity;
        @Schema(description = "销售单价", example = "100.00")
        private BigDecimal unitPrice;
        @Schema(description = "退货金额", example = "1000.00")
        private BigDecimal returnPrice;
        @Schema(description = "MES退货单行编号", example = "1")
        private Long mesReturnLineId;
        @Schema(description = "备注")
        private String remark;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
 * å”®åŽé€€è´§ç”³è¯·ä¿å­˜ VO
 */
@Schema(description = "售后退货申请 - ä¿å­˜ Request VO")
@Data
public class AfterSaleReturnSaveReqVO {
    @Schema(description = "退货申请编号", example = "1")
    private Long id;
    @Schema(description = "关联工单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "关联工单不能为空")
    private Long ticketId;
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "客户不能为空")
    private Long customerId;
    @Schema(description = "关联销售订单编号", example = "1")
    private Long saleOrderId;
    @Schema(description = "退货类型: 1-退款退货 2-换货 3-仅退款", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "退货类型不能为空")
    private Integer returnType;
    @Schema(description = "退货原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品外观缺陷")
    @NotEmpty(message = "退货原因不能为空")
    private String returnReason;
    @Schema(description = "缺陷分类", example = "外观缺陷")
    private String defectCategory;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "退货明细行列表")
    private List<Item> items;
    @Schema(description = "退货明细行")
    @Data
    public static class Item {
        @Schema(description = "工单明细行编号", requiredMode = Schema.RequiredMode.REQUIRED)
        @NotNull(message = "工单明细行不能为空")
        private Long ticketItemId;
        @Schema(description = "MDM物料编号", requiredMode = Schema.RequiredMode.REQUIRED)
        @NotNull(message = "物料不能为空")
        private Long itemId;
        @Schema(description = "物料编码", example = "MAT-001")
        private String itemCode;
        @Schema(description = "物料名称", example = "电子元器件")
        private String itemName;
        @Schema(description = "批次编号", example = "1")
        private Long batchId;
        @Schema(description = "批次号", example = "BATCH-2024-001")
        private String batchCode;
        @Schema(description = "退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.000")
        @NotNull(message = "退货数量不能为空")
        private BigDecimal returnQuantity;
        @Schema(description = "销售单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        @NotNull(message = "销售单价不能为空")
        private BigDecimal unitPrice;
        @Schema(description = "退货金额", example = "1000.00")
        private BigDecimal returnPrice;
        @Schema(description = "备注")
        private String remark;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.ticket;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketRespVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
import cn.iocoder.yudao.module.aftersales.service.ticket.AfterSaleTicketService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - å”®åŽå·¥å•")
@RestController
@RequestMapping("/aftersales/ticket")
@Validated
public class AfterSaleTicketController {
    @Resource
    private AfterSaleTicketService ticketService;
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建售后工单")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:create')")
    public CommonResult<Long> createTicket(@Valid @RequestBody AfterSaleTicketSaveReqVO createReqVO) {
        return success(ticketService.createTicket(createReqVO, getLoginUserId()));
    }
    @PutMapping("/update")
    @Operation(summary = "更新售后工单")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
    public CommonResult<Boolean> updateTicket(@Valid @RequestBody AfterSaleTicketSaveReqVO updateReqVO) {
        ticketService.updateTicket(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除售后工单")
    @Parameter(name = "id", description = "工单编号", required = true)
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:delete')")
    public CommonResult<Boolean> deleteTicket(@RequestParam("id") Long id) {
        ticketService.deleteTicket(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得售后工单")
    @Parameter(name = "id", description = "工单编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:query')")
    public CommonResult<AfterSaleTicketRespVO> getTicket(@RequestParam("id") Long id) {
        AfterSaleTicketDO ticket = ticketService.getTicket(id);
        List<AfterSaleTicketItemDO> items = ticketService.getTicketItemListByTicketId(id);
        // æ‰¹é‡åŠ è½½å…³è”åç§°
        List<AfterSaleTicketRespVO> enrichedList = buildTicketDetailList(List.of(ticket));
        AfterSaleTicketRespVO respVO = enrichedList.get(0);
        respVO.setItems(BeanUtils.toBean(items, AfterSaleTicketRespVO.Item.class));
        return success(respVO);
    }
    @GetMapping("/page")
    @Operation(summary = "获得售后工单分页")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:query')")
    public CommonResult<PageResult<AfterSaleTicketRespVO>> getTicketPage(@Valid AfterSaleTicketPageReqVO pageReqVO) {
        PageResult<AfterSaleTicketDO> pageResult = ticketService.getTicketPage(pageReqVO, getLoginUserId());
        List<AfterSaleTicketRespVO> list = buildTicketDetailList(pageResult.getList());
        return success(new PageResult<>(list, pageResult.getTotal()));
    }
    @PostMapping("/assign")
    @Operation(summary = "分配处理人")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
    public CommonResult<Boolean> assignTicket(@RequestParam("id") Long id,
                                              @RequestParam("handlerUserId") Long handlerUserId) {
        ticketService.assignTicket(id, handlerUserId);
        return success(true);
    }
    @PostMapping("/classify")
    @Operation(summary = "问题判定")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
    public CommonResult<Boolean> classifyTicket(@RequestParam("id") Long id,
                                                @RequestParam("issueType") Integer issueType,
                                                @RequestParam(value = "issueCategory", required = false) String issueCategory,
                                                @RequestParam(value = "severityLevel", required = false) Integer severityLevel) {
        ticketService.classifyTicket(id, issueType, issueCategory, severityLevel);
        return success(true);
    }
    @PutMapping("/resolve")
    @Operation(summary = "完结工单")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
    public CommonResult<Boolean> resolveTicket(@RequestParam("id") Long id) {
        ticketService.resolveTicket(id);
        return success(true);
    }
    @PutMapping("/close")
    @Operation(summary = "关闭工单")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
    public CommonResult<Boolean> closeTicket(@RequestParam("id") Long id) {
        ticketService.closeTicket(id);
        return success(true);
    }
    @PutMapping("/cancel")
    @Operation(summary = "取消工单")
    @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
    public CommonResult<Boolean> cancelTicket(@RequestParam("id") Long id) {
        ticketService.cancelTicket(id);
        return success(true);
    }
    // ========== å†…部方法:批量拼装关联名称 ==========
    private List<AfterSaleTicketRespVO> buildTicketDetailList(List<AfterSaleTicketDO> ticketList) {
        if (CollUtil.isEmpty(ticketList)) {
            return Collections.emptyList();
        }
        // 1. æ‰¹é‡åŠ è½½å®¢æˆ·åç§°
        Set<Long> customerIds = convertSet(ticketList, AfterSaleTicketDO::getCustomerId);
        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(customerIds);
        // 2. æ‰¹é‡åŠ è½½ç”¨æˆ·åç§°ï¼ˆè´Ÿè´£äºº + å¤„理人)
        Set<Long> userIds = new HashSet<>();
        ticketList.forEach(t -> {
            if (t.getOwnerUserId() != null) userIds.add(t.getOwnerUserId());
            if (t.getHandlerUserId() != null) userIds.add(t.getHandlerUserId());
        });
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        // 3. æ‹¼è£…名称字段
        return BeanUtils.toBean(ticketList, AfterSaleTicketRespVO.class, vo -> {
            CrmCustomerRespDTO customer = customerMap.get(vo.getCustomerId());
            if (customer != null) vo.setCustomerName(customer.getName());
            AdminUserRespDTO ownerUser = userMap.get(vo.getOwnerUserId());
            if (ownerUser != null) vo.setOwnerUserName(ownerUser.getNickname());
            AdminUserRespDTO handlerUser = userMap.get(vo.getHandlerUserId());
            if (handlerUser != null) vo.setHandlerUserName(handlerUser.getNickname());
        });
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
 * å”®åŽå·¥å•分页查询 VO
 */
@Schema(description = "售后工单 - åˆ†é¡µæŸ¥è¯¢ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AfterSaleTicketPageReqVO extends PageParam {
    @Schema(description = "工单单号", example = "AS20240101000001")
    private String no;
    @Schema(description = "工单主题", example = "产品外观缺陷")
    private String name;
    @Schema(description = "客户编号", example = "1")
    private Long customerId;
    @Schema(description = "销售订单编号", example = "1")
    private Long saleOrderId;
    @Schema(description = "工单状态", example = "10")
    private Integer status;
    @Schema(description = "问题类型", example = "1")
    private Integer issueType;
    @Schema(description = "场景类型", example = "1")
    private Integer sceneType;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,151 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
 * å”®åŽå·¥å•响应 VO
 */
@Schema(description = "售后工单 - å“åº” VO")
@Data
public class AfterSaleTicketRespVO {
    @Schema(description = "工单编号", example = "1")
    private Long id;
    @Schema(description = "工单单号", example = "AS20240101000001")
    private String no;
    @Schema(description = "工单主题", example = "产品外观缺陷")
    private String name;
    @Schema(description = "客户编号", example = "1")
    private Long customerId;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "联系人编号", example = "1")
    private Long contactId;
    @Schema(description = "联系人名称")
    private String contactName;
    @Schema(description = "销售订单编号", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单号", example = "SO202401010001")
    private String saleOrderNo;
    @Schema(description = "合同编号", example = "1")
    private Long contractId;
    @Schema(description = "问题类型: 1-一般问题 2-维修问题 3-退货问题", example = "1")
    private Integer issueType;
    @Schema(description = "问题分类", example = "外观缺陷")
    private String issueCategory;
    @Schema(description = "问题描述", example = "收到货后发现产品表面有划痕")
    private String issueDescription;
    @Schema(description = "严重程度: 0-轻微 1-一般 2-严重 3-致命", example = "1")
    private Integer severityLevel;
    @Schema(description = "状态: 0-草稿 10-处理中 30-待退货 40-待退款/换货 50-已完结 60-已关闭 99-已取消", example = "0")
    private Integer status;
    @Schema(description = "负责人用户编号", example = "1")
    private Long ownerUserId;
    @Schema(description = "负责人名称")
    private String ownerUserName;
    @Schema(description = "处理人用户编号", example = "2")
    private Long handlerUserId;
    @Schema(description = "处理人名称")
    private String handlerUserName;
    @Schema(description = "BPM å·¥ä½œæµå®žä¾‹ç¼–号")
    private String processInstanceId;
    @Schema(description = "关联退货申请编号", example = "1")
    private Long returnId;
    @Schema(description = "关联维修记录编号", example = "1")
    private Long repairId;
    @Schema(description = "解决时间")
    private LocalDateTime resolvedTime;
    @Schema(description = "关闭时间")
    private LocalDateTime closedTime;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建者")
    private String creator;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "更新者")
    private String updater;
    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
    @Schema(description = "明细行列表")
    private List<Item> items;
    @Schema(description = "工单明细行")
    @Data
    public static class Item {
        @Schema(description = "编号", example = "1")
        private Long id;
        @Schema(description = "工单编号", example = "1")
        private Long ticketId;
        @Schema(description = "销售订单行编号", example = "1")
        private Long saleOrderItemId;
        @Schema(description = "MDM物料编号", example = "1")
        private Long itemId;
        @Schema(description = "物料编码", example = "MAT-001")
        private String itemCode;
        @Schema(description = "物料名称", example = "电子元器件")
        private String itemName;
        @Schema(description = "生产批次编号", example = "1")
        private Long batchId;
        @Schema(description = "生产批次号", example = "BATCH-2024-001")
        private String batchCode;
        @Schema(description = "销售数量", example = "100.000")
        private BigDecimal saleQuantity;
        @Schema(description = "退货数量", example = "10.000")
        private BigDecimal returnQuantity;
        @Schema(description = "是否需要维修", example = "false")
        private Boolean needRepair;
        @Schema(description = "是否需要退货", example = "true")
        private Boolean needReturn;
        @Schema(description = "备注")
        private String remark;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
 * å”®åŽå·¥å•保存 VO
 */
@Schema(description = "售后工单 - ä¿å­˜ Request VO")
@Data
public class AfterSaleTicketSaveReqVO {
    @Schema(description = "工单编号", example = "1")
    private Long id;
    @Schema(description = "工单主题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品外观缺陷")
    @NotEmpty(message = "工单主题不能为空")
    private String name;
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "客户不能为空")
    private Long customerId;
    @Schema(description = "联系人编号", example = "1")
    private Long contactId;
    @Schema(description = "销售订单编号", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单号", example = "SO202401010001")
    private String saleOrderNo;
    @Schema(description = "合同编号", example = "1")
    private Long contractId;
    @Schema(description = "问题描述", example = "收到货后发现产品表面有划痕")
    private String issueDescription;
    @Schema(description = "备注", example = "客户要求尽快处理")
    private String remark;
    @Schema(description = "明细行列表")
    private List<Item> items;
    @Schema(description = "工单明细行")
    @Data
    public static class Item {
        @Schema(description = "销售订单行编号", example = "1")
        private Long saleOrderItemId;
        @Schema(description = "MDM物料编号", requiredMode = Schema.RequiredMode.REQUIRED)
        @NotNull(message = "物料不能为空")
        private Long itemId;
        @Schema(description = "物料编码", example = "MAT-001")
        private String itemCode;
        @Schema(description = "物料名称", example = "电子元器件")
        private String itemName;
        @Schema(description = "生产批次编号", example = "1")
        private Long batchId;
        @Schema(description = "生产批次号", example = "BATCH-2024-001")
        private String batchCode;
        @Schema(description = "销售数量", example = "100.000")
        private BigDecimal saleQuantity;
        @Schema(description = "退货数量", example = "10.000")
        private BigDecimal returnQuantity;
        @Schema(description = "是否需要维修", example = "false")
        private Boolean needRepair;
        @Schema(description = "是否需要退货", example = "true")
        private Boolean needReturn;
        @Schema(description = "备注")
        private String remark;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/permission/AfterSalePermissionDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.permission;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
 * å”®åŽæ¨¡å—数据权限 DO
 */
@TableName("after_sale_permission")
@KeySequence("after_sale_permission_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSalePermissionDO extends BaseDO {
    /**
     * ç¼–号,主键自增
     */
    @TableId
    private Long id;
    /**
     * æ•°æ®ç±»åž‹
     *
     * æžšä¸¾ {@link AfterSaleBizTypeEnum}
     */
    private Integer bizType;
    /**
     * æ•°æ®ç¼–号
     *
     * å…³è” {@link AfterSaleBizTypeEnum} å¯¹åº”模块 DO çš„ id å­—段
     */
    private Long bizId;
    /**
     * ç”¨æˆ·ç¼–号
     */
    private Long userId;
    /**
     * æƒé™çº§åˆ«
     *
     * æžšä¸¾ {@link AfterSalePermissionLevelEnum}
     */
    private Integer level;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/repair/AfterSaleRepairDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.repair;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleRepairStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å”®åŽç»´ä¿®è®°å½• DO
 */
@TableName("after_sale_repair")
@KeySequence("after_sale_repair_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSaleRepairDO extends BaseDO {
    /**
     * ç»´ä¿®è®°å½•编号
     */
    @TableId
    private Long id;
    /**
     * å…³è”工单编号,关联 after_sale_ticket.id
     */
    private Long ticketId;
    /**
     * ç»´ä¿®ç±»åž‹: 1-现场维修 2-返厂维修 3-远程指导
     */
    private Integer repairType;
    /**
     * æ•…障描述
     */
    private String faultDescription;
    /**
     * ç»´ä¿®æŽªæ–½
     */
    private String repairAction;
    /**
     * ç»´ä¿®ç»“æžœ: å·²ä¿®å¤/无法修复/部分修复
     */
    private String repairResult;
    /**
     * ç»´ä¿®è´¹ç”¨
     */
    private BigDecimal repairCost;
    /**
     * ç»´ä¿®äºº
     */
    private Long repairUserId;
    /**
     * ç»´ä¿®çŠ¶æ€
     *
     * æžšä¸¾ {@link AfterSaleRepairStatusEnum}
     */
    private Integer status;
    /**
     * å¼€å§‹ç»´ä¿®æ—¶é—´
     */
    private LocalDateTime startTime;
    /**
     * å®Œæˆç»´ä¿®æ—¶é—´
     */
    private LocalDateTime finishTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj;
// æ³¨ï¼špackage åä½¿ç”¨ returnobj é¿å…ä¸Ž Java å…³é”®å­— return å†²çª
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleReturnStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å”®åŽé€€è´§ç”³è¯· DO
 */
@TableName("after_sale_return")
@KeySequence("after_sale_return_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSaleReturnDO extends BaseDO {
    /**
     * é€€è´§ç”³è¯·ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é€€è´§å•号
     */
    private String no;
    /**
     * å…³è”工单编号,关联 after_sale_ticket.id
     */
    private Long ticketId;
    /**
     * å®¢æˆ·ç¼–号
     */
    private Long customerId;
    /**
     * å…³è”销售订单编号
     */
    private Long saleOrderId;
    /**
     * é€€è´§ç±»åž‹: 1-退款退货 2-换货 3-仅退款
     */
    private Integer returnType;
    /**
     * é€€è´§åŽŸå› 
     */
    private String returnReason;
    /**
     * ç¼ºé™·åˆ†ç±»
     */
    private String defectCategory;
    /**
     * åº”退总金额
     */
    private BigDecimal totalReturnPrice;
    /**
     * å®žé€€é‡‘额
     */
    private BigDecimal actualRefundPrice;
    /**
     * é€€è´§çŠ¶æ€
     *
     * æžšä¸¾ {@link AfterSaleReturnStatusEnum}
     */
    private Integer status;
    /**
     * å®¡æ ¸äºº
     */
    private Long auditUserId;
    /**
     * å®¡æ ¸æ—¶é—´
     */
    private LocalDateTime auditTime;
    /**
     * MES é€€è´§å•编号,关联 mes_wm_return_sales.id
     */
    private Long mesReturnId;
    /**
     * MES é€€è´§å•号(冗余)
     */
    private String mesReturnCode;
    /**
     * è´¨æ£€ç»“论: åˆæ ¼/特采/退货/报废
     */
    private String qualityResult;
    /**
     * è´¨æ£€æŠ¥å‘Šé™„ä»¶URL
     */
    private String qualityReportUrl;
    /**
     * é€€è´§å…¥åº“完成时间
     */
    private LocalDateTime returnInboundTime;
    /**
     * é€€æ¬¾/换货完成时间
     */
    private LocalDateTime refundTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnItemDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj;
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.*;
import java.math.BigDecimal;
/**
 * å”®åŽé€€è´§æ˜Žç»†è¡Œ DO
 */
@TableName("after_sale_return_item")
@KeySequence("after_sale_return_item_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSaleReturnItemDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é€€è´§ç”³è¯·ç¼–号,关联 after_sale_return.id
     */
    private Long returnId;
    /**
     * å·¥å•明细行编号
     */
    private Long ticketItemId;
    /**
     * MDM ç‰©æ–™ç¼–号
     */
    private Long itemId;
    /**
     * ç‰©æ–™ç¼–码(冗余)
     */
    private String itemCode;
    /**
     * ç‰©æ–™åç§°(冗余)
     */
    private String itemName;
    /**
     * æ‰¹æ¬¡ç¼–号
     */
    private Long batchId;
    /**
     * æ‰¹æ¬¡å·
     */
    private String batchCode;
    /**
     * é€€è´§æ•°é‡
     */
    private BigDecimal returnQuantity;
    /**
     * é”€å”®å•ä»·
     */
    private BigDecimal unitPrice;
    /**
     * é€€è´§é‡‘额
     */
    private BigDecimal returnPrice;
    /**
     * MES é€€è´§å•行编号
     */
    private Long mesReturnLineId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleIssueTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * å”®åŽå·¥å• DO
 */
@TableName("after_sale_ticket")
@KeySequence("after_sale_ticket_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSaleTicketDO extends BaseDO {
    /**
     * å·¥å•编号
     */
    @TableId
    private Long id;
    /**
     * å·¥å•单号
     */
    private String no;
    /**
     * å·¥å•主题/标题
     */
    private String name;
    /**
     * å®¢æˆ·ç¼–号,关联 crm_customer.id
     */
    private Long customerId;
    /**
     * è”系人编号,关联 crm_contact.id
     */
    private Long contactId;
    /**
     * é”€å”®è®¢å•编号,关联 erp_sale_order.id
     */
    private Long saleOrderId;
    /**
     * é”€å”®è®¢å•号(冗余)
     */
    private String saleOrderNo;
    /**
     * åˆåŒç¼–号,关联 crm_contract.id
     */
    private Long contractId;
    /**
     * é—®é¢˜ç±»åž‹
     *
     * æžšä¸¾ {@link AfterSaleIssueTypeEnum}
     */
    private Integer issueType;
    /**
     * é—®é¢˜åˆ†ç±»: å¤–观缺陷/功能故障/尺寸偏差/包装破损/其他
     */
    private String issueCategory;
    /**
     * é—®é¢˜æè¿°
     */
    private String issueDescription;
    /**
     * ä¸¥é‡ç¨‹åº¦: 0-轻微 1-一般 2-严重 3-致命
     */
    private Integer severityLevel;
    /**
     * å·¥å•状态
     *
     * æžšä¸¾ {@link AfterSaleTicketStatusEnum}
     */
    private Integer status;
    /**
     * è´Ÿè´£äººç”¨æˆ·ç¼–号
     */
    private Long ownerUserId;
    /**
     * å¤„理人用户编号
     */
    private Long handlerUserId;
    /**
     * BPM å·¥ä½œæµå®žä¾‹ç¼–号
     */
    private String processInstanceId;
    /**
     * å…³è”退货申请编号,关联 after_sale_return.id
     */
    private Long returnId;
    /**
     * å…³è”维修记录编号,关联 after_sale_repair.id
     */
    private Long repairId;
    /**
     * è§£å†³æ—¶é—´
     */
    private LocalDateTime resolvedTime;
    /**
     * å…³é—­æ—¶é—´
     */
    private LocalDateTime closedTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketItemDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket;
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.*;
import java.math.BigDecimal;
/**
 * å”®åŽå·¥å•明细行 DO
 */
@TableName("after_sale_ticket_item")
@KeySequence("after_sale_ticket_item_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSaleTicketItemDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å·¥å•编号,关联 after_sale_ticket.id
     */
    private Long ticketId;
    /**
     * é”€å”®è®¢å•行编号
     */
    private Long saleOrderItemId;
    /**
     * MDM ç‰©æ–™ç¼–号
     */
    private Long itemId;
    /**
     * ç‰©æ–™ç¼–码(冗余)
     */
    private String itemCode;
    /**
     * ç‰©æ–™åç§°(冗余)
     */
    private String itemName;
    /**
     * ç”Ÿäº§æ‰¹æ¬¡ç¼–号
     */
    private Long batchId;
    /**
     * ç”Ÿäº§æ‰¹æ¬¡å·
     */
    private String batchCode;
    /**
     * é”€å”®æ•°é‡
     */
    private BigDecimal saleQuantity;
    /**
     * é€€è´§æ•°é‡
     */
    private BigDecimal returnQuantity;
    /**
     * æ˜¯å¦éœ€è¦ç»´ä¿®
     */
    private Boolean needRepair;
    /**
     * æ˜¯å¦éœ€è¦é€€è´§
     */
    private Boolean needReturn;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/permission/AfterSalePermissionMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.permission;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * å”®åŽæ¨¡å—数据权限 Mapper
 */
@Mapper
public interface AfterSalePermissionMapper extends BaseMapperX<AfterSalePermissionDO> {
    default List<AfterSalePermissionDO> selectListByBiz(Integer bizType, Long bizId) {
        return selectList(new LambdaQueryWrapperX<AfterSalePermissionDO>()
                .eq(AfterSalePermissionDO::getBizType, bizType)
                .eq(AfterSalePermissionDO::getBizId, bizId));
    }
    default List<AfterSalePermissionDO> selectListByBiz(Integer bizType, Collection<Long> bizIds) {
        return selectList(new LambdaQueryWrapperX<AfterSalePermissionDO>()
                .eq(AfterSalePermissionDO::getBizType, bizType)
                .in(AfterSalePermissionDO::getBizId, bizIds));
    }
    default List<AfterSalePermissionDO> selectListByBizTypeAndUserId(Integer bizType, Long userId) {
        return selectList(new LambdaQueryWrapperX<AfterSalePermissionDO>()
                .eq(AfterSalePermissionDO::getBizType, bizType)
                .eq(AfterSalePermissionDO::getUserId, userId));
    }
    default AfterSalePermissionDO selectByBizAndUser(Integer bizType, Long bizId, Long userId) {
        return selectOne(new LambdaQueryWrapperX<AfterSalePermissionDO>()
                .eq(AfterSalePermissionDO::getBizType, bizType)
                .eq(AfterSalePermissionDO::getBizId, bizId)
                .eq(AfterSalePermissionDO::getUserId, userId));
    }
    default void deleteByBiz(Integer bizType, Long bizId) {
        delete(new LambdaQueryWrapperX<AfterSalePermissionDO>()
                .eq(AfterSalePermissionDO::getBizType, bizType)
                .eq(AfterSalePermissionDO::getBizId, bizId));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/repair/AfterSaleRepairMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.repair;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.repair.AfterSaleRepairDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å”®åŽç»´ä¿®è®°å½• Mapper
 */
@Mapper
public interface AfterSaleRepairMapper extends BaseMapperX<AfterSaleRepairDO> {
    default AfterSaleRepairDO selectByTicketId(Long ticketId) {
        return selectOne(AfterSaleRepairDO::getTicketId, ticketId);
    }
    default List<AfterSaleRepairDO> selectListByTicketId(Long ticketId) {
        return selectList(new LambdaQueryWrapperX<AfterSaleRepairDO>()
                .eq(AfterSaleRepairDO::getTicketId, ticketId));
    }
    default PageResult<AfterSaleRepairDO> selectPage(PageParam pageParam, Long ticketId, Integer status) {
        return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleRepairDO>()
                .eqIfPresent(AfterSaleRepairDO::getTicketId, ticketId)
                .eqIfPresent(AfterSaleRepairDO::getStatus, status)
                .orderByDesc(AfterSaleRepairDO::getId));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnItemMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å”®åŽé€€è´§æ˜Žç»†è¡Œ Mapper
 */
@Mapper
public interface AfterSaleReturnItemMapper extends BaseMapperX<AfterSaleReturnItemDO> {
    default List<AfterSaleReturnItemDO> selectListByReturnId(Long returnId) {
        return selectList(new LambdaQueryWrapperX<AfterSaleReturnItemDO>()
                .eq(AfterSaleReturnItemDO::getReturnId, returnId));
    }
    default void deleteByReturnId(Long returnId) {
        delete(new LambdaQueryWrapperX<AfterSaleReturnItemDO>()
                .eq(AfterSaleReturnItemDO::getReturnId, returnId));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.util.AfterSalePermissionUtils;
import org.apache.ibatis.annotations.Mapper;
/**
 * å”®åŽé€€è´§ç”³è¯· Mapper
 */
@Mapper
public interface AfterSaleReturnMapper extends BaseMapperX<AfterSaleReturnDO> {
    default AfterSaleReturnDO selectByNo(String no) {
        return selectOne(AfterSaleReturnDO::getNo, no);
    }
    default AfterSaleReturnDO selectByTicketId(Long ticketId) {
        return selectOne(AfterSaleReturnDO::getTicketId, ticketId);
    }
    default PageResult<AfterSaleReturnDO> selectPage(PageParam pageParam, Long userId, Integer sceneType,
                                                     String no, Long ticketId,
                                                     Long customerId, Integer status) {
        MPJLambdaWrapperX<AfterSaleReturnDO> query = new MPJLambdaWrapperX<>();
        // æ•°æ®æƒé™
        AfterSalePermissionUtils.appendPermissionCondition(query, AfterSaleBizTypeEnum.AFTER_SALE_RETURN.getType(),
                AfterSaleReturnDO::getId, userId, sceneType);
        // æŸ¥è¯¢æ¡ä»¶
        query.selectAll(AfterSaleReturnDO.class)
                .likeIfPresent(AfterSaleReturnDO::getNo, no)
                .eqIfPresent(AfterSaleReturnDO::getTicketId, ticketId)
                .eqIfPresent(AfterSaleReturnDO::getCustomerId, customerId)
                .eqIfPresent(AfterSaleReturnDO::getStatus, status)
                .orderByDesc(AfterSaleReturnDO::getId);
        return selectJoinPage(pageParam, AfterSaleReturnDO.class, query);
    }
    default AfterSaleReturnDO selectByMesReturnId(Long mesReturnId) {
        return selectOne(AfterSaleReturnDO::getMesReturnId, mesReturnId);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketItemMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.ticket;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å”®åŽå·¥å•明细行 Mapper
 */
@Mapper
public interface AfterSaleTicketItemMapper extends BaseMapperX<AfterSaleTicketItemDO> {
    default List<AfterSaleTicketItemDO> selectListByTicketId(Long ticketId) {
        return selectList(new LambdaQueryWrapperX<AfterSaleTicketItemDO>()
                .eq(AfterSaleTicketItemDO::getTicketId, ticketId));
    }
    default void deleteByTicketId(Long ticketId) {
        delete(new LambdaQueryWrapperX<AfterSaleTicketItemDO>()
                .eq(AfterSaleTicketItemDO::getTicketId, ticketId));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.ticket;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.util.AfterSalePermissionUtils;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å”®åŽå·¥å• Mapper
 */
@Mapper
public interface AfterSaleTicketMapper extends BaseMapperX<AfterSaleTicketDO> {
    default AfterSaleTicketDO selectByNo(String no) {
        return selectOne(AfterSaleTicketDO::getNo, no);
    }
    default PageResult<AfterSaleTicketDO> selectPage(PageParam pageParam, Long userId, Integer sceneType,
                                                     String no, String name,
                                                     Long customerId, Long saleOrderId,
                                                     Integer status, Integer issueType) {
        MPJLambdaWrapperX<AfterSaleTicketDO> query = new MPJLambdaWrapperX<>();
        // æ•°æ®æƒé™
        AfterSalePermissionUtils.appendPermissionCondition(query, AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(),
                AfterSaleTicketDO::getId, userId, sceneType);
        // æŸ¥è¯¢æ¡ä»¶
        query.selectAll(AfterSaleTicketDO.class)
                .likeIfPresent(AfterSaleTicketDO::getNo, no)
                .likeIfPresent(AfterSaleTicketDO::getName, name)
                .eqIfPresent(AfterSaleTicketDO::getCustomerId, customerId)
                .eqIfPresent(AfterSaleTicketDO::getSaleOrderId, saleOrderId)
                .eqIfPresent(AfterSaleTicketDO::getStatus, status)
                .eqIfPresent(AfterSaleTicketDO::getIssueType, issueType)
                .orderByDesc(AfterSaleTicketDO::getId);
        return selectJoinPage(pageParam, AfterSaleTicketDO.class, query);
    }
    default List<AfterSaleTicketDO> selectListByCustomerId(Long customerId) {
        return selectList(new LambdaQueryWrapperX<AfterSaleTicketDO>()
                .eq(AfterSaleTicketDO::getCustomerId, customerId));
    }
    default AfterSaleTicketDO selectBySaleOrderId(Long saleOrderId) {
        return selectOne(AfterSaleTicketDO::getSaleOrderId, saleOrderId);
    }
    default Long selectCountByOwnerUserId(Long ownerUserId) {
        return selectCount(AfterSaleTicketDO::getOwnerUserId, ownerUserId);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/RedisKeyConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.aftersales.dal.redis;
/**
 * å”®åŽæ¨¡å— Redis Key å¸¸é‡
 */
public interface RedisKeyConstants {
    /**
     * åºå·çš„缓存
     *
     * KEY æ ¼å¼ï¼šaftersales:seq_no:{prefix}
     * VALUE æ•°æ®æ ¼å¼ï¼šç¼–号自增
     */
    String NO = "aftersales:seq_no:";
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.aftersales.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.aftersales.dal.redis.RedisKeyConstants;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
 * å”®åŽæ¨¡å—单号 Redis DAO
 */
@Repository
public class AfterSaleNoRedisDAO {
    /**
     * å”®åŽå·¥å• {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO}
     */
    public static final String TICKET_NO_PREFIX = "AS";
    /**
     * é€€è´§ç”³è¯· {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO}
     */
    public static final String RETURN_NO_PREFIX = "RT";
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMMdd + 6 ä½è‡ªå¢ž
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     */
    public String generate(String prefix) {
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%06d", no);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleBizTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * å”®åŽä¸šåŠ¡ç±»åž‹æžšä¸¾
 */
@RequiredArgsConstructor
@Getter
public enum AfterSaleBizTypeEnum implements ArrayValuable<Integer> {
    AFTER_SALE_TICKET(1, "售后工单"),
    AFTER_SALE_RETURN(2, "退货申请"),
    AFTER_SALE_REPAIR(3, "维修记录"),
    ;
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(AfterSaleBizTypeEnum::getType).toArray(Integer[]::new);
    private final Integer type;
    private final String name;
    public static String getNameByType(Integer type) {
        AfterSaleBizTypeEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(AfterSaleBizTypeEnum.values()),
                item -> ObjUtil.equal(item.type, type));
        return typeEnum == null ? null : typeEnum.getName();
    }
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleIssueTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * å”®åŽé—®é¢˜ç±»åž‹æžšä¸¾
 */
@RequiredArgsConstructor
@Getter
public enum AfterSaleIssueTypeEnum implements ArrayValuable<Integer> {
    UNCLASSIFIED(0, "未判定"),
    GENERAL(1, "一般问题"),
    REPAIR(2, "维修问题"),
    RETURN(3, "退货问题");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(AfterSaleIssueTypeEnum::getType).toArray(Integer[]::new);
    private final Integer type;
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRepairStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * å”®åŽç»´ä¿®çŠ¶æ€æžšä¸¾
 */
@RequiredArgsConstructor
@Getter
public enum AfterSaleRepairStatusEnum implements ArrayValuable<Integer> {
    PENDING(0, "待处理"),
    IN_PROGRESS(10, "维修中"),
    COMPLETED(20, "已完成"),
    CLOSED(30, "已关闭");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(AfterSaleRepairStatusEnum::getStatus).toArray(Integer[]::new);
    private final Integer status;
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleReturnStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * å”®åŽé€€è´§çŠ¶æ€æžšä¸¾
 */
@RequiredArgsConstructor
@Getter
public enum AfterSaleReturnStatusEnum implements ArrayValuable<Integer> {
    DRAFT(0, "草稿"),
    AUDITING(10, "销售审核中"),
    APPROVED(20, "审核通过-已同步MES"),
    INSPECTING(30, "检验中"),
    INSPECTED(40, "检验完成"),
    INBOUND(50, "已入库"),
    REFUNDED(60, "退款/换货完成"),
    CLOSED(70, "已关闭"),
    CANCELED(99, "已取消");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(AfterSaleReturnStatusEnum::getStatus).toArray(Integer[]::new);
    private final Integer status;
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * å”®åŽå·¥å•状态枚举
 */
@RequiredArgsConstructor
@Getter
public enum AfterSaleTicketStatusEnum implements ArrayValuable<Integer> {
    DRAFT(0, "草稿"),
    PROCESSING(10, "处理中"),
    WAITING_RETURN(30, "待退货"),
    WAITING_REFUND(40, "待退款/换货"),
    COMPLETED(50, "已完结"),
    CLOSED(60, "已关闭"),
    CANCELED(99, "已取消");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(AfterSaleTicketStatusEnum::getStatus).toArray(Integer[]::new);
    private final Integer status;
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
 * å”®åŽæ¨¡å—错误码枚举类
 * <p>
 * aftersales ç³»ç»Ÿï¼Œä½¿ç”¨ 1-022-000-000 æ®µ
 */
public interface ErrorCodeConstants {
    // ========== å”®åŽå·¥å• 1-022-000-000 ==========
    ErrorCode TICKET_NOT_EXISTS = new ErrorCode(1_022_000_000, "售后工单不存在");
    ErrorCode TICKET_NO_EXISTS = new ErrorCode(1_022_000_001, "生成工单号重复,请重试");
    ErrorCode TICKET_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_022_000_002, "工单不是草稿状态,无法删除");
    ErrorCode TICKET_UPDATE_FAIL_STATUS = new ErrorCode(1_022_000_003, "更新工单状态失败,原因:已经是该状态");
    ErrorCode TICKET_RESOLVE_FAIL = new ErrorCode(1_022_000_004, "完结工单失败,原因:工单不是处理中状态");
    ErrorCode TICKET_CLOSE_FAIL = new ErrorCode(1_022_000_005, "关闭工单失败,原因:工单未完结");
    ErrorCode TICKET_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_022_000_006, "提交工单审批失败,原因:工单不是草稿状态");
    ErrorCode TICKET_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_022_000_007, "售后工单审批流程不存在或未激活,请检查 BPM é…ç½®");
    ErrorCode TICKET_STATUS_ERROR = new ErrorCode(1_022_000_008, "工单当前状态不允许此操作");
    // ========== å”®åŽå·¥å•明细行 1-022-001-000 ==========
    ErrorCode TICKET_ITEM_NOT_EXISTS = new ErrorCode(1_022_001_000, "工单明细行不存在");
    ErrorCode TICKET_ITEM_EMPTY = new ErrorCode(1_022_001_001, "工单明细行不能为空");
    // ========== é€€è´§ç”³è¯· 1-022-002-000 ==========
    ErrorCode RETURN_NOT_EXISTS = new ErrorCode(1_022_002_000, "退货申请不存在");
    ErrorCode RETURN_NO_EXISTS = new ErrorCode(1_022_002_001, "生成退货单号重复,请重试");
    ErrorCode RETURN_APPROVE_FAIL = new ErrorCode(1_022_002_002, "审核退货申请失败,原因:不是待审核状态");
    ErrorCode RETURN_ITEM_EMPTY = new ErrorCode(1_022_002_003, "退货明细行不能为空");
    ErrorCode RETURN_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_022_002_004, "退货申请不是草稿状态,无法删除");
    // ========== ç»´ä¿®è®°å½• 1-022-003-000 ==========
    ErrorCode REPAIR_NOT_EXISTS = new ErrorCode(1_022_003_000, "维修记录不存在");
    ErrorCode REPAIR_UPDATE_FAIL_STATUS = new ErrorCode(1_022_003_001, "更新维修状态失败,原因:已经是该状态");
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.aftersales.enums;
/**
 * å”®åŽæ¨¡å—操作日志常量
 */
public interface LogRecordConstants {
    // ======================= å”®åŽå·¥å• =======================
    String AFTER_SALE_TICKET_TYPE = "售后工单";
    String AFTER_SALE_TICKET_CREATE_SUB_TYPE = "创建售后工单";
    String AFTER_SALE_TICKET_CREATE_SUCCESS = "创建了售后工单【{{#ticket.name}}】";
    String AFTER_SALE_TICKET_UPDATE_SUB_TYPE = "更新售后工单";
    String AFTER_SALE_TICKET_UPDATE_SUCCESS = "更新了售后工单【{{#ticketName}}】: {_DIFF{#updateReqVO}}";
    String AFTER_SALE_TICKET_DELETE_SUB_TYPE = "删除售后工单";
    String AFTER_SALE_TICKET_DELETE_SUCCESS = "删除了售后工单【{{#ticketName}}】";
    String AFTER_SALE_TICKET_ASSIGN_SUB_TYPE = "分配处理人";
    String AFTER_SALE_TICKET_ASSIGN_SUCCESS = "将售后工单【{{#ticketName}}】分配给【{getAdminUserById{#handlerUserId}}】";
    String AFTER_SALE_TICKET_CLASSIFY_SUB_TYPE = "问题判定";
    String AFTER_SALE_TICKET_CLASSIFY_SUCCESS = "对售后工单【{{#ticketName}}】进行了问题判定:【{{#issueTypeName}}】";
    String AFTER_SALE_TICKET_RESOLVE_SUB_TYPE = "完结工单";
    String AFTER_SALE_TICKET_RESOLVE_SUCCESS = "完结了售后工单【{{#ticketName}}】";
    String AFTER_SALE_TICKET_CLOSE_SUB_TYPE = "关闭工单";
    String AFTER_SALE_TICKET_CLOSE_SUCCESS = "关闭了售后工单【{{#ticketName}}】";
    String AFTER_SALE_TICKET_CANCEL_SUB_TYPE = "取消工单";
    String AFTER_SALE_TICKET_CANCEL_SUCCESS = "取消了售后工单【{{#ticketName}}】";
    String AFTER_SALE_TICKET_SUBMIT_SUB_TYPE = "提交工单审批";
    String AFTER_SALE_TICKET_SUBMIT_SUCCESS = "提交售后工单【{{#ticketName}}】审批成功";
    // ======================= é€€è´§ç”³è¯· =======================
    String AFTER_SALE_RETURN_TYPE = "售后退货";
    String AFTER_SALE_RETURN_CREATE_SUB_TYPE = "创建退货申请";
    String AFTER_SALE_RETURN_CREATE_SUCCESS = "创建了退货申请【{{#returnDO.no}}】";
    String AFTER_SALE_RETURN_APPROVE_SUB_TYPE = "审核退货申请";
    String AFTER_SALE_RETURN_APPROVE_SUCCESS = "审核通过了退货申请【{{#returnNo}}】,已同步到MES退货";
    String AFTER_SALE_RETURN_DELETE_SUB_TYPE = "删除退货申请";
    String AFTER_SALE_RETURN_DELETE_SUCCESS = "删除了退货申请【{{#returnNo}}】";
    // ======================= ç»´ä¿®è®°å½• =======================
    String AFTER_SALE_REPAIR_TYPE = "售后维修";
    String AFTER_SALE_REPAIR_CREATE_SUB_TYPE = "创建维修记录";
    String AFTER_SALE_REPAIR_CREATE_SUCCESS = "创建了维修记录【{{#repair.id}}】";
    String AFTER_SALE_REPAIR_COMPLETE_SUB_TYPE = "完成维修";
    String AFTER_SALE_REPAIR_COMPLETE_SUCCESS = "完成了维修记录【{{#repairId}}】,维修结果:【{{#repairResult}}】";
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSalePermissionLevelEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.aftersales.enums.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * å”®åŽæ¨¡å—数据权限级别枚举
 *
 * OWNER > WRITE > READ
 */
@Getter
@AllArgsConstructor
public enum AfterSalePermissionLevelEnum implements ArrayValuable<Integer> {
    OWNER(1, "负责人"),
    READ(2, "只读"),
    WRITE(3, "读写");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSalePermissionLevelEnum::getLevel).toArray(Integer[]::new);
    /**
     * çº§åˆ«
     */
    private final Integer level;
    /**
     * çº§åˆ«åç§°
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public static boolean isOwner(Integer level) {
        return ObjUtil.equal(OWNER.level, level);
    }
    public static boolean isRead(Integer level) {
        return ObjUtil.equal(READ.level, level);
    }
    public static boolean isWrite(Integer level) {
        return ObjUtil.equal(WRITE.level, level);
    }
    public static String getNameByLevel(Integer level) {
        AfterSalePermissionLevelEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(AfterSalePermissionLevelEnum.values()),
                item -> ObjUtil.equal(item.level, level));
        return typeEnum == null ? null : typeEnum.getName();
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSaleSceneTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.aftersales.enums.permission;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * å”®åŽæ¨¡å—列表检索场景
 */
@Getter
@AllArgsConstructor
public enum AfterSaleSceneTypeEnum implements ArrayValuable<Integer> {
    OWNER(1, "我负责的"),
    INVOLVED(2, "我参与的"),
    SUBORDINATE(3, "下属负责的"),
    ALL(4, "全部");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleSceneTypeEnum::getType).toArray(Integer[]::new);
    /**
     * åœºæ™¯ç±»åž‹
     */
    private final Integer type;
    /**
     * åœºæ™¯åç§°
     */
    private final String name;
    public static boolean isOwner(Integer type) {
        return ObjUtil.equal(OWNER.getType(), type);
    }
    public static boolean isInvolved(Integer type) {
        return ObjUtil.equal(INVOLVED.getType(), type);
    }
    public static boolean isSubordinate(Integer type) {
        return ObjUtil.equal(SUBORDINATE.getType(), type);
    }
    public static boolean isAll(Integer type) {
        return ObjUtil.equal(ALL.getType(), type);
    }
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.aftersales.service.permission;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import java.util.Collection;
import java.util.List;
/**
 * å”®åŽæ¨¡å—数据权限 Service æŽ¥å£
 */
public interface AfterSalePermissionService {
    /**
     * åˆ›å»ºæ•°æ®æƒé™ï¼ˆè´Ÿè´£äººæƒé™ï¼‰
     *
     * @param createReqBO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createPermission(AfterSalePermissionCreateReqBO createReqBO);
    /**
     * åˆ é™¤æ•°æ®æƒé™
     *
     * @param bizType æ•°æ®ç±»åž‹ï¼Œå…³è” {@link AfterSaleBizTypeEnum}
     * @param bizId   æ•°æ®ç¼–号
     */
    void deletePermission(Integer bizType, Long bizId);
    /**
     * èŽ·å–æ•°æ®æƒé™åˆ—è¡¨
     *
     * @param bizType æ•°æ®ç±»åž‹
     * @param bizId   æ•°æ®ç¼–号
     * @return æƒé™åˆ—表
     */
    List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Long bizId);
    /**
     * èŽ·å–æ•°æ®æƒé™åˆ—è¡¨ï¼ˆæ‰¹é‡ï¼‰
     *
     * @param bizType æ•°æ®ç±»åž‹
     * @param bizIds  æ•°æ®ç¼–号列表
     * @return æƒé™åˆ—表
     */
    List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Collection<Long> bizIds);
    /**
     * æ ¡éªŒæ˜¯å¦æœ‰æŒ‡å®šæ•°æ®çš„æ“ä½œæƒé™
     *
     * @param bizType æ•°æ®ç±»åž‹
     * @param bizId   æ•°æ®ç¼–号
     * @param userId  ç”¨æˆ·ç¼–号
     * @param level   æƒé™çº§åˆ«
     * @return æ˜¯å¦æœ‰æƒé™
     */
    boolean hasPermission(Integer bizType, Long bizId, Long userId, AfterSalePermissionLevelEnum level);
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.aftersales.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.permission.AfterSalePermissionMapper;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
 * å”®åŽæ¨¡å—数据权限 Service å®žçŽ°ç±»
 */
@Service
@Validated
public class AfterSalePermissionServiceImpl implements AfterSalePermissionService {
    @Resource
    private AfterSalePermissionMapper afterSalePermissionMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createPermission(AfterSalePermissionCreateReqBO createReqBO) {
        AfterSalePermissionDO permission = BeanUtils.toBean(createReqBO, AfterSalePermissionDO.class);
        afterSalePermissionMapper.insert(permission);
        return permission.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deletePermission(Integer bizType, Long bizId) {
        afterSalePermissionMapper.deleteByBiz(bizType, bizId);
    }
    @Override
    public List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Long bizId) {
        return afterSalePermissionMapper.selectListByBiz(bizType, bizId);
    }
    @Override
    public List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Collection<Long> bizIds) {
        if (CollUtil.isEmpty(bizIds)) {
            return Collections.emptyList();
        }
        return afterSalePermissionMapper.selectListByBiz(bizType, bizIds);
    }
    @Override
    public boolean hasPermission(Integer bizType, Long bizId, Long userId, AfterSalePermissionLevelEnum level) {
        List<AfterSalePermissionDO> permissionList = getPermissionListByBiz(bizType, bizId);
        return permissionList.stream()
                .anyMatch(p -> ObjUtil.equal(p.getUserId(), userId)
                        && ObjUtil.equal(p.getLevel(), level.getLevel()));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/bo/AfterSalePermissionCreateReqBO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.aftersales.service.permission.bo;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
 * å”®åŽæ¨¡å—数据权限创建 Request BO
 */
@Data
public class AfterSalePermissionCreateReqBO {
    /**
     * æ•°æ®ç±»åž‹ï¼Œå…³è” {@link AfterSaleBizTypeEnum}
     */
    @NotNull(message = "数据类型不能为空")
    private Integer bizType;
    /**
     * æ•°æ®ç¼–号
     */
    @NotNull(message = "数据编号不能为空")
    private Long bizId;
    /**
     * ç”¨æˆ·ç¼–号
     */
    @NotNull(message = "用户编号不能为空")
    private Long userId;
    /**
     * æƒé™çº§åˆ«ï¼Œå…³è” {@link AfterSalePermissionLevelEnum}
     */
    @NotNull(message = "权限级别不能为空")
    private Integer level;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.aftersales.service.repair;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.repair.AfterSaleRepairDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
 * å”®åŽç»´ä¿®è®°å½• Service æŽ¥å£
 */
public interface AfterSaleRepairService {
    /**
     * åˆ›å»ºç»´ä¿®è®°å½•
     *
     * @param repairDO ç»´ä¿®ä¿¡æ¯
     * @return ç¼–号
     */
    Long createRepair(@Valid AfterSaleRepairDO repairDO);
    /**
     * æ›´æ–°ç»´ä¿®è®°å½•
     *
     * @param repairDO æ›´æ–°ä¿¡æ¯
     */
    void updateRepair(@Valid AfterSaleRepairDO repairDO);
    /**
     * å®Œæˆç»´ä¿®
     *
     * @param id ç»´ä¿®è®°å½•编号
     */
    void completeRepair(Long id);
    /**
     * èŽ·å¾—ç»´ä¿®è®°å½•
     *
     * @param id ç¼–号
     * @return ç»´ä¿®è®°å½•
     */
    AfterSaleRepairDO getRepair(Long id);
    /**
     * èŽ·å¾—ç»´ä¿®è®°å½•åˆ—è¡¨
     *
     * @param ids ç¼–号列表
     * @return ç»´ä¿®è®°å½•列表
     */
    List<AfterSaleRepairDO> getRepairList(Collection<Long> ids);
    /**
     * èŽ·å¾—ç»´ä¿®è®°å½•åˆ†é¡µ
     *
     * @param pageParam åˆ†é¡µå‚æ•°
     * @param ticketId  å…³è”工单编号
     * @param status    ç»´ä¿®çŠ¶æ€
     * @return ç»´ä¿®è®°å½•分页
     */
    PageResult<AfterSaleRepairDO> getRepairPage(Object pageParam, Long ticketId, Integer status);
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.aftersales.service.repair;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.repair.AfterSaleRepairDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.repair.AfterSaleRepairMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleRepairStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
/**
 * å”®åŽç»´ä¿®è®°å½• Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class AfterSaleRepairServiceImpl implements AfterSaleRepairService {
    @Resource
    private AfterSaleRepairMapper repairMapper;
    @Resource
    private AfterSaleTicketMapper ticketMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createRepair(AfterSaleRepairDO repairDO) {
        // æ ¡éªŒå·¥å•存在
        AfterSaleTicketDO ticket = ticketMapper.selectById(repairDO.getTicketId());
        if (ticket == null) {
            throw exception(TICKET_NOT_EXISTS);
        }
        repairDO.setStatus(AfterSaleRepairStatusEnum.PENDING.getStatus());
        repairMapper.insert(repairDO);
        // æ›´æ–°å·¥å•关联维修记录
        ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
                .setRepairId(repairDO.getId()));
        return repairDO.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateRepair(AfterSaleRepairDO repairDO) {
        validateRepairExists(repairDO.getId());
        repairMapper.updateById(repairDO);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void completeRepair(Long id) {
        AfterSaleRepairDO repair = validateRepairExists(id);
        repairMapper.updateById(new AfterSaleRepairDO().setId(id)
                .setStatus(AfterSaleRepairStatusEnum.COMPLETED.getStatus())
                .setFinishTime(LocalDateTime.now()));
        // æ›´æ–°å·¥å•状态
        AfterSaleTicketDO ticket = ticketMapper.selectById(repair.getTicketId());
        if (ticket != null) {
            ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
                    .setStatus(cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum.COMPLETED.getStatus())
                    .setResolvedTime(LocalDateTime.now()));
        }
    }
    @Override
    public AfterSaleRepairDO getRepair(Long id) {
        return validateRepairExists(id);
    }
    @Override
    public List<AfterSaleRepairDO> getRepairList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return List.of();
        }
        return repairMapper.selectBatchIds(ids);
    }
    @Override
    public PageResult<AfterSaleRepairDO> getRepairPage(Object pageParam, Long ticketId, Integer status) {
        if (pageParam instanceof PageParam) {
            return repairMapper.selectPage((PageParam) pageParam, ticketId, status);
        }
        return repairMapper.selectPage(new PageParam(), ticketId, status);
    }
    // ========== å†…部方法 ==========
    private AfterSaleRepairDO validateRepairExists(Long id) {
        AfterSaleRepairDO repair = repairMapper.selectById(id);
        if (repair == null) {
            throw exception(REPAIR_NOT_EXISTS);
        }
        return repair;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.aftersales.service.returnobj;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
import jakarta.validation.Valid;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
/**
 * å”®åŽé€€è´§ç”³è¯· Service æŽ¥å£
 */
public interface AfterSaleReturnService {
    /**
     * åˆ›å»ºé€€è´§ç”³è¯·
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @param userId      ç”¨æˆ·ç¼–号
     * @return ç¼–号
     */
    Long createReturn(@Valid AfterSaleReturnSaveReqVO createReqVO, Long userId);
    /**
     * å®¡æ ¸é€šè¿‡ï¼ˆè§¦å‘ MES é€€è´§æµç¨‹ï¼‰
     *
     * @param id é€€è´§ç”³è¯·ç¼–号
     */
    void approveReturn(Long id);
    /**
     * èŽ·å¾—é€€è´§ç”³è¯·
     *
     * @param id ç¼–号
     * @return é€€è´§ç”³è¯·
     */
    AfterSaleReturnDO getReturn(Long id);
    /**
     * èŽ·å¾—é€€è´§ç”³è¯·åˆ—è¡¨
     *
     * @param ids ç¼–号列表
     * @return é€€è´§ç”³è¯·åˆ—表
     */
    List<AfterSaleReturnDO> getReturnList(Collection<Long> ids);
    /**
     * èŽ·å¾—é€€è´§ç”³è¯·åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @param userId    ç”¨æˆ·ç¼–号
     * @return é€€è´§ç”³è¯·åˆ†é¡µ
     */
    PageResult<AfterSaleReturnDO> getReturnPage(AfterSaleReturnPageReqVO pageReqVO, Long userId);
    /**
     * èŽ·å¾—é€€è´§æ˜Žç»†è¡Œåˆ—è¡¨
     *
     * @param returnId é€€è´§ç”³è¯·ç¼–号
     * @return æ˜Žç»†è¡Œåˆ—表
     */
    List<AfterSaleReturnItemDO> getReturnItemListByReturnId(Long returnId);
    /**
     * MES è´¨æ£€å®Œæˆå›žè°ƒ
     *
     * @param returnId      é€€è´§ç”³è¯·ç¼–号
     * @param qualityResult è´¨æ£€ç»“论
     * @param reportUrl     è´¨æ£€æŠ¥å‘Š URL
     */
    void onQualityFinished(Long returnId, String qualityResult, String reportUrl);
    /**
     * MES é€€è´§å…¥åº“完成回调
     *
     * @param returnId é€€è´§ç”³è¯·ç¼–号
     */
    void onInboundFinished(Long returnId);
    /**
     * ERP é€€æ¬¾å®Œæˆå›žè°ƒ
     *
     * @param returnId           é€€è´§ç”³è¯·ç¼–号
     * @param actualRefundPrice  å®žé€€é‡‘额
     */
    void onRefundCompleted(Long returnId, BigDecimal actualRefundPrice);
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,221 @@
package cn.iocoder.yudao.module.aftersales.service.returnobj;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnItemMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleReturnStatusEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService;
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import cn.iocoder.yudao.module.mes.api.returnsales.MesWmReturnSalesApi;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
/**
 * å”®åŽé€€è´§ç”³è¯· Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class AfterSaleReturnServiceImpl implements AfterSaleReturnService {
    @Resource
    private AfterSaleReturnMapper returnMapper;
    @Resource
    private AfterSaleReturnItemMapper returnItemMapper;
    @Resource
    private AfterSaleTicketMapper ticketMapper;
    @Resource
    private AfterSaleNoRedisDAO noRedisDAO;
    @Resource
    private AfterSalePermissionService permissionService;
    @Resource
    private MesWmReturnSalesApi mesWmReturnSalesApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createReturn(AfterSaleReturnSaveReqVO createReqVO, Long userId) {
        // 1. æ ¡éªŒå·¥å•存在
        AfterSaleTicketDO ticket = ticketMapper.selectById(createReqVO.getTicketId());
        if (ticket == null) {
            throw exception(TICKET_NOT_EXISTS);
        }
        // 2. ç”Ÿæˆå•号
        String no = noRedisDAO.generate(AfterSaleNoRedisDAO.RETURN_NO_PREFIX);
        if (returnMapper.selectByNo(no) != null) {
            throw exception(RETURN_NO_EXISTS);
        }
        // 3. è®¡ç®—应退总金额
        BigDecimal totalReturnPrice = BigDecimal.ZERO;
        if (CollUtil.isNotEmpty(createReqVO.getItems())) {
            totalReturnPrice = createReqVO.getItems().stream()
                    .map(item -> item.getReturnPrice() != null ? item.getReturnPrice() : BigDecimal.ZERO)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
        }
        // 4. æ’入退货申请
        AfterSaleReturnDO returnDO = BeanUtils.toBean(createReqVO, AfterSaleReturnDO.class)
                .setNo(no)
                .setTotalReturnPrice(totalReturnPrice)
                .setStatus(AfterSaleReturnStatusEnum.DRAFT.getStatus());
        returnMapper.insert(returnDO);
        // 5. æ’入退货明细行
        if (CollUtil.isNotEmpty(createReqVO.getItems())) {
            createReqVO.getItems().forEach(item -> {
                AfterSaleReturnItemDO itemDO = BeanUtils.toBean(item, AfterSaleReturnItemDO.class);
                itemDO.setReturnId(returnDO.getId());
                returnItemMapper.insert(itemDO);
            });
        }
        // 6. åˆ›å»ºæ•°æ®æƒé™
        AfterSalePermissionCreateReqBO permBO = new AfterSalePermissionCreateReqBO();
        permBO.setBizType(AfterSaleBizTypeEnum.AFTER_SALE_RETURN.getType());
        permBO.setBizId(returnDO.getId());
        permBO.setUserId(userId);
        permBO.setLevel(AfterSalePermissionLevelEnum.OWNER.getLevel());
        permissionService.createPermission(permBO);
        // 7. æ›´æ–°å·¥å•状态为待退货
        ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
                .setReturnId(returnDO.getId())
                .setStatus(AfterSaleTicketStatusEnum.WAITING_RETURN.getStatus()));
        return returnDO.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approveReturn(Long id) {
        // 1. æ ¡éªŒé€€è´§ç”³è¯·
        AfterSaleReturnDO returnDO = validateReturnExists(id);
        if (!AfterSaleReturnStatusEnum.DRAFT.getStatus().equals(returnDO.getStatus())) {
            throw exception(RETURN_APPROVE_FAIL);
        }
        // 2. èŽ·å–å…³è”å·¥å•ï¼ˆç”¨äºŽèŽ·å–é”€å”®è®¢å•å·ç­‰å†—ä½™ä¿¡æ¯ï¼‰
        AfterSaleTicketDO ticket = ticketMapper.selectById(returnDO.getTicketId());
        // 3. è°ƒç”¨ MES åˆ›å»ºé”€å”®é€€è´§å•
        MesWmReturnSalesCreateReqDTO mesReqDTO = new MesWmReturnSalesCreateReqDTO();
        mesReqDTO.setCode(returnDO.getNo());
        mesReqDTO.setName("售后退货-" + returnDO.getNo());
        mesReqDTO.setSaleOrderId(returnDO.getSaleOrderId());
        mesReqDTO.setSalesOrderCode(ticket != null ? ticket.getSaleOrderNo() : null);
        mesReqDTO.setClientId(returnDO.getCustomerId());
        mesReqDTO.setReturnDate(LocalDate.now());
        mesReqDTO.setReturnReason(returnDO.getReturnReason());
        mesReqDTO.setRemark("售后工单退货 - å·¥å•号: " + (ticket != null ? ticket.getNo() : ""));
        mesReqDTO.setAfterSaleReturnId(returnDO.getId());
        Long mesReturnId = mesWmReturnSalesApi.createReturnSales(mesReqDTO);
        // 4. æ›´æ–°é€€è´§ç”³è¯·çŠ¶æ€
        returnMapper.updateById(new AfterSaleReturnDO().setId(id)
                .setStatus(AfterSaleReturnStatusEnum.APPROVED.getStatus())
                .setMesReturnId(mesReturnId)
                .setMesReturnCode(returnDO.getNo())
                .setAuditTime(LocalDateTime.now()));
    }
    @Override
    public AfterSaleReturnDO getReturn(Long id) {
        return validateReturnExists(id);
    }
    @Override
    public List<AfterSaleReturnDO> getReturnList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return List.of();
        }
        return returnMapper.selectBatchIds(ids);
    }
    @Override
    public PageResult<AfterSaleReturnDO> getReturnPage(AfterSaleReturnPageReqVO pageReqVO, Long userId) {
        return returnMapper.selectPage(pageReqVO, userId, pageReqVO.getSceneType(),
                pageReqVO.getNo(), pageReqVO.getTicketId(),
                pageReqVO.getCustomerId(), pageReqVO.getStatus());
    }
    @Override
    public List<AfterSaleReturnItemDO> getReturnItemListByReturnId(Long returnId) {
        return returnItemMapper.selectListByReturnId(returnId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void onQualityFinished(Long returnId, String qualityResult, String reportUrl) {
        AfterSaleReturnDO returnDO = validateReturnExists(returnId);
        returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
                .setQualityResult(qualityResult)
                .setQualityReportUrl(reportUrl)
                .setStatus(AfterSaleReturnStatusEnum.INSPECTED.getStatus()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void onInboundFinished(Long returnId) {
        AfterSaleReturnDO returnDO = validateReturnExists(returnId);
        returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
                .setStatus(AfterSaleReturnStatusEnum.INBOUND.getStatus())
                .setReturnInboundTime(LocalDateTime.now()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void onRefundCompleted(Long returnId, BigDecimal actualRefundPrice) {
        AfterSaleReturnDO returnDO = validateReturnExists(returnId);
        returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
                .setActualRefundPrice(actualRefundPrice)
                .setStatus(AfterSaleReturnStatusEnum.REFUNDED.getStatus())
                .setRefundTime(LocalDateTime.now()));
        // æ›´æ–°å…³è”工单状态为待退款/换货完成
        AfterSaleTicketDO ticket = ticketMapper.selectById(returnDO.getTicketId());
        if (ticket != null) {
            ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
                    .setStatus(AfterSaleTicketStatusEnum.WAITING_REFUND.getStatus()));
        }
    }
    // ========== å†…部方法 ==========
    private AfterSaleReturnDO validateReturnExists(Long id) {
        AfterSaleReturnDO returnDO = returnMapper.selectById(id);
        if (returnDO == null) {
            throw exception(RETURN_NOT_EXISTS);
        }
        return returnDO;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
package cn.iocoder.yudao.module.aftersales.service.ticket;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
 * å”®åŽå·¥å• Service æŽ¥å£
 */
public interface AfterSaleTicketService {
    /**
     * åˆ›å»ºå·¥å•
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @param userId      ç”¨æˆ·ç¼–号
     * @return ç¼–号
     */
    Long createTicket(@Valid AfterSaleTicketSaveReqVO createReqVO, Long userId);
    /**
     * æ›´æ–°å·¥å•
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateTicket(@Valid AfterSaleTicketSaveReqVO updateReqVO);
    /**
     * åˆ é™¤å·¥å•
     *
     * @param id ç¼–号
     */
    void deleteTicket(Long id);
    /**
     * èŽ·å¾—å·¥å•
     *
     * @param id ç¼–号
     * @return å·¥å•
     */
    AfterSaleTicketDO getTicket(Long id);
    /**
     * èŽ·å¾—å·¥å•åˆ—è¡¨
     *
     * @param ids ç¼–号列表
     * @return å·¥å•列表
     */
    List<AfterSaleTicketDO> getTicketList(Collection<Long> ids);
    /**
     * èŽ·å¾—å·¥å•åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @param userId    ç”¨æˆ·ç¼–号
     * @return å·¥å•分页
     */
    PageResult<AfterSaleTicketDO> getTicketPage(AfterSaleTicketPageReqVO pageReqVO, Long userId);
    /**
     * èŽ·å¾—å·¥å•æ˜Žç»†è¡Œåˆ—è¡¨
     *
     * @param ticketId å·¥å•编号
     * @return æ˜Žç»†è¡Œåˆ—表
     */
    List<AfterSaleTicketItemDO> getTicketItemListByTicketId(Long ticketId);
    /**
     * åˆ†é…å¤„理人
     *
     * @param id              å·¥å•编号
     * @param handlerUserId   å¤„理人用户编号
     */
    void assignTicket(Long id, Long handlerUserId);
    /**
     * é—®é¢˜åˆ¤å®šï¼ˆç¡®å®šé—®é¢˜ç±»åž‹ã€åˆ†ç±»ã€ä¸¥é‡ç¨‹åº¦ï¼‰
     *
     * @param id            å·¥å•编号
     * @param issueType     é—®é¢˜ç±»åž‹
     * @param issueCategory é—®é¢˜åˆ†ç±»
     * @param severityLevel ä¸¥é‡ç¨‹åº¦
     */
    void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel);
    /**
     * å®Œç»“工单
     *
     * @param id å·¥å•编号
     */
    void resolveTicket(Long id);
    /**
     * å…³é—­å·¥å•
     *
     * @param id å·¥å•编号
     */
    void closeTicket(Long id);
    /**
     * å–消工单
     *
     * @param id å·¥å•编号
     */
    void cancelTicket(Long id);
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,212 @@
package cn.iocoder.yudao.module.aftersales.service.ticket;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketItemMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService;
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
/**
 * å”®åŽå·¥å• Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class AfterSaleTicketServiceImpl implements AfterSaleTicketService {
    @Resource
    private AfterSaleTicketMapper ticketMapper;
    @Resource
    private AfterSaleTicketItemMapper ticketItemMapper;
    @Resource
    private AfterSaleNoRedisDAO noRedisDAO;
    @Resource
    private AfterSalePermissionService permissionService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createTicket(AfterSaleTicketSaveReqVO createReqVO, Long userId) {
        // 1. ç”Ÿæˆå•号
        String no = noRedisDAO.generate(AfterSaleNoRedisDAO.TICKET_NO_PREFIX);
        if (ticketMapper.selectByNo(no) != null) {
            throw exception(TICKET_NO_EXISTS);
        }
        // 2. æ’入工单
        AfterSaleTicketDO ticket = BeanUtils.toBean(createReqVO, AfterSaleTicketDO.class)
                .setNo(no)
                .setStatus(AfterSaleTicketStatusEnum.DRAFT.getStatus())
                .setOwnerUserId(userId);
        ticketMapper.insert(ticket);
        // 3. æ’入明细行
        if (CollUtil.isNotEmpty(createReqVO.getItems())) {
            createReqVO.getItems().forEach(item -> {
                AfterSaleTicketItemDO itemDO = BeanUtils.toBean(item, AfterSaleTicketItemDO.class);
                itemDO.setTicketId(ticket.getId());
                ticketItemMapper.insert(itemDO);
            });
        }
        // 4. åˆ›å»ºæ•°æ®æƒé™ï¼ˆè´Ÿè´£äººï¼‰
        AfterSalePermissionCreateReqBO permBO = new AfterSalePermissionCreateReqBO();
        permBO.setBizType(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType());
        permBO.setBizId(ticket.getId());
        permBO.setUserId(userId);
        permBO.setLevel(AfterSalePermissionLevelEnum.OWNER.getLevel());
        permissionService.createPermission(permBO);
        return ticket.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateTicket(AfterSaleTicketSaveReqVO updateReqVO) {
        // 1. æ ¡éªŒå­˜åœ¨
        AfterSaleTicketDO ticket = validateTicketExists(updateReqVO.getId());
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT);
        // 2. æ›´æ–°å·¥å•
        AfterSaleTicketDO updateObj = BeanUtils.toBean(updateReqVO, AfterSaleTicketDO.class);
        ticketMapper.updateById(updateObj);
        // 3. æ›´æ–°æ˜Žç»†è¡Œï¼ˆå…ˆåˆ åŽå¢žï¼‰
        ticketItemMapper.deleteByTicketId(ticket.getId());
        if (CollUtil.isNotEmpty(updateReqVO.getItems())) {
            updateReqVO.getItems().forEach(item -> {
                AfterSaleTicketItemDO itemDO = BeanUtils.toBean(item, AfterSaleTicketItemDO.class);
                itemDO.setTicketId(ticket.getId());
                ticketItemMapper.insert(itemDO);
            });
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteTicket(Long id) {
        AfterSaleTicketDO ticket = validateTicketExists(id);
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.CANCELED);
        // è½¯åˆ é™¤
        ticketMapper.deleteById(id);
        ticketItemMapper.deleteByTicketId(id);
        permissionService.deletePermission(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(), id);
    }
    @Override
    public AfterSaleTicketDO getTicket(Long id) {
        return validateTicketExists(id);
    }
    @Override
    public List<AfterSaleTicketDO> getTicketList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return List.of();
        }
        return ticketMapper.selectBatchIds(ids);
    }
    @Override
    public PageResult<AfterSaleTicketDO> getTicketPage(AfterSaleTicketPageReqVO pageReqVO, Long userId) {
        return ticketMapper.selectPage(pageReqVO, userId, pageReqVO.getSceneType(),
                pageReqVO.getNo(), pageReqVO.getName(),
                pageReqVO.getCustomerId(), pageReqVO.getSaleOrderId(),
                pageReqVO.getStatus(), pageReqVO.getIssueType());
    }
    @Override
    public List<AfterSaleTicketItemDO> getTicketItemListByTicketId(Long ticketId) {
        return ticketItemMapper.selectListByTicketId(ticketId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void assignTicket(Long id, Long handlerUserId) {
        AfterSaleTicketDO ticket = validateTicketExists(id);
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
        ticketMapper.updateById(new AfterSaleTicketDO().setId(id).setHandlerUserId(handlerUserId)
                .setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel) {
        AfterSaleTicketDO ticket = validateTicketExists(id);
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING);
        ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                .setIssueType(issueType).setIssueCategory(issueCategory).setSeverityLevel(severityLevel));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resolveTicket(Long id) {
        AfterSaleTicketDO ticket = validateTicketExists(id);
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING,
                AfterSaleTicketStatusEnum.WAITING_RETURN, AfterSaleTicketStatusEnum.WAITING_REFUND);
        ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                .setStatus(AfterSaleTicketStatusEnum.COMPLETED.getStatus())
                .setResolvedTime(LocalDateTime.now()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void closeTicket(Long id) {
        AfterSaleTicketDO ticket = validateTicketExists(id);
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.COMPLETED);
        ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                .setStatus(AfterSaleTicketStatusEnum.CLOSED.getStatus())
                .setClosedTime(LocalDateTime.now()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelTicket(Long id) {
        AfterSaleTicketDO ticket = validateTicketExists(id);
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
        ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
                .setStatus(AfterSaleTicketStatusEnum.CANCELED.getStatus()));
    }
    // ========== å†…部方法 ==========
    private AfterSaleTicketDO validateTicketExists(Long id) {
        AfterSaleTicketDO ticket = ticketMapper.selectById(id);
        if (ticket == null) {
            throw exception(TICKET_NOT_EXISTS);
        }
        return ticket;
    }
    private void validateTicketStatus(AfterSaleTicketDO ticket, AfterSaleTicketStatusEnum... allowedStatuses) {
        for (AfterSaleTicketStatusEnum status : allowedStatuses) {
            if (status.getStatus().equals(ticket.getStatus())) {
                return;
            }
        }
        throw exception(TICKET_STATUS_ERROR);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/util/AfterSalePermissionUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
package cn.iocoder.yudao.module.aftersales.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSaleSceneTypeEnum;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.github.yulichang.autoconfigure.MybatisPlusJoinProperties;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.springframework.util.Assert;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
 * å”®åŽæ¨¡å—数据权限工具类
 */
public class AfterSalePermissionUtils {
    /**
     * æ ¡éªŒç”¨æˆ·æ˜¯å¦æ˜¯å”®åŽç®¡ç†å‘˜
     *
     * @return æ˜¯/否
     */
    public static boolean isAftersaleAdmin() {
        PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class);
        return permissionApi.hasAnyRoles(getLoginUserId(), "aftersales_admin");
    }
    /**
     * æ ¡éªŒç”¨æˆ·æ˜¯å¦æœ‰æŸ¥çœ‹å…¨éƒ¨å”®åŽæ•°æ®çš„æƒé™
     */
    private static boolean hasViewAllPermission() {
        PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class);
        return permissionApi.hasAnyPermissions(getLoginUserId(), "aftersales:view-all");
    }
    /**
     * æž„造售后模块数据【分页】查询条件
     *
     * @param query     è¿žè¡¨æŸ¥è¯¢å¯¹è±¡
     * @param bizType   æ•°æ®ç±»åž‹ {@link AfterSaleBizTypeEnum}
     * @param bizId     æ•°æ®ç¼–号
     * @param userId    ç”¨æˆ·ç¼–号
     * @param sceneType åœºæ™¯ç±»åž‹
     */
    public static <T extends MPJLambdaWrapper<?>, S> void appendPermissionCondition(T query, Integer bizType,
                                                                                     SFunction<S, ?> bizId,
                                                                                     Long userId, Integer sceneType) {
        MybatisPlusJoinProperties mybatisPlusJoinProperties = SpringUtil.getBean(MybatisPlusJoinProperties.class);
        final String ownerUserIdField = mybatisPlusJoinProperties.getTableAlias() + ".owner_user_id";
        // åœºæ™¯ä¸€ï¼šæˆ‘负责的数据
        if (AfterSaleSceneTypeEnum.isOwner(sceneType)) {
            query.eq(ownerUserIdField, userId);
        }
        // åœºæ™¯äºŒï¼šæˆ‘参与的数据(我有读或写权限,并且不是负责人)
        if (AfterSaleSceneTypeEnum.isInvolved(sceneType)) {
            if (isAftersaleAdmin()) {
                return;
            }
            query.innerJoin(AfterSalePermissionDO.class, on -> on.eq(AfterSalePermissionDO::getBizType, bizType)
                    .eq(AfterSalePermissionDO::getBizId, bizId)
                    .in(AfterSalePermissionDO::getLevel, AfterSalePermissionLevelEnum.READ.getLevel(),
                            AfterSalePermissionLevelEnum.WRITE.getLevel())
                    .eq(AfterSalePermissionDO::getUserId, userId));
            query.ne(ownerUserIdField, userId);
        }
        // åœºæ™¯ä¸‰ï¼šä¸‹å±žè´Ÿè´£çš„æ•°æ®ï¼ˆä¸‹å±žæ˜¯è´Ÿè´£äººï¼‰
        if (AfterSaleSceneTypeEnum.isSubordinate(sceneType)) {
            AdminUserApi adminUserApi = SpringUtil.getBean(AdminUserApi.class);
            List<AdminUserRespDTO> subordinateUsers = adminUserApi.getUserListBySubordinate(userId);
            if (CollUtil.isEmpty(subordinateUsers)) {
                query.eq(ownerUserIdField, -1);
            } else {
                query.in(ownerUserIdField, convertSet(subordinateUsers, AdminUserRespDTO::getId));
            }
        }
        // åœºæ™¯å››ï¼šå…¨éƒ¨æ•°æ®ï¼ˆéœ€è¦æœ‰ view-all æƒé™æˆ–售后管理员)
        if (AfterSaleSceneTypeEnum.isAll(sceneType)) {
            if (isAftersaleAdmin() || hasViewAllPermission()) {
                return;
            }
            query.eq(ownerUserIdField, userId);
        }
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -51,7 +51,11 @@
    default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO pageReqVO, Long ownerUserId) {
        MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
        // æ‹¼æŽ¥æ•°æ®æƒé™çš„æŸ¥è¯¢æ¡ä»¶
        if (Boolean.TRUE.equals(pageReqVO.getPool())) {
        if (CrmSceneTypeEnum.isAll(pageReqVO.getSceneType())) {
            // åœºæ™¯å››ï¼šå…¨éƒ¨æ•°æ®ï¼Œä¸åŒºåˆ†å…¬æµ·/非公海,由 appendPermissionCondition ç»Ÿä¸€å¤„理
            CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
                    CrmCustomerDO::getId, ownerUserId, pageReqVO.getSceneType());
        } else if (Boolean.TRUE.equals(pageReqVO.getPool())) {
            query.isNull(CrmCustomerDO::getOwnerUserId);
        } else {
            CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmSceneTypeEnum.java
@@ -18,7 +18,8 @@
    OWNER(1, "我负责的"),
    INVOLVED(2, "我参与的"),
    SUBORDINATE(3, "下属负责的");
    SUBORDINATE(3, "下属负责的"),
    ALL(4, "全部");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(CrmSceneTypeEnum::getType).toArray(Integer[]::new);
@@ -43,6 +44,10 @@
        return ObjUtil.equal(SUBORDINATE.getType(), type);
    }
    public static boolean isAll(Integer type) {
        return ObjUtil.equal(ALL.getType(), type);
    }
    @Override
    public Integer[] array() {
        return ARRAYS;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java
@@ -37,6 +37,16 @@
    }
    /**
     * æ ¡éªŒç”¨æˆ·æ˜¯å¦æœ‰æŸ¥çœ‹å…¨éƒ¨å®¢æˆ·çš„æƒé™
     *
     * @return æ˜¯/否
     */
    private static boolean hasCustomerViewAllPermission() {
        PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class);
        return permissionApi.hasAnyPermissions(getLoginUserId(), "crm:customer:view-all");
    }
    /**
     * æž„造 CRM æ•°æ®ç±»åž‹æ•°æ®ã€åˆ†é¡µã€‘查询条件
     *
     * @param query     è¿žè¡¨æŸ¥è¯¢å¯¹è±¡
@@ -79,6 +89,14 @@
                query.in(ownerUserIdField, convertSet(subordinateUsers, AdminUserRespDTO::getId));
            }
        }
        // åœºæ™¯å››ï¼šå…¨éƒ¨æ•°æ®ï¼ˆéœ€è¦æœ‰ view-all æƒé™æˆ– CRM ç®¡ç†å‘˜ï¼‰
        if (CrmSceneTypeEnum.isAll(sceneType)) {
            if (isCrmAdmin() || hasCustomerViewAllPermission()) {
                return; // è·³è¿‡è¿‡æ»¤ï¼ŒæŸ¥çœ‹å…¨éƒ¨
            }
            // æ— æƒé™åˆ™é™çº§ä¸º"我负责的"
            query.eq(ownerUserIdField, userId);
        }
    }
    /**
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
@@ -5,6 +5,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -119,7 +120,7 @@
        private BigDecimal taxPrice;
        @Schema(description = "需求日期")
        private LocalDateTime demandTime;
        private LocalDate demandTime;
        @Schema(description = "备注", example = "备注")
        private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
@@ -7,6 +7,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -70,7 +71,7 @@
        private BigDecimal taxPercent;
        @Schema(description = "需求日期")
        private LocalDateTime demandTime;
        private LocalDate demandTime;
        @Schema(description = "备注", example = "备注")
        private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -19,6 +19,7 @@
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.productsales.MesWmProductSalesLineApi;
import cn.iocoder.yudao.module.mes.api.salesnotice.MesWmSalesNoticeApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -60,6 +61,8 @@
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmSalesNoticeApi salesNoticeApi;
    @Resource
    private MesWmProductSalesLineApi productSalesLineApi;
    @Resource
    private MesProMpsApi mpsApi;
    @Resource
@@ -128,10 +131,13 @@
        List<ErpSaleOrderItemDO> saleOrderItemList = saleOrderService.getSaleOrderItemListByOrderId(id);
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductId));
        Map<Long, String> batchCodeMap = productSalesLineApi.getBatchCodesBySaleOrderItemIds(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getId));
        return success(BeanUtils.toBean(saleOrder, ErpSaleOrderRespVO.class, saleOrderVO ->
                saleOrderVO.setItems(BeanUtils.toBean(saleOrderItemList, ErpSaleOrderRespVO.Item.class, item -> {
                    // åº“存信息暂不查询(仓储物流模块)
                    item.setStockCount(BigDecimal.ZERO);
                    item.setBatchCode(batchCodeMap.get(item.getId()));
                    MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                            .setProductBarCode(mdmItem.getBarCode()));
                })).setAttachmentList(storageAttachmentApi.listAttachments("erp_sale_order", id))));
@@ -177,7 +183,10 @@
        // 1.5 å‘货通知单信息
        Map<Long, Boolean> salesNoticeMap = salesNoticeApi.checkSalesNoticeBySaleOrderIds(
                convertSet(pageResult.getList(), ErpSaleOrderDO::getId));
        // 1.6 æ£€æŸ¥æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单(需要生产的明细对应的 MPS æ˜¯å¦å·²å®Œæˆï¼‰
        // 1.6 å‡ºåº“批号
        Map<Long, String> batchCodeMap = productSalesLineApi.getBatchCodesBySaleOrderItemIds(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getId));
        // 1.7 æ£€æŸ¥æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单(需要生产的明细对应的 MPS æ˜¯å¦å·²å®Œæˆï¼‰
        // ç­›é€‰å‡ºéœ€è¦ç”Ÿäº§çš„æ˜Žç»†ID
        List<Long> needProductionItemIds = saleOrderItemList.stream()
                .filter(item -> item.getNeedProduction() != null && item.getNeedProduction() == 1)
@@ -214,8 +223,11 @@
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class, saleOrder -> {
            saleOrder.setItems(BeanUtils.toBean(saleOrderItemMap.get(saleOrder.getId()), ErpSaleOrderRespVO.Item.class,
                    item -> MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                            .setProductBarCode(mdmItem.getBarCode()))));
                    item -> {
                        item.setBatchCode(batchCodeMap.get(item.getId()));
                        MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                                .setProductBarCode(mdmItem.getBarCode()));
                    }));
            saleOrder.setProductNames(CollUtil.join(saleOrder.getItems(), ",", ErpSaleOrderRespVO.Item::getProductName));
            MapUtils.findAndThen(customerMap, saleOrder.getCustomerId(), supplier -> saleOrder.setCustomerName(supplier.getName()));
            MapUtils.findAndThen(userMap, Long.parseLong(saleOrder.getCreator()), user -> saleOrder.setCreatorName(user.getNickname()));
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
@@ -33,6 +33,15 @@
    @Schema(description = "销售状态", example = "2")
    private Integer status;
    @Schema(description = "出库状态", example = "3")
    private Integer outStatus;
    @Schema(description = "生产状态", example = "0")
    private Integer productionStatus;
    @Schema(description = "是否需要生产", example = "1")
    private Integer needProduction;
    @Schema(description = "创建者")
    private String creator;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -164,6 +164,11 @@
        @Schema(description = "关联的 MPS ç¼–号", example = "1024")
        private Long mpsId;
        // ========== å‡ºåº“批号 ==========
        @Schema(description = "出库批号", example = "B2026001、B2026002")
        private String batchCode;
        // ========== å…³è”字段 ==========
        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
@@ -7,7 +7,7 @@
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalDate;
/**
 * ERP é‡‡è´­ç”³è¯·æ˜Žç»† DO
@@ -71,7 +71,7 @@
    /**
     * éœ€æ±‚日期
     */
    private LocalDateTime demandTime;
    private LocalDate demandTime;
    /**
     * å¤‡æ³¨
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
@@ -27,12 +27,15 @@
                .eqIfPresent(ErpSaleOrderDO::getCustomerId, reqVO.getCustomerId())
                .betweenIfPresent(ErpSaleOrderDO::getOrderTime, reqVO.getOrderTime())
                .eqIfPresent(ErpSaleOrderDO::getStatus, reqVO.getStatus())
                .eqIfPresent(ErpSaleOrderDO::getOutStatus, reqVO.getOutStatus())
                .eqIfPresent(ErpSaleOrderDO::getProductionStatus, reqVO.getProductionStatus())
                .likeIfPresent(ErpSaleOrderDO::getRemark, reqVO.getRemark())
                .eqIfPresent(ErpSaleOrderDO::getCreator, reqVO.getCreator())
                .orderByDesc(ErpSaleOrderDO::getId);
        if (reqVO.getProductId() != null) {
        if (reqVO.getProductId() != null || reqVO.getNeedProduction() != null) {
            query.leftJoin(ErpSaleOrderItemDO.class, ErpSaleOrderItemDO::getOrderId, ErpSaleOrderDO::getId)
                    .eq(reqVO.getProductId() != null, ErpSaleOrderItemDO::getProductId, reqVO.getProductId())
                    .eq(reqVO.getNeedProduction() != null, ErpSaleOrderItemDO::getNeedProduction, reqVO.getNeedProduction())
                    .groupBy(ErpSaleOrderDO::getId); // é¿å… 1 å¯¹å¤šæŸ¥è¯¢ï¼Œäº§ç”Ÿç›¸åŒçš„ 1
        }
        return selectJoinPage(reqVO, ErpSaleOrderDO.class, query);
@@ -47,6 +50,8 @@
                .eqIfPresent(ErpSaleOrderDO::getCustomerId, reqVO.getCustomerId())
                .betweenIfPresent(ErpSaleOrderDO::getOrderTime, reqVO.getOrderTime())
                .eqIfPresent(ErpSaleOrderDO::getStatus, reqVO.getStatus())
                .eqIfPresent(ErpSaleOrderDO::getOutStatus, reqVO.getOutStatus())
                .eqIfPresent(ErpSaleOrderDO::getProductionStatus, reqVO.getProductionStatus())
                .likeIfPresent(ErpSaleOrderDO::getRemark, reqVO.getRemark())
                .eqIfPresent(ErpSaleOrderDO::getCreator, reqVO.getCreator())
                .orderByDesc(ErpSaleOrderDO::getId);
@@ -57,9 +62,10 @@
            }
            query.in(ErpSaleOrderDO::getCustomerId, permittedCustomerIds);
        }
        if (reqVO.getProductId() != null) {
        if (reqVO.getProductId() != null || reqVO.getNeedProduction() != null) {
            query.leftJoin(ErpSaleOrderItemDO.class, ErpSaleOrderItemDO::getOrderId, ErpSaleOrderDO::getId)
                    .eq(reqVO.getProductId() != null, ErpSaleOrderItemDO::getProductId, reqVO.getProductId())
                    .eq(reqVO.getNeedProduction() != null, ErpSaleOrderItemDO::getNeedProduction, reqVO.getNeedProduction())
                    .groupBy(ErpSaleOrderDO::getId); // é¿å… 1 å¯¹å¤šæŸ¥è¯¢ï¼Œäº§ç”Ÿç›¸åŒçš„ 1
        }
        return selectJoinPage(reqVO, ErpSaleOrderDO.class, query);
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.api.productsales;
import java.util.Collection;
import java.util.Map;
/**
 * MES é”€å”®å‡ºåº“单行 API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MesWmProductSalesLineApi {
    /**
     * æ ¹æ®é”€å”®è®¢å•明细 ID åˆ—表查询出库批号
     *
     * @param saleOrderItemIds é”€å”®è®¢å•明细 ID åˆ—表
     * @return Map<销售订单明细ID, æ‰¹å·ä¸²è”字符串>
     */
    Map<Long, String> getBatchCodesBySaleOrderItemIds(Collection<Long> saleOrderItemIds);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.api.returnsales;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
/**
 * MES é”€å”®é€€è´§å• API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MesWmReturnSalesApi {
    /**
     * åˆ›å»ºé”€å”®é€€è´§å•(由售后模块调用)
     *
     * @param reqDTO åˆ›å»ºè¯·æ±‚参数
     * @return åˆ›å»ºçš„销售退货单编号
     */
    Long createReturnSales(MesWmReturnSalesCreateReqDTO reqDTO);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.mes.api.returnsales.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
/**
 * MES é”€å”®é€€è´§å•创建 Request DTO
 *
 * @author èŠ‹é“æºç 
 */
@Schema(description = "RPC æœåŠ¡ - MES é”€å”®é€€è´§å•创建 Request DTO")
@Data
public class MesWmReturnSalesCreateReqDTO {
    @Schema(description = "退货单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "RS202603010001")
    private String code;
    @Schema(description = "退货单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "售后退货")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long clientId;
    @Schema(description = "退货日期", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDate returnDate;
    @Schema(description = "退货原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "质量问题")
    private String returnReason;
    @Schema(description = "备注", example = "售后退货申请关联")
    private String remark;
    @Schema(description = "售后退货申请编号(关联 after_sale_return.id)", example = "1")
    private Long afterSaleReturnId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.api.productsales;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.Map;
/**
 * MES é”€å”®å‡ºåº“单行 API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MesWmProductSalesLineApiImpl implements MesWmProductSalesLineApi {
    @Resource
    private MesWmProductSalesLineMapper productSalesLineMapper;
    @Override
    public Map<Long, String> getBatchCodesBySaleOrderItemIds(Collection<Long> saleOrderItemIds) {
        return productSalesLineMapper.selectBatchCodesBySaleOrderItemIds(saleOrderItemIds);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.api.returnsales;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesSaveReqVO;
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
 * MES é”€å”®é€€è´§å• API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
public class MesWmReturnSalesApiImpl implements MesWmReturnSalesApi {
    @Resource
    private MesWmReturnSalesService returnSalesService;
    @Override
    public Long createReturnSales(MesWmReturnSalesCreateReqDTO reqDTO) {
        MesWmReturnSalesSaveReqVO createReqVO = BeanUtils.toBean(reqDTO, MesWmReturnSalesSaveReqVO.class);
        return returnSalesService.createReturnSales(createReqVO);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/MesDvMachineryController.java
@@ -52,6 +52,8 @@
    private MesDvMachineryTypeService machineryTypeService;
    @Resource
    private MesMdWorkshopService workshopService;
    @Resource
    private cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService workstationService;
    @PostMapping("/create")
    @Operation(summary = "创建设备")
@@ -151,17 +153,21 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. æ‰¹é‡èŽ·å–è®¾å¤‡ç±»åž‹å’Œè½¦é—´ä¿¡æ¯
        // 1. æ‰¹é‡èŽ·å–è®¾å¤‡ç±»åž‹ã€è½¦é—´å’Œå·¥ä½œç«™ä¿¡æ¯
        Map<Long, MesDvMachineryTypeDO> machineryTypeMap = machineryTypeService.getMachineryTypeMap(
                convertSet(list, MesDvMachineryDO::getMachineryTypeId));
        Map<Long, MesMdWorkshopDO> workshopMap = workshopService.getWorkshopMap(
                convertSet(list, MesDvMachineryDO::getWorkshopId));
        Map<Long, cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO> workstationMap =
                workstationService.getWorkstationMap(convertSet(list, MesDvMachineryDO::getWorkstationId));
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesDvMachineryRespVO.class, vo -> {
            MapUtils.findAndThen(machineryTypeMap, vo.getMachineryTypeId(),
                    machineryType -> vo.setMachineryTypeName(machineryType.getName()));
            MapUtils.findAndThen(workshopMap, vo.getWorkshopId(),
                    workshop -> vo.setWorkshopName(workshop.getName()));
            MapUtils.findAndThen(workstationMap, vo.getWorkstationId(),
                    workstation -> vo.setWorkstationName(workstation.getName()));
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryImportExcelVO.java
@@ -43,4 +43,13 @@
    @ExcelProperty("备注")
    private String remark;
    @ExcelProperty("生产效率(每小时)")
    private java.math.BigDecimal productionEfficiency;
    @ExcelProperty("传送履带")
    private Integer conveyorBelt;
    @ExcelProperty("工作站编码")
    private String workstationCode;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryPageReqVO.java
@@ -35,4 +35,7 @@
    @Schema(description = "设备状态", example = "1")
    private Integer status;
    @Schema(description = "工作站编号", example = "300")
    private Long workstationId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryRespVO.java
@@ -66,6 +66,21 @@
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "生产效率(每小时)", example = "10.5")
    @ExcelProperty("生产效率(每小时)")
    private java.math.BigDecimal productionEfficiency;
    @Schema(description = "传送履带(2=双倍效率)", example = "2")
    @ExcelProperty("传送履带")
    private Integer conveyorBelt;
    @Schema(description = "工作站编号", example = "300")
    private Long workstationId;
    @Schema(description = "工作站名称", example = "一号工作站")
    @ExcelProperty("工作站")
    private String workstationName;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachinerySaveReqVO.java
@@ -49,4 +49,13 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "生产效率(每小时)", example = "10.5")
    private java.math.BigDecimal productionEfficiency;
    @Schema(description = "传送履带(2=双倍效率)", example = "2")
    private Integer conveyorBelt;
    @Schema(description = "工作站编号", example = "300")
    private Long workstationId;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/documentaudit/vo/MesPdDocumentAuditPageReqVO.java
@@ -21,4 +21,7 @@
    @Schema(description = "审核状态", example = "0")
    private Integer auditStatus;
    @Schema(description = "项目ID", example = "1")
    private Long projectId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
@@ -34,6 +34,7 @@
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@@ -98,6 +99,20 @@
        return success(true);
    }
    @GetMapping("/calculate-duration")
    @Operation(summary = "计算生产时长", description = "根据工作站绑定设备的生产效率和传送履带,自动计算生产时长(小时)")
    @Parameters({
            @Parameter(name = "workstationId", description = "工作站编号", required = true),
            @Parameter(name = "quantity", description = "排产数量", required = true)
    })
    @PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
    public CommonResult<java.util.Map<String, Object>> calculateDuration(
            @RequestParam("workstationId") Long workstationId,
            @RequestParam("quantity") BigDecimal quantity) {
        Integer duration = taskService.calculateDuration(workstationId, quantity);
        return success(java.util.Map.of("duration", duration));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除生产任务")
    @Parameter(name = "id", description = "编号", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java
@@ -6,9 +6,9 @@
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
/**
 * ç®¡ç†åŽå° - MES é”€å”®é€€è´§ç›¸å…³
@@ -26,6 +26,9 @@
    @Schema(description = "退货单名称", example = "销售退货")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
@@ -36,7 +39,7 @@
    private Integer status;
    @Schema(description = "退货日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] returnDate;
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate[] returnDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -23,6 +24,9 @@
    @Schema(description = "退货单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "销售退货")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
@@ -33,7 +37,7 @@
    private String clientName;
    @Schema(description = "退货日期")
    private LocalDateTime returnDate;
    private LocalDate returnDate;
    @Schema(description = "退货原因", example = "质量问题")
    private String returnReason;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java
@@ -5,7 +5,7 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.LocalDate;
/**
 * ç®¡ç†åŽå° - MES é”€å”®é€€è´§å•新增/修改 Request VO
@@ -27,6 +27,9 @@
    @NotBlank(message = "退货单名称不能为空")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
@@ -36,7 +39,7 @@
    @Schema(description = "退货日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "退货日期不能为空")
    private LocalDateTime returnDate;
    private LocalDate returnDate;
    @Schema(description = "退货原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "质量问题")
    @NotBlank(message = "退货原因不能为空")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES ä¾›åº”商退货单 Response VO")
@@ -34,7 +35,7 @@
    private String vendorNickname;
    @Schema(description = "退货日期")
    private LocalDateTime returnDate;
    private LocalDate returnDate;
    @Schema(description = "退货原因", example = "质量不合格")
    private String returnReason;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java
@@ -5,7 +5,7 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES ä¾›åº”商退货单新增/修改 Request VO")
@Data
@@ -31,7 +31,7 @@
    @Schema(description = "退货日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "退货日期不能为空")
    private LocalDateTime returnDate;
    private LocalDate returnDate;
    @Schema(description = "退货原因", example = "质量不合格")
    private String returnReason;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java
@@ -76,5 +76,19 @@
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * ç”Ÿäº§æ•ˆçŽ‡ï¼ˆæ¯å°æ—¶ï¼‰
     */
    private java.math.BigDecimal productionEfficiency;
    /**
     * ä¼ é€å±¥å¸¦ï¼ˆ2=双倍效率)
     */
    private Integer conveyorBelt;
    /**
     * å·¥ä½œç«™ç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO#getId()}
     */
    private Long workstationId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java
@@ -7,7 +7,7 @@
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
import java.time.LocalDate;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
/**
@@ -39,6 +39,10 @@
     */
    private String name;
    /**
     * é”€å”®è®¢å• ID
     */
    private Long saleOrderId;
    /**
     * é”€å”®è®¢å•编号
     */
    private String salesOrderCode;
@@ -51,7 +55,7 @@
    /**
     * é€€è´§æ—¥æœŸ
     */
    private LocalDateTime returnDate;
    private LocalDate returnDate;
    /**
     * é€€è´§åŽŸå› 
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java
@@ -7,7 +7,7 @@
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
import java.time.LocalDate;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
/**
@@ -49,7 +49,7 @@
    /**
     * é€€è´§æ—¥æœŸ
     */
    private LocalDateTime returnDate;
    private LocalDate returnDate;
    /**
     * é€€è´§åŽŸå› 
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/machinery/MesDvMachineryMapper.java
@@ -24,6 +24,7 @@
                .eqIfPresent(MesDvMachineryDO::getMachineryTypeId, reqVO.getMachineryTypeId())
                .inIfPresent(MesDvMachineryDO::getMachineryTypeId, reqVO.getMachineryTypeIds())
                .eqIfPresent(MesDvMachineryDO::getWorkshopId, reqVO.getWorkshopId())
                .eqIfPresent(MesDvMachineryDO::getWorkstationId, reqVO.getWorkstationId())
                .eqIfPresent(MesDvMachineryDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesDvMachineryDO::getId));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pd/documentaudit/MesPdDocumentAuditMapper.java
@@ -21,6 +21,16 @@
                .orderByDesc(MesPdDocumentAuditDO::getId));
    }
    default PageResult<MesPdDocumentAuditDO> selectPage(MesPdDocumentAuditPageReqVO reqVO,
                                                         Collection<Long> documentIds) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesPdDocumentAuditDO>()
                .eqIfPresent(MesPdDocumentAuditDO::getDocumentId, reqVO.getDocumentId())
                .likeIfPresent(MesPdDocumentAuditDO::getAuditNo, reqVO.getAuditNo())
                .eqIfPresent(MesPdDocumentAuditDO::getAuditStatus, reqVO.getAuditStatus())
                .inIfPresent(MesPdDocumentAuditDO::getDocumentId, documentIds)
                .orderByDesc(MesPdDocumentAuditDO::getId));
    }
    default List<MesPdDocumentAuditDO> selectListByDocumentId(Long documentId) {
        return selectList(MesPdDocumentAuditDO::getDocumentId, documentId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java
@@ -1,11 +1,13 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales;
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.wm.productsales.MesWmProductSalesLineDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;
/**
 * MES é”€å”®å‡ºåº“单行 Mapper
@@ -31,6 +33,26 @@
                .orderByDesc(MesWmProductSalesLineDO::getId));
    }
    /**
     * æ ¹æ®é”€å”®è®¢å•明细 ID åˆ—表查询出库行,按 saleOrderItemId åˆ†ç»„返回批号
     */
    default Map<Long, String> selectBatchCodesBySaleOrderItemIds(java.util.Collection<Long> saleOrderItemIds) {
        if (saleOrderItemIds == null || saleOrderItemIds.isEmpty()) {
            return java.util.Collections.emptyMap();
        }
        List<MesWmProductSalesLineDO> lines = selectList(new LambdaQueryWrapperX<MesWmProductSalesLineDO>()
                .in(MesWmProductSalesLineDO::getSaleOrderItemId, saleOrderItemIds));
        return lines.stream()
                .filter(l -> l.getBatchCode() != null && !l.getBatchCode().isEmpty())
                .collect(java.util.stream.Collectors.groupingBy(
                        MesWmProductSalesLineDO::getSaleOrderItemId,
                        java.util.LinkedHashMap::new,
                        java.util.stream.Collectors.collectingAndThen(
                                java.util.stream.Collectors.mapping(MesWmProductSalesLineDO::getBatchCode,
                                        java.util.stream.Collectors.toSet()),
                                codes -> String.join("、", codes))));
    }
    default void updateQualityStatusByIds(List<Long> ids, Integer qualityStatus) {
        update(new MesWmProductSalesLineDO().setQualityStatus(qualityStatus),
                new LambdaUpdateWrapper<MesWmProductSalesLineDO>().in(MesWmProductSalesLineDO::getId, ids));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -169,6 +169,7 @@
    ErrorCode DV_MACHINERY_HAS_CHECK_RECORD = new ErrorCode(1_040_301_004, "设备已关联点检记录,无法删除");
    ErrorCode DV_MACHINERY_HAS_MAINTEN_RECORD = new ErrorCode(1_040_301_005, "设备已关联保养记录,无法删除");
    ErrorCode DV_MACHINERY_HAS_REPAIR = new ErrorCode(1_040_301_006, "设备已关联维修工单,无法删除");
    ErrorCode DV_MACHINERY_WORKSTATION_NOT_EXISTS = new ErrorCode(1_040_301_008, "工作站编码不存在");
    // ========== MES è®¾å¤‡ç®¡ç†-点检保养项目(1-040-304-000) ==========
    ErrorCode DV_SUBJECT_NOT_EXISTS = new ErrorCode(1_040_304_000, "点检保养项目不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java
@@ -54,6 +54,9 @@
    @Lazy
    private MesMdWorkshopService workshopService;
    @Resource
    @Lazy
    private cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService workstationService;
    @Resource
    private MesWmBarcodeService barcodeService;
    @Resource
    @Lazy
@@ -201,7 +204,7 @@
            throw exception(DV_MACHINERY_IMPORT_LIST_IS_EMPTY);
        }
        // 2. æ‰¹é‡åŠ è½½è®¾å¤‡ç±»åž‹å’Œè½¦é—´ï¼Œæž„å»ºç¼–ç åˆ°å®žä½“çš„æ˜ å°„
        // 2. æ‰¹é‡åŠ è½½è®¾å¤‡ç±»åž‹ã€è½¦é—´å’Œå·¥ä½œç«™ï¼Œæž„å»ºç¼–ç åˆ°å®žä½“çš„æ˜ å°„
        List<MesDvMachineryTypeDO> allTypes = machineryTypeService.getMachineryTypeList(
                new cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo.type.MesDvMachineryTypeListReqVO());
        Map<String, MesDvMachineryTypeDO> typeCodeMap = allTypes.stream()
@@ -210,6 +213,13 @@
                cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.ENABLE.getStatus());
        Map<String, MesMdWorkshopDO> workshopCodeMap = allWorkshops.stream()
                .collect(Collectors.toMap(MesMdWorkshopDO::getCode, w -> w, (a, b) -> a));
        List<cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO> allWorkstations =
                workstationService.getWorkstationListByStatus(
                        cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.ENABLE.getStatus());
        Map<String, cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO> workstationCodeMap =
                allWorkstations.stream().collect(Collectors.toMap(
                        cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO::getCode,
                        w -> w, (a, b) -> a));
        // 3. éåŽ†ï¼Œé€ä¸ªåˆ›å»º or æ›´æ–°
        MesDvMachineryImportRespVO respVO = MesDvMachineryImportRespVO.builder()
@@ -249,24 +259,38 @@
                return;
            }
            // 3.4 åˆ¤æ–­ï¼šåˆ›å»º or æ›´æ–°
            // 3.4 æ ¡éªŒå·¥ä½œç«™ç¼–码(可选)
            Long workstationId = null;
            if (StrUtil.isNotBlank(importItem.getWorkstationCode())) {
                cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO workstation =
                        workstationCodeMap.get(importItem.getWorkstationCode());
                if (workstation == null) {
                    respVO.getFailureCodes().put(key, "工作站编码[" + importItem.getWorkstationCode() + "]不存在");
                    return;
                }
                workstationId = workstation.getId();
            }
            // 3.5 åˆ¤æ–­ï¼šåˆ›å»º or æ›´æ–°
            MesDvMachineryDO existMachinery = machineryMapper.selectByCode(importItem.getCode());
            if (existMachinery == null) {
                // 3.4.1 åˆ›å»º
                // 3.5.1 åˆ›å»º
                MesDvMachineryDO machinery = BeanUtils.toBean(importItem, MesDvMachineryDO.class);
                machinery.setMachineryTypeId(machineryType.getId());
                machinery.setWorkshopId(workshop.getId());
                machinery.setWorkstationId(workstationId);
                machineryMapper.insert(machinery);
                // è‡ªåŠ¨ç”Ÿæˆæ¡ç 
                barcodeService.autoGenerateBarcode(BarcodeBizTypeEnum.MACHINERY.getValue(),
                        machinery.getId(), machinery.getCode(), machinery.getName());
                respVO.getCreateCodes().add(importItem.getCode());
            } else if (updateSupport) {
                // 3.4.2 æ›´æ–°
                // 3.5.2 æ›´æ–°
                MesDvMachineryDO updateObj = BeanUtils.toBean(importItem, MesDvMachineryDO.class);
                updateObj.setId(existMachinery.getId());
                updateObj.setMachineryTypeId(machineryType.getId());
                updateObj.setWorkshopId(workshop.getId());
                updateObj.setWorkstationId(workstationId);
                machineryMapper.updateById(updateObj);
                respVO.getUpdateCodes().add(importItem.getCode());
            } else {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/documentaudit/MesPdDocumentAuditServiceImpl.java
@@ -4,7 +4,9 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pd.documentaudit.vo.MesPdDocumentAuditPageReqVO;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.mes.controller.admin.pd.documentaudit.vo.MesPdDocumentAuditSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.documentaudit.MesPdDocumentAuditDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pd.documentaudit.MesPdDocumentAuditMapper;
import cn.iocoder.yudao.module.mes.service.pd.document.MesPdDocumentService;
@@ -14,7 +16,9 @@
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PD_DOCUMENT_AUDIT_NOT_EXISTS;
@@ -72,6 +76,17 @@
    @Override
    public PageResult<MesPdDocumentAuditDO> getDocumentAuditPage(MesPdDocumentAuditPageReqVO pageReqVO) {
        // æŒ‰é¡¹ç›®ç­›é€‰ï¼šå…ˆæŸ¥å‡ºé¡¹ç›®ä¸‹æ‰€æœ‰èµ„料,再按资料 ID è¿‡æ»¤å®¡æ ¸è®°å½•
        if (pageReqVO.getProjectId() != null) {
            List<MesPdDocumentDO> documents = documentService.getDocumentListByProjectId(
                    pageReqVO.getProjectId());
            if (CollUtil.isEmpty(documents)) {
                return PageResult.empty();
            }
            List<Long> documentIds = documents.stream()
                    .map(MesPdDocumentDO::getId).collect(Collectors.toList());
            return documentAuditMapper.selectPage(pageReqVO, documentIds);
        }
        return documentAuditMapper.selectPage(pageReqVO);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java
@@ -123,6 +123,15 @@
    void cancelTaskByOrderId(Long workOrderId);
    /**
     * æ ¹æ®å·¥ä½œç«™ç»‘定设备的生产效率和传送履带,计算生产时长(单位:小时)
     *
     * @param workstationId å·¥ä½œç«™ç¼–号
     * @param quantity      æŽ’产数量
     * @return ç”Ÿäº§æ—¶é•¿ï¼ˆå°æ—¶ï¼‰ï¼Œæ— è®¾å¤‡åˆ™è¿”回 0
     */
    Integer calculateDuration(Long workstationId, BigDecimal quantity);
    /**
     * ç´¯åŠ ä»»åŠ¡çš„å·²ç”Ÿäº§ã€åˆæ ¼ã€ä¸åˆæ ¼æ•°é‡
     *
     * @param id                    ä»»åŠ¡ç¼–å·
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -7,17 +7,21 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
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.MesMdWorkstationMachineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationMachineService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
@@ -40,9 +44,11 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_ALREADY_FINISHED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_HAS_FEEDBACK;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_NOT_EXISTS;
@@ -86,6 +92,11 @@
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private MesMdWorkstationMachineService workstationMachineService;
    @Resource
    @Lazy
    private MesDvMachineryService machineryService;
    @Override
    public Long createTask(MesProTaskSaveReqVO createReqVO) {
@@ -318,6 +329,33 @@
    }
    @Override
    public Integer calculateDuration(Long workstationId, BigDecimal quantity) {
        if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
            return 0;
        }
        // 1. æŸ¥è¯¢å·¥ä½œç«™ç»‘定的设备
        List<MesMdWorkstationMachineDO> machines = workstationMachineService
                .getWorkstationMachineListByWorkstationId(workstationId);
        if (CollUtil.isEmpty(machines)) {
            return 0;
        }
        // 2. èŽ·å–è®¾å¤‡è¯¦æƒ…ï¼Œè®¡ç®—æ€»æ•ˆçŽ‡
        Set<Long> machineryIds = convertSet(machines, MesMdWorkstationMachineDO::getMachineryId);
        List<MesDvMachineryDO> machineryList = machineryService.getMachineryList(machineryIds);
        BigDecimal totalEfficiency = BigDecimal.ZERO;
        for (MesDvMachineryDO m : machineryList) {
            BigDecimal efficiency = m.getProductionEfficiency() != null ? m.getProductionEfficiency() : BigDecimal.ZERO;
            Integer belt = m.getConveyorBelt() != null && m.getConveyorBelt() > 0 ? m.getConveyorBelt() : 1;
            totalEfficiency = totalEfficiency.add(efficiency.multiply(BigDecimal.valueOf(belt)));
        }
        if (totalEfficiency.compareTo(BigDecimal.ZERO) <= 0) {
            return 0;
        }
        // 3. ç”Ÿäº§æ—¶é•¿ï¼ˆå°æ—¶ï¼‰ = ceil(排产数量 / æ€»æ•ˆçއ),效率单位为每小时产量
        return quantity.divide(totalEfficiency, 0, java.math.RoundingMode.CEILING).intValue();
    }
    @Override
    public MesProTaskAutoGeneratePreviewRespVO previewAutoGenerate(MesProTaskAutoGenerateReqVO reqVO) {
        // 1. æ ¡éªŒå·¥å•已确认
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidAwardController.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.*;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmBidAwardDO;
import cn.iocoder.yudao.module.srm.service.tender.SrmBidAwardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -42,7 +43,20 @@
    @Operation(summary = "根据招标项目ID获取定标详情")
    @PreAuthorize("@ss.hasPermission('srm:bid-award:query')")
    public CommonResult<SrmBidAwardRespVO> getAwardByProject(@RequestParam("tenderProjectId") Long tenderProjectId) {
        return success(BeanUtils.toBean(bidAwardService.getAwardByProjectId(tenderProjectId), SrmBidAwardRespVO.class));
        SrmBidAwardDO award = bidAwardService.getAwardByProjectId(tenderProjectId);
        if (award == null) {
            return success((SrmBidAwardRespVO) null);
        }
        return success(BeanUtils.toBean(award, SrmBidAwardRespVO.class));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除定标")
    @Parameter(name = "id", description = "定标ID", required = true)
    @PreAuthorize("@ss.hasPermission('srm:bid-award:update')")
    public CommonResult<Boolean> deleteAward(@RequestParam("id") Long id) {
        bidAwardService.deleteAward(id);
        return success(true);
    }
    @PutMapping("/approve")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmBidEvaluationController.java
@@ -40,6 +40,14 @@
        return success(bidEvaluationService.createEvaluation(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新评标记录")
    @PreAuthorize("@ss.hasPermission('srm:bid-evaluation:update')")
    public CommonResult<Boolean> updateEvaluation(@Valid @RequestBody SrmBidEvaluationSaveReqVO updateReqVO) {
        bidEvaluationService.updateEvaluation(updateReqVO);
        return success(true);
    }
    @GetMapping("/list-by-project")
    @Operation(summary = "按招标项目获取评标列表")
    @PreAuthorize("@ss.hasPermission('srm:bid-evaluation:query')")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderBidController.java
@@ -111,6 +111,7 @@
            if (material != null) {
                vo.setTenderMaterialCode(material.getProductCode());
                vo.setTenderMaterialName(material.getProductName());
                vo.setMaterialName(material.getProductName());
                vo.setTenderMaterialSpec(material.getProductSpec());
                vo.setTenderMaterialUnit(material.getUnit());
            }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmSupplierQuoteRespVO.java
@@ -31,6 +31,9 @@
    @Schema(description = "物料名称")
    private String tenderMaterialName;
    @Schema(description = "物料名称(与前端 materialName å¯¹åº”)")
    private String materialName;
    @Schema(description = "物料规格")
    private String tenderMaterialSpec;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/mysql/SrmTenderBidMapper.java
@@ -21,4 +21,9 @@
                .eq(SrmTenderBidDO::getSupplierId, supplierId));
    }
    default boolean existsByBidNo(String bidNo) {
        return exists(new LambdaQueryWrapperX<SrmTenderBidDO>()
                .eq(SrmTenderBidDO::getBidNo, bidNo));
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java
@@ -37,6 +37,7 @@
    ErrorCode TENDER_STATUS_ALREADY_CLOSED = new ErrorCode(1_040_500_010, "已关闭的项目不允许操作");
    ErrorCode BID_NOT_EXISTS = new ErrorCode(1_040_500_003, "投标记录不存在");
    ErrorCode BID_SUPPLIER_EXISTS = new ErrorCode(1_040_500_004, "供应商已参与投标");
    ErrorCode BID_NO_EXISTS = new ErrorCode(1_040_500_016, "投标编号已存在");
    ErrorCode BID_PAST_END_TIME = new ErrorCode(1_040_500_005, "投标已截止");
    ErrorCode BID_SUPPLIER_NOT_APPROVED = new ErrorCode(1_040_500_011, "供应商未通过准入审批,无法参与投标");
    ErrorCode BID_WITHDRAWN_CANNOT_QUOTE = new ErrorCode(1_040_500_012, "投标已撤标,不允许报价");
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java
@@ -17,6 +17,7 @@
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.hutool.core.collection.CollUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardService.java
@@ -25,6 +25,11 @@
    SrmBidAwardDO getAwardByProjectId(Long tenderProjectId);
    /**
     * åˆ é™¤å®šæ ‡
     */
    void deleteAward(Long id);
    /**
     * å®¡æ‰¹å®šæ ‡
     */
    void approveAward(Long id);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java
@@ -15,10 +15,12 @@
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
@@ -29,6 +31,7 @@
 */
@Service
@Validated
@Slf4j
public class SrmBidAwardServiceImpl implements SrmBidAwardService {
    @Resource
@@ -86,6 +89,13 @@
    @Override
    public SrmBidAwardDO getAwardByProjectId(Long tenderProjectId) {
        return bidAwardMapper.selectByTenderProjectId(tenderProjectId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteAward(Long id) {
        SrmBidAwardDO award = validateAwardExists(id);
        bidAwardMapper.deleteById(id);
    }
    @Override
@@ -169,15 +179,33 @@
        ErpPurchaseOrderCreateReqDTO createReq = new ErpPurchaseOrderCreateReqDTO();
        createReq.setSupplierId(srmSupplier.getId());
        createReq.setOrderTime(LocalDateTime.now());
        createReq.setDepositPrice(BigDecimal.ZERO);
        createReq.setItems(orderItems);
        createReq.setRemark("SRM å®šæ ‡è‡ªåŠ¨ç”Ÿæˆ, å®šæ ‡ç¼–号: " + award.getAwardNo());
        Long purchaseOrderId = erpPurchaseOrderApi.createPurchaseOrder(createReq).getCheckedData();
        // èŽ·å–é‡‡è´­è®¢å•å·
        String purchaseOrderNo = null;
        try {
            purchaseOrderNo = erpPurchaseOrderApi.getPurchaseOrder(purchaseOrderId)
                    .getCheckedData().getNo();
        } catch (Exception e) {
            log.warn("[generatePurchaseOrder] èŽ·å–é‡‡è´­è®¢å•å·å¤±è´¥ï¼ŒpurchaseOrderId={}", purchaseOrderId, e);
        }
        // 8. æ›´æ–°å®šæ ‡è®°å½•
        award.setPurchaseOrderId(purchaseOrderId);
        award.setPurchaseOrderNo(purchaseOrderNo);
        award.setAwardStatus(AwardStatusEnum.ORDER_GENERATED.getCode());
        bidAwardMapper.updateById(award);
        // 9. æ›´æ–°æ‹›æ ‡é¡¹ç›®çŠ¶æ€
        SrmTenderProjectDO project = tenderProjectMapper.selectById(award.getTenderProjectId());
        if (project != null) {
            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/SrmBidEvaluationService.java
@@ -20,6 +20,11 @@
    Long createEvaluation(@Valid SrmBidEvaluationSaveReqVO createReqVO);
    /**
     * æ›´æ–°è¯„标记录
     */
    void updateEvaluation(@Valid SrmBidEvaluationSaveReqVO updateReqVO);
    /**
     * èŽ·å–è¯„æ ‡åˆ—è¡¨ï¼ˆæŒ‰æ‹›æ ‡é¡¹ç›®ï¼‰
     */
    List<SrmBidEvaluationDO> getEvaluationList(Long tenderProjectId);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidEvaluationServiceImpl.java
@@ -44,6 +44,8 @@
    @Transactional(rollbackFor = Exception.class)
    public Long createEvaluation(SrmBidEvaluationSaveReqVO createReqVO) {
        SrmBidEvaluationDO evaluation = BeanUtils.toBean(createReqVO, SrmBidEvaluationDO.class);
        // å¼ºåˆ¶ç½®ç©º id,避免前端传入已有 id å¯¼è‡´ä¸»é”®å†²çªï¼Œç”±æ•°æ®åº“序列自动生成
        evaluation.setId(null);
        // è®¡ç®—综合评分: ä»·æ ¼*0.4 + æŠ€æœ¯*0.3 + äº¤ä»˜*0.2 + æœåŠ¡*0.1
        BigDecimal price = nvl(evaluation.getPriceScore());
        BigDecimal tech = nvl(evaluation.getTechScore());
@@ -67,6 +69,28 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateEvaluation(SrmBidEvaluationSaveReqVO updateReqVO) {
        SrmBidEvaluationDO existing = bidEvaluationMapper.selectById(updateReqVO.getId());
        if (existing == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.EVALUATION_NOT_EXISTS_TENDER);
        }
        SrmBidEvaluationDO evaluation = BeanUtils.toBean(updateReqVO, SrmBidEvaluationDO.class);
        BigDecimal price = nvl(evaluation.getPriceScore());
        BigDecimal tech = nvl(evaluation.getTechScore());
        BigDecimal delivery = nvl(evaluation.getDeliveryScore());
        BigDecimal service = nvl(evaluation.getServiceScore());
        BigDecimal composite = price.multiply(new BigDecimal("0.4"))
                .add(tech.multiply(new BigDecimal("0.3")))
                .add(delivery.multiply(new BigDecimal("0.2")))
                .add(service.multiply(new BigDecimal("0.1")))
                .setScale(2, RoundingMode.HALF_UP);
        evaluation.setCompositeScore(composite);
        evaluation.setEvaluationTime(LocalDateTime.now());
        bidEvaluationMapper.updateById(evaluation);
    }
    @Override
    public List<SrmBidEvaluationDO> getEvaluationList(Long tenderProjectId) {
        return bidEvaluationMapper.selectListByTenderProjectId(tenderProjectId);
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java
@@ -51,6 +51,10 @@
        if (!supplierApplyMapper.existsApprovedBySupplierId(createReqVO.getSupplierId())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_SUPPLIER_NOT_APPROVED);
        }
        // æ ¡éªŒæŠ•标编号是否重复
        if (tenderBidMapper.existsByBidNo(createReqVO.getBidNo())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_NO_EXISTS);
        }
        // æ ¡éªŒä¾›åº”商是否已参与投标
        SrmTenderBidDO existing = tenderBidMapper.selectByTenderAndSupplier(createReqVO.getTenderProjectId(), createReqVO.getSupplierId());
        if (existing != null) {
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java
@@ -11,6 +11,7 @@
import cn.iocoder.yudao.module.srm.dal.mysql.SrmBidAwardMapper;
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;
@@ -64,7 +65,15 @@
    @Override
    public SrmTenderProjectDO getTenderProject(Long id) {
        return tenderProjectMapper.selectById(id);
        SrmTenderProjectDO project = tenderProjectMapper.selectById(id);
        if (project != null) {
            SrmBidAwardDO award = bidAwardMapper.selectByTenderProjectId(id);
            if (award != null) {
                project.setAwardId(award.getId());
                project.setPurchaseOrderNo(award.getPurchaseOrderNo());
            }
        }
        return project;
    }
    @Override
@@ -82,6 +91,10 @@
                if (award != null) {
                    project.setAwardId(award.getId());
                    project.setPurchaseOrderNo(award.getPurchaseOrderNo());
                    // é˜²å¾¡ï¼šå¦‚果定标已生成订单但项目状态未更新,同步状态
                    if (AwardStatusEnum.ORDER_GENERATED.getCode().equals(award.getAwardStatus())) {
                        project.setTenderStatus(TenderStatusEnum.CLOSED.getCode());
                    }
                }
            });
        }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -53,6 +53,10 @@
    HRM_RESIGNATION_APPLICATION("hrm_resignation_application", "HRM ç¦»èŒç”³è¯·"),
    HRM_ATTENDANCE_EXCEPTION("hrm_attendance_exception", "HRM è€ƒå‹¤å¼‚常"),
    // ===== å”®åŽ =====
    AFTER_SALE_TICKET("aftersales_ticket", "售后工单"),
    AFTER_SALE_RETURN("aftersales_return", "售后退货申请"),
    // ===== MES =====
    MES_PD_DOCUMENT("mes_pd_document", "MES è®¾è®¡èµ„æ–™");
yudao-server/pom.xml
@@ -150,6 +150,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- å”®åŽå·¥å•管理模块 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-aftersales</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- spring boot é…ç½®æ‰€éœ€ä¾èµ– -->
        <dependency>
            <groupId>org.springframework.boot</groupId>