From c215f6c20445b973ff730dcdf4206426992da81d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 19 十月 2026 11:30:57 +0800
Subject: [PATCH] feat(mes): 完善质检流程功能

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java            |   47 +++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java                                    |  117 +++++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java          |    3 
 yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java                                |  108 +++++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java             |    5 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java                            |    4 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java                        |   16 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java                             |    1 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java                                 |   15 +
 docs/production_quality_flow_20261019.md                                                                                              |   37 ++
 sql/mysql/mes_seed_quality_workflow_20261019.sql                                                                                      |   78 +++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java                      |   12 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java                      |    7 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java                       |    4 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java                          |    2 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java                                       |    5 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java                                  |    9 
 yudao-module-mes/src/test/resources/sql/create_tables.sql                                                                             |   13 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityActionDO.java                |   20 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java      |   12 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java                    |  164 ++++++++++-
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java                            |   26 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java |   15 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java                                        |    4 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java                                |   19 +
 yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java                        |   51 +++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityActionMapper.java                 |   15 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java                        |    6 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRetestReqVO.java        |   15 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java                                      |    2 
 30 files changed, 800 insertions(+), 32 deletions(-)

diff --git a/docs/production_quality_flow_20261019.md b/docs/production_quality_flow_20261019.md
new file mode 100644
index 0000000..20ae47f
--- /dev/null
+++ b/docs/production_quality_flow_20261019.md
@@ -0,0 +1,37 @@
+# 鐢熶骇璐ㄦ娴佺▼涓庤彍鍗曟槧灏�
+
+渚濇嵁鐢ㄦ埛鎻愪緵鐨勭敓浜ф祦绋嬪浘鍜屽綋鍓� `system_menu`锛屼繚鐣欏凡鏈夎川閲忕鐞嗚彍鍗曪紝鏂板鈥滈攢鍞川妫�鈥濆叆鍙c��
+
+| 娴佺▼鍥捐妭鐐� | 绯荤粺鍏ュ彛 | 瀹炵幇 |
+| --- | --- | --- |
+| 48/100 琚嬫壂鐮佸叆搴撱�佺敓鎴愭墭鐩樼爜 | 涓�琚嬩竴鐮� / 浠撳偍鐗╂祦 | 鏃㈡湁琚嬬爜銆佹墭鐩樼爜銆佹壒娆$爜 |
+| 鍔犲伐閮ㄥ垵妫� | 璐ㄩ噺绠$悊 / 绉嶅瓙璐ㄦ | 鍙寜琚嬨�佹墭鐩樸�佹壒娆℃彁浜わ紱姣忚鐙珛璁板綍 |
+| 鍒濇涓嶅悎鏍笺�佸妫� | 璐ㄩ噺绠$悊 / 璐ㄦ璁板綍 | 淇濈暀鏆傚瓨搴撳瓨锛岃褰曞妫�缁撴灉涓庡巻娆℃寚鏍� |
+| 鍔犲伐閮ㄩ瀵煎鎵� | 璐ㄩ噺绠$悊 / 璐ㄦ璁板綍 | 鍗曟潯鎴栧悓鐘舵�佹壒閲忓鎵癸紝鍙┏鍥為噸妫� |
+| 鍏徃璐ㄦ瀹℃壒 | 璐ㄩ噺绠$悊 / 璐ㄦ璁板綍 | 鍗曟潯鎴栧悓鐘舵�佹壒閲忓鎵癸紱閫氳繃鍚庢墠杩佺Щ鎴栨姤搴熷簱瀛� |
+| 閿�鍞儴璐ㄦ | 璐ㄩ噺绠$悊 / 閿�鍞川妫� | 鐙珛鑿滃崟锛屽鐢ㄦ棦鏈� OQC 閿�鍞嚭搴撳崟妫�楠屽崟 |
+| 閿�鍞妫�涓庡鎵� | 璐ㄩ噺绠$悊 / 閿�鍞川妫� | 涓嶅悎鏍煎厛澶嶆锛岄攢鍞儴涓庡叕鍙歌川妫�瀹℃壒鍚庡洖鍐欏嚭搴撹川閲忕姸鎬� |
+| 涓嶅悎鏍煎搧鐣欑棔璇勫 | 璐ㄩ噺绠$悊 / 涓嶅悎鏍煎搧澶勭悊 | 鏃㈡湁 OQC/IPQC 鑷姩鍒涘缓 NCR |
+| 姝e紡鍏ュ簱銆佸嚭搴撱�佹壂鐮佽拷婧� | 浠撳偍鐗╂祦 / 鍑哄簱娴侀�� / 杩芥函 | 瀹℃壒鏀捐鍚庤鐮� `qc_status=20`锛涙湭鏀捐涓嶈兘鎵爜鍑哄簱 |
+
+## 绉嶅瓙璐ㄦ鐘舵��
+
+- 10 寰呭妫�锛氬垵妫�澶辫触鎴栧鎵归┏鍥烇紱搴撳瓨浠嶅湪鏆傚瓨搴撱��
+- 20 寰呭姞宸ラ儴瀹℃壒锛氬垵妫�鎴栧妫�鍚堟牸銆�
+- 30 寰呭叕鍙歌川妫�瀹℃壒锛氬姞宸ラ儴鍚屾剰鏀捐銆�
+- 40 宸叉斁琛岋細鍏徃璐ㄦ鍚屾剰锛屼簨鍔℃�у湴灏嗚琚嬬敱鏆傚瓨搴撶Щ鍏ュ悎鏍煎簱銆�
+- 50 寰呭姞宸ラ儴涓嶅悎鏍艰瘎瀹★細澶嶆涓嶅悎鏍笺��
+- 60 寰呭叕鍙镐笉鍚堟牸瀹℃壒锛氬姞宸ラ儴鍚屾剰鎶ュ簾銆�
+- 70 宸叉姤搴燂細鍏徃璐ㄦ鍚屾剰锛屼簨鍔℃�у湴绉婚櫎璇ヨ鏆傚瓨搴撳瓨銆�
+
+瀹℃壒椹冲洖鍥炲埌 10銆傛瘡娆″垵妫�銆佸妫�鍜屽鎵瑰啓鍏� `mes_qc_seed_quality_action`锛屽寘鍚搷浣滀汉銆佹椂闂淬�佹剰瑙佸拰妫�楠屾寚鏍囧揩鐓с�傛壒閲忓鎵瑰湪鍚屼竴鏁版嵁搴撲簨鍔″唴鎵ц锛屼换涓�琚嬪簱瀛樻祦杞け璐ュ垯鍏ㄩ儴鍥炴粴銆�
+
+閿�鍞川妫�鐨勬祦绋嬬姸鎬佷笌绉嶅瓙璐ㄦ鐩稿悓锛屽叾涓� 20/50 鐢遍攢鍞儴瀹℃壒锛�40 琛ㄧず鍑鸿揣妫�楠岄�氳繃骞跺洖鍐欓攢鍞嚭搴撳崟琛岋紝70 琛ㄧず涓嶅悎鏍煎鎵瑰畬鎴愬苟鍥炲啓涓轰笉鍚堟牸锛屽悓鏃跺垱寤� NCR銆傚垵妫�涓庡妫�涓嶅洖鍐欏嚭搴撳崟璐ㄩ噺缁撴灉锛屽洜姝ゅ鎵瑰墠浠嶅浜庡緟妫�鐘舵�併�傚妫�鍓嶅悗鏁伴噺鍙婂鎵规剰瑙佸啓鍏� `mes_qc_oqc_action`銆�
+
+## 閮ㄧ讲椤哄簭
+
+1. 澶囦唤鏁版嵁搴擄紝鎵ц `sql/mysql/mes_seed_quality_workflow_20261019.sql`銆傝剼鏈槸涓�娆℃�ц縼绉伙紱鍘嗗彶宸叉祦杞殑鍚堟牸/涓嶅悎鏍艰褰曞垎鍒爣璁颁负 40/70锛岄伩鍏嶉噸澶嶅簱瀛樹簨鍔°��
+2. 閮ㄧ讲鍚庣銆佸墠绔�傚墠绔瀯寤轰骇鐗╁湪 `mom-pro2-before/dist.zip`銆�
+3. 涓哄姞宸ラ儴涓庡叕鍙歌川妫�瑙掕壊鍒嗗埆鎺堜簣 `mes:qc-seed-quality:process-approve`銆乣mes:qc-seed-quality:company-approve`锛涘妫�浜哄憳鎺堜簣 `mes:qc-seed-quality:retest`銆備负閿�鍞儴涓庡叕鍙歌川妫�瑙掕壊鍒嗗埆鎺堜簣 `mes:qc-oqc:sales-approve`銆乣mes:qc-oqc:company-approve`锛涢攢鍞妫�浜哄憳鎺堜簣 `mes:qc-oqc:retest`銆傛牳瀵光�滈攢鍞川妫�鈥濊彍鍗曡鑹叉潈闄愩��
+
+鈥滈攢鍞川妫�鈥濅笌鍘熲�滃嚭璐ф楠屸�濆叡鐢� OQC 妫�楠屽崟鍙婃棦鏈夊嚭璐ф楠岃〃鍗曪紝浣嗘湁鐙珛鑿滃崟涓庝笓闂ㄧ殑澶嶆銆佸鎵规搷浣溿�傚師鈥滆繃绋嬫楠岋紙IPQC锛夆�濅粛鏈嶅姟鐢熶骇浠诲姟鍜屾姤宸ワ紝鏈苟鍏ユ寜琚嬬爜娴佽浆鐨勭瀛愯川妫�瀹℃壒閾俱��
diff --git a/sql/mysql/mes_seed_quality_workflow_20261019.sql b/sql/mysql/mes_seed_quality_workflow_20261019.sql
new file mode 100644
index 0000000..d639fcb
--- /dev/null
+++ b/sql/mysql/mes_seed_quality_workflow_20261019.sql
@@ -0,0 +1,78 @@
+-- 鐢熶骇璐ㄦ锛氬垵妫�銆佸妫�銆佸姞宸ラ儴瀹℃壒銆佸叕鍙歌川妫�瀹℃壒鍚庢墠鏀捐搴撳瓨銆�
+-- 鍏堥儴缃叉暟鎹簱鍙樻洿锛屽啀閮ㄧ讲瀵瑰簲鍚庣涓庡墠绔紱鍘嗗彶鍚堟牸璁板綍宸插畬鎴愬簱瀛樻祦杞��
+ALTER TABLE mes_qc_seed_quality
+  ADD COLUMN workflow_status tinyint NOT NULL DEFAULT 40 COMMENT '10寰呭妫� 20寰呭姞宸ラ儴瀹℃壒 30寰呭叕鍙歌川妫�瀹℃壒 40宸叉斁琛� 50寰呭姞宸ラ儴涓嶅悎鏍艰瘎瀹� 60寰呭叕鍙镐笉鍚堟牸瀹℃壒 70宸叉姤搴�',
+  ADD COLUMN qualified_warehouse_id bigint DEFAULT NULL COMMENT '瀹℃壒閫氳繃鍚庣殑鐩爣鍚堟牸搴�',
+  ADD COLUMN retest_result tinyint DEFAULT NULL COMMENT '澶嶆缁撴灉 1鍚堟牸 2涓嶅悎鏍�',
+  ADD COLUMN retested_at datetime DEFAULT NULL COMMENT '澶嶆鏃堕棿',
+  ADD COLUMN process_approved_at datetime DEFAULT NULL COMMENT '鍔犲伐閮ㄥ鎵规椂闂�',
+  ADD COLUMN company_approved_at datetime DEFAULT NULL COMMENT '鍏徃璐ㄦ瀹℃壒鏃堕棿';
+CREATE INDEX idx_mes_seed_quality_workflow ON mes_qc_seed_quality (workflow_status, inspected_at);
+
+-- 鏃ф暟鎹凡缁忔寜鏃ф祦绋嬫斁琛屾垨鎶ュ簾锛屼笉鑳介噸鏂板鎵规垨閲嶅鎵e簱瀛樸��
+UPDATE mes_qc_seed_quality SET workflow_status = CASE WHEN result = 1 THEN 40 WHEN result = 2 THEN 70 ELSE 10 END;
+
+CREATE TABLE IF NOT EXISTS mes_qc_seed_quality_action (
+  id bigint NOT NULL AUTO_INCREMENT,
+  quality_id bigint NOT NULL,
+  action varchar(32) NOT NULL COMMENT 'INITIAL/RETEST/PROCESS_APPROVE/COMPANY_APPROVE/REJECT',
+  result tinyint DEFAULT NULL,
+  opinion varchar(500) DEFAULT NULL,
+  indicators_json text DEFAULT NULL,
+  operator_id bigint DEFAULT NULL,
+  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (id),
+  KEY idx_quality_time (quality_id, create_time)
+) ENGINE=InnoDB COMMENT='绉嶅瓙璐ㄦ娴佺▼鎿嶄綔鐣欑棔';
+
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '澶嶆', 'mes:qc-seed-quality:retest', 3, 3, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:retest' AND m.deleted = b'0');
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '鍔犲伐閮ㄥ鎵�', 'mes:qc-seed-quality:process-approve', 3, 4, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:process-approve' AND m.deleted = b'0');
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '鍏徃璐ㄦ瀹℃壒', 'mes:qc-seed-quality:company-approve', 3, 5, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:company-approve' AND m.deleted = b'0');
+
+-- 鐙珛閿�鍞川妫�鍏ュ彛锛涗繚鐣欏師鈥滃嚭璐ф楠屸�濊彍鍗曪紝澶嶇敤鍏跺凡瀹炵幇鐨勯攢鍞嚭搴撳崟妫�楠岃兘鍔涖��
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '閿�鍞川妫�', '', 2, 8, 5500, 'sales-quality', 'ep:document-checked', 'mes/qc/salesQuality/index', 'MesQcSalesQuality', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0');
+
+-- 閿�鍞川妫�澶嶆鍙婁袱绾у鎵癸紱鍘嗗彶宸插畬鎴愬嚭璐ф楠屽崟缁存寔鏃㈡湁鏀捐缁撴灉銆�
+ALTER TABLE mes_qc_oqc
+  ADD COLUMN workflow_status tinyint NOT NULL DEFAULT 0 COMMENT '0鑽夌 10寰呭妫� 20寰呴攢鍞儴瀹℃壒 30寰呭叕鍙歌川妫�瀹℃壒 40宸叉斁琛� 50寰呴攢鍞儴涓嶅悎鏍艰瘎瀹� 60寰呭叕鍙镐笉鍚堟牸瀹℃壒 70宸叉嫆缁濆嚭璐�';
+CREATE INDEX idx_mes_oqc_workflow ON mes_qc_oqc (workflow_status, id);
+UPDATE mes_qc_oqc SET workflow_status = CASE
+  WHEN status = 0 THEN 0
+  WHEN check_result = 1 THEN 40
+  ELSE 70 END;
+CREATE TABLE IF NOT EXISTS mes_qc_oqc_action (
+  id bigint NOT NULL AUTO_INCREMENT,
+  oqc_id bigint NOT NULL,
+  action varchar(32) NOT NULL,
+  result tinyint DEFAULT NULL,
+  opinion varchar(500) DEFAULT NULL,
+  quantities_json text DEFAULT NULL,
+  operator_id bigint DEFAULT NULL,
+  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (id),
+  KEY idx_oqc_time (oqc_id, create_time)
+) ENGINE=InnoDB COMMENT='閿�鍞川妫�澶嶆鍜屽鎵圭暀鐥�';
+
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '閿�鍞妫�', 'mes:qc-oqc:retest', 3, 1, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:retest' AND m.deleted = b'0');
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '閿�鍞儴瀹℃壒', 'mes:qc-oqc:sales-approve', 3, 2, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:sales-approve' AND m.deleted = b'0');
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '閿�鍞叕鍙歌川妫�瀹℃壒', 'mes:qc-oqc:company-approve', 3, 3, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:company-approve' AND m.deleted = b'0');
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
index 973218b..e34b23b 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
@@ -10,6 +10,8 @@
 import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRespVO;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcApprovalReqVO;
 import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
 import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
