feat: 补齐需求10/15/16/17/18/21/23,改用现有模块替代 ERP 依赖
## 问题
按验收计划逐项复测时确认以下缺口:
1. 需求10 退单管理、需求23 成本分析依赖 ERP 模块,而该模块在构建中被排除
(yudao-server DefaultController 对 ERP 路径返回 501),导致无法验收
2. 需求15 执行反馈只有报工与任务列表,缺设备运行数据、物料消耗、交期偏差
3. 需求16 质量追溯缺原材料合规证书与售后故障关联
4. 需求17 故障报修缺远程诊断能力
5. 需求18 OEE 缺停机明细报表
6. 需求21 风险监测只有人工录入当前值,未接设备采集
## 修复
### 需求10 退单管理(去 ERP)
- 新增 after_sale_cancellation 表与完整受理流程:
退货 / 合同变更 / 合同终止统一受理,统一审批
- 审批通过后按类型执行下游动作:
退货 → 生成售后退货单;合同终止 → 作废 CRM 合同(status=50)
- 关键设计:状态流转用乐观锁;下游动作结果写入 executed/executeRemark,
失败不静默吞掉,便于人工补做;驳回强制填意见
### 需求23 成本分析(去 ERP)
- 新增 SrmCostAnalysisService,改用 SRM 招标需求量 + 供应商报价
- 加权均价 = Σ(报价单价 × 需求数量) / Σ(需求数量)
- 输出采购量、加权均价、最低/最高价、报价次数、供应商数、预估价偏差率、价格变动
- 另提供按月价格趋势接口
### 需求15 执行反馈
- 新增 MesProTaskExecutionService:汇总计划/报工/设备采集/物料消耗/交期偏差
- 延期原因只列可观测信号(告警、缺料、报工滞后、耗时超计划),不推断管理原因
### 需求16 质量追溯
- srm-api 新增 SrmSupplierCertificateApi:按供应商查询合规证书(含过期标记)
- 批次追溯新增 /certificates(原材料合规证书)与 /aftersales(售后故障记录)
- aftersales-api 新增 getTicketsBySaleOrderNo,供 MES 反查售后
### 需求17 故障报修
- 新增 MesDvRepairDiagnosisService:基于实时采集 + 状态历史 + 告警做远程诊断
- 输出风险等级、诊断结论、逐条诊断依据;不引入新外部接口
### 需求18 OEE
- 新增停机明细报表:按采集间隔推算停机区间与合计,复用 CollectOfflinePolicy
### 需求21 风险监测
- 危险源新增 machineryId + metricKey,从 mes_collect_realtime 取值
- 新增 /safety/risk-source/refresh:批量刷新并按阈值判定,返回报警清单
- 取不到采集值时置 UNKNOWN,不覆盖历史观测时间
## 数据库
- after_sale_cancellation_20261107.sql:退单受理表 + 菜单权限
- safety_risk_source_20261107.sql:危险源台账表 + 菜单
- safety_risk_source_collect_20261107.sql:危险源加设备绑定列
- safety_hazard_employee_link_20261107.sql:补隐患表缺失的
reporter_id/assignee_id/rechecker_id(修复创建隐患 500)
- mes_work_order_schedule_priority/market_type、mdm_item_outsource_lead_time:
排产约束字段
- purchase_order_srm_menu、bpm_invoice_legacy_derived_from:菜单与数据迁移
## 验证
mvn compile EXIT=0;mvn test-compile EXIT=0。
本地复测(mom_local):
- 需求10 11/11 通过(含退货生成、合同作废、驳回路径)
- 需求23 6/6 通过(样本:钢木电缆盘 加权均价659.6)
- 需求13/15/17/18/21 21/21 通过
- 需求16/22 通过;需求20 未绑定账号场景 6/6 通过
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ç»ä¸éååçåï¼éè´§ / åååæ´ / ååç»æ¢ï¼èµ°å®¡æ¹éç¯ã |
| | | -- 说æï¼ä¸ä¾èµ ERP éå®è®¢åï¼ç¨ CRM ååä¸å®åéè´§åä½ä¸ºæ§è¡å¯¹è±¡ã |
| | | CREATE TABLE IF NOT EXISTS after_sale_cancellation ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', |
| | | code VARCHAR(64) NOT NULL COMMENT 'åçåå·', |
| | | type VARCHAR(32) NOT NULL COMMENT 'RETURN éè´§ / CONTRACT_CHANGE åååæ´ / CONTRACT_TERMINATE ååç»æ¢', |
| | | status TINYINT NOT NULL DEFAULT 0 COMMENT '0 è稿 1 å¾
å®¡æ¹ 2 å·²éè¿ 3 已驳å 4 已忶', |
| | | ticket_id BIGINT DEFAULT NULL COMMENT 'å
³èå®åå·¥å', |
| | | customer_id BIGINT DEFAULT NULL COMMENT '客æ·ç¼å·', |
| | | contract_id BIGINT DEFAULT NULL COMMENT 'CRM ååç¼å·', |
| | | sale_order_no VARCHAR(64) DEFAULT NULL COMMENT 'éå®è®¢åå·ï¼æåè®°å½ï¼æ ERP æ¶å¯ä¸ºç©ºï¼', |
| | | return_type TINYINT DEFAULT NULL COMMENT 'éè´§ç±»åï¼1 鿬¾éè´§ 2 æ¢è´§ 3 ä»
鿬¾', |
| | | reason VARCHAR(500) NOT NULL COMMENT 'ç³è¯·åå ', |
| | | expected_amount DECIMAL(20,6) DEFAULT NULL COMMENT 'é¢è®¡éé¢', |
| | | applicant_id BIGINT DEFAULT NULL COMMENT 'ç³è¯·äºº', |
| | | approver_id BIGINT DEFAULT NULL COMMENT '审æ¹äºº', |
| | | approval_time DATETIME DEFAULT NULL COMMENT 'å®¡æ¹æ¶é´', |
| | | approval_remark VARCHAR(500) DEFAULT NULL COMMENT 'å®¡æ¹æè§', |
| | | return_id BIGINT DEFAULT NULL COMMENT '审æ¹éè¿åçæçéè´§åç¼å·', |
| | | executed TINYINT NOT NULL DEFAULT 0 COMMENT 'æ¯å¦å·²æ§è¡ä¸æ¸¸å¨ä½ï¼0 æªæ§è¡ 1 å·²æ§è¡', |
| | | execute_remark VARCHAR(500) DEFAULT NULL COMMENT '䏿¸¸æ§è¡è¯´ææå¤±è´¥åå ', |
| | | remark VARCHAR(500) DEFAULT NULL, |
| | | tenant_id BIGINT NOT NULL DEFAULT 0, |
| | | creator VARCHAR(64) DEFAULT '', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | updater VARCHAR(64) DEFAULT '', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | deleted BIT(1) NOT NULL DEFAULT b'0', |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_after_sale_cancellation_code (code), |
| | | KEY idx_after_sale_cancellation_status (status, deleted), |
| | | KEY idx_after_sale_cancellation_ticket (ticket_id, deleted), |
| | | KEY idx_after_sale_cancellation_contract (contract_id, deleted) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='å®åç»ä¸éå/åæ´åç'; |
| | | |
| | | -- èåï¼æå¨å®åæå¡ä¸ |
| | | SET @as_parent := (SELECT id FROM system_menu WHERE name='å®åæå¡' AND parent_id=0 AND CAST(deleted AS UNSIGNED)=0 LIMIT 1); |
| | | INSERT INTO system_menu (name,permission,type,sort,parent_id,path,icon,component,component_name,status,visible,keep_alive,always_show,creator,updater) |
| | | SELECT 'éå管ç','',2,90,@as_parent,'cancellation','lucide:undo-2','aftersales/cancellation/index','AfterSaleCancellation',0,b'1',b'1',b'1','1','1' |
| | | WHERE @as_parent IS NOT NULL AND NOT EXISTS (SELECT 1 FROM system_menu WHERE component='aftersales/cancellation/index' AND CAST(deleted AS UNSIGNED)=0); |
| | | |
| | | SET @pg := (SELECT id FROM system_menu WHERE component='aftersales/cancellation/index' AND CAST(deleted AS UNSIGNED)=0 LIMIT 1); |
| | | INSERT INTO system_menu (name,permission,type,sort,parent_id,path,icon,status,visible,keep_alive,always_show,creator,updater) |
| | | SELECT p.name,p.permission,3,p.sort_no,@pg,'','',0,b'1',b'1',b'1','1','1' |
| | | FROM (SELECT 'æ¥è¯¢' name,'aftersales:cancellation:query' permission,1 sort_no |
| | | UNION ALL SELECT 'æ°å¢','aftersales:cancellation:create',2 |
| | | UNION ALL SELECT 'æäº¤','aftersales:cancellation:submit',3 |
| | | UNION ALL SELECT '审æ¹','aftersales:cancellation:approve',4 |
| | | UNION ALL SELECT 'åæ¶','aftersales:cancellation:cancel',5 |
| | | UNION ALL SELECT 'å é¤','aftersales:cancellation:delete',6) p |
| | | WHERE @pg IS NOT NULL AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id=@pg AND m.permission=p.permission AND CAST(m.deleted AS UNSIGNED)=0); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ä»
ä¿®æ£æ§çæ¥ç¥¨å®¡æ¹å®ä¹ä¸ä»¥ç©ºå符串ä¿åçæ´¾çæ¥æºã |
| | | -- 空åç¬¦ä¸²ä¸æ¯ææçæ´¾çæµç¨ç¼å·ï¼Flowable æ¥è¯¢æ®éæµç¨æ¶è¦æ±è¯¥å段为 NULLã |
| | | -- æ§è¡ååºå
å¤ä»½å¹¶ç¡®è®¤ç®æ åºãè®°å½ä»¥åå岿µç¨çä¸å¡å½å±ã |
| | | UPDATE act_re_procdef |
| | | SET DERIVED_FROM_ = NULL |
| | | WHERE KEY_ = 'erp-purchase-invoice-audit' |
| | | AND VERSION_ = 1 |
| | | AND DERIVED_VERSION_ = 0 |
| | | AND DERIVED_FROM_ = ''; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ç©æå¤åæåæç±ä¸å¡äººåç»´æ¤ï¼ç©ºå¼è¡¨ç¤ºæªé
ç½®ã |
| | | SET @has_outsource_lead_time := (SELECT COUNT(*) FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdm_item' AND COLUMN_NAME='outsource_lead_time'); |
| | | SET @lead_sql := IF(@has_outsource_lead_time=0, |
| | | 'ALTER TABLE mdm_item ADD COLUMN outsource_lead_time INT NULL COMMENT ''å¤åæåæï¼å¤©ï¼''', |
| | | 'SELECT 1'); |
| | | PREPARE lead_stmt FROM @lead_sql; |
| | | EXECUTE lead_stmt; |
| | | DEALLOCATE PREPARE lead_stmt; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- å
å¤éåç±»ç±ä¸å¡äººåç»´æ¤ï¼ä¸æ ¹æ®å®¢æ·åç§°æ¨æã |
| | | SET @has_market_type := (SELECT COUNT(*) FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mes_pro_work_order' AND COLUMN_NAME='market_type'); |
| | | SET @market_sql := IF(@has_market_type=0, |
| | | 'ALTER TABLE mes_pro_work_order ADD COLUMN market_type VARCHAR(16) NULL COMMENT ''å¸åºç±»å DOMESTIC/EXPORT''', |
| | | 'SELECT 1'); |
| | | PREPARE market_stmt FROM @market_sql; |
| | | EXECUTE market_stmt; |
| | | DEALLOCATE PREPARE market_stmt; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- æå®é
ä¸å¡ç»´æ¤æäº§ä¼å
çº§ï¼æ°åè¶å¤§è¶å
æï¼åå²å·¥åä¿æ 0ã |
| | | SET @has_schedule_priority := (SELECT COUNT(*) FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mes_pro_work_order' AND COLUMN_NAME='schedule_priority'); |
| | | SET @priority_sql := IF(@has_schedule_priority=0, |
| | | 'ALTER TABLE mes_pro_work_order ADD COLUMN schedule_priority INT NOT NULL DEFAULT 0 COMMENT ''æäº§ä¼å
çº§ï¼æ°åè¶å¤§è¶ä¼å
''', |
| | | 'SELECT 1'); |
| | | PREPARE priority_stmt FROM @priority_sql; |
| | | EXECUTE priority_stmt; |
| | | DEALLOCATE PREPARE priority_stmt; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- åéè´ä¸çº§èååç¨æ¶ï¼å°åéè´è®¢ååå
¶æé®å½å
¥ç°æä»å¨ç©æµ/SRM ç®å½ã |
| | | -- ä¸å建ä¸çº§èåï¼ä¸æäºæ°çéè´ä¸å¡æéï¼ç®æ ä¸åå¨æè·¯å¾å²çªæ¶ä¸è¿ç§»ã |
| | | SET @old_purchase := (SELECT id FROM system_menu WHERE parent_id=0 AND path='/purchase' AND type=1 AND status=1 AND deleted=b'0' LIMIT 1); |
| | | SET @warehouse := (SELECT id FROM system_menu WHERE parent_id=0 AND path='/wls' AND type=1 AND status=0 AND deleted=b'0' LIMIT 1); |
| | | SET @srm := (SELECT id FROM system_menu WHERE parent_id=@warehouse AND path='srm' AND type=1 AND status=0 AND deleted=b'0' LIMIT 1); |
| | | SET @order := (SELECT id FROM system_menu WHERE parent_id=@old_purchase AND component='erp/purchase/order/index' AND type=2 AND deleted=b'0' LIMIT 1); |
| | | SET @conflict := (SELECT COUNT(*) FROM system_menu WHERE parent_id=@srm AND path='order' AND deleted=b'0' AND id<>IFNULL(@order,0)); |
| | | UPDATE system_menu SET parent_id=@srm, updater='1', update_time=NOW() |
| | | WHERE id=@order AND @srm IS NOT NULL AND @conflict=0; |
| | | INSERT INTO system_role_menu (role_id,menu_id,creator,updater,tenant_id) |
| | | SELECT DISTINCT rm.role_id, parents.id, '1','1',rm.tenant_id |
| | | FROM system_role_menu rm JOIN system_menu parents ON parents.id IN (@warehouse,@srm) |
| | | WHERE rm.menu_id=@order AND rm.deleted=b'0' AND @srm IS NOT NULL AND @conflict=0 |
| | | AND NOT EXISTS (SELECT 1 FROM system_role_menu e WHERE e.role_id=rm.role_id AND e.tenant_id=rm.tenant_id AND e.menu_id=parents.id AND e.deleted=b'0'); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- 鿣å°è´¦è¡¥é½åå·¥å
³èåï¼reporter_id / assignee_id / rechecker_idï¼ |
| | | -- èæ¯ï¼safety_hazard æ©æå»ºè¡¨éæ¼äºè¿ä¸ä¸ªå
³èåï¼å¯¼è´éæ£å建/åæ´¾æ¥å£ 500ã |
| | | -- éç¨ DO åæ®µ -> ååæ å°ï¼å¹çæ§è¡ã |
| | | |
| | | SET @c1 := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_hazard' AND COLUMN_NAME='reporter_id'); |
| | | SET @s1 := IF(@c1=0, 'ALTER TABLE safety_hazard ADD COLUMN reporter_id BIGINT DEFAULT NULL COMMENT ''䏿¥äººï¼hrm_employee.idï¼'' AFTER photo_urls', 'SELECT 1'); |
| | | PREPARE a1 FROM @s1; EXECUTE a1; DEALLOCATE PREPARE a1; |
| | | |
| | | SET @c2 := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_hazard' AND COLUMN_NAME='assignee_id'); |
| | | SET @s2 := IF(@c2=0, 'ALTER TABLE safety_hazard ADD COLUMN assignee_id BIGINT DEFAULT NULL COMMENT ''æ´æ¹è´£ä»»äººï¼hrm_employee.idï¼'' AFTER report_time', 'SELECT 1'); |
| | | PREPARE a2 FROM @s2; EXECUTE a2; DEALLOCATE PREPARE a2; |
| | | |
| | | SET @c3 := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_hazard' AND COLUMN_NAME='rechecker_id'); |
| | | SET @s3 := IF(@c3=0, 'ALTER TABLE safety_hazard ADD COLUMN rechecker_id BIGINT DEFAULT NULL COMMENT ''夿¥äººï¼hrm_employee.idï¼'' AFTER rectify_time', 'SELECT 1'); |
| | | PREPARE a3 FROM @s3; EXECUTE a3; DEALLOCATE PREPARE a3; |
| | | |
| | | SET @i1 := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_hazard' AND INDEX_NAME='idx_safety_hazard_reporter'); |
| | | SET @s4 := IF(@i1=0, 'ALTER TABLE safety_hazard ADD KEY idx_safety_hazard_reporter (reporter_id, deleted)', 'SELECT 1'); |
| | | PREPARE a4 FROM @s4; EXECUTE a4; DEALLOCATE PREPARE a4; |
| | | |
| | | SET @i2 := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_hazard' AND INDEX_NAME='idx_safety_hazard_assignee'); |
| | | SET @s5 := IF(@i2=0, 'ALTER TABLE safety_hazard ADD KEY idx_safety_hazard_assignee (assignee_id, status, deleted)', 'SELECT 1'); |
| | | PREPARE a5 FROM @s5; EXECUTE a5; DEALLOCATE PREPARE a5; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- 人工维æ¤å±é©æºå½åå¼ï¼ä¸è¡¨ç¤ºå·²æ¥å
¥èªå¨æ°éï¼ä¹ä¸ä¿çåå²è§æµåºåã |
| | | CREATE TABLE IF NOT EXISTS safety_risk_source ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, |
| | | name VARCHAR(120) NOT NULL, category VARCHAR(24) NOT NULL, location VARCHAR(160) DEFAULT NULL, |
| | | metric_name VARCHAR(80) NOT NULL, unit VARCHAR(32) DEFAULT NULL, |
| | | lower_limit DECIMAL(20,6) DEFAULT NULL, upper_limit DECIMAL(20,6) DEFAULT NULL, |
| | | current_value DECIMAL(20,6) DEFAULT NULL, observed_at DATETIME DEFAULT NULL, |
| | | risk_status VARCHAR(16) NOT NULL DEFAULT 'UNKNOWN', remark VARCHAR(500) DEFAULT NULL, |
| | | tenant_id BIGINT NOT NULL DEFAULT 0, |
| | | creator VARCHAR(64) DEFAULT '', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | updater VARCHAR(64) DEFAULT '', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | deleted BIT(1) NOT NULL DEFAULT b'0', KEY idx_safety_risk_status (risk_status,deleted) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='å±é©æºäººå·¥çæµå°è´¦'; |
| | | SET @risk_parent := (SELECT id FROM system_menu WHERE parent_id=0 AND path='/safety' AND type=1 AND deleted=b'0' LIMIT 1); |
| | | INSERT INTO system_menu (name,permission,type,sort,parent_id,path,icon,component,component_name,status,visible,keep_alive,always_show,creator,updater) |
| | | SELECT 'å±é©æºå°è´¦','',2,36,@risk_parent,'risk-source','lucide:triangle-alert','safety/risk-source/index','SafetyRiskSource',0,b'1',b'1',b'1','1','1' |
| | | WHERE @risk_parent IS NOT NULL AND NOT EXISTS (SELECT 1 FROM system_menu WHERE component='safety/risk-source/index' AND deleted=b'0'); |
| | | SET @risk_page := (SELECT id FROM system_menu WHERE component='safety/risk-source/index' AND deleted=b'0' LIMIT 1); |
| | | INSERT INTO system_menu (name,permission,type,sort,parent_id,path,icon,status,visible,keep_alive,always_show,creator,updater) |
| | | SELECT p.name,p.permission,3,p.sort_no,@risk_page,'','',0,b'1',b'1',b'1','1','1' |
| | | FROM (SELECT 'æ¥è¯¢' name,'safety:risk:query' permission,1 sort_no UNION ALL SELECT 'æ°å¢','safety:risk:create',2 UNION ALL SELECT 'ä¿®æ¹','safety:risk:update',3 UNION ALL SELECT 'å é¤','safety:risk:delete',4) p |
| | | WHERE @risk_page IS NOT NULL AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id=@risk_page AND m.permission=p.permission AND m.deleted=b'0'); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- å±é©æºæ¥å
¥è®¾å¤ééï¼ç»å®è®¾å¤å°è´¦ä¸æ°éææ é® |
| | | -- ç»å®åç± POST /safety/risk-source/refresh ä» mes_collect_realtime åå¼å¹¶æéå¼å¤å®ï¼ |
| | | -- ä¸è¦ç人工å½å
¥æ¨¡å¼ï¼machinery_id 为空æ¶ä»ä»¥ current_value 为åï¼ã |
| | | |
| | | SET @has_machinery := (SELECT COUNT(*) FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_risk_source' AND COLUMN_NAME='machinery_id'); |
| | | SET @s := IF(@has_machinery=0, |
| | | 'ALTER TABLE safety_risk_source ADD COLUMN machinery_id BIGINT DEFAULT NULL COMMENT ''å
³è设å¤å°è´¦ç¼å·ï¼ä¸ºç©ºè¡¨ç¤ºäººå·¥å½å
¥'' AFTER metric_name', |
| | | 'SELECT 1'); |
| | | PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; |
| | | |
| | | SET @has_metric_key := (SELECT COUNT(*) FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_risk_source' AND COLUMN_NAME='metric_key'); |
| | | SET @s2 := IF(@has_metric_key=0, |
| | | 'ALTER TABLE safety_risk_source ADD COLUMN metric_key VARCHAR(64) DEFAULT NULL COMMENT ''æ°éææ é®ï¼data_json åæ®µåï¼'' AFTER machinery_id', |
| | | 'SELECT 1'); |
| | | PREPARE st2 FROM @s2; EXECUTE st2; DEALLOCATE PREPARE st2; |
| | | |
| | | SET @has_idx := (SELECT COUNT(*) FROM information_schema.STATISTICS |
| | | WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_risk_source' AND INDEX_NAME='idx_safety_risk_machinery'); |
| | | SET @s3 := IF(@has_idx=0, |
| | | 'ALTER TABLE safety_risk_source ADD KEY idx_safety_risk_machinery (machinery_id, deleted)', |
| | | 'SELECT 1'); |
| | | PREPARE st3 FROM @s3; EXECUTE st3; DEALLOCATE PREPARE st3; |
| | |
| | | import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleTicketRespDTO; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * å®åå·¥å API æ¥å£ï¼ä¾ MES/ERP 跨模åè°ç¨ï¼ |
| | |
| | | */ |
| | | CommonResult<AfterSaleReturnRespDTO> getReturn(Long id); |
| | | |
| | | /** |
| | | * æéå®è®¢åå·æ¥è¯¢å®åå·¥åï¼ä¾ MES è´¨é追溯å
³èå®åæ
éè®°å½ï¼ |
| | | * |
| | | * @param saleOrderNo éå®è®¢åå· |
| | | * @return å·¥ååè¡¨ï¼æ å
³èæ¶ä¸ºç©ºå表 |
| | | */ |
| | | CommonResult<List<AfterSaleTicketRespDTO>> getTicketsBySaleOrderNo(String saleOrderNo); |
| | | |
| | | } |
| | |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | |
| | |
| | | return success(BeanUtils.toBean(returnDO, AfterSaleReturnRespDTO.class)); |
| | | } |
| | | |
| | | @Override |
| | | public CommonResult<List<AfterSaleTicketRespDTO>> getTicketsBySaleOrderNo(String saleOrderNo) { |
| | | if (saleOrderNo == null || saleOrderNo.isBlank()) return success(List.of()); |
| | | List<AfterSaleTicketDO> list = ticketService.getTicketListBySaleOrderNo(saleOrderNo); |
| | | return success(list.stream().map(t -> BeanUtils.toBean(t, AfterSaleTicketRespDTO.class)).toList()); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.controller.admin.cancellation; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.module.aftersales.controller.admin.cancellation.vo.AfterSaleCancellationApproveReqVO; |
| | | import cn.iocoder.yudao.module.aftersales.controller.admin.cancellation.vo.AfterSaleCancellationSaveReqVO; |
| | | import cn.iocoder.yudao.module.aftersales.dal.dataobject.cancellation.AfterSaleCancellationDO; |
| | | import cn.iocoder.yudao.module.aftersales.service.cancellation.AfterSaleCancellationService; |
| | | 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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | |
| | | @Tag(name = "管çåå° - å®åéå/åæ´åç") |
| | | @RestController |
| | | @RequestMapping("/aftersales/cancellation") |
| | | @Validated |
| | | public class AfterSaleCancellationController { |
| | | |
| | | @Resource |
| | | private AfterSaleCancellationService service; |
| | | |
| | | @GetMapping("/page") |
| | | @Operation(summary = "è·å¾éå/åæ´åçå页") |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:query')") |
| | | public CommonResult<PageResult<AfterSaleCancellationDO>> page( |
| | | @Valid PageParam page, |
| | | @RequestParam(value = "code", required = false) String code, |
| | | @RequestParam(value = "type", required = false) String type, |
| | | @RequestParam(value = "status", required = false) Integer status, |
| | | @RequestParam(value = "customerId", required = false) Long customerId) { |
| | | return success(service.page(page, code, type, status, customerId)); |
| | | } |
| | | |
| | | @GetMapping("/get") |
| | | @Operation(summary = "è·å¾éå/åæ´åç") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:query')") |
| | | public CommonResult<AfterSaleCancellationDO> get(@RequestParam("id") Long id) { |
| | | return success(service.get(id)); |
| | | } |
| | | |
| | | @PostMapping("/create") |
| | | @Operation(summary = "å建éå/åæ´åç") |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:create')") |
| | | public CommonResult<Long> create(@Valid @RequestBody AfterSaleCancellationSaveReqVO req) { |
| | | req.setId(null); |
| | | return success(service.create(req, getLoginUserId())); |
| | | } |
| | | |
| | | @PutMapping("/update") |
| | | @Operation(summary = "ä¿®æ¹éå/åæ´åçï¼ä»
è稿ï¼") |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:update')") |
| | | public CommonResult<Boolean> update(@Valid @RequestBody AfterSaleCancellationSaveReqVO req) { |
| | | service.update(req); |
| | | return success(true); |
| | | } |
| | | |
| | | @PutMapping("/submit") |
| | | @Operation(summary = "æäº¤å®¡æ¹") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:submit')") |
| | | public CommonResult<Boolean> submit(@RequestParam("id") Long id) { |
| | | service.submit(id); |
| | | return success(true); |
| | | } |
| | | |
| | | @PutMapping("/approve") |
| | | @Operation(summary = "审æ¹ï¼éè¿åæ§è¡ä¸æ¸¸å¨ä½ï¼") |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:approve')") |
| | | public CommonResult<Long> approve(@Valid @RequestBody AfterSaleCancellationApproveReqVO req) { |
| | | return success(service.approve(req, getLoginUserId())); |
| | | } |
| | | |
| | | @PutMapping("/cancel") |
| | | @Operation(summary = "åæ¶åç") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:cancel')") |
| | | public CommonResult<Boolean> cancel(@RequestParam("id") Long id) { |
| | | service.cancel(id); |
| | | return success(true); |
| | | } |
| | | |
| | | @DeleteMapping("/delete") |
| | | @Operation(summary = "å é¤éå/åæ´åçï¼ä»
è稿ï¼") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('aftersales:cancellation:delete')") |
| | | public CommonResult<Boolean> delete(@RequestParam("id") Long id) { |
| | | service.delete(id); |
| | | return success(true); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.controller.admin.cancellation.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | import lombok.Data; |
| | | |
| | | @Schema(description = "管çåå° - å®åéå/åæ´åç å®¡æ¹ Request VO") |
| | | @Data |
| | | public class AfterSaleCancellationApproveReqVO { |
| | | |
| | | @Schema(description = "åçåç¼å·", requiredMode = Schema.RequiredMode.REQUIRED) |
| | | @NotNull(message = "åçåç¼å·ä¸è½ä¸ºç©º") |
| | | private Long id; |
| | | |
| | | @Schema(description = "æ¯å¦éè¿", requiredMode = Schema.RequiredMode.REQUIRED) |
| | | @NotNull(message = "审æ¹ç»è®ºä¸è½ä¸ºç©º") |
| | | private Boolean approved; |
| | | |
| | | @Schema(description = "å®¡æ¹æè§ï¼é©³åæ¶å¿
å¡«ï¼") |
| | | @Size(max = 500) |
| | | private String remark; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.controller.admin.cancellation.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import jakarta.validation.constraints.*; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Schema(description = "管çåå° - å®åéå/åæ´åç ä¿å Request VO") |
| | | @Data |
| | | public class AfterSaleCancellationSaveReqVO { |
| | | |
| | | private Long id; |
| | | |
| | | @Schema(description = "ç±»åï¼RETURN éè´§ / CONTRACT_CHANGE åååæ´ / CONTRACT_TERMINATE ååç»æ¢", |
| | | requiredMode = Schema.RequiredMode.REQUIRED) |
| | | @NotBlank |
| | | @Pattern(regexp = "RETURN|CONTRACT_CHANGE|CONTRACT_TERMINATE", message = "åçç±»åä¸åæ³") |
| | | private String type; |
| | | |
| | | @Schema(description = "å
³èå®åå·¥åç¼å·") |
| | | private Long ticketId; |
| | | |
| | | @Schema(description = "客æ·ç¼å·") |
| | | private Long customerId; |
| | | |
| | | @Schema(description = "CRM ååç¼å·") |
| | | private Long contractId; |
| | | |
| | | @Schema(description = "éå®è®¢åå·ï¼æ ERP æ¶å¯ä¸ºç©ºï¼") |
| | | @Size(max = 64) |
| | | private String saleOrderNo; |
| | | |
| | | @Schema(description = "éè´§ç±»åï¼1 鿬¾éè´§ 2 æ¢è´§ 3 ä»
鿬¾") |
| | | private Integer returnType; |
| | | |
| | | @Schema(description = "ç³è¯·åå ", requiredMode = Schema.RequiredMode.REQUIRED) |
| | | @NotBlank(message = "ç³è¯·åå ä¸è½ä¸ºç©º") |
| | | @Size(max = 500) |
| | | private String reason; |
| | | |
| | | @Schema(description = "é¢è®¡éé¢") |
| | | @Digits(integer = 14, fraction = 6) |
| | | private BigDecimal expectedAmount; |
| | | |
| | | @Schema(description = "夿³¨") |
| | | @Size(max = 500) |
| | | private String remark; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.dal.dataobject.cancellation; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * å®åç»ä¸éå/åæ´åç DOã |
| | | * |
| | | * <p>æã客æ·éè´§ãåååæ´ãååç»æ¢ãæ¶æå°åä¸å¼ åçåï¼ |
| | | * ç»ä¸èµ°å®¡æ¹ï¼å®¡æ¹éè¿åæç±»åæ§è¡ä¸æ¸¸å¨ä½ï¼ |
| | | * éè´§çæå®åéè´§åï¼ååç»æ¢ä½åº CRM ååã |
| | | */ |
| | | @TableName("after_sale_cancellation") |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | public class AfterSaleCancellationDO extends BaseDO { |
| | | |
| | | @TableId |
| | | private Long id; |
| | | /** åçåå· */ |
| | | private String code; |
| | | /** ç±»åï¼RETURN / CONTRACT_CHANGE / CONTRACT_TERMINATE */ |
| | | private String type; |
| | | /** ç¶æï¼0 è稿 1 å¾
å®¡æ¹ 2 å·²éè¿ 3 已驳å 4 已忶 */ |
| | | private Integer status; |
| | | private Long ticketId; |
| | | private Long customerId; |
| | | private Long contractId; |
| | | private String saleOrderNo; |
| | | /** éè´§ç±»åï¼1 鿬¾éè´§ 2 æ¢è´§ 3 ä»
鿬¾ */ |
| | | private Integer returnType; |
| | | private String reason; |
| | | private BigDecimal expectedAmount; |
| | | private Long applicantId; |
| | | private Long approverId; |
| | | private LocalDateTime approvalTime; |
| | | private String approvalRemark; |
| | | /** 审æ¹éè¿åçæçéè´§åç¼å· */ |
| | | private Long returnId; |
| | | /** 䏿¸¸å¨ä½æ¯å¦å·²æ§è¡ï¼0 æªæ§è¡ 1 å·²æ§è¡ */ |
| | | private Integer executed; |
| | | /** 䏿¸¸æ§è¡è¯´ææå¤±è´¥åå */ |
| | | private String executeRemark; |
| | | private String remark; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.dal.mysql.cancellation; |
| | | |
| | | 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.cancellation.AfterSaleCancellationDO; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface AfterSaleCancellationMapper extends BaseMapperX<AfterSaleCancellationDO> { |
| | | |
| | | default AfterSaleCancellationDO selectByCode(String code) { |
| | | return selectOne(AfterSaleCancellationDO::getCode, code); |
| | | } |
| | | |
| | | default PageResult<AfterSaleCancellationDO> selectPage(PageParam pageParam, String code, String type, |
| | | Integer status, Long customerId) { |
| | | return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleCancellationDO>() |
| | | .likeIfPresent(AfterSaleCancellationDO::getCode, code) |
| | | .eqIfPresent(AfterSaleCancellationDO::getType, type) |
| | | .eqIfPresent(AfterSaleCancellationDO::getStatus, status) |
| | | .eqIfPresent(AfterSaleCancellationDO::getCustomerId, customerId) |
| | | .orderByDesc(AfterSaleCancellationDO::getId)); |
| | | } |
| | | } |
| | |
| | | return selectCount(AfterSaleTicketDO::getOwnerUserId, ownerUserId); |
| | | } |
| | | |
| | | /** æéå®è®¢åå·æ¥è¯¢å·¥åï¼è´¨é追溯å
³èå®åæ
éè®°å½ï¼ */ |
| | | default List<AfterSaleTicketDO> selectListBySaleOrderNo(String saleOrderNo) { |
| | | return selectList(new LambdaQueryWrapperX<AfterSaleTicketDO>() |
| | | .eq(AfterSaleTicketDO::getSaleOrderNo, saleOrderNo) |
| | | .orderByDesc(AfterSaleTicketDO::getId)); |
| | | } |
| | | |
| | | } |
| | |
| | | public static final String RETURN_NO_PREFIX = "RT"; |
| | | |
| | | /** |
| | | * éå/åæ´åç {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.cancellation.AfterSaleCancellationDO} |
| | | */ |
| | | public static final String CANCELLATION_NO_PREFIX = "CX"; |
| | | |
| | | /** |
| | | * å®åé®é¢åº {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO} |
| | | */ |
| | | public static final String PROBLEM_NO_PREFIX = "AP"; |
| | |
| | | ErrorCode PROBLEM_PUBLISH_FAIL_STATUS = new ErrorCode(1_022_005_004, "é®é¢åå¸å¤±è´¥ï¼åå ï¼å½åç¶æä¸å¯åå¸"); |
| | | ErrorCode PROBLEM_DISABLE_FAIL_STATUS = new ErrorCode(1_022_005_005, "é®é¢åç¨å¤±è´¥ï¼åå ï¼å½åç¶æä¸å¯åç¨"); |
| | | |
| | | // ========== å®åç»ä¸éå/åæ´åç 1-022-006-000 ========== |
| | | ErrorCode CANCELLATION_NOT_EXISTS = new ErrorCode(1_022_006_000, "åçåä¸åå¨"); |
| | | ErrorCode CANCELLATION_UPDATE_FAIL_STATUS = new ErrorCode(1_022_006_001, "åçåä¿®æ¹å¤±è´¥ï¼åå ï¼ä»
èç¨¿ç¶æå¯ç¼è¾"); |
| | | ErrorCode CANCELLATION_SUBMIT_FAIL_STATUS = new ErrorCode(1_022_006_002, "åçåæäº¤å¤±è´¥ï¼åå ï¼ä»
èç¨¿ç¶æå¯æäº¤"); |
| | | ErrorCode CANCELLATION_APPROVE_FAIL_STATUS = new ErrorCode(1_022_006_003, "åçå审æ¹å¤±è´¥ï¼åå ï¼ä»
å¾
审æ¹ç¶æå¯å®¡æ¹"); |
| | | ErrorCode CANCELLATION_CANCEL_FAIL_STATUS = new ErrorCode(1_022_006_004, "åçååæ¶å¤±è´¥ï¼åå ï¼å½åç¶æä¸å¯åæ¶"); |
| | | ErrorCode CANCELLATION_APPROVE_REMARK_REQUIRED = new ErrorCode(1_022_006_005, "驳忶å¿
须填åå®¡æ¹æè§"); |
| | | ErrorCode CANCELLATION_RETURN_TICKET_REQUIRED = new ErrorCode(1_022_006_006, "éè´§ç±»ååçåå¿
é¡»å
³èå®åå·¥å"); |
| | | ErrorCode CANCELLATION_CONTRACT_REQUIRED = new ErrorCode(1_022_006_007, "åååæ´/ç»æ¢å¿
须鿩 CRM åå"); |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.service.cancellation; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import cn.iocoder.yudao.module.aftersales.controller.admin.cancellation.vo.AfterSaleCancellationApproveReqVO; |
| | | import cn.iocoder.yudao.module.aftersales.controller.admin.cancellation.vo.AfterSaleCancellationSaveReqVO; |
| | | import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO; |
| | | import cn.iocoder.yudao.module.aftersales.dal.dataobject.cancellation.AfterSaleCancellationDO; |
| | | import cn.iocoder.yudao.module.aftersales.dal.mysql.cancellation.AfterSaleCancellationMapper; |
| | | import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO; |
| | | import cn.iocoder.yudao.module.aftersales.service.returnobj.AfterSaleReturnService; |
| | | import cn.iocoder.yudao.module.crm.api.contract.CrmContractApi; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.dao.DuplicateKeyException; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.Objects; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | | * å®åç»ä¸éå/åæ´åçã |
| | | * |
| | | * <p>æã客æ·éè´§ãåååæ´ãååç»æ¢ãæ¶æä¸ºåä¸åçæµç¨ï¼ä¸ä¾èµ ERP éå®è®¢åï¼ |
| | | * <ul> |
| | | * <li>éè´§ï¼å®¡æ¹éè¿åçæå®åéè´§åï¼å¤ç¨ {@link AfterSaleReturnService}ï¼ï¼</li> |
| | | * <li>ååç»æ¢ï¼å®¡æ¹éè¿åä½åº CRM ååï¼status=50ï¼ï¼</li> |
| | | * <li>åååæ´ï¼CRM æ ãåæ´ä¸ãç¶æï¼å®¡æ¹éè¿ä»
è½å°åçè®°å½ï¼åæ´å
容å¨åå夿³¨æéä»¶ä½ç°ã</li> |
| | | * </ul> |
| | | * 䏿¸¸å¨ä½æ§è¡ç»æåå
¥ executed / executeRemarkï¼ä¾¿äºå¤±è´¥å人工补åï¼ä¸éé»åæé误ã |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class AfterSaleCancellationService { |
| | | |
| | | /** ç¶æï¼0 è稿 1 å¾
å®¡æ¹ 2 å·²éè¿ 3 已驳å 4 已忶 */ |
| | | private static final int ST_DRAFT = 0; |
| | | private static final int ST_PENDING = 1; |
| | | private static final int ST_APPROVED = 2; |
| | | private static final int ST_REJECTED = 3; |
| | | private static final int ST_CANCELLED = 4; |
| | | |
| | | /** CRM ååä½åºç¶æ */ |
| | | private static final int CRM_CONTRACT_INVALID = 50; |
| | | |
| | | @Resource private AfterSaleCancellationMapper mapper; |
| | | @Resource private AfterSaleNoRedisDAO noRedisDAO; |
| | | @Resource private AfterSaleReturnService returnService; |
| | | @Resource private CrmContractApi crmContractApi; |
| | | |
| | | public PageResult<AfterSaleCancellationDO> page(PageParam page, String code, String type, |
| | | Integer status, Long customerId) { |
| | | return mapper.selectPage(page, code, type, status, customerId); |
| | | } |
| | | |
| | | public AfterSaleCancellationDO get(Long id) { |
| | | AfterSaleCancellationDO row = id == null ? null : mapper.selectById(id); |
| | | if (row == null) throw exception(CANCELLATION_NOT_EXISTS); |
| | | return row; |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long create(AfterSaleCancellationSaveReqVO req, Long userId) { |
| | | validateByType(req); |
| | | var row = BeanUtils.toBean(req, AfterSaleCancellationDO.class); |
| | | row.setStatus(ST_DRAFT); |
| | | row.setApplicantId(userId); |
| | | row.setExecuted(0); |
| | | row.setCode(noRedisDAO.generate(AfterSaleNoRedisDAO.CANCELLATION_NO_PREFIX)); |
| | | try { |
| | | mapper.insert(row); |
| | | } catch (DuplicateKeyException e) { |
| | | throw exception(CANCELLATION_NOT_EXISTS); |
| | | } |
| | | return row.getId(); |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void update(AfterSaleCancellationSaveReqVO req) { |
| | | AfterSaleCancellationDO old = get(req.getId()); |
| | | if (!Objects.equals(old.getStatus(), ST_DRAFT)) throw exception(CANCELLATION_UPDATE_FAIL_STATUS); |
| | | validateByType(req); |
| | | var update = new AfterSaleCancellationDO(); |
| | | BeanUtils.copyProperties(req, update); |
| | | update.setId(old.getId()); |
| | | update.setCode(old.getCode()); |
| | | update.setStatus(ST_DRAFT); |
| | | mapper.updateById(update); |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submit(Long id) { |
| | | AfterSaleCancellationDO old = get(id); |
| | | if (!Objects.equals(old.getStatus(), ST_DRAFT)) throw exception(CANCELLATION_SUBMIT_FAIL_STATUS); |
| | | if (mapper.update(null, new LambdaUpdateWrapper<AfterSaleCancellationDO>() |
| | | .eq(AfterSaleCancellationDO::getId, id) |
| | | .eq(AfterSaleCancellationDO::getStatus, ST_DRAFT) |
| | | .set(AfterSaleCancellationDO::getStatus, ST_PENDING)) != 1) { |
| | | throw exception(CANCELLATION_SUBMIT_FAIL_STATUS); |
| | | } |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void cancel(Long id) { |
| | | AfterSaleCancellationDO old = get(id); |
| | | if (!Objects.equals(old.getStatus(), ST_DRAFT) && !Objects.equals(old.getStatus(), ST_PENDING)) { |
| | | throw exception(CANCELLATION_CANCEL_FAIL_STATUS); |
| | | } |
| | | if (mapper.update(null, new LambdaUpdateWrapper<AfterSaleCancellationDO>() |
| | | .eq(AfterSaleCancellationDO::getId, id) |
| | | .in(AfterSaleCancellationDO::getStatus, ST_DRAFT, ST_PENDING) |
| | | .set(AfterSaleCancellationDO::getStatus, ST_CANCELLED)) != 1) { |
| | | throw exception(CANCELLATION_CANCEL_FAIL_STATUS); |
| | | } |
| | | } |
| | | |
| | | public void delete(Long id) { |
| | | AfterSaleCancellationDO old = get(id); |
| | | if (!Objects.equals(old.getStatus(), ST_DRAFT)) throw exception(CANCELLATION_UPDATE_FAIL_STATUS); |
| | | mapper.deleteById(id); |
| | | } |
| | | |
| | | @javax.annotation.Nullable |
| | | public Long getReturnId(Long id) { |
| | | AfterSaleCancellationDO row = mapper.selectById(id); |
| | | return row == null ? null : row.getReturnId(); |
| | | } |
| | | |
| | | /** |
| | | * 审æ¹ï¼éè¿åæ§è¡ä¸æ¸¸å¨ä½ï¼é©³åå¿
须填æè§ã |
| | | * |
| | | * @return éè¿æ¶çæçéè´§åç¼å·ï¼ééè´§ç±»å为 nullï¼ |
| | | */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long approve(AfterSaleCancellationApproveReqVO req, Long userId) { |
| | | if (!Boolean.TRUE.equals(req.getApproved()) |
| | | && (req.getRemark() == null || req.getRemark().isBlank())) { |
| | | throw exception(CANCELLATION_APPROVE_REMARK_REQUIRED); |
| | | } |
| | | AfterSaleCancellationDO task = get(req.getId()); |
| | | if (!Objects.equals(task.getStatus(), ST_PENDING)) throw exception(CANCELLATION_APPROVE_FAIL_STATUS); |
| | | |
| | | int target = Boolean.TRUE.equals(req.getApproved()) ? ST_APPROVED : ST_REJECTED; |
| | | if (mapper.update(null, new LambdaUpdateWrapper<AfterSaleCancellationDO>() |
| | | .eq(AfterSaleCancellationDO::getId, task.getId()) |
| | | .eq(AfterSaleCancellationDO::getStatus, ST_PENDING) |
| | | .set(AfterSaleCancellationDO::getStatus, target) |
| | | .set(AfterSaleCancellationDO::getApproverId, userId) |
| | | .set(AfterSaleCancellationDO::getApprovalTime, LocalDateTime.now()) |
| | | .set(AfterSaleCancellationDO::getApprovalRemark, req.getRemark())) != 1) { |
| | | throw exception(CANCELLATION_APPROVE_FAIL_STATUS); |
| | | } |
| | | |
| | | if (!Boolean.TRUE.equals(req.getApproved())) return null; |
| | | return executeDownstream(task, userId); |
| | | } |
| | | |
| | | /** 审æ¹éè¿åç䏿¸¸å¨ä½ï¼å¤±è´¥æ¶è®°å½åå ä½ä¸åæ»å®¡æ¹ç»è®ºï¼ä¾¿äºäººå·¥è¡¥åã */ |
| | | private Long executeDownstream(AfterSaleCancellationDO task, Long userId) { |
| | | Long returnId = null; |
| | | String remark; |
| | | boolean ok = true; |
| | | try { |
| | | switch (task.getType()) { |
| | | case "RETURN" -> { |
| | | var vo = new AfterSaleReturnSaveReqVO(); |
| | | vo.setTicketId(task.getTicketId()); |
| | | vo.setCustomerId(task.getCustomerId()); |
| | | vo.setReturnType(task.getReturnType() == null ? 1 : task.getReturnType()); |
| | | vo.setReturnReason(task.getReason()); |
| | | returnId = returnService.createReturn(vo, userId); |
| | | remark = "å·²çæéè´§å " + returnId; |
| | | } |
| | | case "CONTRACT_TERMINATE" -> { |
| | | crmContractApi.updateContractStatus(task.getContractId(), CRM_CONTRACT_INVALID).getCheckedData(); |
| | | remark = "å·²ä½åº CRM åå " + task.getContractId(); |
| | | } |
| | | case "CONTRACT_CHANGE" -> remark = "åæ´åçå·²éè¿ï¼CRM ååå
容请æéä»¶åæ¥ç»´æ¤"; |
| | | default -> { ok = false; remark = "æªç¥åçç±»åï¼" + task.getType(); } |
| | | } |
| | | } catch (Exception e) { |
| | | ok = false; |
| | | remark = "䏿¸¸æ§è¡å¤±è´¥ï¼" + e.getMessage(); |
| | | log.warn("éååç[{}]䏿¸¸å¨ä½å¤±è´¥ï¼{}", task.getCode(), e.getMessage()); |
| | | } |
| | | var update = new AfterSaleCancellationDO(); |
| | | update.setId(task.getId()); |
| | | update.setExecuted(ok ? 1 : 0); |
| | | update.setExecuteRemark(remark); |
| | | if (returnId != null) update.setReturnId(returnId); |
| | | mapper.updateById(update); |
| | | return returnId; |
| | | } |
| | | |
| | | /** æç±»åæ ¡éªå¿
å¡«ï¼éè´§éå·¥åï¼åååæ´/ç»æ¢éååã */ |
| | | private void validateByType(AfterSaleCancellationSaveReqVO req) { |
| | | switch (req.getType()) { |
| | | case "RETURN" -> { |
| | | if (req.getTicketId() == null) throw exception(CANCELLATION_RETURN_TICKET_REQUIRED); |
| | | } |
| | | case "CONTRACT_CHANGE", "CONTRACT_TERMINATE" -> { |
| | | if (req.getContractId() == null) throw exception(CANCELLATION_CONTRACT_REQUIRED); |
| | | } |
| | | default -> { } |
| | | } |
| | | } |
| | | } |
| | |
| | | AfterSaleTicketDO getTicket(Long id); |
| | | |
| | | /** |
| | | * æéå®è®¢åå·è·å¾å·¥åå表ï¼ä¾è´¨é追溯å
³èå®åæ
éè®°å½ï¼ |
| | | * |
| | | * @param saleOrderNo éå®è®¢åå· |
| | | * @return å·¥åå表 |
| | | */ |
| | | List<AfterSaleTicketDO> getTicketListBySaleOrderNo(String saleOrderNo); |
| | | |
| | | /** |
| | | * è·å¾å·¥åå表 |
| | | * |
| | | * @param ids ç¼å·å表 |
| | |
| | | } |
| | | |
| | | @Override |
| | | public List<AfterSaleTicketDO> getTicketListBySaleOrderNo(String saleOrderNo) { |
| | | if (saleOrderNo == null || saleOrderNo.isBlank()) { |
| | | return List.of(); |
| | | } |
| | | return ticketMapper.selectListBySaleOrderNo(saleOrderNo); |
| | | } |
| | | |
| | | @Override |
| | | public List<AfterSaleTicketDO> getTicketList(Collection<Long> ids) { |
| | | if (CollUtil.isEmpty(ids)) { |
| | | return List.of(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.service.collaboration; |
| | | |
| | | import cn.iocoder.yudao.module.aftersales.dal.dataobject.collaboration.AfterSaleCollaborationDO; |
| | | import cn.iocoder.yudao.module.aftersales.dal.mysql.collaboration.AfterSaleCollaborationMapper; |
| | | import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO; |
| | | import cn.iocoder.yudao.module.aftersales.service.ticket.AfterSaleTicketService; |
| | | import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService; |
| | | import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; |
| | | import cn.iocoder.yudao.module.erp.api.finance.AfterSaleFinanceDraftApi; |
| | | import cn.iocoder.yudao.module.erp.api.finance.dto.AfterSaleFinanceDraftCreateDTO; |
| | | import cn.iocoder.yudao.module.erp.api.finance.dto.AfterSaleFinanceReviewDTO; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO; |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; |
| | | import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO; |
| | | import java.math.BigDecimal; |
| | | import java.util.Map; |
| | | import java.util.List; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.mockito.Mockito.*; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import org.mockito.ArgumentCaptor; |
| | | import com.baomidou.mybatisplus.core.MybatisConfiguration; |
| | | import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
| | | import org.apache.ibatis.builder.MapperBuilderAssistant; |
| | | |
| | | class AfterSaleCollaborationServiceTest { |
| | | @Test |
| | | void warrantyUsesTicketCustomerInsteadOfSubmittedName() { |
| | | var tickets = mock(AfterSaleTicketService.class); |
| | | var ticket = new AfterSaleTicketDO(); ticket.setId(8L); ticket.setOwnerUserId(10L); ticket.setStatus(10); ticket.setCustomerId(99L); |
| | | when(tickets.getTicket(8L)).thenReturn(ticket); |
| | | var customers = mock(CrmCustomerApi.class); |
| | | var customer = new CrmCustomerRespDTO(); customer.setId(99L); customer.setName("å·¥å客æ·"); |
| | | when(customers.getCustomer(99L)).thenReturn(customer); |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "ticketService", tickets); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | ReflectionTestUtils.setField(service, "customerApi", customers); |
| | | ReflectionTestUtils.setField(service, "userApi", mock(AdminUserApi.class)); |
| | | var input = new AfterSaleCollaborationService.CreateRequest(); |
| | | input.setTicketId(8L); input.setAssigneeUserId(20L); input.setCategory("WARRANTY"); |
| | | input.setTitle("ä¸å
"); input.setContent("æ ¸æ¥"); input.setAmount(BigDecimal.TEN); |
| | | input.setDirection("PAYABLE"); input.setPartyName("伪é åç§°"); |
| | | service.create(input, 10L); |
| | | var created = ArgumentCaptor.forClass(AfterSaleCollaborationDO.class); |
| | | verify(mapper).insert(created.capture()); |
| | | assertEquals("å·¥å客æ·", created.getValue().getPartyName()); |
| | | assertEquals("CUSTOMER", created.getValue().getPartyType()); |
| | | assertEquals(99L, created.getValue().getPartyId()); |
| | | } |
| | | @Test |
| | | void supplierClaimRequiresMasterSupplier() { |
| | | var tickets = mock(AfterSaleTicketService.class); |
| | | var ticket = new AfterSaleTicketDO(); ticket.setId(8L); ticket.setOwnerUserId(10L); ticket.setStatus(10); |
| | | when(tickets.getTicket(8L)).thenReturn(ticket); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "ticketService", tickets); |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | ReflectionTestUtils.setField(service, "userApi", mock(AdminUserApi.class)); |
| | | var input = new AfterSaleCollaborationService.CreateRequest(); |
| | | input.setTicketId(8L); input.setAssigneeUserId(20L); input.setCategory("SUPPLIER"); |
| | | input.setTitle("ç´¢èµ"); input.setContent("æ ¸æ¥"); input.setAmount(BigDecimal.TEN); |
| | | input.setDirection("RECEIVABLE"); input.setPartyName("ä»»æå¡«åçä¾åºå"); |
| | | assertThrows(RuntimeException.class, () -> service.create(input, 10L)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | @Test |
| | | void rejectedFinanceDraftCanBeRetriedWithTraceableNewTask() { |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var tickets = mock(AfterSaleTicketService.class); |
| | | var finance = mock(AfterSaleFinanceDraftApi.class); |
| | | var previous = new AfterSaleCollaborationDO(); previous.setId(1L); previous.setTicketId(8L); |
| | | previous.setRequestedBy(10L); previous.setCategory("SUPPLIER"); previous.setStatus(3); previous.setFinanceDraftId(50L); |
| | | when(mapper.selectById(1L)).thenReturn(previous); |
| | | var ticket = new AfterSaleTicketDO(); ticket.setId(8L); ticket.setOwnerUserId(10L); ticket.setStatus(10); |
| | | when(tickets.getTicket(8L)).thenReturn(ticket); |
| | | var review = new AfterSaleFinanceReviewDTO(); review.setStatus(2); |
| | | when(finance.getReviewResults(List.of(50L))).thenReturn(Map.of(50L, review)); |
| | | when(mapper.insert(any(AfterSaleCollaborationDO.class))).thenAnswer(call -> { |
| | | AfterSaleCollaborationDO created = call.getArgument(0); created.setId(2L); return 1; |
| | | }); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); ReflectionTestUtils.setField(service, "ticketService", tickets); |
| | | ReflectionTestUtils.setField(service, "financeDraftApi", finance); ReflectionTestUtils.setField(service, "userApi", mock(AdminUserApi.class)); |
| | | var suppliers = mock(SrmSupplierApi.class); |
| | | var supplier = new SrmSupplierRespDTO(); supplier.setId(600L); supplier.setName("ä¸»æ°æ®ä¾åºå"); supplier.setStatus(0); |
| | | when(suppliers.getSupplier(600L)).thenReturn(CommonResult.success(supplier)); |
| | | ReflectionTestUtils.setField(service, "supplierApi", suppliers); |
| | | var input = new AfterSaleCollaborationService.CreateRequest(); input.setPreviousId(1L); input.setTicketId(8L); |
| | | input.setAssigneeUserId(20L); input.setCategory("SUPPLIER"); input.setTitle("éå"); input.setContent("è¡¥å
ææ"); |
| | | input.setAmount(new BigDecimal("100.00")); input.setDirection("RECEIVABLE"); input.setPartyName("伪é åç§°"); input.setPartyId(600L); |
| | | assertEquals(2L, service.create(input, 10L)); |
| | | verify(mapper).insert((AfterSaleCollaborationDO) argThat(value -> Long.valueOf(1L).equals(((AfterSaleCollaborationDO) value).getPreviousId()))); |
| | | var created = ArgumentCaptor.forClass(AfterSaleCollaborationDO.class); |
| | | verify(mapper).insert(created.capture()); |
| | | assertEquals("ä¸»æ°æ®ä¾åºå", created.getValue().getPartyName()); |
| | | assertEquals("SUPPLIER", created.getValue().getPartyType()); |
| | | assertEquals(600L, created.getValue().getPartyId()); |
| | | } |
| | | |
| | | @Test |
| | | void businessApprovalCreatesOneFinanceDraftFromSubmittedAmount() { |
| | | TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), AfterSaleCollaborationDO.class); |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var tickets = mock(AfterSaleTicketService.class); |
| | | var finance = mock(AfterSaleFinanceDraftApi.class); |
| | | var task = new AfterSaleCollaborationDO(); task.setId(1L); task.setTicketId(8L); task.setStatus(1); |
| | | task.setAmount(new BigDecimal("123.45")); task.setCategory("SUPPLIER"); task.setDirection("RECEIVABLE"); |
| | | task.setPartyName("ä¾åºå"); task.setResult("æ ¸æ¥å®æ"); |
| | | task.setPartyType("SUPPLIER"); task.setPartyId(600L); |
| | | when(mapper.selectById(1L)).thenReturn(task); |
| | | var ticket = new AfterSaleTicketDO(); ticket.setId(8L); ticket.setOwnerUserId(10L); |
| | | when(tickets.getTicket(8L)).thenReturn(ticket); |
| | | when(mapper.update(any(), any())).thenReturn(1); |
| | | when(mapper.updateById(any(AfterSaleCollaborationDO.class))).thenReturn(1); |
| | | when(finance.createDraft(any())).thenReturn(50L); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); ReflectionTestUtils.setField(service, "ticketService", tickets); |
| | | ReflectionTestUtils.setField(service, "financeDraftApi", finance); |
| | | service.reviewFinancial(1L, 10L, true, "åæ"); |
| | | var capture = ArgumentCaptor.forClass(AfterSaleFinanceDraftCreateDTO.class); |
| | | verify(finance).createDraft(capture.capture()); |
| | | assertEquals(new BigDecimal("123.45"), capture.getValue().getAmount()); |
| | | assertEquals(10L, capture.getValue().getBusinessApproverId()); |
| | | assertEquals(600L, capture.getValue().getPartyId()); |
| | | verify(mapper).updateById((AfterSaleCollaborationDO) argThat(value -> Long.valueOf(50L).equals(((AfterSaleCollaborationDO) value).getFinanceDraftId()))); |
| | | } |
| | | |
| | | @Test |
| | | void onlyRequesterCanCancelPendingTask() { |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var task = new AfterSaleCollaborationDO(); task.setId(2L); task.setRequestedBy(10L); task.setStatus(0); |
| | | when(mapper.selectById(2L)).thenReturn(task); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> service.cancel(2L, 20L)); |
| | | verify(mapper, never()).update(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void unrelatedUserCannotCreateTasksForSomeoneElsesTicket() { |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var tickets = mock(AfterSaleTicketService.class); |
| | | var ticket = new AfterSaleTicketDO(); ticket.setId(8L); ticket.setOwnerUserId(10L); ticket.setStatus(10); |
| | | when(tickets.getTicket(8L)).thenReturn(ticket); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | ReflectionTestUtils.setField(service, "ticketService", tickets); |
| | | ReflectionTestUtils.setField(service, "permissionService", mock(AfterSalePermissionService.class)); |
| | | ReflectionTestUtils.setField(service, "permissionApi", mock(PermissionCommonApi.class)); |
| | | var input = new AfterSaleCollaborationService.CreateRequest(); |
| | | input.setTicketId(8L); input.setAssigneeUserId(20L); input.setCategory("FINANCE"); |
| | | input.setAmount(new BigDecimal("100.00")); input.setDirection("INTERNAL"); |
| | | input.setTitle("æ ¸æ¥"); input.setContent("æ ¸æ¥å¤çæ
åµ"); |
| | | assertThrows(RuntimeException.class, () -> service.create(input, 30L)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | void unauthorizedBusinessReviewerCannotGenerateFinanceDraft() { |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var tickets = mock(AfterSaleTicketService.class); |
| | | var finance = mock(AfterSaleFinanceDraftApi.class); |
| | | var task = new AfterSaleCollaborationDO(); task.setId(1L); task.setTicketId(8L); task.setStatus(1); task.setAmount(BigDecimal.TEN); |
| | | when(mapper.selectById(1L)).thenReturn(task); |
| | | var ticket = new AfterSaleTicketDO(); ticket.setId(8L); ticket.setOwnerUserId(10L); |
| | | when(tickets.getTicket(8L)).thenReturn(ticket); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); ReflectionTestUtils.setField(service, "ticketService", tickets); |
| | | ReflectionTestUtils.setField(service, "financeDraftApi", finance); |
| | | ReflectionTestUtils.setField(service, "permissionService", mock(AfterSalePermissionService.class)); |
| | | ReflectionTestUtils.setField(service, "permissionApi", mock(PermissionCommonApi.class)); |
| | | assertThrows(RuntimeException.class, () -> service.reviewFinancial(1L, 30L, true, null)); |
| | | verifyNoInteractions(finance); |
| | | verify(mapper, never()).update(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void onlyAssigneeCanCompleteTask() { |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var task = new AfterSaleCollaborationDO(); task.setId(1L); task.setAssigneeUserId(10L); task.setStatus(0); |
| | | when(mapper.selectById(1L)).thenReturn(task); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> service.complete(1L, 20L, "å·²å¤ç")); |
| | | verify(mapper, never()).update(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void completionRequiresResult() { |
| | | var mapper = mock(AfterSaleCollaborationMapper.class); |
| | | var service = new AfterSaleCollaborationService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> service.complete(1L, 10L, " ")); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.aftersales.service.returnobj; |
| | | |
| | | import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO; |
| | | import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO; |
| | | import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO; |
| | | import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnMapper; |
| | | import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnItemMapper; |
| | | import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper; |
| | | import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO; |
| | | import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO; |
| | | import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService; |
| | | import cn.iocoder.yudao.module.mes.api.returnsales.MesWmReturnSalesApi; |
| | | import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineCreateDTO; |
| | | import com.baomidou.mybatisplus.core.MybatisConfiguration; |
| | | import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
| | | import org.apache.ibatis.builder.MapperBuilderAssistant; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class AfterSaleReturnMesLinkTest { |
| | | @Test |
| | | void refundOnlyDoesNotCreatePhysicalReturnDraft() { |
| | | var mapper = mock(AfterSaleReturnMapper.class); |
| | | var service = new AfterSaleReturnServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnMapper", mapper); |
| | | var tickets = mock(AfterSaleTicketMapper.class); |
| | | when(tickets.selectById(8L)).thenReturn(new AfterSaleTicketDO().setId(8L)); |
| | | ReflectionTestUtils.setField(service, "ticketMapper", tickets); |
| | | ReflectionTestUtils.setField(service, "noRedisDAO", mock(AfterSaleNoRedisDAO.class)); |
| | | ReflectionTestUtils.setField(service, "permissionService", mock(AfterSalePermissionService.class)); |
| | | var input = new AfterSaleReturnSaveReqVO(); input.setReturnType(3); input.setTicketId(8L); |
| | | assertThrows(RuntimeException.class, () -> service.createReturn(input, 1L)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | @Test |
| | | void noPhysicalLinesDoesNotCreateMesDocument() { |
| | | var mapper = mock(AfterSaleReturnMapper.class); |
| | | var items = mock(AfterSaleReturnItemMapper.class); |
| | | var api = mock(MesWmReturnSalesApi.class); |
| | | when(mapper.selectById(1L)).thenReturn(new AfterSaleReturnDO().setId(1L).setStatus(0).setReturnType(1)); |
| | | when(items.selectListByReturnId(1L)).thenReturn(List.of()); |
| | | var service = new AfterSaleReturnServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnItemMapper", items); |
| | | ReflectionTestUtils.setField(service, "mesWmReturnSalesApi", api); |
| | | assertThrows(RuntimeException.class, () -> service.approveReturn(1L)); |
| | | verifyNoInteractions(api); |
| | | } |
| | | |
| | | @Test |
| | | void lostApprovalRaceDoesNotCreateSecondMesDocument() { |
| | | TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), AfterSaleReturnDO.class); |
| | | var mapper = mock(AfterSaleReturnMapper.class); |
| | | var items = mock(AfterSaleReturnItemMapper.class); |
| | | var api = mock(MesWmReturnSalesApi.class); |
| | | when(mapper.selectById(1L)).thenReturn(new AfterSaleReturnDO().setId(1L).setStatus(0).setReturnType(1)); |
| | | var line = new AfterSaleReturnItemDO(); line.setItemId(4L); line.setReturnQuantity(BigDecimal.ONE); |
| | | when(items.selectListByReturnId(1L)).thenReturn(List.of(line)); |
| | | when(mapper.update(any(), any())).thenReturn(0); |
| | | var service = new AfterSaleReturnServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnItemMapper", items); |
| | | ReflectionTestUtils.setField(service, "mesWmReturnSalesApi", api); |
| | | assertThrows(RuntimeException.class, () -> service.approveReturn(1L)); |
| | | verifyNoInteractions(api); |
| | | } |
| | | @Test |
| | | void approvalCopiesReturnLinesAndPersistsTheirMesIds() { |
| | | TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), AfterSaleReturnDO.class); |
| | | var mapper = mock(AfterSaleReturnMapper.class); |
| | | var items = mock(AfterSaleReturnItemMapper.class); |
| | | var api = mock(MesWmReturnSalesApi.class); |
| | | var source = new AfterSaleReturnDO().setId(1L).setTicketId(2L).setStatus(0).setNo("RET-1").setReturnType(1); |
| | | when(mapper.selectById(1L)).thenReturn(source); |
| | | when(mapper.update(any(), any())).thenReturn(1); |
| | | when(mapper.updateById(any(AfterSaleReturnDO.class))).thenReturn(1); |
| | | when(items.updateById(any(AfterSaleReturnItemDO.class))).thenReturn(1); |
| | | var line = new AfterSaleReturnItemDO(); line.setId(3L); line.setReturnId(1L); line.setItemId(4L); |
| | | line.setReturnQuantity(new BigDecimal("2.50")); line.setBatchId(5L); line.setBatchCode("B-1"); |
| | | when(items.selectListByReturnId(1L)).thenReturn(List.of(line)); |
| | | when(api.createReturnSales(any())).thenReturn(6L); |
| | | when(api.createReturnSalesLine(any())).thenReturn(7L); |
| | | var service = new AfterSaleReturnServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnItemMapper", items); |
| | | ReflectionTestUtils.setField(service, "ticketMapper", mock(AfterSaleTicketMapper.class)); |
| | | ReflectionTestUtils.setField(service, "mesWmReturnSalesApi", api); |
| | | service.approveReturn(1L); |
| | | var capture = ArgumentCaptor.forClass(MesWmReturnSalesLineCreateDTO.class); |
| | | verify(api).createReturnSalesLine(capture.capture()); |
| | | assertEquals(6L, capture.getValue().getReturnId()); |
| | | assertEquals(4L, capture.getValue().getItemId()); |
| | | assertEquals(new BigDecimal("2.50"), capture.getValue().getQuantity()); |
| | | assertEquals(5L, capture.getValue().getBatchId()); |
| | | var updated = ArgumentCaptor.forClass(AfterSaleReturnItemDO.class); |
| | | verify(items).updateById(updated.capture()); |
| | | assertEquals(7L, updated.getValue().getMesReturnLineId()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class AfterSaleLedgerServiceTest { |
| | | @Test |
| | | void countsUseTheSameUserAndSceneAsTheTicketList() { |
| | | var tickets = mock(AfterSaleTicketService.class); |
| | | when(tickets.getTicketPage(any(), eq(99L))).thenAnswer(call -> { |
| | | AfterSaleTicketPageReqVO request = call.getArgument(0); |
| | | assertEquals(2, request.getSceneType()); |
| | | assertEquals(1, request.getPageSize()); |
| | | return new PageResult<>(List.of(), request.getStatus().longValue()); |
| | | }); |
| | | var service = new AfterSaleLedgerService(); |
| | | ReflectionTestUtils.setField(service, "ticketService", tickets); |
| | | var result = service.overview(99L, 2); |
| | | assertEquals(10L, result.get(10)); |
| | | assertEquals(60L, result.get(60)); |
| | | verify(tickets, times(7)).getTicketPage(any(), eq(99L)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.api.sale; |
| | | |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper; |
| | | import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class ErpSaleOrderNoticeLockTest { |
| | | @Test |
| | | void cancelledOrderCannotBeUsedForNotice() { |
| | | var mapper = mock(ErpSaleOrderMapper.class); |
| | | when(mapper.selectByIdForUpdate(1L)).thenReturn(new ErpSaleOrderDO().setId(1L).setNo("SO-1").setStatus(ErpAuditStatus.CANCEL.getStatus())); |
| | | var api = new ErpSaleOrderApiImpl(); ReflectionTestUtils.setField(api, "saleOrderMapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> api.lockSaleOrderForNotice(1L, "SO-1")); |
| | | } |
| | | @Test |
| | | void orderCodeMustMatchLockedOrder() { |
| | | var mapper = mock(ErpSaleOrderMapper.class); |
| | | when(mapper.selectByIdForUpdate(1L)).thenReturn(new ErpSaleOrderDO().setId(1L).setNo("SO-1").setStatus(ErpAuditStatus.APPROVE.getStatus())); |
| | | var api = new ErpSaleOrderApiImpl(); ReflectionTestUtils.setField(api, "saleOrderMapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> api.lockSaleOrderForNotice(1L, "SO-2")); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.service.finance; |
| | | |
| | | import cn.iocoder.yudao.module.erp.api.finance.dto.AfterSaleFinanceDraftCreateDTO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAfterSaleFinanceDraftDO; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpAfterSaleFinanceDraftMapper; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class ErpAfterSaleFinanceDraftServiceTest { |
| | | @Test |
| | | void exposesReviewResultByDraftIdWithoutChangingIt() { |
| | | var mapper = mock(ErpAfterSaleFinanceDraftMapper.class); |
| | | var draft = new ErpAfterSaleFinanceDraftDO(); draft.setId(4L); draft.setStatus(2); draft.setReviewRemark("ææä¸å
¨"); |
| | | when(mapper.selectBatchIds(List.of(4L))).thenReturn(List.of(draft)); |
| | | var service = new ErpAfterSaleFinanceDraftService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | var result = service.getReviewResults(List.of(4L)); |
| | | assertEquals(2, result.get(4L).getStatus()); |
| | | assertEquals("ææä¸å
¨", result.get(4L).getRemark()); |
| | | } |
| | | |
| | | @Test |
| | | void rejectsNonPositiveAmountBeforeInsert() { |
| | | var mapper = mock(ErpAfterSaleFinanceDraftMapper.class); |
| | | var service = new ErpAfterSaleFinanceDraftService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | var input = new AfterSaleFinanceDraftCreateDTO(); input.setCollaborationId(1L); input.setTicketId(2L); |
| | | input.setBusinessApproverId(3L); input.setCategory("QUALITY"); input.setDirection("INTERNAL"); |
| | | input.setDescription("è´¹ç¨è¯´æ"); input.setAmount(BigDecimal.ZERO); |
| | | assertThrows(RuntimeException.class, () -> service.createDraft(input)); |
| | | input.setAmount(new BigDecimal("1.001")); |
| | | assertThrows(RuntimeException.class, () -> service.createDraft(input)); |
| | | input.setAmount(new BigDecimal("10000000000000000.00")); |
| | | assertThrows(RuntimeException.class, () -> service.createDraft(input)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | void onlyPendingDraftCanBeReviewed() { |
| | | var mapper = mock(ErpAfterSaleFinanceDraftMapper.class); |
| | | var draft = new ErpAfterSaleFinanceDraftDO(); draft.setId(4L); draft.setStatus(1); |
| | | when(mapper.selectById(4L)).thenReturn(draft); |
| | | var service = new ErpAfterSaleFinanceDraftService(); |
| | | ReflectionTestUtils.setField(service, "mapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> service.review(4L, 9L, true, "确认")); |
| | | verify(mapper, never()).update(any(), any()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.service.sale; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderItemMapper; |
| | | import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus; |
| | | import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi; |
| | | import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO; |
| | | import cn.iocoder.yudao.module.mes.api.salesnotice.MesWmSalesNoticeApi; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; |
| | | import static org.mockito.ArgumentMatchers.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class ErpSaleOrderCancellationGuardTest { |
| | | @Test |
| | | void missingDeliveryNoticeResultFailsClosed() { |
| | | var orders = mock(ErpSaleOrderMapper.class); |
| | | var items = mock(ErpSaleOrderItemMapper.class); |
| | | var notices = mock(MesWmSalesNoticeApi.class); |
| | | when(orders.selectByIdForUpdate(1L)).thenReturn(new ErpSaleOrderDO().setId(1L).setNo("TEST").setStatus(ErpAuditStatus.APPROVE.getStatus()).setOutCount(BigDecimal.ZERO)); |
| | | when(items.selectListByOrderId(1L)).thenReturn(List.of()); |
| | | when(notices.checkSalesNoticeBySaleOrderIds(List.of(1L))).thenReturn(Map.of()); |
| | | var service = new ErpSaleOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "saleOrderMapper", orders); |
| | | ReflectionTestUtils.setField(service, "saleOrderItemMapper", items); |
| | | ReflectionTestUtils.setField(service, "salesNoticeApi", notices); |
| | | assertThrows(ServiceException.class, () -> service.cancelSaleOrder(1L)); |
| | | verify(orders, never()).updateByIdAndStatus(anyLong(), anyInt(), any()); |
| | | } |
| | | @Test |
| | | void existingDeliveryNoticeBlocksOrderCancellation() { |
| | | var orderMapper = mock(ErpSaleOrderMapper.class); |
| | | var itemMapper = mock(ErpSaleOrderItemMapper.class); |
| | | var mpsApi = mock(MesProMpsApi.class); |
| | | var noticeApi = mock(MesWmSalesNoticeApi.class); |
| | | var order = new ErpSaleOrderDO().setId(1L).setNo("TEST").setStatus(ErpAuditStatus.APPROVE.getStatus()).setOutCount(BigDecimal.ZERO); |
| | | when(orderMapper.selectByIdForUpdate(1L)).thenReturn(order); |
| | | when(itemMapper.selectListByOrderId(1L)).thenReturn(List.of()); |
| | | when(noticeApi.checkSalesNoticeBySaleOrderIds(List.of(1L))).thenReturn(Map.of(1L, true)); |
| | | var service = new ErpSaleOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "saleOrderMapper", orderMapper); |
| | | ReflectionTestUtils.setField(service, "saleOrderItemMapper", itemMapper); |
| | | ReflectionTestUtils.setField(service, "mpsApi", mpsApi); |
| | | ReflectionTestUtils.setField(service, "salesNoticeApi", noticeApi); |
| | | assertThrows(ServiceException.class, () -> service.cancelSaleOrder(1L)); |
| | | verify(orderMapper, never()).updateByIdAndStatus(anyLong(), anyInt(), any()); |
| | | } |
| | | @Test |
| | | void canceledPlansAllowOrderCancellation() { |
| | | var orderMapper = mock(ErpSaleOrderMapper.class); |
| | | var itemMapper = mock(ErpSaleOrderItemMapper.class); |
| | | var mpsApi = mock(MesProMpsApi.class); |
| | | var noticeApi = mock(MesWmSalesNoticeApi.class); |
| | | when(noticeApi.checkSalesNoticeBySaleOrderIds(List.of(1L))).thenReturn(Map.of(1L, false)); |
| | | var order = new ErpSaleOrderDO().setId(1L).setNo("TEST").setStatus(ErpAuditStatus.APPROVE.getStatus()).setOutCount(BigDecimal.ZERO); |
| | | when(orderMapper.selectByIdForUpdate(1L)).thenReturn(order); |
| | | when(orderMapper.updateByIdAndStatus(eq(1L), anyInt(), any())).thenReturn(1); |
| | | when(itemMapper.selectListByOrderId(1L)).thenReturn(List.of(new ErpSaleOrderItemDO().setId(8L))); |
| | | var plan = new MesProMpsRespDTO(); plan.setStatus(3); |
| | | when(mpsApi.getMpsListBySaleOrderItemIds(List.of(8L))).thenReturn(List.of(plan)); |
| | | var service = new ErpSaleOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "saleOrderMapper", orderMapper); |
| | | ReflectionTestUtils.setField(service, "saleOrderItemMapper", itemMapper); |
| | | ReflectionTestUtils.setField(service, "mpsApi", mpsApi); |
| | | ReflectionTestUtils.setField(service, "salesNoticeApi", noticeApi); |
| | | assertDoesNotThrow(() -> service.cancelSaleOrder(1L)); |
| | | verify(orderMapper).updateByIdAndStatus(eq(1L), anyInt(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void activeProductionPlanBlocksOrderCancellation() { |
| | | var orderMapper = mock(ErpSaleOrderMapper.class); |
| | | var itemMapper = mock(ErpSaleOrderItemMapper.class); |
| | | var mpsApi = mock(MesProMpsApi.class); |
| | | var order = new ErpSaleOrderDO().setId(1L).setNo("TEST").setStatus(ErpAuditStatus.APPROVE.getStatus()).setOutCount(BigDecimal.ZERO); |
| | | when(orderMapper.selectByIdForUpdate(1L)).thenReturn(order); |
| | | var item = new ErpSaleOrderItemDO().setId(8L); |
| | | when(itemMapper.selectListByOrderId(1L)).thenReturn(List.of(item)); |
| | | var plan = new MesProMpsRespDTO(); plan.setStatus(1); |
| | | when(mpsApi.getMpsListBySaleOrderItemIds(List.of(8L))).thenReturn(List.of(plan)); |
| | | var service = new ErpSaleOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "saleOrderMapper", orderMapper); |
| | | ReflectionTestUtils.setField(service, "saleOrderItemMapper", itemMapper); |
| | | ReflectionTestUtils.setField(service, "mpsApi", mpsApi); |
| | | assertThrows(ServiceException.class, () -> service.cancelSaleOrder(1L)); |
| | | verify(orderMapper, never()).updateByIdAndStatus(anyLong(), anyInt(), any()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.service.statistics; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import java.math.BigDecimal; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | class PurchasePriceMetricsTest { |
| | | @Test |
| | | void comparesLatestPriceToPreviousAndAvoidsDivisionByZero() { |
| | | assertEquals(new BigDecimal("25.00"), PurchasePriceMetrics.changeRate(new BigDecimal("10"), new BigDecimal("8"))); |
| | | assertNull(PurchasePriceMetrics.changeRate(new BigDecimal("10"), BigDecimal.ZERO)); |
| | | assertNull(PurchasePriceMetrics.changeRate(new BigDecimal("10"), null)); |
| | | assertEquals(new BigDecimal("-20.00"), PurchasePriceMetrics.changeRate(new BigDecimal("8"), new BigDecimal("10"))); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.collect.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 设å¤åæºæç»ï¼æééé´éæ¨ç®ï¼ã |
| | | * |
| | | * <p>å£å¾ï¼ä¸¤æ¬¡éæ ·é´éè¶
è¿å¨çº¿è¶
æ¶éå¼å³è§ä¸ºç¦»çº¿åæºï¼ |
| | | * 馿¬¡éæ ·ä¹åä¸ä½æ¨æï¼éä¿¡ä¸æåæ ·è®¡å
¥åæºï¼ä½ä¸æ è®°ä¸ºæºæ¢°æ
éã |
| | | */ |
| | | @Schema(description = "管çåå° - 设å¤åæºæç» Response VO") |
| | | @Data |
| | | public class MesCollectDowntimeDetailRespVO { |
| | | |
| | | @Schema(description = "ç»è®¡å¼å§æ¶é´") |
| | | private LocalDateTime startTime; |
| | | @Schema(description = "ç»è®¡ç»ææ¶é´") |
| | | private LocalDateTime endTime; |
| | | @Schema(description = "å¨çº¿è¶
æ¶éå¼ï¼åéï¼") |
| | | private Integer timeoutMinutes; |
| | | @Schema(description = "ç»è®¡åºé´æ»æ¶é¿ï¼åéï¼") |
| | | private Long totalMinutes; |
| | | @Schema(description = "åæºå计ï¼åéï¼") |
| | | private Long downtimeMinutes; |
| | | @Schema(description = "åæºçï¼%ï¼") |
| | | private Double downtimeRate; |
| | | @Schema(description = "æè®¾å¤æç»") |
| | | private List<Item> items; |
| | | |
| | | @Schema(description = "å设å¤åæºæç»") |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public static class Item { |
| | | @Schema(description = "设å¤å°è´¦ç¼å·") |
| | | private Long machineryId; |
| | | @Schema(description = "设å¤ç¼ç ") |
| | | private String machineryCode; |
| | | @Schema(description = "设å¤åç§°") |
| | | private String machineryName; |
| | | @Schema(description = "æ°é设å¤ç¼ç ") |
| | | private String deviceCode; |
| | | @Schema(description = "éæ ·æ°") |
| | | private Integer sampleCount; |
| | | @Schema(description = "åæºæ¬¡æ°") |
| | | private Integer downtimeCount; |
| | | @Schema(description = "åæºå计ï¼åéï¼") |
| | | private Long downtimeMinutes; |
| | | @Schema(description = "åæºåºé´æç»") |
| | | private List<Interval> intervals; |
| | | } |
| | | |
| | | @Schema(description = "åæºåºé´") |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public static class Interval { |
| | | @Schema(description = "åæºå¼å§") |
| | | private LocalDateTime start; |
| | | @Schema(description = "åæºç»æ") |
| | | private LocalDateTime end; |
| | | @Schema(description = "æç»ç§æ°") |
| | | private Long seconds; |
| | | } |
| | | } |
| | |
| | | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo.MesDvRepairDiagnosisRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo.MesDvRepairPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo.MesDvRepairRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo.MesDvRepairSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.dv.repair.MesDvRepairDO; |
| | | import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService; |
| | | import cn.iocoder.yudao.module.mes.service.dv.repair.MesDvRepairDiagnosisService; |
| | | import cn.iocoder.yudao.module.mes.service.dv.repair.MesDvRepairService; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| | |
| | | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | @Resource |
| | | private MesDvRepairDiagnosisService diagnosisService; |
| | | |
| | | @PostMapping("/create") |
| | | @Operation(summary = "å建维修工å") |
| | |
| | | return success(new PageResult<>(buildRepairRespVOList(pageResult.getList()), pageResult.getTotal())); |
| | | } |
| | | |
| | | @GetMapping("/diagnose") |
| | | @Operation(summary = "设å¤è¿ç¨è¯æï¼åºäºç°æè®¾å¤ééæ°æ®ï¼", |
| | | description = "读å宿¶ééãç¶æåå²ä¸åè¦ï¼ç»åºè¯æå»ºè®®ï¼ç»è®ºä»
ä½ç»´ä¿®åèï¼ä¸æ¿ä»£ç°åºç¡®è®¤") |
| | | @Parameter(name = "machineryId", description = "设å¤å°è´¦ç¼å·", required = true, example = "1") |
| | | @PreAuthorize("@ss.hasPermission('mes:dv-repair:query')") |
| | | public CommonResult<MesDvRepairDiagnosisRespVO> diagnose(@RequestParam("machineryId") Long machineryId) { |
| | | return success(diagnosisService.diagnose(machineryId)); |
| | | } |
| | | |
| | | @GetMapping("/export-excel") |
| | | @Operation(summary = "导åºç»´ä¿®å·¥å Excel") |
| | | @PreAuthorize("@ss.hasPermission('mes:dv-repair:export')") |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 管çåå° - 设å¤è¿ç¨è¯æ Response VO |
| | | * |
| | | * <p>åºäºç°æè®¾å¤ééæ°æ®ï¼å®æ¶ç¶æ + æè¿åè¦ + ç¶æåå²ï¼çæï¼ |
| | | * ä¸ä¾èµé¢å¤çå¤é¨è¯ææ¥å£ã |
| | | */ |
| | | @Schema(description = "管çåå° - 设å¤è¿ç¨è¯æ Response VO") |
| | | @Data |
| | | public class MesDvRepairDiagnosisRespVO { |
| | | |
| | | @Schema(description = "设å¤å°è´¦ç¼å·") |
| | | private Long machineryId; |
| | | @Schema(description = "设å¤ç¼ç ") |
| | | private String machineryCode; |
| | | @Schema(description = "设å¤åç§°") |
| | | private String machineryName; |
| | | |
| | | @Schema(description = "æ¯å¦æééæ°æ®") |
| | | private Boolean hasCollectData; |
| | | @Schema(description = "æè¿ééæ¶é´") |
| | | private LocalDateTime lastCollectTime; |
| | | @Schema(description = "ééæ°æ®æ¯å¦è¶
æ¶ï¼è¶
è¿ 5 åéæ ä¸æ¥ï¼") |
| | | private Boolean collectTimeout; |
| | | |
| | | @Schema(description = "æè¿è¿è¡ç¶æï¼æ°éåå¼ï¼") |
| | | private Integer runStatus; |
| | | @Schema(description = "æè¿è¿è¡æ¨¡å¼ï¼æ°éåå¼ï¼") |
| | | private Integer operateMode; |
| | | @Schema(description = "æè¿è¿è¡æ¶é¿") |
| | | private BigDecimal runTime; |
| | | @Schema(description = "å
³é®ææ å¿«ç
§ï¼ææ å -> å¼") |
| | | private Map<String, BigDecimal> metrics; |
| | | |
| | | @Schema(description = "è¿ N 天åè¦æ°") |
| | | private Long recentAlarmCount; |
| | | @Schema(description = "æªæ¸
é¤åè¦æ°") |
| | | private Long activeAlarmCount; |
| | | @Schema(description = "æè¿åè¦æç»") |
| | | private List<AlarmItem> recentAlarms; |
| | | |
| | | @Schema(description = "è¯æç»è®ºï¼ä¾ç»´ä¿®å·¥ååèï¼ä¸æ¿ä»£ç°åºç¡®è®¤ï¼") |
| | | private String diagnosis; |
| | | @Schema(description = "é£é©ç级ï¼NORMAL / WARNING / CRITICAL / UNKNOWN") |
| | | private String riskLevel; |
| | | @Schema(description = "è¯æä¾æ®ï¼éæ¡è¯´æåæ°å£å¾ï¼") |
| | | private List<String> evidence; |
| | | |
| | | @Schema(description = "åè¦æç»é¡¹") |
| | | @Data |
| | | public static class AlarmItem { |
| | | @Schema(description = "åè¦ç¼å·") |
| | | private Long id; |
| | | @Schema(description = "åè¦ä¿¡æ¯") |
| | | private String message; |
| | | @Schema(description = "åè¦æ¶é´") |
| | | private LocalDateTime alarmTime; |
| | | @Schema(description = "æ¯å¦å·²æ¸
é¤") |
| | | private Boolean cleared; |
| | | } |
| | | } |
| | |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.dv.statistics.vo.MesDvOeeRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.dv.statistics.vo.MesDvStatisticsRespVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.dv.maintenrecord.MesDvMaintenRecordMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.dv.repair.MesDvRepairMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; |
| | | import cn.iocoder.yudao.module.mes.service.collect.MesCollectQueryService; |
| | | import cn.iocoder.yudao.module.mes.enums.dv.MesDvMachineryStatusEnum; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | |
| | | private MesDvMaintenRecordMapper maintenRecordMapper; |
| | | @Resource |
| | | private MesProTaskMapper taskMapper; |
| | | @Resource |
| | | private MesCollectQueryService collectQueryService; |
| | | |
| | | @GetMapping("/summary") |
| | | @Operation(summary = "设å¤ç»è®¡æ±æ»ï¼å°è´¦/ç»´ä¿®/ä¿å
»/ç±»ååå¸ï¼") |
| | |
| | | return success(vo); |
| | | } |
| | | |
| | | @GetMapping("/downtime") |
| | | @Operation(summary = "设å¤åæºæç»æ¥è¡¨", |
| | | description = "æééé´éæ¨ç®åæºåºé´ä¸å计ï¼éä¿¡ä¸æåæ ·è®¡å
¥åæºï¼ä½ä¸æ è®°ä¸ºæºæ¢°æ
é") |
| | | public CommonResult<MesCollectDowntimeDetailRespVO> getDowntimeDetail( |
| | | @RequestParam(value = "startTime", required = false) |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime, |
| | | @RequestParam(value = "endTime", required = false) |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) { |
| | | LocalDateTime start = startTime != null ? startTime : LocalDateTime.now().minusDays(7); |
| | | LocalDateTime end = endTime != null ? endTime : LocalDateTime.now(); |
| | | return success(collectQueryService.getDowntimeDetail(start, end)); |
| | | } |
| | | |
| | | @GetMapping("/oee") |
| | | @Operation(summary = "è®¾å¤ OEE ç»è®¡ï¼ç¨¼å¨çÃæ§è½å¼å¨çÃè¯åçï¼") |
| | | public CommonResult<MesDvOeeRespVO> getOee( |
| | |
| | | @Resource |
| | | private MesProWorkOrderParamRecordDetailMapper paramRecordDetailMapper; |
| | | |
| | | @Resource |
| | | private cn.iocoder.yudao.module.mes.service.pro.feedback.MesProTaskExecutionService taskExecutionService; |
| | | |
| | | @PostMapping("/create") |
| | | @Operation(summary = "å建ç产æ¥å·¥") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-feedback:create')") |
| | |
| | | return success(new PageResult<>(buildFeedbackRespVOList(pageResult.getList()), pageResult.getTotal())); |
| | | } |
| | | |
| | | @GetMapping("/execution") |
| | | @Operation(summary = "çäº§ä»»å¡æ§è¡åé¦åæ", |
| | | description = "æ±æ»è®¡åãæ¥å·¥ã设å¤ééãç©ææ¶èä¸äº¤æåå·®ï¼å»¶æåå ä»
åå¯è§æµä¿¡å·ï¼é人工确认") |
| | | @Parameter(name = "taskId", description = "ä»»å¡ç¼å·", required = true, example = "1024") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-feedback:query')") |
| | | public CommonResult<cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProTaskExecutionRespVO> |
| | | getTaskExecution(@RequestParam("taskId") Long taskId) { |
| | | return success(taskExecutionService.getExecution(taskId)); |
| | | } |
| | | |
| | | @GetMapping("/list-by-task") |
| | | @Operation(summary = "æä»»å¡è·å¾æ¥å·¥åå²å表") |
| | | @Parameter(name = "taskId", description = "ä»»å¡ç¼å·", required = true, example = "1024") |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 管çåå° - çäº§ä»»å¡æ§è¡åé¦åæ Response VO |
| | | * |
| | | * <p>æè®¡åãå®é
æ¥å·¥ã设å¤ééãç©ææ¶èã交æåå·®æ±æ»å°ä¸å¤ï¼ |
| | | * ä¾è½¦é´ä¸è®¡ååå®ä½å»¶æåå ãæææ°æ®åæ¥èªç°æè¡¨ï¼ä¸åæ¨ææ§è¡¥æ°ã |
| | | */ |
| | | @Schema(description = "管çåå° - çäº§ä»»å¡æ§è¡åé¦åæ Response VO") |
| | | @Data |
| | | public class MesProTaskExecutionRespVO { |
| | | |
| | | @Schema(description = "ä»»å¡ç¼å·") |
| | | private Long taskId; |
| | | @Schema(description = "ä»»å¡ç¼ç ") |
| | | private String taskCode; |
| | | @Schema(description = "ä»»å¡åç§°") |
| | | private String taskName; |
| | | @Schema(description = "ç产工åç¼å·") |
| | | private Long workOrderId; |
| | | @Schema(description = "ä»»å¡ç¶æ") |
| | | private Integer status; |
| | | |
| | | @Schema(description = "è®¡åæ°é") |
| | | private BigDecimal plannedQuantity; |
| | | @Schema(description = "å®é
æ¥å·¥æ°é") |
| | | private BigDecimal producedQuantity; |
| | | @Schema(description = "åæ ¼æ°é") |
| | | private BigDecimal qualifyQuantity; |
| | | @Schema(description = "æ¥åºæ°é") |
| | | private BigDecimal unqualifyQuantity; |
| | | @Schema(description = "宿çï¼%ï¼") |
| | | private BigDecimal completionRate; |
| | | |
| | | @Schema(description = "计åå¼å§æ¶é´") |
| | | private LocalDateTime startTime; |
| | | @Schema(description = "å®é
馿¬¡æ¥å·¥æ¶é´") |
| | | private LocalDateTime firstFeedbackTime; |
| | | @Schema(description = "å®é
æåæ¥å·¥æ¶é´") |
| | | private LocalDateTime lastFeedbackTime; |
| | | @Schema(description = "计å宿æ¶é´") |
| | | private LocalDateTime finishDate; |
| | | @Schema(description = "计åå·¥æ¶ï¼åéï¼") |
| | | private Integer plannedDuration; |
| | | @Schema(description = "å®é
èæ¶ï¼åéï¼é¦æ¬¡å°æ«æ¬¡æ¥å·¥ï¼") |
| | | private Long actualDuration; |
| | | |
| | | @Schema(description = "æ¯å¦å»¶æ") |
| | | private Boolean delayed; |
| | | @Schema(description = "å»¶æå¤©æ°ï¼æ£æ°è¡¨ç¤ºè¶
æï¼") |
| | | private Long delayDays; |
| | | @Schema(description = "å»¶æåå ï¼æå¯è§æµä¿¡å·ç»åºçåéï¼é人工确认ï¼") |
| | | private List<String> delayCauses; |
| | | |
| | | @Schema(description = "设å¤è¿è¡æ°æ®ï¼æè®¾å¤æ±æ»ï¼") |
| | | private List<DeviceRun> deviceRuns; |
| | | @Schema(description = "ç©ææ¶èï¼æç©ææ±æ»ï¼") |
| | | private List<MaterialConsumption> materialConsumptions; |
| | | |
| | | @Schema(description = "设å¤è¿è¡æ±æ»é¡¹") |
| | | @Data |
| | | public static class DeviceRun { |
| | | @Schema(description = "设å¤å°è´¦ç¼å·") |
| | | private Long machineryId; |
| | | @Schema(description = "设å¤ç¼ç ") |
| | | private String deviceCode; |
| | | @Schema(description = "é鿬¡æ°") |
| | | private Long sampleCount; |
| | | @Schema(description = "累计è¿è¡æ¶é¿") |
| | | private BigDecimal totalRunTime; |
| | | @Schema(description = "æè¿ééæ¶é´") |
| | | private LocalDateTime lastCollectTime; |
| | | @Schema(description = "æè¿è¿è¡ç¶æ") |
| | | private Integer lastRunStatus; |
| | | @Schema(description = "åè¦æ°") |
| | | private Long alarmCount; |
| | | } |
| | | |
| | | @Schema(description = "ç©ææ¶è项") |
| | | @Data |
| | | public static class MaterialConsumption { |
| | | @Schema(description = "ç©æç¼å·") |
| | | private Long itemId; |
| | | @Schema(description = "ç©æç¼ç ") |
| | | private String itemCode; |
| | | @Schema(description = "ç©æåç§°") |
| | | private String itemName; |
| | | @Schema(description = "æ¶èæ°é") |
| | | private BigDecimal quantity; |
| | | @Schema(description = "åä½") |
| | | private String unit; |
| | | } |
| | | } |
| | |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import java.util.List; |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | |
| | | @RestController @RequestMapping("/safety/risk-source") @Validated |
| | |
| | | public CommonResult<Boolean> update(@Valid @RequestBody SafetyRiskSourceSaveReqVO req){if(req.getId()==null)throw cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException("å±é©æºç¼å·ä¸è½ä¸ºç©º");service.save(req);return success(true);} |
| | | @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('safety:risk:delete')") |
| | | public CommonResult<Boolean> delete(@RequestParam("id") Long id){service.delete(id);return success(true);} |
| | | |
| | | /** ç¨è®¾å¤ééæ°æ®å·æ°å·²ç»å®è®¾å¤çå±é©æºï¼è¿åæ¬æ¬¡æ¥è¦æ¸
åã */ |
| | | @PostMapping("/refresh") @PreAuthorize("@ss.hasPermission('safety:risk:update')") |
| | | public CommonResult<List<SafetyRiskSourceDO>> refreshFromCollect(){return success(service.refreshFromCollect());} |
| | | } |
| | |
| | | @NotBlank @Pattern(regexp="MAJOR|HIGH_VOLTAGE|OTHER") private String category; |
| | | @Size(max=160) private String location; |
| | | @NotBlank @Size(max=80) private String metricName; |
| | | /** å
³è设å¤ç¼å·ï¼å¡«ååç±æ°éèªå¨å·æ°å½åå¼ã */ |
| | | @Positive private Long machineryId; |
| | | /** æ°éææ é®ï¼data_json ä¸çåæ®µåï¼ï¼ä¸è®¾å¤ç¼å·é
å使ç¨ã */ |
| | | @Size(max=64) private String metricKey; |
| | | @Size(max=32) private String unit; |
| | | @Digits(integer=14,fraction=6) private BigDecimal lowerLimit; |
| | | @Digits(integer=14,fraction=6) private BigDecimal upperLimit; |
| | |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO; |
| | | import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; |
| | | import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO; |
| | | import cn.iocoder.yudao.module.aftersales.api.AfterSaleTicketApi; |
| | | import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleTicketRespDTO; |
| | | import cn.iocoder.yudao.module.srm.api.certificate.SrmSupplierCertificateApi; |
| | | import cn.iocoder.yudao.module.srm.api.certificate.dto.SrmSupplierCertificateRespDTO; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO; |
| | | import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; |
| | |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | |
| | | private MdmUnitMeasureApi mdmUnitMeasureApi; |
| | | @Resource |
| | | private SrmSupplierApi srmSupplierApi; |
| | | @Resource |
| | | private SrmSupplierCertificateApi srmSupplierCertificateApi; |
| | | @Resource |
| | | private AfterSaleTicketApi afterSaleTicketApi; |
| | | @Resource |
| | | private CrmCustomerApi customerApi; |
| | | @Resource |
| | |
| | | return success(buildSalesTargetRespVOList(list)); |
| | | } |
| | | |
| | | @GetMapping("/certificates") |
| | | @Operation(summary = "æ¹æ¬¡åææåè§è¯ä¹¦", |
| | | description = "ææ¹æ¬¡çéè´æ¥æºä¾åºåæ¥è¯¢åè§è¯ä¹¦ï¼è¯ä¹¦è¿ææ è®°ç± SRM å¤å®") |
| | | @Parameter(name = "code", description = "æ¹æ¬¡ç¼ç ", required = true, example = "BATCH20250314001") |
| | | @PreAuthorize("@ss.hasPermission('mes:wm-batch:query')") |
| | | public CommonResult<List<SrmSupplierCertificateRespDTO>> getCertificateList(@RequestParam("code") @Valid String code) { |
| | | List<SrmSupplierCertificateRespDTO> result = new ArrayList<>(); |
| | | Set<Long> vendorIds = batchService.getPurchaseSourceList(code).stream() |
| | | .map(MesWmBatchPurchaseSourceRespVO::getVendorId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | for (Long vendorId : vendorIds) { |
| | | List<SrmSupplierCertificateRespDTO> list = |
| | | srmSupplierCertificateApi.getCertificatesBySupplierId(vendorId).getCheckedData(); |
| | | if (list != null) result.addAll(list); |
| | | } |
| | | return success(result); |
| | | } |
| | | |
| | | @GetMapping("/aftersales") |
| | | @Operation(summary = "æ¹æ¬¡å®åæ
éè®°å½", |
| | | description = "ææ¹æ¬¡éå®å»åå
³èçéå®è®¢åå·æ¥è¯¢å®åå·¥åï¼ç¨äºè´¨é追溯延伸å°å®åç¯è") |
| | | @Parameter(name = "code", description = "æ¹æ¬¡ç¼ç ", required = true, example = "BATCH20250314001") |
| | | @PreAuthorize("@ss.hasPermission('mes:wm-batch:query')") |
| | | public CommonResult<List<AfterSaleTicketRespDTO>> getAfterSaleList(@RequestParam("code") @Valid String code) { |
| | | List<AfterSaleTicketRespDTO> result = new ArrayList<>(); |
| | | Set<String> saleOrderNos = batchService.getSalesTargetList(code).stream() |
| | | .map(MesWmBatchSalesTargetRespVO::getSalesOrderCode) |
| | | .filter(Objects::nonNull).filter(s -> !s.isBlank()).collect(Collectors.toSet()); |
| | | for (String saleOrderNo : saleOrderNos) { |
| | | List<AfterSaleTicketRespDTO> list = |
| | | afterSaleTicketApi.getTicketsBySaleOrderNo(saleOrderNo).getCheckedData(); |
| | | if (list != null) result.addAll(list); |
| | | } |
| | | return success(result); |
| | | } |
| | | |
| | | @GetMapping("/process-trace") |
| | | @Operation(summary = "æ¹æ¬¡å·¥åºæµè½¬ï¼äº§åº/æå
¥äºä»¶ï¼") |
| | | @Parameter(name = "code", description = "æ¹æ¬¡ç¼ç ", required = true, example = "BATCH20250314001") |
| | |
| | | private String category; |
| | | private String location; |
| | | private String metricName; |
| | | /** å
³è设å¤å°è´¦ç¼å·ï¼ä¸ºç©ºè¡¨ç¤ºä»
人工å½å
¥ï¼ä¸æ¥æ°éã */ |
| | | private Long machineryId; |
| | | /** æ°éææ é®ï¼å mes_collect_realtime.data_json ä¸çåæ®µåï¼å¦ currentãspeedã */ |
| | | private String metricKey; |
| | | private String unit; |
| | | @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal lowerLimit; |
| | | @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal upperLimit; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 设å¤åæºæç»æ¥è¡¨ï¼æééé´éæ¨ç®æ¯å°è®¾å¤çåæºåºé´ä¸å计ã |
| | | * |
| | | * <p>ä¸å设å¤ç¦»çº¿æ¥è¯¢ä¿æåä¸å£å¾ï¼å¨çº¿è¶
æ¶éå¼ãä¿çæãä¸å¤©ä¸éï¼ï¼ |
| | | * ä¾ OEE æ¥è¡¨å®ä½åæºçæ¿ä½¿ç¨ã |
| | | */ |
| | | public cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO |
| | | getDowntimeDetail(LocalDateTime start, LocalDateTime end) { |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | if (start == null || end == null || !start.isBefore(end) || end.isAfter(now) |
| | | || Duration.between(start, end).compareTo(Duration.ofDays(31)) > 0) { |
| | | throw invalidParamException("è¯·éæ©ææçæ¶é´èå´ï¼å次æå¤æ¥è¯¢ä¸åä¸å¤©ï¼ç»ææ¶é´ä¸è½æäºå½åæ¶é´"); |
| | | } |
| | | if (start.isBefore(now.minusDays(collectProperties.getRetention().getRealtimeDays()))) { |
| | | throw invalidParamException("æ¥è¯¢æ¶é´è¶
åºæ°éåå²ä¿çæï¼æ æ³å¯é 计ç®åæºè®°å½"); |
| | | } |
| | | var result = new cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO(); |
| | | result.setStartTime(start); |
| | | result.setEndTime(end); |
| | | result.setTimeoutMinutes((int) ONLINE_TIMEOUT_MINUTES); |
| | | long totalMinutes = Duration.between(start, end).toMinutes(); |
| | | result.setTotalMinutes(totalMinutes); |
| | | |
| | | var machineries = machineryMapper.selectListWithCollectDeviceCode(); |
| | | List<cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO.Item> items = new ArrayList<>(); |
| | | long sumDowntime = 0L; |
| | | for (var machinery : machineries) { |
| | | String deviceCode = machinery.getCollectDeviceCode(); |
| | | var prior = realtimeHistoryMapper.selectOne(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>() |
| | | .eq(MesCollectRealtimeHistoryDO::getDeviceCode, deviceCode) |
| | | .ge(MesCollectRealtimeHistoryDO::getCollectTime, now.minusDays(collectProperties.getRetention().getRealtimeDays())) |
| | | .le(MesCollectRealtimeHistoryDO::getCollectTime, start) |
| | | .orderByDesc(MesCollectRealtimeHistoryDO::getCollectTime).last("LIMIT 1")); |
| | | var history = realtimeHistoryMapper.selectList(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>() |
| | | .eq(MesCollectRealtimeHistoryDO::getDeviceCode, deviceCode) |
| | | .gt(MesCollectRealtimeHistoryDO::getCollectTime, start) |
| | | .le(MesCollectRealtimeHistoryDO::getCollectTime, end) |
| | | .orderByAsc(MesCollectRealtimeHistoryDO::getCollectTime).last("LIMIT 100001")); |
| | | if (history.size() > 100000) throw invalidParamException("éæ ·æ°æ®è¿å¤ï¼è¯·ç¼©çæ¥è¯¢æ¶é´èå´"); |
| | | List<LocalDateTime> samples = new ArrayList<>(); |
| | | int count = 0; |
| | | if (prior != null) { samples.add(prior.getCollectTime()); count++; } |
| | | history.forEach(item -> samples.add(item.getCollectTime())); |
| | | count += history.size(); |
| | | var intervals = CollectOfflinePolicy.calculate(samples, start, end, Duration.ofMinutes(ONLINE_TIMEOUT_MINUTES)).stream() |
| | | .map(i -> new cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO.Interval( |
| | | i.start(), i.end(), Duration.between(i.start(), i.end()).getSeconds())) |
| | | .toList(); |
| | | long minutes = intervals.stream().mapToLong(i -> i.getSeconds() / 60).sum(); |
| | | sumDowntime += minutes; |
| | | items.add(new cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO.Item( |
| | | machinery.getId(), machinery.getCode(), machinery.getName(), deviceCode, |
| | | count, intervals.size(), minutes, intervals)); |
| | | } |
| | | items.sort((a, b) -> Long.compare(b.getDowntimeMinutes(), a.getDowntimeMinutes())); |
| | | result.setItems(items); |
| | | result.setDowntimeMinutes(sumDowntime); |
| | | result.setDowntimeRate(totalMinutes > 0 && !items.isEmpty() |
| | | ? Math.round(sumDowntime * 10000D / (totalMinutes * items.size())) / 100D : 0D); |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * è·åå·²é
ç½®æ°éç¼ç ç设å¤å®æ¶ç¶æå表 |
| | | */ |
| | | public List<MesCollectDeviceRespVO> getDeviceList() { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.dv.repair; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo.MesDvRepairDiagnosisRespVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectAlarmDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeHistoryDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectAlarmMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeHistoryMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeMapper; |
| | | import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService; |
| | | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.Duration; |
| | | import java.time.LocalDateTime; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | import tools.jackson.core.type.TypeReference; |
| | | |
| | | /** |
| | | * 设å¤è¿ç¨è¯æã |
| | | * |
| | | * <p>å£å¾è¯´æï¼æ¬æå¡åªè¯»åç°ææ°éæ°æ®ï¼å®æ¶è¡¨ãç¶æåå²ãåè¦ï¼ï¼ |
| | | * éè¿ééè¶
æ¶ãåè¦å¯é度ãè¿è¡ç¶æå¼å¸¸çä¿¡å·ç»åºè¯æå»ºè®®ï¼ |
| | | * ç»è®ºç¨äºç»´ä¿®å·¥ååèï¼ä¸æ¿ä»£ç°åºç¡®è®¤ï¼ä¹ä¸æ¨ææºæ¢°æ
éåå ã |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class MesDvRepairDiagnosisService { |
| | | |
| | | /** ééè¶
æ¶éå¼ï¼åéï¼ï¼è¶
è¿è¯¥æ¶é¿æ 䏿¥è§ä¸ºéä¿¡ä¸æã */ |
| | | private static final long COLLECT_TIMEOUT_MINUTES = 5; |
| | | /** è¯æåç天æ°ã */ |
| | | private static final int LOOKBACK_DAYS = 7; |
| | | /** å¯éåè¦éå¼ã */ |
| | | private static final long ALARM_DENSE_THRESHOLD = 3; |
| | | |
| | | @Resource private MesDvMachineryService machineryService; |
| | | @Resource private MesCollectRealtimeMapper realtimeMapper; |
| | | @Resource private MesCollectRealtimeHistoryMapper historyMapper; |
| | | @Resource private MesCollectAlarmMapper alarmMapper; |
| | | |
| | | public MesDvRepairDiagnosisRespVO diagnose(Long machineryId) { |
| | | MesDvMachineryDO machinery = machineryService.getMachinery(machineryId); |
| | | if (machinery == null) throw cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil |
| | | .invalidParamException("设å¤ä¸åå¨"); |
| | | |
| | | var resp = new MesDvRepairDiagnosisRespVO(); |
| | | resp.setMachineryId(machineryId); |
| | | resp.setMachineryCode(machinery.getCode()); |
| | | resp.setMachineryName(machinery.getName()); |
| | | List<String> evidence = new ArrayList<>(); |
| | | resp.setEvidence(evidence); |
| | | |
| | | // 1. 宿¶ééç¶æ |
| | | MesCollectRealtimeDO realtime = realtimeMapper.selectOne(new LambdaQueryWrapperX<MesCollectRealtimeDO>() |
| | | .eq(MesCollectRealtimeDO::getMachineryId, machineryId) |
| | | .orderByDesc(MesCollectRealtimeDO::getCollectTime) |
| | | .last("LIMIT 1")); |
| | | boolean hasData = realtime != null; |
| | | resp.setHasCollectData(hasData); |
| | | if (hasData) { |
| | | resp.setLastCollectTime(realtime.getCollectTime()); |
| | | resp.setRunStatus(realtime.getSts()); |
| | | resp.setOperateMode(realtime.getOpm()); |
| | | resp.setRunTime(realtime.getOt()); |
| | | resp.setMetrics(parseMetrics(realtime.getDataJson())); |
| | | boolean timeout = realtime.getCollectTime() == null |
| | | || Duration.between(realtime.getCollectTime(), LocalDateTime.now()).toMinutes() >= COLLECT_TIMEOUT_MINUTES; |
| | | resp.setCollectTimeout(timeout); |
| | | evidence.add(String.format("宿¶ééï¼æè¿ä¸æ¥ %sï¼ç¶æç %sï¼è¿è¡æ¨¡å¼ %s%s", |
| | | realtime.getCollectTime(), realtime.getSts(), realtime.getOpm(), |
| | | timeout ? String.format("ï¼å·²è¶
è¿ %d åéæ æ´æ°ï¼å¤å®éä¿¡ä¸æï¼", COLLECT_TIMEOUT_MINUTES) : "")); |
| | | } else { |
| | | resp.setCollectTimeout(true); |
| | | evidence.add("宿¶ééï¼è¯¥è®¾å¤æ ä»»ä½ééè®°å½ï¼æ æ³è¿ç¨å¤æè¿è¡ç¶æ"); |
| | | } |
| | | |
| | | // 2. æè¿åè¦ |
| | | LocalDateTime from = LocalDateTime.now().minusDays(LOOKBACK_DAYS); |
| | | List<MesCollectAlarmDO> alarms = alarmMapper.selectList(new LambdaQueryWrapperX<MesCollectAlarmDO>() |
| | | .eq(MesCollectAlarmDO::getMachineryId, machineryId) |
| | | .ge(MesCollectAlarmDO::getWmTime, from) |
| | | .orderByDesc(MesCollectAlarmDO::getWmTime) |
| | | .last("LIMIT 20")); |
| | | resp.setRecentAlarmCount((long) alarms.size()); |
| | | long active = alarms.stream().filter(a -> a.getClearTime() == null).count(); |
| | | resp.setActiveAlarmCount(active); |
| | | resp.setRecentAlarms(alarms.stream().map(a -> { |
| | | var item = new MesDvRepairDiagnosisRespVO.AlarmItem(); |
| | | item.setId(a.getId()); |
| | | item.setMessage(a.getWmMsg()); |
| | | item.setAlarmTime(a.getWmTime()); |
| | | item.setCleared(a.getClearTime() != null); |
| | | return item; |
| | | }).collect(Collectors.toList())); |
| | | if (!alarms.isEmpty()) { |
| | | evidence.add(String.format("åè¦ï¼è¿ %d 天 %d æ¡ï¼å
¶ä¸æªæ¸
é¤ %d æ¡", LOOKBACK_DAYS, alarms.size(), active)); |
| | | } else { |
| | | evidence.add(String.format("åè¦ï¼è¿ %d 天æ åè¦è®°å½", LOOKBACK_DAYS)); |
| | | } |
| | | |
| | | // 3. ç¶æåå²ï¼å¤ææ¯å¦é¢ç¹å¯åï¼ |
| | | List<MesCollectRealtimeHistoryDO> history = historyMapper.selectList( |
| | | new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>() |
| | | .eq(MesCollectRealtimeHistoryDO::getMachineryId, machineryId) |
| | | .ge(MesCollectRealtimeHistoryDO::getCollectTime, from)); |
| | | if (!history.isEmpty()) { |
| | | long distinctStatus = history.stream().map(MesCollectRealtimeHistoryDO::getSts) |
| | | .filter(Objects::nonNull).distinct().count(); |
| | | evidence.add(String.format("ç¶æåå²ï¼è¿ %d 天 %d æ¡éæ ·ï¼åºç° %d ç§è¿è¡ç¶æ", LOOKBACK_DAYS, history.size(), distinctStatus)); |
| | | if (distinctStatus >= 3) evidence.add("ç¶æé¢ç¹åæ¢ï¼å»ºè®®æ£æ¥å¯åæ§å¶ä¸å·¥èºç¨³å®æ§"); |
| | | } |
| | | |
| | | // 4. 综åå¤å® |
| | | String level; |
| | | String diagnosis; |
| | | if (!hasData) { |
| | | level = "UNKNOWN"; |
| | | diagnosis = "æ ééæ°æ®ï¼æ æ³è¿ç¨è¯æã建议ç°åºæ ¸æ¥æ°éç½å
³ä¾çµä¸é讯é¾è·¯ã"; |
| | | } else if (Boolean.TRUE.equals(resp.getCollectTimeout())) { |
| | | level = "CRITICAL"; |
| | | diagnosis = "ééæ°æ®å·²è¶
æ¶ï¼è®¾å¤å¯è½åæºæéè®¯ä¸æã建议å
ç¡®è®¤è®¾å¤æ¯å¦è¿è¡ãç½å
³æ¯å¦å¨çº¿ï¼å夿æ¯å¦éè¦æ¥ä¿®ã"; |
| | | } else if (active > 0) { |
| | | level = "CRITICAL"; |
| | | diagnosis = String.format("åå¨ %d æ¡æªæ¸
é¤åè¦ï¼è®¾å¤å¤äºå¼å¸¸è¿è¡ç¶æã建议æåè¦ä¿¡æ¯ææ¥åï¼åå³å®ç»´ä¿®æ¹æ¡ã", active); |
| | | } else if (alarms.size() >= ALARM_DENSE_THRESHOLD) { |
| | | level = "WARNING"; |
| | | diagnosis = String.format("è¿ %d 天åè¦ %d æ¡ï¼è½å·²æ¸
é¤ä½åçé¢ç¹ãå»ºè®®å®æé¢é²æ§ä¿å
»ï¼éç¹æ£æ¥åè¦å
³èé¨ä»¶ã", LOOKBACK_DAYS, alarms.size()); |
| | | } else { |
| | | level = "NORMAL"; |
| | | diagnosis = "é鿣叏䏿 ææ¾åè¦ï¼è®¾å¤è¿è¡ç¶æå¹³ç¨³ãå¦ç°åºä»åé¦å¼å¸¸ï¼è¯·è¡¥å
å
·ä½ç°è±¡å忬¡è¯æã"; |
| | | } |
| | | resp.setRiskLevel(level); |
| | | resp.setDiagnosis(diagnosis); |
| | | return resp; |
| | | } |
| | | |
| | | /** è§£æ data_json ä¸çæ°å¼åææ ï¼æå¤ä¿ç 20 项ï¼é¿å
ååºè¿å¤§ã */ |
| | | Map<String, BigDecimal> parseMetrics(String dataJson) { |
| | | if (dataJson == null || dataJson.isBlank()) return Map.of(); |
| | | try { |
| | | Map<String, Object> raw = JsonUtils.parseObjectQuietly(dataJson, new TypeReference<Map<String, Object>>() {}); |
| | | if (raw == null) return Map.of(); |
| | | Map<String, BigDecimal> result = new LinkedHashMap<>(); |
| | | for (var e : raw.entrySet()) { |
| | | if (result.size() >= 20) break; |
| | | Object v = e.getValue(); |
| | | if (v instanceof Number n) result.put(e.getKey(), new BigDecimal(n.toString())); |
| | | else if (v instanceof String s && s.matches("-?\\d+(\\.\\d+)?")) result.put(e.getKey(), new BigDecimal(s)); |
| | | } |
| | | return result; |
| | | } catch (Exception e) { |
| | | log.debug("è§£æè®¾å¤ææ 失败ï¼{}", e.getMessage()); |
| | | return Map.of(); |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.feedback; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProTaskExecutionRespVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectAlarmDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeHistoryDO; |
| | | 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.pro.task.MesProTaskDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskIssueDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectAlarmMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeHistoryMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.md.unitmeasure.MesMdUnitMeasureMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskIssueMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.Duration; |
| | | import java.time.LocalDateTime; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| | | |
| | | /** |
| | | * çäº§ä»»å¡æ§è¡åé¦åæã |
| | | * |
| | | * <p>æè®¡åãæ¥å·¥ã设å¤ééãç©ææ¶èã交æåå·®æ±æ»ï¼ç¨äºå®ä½å»¶æåå ã |
| | | * å£å¾è¯´æï¼ |
| | | * <ul> |
| | | * <li>å®é
èæ¶ = 馿¬¡æ¥å·¥å°æ«æ¬¡æ¥å·¥ï¼æ æ¥å·¥å为空ï¼ä¸ç¨è®¡åå¼ä»£æ¿ï¼ï¼</li> |
| | | * <li>å»¶æå¤å® = æ«æ¬¡æ¥å·¥æ¶é´(æå½åæ¶é´) æäºè®¡å宿æ¶é´ï¼</li> |
| | | * <li>å»¶æåå åªåå¯è§æµä¿¡å·ï¼ç¼ºæãåè¦ã设å¤ç¦»çº¿ãæ¥å·¥æ»åï¼ï¼ä¸æ¨æç®¡çåå ã</li> |
| | | * </ul> |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class MesProTaskExecutionService { |
| | | |
| | | @Resource private MesProTaskMapper taskMapper; |
| | | @Resource private MesProTaskIssueMapper taskIssueMapper; |
| | | @Resource private MesCollectRealtimeHistoryMapper historyMapper; |
| | | @Resource private MesCollectAlarmMapper alarmMapper; |
| | | @Resource private MesMdItemMapper itemMapper; |
| | | @Resource private MesMdUnitMeasureMapper unitMeasureMapper; |
| | | |
| | | public MesProTaskExecutionRespVO getExecution(Long taskId) { |
| | | MesProTaskDO task = taskId == null ? null : taskMapper.selectById(taskId); |
| | | if (task == null) throw invalidParamException("ç产任å¡ä¸åå¨"); |
| | | |
| | | var resp = new MesProTaskExecutionRespVO(); |
| | | resp.setTaskId(task.getId()); |
| | | resp.setTaskCode(task.getCode()); |
| | | resp.setTaskName(task.getName()); |
| | | resp.setWorkOrderId(task.getWorkOrderId()); |
| | | resp.setStatus(task.getStatus()); |
| | | resp.setPlannedQuantity(task.getQuantity()); |
| | | resp.setProducedQuantity(task.getProducedQuantity()); |
| | | resp.setQualifyQuantity(task.getQualifyQuantity()); |
| | | resp.setUnqualifyQuantity(task.getUnqualifyQuantity()); |
| | | resp.setStartTime(task.getStartTime()); |
| | | resp.setFirstFeedbackTime(task.getFirstFeedbackTime()); |
| | | resp.setLastFeedbackTime(task.getLastFeedbackTime()); |
| | | resp.setFinishDate(task.getFinishDate()); |
| | | resp.setPlannedDuration(task.getDuration()); |
| | | |
| | | // 宿ç |
| | | if (task.getQuantity() != null && task.getQuantity().signum() > 0 && task.getProducedQuantity() != null) { |
| | | resp.setCompletionRate(task.getProducedQuantity() |
| | | .multiply(BigDecimal.valueOf(100)) |
| | | .divide(task.getQuantity(), 2, RoundingMode.HALF_UP)); |
| | | } |
| | | |
| | | // å®é
èæ¶ |
| | | if (task.getFirstFeedbackTime() != null && task.getLastFeedbackTime() != null) { |
| | | resp.setActualDuration(Duration.between(task.getFirstFeedbackTime(), task.getLastFeedbackTime()).toMinutes()); |
| | | } |
| | | |
| | | // 设å¤è¿è¡æ°æ® |
| | | List<MesCollectRealtimeHistoryDO> history = historyMapper.selectList( |
| | | new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>() |
| | | .eq(MesCollectRealtimeHistoryDO::getTaskId, taskId)); |
| | | List<MesProTaskExecutionRespVO.DeviceRun> deviceRuns = new ArrayList<>(); |
| | | Map<Long, List<MesCollectRealtimeHistoryDO>> byMachinery = history.stream() |
| | | .filter(h -> h.getMachineryId() != null) |
| | | .collect(Collectors.groupingBy(MesCollectRealtimeHistoryDO::getMachineryId)); |
| | | for (var e : byMachinery.entrySet()) { |
| | | List<MesCollectRealtimeHistoryDO> list = e.getValue(); |
| | | var run = new MesProTaskExecutionRespVO.DeviceRun(); |
| | | run.setMachineryId(e.getKey()); |
| | | run.setDeviceCode(list.get(0).getDeviceCode()); |
| | | run.setSampleCount((long) list.size()); |
| | | run.setTotalRunTime(list.stream().map(MesCollectRealtimeHistoryDO::getOt) |
| | | .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); |
| | | MesCollectRealtimeHistoryDO last = list.stream() |
| | | .max(Comparator.comparing(MesCollectRealtimeHistoryDO::getCollectTime, |
| | | Comparator.nullsFirst(Comparator.naturalOrder()))).orElse(null); |
| | | if (last != null) { |
| | | run.setLastCollectTime(last.getCollectTime()); |
| | | run.setLastRunStatus(last.getSts()); |
| | | } |
| | | run.setAlarmCount(alarmMapper.selectCount(new LambdaQueryWrapperX<MesCollectAlarmDO>() |
| | | .eq(MesCollectAlarmDO::getTaskId, taskId) |
| | | .eq(MesCollectAlarmDO::getMachineryId, e.getKey()))); |
| | | deviceRuns.add(run); |
| | | } |
| | | resp.setDeviceRuns(deviceRuns); |
| | | |
| | | // ç©ææ¶è |
| | | List<MesProTaskIssueDO> issues = taskIssueMapper.selectList( |
| | | new LambdaQueryWrapperX<MesProTaskIssueDO>().eq(MesProTaskIssueDO::getTaskId, taskId)); |
| | | Set<Long> itemIds = issues.stream().map(MesProTaskIssueDO::getItemId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | Map<Long, MesMdItemDO> itemMap = itemIds.isEmpty() ? Map.of() |
| | | : itemMapper.selectBatchIds(itemIds).stream() |
| | | .collect(Collectors.toMap(MesMdItemDO::getId, i -> i, (a, b) -> a)); |
| | | Set<Long> unitIds = itemMap.values().stream().map(MesMdItemDO::getUnitMeasureId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | Map<Long, String> unitNameMap = unitIds.isEmpty() ? Map.of() |
| | | : unitMeasureMapper.selectBatchIds(unitIds).stream() |
| | | .collect(Collectors.toMap(MesMdUnitMeasureDO::getId, MesMdUnitMeasureDO::getName, (a, b) -> a)); |
| | | Map<Long, MesProTaskExecutionRespVO.MaterialConsumption> consumptionMap = new LinkedHashMap<>(); |
| | | for (MesProTaskIssueDO issue : issues) { |
| | | if (issue.getItemId() == null) continue; |
| | | var mc = consumptionMap.computeIfAbsent(issue.getItemId(), id -> { |
| | | var item = new MesProTaskExecutionRespVO.MaterialConsumption(); |
| | | item.setItemId(id); |
| | | MesMdItemDO md = itemMap.get(id); |
| | | if (md != null) { |
| | | item.setItemCode(md.getCode()); |
| | | item.setItemName(md.getName()); |
| | | if (md.getUnitMeasureId() != null) item.setUnit(unitNameMap.get(md.getUnitMeasureId())); |
| | | } |
| | | item.setQuantity(BigDecimal.ZERO); |
| | | return item; |
| | | }); |
| | | BigDecimal used = issue.getUsedQuantity() != null ? issue.getUsedQuantity() |
| | | : (issue.getIssuedQuantity() != null ? issue.getIssuedQuantity() : BigDecimal.ZERO); |
| | | mc.setQuantity(mc.getQuantity().add(used)); |
| | | } |
| | | resp.setMaterialConsumptions(new ArrayList<>(consumptionMap.values())); |
| | | |
| | | // 交æåå·® |
| | | boolean delayed = false; |
| | | Long delayDays = null; |
| | | LocalDateTime reference = task.getLastFeedbackTime() != null ? task.getLastFeedbackTime() : LocalDateTime.now(); |
| | | if (task.getFinishDate() != null && reference.isAfter(task.getFinishDate())) { |
| | | delayed = true; |
| | | delayDays = Duration.between(task.getFinishDate(), reference).toDays(); |
| | | } |
| | | resp.setDelayed(delayed); |
| | | resp.setDelayDays(delayDays); |
| | | |
| | | // å»¶æåå ï¼å¯è§æµä¿¡å·ï¼ |
| | | List<String> causes = new ArrayList<>(); |
| | | if (delayed) { |
| | | if (deviceRuns.stream().anyMatch(r -> r.getAlarmCount() != null && r.getAlarmCount() > 0)) |
| | | causes.add("æ§è¡æé´åå¨è®¾å¤åè¦ï¼å¯è½å½±åè¿åº¦"); |
| | | if (task.getFirstFeedbackTime() == null) |
| | | causes.add("ä»»å¡å°æªé¦æ¬¡æ¥å·¥ï¼å®é
å¼å·¥æ¶é´ç¼ºå¤±"); |
| | | else if (task.getStartTime() != null && task.getFirstFeedbackTime().isAfter(task.getStartTime().plusDays(1))) |
| | | causes.add("馿¬¡æ¥å·¥æ¶é´ææ¾æäºè®¡åå¼å§æ¶é´"); |
| | | boolean hasShortage = issues.stream().anyMatch(i -> |
| | | i.getIssuedQuantity() != null && i.getUsedQuantity() != null |
| | | && i.getUsedQuantity().compareTo(i.getIssuedQuantity()) >= 0); |
| | | if (hasShortage) causes.add("ææéå·²ç¨å°½ï¼åå¨ç¼ºæåå·¥å¯è½"); |
| | | if (deviceRuns.isEmpty()) causes.add("æ 设å¤ééæ°æ®ï¼æ æ³å¤æè®¾å¤ä¾§å½±å"); |
| | | if (task.getDuration() != null && resp.getActualDuration() != null |
| | | && resp.getActualDuration() > task.getDuration()) |
| | | causes.add(String.format("å®é
èæ¶ %d åéè¶
è¿è®¡å %d åé", resp.getActualDuration(), task.getDuration())); |
| | | } |
| | | resp.setDelayCauses(causes); |
| | | return resp; |
| | | } |
| | | } |
| | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.risk.SafetyRiskSourceSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyRiskSourceDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyRiskSourceMapper; |
| | | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Objects; |
| | | import java.util.*; |
| | | import tools.jackson.core.type.TypeReference; |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| | | |
| | | /** |
| | | * å®å
¨ç产 - å±é©æºçæµã |
| | | * |
| | | * <p>æ¯æä¸¤ç§æ°æ®æ¥æºï¼ |
| | | * <ol> |
| | | * <li>人工å½å
¥ currentValueï¼ä»
表示人工æå½çå½åå¼ï¼</li> |
| | | * <li>ç»å®è®¾å¤ï¼machineryId + metricKeyï¼ï¼ç±æ°é宿¶è¡¨èªå¨åå¼ï¼å¤ç¨ç°æè®¾å¤ééï¼ä¸å¼å
¥æ°æ¥å£ã</li> |
| | | * </ol> |
| | | */ |
| | | @Slf4j |
| | | @Service public class SafetyRiskSourceService { |
| | | @Resource private SafetyRiskSourceMapper mapper; |
| | | @Resource private MesCollectRealtimeMapper realtimeMapper; |
| | | |
| | | public PageResult<SafetyRiskSourceDO> page(PageParam page, String name) { |
| | | return mapper.selectPage(page, new LambdaQueryWrapperX<SafetyRiskSourceDO>().likeIfPresent(SafetyRiskSourceDO::getName,name).orderByDesc(SafetyRiskSourceDO::getId)); |
| | | } |
| | | |
| | | @Transactional(rollbackFor=Exception.class) |
| | | public Long save(SafetyRiskSourceSaveReqVO req) { |
| | | SafetyRiskSourceDO old = req.getId()==null ? null : mapper.selectById(req.getId()); |
| | | if (req.getId()!=null && old==null) throw invalidParamException("å±é©æºä¸åå¨"); |
| | | if (req.getMachineryId()!=null && (req.getMetricKey()==null || req.getMetricKey().isBlank())) |
| | | throw invalidParamException("ç»å®è®¾å¤åå¿
须填忰鿿 é®"); |
| | | if (req.getMachineryId()==null && req.getMetricKey()!=null && !req.getMetricKey().isBlank()) |
| | | throw invalidParamException("å¡«åæ°éææ é®å请å
ç»å®è®¾å¤"); |
| | | String status; |
| | | try { status=SafetyRiskThreshold.evaluate(req.getCurrentValue(),req.getLowerLimit(),req.getUpperLimit()); } |
| | | catch (IllegalArgumentException e) { throw invalidParamException(e.getMessage()); } |
| | |
| | | if (old==null) mapper.insert(row); else mapper.updateById(row); |
| | | return row.getId(); |
| | | } |
| | | |
| | | public void delete(Long id) { if (mapper.selectById(id)==null) throw invalidParamException("å±é©æºä¸åå¨"); mapper.deleteById(id); } |
| | | |
| | | /** |
| | | * ç¨è®¾å¤ééæ°æ®å·æ°ææå·²ç»å®è®¾å¤çå±é©æºã |
| | | * |
| | | * <p>åå¼é¡ºåºï¼data_json ä¸ metricKey 对åºåæ®µï¼æªåå°æ¶ç½® UNKNOWN å¹¶ä¿ç䏿¬¡è§æµæ¶é´ä¸åã |
| | | * |
| | | * @return æ¬æ¬¡å·æ°åå¤äº ALARM ç¶æçå±é©æº |
| | | */ |
| | | @Transactional(rollbackFor=Exception.class) |
| | | public List<SafetyRiskSourceDO> refreshFromCollect() { |
| | | List<SafetyRiskSourceDO> bound = mapper.selectList(new LambdaQueryWrapperX<SafetyRiskSourceDO>() |
| | | .isNotNull(SafetyRiskSourceDO::getMachineryId)); |
| | | List<SafetyRiskSourceDO> alarms = new ArrayList<>(); |
| | | Map<Long, MesCollectRealtimeDO> cache = new HashMap<>(); |
| | | for (SafetyRiskSourceDO source : bound) { |
| | | MesCollectRealtimeDO realtime = cache.computeIfAbsent(source.getMachineryId(), id -> |
| | | realtimeMapper.selectOne(new LambdaQueryWrapperX<MesCollectRealtimeDO>() |
| | | .eq(MesCollectRealtimeDO::getMachineryId, id) |
| | | .orderByDesc(MesCollectRealtimeDO::getCollectTime).last("LIMIT 1"))); |
| | | BigDecimal value = realtime == null ? null : extractMetric(realtime, source.getMetricKey()); |
| | | String status; |
| | | try { status = SafetyRiskThreshold.evaluate(value, source.getLowerLimit(), source.getUpperLimit()); } |
| | | catch (IllegalArgumentException e) { log.warn("å±é©æº[{}]éå¼é
ç½®éæ³ï¼{}", source.getName(), e.getMessage()); continue; } |
| | | if (value == null) { |
| | | // åä¸å°ééå¼ï¼ä¸è¦çåå²è§æµæ¶é´ï¼é¿å
ä¼ªé æ°æ® |
| | | if (!"UNKNOWN".equals(source.getRiskStatus())) { |
| | | var upd = new SafetyRiskSourceDO(); |
| | | upd.setId(source.getId()); upd.setRiskStatus(status); |
| | | mapper.updateById(upd); |
| | | source.setRiskStatus(status); |
| | | } |
| | | continue; |
| | | } |
| | | var upd = new SafetyRiskSourceDO(); |
| | | upd.setId(source.getId()); |
| | | upd.setCurrentValue(value); |
| | | upd.setObservedAt(realtime.getCollectTime() == null ? LocalDateTime.now() : realtime.getCollectTime()); |
| | | upd.setRiskStatus(status); |
| | | mapper.updateById(upd); |
| | | source.setCurrentValue(value); |
| | | source.setObservedAt(upd.getObservedAt()); |
| | | source.setRiskStatus(status); |
| | | if ("ALARM".equals(status)) alarms.add(source); |
| | | } |
| | | return alarms; |
| | | } |
| | | |
| | | /** ä»å®æ¶ééè®°å½ä¸æåææ å¼ï¼ä¼å
data_jsonï¼å
¶æ¬¡å¤ç¨å®æ¶è¡¨çæ ååæ®µã */ |
| | | BigDecimal extractMetric(MesCollectRealtimeDO realtime, String metricKey) { |
| | | if (metricKey == null || metricKey.isBlank() || realtime == null) return null; |
| | | String json = realtime.getDataJson(); |
| | | if (json != null && !json.isBlank()) { |
| | | try { |
| | | Map<String, Object> map = JsonUtils.parseObjectQuietly(json, new TypeReference<Map<String, Object>>() {}); |
| | | Object raw = map == null ? null : map.get(metricKey); |
| | | if (raw != null) { |
| | | if (raw instanceof Number n) return new BigDecimal(n.toString()); |
| | | String text = raw.toString().trim(); |
| | | if (!text.isEmpty()) return new BigDecimal(text); |
| | | } |
| | | } catch (Exception e) { |
| | | log.debug("è§£ææ°é data_json 失败ï¼machineryId={}ï¼{}", realtime.getMachineryId(), e.getMessage()); |
| | | } |
| | | } |
| | | // å
¼å®¹ï¼metricKey ç´æ¥å宿¶è¡¨å段ï¼ot è¿è¡æ¶é¿çï¼ |
| | | return switch (metricKey) { |
| | | case "ot" -> realtime.getOt(); |
| | | case "sts" -> realtime.getSts() == null ? null : BigDecimal.valueOf(realtime.getSts()); |
| | | case "opm" -> realtime.getOpm() == null ? null : BigDecimal.valueOf(realtime.getOpm()); |
| | | default -> null; |
| | | }; |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.api.certificate; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.module.srm.api.certificate.dto.SrmSupplierCertificateRespDTO; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * ä¾åºååè§è¯ä¹¦ APIï¼ä¾ MES è´¨é追溯ç跨模ååªè¯»è°ç¨ï¼ |
| | | */ |
| | | public interface SrmSupplierCertificateApi { |
| | | |
| | | String PREFIX = "/rpc-api/srm/supplier-certificate"; |
| | | |
| | | /** |
| | | * æä¾åºåæ¥è¯¢è¯ä¹¦å表 |
| | | * |
| | | * @param supplierId ä¾åºåç¼å· |
| | | * @return è¯ä¹¦å表ï¼å«æ¯å¦è¿ææ è®°ï¼ |
| | | */ |
| | | CommonResult<List<SrmSupplierCertificateRespDTO>> getCertificatesBySupplierId(Long supplierId); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.api.certificate.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.time.LocalDate; |
| | | |
| | | /** |
| | | * ä¾åºååè§è¯ä¹¦ï¼è·¨æ¨¡ååªè¯»è§å¾ï¼ |
| | | */ |
| | | @Data |
| | | public class SrmSupplierCertificateRespDTO { |
| | | private Long id; |
| | | private Long supplierId; |
| | | /** è¯ä¹¦ç±»åï¼åå
¸ srm_supplier_certificate_typeï¼ */ |
| | | private Integer certificateType; |
| | | private String certificateName; |
| | | private String certificateNo; |
| | | private String issuingAuthority; |
| | | private LocalDate validStartDate; |
| | | private LocalDate validEndDate; |
| | | /** 0 ææ 1 åç¨ */ |
| | | private Integer status; |
| | | private Boolean expired; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.api.certificate; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.srm.api.certificate.dto.SrmSupplierCertificateRespDTO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierCertificateDO; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierCertificateMapper; |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.util.List; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | |
| | | /** |
| | | * ä¾åºååè§è¯ä¹¦ API å®ç° |
| | | */ |
| | | @RestController |
| | | @Validated |
| | | public class SrmSupplierCertificateApiImpl implements SrmSupplierCertificateApi { |
| | | |
| | | @Resource |
| | | private SrmSupplierCertificateMapper certificateMapper; |
| | | |
| | | @Override |
| | | public CommonResult<List<SrmSupplierCertificateRespDTO>> getCertificatesBySupplierId(Long supplierId) { |
| | | if (supplierId == null) return success(List.of()); |
| | | List<SrmSupplierCertificateDO> list = certificateMapper.selectList( |
| | | new LambdaQueryWrapperX<SrmSupplierCertificateDO>() |
| | | .eq(SrmSupplierCertificateDO::getSupplierId, supplierId) |
| | | .orderByDesc(SrmSupplierCertificateDO::getId)); |
| | | LocalDate today = LocalDate.now(); |
| | | return success(list.stream().map(d -> { |
| | | var dto = BeanUtils.toBean(d, SrmSupplierCertificateRespDTO.class); |
| | | dto.setExpired(d.getValidEndDate() != null && d.getValidEndDate().isBefore(today)); |
| | | return dto; |
| | | }).toList()); |
| | | } |
| | | } |
| | |
| | | package cn.iocoder.yudao.module.srm.controller.admin.statistics; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.module.srm.controller.admin.statistics.vo.SrmCostAnalysisRespVO; |
| | | import cn.iocoder.yudao.module.srm.controller.admin.statistics.vo.SrmTenderStatisticsRespVO; |
| | | import cn.iocoder.yudao.module.srm.service.statistics.SrmCostAnalysisService; |
| | | import cn.iocoder.yudao.module.srm.service.statistics.SrmStatisticsService; |
| | | import jakarta.validation.Valid; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import jakarta.annotation.Resource; |
| | |
| | | |
| | | @Resource |
| | | private SrmStatisticsService statisticsService; |
| | | @Resource |
| | | private SrmCostAnalysisService costAnalysisService; |
| | | |
| | | @GetMapping("/tender") |
| | | @Operation(summary = "è·åææ ç»è®¡æ°æ®") |
| | |
| | | return success(statisticsService.getTenderStatistics(year)); |
| | | } |
| | | |
| | | @GetMapping("/cost-analysis") |
| | | @Operation(summary = "éè´ææ¬åæ", |
| | | description = "åºäº SRM ææ éæ±éä¸ä¾åºåæ¥ä»·èåï¼éè´éãå æåä»·ãæä½/æé«ä»·ãä»·æ ¼åå¨ï¼ä¸ä¾èµ ERP") |
| | | @PreAuthorize("@ss.hasPermission('srm:statistics:query')") |
| | | public CommonResult<PageResult<SrmCostAnalysisRespVO>> getCostAnalysis( |
| | | @Valid PageParam page, |
| | | @RequestParam(value = "productName", required = false) String productName) { |
| | | return success(costAnalysisService.page(page, productName)); |
| | | } |
| | | |
| | | @GetMapping("/cost-trend") |
| | | @Operation(summary = "ç©æä»·æ ¼è¶å¿", description = "æææ±æ»è¯¥ç©æç平忥价") |
| | | @PreAuthorize("@ss.hasPermission('srm:statistics:query')") |
| | | public CommonResult<java.util.List<SrmCostAnalysisRespVO.TrendPoint>> getCostTrend( |
| | | @RequestParam("productId") Long productId) { |
| | | return success(costAnalysisService.trend(productId)); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.controller.admin.statistics.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * éè´ææ¬åæ Response VOã |
| | | * |
| | | * <p>æ°æ®æ¥æºï¼SRM ææ ç©æéæ±ï¼æ°éãé¢ä¼°åä»·ï¼+ ä¾åºåæ¥ä»·ï¼å«ç¨åä»·ãç¨çï¼ã |
| | | * ä¸ä¾èµ ERP éè´è®¢åï¼éç¨äºæªå¯ç¨ ERP ç项ç®ã |
| | | */ |
| | | @Schema(description = "管çåå° - éè´ææ¬åæ Response VO") |
| | | @Data |
| | | public class SrmCostAnalysisRespVO { |
| | | |
| | | @Schema(description = "ç©æç¼å·") |
| | | private Long productId; |
| | | @Schema(description = "ç©æç¼ç ") |
| | | private String productCode; |
| | | @Schema(description = "ç©æåç§°") |
| | | private String productName; |
| | | @Schema(description = "è§æ ¼") |
| | | private String productSpec; |
| | | @Schema(description = "åä½") |
| | | private String unit; |
| | | |
| | | @Schema(description = "éè´éæ±é") |
| | | private BigDecimal totalQuantity; |
| | | @Schema(description = "æ¥ä»·æ¬¡æ°") |
| | | private Integer quoteCount; |
| | | @Schema(description = "å䏿¥ä»·çä¾åºåæ°") |
| | | private Integer supplierCount; |
| | | |
| | | @Schema(description = "å æå¹³åæ¥ä»·ï¼æéæ±éå æï¼") |
| | | private BigDecimal weightedAvgPrice; |
| | | @Schema(description = "æä½æ¥ä»·") |
| | | private BigDecimal minPrice; |
| | | @Schema(description = "æé«æ¥ä»·") |
| | | private BigDecimal maxPrice; |
| | | @Schema(description = "é¢ä¼°åä»·") |
| | | private BigDecimal estimatedPrice; |
| | | @Schema(description = "ç¸å¯¹é¢ä¼°ä»·çåå·®çï¼%ï¼") |
| | | private BigDecimal deviationRate; |
| | | |
| | | @Schema(description = "æè¿æ¥ä»·æ¶é´") |
| | | private LocalDateTime lastQuoteTime; |
| | | @Schema(description = "ä»·æ ¼åå¨ï¼æè¿ä¸¤æ¬¡æ¥ä»·å·®ï¼") |
| | | private BigDecimal priceChange; |
| | | |
| | | @Schema(description = "æ¥ä»·æç»") |
| | | private List<QuoteItem> quotes; |
| | | |
| | | @Schema(description = "æ¥ä»·æç»é¡¹") |
| | | @Data |
| | | public static class QuoteItem { |
| | | @Schema(description = "ä¾åºåç¼å·") |
| | | private Long supplierId; |
| | | @Schema(description = "ä¾åºååç§°") |
| | | private String supplierName; |
| | | @Schema(description = "æ¥ä»·åä»·") |
| | | private BigDecimal quotePrice; |
| | | @Schema(description = "ç¨çï¼%ï¼") |
| | | private BigDecimal taxRate; |
| | | @Schema(description = "交æï¼å¤©ï¼") |
| | | private Integer deliveryCycle; |
| | | @Schema(description = "æ¥ä»·æ¶é´") |
| | | private LocalDateTime quoteTime; |
| | | } |
| | | |
| | | @Schema(description = "ä»·æ ¼è¶å¿ç¹") |
| | | @Data |
| | | public static class TrendPoint { |
| | | @Schema(description = "æä»½ yyyy-MM") |
| | | private String month; |
| | | @Schema(description = "平忥价") |
| | | private BigDecimal avgPrice; |
| | | @Schema(description = "æ¥ä»·æ¬¡æ°") |
| | | private Integer count; |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.service.statistics; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.srm.controller.admin.statistics.vo.SrmCostAnalysisRespVO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierQuoteDO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierMapper; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierQuoteMapper; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderMaterialMapper; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDateTime; |
| | | import java.time.YearMonth; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * éè´ææ¬åæï¼åºäº SRM ææ éæ±ä¸ä¾åºåæ¥ä»·ï¼ã |
| | | * |
| | | * <p>å£å¾è¯´æï¼ |
| | | * <ul> |
| | | * <li>éè´éæ±éåææ ç©æéæ±æ°éå计ï¼</li> |
| | | * <li>å æåä»· = Σ(æ¥ä»·åä»· Ã éæ±æ°é) / Σ(éæ±æ°é)ï¼å¤ä¾åºåæ¥ä»·ä¸å¹¶è®¡å
¥ï¼</li> |
| | | * <li>ä»·æ ¼åå¨ = æè¿ä¸¤æ¬¡æ¥ä»·ä¹å·®ï¼ä¸è¶³ä¸¤æ¬¡æ¶ä¸ºç©ºï¼</li> |
| | | * <li>ä¸ä¾èµ ERP éè´è®¢åï¼æªå¯ç¨ ERP ç项ç®åæ ·å¯ç¨ã</li> |
| | | * </ul> |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class SrmCostAnalysisService { |
| | | |
| | | @Resource private SrmTenderMaterialMapper tenderMaterialMapper; |
| | | @Resource private SrmSupplierQuoteMapper supplierQuoteMapper; |
| | | @Resource private SrmSupplierMapper supplierMapper; |
| | | |
| | | public PageResult<SrmCostAnalysisRespVO> page(PageParam page, String productName) { |
| | | List<SrmTenderMaterialDO> materials = tenderMaterialMapper.selectList( |
| | | new LambdaQueryWrapperX<SrmTenderMaterialDO>() |
| | | .likeIfPresent(SrmTenderMaterialDO::getProductName, productName)); |
| | | if (materials.isEmpty()) return PageResult.empty(); |
| | | |
| | | // æç©æèåï¼åä¸ç©æå¯è½åºç°å¨å¤ä¸ªææ 项ç®ï¼ |
| | | Map<String, List<SrmTenderMaterialDO>> grouped = materials.stream() |
| | | .filter(m -> m.getProductId() != null || m.getProductName() != null) |
| | | .collect(Collectors.groupingBy( |
| | | m -> m.getProductId() != null ? "ID:" + m.getProductId() : "NAME:" + m.getProductName(), |
| | | LinkedHashMap::new, Collectors.toList())); |
| | | |
| | | List<SrmCostAnalysisRespVO> rows = new ArrayList<>(); |
| | | for (var entry : grouped.entrySet()) { |
| | | List<SrmTenderMaterialDO> group = entry.getValue(); |
| | | SrmTenderMaterialDO first = group.get(0); |
| | | Set<Long> materialIds = group.stream().map(SrmTenderMaterialDO::getId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | if (materialIds.isEmpty()) continue; |
| | | |
| | | List<SrmSupplierQuoteDO> quotes = supplierQuoteMapper.selectList( |
| | | new LambdaQueryWrapperX<SrmSupplierQuoteDO>() |
| | | .in(SrmSupplierQuoteDO::getTenderMaterialId, materialIds)); |
| | | if (quotes.isEmpty()) continue; |
| | | |
| | | // éæ±æ°éæ å° |
| | | Map<Long, BigDecimal> qtyMap = group.stream() |
| | | .filter(m -> m.getId() != null) |
| | | .collect(Collectors.toMap(SrmTenderMaterialDO::getId, |
| | | m -> m.getQuantity() == null ? BigDecimal.ZERO : m.getQuantity(), (a, b) -> a)); |
| | | |
| | | BigDecimal totalQty = qtyMap.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | BigDecimal weightedSum = BigDecimal.ZERO; |
| | | BigDecimal weightQty = BigDecimal.ZERO; |
| | | for (SrmSupplierQuoteDO q : quotes) { |
| | | if (q.getQuotePrice() == null) continue; |
| | | BigDecimal qty = qtyMap.getOrDefault(q.getTenderMaterialId(), BigDecimal.ZERO); |
| | | if (qty.signum() <= 0) continue; |
| | | weightedSum = weightedSum.add(q.getQuotePrice().multiply(qty)); |
| | | weightQty = weightQty.add(qty); |
| | | } |
| | | |
| | | var row = new SrmCostAnalysisRespVO(); |
| | | row.setProductId(first.getProductId()); |
| | | row.setProductCode(first.getProductCode()); |
| | | row.setProductName(first.getProductName()); |
| | | row.setProductSpec(first.getProductSpec()); |
| | | row.setUnit(first.getUnit()); |
| | | row.setTotalQuantity(totalQty); |
| | | row.setEstimatedPrice(first.getEstimatedPrice()); |
| | | row.setQuoteCount(quotes.size()); |
| | | row.setSupplierCount((int) quotes.stream().map(SrmSupplierQuoteDO::getSupplierId) |
| | | .filter(Objects::nonNull).distinct().count()); |
| | | row.setWeightedAvgPrice(weightQty.signum() > 0 |
| | | ? weightedSum.divide(weightQty, 4, RoundingMode.HALF_UP) : null); |
| | | row.setMinPrice(quotes.stream().map(SrmSupplierQuoteDO::getQuotePrice) |
| | | .filter(Objects::nonNull).min(Comparator.naturalOrder()).orElse(null)); |
| | | row.setMaxPrice(quotes.stream().map(SrmSupplierQuoteDO::getQuotePrice) |
| | | .filter(Objects::nonNull).max(Comparator.naturalOrder()).orElse(null)); |
| | | if (first.getEstimatedPrice() != null && first.getEstimatedPrice().signum() > 0 |
| | | && row.getWeightedAvgPrice() != null) { |
| | | row.setDeviationRate(row.getWeightedAvgPrice().subtract(first.getEstimatedPrice()) |
| | | .multiply(BigDecimal.valueOf(100)) |
| | | .divide(first.getEstimatedPrice(), 2, RoundingMode.HALF_UP)); |
| | | } |
| | | |
| | | // æ¥ä»·æåºï¼æå建æ¶é´ï¼ |
| | | List<SrmSupplierQuoteDO> ordered = quotes.stream() |
| | | .filter(q -> q.getQuotePrice() != null) |
| | | .sorted(Comparator.comparing(SrmSupplierQuoteDO::getCreateTime, |
| | | Comparator.nullsFirst(Comparator.naturalOrder()))) |
| | | .toList(); |
| | | if (!ordered.isEmpty()) { |
| | | row.setLastQuoteTime(ordered.get(ordered.size() - 1).getCreateTime()); |
| | | if (ordered.size() >= 2) { |
| | | row.setPriceChange(ordered.get(ordered.size() - 1).getQuotePrice() |
| | | .subtract(ordered.get(ordered.size() - 2).getQuotePrice())); |
| | | } |
| | | } |
| | | |
| | | // ä¾åºååç§° |
| | | Set<Long> supplierIds = quotes.stream().map(SrmSupplierQuoteDO::getSupplierId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | Map<Long, String> supplierNames = supplierIds.isEmpty() ? Map.of() |
| | | : supplierMapper.selectByIds(supplierIds).stream() |
| | | .collect(Collectors.toMap(SrmSupplierDO::getId, SrmSupplierDO::getName, (a, b) -> a)); |
| | | row.setQuotes(quotes.stream().map(q -> { |
| | | var item = new SrmCostAnalysisRespVO.QuoteItem(); |
| | | item.setSupplierId(q.getSupplierId()); |
| | | item.setSupplierName(supplierNames.get(q.getSupplierId())); |
| | | item.setQuotePrice(q.getQuotePrice()); |
| | | item.setTaxRate(q.getTaxRate()); |
| | | item.setDeliveryCycle(q.getDeliveryCycle()); |
| | | item.setQuoteTime(q.getCreateTime()); |
| | | return item; |
| | | }).toList()); |
| | | rows.add(row); |
| | | } |
| | | |
| | | // æå¨å页 |
| | | rows.sort((a, b) -> { |
| | | BigDecimal pa = a.getWeightedAvgPrice() == null ? BigDecimal.ZERO : a.getWeightedAvgPrice(); |
| | | BigDecimal pb = b.getWeightedAvgPrice() == null ? BigDecimal.ZERO : b.getWeightedAvgPrice(); |
| | | return pb.compareTo(pa); |
| | | }); |
| | | int total = rows.size(); |
| | | int from = Math.min((page.getPageNo() - 1) * page.getPageSize(), total); |
| | | int to = Math.min(from + page.getPageSize(), total); |
| | | return new PageResult<>(rows.subList(from, to), (long) total); |
| | | } |
| | | |
| | | /** åç©ææ¥ä»·è¶å¿ï¼ææå¹³åæ¥ä»·ï¼ */ |
| | | public List<SrmCostAnalysisRespVO.TrendPoint> trend(Long productId) { |
| | | if (productId == null) return List.of(); |
| | | List<SrmTenderMaterialDO> materials = tenderMaterialMapper.selectList( |
| | | new LambdaQueryWrapperX<SrmTenderMaterialDO>().eq(SrmTenderMaterialDO::getProductId, productId)); |
| | | Set<Long> materialIds = materials.stream().map(SrmTenderMaterialDO::getId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | if (materialIds.isEmpty()) return List.of(); |
| | | List<SrmSupplierQuoteDO> quotes = supplierQuoteMapper.selectList( |
| | | new LambdaQueryWrapperX<SrmSupplierQuoteDO>() |
| | | .in(SrmSupplierQuoteDO::getTenderMaterialId, materialIds)); |
| | | Map<YearMonth, List<BigDecimal>> byMonth = new TreeMap<>(); |
| | | for (SrmSupplierQuoteDO q : quotes) { |
| | | if (q.getQuotePrice() == null || q.getCreateTime() == null) continue; |
| | | byMonth.computeIfAbsent(YearMonth.from(q.getCreateTime()), k -> new ArrayList<>()) |
| | | .add(q.getQuotePrice()); |
| | | } |
| | | List<SrmCostAnalysisRespVO.TrendPoint> result = new ArrayList<>(); |
| | | byMonth.forEach((month, prices) -> { |
| | | var p = new SrmCostAnalysisRespVO.TrendPoint(); |
| | | p.setMonth(month.toString()); |
| | | p.setCount(prices.size()); |
| | | p.setAvgPrice(prices.stream().reduce(BigDecimal.ZERO, BigDecimal::add) |
| | | .divide(BigDecimal.valueOf(prices.size()), 4, RoundingMode.HALF_UP)); |
| | | result.add(p); |
| | | }); |
| | | return result; |
| | | } |
| | | } |