1.0.551.sql 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. -- 1.0.551.sql
  2. -- P0-A 数据纠偏:精准清 std 种群 A + 保全 DWD 不可再生历史 + 清空重建 DWD + schema 收口。
  3. --
  4. -- ⚠️ 本脚本尚未在任何库执行(MIGRATION NOT EXECUTED)。
  5. -- 未执行原因是 Stop-Write Gate 未通过:S3 写入方仍在活跃跑批
  6. -- (job_s3_mdp_sync_transform 小时级触发,job_smart_ops_kpi_mdp_bootstrap 随启动触发)。
  7. -- 执行前必须先取得真正的停写窗口,并确认 dwd_process_outsource_delivery
  8. -- 的 MAX(calc_time) 与 COUNT(*) 都不再前进。
  9. --
  10. -- 号段说明(CLAUDE.md §二·7):本文件曾以 WIP-P0A-CLEANUP.sql 占位;
  11. -- rebase 到 origin/master 解决 1.0.536 撞号后先取 1.0.550,
  12. -- 随后与另一实例的 1.0.550(S8 菜单退役)实时撞号,本批让号顺延为 1.0.551。
  13. --
  14. -- ============================================================================
  15. -- 背景(全部为只读实测结论,证据见本批审计回执)
  16. -- dwd_process_outsource_delivery 曾混装两个业务对象:
  17. -- 种群 A = RoutingOpDetail(S0 标准工艺路线明细,主数据),无 work_order / po_no / po_line,
  18. -- 其 std 物化把 po_no/po_line 硬编码为 NULL;而 uk_work_op_stat 含这两列,
  19. -- MySQL 唯一索引 NULL-distinct 语义使 ODKU 永不命中 → 每轮全量追加。
  20. -- 种群 B = PurOrdMaster(Potype='PW') JOIN PurOrdDetail,真实工序委外采购订单行。
  21. -- 代码侧止血已完成(S3_ROUTING_OUTSOURCE 入站注册与 std INSERT 已移除,DWD 投影已补
  22. -- IFNULL(po_no,'') 使 po_no 不再为 NULL)。本脚本只处理存量。
  23. --
  24. -- Option A 目标粒度:一行 = 一条真实 PW 委外订单行在某 stat_date 的交付状态。
  25. -- 目标唯一键:(tenant_id, factory_id, stat_date, po_no, po_line)。
  26. --
  27. -- 历史 stat_date 不可复现(@StatDate = 运行时墙钟日期,无回填入参),且 31 行对应的源单
  28. -- 已从 PurOrdMaster 物理删除 → 保全那 31 行就是历史恢复方案,不做"重新计算"。
  29. -- ============================================================================
  30. SET NAMES utf8mb4;
  31. -- ===========================================================================
  32. -- Step 0 · 前置门禁(fail-fast;任一不成立即中止,绝不带着错误前提往下做)
  33. -- 断言集合关系而非硬编码行数:行数会随跑批漂移,集合关系不会。
  34. -- ===========================================================================
  35. -- 种群 A 主谓词(与审计一致:批次来源 + biz_key 形态 + 结构特征三重冗余)
  36. SET @std_total := (SELECT COUNT(*) FROM `mdp_std_process_outsource_order`);
  37. SET @pop_a := (
  38. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  39. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%'
  40. AND `source_biz_key` NOT LIKE '%|%'
  41. AND `work_order` = ''
  42. AND `po_no` IS NULL
  43. AND `po_line` IS NULL
  44. );
  45. SET @survivors := (
  46. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  47. WHERE NOT (`sync_batch_id` LIKE 'S3\_MDP\_%'
  48. AND `source_biz_key` NOT LIKE '%|%'
  49. AND `work_order` = ''
  50. AND `po_no` IS NULL
  51. AND `po_line` IS NULL)
  52. );
  53. SELECT @std_total AS std_total, @pop_a AS population_a, @survivors AS survivors;
  54. -- G1 · 集合完备:种群 A + 幸存者 必须恰好等于总量(不允许存在第三类)
  55. SET @sql := IF(@pop_a + @survivors <> @std_total,
  56. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G1: population_a + survivors <> std_total — predicate incomplete, abort'",
  57. 'DO 0');
  58. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  59. -- G2 · 血缘交叉验证:主谓词命中集必须与 stg 血缘回溯集完全一致(双向差集为 0)
  60. -- 注意:不得在 EXISTS 里附加 source_system 等值条件 —— stg 侧两代分属
  61. -- AIDOP(colon 形态) 与 AIDOPDEV_MYSQL(digits 形态),而 std 侧被统一硬编码为 'AIDOP',
  62. -- 加上该条件会假阴约 7.6 万行。
  63. SET @main_not_prov := (
  64. SELECT COUNT(*) FROM `mdp_std_process_outsource_order` o
  65. WHERE o.`sync_batch_id` LIKE 'S3\_MDP\_%' AND o.`source_biz_key` NOT LIKE '%|%'
  66. AND o.`work_order` = '' AND o.`po_no` IS NULL AND o.`po_line` IS NULL
  67. AND NOT EXISTS (SELECT 1 FROM `mdp_stg_work_order_material` s
  68. WHERE s.`source_table` = 'RoutingOpDetail'
  69. AND s.`source_biz_key` = o.`source_biz_key`)
  70. );
  71. SET @prov_not_main := (
  72. SELECT COUNT(*) FROM `mdp_std_process_outsource_order` o
  73. WHERE EXISTS (SELECT 1 FROM `mdp_stg_work_order_material` s
  74. WHERE s.`source_table` = 'RoutingOpDetail'
  75. AND s.`source_biz_key` = o.`source_biz_key`)
  76. AND NOT (o.`sync_batch_id` LIKE 'S3\_MDP\_%' AND o.`source_biz_key` NOT LIKE '%|%'
  77. AND o.`work_order` = '' AND o.`po_no` IS NULL AND o.`po_line` IS NULL)
  78. );
  79. SELECT @main_not_prov AS main_not_provenance, @prov_not_main AS provenance_not_main;
  80. SET @sql := IF(@main_not_prov > 0 OR @prov_not_main > 0,
  81. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G2: cleanup predicate disagrees with stg provenance — abort'",
  82. 'DO 0');
  83. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  84. -- G3 · 幸存者必须全部是真实 PW 行(biz_key 形如 PurOrd|Line 且 po_no 非空)
  85. SET @bad_survivor := (
  86. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  87. WHERE NOT (`sync_batch_id` LIKE 'S3\_MDP\_%' AND `source_biz_key` NOT LIKE '%|%'
  88. AND `work_order` = '' AND `po_no` IS NULL AND `po_line` IS NULL)
  89. AND (`source_biz_key` NOT LIKE '%|%' OR IFNULL(`po_no`,'') = '')
  90. );
  91. SET @sql := IF(@bad_survivor > 0,
  92. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G3: survivor set contains non-PW rows — abort'",
  93. 'DO 0');
  94. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  95. -- G4 · overlap:种群 A 与真实 PW 集不得有交集
  96. SET @overlap := (
  97. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  98. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%' AND `source_biz_key` NOT LIKE '%|%'
  99. AND `work_order` = '' AND `po_no` IS NULL AND `po_line` IS NULL
  100. AND `source_biz_key` LIKE '%|%'
  101. );
  102. SET @sql := IF(@overlap > 0,
  103. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G4: population_a overlaps real PW rows — abort'",
  104. 'DO 0');
  105. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  106. -- G5 · 待保全的 DWD 历史必须非空且 po_no/po_line 干净
  107. -- ⚠️ 谓词必须是 IFNULL(po_no,'') <> '' 而不是 po_no IS NOT NULL:
  108. -- 代码侧 IFNULL 补丁上线后,种群 A 的行 po_no 变成**空串**而非 NULL,
  109. -- 用 IS NOT NULL 会把这些垃圾一并当成"不可再生历史"保下来。
  110. SET @preserve_cnt := (SELECT COUNT(*) FROM `dwd_process_outsource_delivery` WHERE IFNULL(`po_no`,'') <> '');
  111. SELECT @preserve_cnt AS dwd_rows_to_preserve;
  112. SET @sql := IF(@preserve_cnt = 0,
  113. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G5: no real-PO rows found in DWD — refuse to truncate blindly'",
  114. 'DO 0');
  115. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  116. -- ===========================================================================
  117. -- Step 1 · 保全 DWD 不可再生历史 + 元数据留证
  118. -- 只备份真实 PO 子集(约 31 行),不复制 3269 万行错误派生数据:
  119. -- 零消费者 / 零 FK / 可重建(今后)/ 历史上已 TRUNCATE 过 / 权威源仍在。
  120. -- ===========================================================================
  121. CREATE TABLE IF NOT EXISTS `dwd_process_outsource_delivery_keep_p0a` LIKE `dwd_process_outsource_delivery`;
  122. INSERT IGNORE INTO `dwd_process_outsource_delivery_keep_p0a`
  123. SELECT * FROM `dwd_process_outsource_delivery` WHERE IFNULL(`po_no`,'') <> '';
  124. -- 保全完整性断言:备份行数必须等于源侧真实 PO 行数
  125. SET @kept := (SELECT COUNT(*) FROM `dwd_process_outsource_delivery_keep_p0a`);
  126. SET @sql := IF(@kept <> @preserve_cnt,
  127. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-S1: preserved row count mismatch — abort before truncate'",
  128. 'DO 0');
  129. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  130. -- 元数据留证(复用 1.0.350 既有范式与表)
  131. CREATE TABLE IF NOT EXISTS `aidop_migration_derived_cleanup_log` (
  132. `id` BIGINT NOT NULL AUTO_INCREMENT,
  133. `migration_version` VARCHAR(32) NOT NULL,
  134. `table_name` VARCHAR(128) NOT NULL,
  135. `estimated_rows` BIGINT NOT NULL DEFAULT 0,
  136. `data_length` BIGINT NOT NULL DEFAULT 0,
  137. `reason` VARCHAR(500) NOT NULL,
  138. `captured_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  139. PRIMARY KEY (`id`),
  140. UNIQUE KEY `uk_migration_cleanup` (`migration_version`,`table_name`)
  141. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  142. INSERT INTO `aidop_migration_derived_cleanup_log`
  143. (`migration_version`,`table_name`,`estimated_rows`,`data_length`,`reason`)
  144. SELECT '1.0.551','dwd_process_outsource_delivery',IFNULL(TABLE_ROWS,0),IFNULL(DATA_LENGTH,0),
  145. CONCAT('P0-A:RoutingOpDetail 主数据被误建模为委外交付事实,叠加 uk 含可空 po_no 致 ODKU 永不命中,膨胀至 3269 万行;',
  146. '真实 PO 子集 ', @preserve_cnt, ' 行已存入 dwd_process_outsource_delivery_keep_p0a 并于 schema 收口后回插;',
  147. '其余为错误派生数据,零消费者、零 FK、可由 std 重新生成,不做全量备份')
  148. FROM information_schema.TABLES
  149. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery'
  150. ON DUPLICATE KEY UPDATE `reason`=VALUES(`reason`), `estimated_rows`=VALUES(`estimated_rows`), `data_length`=VALUES(`data_length`);
  151. -- ===========================================================================
  152. -- Step 2 · 精准清 std 种群 A(保留真实 PW;不得 TRUNCATE 整表)
  153. -- 备份沿用 1.0.351/352/353 的范式 A:CREATE TABLE LIKE + INSERT IGNORE + DELETE。
  154. -- ===========================================================================
  155. CREATE TABLE IF NOT EXISTS `mdp_std_process_outsource_order_bak_p0a` LIKE `mdp_std_process_outsource_order`;
  156. INSERT IGNORE INTO `mdp_std_process_outsource_order_bak_p0a`
  157. SELECT * FROM `mdp_std_process_outsource_order`
  158. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%'
  159. AND `source_biz_key` NOT LIKE '%|%'
  160. AND `work_order` = ''
  161. AND `po_no` IS NULL
  162. AND `po_line` IS NULL;
  163. DELETE FROM `mdp_std_process_outsource_order`
  164. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%'
  165. AND `source_biz_key` NOT LIKE '%|%'
  166. AND `work_order` = ''
  167. AND `po_no` IS NULL
  168. AND `po_line` IS NULL;
  169. -- 清理后立即断言:种群 A 归零,且幸存者数量与门禁阶段一致
  170. SET @pop_a_after := (
  171. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  172. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%' AND `source_biz_key` NOT LIKE '%|%'
  173. AND `work_order` = '' AND `po_no` IS NULL AND `po_line` IS NULL
  174. );
  175. SET @std_after := (SELECT COUNT(*) FROM `mdp_std_process_outsource_order`);
  176. SELECT @pop_a_after AS population_a_after, @std_after AS std_total_after;
  177. SET @sql := IF(@pop_a_after <> 0 OR @std_after <> @survivors,
  178. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-S2: std cleanup result mismatch — abort before DWD truncate'",
  179. 'DO 0');
  180. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  181. -- 注:tenant 0 / 1300000000001 的真实 PW 行仍保留在 std。
  182. -- "进不了 DWD"(被 CHECK 拦)不等于"应该从 std 删除"。
  183. -- ===========================================================================
  184. -- Step 3 · 清空 DWD
  185. -- 选 TRUNCATE 而非分批 DELETE:错误数据无业务价值、零 FK、零消费者,
  186. -- 且 DELETE 会产生约 5-8GB binlog(ROW+FULL image)与数十 GB undo,
  187. -- 却**不把空间归还 OS**;TRUNCATE 立即归还约 7.9GB。
  188. -- ===========================================================================
  189. TRUNCATE TABLE `dwd_process_outsource_delivery`;
  190. -- ===========================================================================
  191. -- Step 4 · 空表期 schema 收口(此时行数为 0,DDL 代价最低且不会因存量 NULL 失败)
  192. -- ===========================================================================
  193. -- 4.1 po_no / po_line → NOT NULL DEFAULT ''(幂等:仅当当前仍 nullable 时执行)
  194. SET @po_no_nullable := (SELECT IS_NULLABLE FROM information_schema.COLUMNS
  195. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND COLUMN_NAME='po_no');
  196. SET @sql := IF(@po_no_nullable = 'YES',
  197. "ALTER TABLE `dwd_process_outsource_delivery` MODIFY COLUMN `po_no` VARCHAR(100) NOT NULL DEFAULT ''",
  198. 'DO 0');
  199. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  200. SET @po_line_nullable := (SELECT IS_NULLABLE FROM information_schema.COLUMNS
  201. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND COLUMN_NAME='po_line');
  202. SET @sql := IF(@po_line_nullable = 'YES',
  203. "ALTER TABLE `dwd_process_outsource_delivery` MODIFY COLUMN `po_line` VARCHAR(50) NOT NULL DEFAULT ''",
  204. 'DO 0');
  205. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  206. -- 4.2 唯一键改为 Option A 粒度:(tenant_id, factory_id, stat_date, po_no, po_line)
  207. -- 旧键 uk_work_op_stat 含 work_order/op_code,对 RoutingOpDetail 来源无区分度。
  208. SET @has_old_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  209. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND INDEX_NAME='uk_work_op_stat');
  210. SET @sql := IF(@has_old_uk > 0,
  211. 'ALTER TABLE `dwd_process_outsource_delivery` DROP INDEX `uk_work_op_stat`',
  212. 'DO 0');
  213. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  214. SET @has_new_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  215. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND INDEX_NAME='uk_po_line_stat');
  216. SET @sql := IF(@has_new_uk = 0,
  217. 'ALTER TABLE `dwd_process_outsource_delivery` ADD UNIQUE INDEX `uk_po_line_stat` (`tenant_id`,`factory_id`,`stat_date`,`po_no`,`po_line`)',
  218. 'DO 0');
  219. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  220. -- 4.3 租户守卫 CHECK:按**实际名**处理,不得沿用 1.0.353 声明的名字
  221. -- 实测漂移:1.0.353 声明 ck_dwd_process_outsource_valid_tenant,
  222. -- 但该表在 1.0.353 之后被 CodeFirst 重建过,约束被匿名重建为
  223. -- dwd_process_outsource_delivery_chk_1(全库唯一的自动命名 CHECK)。
  224. -- 此处按语义(CHECK_CLAUSE 含 tenant_id 且含 1300000000001)定位,统一收敛到稳定名。
  225. SET @tenant_chk_cnt := (
  226. SELECT COUNT(*) FROM information_schema.CHECK_CONSTRAINTS cc
  227. JOIN information_schema.TABLE_CONSTRAINTS tc
  228. ON tc.CONSTRAINT_SCHEMA=cc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME=cc.CONSTRAINT_NAME
  229. WHERE tc.TABLE_SCHEMA=DATABASE() AND tc.TABLE_NAME='dwd_process_outsource_delivery'
  230. AND tc.CONSTRAINT_TYPE='CHECK'
  231. AND cc.CHECK_CLAUSE LIKE '%tenant_id%' AND cc.CHECK_CLAUSE LIKE '%1300000000001%'
  232. );
  233. SET @sql := IF(@tenant_chk_cnt > 1,
  234. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-S4: duplicate tenant-guard CHECK constraints found — manual review required'",
  235. 'DO 0');
  236. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  237. SET @tenant_chk_name := (
  238. SELECT tc.CONSTRAINT_NAME FROM information_schema.CHECK_CONSTRAINTS cc
  239. JOIN information_schema.TABLE_CONSTRAINTS tc
  240. ON tc.CONSTRAINT_SCHEMA=cc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME=cc.CONSTRAINT_NAME
  241. WHERE tc.TABLE_SCHEMA=DATABASE() AND tc.TABLE_NAME='dwd_process_outsource_delivery'
  242. AND tc.CONSTRAINT_TYPE='CHECK'
  243. AND cc.CHECK_CLAUSE LIKE '%tenant_id%' AND cc.CHECK_CLAUSE LIKE '%1300000000001%'
  244. LIMIT 1
  245. );
  246. -- 若现存名不是稳定名,则丢弃后按稳定名重建(空表期,代价可忽略)
  247. SET @sql := IF(@tenant_chk_name IS NOT NULL AND @tenant_chk_name <> 'ck_dwd_process_outsource_valid_tenant',
  248. CONCAT('ALTER TABLE `dwd_process_outsource_delivery` DROP CHECK `', @tenant_chk_name, '`'),
  249. 'DO 0');
  250. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  251. SET @has_stable_chk := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
  252. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery'
  253. AND CONSTRAINT_TYPE='CHECK' AND CONSTRAINT_NAME='ck_dwd_process_outsource_valid_tenant');
  254. SET @sql := IF(@has_stable_chk = 0,
  255. 'ALTER TABLE `dwd_process_outsource_delivery` ADD CONSTRAINT `ck_dwd_process_outsource_valid_tenant` CHECK (`tenant_id` NOT IN (0,1,1300000000001))',
  256. 'DO 0');
  257. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  258. -- ===========================================================================
  259. -- Step 5 · 回插保全的历史
  260. -- 显式排除非法租户(不依赖隐式 filter),并显式要求 po_no/po_line 非空。
  261. -- ===========================================================================
  262. INSERT INTO `dwd_process_outsource_delivery`
  263. SELECT * FROM `dwd_process_outsource_delivery_keep_p0a`
  264. WHERE `tenant_id` NOT IN (0,1,1300000000001)
  265. AND IFNULL(`po_no`,'') <> ''
  266. AND IFNULL(`po_line`,'') <> '';
  267. SELECT COUNT(*) AS dwd_rows_after_restore FROM `dwd_process_outsource_delivery`;
  268. SELECT `estimated_rows` AS rows_before_cleanup FROM `aidop_migration_derived_cleanup_log`
  269. WHERE `migration_version`='1.0.551' AND `table_name`='dwd_process_outsource_delivery';
  270. -- ===========================================================================
  271. -- 回滚(注释,非可执行;执行前请确认无消费者)
  272. -- std:INSERT IGNORE INTO mdp_std_process_outsource_order
  273. -- SELECT * FROM mdp_std_process_outsource_order_bak_p0a;
  274. -- dwd:错误派生数据不可回滚也不需要回滚(零消费者、可由 std 重新生成);
  275. -- 真实历史由 dwd_process_outsource_delivery_keep_p0a 保全,可再次回插。
  276. -- schema:ALTER ... MODIFY po_no/po_line 恢复 nullable;
  277. -- DROP INDEX uk_po_line_stat; ADD UNIQUE uk_work_op_stat(...原列序...)。
  278. -- ⚠️ 回滚 schema 会让 ODKU 重新失效、膨胀复发,仅在确认整改方案作废时执行。
  279. -- ===========================================================================