@@ -83,6 +85,30 @@
         return success(true);
     }
 
+    @PostMapping("/retest") @Operation(summary = "閿�鍞川妫�澶嶆")
+    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:retest')")
+    public CommonResult<Boolean> retestOqc(@Valid @RequestBody MesQcOqcRetestReqVO reqVO) {
+        oqcService.retestOqc(reqVO);
+        return success(true);
+    }
+    @PostMapping("/sales-approve") @Operation(summary = "閿�鍞儴瀹℃壒")
+    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:sales-approve')")
+    public CommonResult<Boolean> salesApprove(@Valid @RequestBody MesQcOqcApprovalReqVO reqVO) {
+        oqcService.salesApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
+        return success(true);
+    }
+    @PostMapping("/company-approve") @Operation(summary = "鍏徃璐ㄦ瀹℃壒閿�鍞嚭璐�")
+    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:company-approve')")
+    public CommonResult<Boolean> companyApprove(@Valid @RequestBody MesQcOqcApprovalReqVO reqVO) {
+        oqcService.companyApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
+        return success(true);
+    }
+    @GetMapping("/actions") @Operation(summary = "閿�鍞川妫�娴佺▼璁板綍")
+    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:query')")
+    public CommonResult<List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO>> actions(@RequestParam Long id) {
+        return success(oqcService.getActions(id));
+    }
+
     @DeleteMapping("/delete")
     @Operation(summary = "鍒犻櫎鍑鸿揣妫�楠屽崟")
     @Parameter(name = "id", description = "缂栧彿", required = true)
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java
new file mode 100644
index 0000000..7baa189
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java
@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class MesQcOqcApprovalReqVO {
+    @NotNull private Long id;
+    @NotNull private Boolean approved;
+    @NotBlank private String opinion;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java
index 47926c1..ee70b3d 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java
@@ -26,5 +26,7 @@
 
     @Schema(description = "妫�娴嬬粨鏋�", example = "1")
     private Integer checkResult;
+    @Schema(description = "鏉ユ簮鍗曟嵁绫诲瀷")
+    private Integer sourceDocType;
 
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
index 79d9bed..7eefa7c 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
@@ -142,6 +142,7 @@
     @Schema(description = "鐘舵��", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
     @ExcelProperty("鐘舵��")
     private Integer status;
+    private Integer workflowStatus;
 
     @Schema(description = "澶囨敞", example = "澶囨敞")
     @ExcelProperty("澶囨敞")
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java
new file mode 100644
index 0000000..46427fc
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo;
+
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class MesQcOqcRetestReqVO {
+    @NotNull private Long id;
+    @NotNull @DecimalMin("0") private BigDecimal checkQuantity;
+    @NotNull @DecimalMin("0") private BigDecimal qualifiedQuantity;
+    @NotNull @DecimalMin("0") private BigDecimal unqualifiedQuantity;
+    @NotNull private Integer checkResult;
+    private String opinion;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
index d9c5f98..cde92bd 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
@@ -6,6 +6,9 @@
 import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRespVO;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityApprovalReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchApprovalReqVO;
 import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -33,6 +36,17 @@
         return success(BeanUtils.toBean(service.getQualityPage(pageReqVO), MesQcSeedQualityRespVO.class));
     }
 
+    @GetMapping("/indicators") @Operation(summary = "鑾峰緱绉嶅瓙璐ㄦ鏈�鏂版寚鏍�")
+    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')")
+    public CommonResult<java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO>> indicators(@RequestParam Long id) {
+        return success(service.getIndicatorsByQualityId(id));
+    }
+    @GetMapping("/actions") @Operation(summary = "鑾峰緱绉嶅瓙璐ㄦ娴佺▼鐣欑棔")
+    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')")
+    public CommonResult<java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO>> actions(@RequestParam Long id) {
+        return success(service.getActions(id));
+    }
+
     @PostMapping("/submit") @Operation(summary = "鎻愪氦绉嶅瓙璐ㄦ")
     @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')")
     public CommonResult<Long> submit(@Valid @RequestBody MesQcSeedQualitySubmitReqVO reqVO) { return success(service.submit(reqVO)); }
@@ -42,4 +56,37 @@
     public CommonResult<java.util.Map<String, Object>> submitGroup(@Valid @RequestBody cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO reqVO) {
         return success(service.submitGroup(reqVO));
     }
+
+    @PostMapping("/retest") @Operation(summary = "绉嶅瓙璐ㄦ澶嶆")
+    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:retest')")
+    public CommonResult<Boolean> retest(@RequestParam Long id, @Valid @RequestBody MesQcSeedQualityRetestReqVO reqVO) {
+        service.retest(id, reqVO);
+        return success(true);
+    }
+
+    @PostMapping("/process-approve") @Operation(summary = "鍔犲伐閮ㄥ鎵�")
+    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:process-approve')")
+    public CommonResult<Boolean> processApprove(@Valid @RequestBody MesQcSeedQualityApprovalReqVO reqVO) {
+        service.processApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
+        return success(true);
+    }
+
+    @PostMapping("/company-approve") @Operation(summary = "鍏徃璐ㄦ瀹℃壒骞舵祦杞簱瀛�")
+    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:company-approve')")
+    public CommonResult<Boolean> companyApprove(@Valid @RequestBody MesQcSeedQualityApprovalReqVO reqVO) {
+        service.companyApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
+        return success(true);
+    }
+    @PostMapping("/process-approve-batch") @Operation(summary = "鎵归噺鍔犲伐閮ㄥ鎵�")
+    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:process-approve')")
+    public CommonResult<Boolean> processApproveBatch(@Valid @RequestBody MesQcSeedQualityBatchApprovalReqVO reqVO) {
+        service.processApproveBatch(reqVO.getIds(), reqVO.getApproved(), reqVO.getOpinion());
+        return success(true);
+    }
+    @PostMapping("/company-approve-batch") @Operation(summary = "鎵归噺鍏徃璐ㄦ瀹℃壒骞舵祦杞簱瀛�")
+    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:company-approve')")
+    public CommonResult<Boolean> companyApproveBatch(@Valid @RequestBody MesQcSeedQualityBatchApprovalReqVO reqVO) {
+        service.companyApproveBatch(reqVO.getIds(), reqVO.getApproved(), reqVO.getOpinion());
+        return success(true);
+    }
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java
new file mode 100644
index 0000000..0878499
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java
@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class MesQcSeedQualityApprovalReqVO {
+    @NotNull private Long id;
+    @NotNull private Boolean approved;
+    @NotBlank private String opinion;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java
new file mode 100644
index 0000000..50b5ad2
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class MesQcSeedQualityBatchApprovalReqVO {
+    @NotEmpty private List<@NotNull Long> ids;
+    @NotNull private Boolean approved;
+    @NotBlank private String opinion;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java
index 2645d67..c7bf339 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java
@@ -18,4 +18,7 @@
 
     @Schema(description = "妫�楠岀粨鏋滐細1鍚堟牸 2涓嶅悎鏍�")
     private Integer result;
+
+    @Schema(description = "娴佺▼鐘舵�侊細10寰呭妫� 20/50寰呭姞宸ラ儴 30/60寰呭叕鍙歌川妫� 40宸叉斁琛� 70宸叉姤搴�")
+    private Integer workflowStatus;
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java
index 6102954..c3c4c95 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java
@@ -15,6 +15,11 @@
     private Long batchId;
     private String batchCode;
     private Integer result;
+    private Integer workflowStatus;
+    private Integer retestResult;
+    private LocalDateTime retestedAt;
+    private LocalDateTime processApprovedAt;
+    private LocalDateTime companyApprovedAt;
     private String reason;
     private Long inspectorId;
     private LocalDateTime inspectedAt;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRetestReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRetestReqVO.java
new file mode 100644
index 0000000..95da179
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRetestReqVO.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class MesQcSeedQualityRetestReqVO {
+    @NotBlank private String code;
+    @NotEmpty @Valid private List<MesQcSeedQualitySubmitReqVO.Indicator> indicators;
+    private String reason;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java
new file mode 100644
index 0000000..6ca31e6
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("mes_qc_oqc_action")
+public class MesQcOqcActionDO {
+    @TableId private Long id;
+    private Long oqcId;
+    private String action;
+    private Integer result;
+    private String opinion;
+    private String quantitiesJson;
+    private Long operatorId;
+    private LocalDateTime createTime;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
index 27d8fa0..4ba8d04 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
@@ -181,6 +181,8 @@
      * 鏋氫妇 {@link cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum}
      */
     private Integer status;
+    /** 0鑽夌锛�10寰呭妫�锛�20寰呴攢鍞儴瀹℃壒锛�30寰呭叕鍙歌川妫�瀹℃壒锛�40宸叉斁琛岋紱50寰呴攢鍞儴涓嶅悎鏍艰瘎瀹★紱60寰呭叕鍙镐笉鍚堟牸瀹℃壒锛�70宸叉嫆缁濆嚭璐с�� */
+    private Integer workflowStatus;
     /**
      * 澶囨敞
      */
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityActionDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityActionDO.java
new file mode 100644
index 0000000..6cbecef
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityActionDO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("mes_qc_seed_quality_action")
+public class MesQcSeedQualityActionDO {
+    @TableId private Long id;
+    private Long qualityId;
+    private String action;
+    private Integer result;
+    private String opinion;
+    private String indicatorsJson;
+    private Long operatorId;
+    private LocalDateTime createTime;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java
index 1ca77b7..32521fa 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java
@@ -17,6 +17,13 @@
     private String batchCode;
     private Integer result;
     private String reason;
+    /** 10 寰呭妫�锛�20 寰呭姞宸ラ儴瀹℃壒锛�30 寰呭叕鍙歌川妫�瀹℃壒锛�40 宸叉斁琛岋紱50 寰呭姞宸ラ儴涓嶅悎鏍艰瘎瀹★紱60 寰呭叕鍙镐笉鍚堟牸瀹℃壒锛�70 宸叉姤搴熴�� */
+    private Integer workflowStatus;
+    private Long qualifiedWarehouseId;
+    private Integer retestResult;
+    private LocalDateTime retestedAt;
+    private LocalDateTime processApprovedAt;
+    private LocalDateTime companyApprovedAt;
     private Long inspectorId;
     private LocalDateTime inspectedAt;
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java
new file mode 100644
index 0000000..62fa651
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO;
+import org.apache.ibatis.annotations.Mapper;
+import java.util.List;
+
+@Mapper
+public interface MesQcOqcActionMapper extends BaseMapperX<MesQcOqcActionDO> {
+    default List<MesQcOqcActionDO> selectByOqcId(Long id) {
+        return selectList(new LambdaQueryWrapperX<MesQcOqcActionDO>()
+                .eq(MesQcOqcActionDO::getOqcId, id).orderByAsc(MesQcOqcActionDO::getId));
+    }
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
index 1a26e94..2b16f33 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
@@ -25,6 +25,7 @@
                 .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
                 .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
                 .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
+                .eqIfPresent(MesQcOqcDO::getSourceDocType, reqVO.getSourceDocType())
                 .orderByDesc(MesQcOqcDO::getId));
     }
 
@@ -38,6 +39,7 @@
                 .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
                 .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
                 .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
+                .eqIfPresent(MesQcOqcDO::getSourceDocType, reqVO.getSourceDocType())
                 .orderByDesc(MesQcOqcDO::getId);
         if (permittedClientIds != null) {
             if (CollUtil.isEmpty(permittedClientIds)) {
@@ -51,5 +53,8 @@
     default MesQcOqcDO selectByCode(String code) {
         return selectOne(MesQcOqcDO::getCode, code);
     }
+    default MesQcOqcDO selectByIdForUpdate(Long id) {
+        return selectOne(new LambdaQueryWrapperX<MesQcOqcDO>().eq(MesQcOqcDO::getId, id).last("FOR UPDATE"));
+    }
 
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityActionMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityActionMapper.java
new file mode 100644
index 0000000..fd3dfad
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityActionMapper.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface MesQcSeedQualityActionMapper extends BaseMapperX<MesQcSeedQualityActionDO> {
+    default java.util.List<MesQcSeedQualityActionDO> selectByQualityId(Long qualityId) {
+        return selectList(new LambdaQueryWrapperX<MesQcSeedQualityActionDO>()
+                .eq(MesQcSeedQualityActionDO::getQualityId, qualityId)
+                .orderByAsc(MesQcSeedQualityActionDO::getId));
+    }
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
index 076ce87..b75c547 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
@@ -17,6 +17,7 @@
                 .likeIfPresent(MesQcSeedQualityDO::getCode, reqVO.getCode())
                 .likeIfPresent(MesQcSeedQualityDO::getBatchCode, reqVO.getBatchCode())
                 .eqIfPresent(MesQcSeedQualityDO::getResult, reqVO.getResult())
+                .eqIfPresent(MesQcSeedQualityDO::getWorkflowStatus, reqVO.getWorkflowStatus())
                 .orderByDesc(MesQcSeedQualityDO::getInspectedAt)
                 .orderByDesc(MesQcSeedQualityDO::getId));
     }
@@ -34,6 +35,9 @@
     default MesQcSeedQualityDO selectByCodeForUpdate(String code) {
         return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getCode, code).last("FOR UPDATE"));
     }
+    default MesQcSeedQualityDO selectByIdForUpdate(Long id) {
+        return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getId, id).last("FOR UPDATE"));
+    }
 
     default List<MesQcSeedQualityDO> selectListByBatchCode(String batchCode) {
         return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>()
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
index cf1ea78..5edf94d 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
@@ -18,6 +18,7 @@
 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
@@ -215,6 +216,14 @@
         if (ipqc.getCheckResult() == null) {
             throw exception(QC_IPQC_CHECK_RESULT_EMPTY);
         }
+        if (ipqc.getUnqualifiedQuantity() != null && ipqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0
+                && MesQcCheckResultEnum.QUALIFIED.getType().equals(ipqc.getCheckResult())) {
+            throw new IllegalArgumentException("瀛樺湪涓嶅悎鏍煎搧鏃讹紝杩囩▼妫�楠岀粨璁轰笉鑳戒负鍚堟牸");
+        }
+        if ((ipqc.getUnqualifiedQuantity() == null || ipqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0)
+                && !MesQcCheckResultEnum.QUALIFIED.getType().equals(ipqc.getCheckResult())) {
+            throw new IllegalArgumentException("涓嶅悎鏍兼暟閲忎负 0 鏃讹紝杩囩▼妫�楠岀粨璁哄繀椤讳负鍚堟牸");
+        }
         // 1.3 鏍¢獙鑷冲皯瀛樺湪涓�鏉℃娴嬬粨鏋�
         indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.IPQC.getType());
 
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java
index b6425fd..b2eecba 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java
@@ -38,6 +38,10 @@
      * @param id 缂栧彿
      */
     void finishOqc(Long id);
+    void retestOqc(cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO reqVO);
+    void salesApprove(Long id, boolean approved, String opinion);
+    void companyApprove(Long id, boolean approved, String opinion);
+    List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO> getActions(Long id);
 
     /**
      * 鍒犻櫎鍑鸿揣妫�楠屽崟
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
index 57b4dd2..26811bf 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -1,14 +1,18 @@
 package cn.iocoder.yudao.module.mes.service.qc.oqc;
 
 import cn.hutool.core.util.ObjUtil;
+import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcActionMapper;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
@@ -36,8 +40,11 @@
 import java.math.RoundingMode;
 import java.util.List;
 import java.util.Objects;
+import java.time.LocalDateTime;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
 /**
@@ -49,8 +56,13 @@
 @Validated
 public class MesQcOqcServiceImpl implements MesQcOqcService {
 
+    private static final int WAIT_RETEST = 10, WAIT_SALES = 20, WAIT_COMPANY = 30,
+            RELEASED = 40, WAIT_SALES_FAIL = 50, WAIT_COMPANY_FAIL = 60, REJECTED = 70;
+
     @Resource
     private MesQcOqcMapper oqcMapper;
+    @Resource
+    private MesQcOqcActionMapper actionMapper;
 
     @Resource
     private MesQcTemplateItemService templateItemService;
@@ -133,26 +145,115 @@
     @Transactional(rollbackFor = Exception.class)
     public void finishOqc(Long id) {
         // 1.1 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
-        MesQcOqcDO oqc = validateOqcStatusPrepare(id);
+        MesQcOqcDO oqc = lock(id);
+        if (!MesQcStatusEnum.DRAFT.getStatus().equals(oqc.getStatus())) throw exception(QC_OQC_NOT_PREPARE);
         // 1.2 鏍¢獙妫�娴嬬粨璁哄繀濉�
         if (oqc.getCheckResult() == null) {
             throw exception(QC_OQC_CHECK_RESULT_EMPTY);
+        }
+        if (oqc.getUnqualifiedQuantity() != null && oqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0
+                && MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult())) {
+            throw new IllegalArgumentException("瀛樺湪涓嶅悎鏍煎搧鏃讹紝鍑鸿揣妫�楠岀粨璁轰笉鑳戒负鍚堟牸");
+        }
+        if ((oqc.getUnqualifiedQuantity() == null || oqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0)
+                && !MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult())) {
+            throw new IllegalArgumentException("涓嶅悎鏍兼暟閲忎负 0 鏃讹紝鍑鸿揣妫�楠岀粨璁哄繀椤讳负鍚堟牸");
         }
         // 1.3 鏍¢獙鑷冲皯瀛樺湪涓�鏉℃娴嬬粨鏋�
         indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.OQC.getType());
 
         // 2. 鏇存柊鐘舵�佷负宸插畬鎴�
         MesQcOqcDO updateObj = new MesQcOqcDO()
-                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus());
+                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus())
+                .setWorkflowStatus(MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult()) ? WAIT_SALES : WAIT_RETEST);
         oqcMapper.updateById(updateObj);
-
-        // 3. 涓嶅悎鏍兼椂鑷姩鍒涘缓 NCR
-        generateNcrIfNeeded(oqc);
-
-        // 4. 鍥炲啓鏉ユ簮鍗曟嵁
-        writeBackSourceDoc(oqc);
+        recordAction(id, "INITIAL", oqc.getCheckResult(), null, oqc);
+        // 閿�鍞嚭搴撴槑缁嗕粛涓哄緟妫�锛涘叕鍙歌川妫�瀹℃壒閫氳繃鍚庢墠鍥炲啓骞跺厑璁稿悗缁嚭搴撱��
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void retestOqc(MesQcOqcRetestReqVO req) {
+        MesQcOqcDO oqc = requireStatus(req.getId(), WAIT_RETEST);
+        if (req.getCheckQuantity().compareTo(req.getQualifiedQuantity().add(req.getUnqualifiedQuantity())) != 0)
+            throw new IllegalArgumentException("妫�娴嬫暟閲忓繀椤荤瓑浜庡悎鏍间笌涓嶅悎鏍兼暟閲忎箣鍜�");
+        if (req.getCheckQuantity().compareTo(oqc.getOutQuantity()) > 0)
+            throw new IllegalArgumentException("妫�娴嬫暟閲忎笉鑳借秴杩囧嚭璐ф暟閲�");
+        if (!Set.of(1, 2).contains(req.getCheckResult())) throw new IllegalArgumentException("澶嶆缁撹鍙兘涓哄悎鏍兼垨涓嶅悎鏍�");
+        boolean passed = req.getCheckResult() == 1;
+        if (passed != (req.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0))
+            throw new IllegalArgumentException("澶嶆缁撹涓庝笉鍚堟牸鏁伴噺涓嶄竴鑷�");
+        if (!passed && (req.getOpinion() == null || req.getOpinion().isBlank()))
+            throw new IllegalArgumentException("澶嶆涓嶅悎鏍煎繀椤诲~鍐欏師鍥�");
+        MesQcOqcDO update = new MesQcOqcDO().setId(req.getId())
+                .setCheckQuantity(req.getCheckQuantity()).setQualifiedQuantity(req.getQualifiedQuantity())
+                .setUnqualifiedQuantity(req.getUnqualifiedQuantity()).setCheckResult(req.getCheckResult())
+                .setInspectDate(LocalDateTime.now()).setInspectorUserId(getLoginUserId())
+                .setWorkflowStatus(passed ? WAIT_SALES : WAIT_SALES_FAIL);
+        oqcMapper.updateById(update);
+        recordAction(req.getId(), "RETEST", req.getCheckResult(), req.getOpinion(), update);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void salesApprove(Long id, boolean approved, String opinion) {
+        MesQcOqcDO oqc = lock(id);
+        if (!Set.of(WAIT_SALES, WAIT_SALES_FAIL).contains(oqc.getWorkflowStatus()))
+            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛岄攢鍞儴瀹℃壒");
+        requireOpinion(opinion);
+        int next = approved ? (oqc.getWorkflowStatus() == WAIT_SALES ? WAIT_COMPANY : WAIT_COMPANY_FAIL) : WAIT_RETEST;
+        oqcMapper.updateById(new MesQcOqcDO().setId(id).setWorkflowStatus(next));
+        recordAction(id, approved ? "SALES_APPROVE" : "SALES_REJECT", oqc.getCheckResult(), opinion, oqc);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void companyApprove(Long id, boolean approved, String opinion) {
+        MesQcOqcDO oqc = lock(id);
+        if (!Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(oqc.getWorkflowStatus()))
+            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛屽叕鍙歌川妫�瀹℃壒");
+        requireOpinion(opinion);
+        if (approved) {
+            if (oqc.getWorkflowStatus() == WAIT_COMPANY_FAIL) generateNcrIfNeeded(oqc);
+            writeBackSourceDoc(oqc);
+        }
+        int next = approved ? (oqc.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : REJECTED) : WAIT_RETEST;
+        oqcMapper.updateById(new MesQcOqcDO().setId(id).setWorkflowStatus(next));
+        recordAction(id, approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", oqc.getCheckResult(), opinion, oqc);
+    }
+
+    private MesQcOqcDO lock(Long id) {
+        MesQcOqcDO oqc = oqcMapper.selectByIdForUpdate(id);
+        if (oqc == null) throw exception(QC_OQC_NOT_EXISTS);
+        return oqc;
+    }
+
+    private MesQcOqcDO requireStatus(Long id, int status) {
+        MesQcOqcDO oqc = lock(id);
+        if (!Objects.equals(oqc.getWorkflowStatus(), status))
+            throw new IllegalArgumentException("閿�鍞川妫�鐘舵�佸凡鍙樺寲锛岃鍒锋柊鍚庨噸璇�");
+        return oqc;
+    }
+
+    private void requireOpinion(String opinion) {
+        if (opinion == null || opinion.isBlank()) throw new IllegalArgumentException("璇峰~鍐欏鎵规剰瑙�");
+    }
+
+    private void recordAction(Long id, String action, Integer result, String opinion, MesQcOqcDO snapshot) {
+        MesQcOqcActionDO event = new MesQcOqcActionDO();
+        event.setOqcId(id);event.setAction(action);event.setResult(result);event.setOpinion(opinion);
+        java.util.Map<String, BigDecimal> quantities = new java.util.HashMap<>();
+        quantities.put("checkQuantity", snapshot.getCheckQuantity());
+        quantities.put("qualifiedQuantity", snapshot.getQualifiedQuantity());
+        quantities.put("unqualifiedQuantity", snapshot.getUnqualifiedQuantity());
+        event.setQuantitiesJson(JSONUtil.toJsonStr(quantities));
+        event.setOperatorId(getLoginUserId());event.setCreateTime(LocalDateTime.now());
+        actionMapper.insert(event);
+    }
+
+    @Override
+    public List<MesQcOqcActionDO> getActions(Long id) { return actionMapper.selectByOqcId(id); }
+
     /**
      * 涓嶅悎鏍兼椂鑷姩鍒涘缓 NCR锛堜笉鍚堟牸鍝佸鐞嗗崟锛�
      *
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
index 72d6ebc..2d290f2 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
@@ -15,6 +15,11 @@
     java.util.Map<String, Object> resolveScan(String code);
     java.util.Map<String, Object> resolveScan(String code, String scanType);
     java.util.Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO reqVO);
+    void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO reqVO);
+    void processApprove(Long id, boolean approved, String opinion);
+    void companyApprove(Long id, boolean approved, String opinion);
+    void processApproveBatch(List<Long> ids, boolean approved, String opinion);
+    void companyApproveBatch(List<Long> ids, boolean approved, String opinion);
 
     PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO);
 
@@ -34,4 +39,5 @@
      * @return 璐ㄦ鎸囨爣鍒楄〃
      */
     List<MesQcSeedQualityIndicatorDO> getIndicatorsByQualityId(Long qualityId);
+    List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO> getActions(Long qualityId);
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
index ada99e6..2a3ba96 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -2,6 +2,7 @@
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
@@ -43,6 +44,7 @@
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_NOT_EXISTS;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_TYPE_NOT_QUALIFIED;
@@ -50,7 +52,10 @@
 @Service @Validated @Slf4j
 public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
     private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
+    private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_COMPANY = 30,
+            RELEASED = 40, WAIT_PROCESS_FAIL = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
     @Resource private MesQcSeedQualityMapper qualityMapper;
+    @Resource private MesQcSeedQualityActionMapper actionMapper;
     @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
     @Resource private MesProBagCodeMapper bagCodeMapper;
     @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper palletMapper;
@@ -197,33 +202,52 @@
         if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) {
             throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED);
         }
-        // 瑙f瀽鍚堟牸搴撻粯璁ゅ簱鍖�/搴撲綅锛堝彇璇ヤ粨搴撲笅绗竴涓簱鍖恒�佸叾涓嬬涓�涓簱浣嶏紝鏈厤缃垯涓� null锛�
-        Long qualifiedLocationId = null;
-        Long qualifiedAreaId = null;
-        List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId());
-        if (CollUtil.isNotEmpty(locations)) {
-            qualifiedLocationId = locations.get(0).getId();
-            List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId);
-            if (CollUtil.isNotEmpty(areas)) {
-                qualifiedAreaId = areas.get(0).getId();
-            }
-        }
         Set<String> codes = new HashSet<>();
         boolean passed = true;
         MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()).itemId(batch.getItemId())
-                .batchCode(batch.getCode()).reason(reqVO.getReason()).result(0)
+                .batchCode(batch.getCode()).batchId(batch.getId()).reason(reqVO.getReason()).result(0)
+                .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(getLoginUserId())
                 .inspectedAt(LocalDateTime.now()).build();
         qualityMapper.insert(quality);
         for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
             if (!REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())) throw new IllegalArgumentException("鎸囨爣蹇呴』涓虹函搴︺�佸噣搴︺�佸彂鑺界巼銆佹按鍒嗕笖涓嶅彲閲嶅");
-            if (item.getMinValue().compareTo(item.getMaxValue()) > 0 || item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
+            if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("鎸囨爣涓嬮檺涓嶈兘澶т簬涓婇檺");
+            if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
             indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
                     .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
         }
         if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("鍥涢」鎸囨爣涓嶅畬鏁�");
         if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("璐ㄦ涓嶅悎鏍煎繀椤诲~鍐欏師鍥�");
         quality.setResult(passed ? 1 : 2);
+        quality.setWorkflowStatus(passed ? WAIT_PROCESS : WAIT_RETEST);
         qualityMapper.updateById(quality);
+        recordAction(quality.getId(), "INITIAL", quality.getResult(), reqVO.getReason(), reqVO.getIndicators());
+        // 鍒濇浠呰褰曠粨璁猴紱琚嬩粛鐣欏湪鏆傚瓨搴擄紝寰呭妫�鍙婁袱绾у鎵瑰悗鍐嶆斁琛屾垨鎶ュ簾銆�
+        return quality.getId();
+    }
+
+    private void finalizeStock(MesQcSeedQualityDO quality, boolean passed) {
+        MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(quality.getCode());
+        if (bag == null || bag.getInboundEventId() == null || !Objects.equals(bag.getQcStatus(), MesProBagCodeQcStatusEnum.UNCHECKED.getStatus()))
+            throw new IllegalArgumentException("琚嬬爜鐘舵�佸凡鍙樺寲锛岀姝㈤噸澶嶅簱瀛樻祦杞�");
+        MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId());
+        if (batch == null) throw new IllegalArgumentException("琚嬬爜鎵�灞炵敓浜ф壒娆′笉瀛樺湪");
+        MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
+        if (wmBatch == null) throw new IllegalArgumentException("鏆傚瓨搴撳瓨鎵�灞炰粨搴撴壒娆′笉瀛樺湪");
+        MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(quality.getQualifiedWarehouseId());
+        if (passed && (qualifiedWarehouse == null || Boolean.TRUE.equals(qualifiedWarehouse.getFrozen())
+                || !MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())))
+            throw new IllegalArgumentException("鐩爣鍚堟牸搴撲笉瀛樺湪鎴栫被鍨嬪凡鍙樺寲");
+        Long qualifiedLocationId = null;
+        Long qualifiedAreaId = null;
+        if (passed) {
+            List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId());
+            if (CollUtil.isNotEmpty(locations)) {
+                qualifiedLocationId = locations.get(0).getId();
+                List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId);
+                if (CollUtil.isNotEmpty(areas)) qualifiedAreaId = areas.get(0).getId();
+            }
+        }
         // 鎸夎鐮佺矑搴︽祦杞簱瀛橈細閿佸畾璇ヨ鍦ㄦ殏瀛樺簱涓殑娲诲姩鏄庣粏锛岀姝㈡寜鎵规浠绘剰鎵e噺
         MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectActiveByBagCode(bag.getCode());
         MesWmMaterialStockDO tempStock = resolveTempStock(bag, wmBatch, stockBag);
@@ -232,7 +256,7 @@
                 .setType(MesWmTransactionTypeEnum.OUT.getType())
                 .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
                 .setBizId(quality.getId())
-                .setBizCode(reqVO.getCode())
+                .setBizCode(quality.getCode())
                 .setBizLineId(quality.getId())
                 .setItemId(batch.getItemId())
                 .setQuantity(BigDecimal.ONE.negate())
@@ -255,7 +279,7 @@
                     .setType(MesWmTransactionTypeEnum.IN.getType())
                     .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
                     .setBizId(quality.getId())
-                    .setBizCode(reqVO.getCode())
+                    .setBizCode(quality.getCode())
                     .setBizLineId(quality.getId())
                     .setItemId(batch.getItemId())
                     .setQuantity(BigDecimal.ONE)
@@ -275,14 +299,113 @@
             // B2. 鍚屾杩佺Щ琚嬬爜鏄庣粏鍒板悎鏍煎簱瀛樿惤鐐�
             materialStockBagService.moveIn(qualInReq, qualifiedStock, qualInTxId);
             bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.QUALIFIED.getStatus());
-            log.info("[submit][璐ㄦ鍚堟牸锛岃鐮� {} 鐢辨殏瀛樺簱杞悎鏍煎簱 {}]", bag.getCode(), qualifiedWarehouse.getName());
+            log.info("[finalizeStock][瀹℃壒鏀捐锛岃鐮� {} 鐢辨殏瀛樺簱杞悎鏍煎簱 {}]", bag.getCode(), qualifiedWarehouse.getName());
         } else {
             // C. 璐ㄦ涓嶅悎鏍� 鈫� 璇ヨ鎶ュ簾锛涙殏瀛樺嚭搴撲簨鍔″凡鎵e噺鏆傚瓨搴撳瓨锛屾爣璁拌鐮佹姤搴�
             materialStockBagService.scrap(tempOutReq, tempOutTxId);
             bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.UNQUALIFIED.getStatus());
-            log.info("[submit][璐ㄦ涓嶅悎鏍硷紝琚嬬爜 {} 鎶ュ簾]", bag.getCode());
+            log.info("[finalizeStock][瀹℃壒鎶ュ簾锛岃鐮� {}]", bag.getCode());
         }
-        return quality.getId();
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO req) {
+        MesQcSeedQualityDO quality = requireStatus(id, WAIT_RETEST);
+        if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("澶嶆琚嬬爜涓庡師璐ㄦ璁板綍涓嶄竴鑷�");
+        boolean passed = validateIndicators(req.getIndicators());
+        if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("澶嶆涓嶅悎鏍煎繀椤诲~鍐欏師鍥�");
+        recordAction(id, "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators());
+        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2)
+                .reason(passed ? "" : req.getReason().trim()).retestResult(passed ? 1 : 2).retestedAt(LocalDateTime.now())
+                .inspectorId(getLoginUserId()).inspectedAt(LocalDateTime.now())
+                .workflowStatus(passed ? WAIT_PROCESS : WAIT_PROCESS_FAIL).build());
+        // 椤甸潰銆佸叕寮�杩芥函璇诲彇涓绘寚鏍囪〃锛涘悓姝ヤ负鏈�鏂颁竴娆℃祴鍊硷紝鍘嗘娴嬪�煎湪鎿嶄綔鐣欑棔琛ㄤ繚鐣欍��
+        Map<String, MesQcSeedQualityIndicatorDO> existing = new HashMap<>();
+        for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityId(id))
+            existing.put(indicator.getIndicatorCode(), indicator);
+        for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) {
+            MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode());
+            if (indicator == null) throw new IllegalArgumentException("璐ㄦ鎸囨爣璁板綍缂哄け");
+            indicatorMapper.updateById(MesQcSeedQualityIndicatorDO.builder().id(indicator.getId())
+                    .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
+        }
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void processApprove(Long id, boolean approved, String opinion) {
+        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
+        if (quality == null || !Set.of(WAIT_PROCESS, WAIT_PROCESS_FAIL).contains(quality.getWorkflowStatus()))
+            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛屽姞宸ラ儴瀹℃壒");
+        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("璇峰~鍐欏鎵规剰瑙�");
+        int next = approved ? (quality.getWorkflowStatus() == WAIT_PROCESS ? WAIT_COMPANY : WAIT_COMPANY_FAIL) : WAIT_RETEST;
+        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
+                .processApprovedAt(LocalDateTime.now()).build());
+        recordAction(id, approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null);
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void companyApprove(Long id, boolean approved, String opinion) {
+        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
+        if (quality == null || !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus()))
+            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛屽叕鍙歌川妫�瀹℃壒");
+        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("璇峰~鍐欏鎵规剰瑙�");
+        if (approved) finalizeStock(quality, quality.getWorkflowStatus() == WAIT_COMPANY);
+        int next = approved ? (quality.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : SCRAPPED) : WAIT_RETEST;
+        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
+                .companyApprovedAt(LocalDateTime.now()).build());
+        recordAction(id, approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null);
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void processApproveBatch(List<Long> ids, boolean approved, String opinion) {
+        for (Long id : approvalIds(ids)) processApprove(id, approved, opinion);
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void companyApproveBatch(List<Long> ids, boolean approved, String opinion) {
+        for (Long id : approvalIds(ids)) companyApprove(id, approved, opinion);
+    }
+
+    private List<Long> approvalIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty() || ids.size() > 500 || ids.stream().anyMatch(Objects::isNull))
+            throw new IllegalArgumentException("璇烽�夋嫨 1 鑷� 500 鏉¤川妫�璁板綍");
+        List<Long> sorted = ids.stream().distinct().sorted().toList();
+        if (sorted.size() != ids.size()) throw new IllegalArgumentException("璐ㄦ璁板綍涓嶈兘閲嶅閫夋嫨");
+        return sorted;
+    }
+
+    private MesQcSeedQualityDO requireStatus(Long id, int expected) {
+        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
+        if (quality == null || !Objects.equals(quality.getWorkflowStatus(), expected))
+            throw new IllegalArgumentException("璐ㄦ璁板綍鐘舵�佸凡鍙樺寲锛岃鍒锋柊鍚庨噸璇�");
+        return quality;
+    }
+
+    private boolean validateIndicators(List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
+        if (indicators == null || indicators.size() != REQUIRED.size()) throw new IllegalArgumentException("蹇呴』鎻愪氦鍥涢」鎸囨爣");
+        Set<String> codes = new HashSet<>();
+        boolean passed = true;
+        for (MesQcSeedQualitySubmitReqVO.Indicator item : indicators) {
+            if (item == null || !REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())
+                    || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null)
+                throw new IllegalArgumentException("鎸囨爣蹇呴』涓虹函搴︺�佸噣搴︺�佸彂鑺界巼銆佹按鍒嗕笖涓嶅彲閲嶅");
+            if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("鎸囨爣涓嬮檺涓嶈兘澶т簬涓婇檺");
+            if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
+        }
+        return passed;
+    }
+
+    private void recordAction(Long id, String action, Integer result, String opinion,
+                              List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
+        MesQcSeedQualityActionDO event = new MesQcSeedQualityActionDO();
+        event.setQualityId(id);
+        event.setAction(action);
+        event.setResult(result);
+        event.setOpinion(opinion);
+        event.setIndicatorsJson(indicators == null ? null : JSONUtil.toJsonStr(indicators));
+        event.setOperatorId(getLoginUserId());
+        event.setCreateTime(LocalDateTime.now());
+        actionMapper.insert(event);
     }
 
     /**
@@ -392,4 +515,9 @@
             return List.of();
         }
     }
+
+    @Override
+    public List<MesQcSeedQualityActionDO> getActions(Long qualityId) {
+        return actionMapper.selectByQualityId(qualityId);
+    }
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
index b88d63c..c02c100 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -378,7 +378,9 @@
         }
         MesQcSeedQualityDO quality = list.get(0);
         MesProTracePublicRespVO.QualityVO vo = new MesProTracePublicRespVO.QualityVO();
-        vo.setResult(quality.getResult()).setResultName(qualityResultName(quality.getResult()))
+        Integer finalResult = Integer.valueOf(40).equals(quality.getWorkflowStatus()) ? 1
+                : Integer.valueOf(70).equals(quality.getWorkflowStatus()) ? 2 : 0;
+        vo.setResult(finalResult).setResultName(finalResult == 0 ? "澶嶆鎴栧鎵逛腑" : qualityResultName(finalResult))
                 .setReason(quality.getReason()).setInspectorName(userName(quality.getInspectorId()))
                 .setInspectedAt(quality.getInspectedAt());
         List<MesQcSeedQualityIndicatorDO> indicators = qcSeedQualityService.getIndicatorsByQualityId(quality.getId());
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
index bcc78f5..d24b047 100644
--- a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
@@ -2,6 +2,7 @@
 
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
 import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
@@ -11,6 +12,9 @@
 import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService;
 import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
 import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
+import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
+import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import jakarta.annotation.Resource;
 import org.junit.jupiter.api.Test;
@@ -55,6 +59,12 @@
     private AdminUserApi adminUserApi;
     @MockitoBean
     private MesQcIndicatorResultService indicatorResultService;
+    @MockitoBean
+    private MesQcNcrService ncrService;
+    @MockitoBean
+    private MdmItemApi mdmItemApi;
+    @MockitoBean
+    private CrmCustomerApi customerApi;
 
     @Test
     public void testFinishOqc_writeBack_productSales() {
@@ -66,6 +76,9 @@
             o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
             o.setSourceLineId(sourceLineId);
             o.setCheckResult(checkResult);
+            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
+            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
+            o.setCheckQuantity(java.math.BigDecimal.TEN);
         });
         oqcMapper.insert(oqc);
 
@@ -75,9 +88,83 @@
         // 鏂█锛氶獙璇佺姸鎬佹洿鏂颁负宸插畬鎴�
         MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
         assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus());
-        // 鏂█锛氶獙璇佸洖鍐欓攢鍞嚭搴撳崟琛�
+        assertEquals(20, updatedOqc.getWorkflowStatus());
+        // 鍒濇瀹屾垚涓嶆斁琛岋紱閿�鍞儴鍜屽叕鍙歌川妫�鍧囧鎵瑰悗鎵嶅洖鍐欍��
+        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
+        oqcService.salesApprove(oqc.getId(), true, "閿�鍞儴鍚屾剰");
+        oqcService.companyApprove(oqc.getId(), true, "鍏徃璐ㄦ鍚屾剰");
         verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(
                 eq(sourceLineId), eq(oqc.getId()), eq(checkResult));
+    }
+
+    @Test
+    public void testFailedInitialRetestPassOnlyReleasesAfterBothApprovals() {
+        Long sourceLineId = randomLongId();
+        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
+            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
+            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
+            o.setSourceLineId(sourceLineId);
+            o.setOutQuantity(java.math.BigDecimal.valueOf(100));
+            o.setCheckQuantity(java.math.BigDecimal.TEN);
+            o.setQualifiedQuantity(java.math.BigDecimal.valueOf(9));
+            o.setUnqualifiedQuantity(java.math.BigDecimal.ONE);
+            o.setCheckResult(2);
+        });
+        oqcMapper.insert(oqc);
+        oqcService.finishOqc(oqc.getId());
+        assertEquals(10, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
+        verifyNoInteractions(ncrService);
+        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
+
+        MesQcOqcRetestReqVO retest = new MesQcOqcRetestReqVO();
+        retest.setId(oqc.getId());
+        retest.setCheckQuantity(java.math.BigDecimal.TEN);
+        retest.setQualifiedQuantity(java.math.BigDecimal.TEN);
+        retest.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
+        retest.setCheckResult(1);
+        oqcService.retestOqc(retest);
+        assertEquals(20, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
+        oqcService.salesApprove(oqc.getId(), true, "閿�鍞儴閫氳繃");
+        assertEquals(30, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
+        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
+        oqcService.companyApprove(oqc.getId(), true, "鍏徃璐ㄦ閫氳繃");
+        assertEquals(40, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
+        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(sourceLineId, oqc.getId(), 1);
+        verifyNoInteractions(ncrService);
+    }
+
+    @Test
+    public void testFailedRetestCreatesNcrOnlyAfterCompanyApproval() {
+        Long sourceLineId = randomLongId();
+        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
+            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
+            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
+            o.setSourceLineId(sourceLineId);
+            o.setOutQuantity(java.math.BigDecimal.valueOf(100));
+            o.setCheckQuantity(java.math.BigDecimal.TEN);
+            o.setQualifiedQuantity(java.math.BigDecimal.valueOf(9));
+            o.setUnqualifiedQuantity(java.math.BigDecimal.ONE);
+            o.setCheckResult(2);
+        });
+        oqcMapper.insert(oqc);
+        oqcService.finishOqc(oqc.getId());
+        MesQcOqcRetestReqVO retest = new MesQcOqcRetestReqVO();
+        retest.setId(oqc.getId());
+        retest.setCheckQuantity(java.math.BigDecimal.TEN);
+        retest.setQualifiedQuantity(java.math.BigDecimal.valueOf(8));
+        retest.setUnqualifiedQuantity(java.math.BigDecimal.valueOf(2));
+        retest.setCheckResult(2);
+        retest.setOpinion("澶嶆浠嶄笉鍚堟牸");
+        oqcService.retestOqc(retest);
+        assertEquals(50, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
+        verifyNoInteractions(ncrService);
+        oqcService.salesApprove(oqc.getId(), true, "閿�鍞儴纭涓嶅悎鏍�");
+        assertEquals(60, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
+        verifyNoInteractions(ncrService);
+        oqcService.companyApprove(oqc.getId(), true, "鍏徃璐ㄦ纭涓嶅悎鏍�");
+        assertEquals(70, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
+        verify(ncrService).createNcr(any());
+        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(sourceLineId, oqc.getId(), 2);
     }
 
     @Test
@@ -86,6 +173,9 @@
         MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
             o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
             o.setCheckResult(1);
+            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
+            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
+            o.setCheckQuantity(java.math.BigDecimal.TEN);
             o.setSourceDocType(null);
             o.setSourceLineId(null);
         });
@@ -108,13 +198,17 @@
         MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
             o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
             o.setCheckResult(1);
+            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
+            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
+            o.setCheckQuantity(java.math.BigDecimal.TEN);
             o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
             o.setSourceLineId(null); // 鑴忔暟鎹�
         });
         oqcMapper.insert(oqc);
 
-        // 璋冪敤锛屽苟鏂█寮傚父
-        assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
+        oqcService.finishOqc(oqc.getId());
+        oqcService.salesApprove(oqc.getId(), true, "閿�鍞儴鍚屾剰");
+        assertThrows(IllegalArgumentException.class, () -> oqcService.companyApprove(oqc.getId(), true, "鍏徃璐ㄦ鍚屾剰"));
     }
 
     @Test
@@ -123,13 +217,17 @@
         MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
             o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
             o.setCheckResult(1);
+            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
+            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
+            o.setCheckQuantity(java.math.BigDecimal.TEN);
             o.setSourceDocType(99999); // 鏃犳硶璇嗗埆鐨勭被鍨�
             o.setSourceLineId(randomLongId());
         });
         oqcMapper.insert(oqc);
 
-        // 璋冪敤锛屽苟鏂█寮傚父
-        assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
+        oqcService.finishOqc(oqc.getId());
+        oqcService.salesApprove(oqc.getId(), true, "閿�鍞儴鍚屾剰");
+        assertThrows(IllegalArgumentException.class, () -> oqcService.companyApprove(oqc.getId(), true, "鍏徃璐ㄦ鍚屾剰"));
     }
 
     @Test
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java
new file mode 100644
index 0000000..45659c9
--- /dev/null
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java
@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.mes.service.qc.seedquality;
+
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityActionMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityMapper;
+import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class SeedQualityWorkflowTest {
+    private final MesQcSeedQualityServiceImpl service = new MesQcSeedQualityServiceImpl();
+    private final MesQcSeedQualityMapper qualityMapper = mock(MesQcSeedQualityMapper.class);
+    private final MesQcSeedQualityActionMapper actionMapper = mock(MesQcSeedQualityActionMapper.class);
+    private final MesWmTransactionService transactionService = mock(MesWmTransactionService.class);
+
+    @BeforeEach
+    void setUp() {
+        ReflectionTestUtils.setField(service, "qualityMapper", qualityMapper);
+        ReflectionTestUtils.setField(service, "actionMapper", actionMapper);
+        ReflectionTestUtils.setField(service, "wmTransactionService", transactionService);
+    }
+
+    @Test
+    void failedRetestNeedsBothApprovalsBeforeDisposal() {
+        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
+                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(50).build(),
+                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(60).build());
+        service.processApprove(1L, true, "纭涓嶅悎鏍硷紝閫佸叕鍙歌川妫�瀹℃壒");
+        verifyNoInteractions(transactionService);
+        service.companyApprove(1L, false, "杩斿洖澶嶆");
+        ArgumentCaptor<MesQcSeedQualityDO> update = ArgumentCaptor.forClass(MesQcSeedQualityDO.class);
+        verify(qualityMapper, times(2)).updateById(update.capture());
+        assertEquals(60, update.getAllValues().get(0).getWorkflowStatus());
+        assertEquals(10, update.getAllValues().get(1).getWorkflowStatus());
+        verifyNoInteractions(transactionService);
+    }
+
+    @Test
+    void skipsApprovalWhenStatusChanged() {
+        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
+                MesQcSeedQualityDO.builder().id(1L).workflowStatus(40).build());
+        assertThrows(IllegalArgumentException.class, () -> service.companyApprove(1L, true, "閲嶅瀹℃壒"));
+        verify(qualityMapper, never()).updateById(any(MesQcSeedQualityDO.class));
+        verifyNoInteractions(transactionService, actionMapper);
+    }
+}
diff --git a/yudao-module-mes/src/test/resources/sql/create_tables.sql b/yudao-module-mes/src/test/resources/sql/create_tables.sql
index a6ef868..1e8d1c7 100644
--- a/yudao-module-mes/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -413,6 +413,7 @@
     "inspect_date" timestamp DEFAULT NULL,
     "inspector_user_id" bigint DEFAULT NULL,
     "status" tinyint NOT NULL DEFAULT 0,
+    "workflow_status" tinyint NOT NULL DEFAULT 0,
     "remark" varchar(500) DEFAULT '',
     "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -423,6 +424,18 @@
     PRIMARY KEY ("id")
 );
 
+CREATE TABLE IF NOT EXISTS "mes_qc_oqc_action" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "oqc_id" bigint NOT NULL,
+    "action" varchar(32) NOT NULL,
+    "result" tinyint DEFAULT NULL,
+    "opinion" varchar(500) DEFAULT NULL,
+    "quantities_json" varchar(4000) DEFAULT NULL,
+    "operator_id" bigint DEFAULT NULL,
+    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+);
+
 -- ----------------------------
 -- MES 閫�璐ф楠屽崟锛圧QC锛�
 -- ----------------------------

--
Gitblit v1.9.3