# 六库合一:重名表冲突处理表 > 方案 §4.4 第 1 步要求的交付物。数据来源见 [`legacy-db-actual.md`](./legacy-db-actual.md)。 > > **行数分两层,后一层取代前一层**: > 1. ~~`information_schema.TABLES.TABLE_ROWS` 估算~~(首版;`TABLE_ROWS` 是采样值,不准) > 2. **`COUNT(*)` 精确值 + 跨库 `LEFT JOIN` 子集证明** —— 由 > [`tools/check_subset.py`](./tools/check_subset.py) 产出,**2026-10-09** 跑。 > 六个库在同一实例上,所以"副本里有多少行是主份没有的"可以**问死**,不用抽样。 > 本文件正文里的行数一律是这一层的实测值;被推翻的首版估算不再保留,避免误引。 > > ⚠️ `zt_park` 是**在线库**,仍在写入:`park_in_out_record` 两次跑分别是 > 97,980,500 / 97,980,455 行。**差值是活数据,不影响结论**(结论是"0 行独有")。 ## 0. 通用规则 **默认动作:保留"业务域归属库"里的那一份,其余份先不搬**(老库保留只读,随时可取)。 因为 `legacy-db-actual.md` §1 的模块×库矩阵已证明代码访问的就是归属库那一份。 ⚠️ 三条**必须逐一确认**的例外: 1. **副本上有独有列** → 说明可能有代码在写副本,先查调用方(标 🔴) 2. **两侧行数都非 0** → 不能假定"主份已包含副本全部行",**必须做子集证明**(标 🟡/🔴) 3. **键要选对**——见下 ### 关于「键」 判定"副本的行在不在主份里",必须用**业务键**而不是主键:副本常换过 id 空间 (`parking_lot` 的 `A150402000` 在主份是 `P15040225Z`;`td_b_wx_config` 的 `wx1e9001e5940605b1` 在两侧 id 不同)。但键选错会造出**成片假独有**: | 表 | 键 | 独有行数 | 真话在哪 | |---|---|---:|---| | `t_vip_card_cust_info` | `id` → 27"独有" | **`card_no` → 0** | 同一批卡换了 id | | `td_b_wx_config` | `org_id` → 18"独有" | **`appid` → 3** | 老库 `org_id` 大量为 NULL,`NULL=NULL` 不成立,join 必然落空 | > 规律:**键列里可能有 NULL,用它 join 就一定会误报**。选键前先问 > 「这个列在老数据里是否普遍为空」。`td_b_wx_config` 的真实查询键是 `appid`—— > `WxPayApiConfigKit` 就是按 appId 找配置的(找不到直接抛 `IllegalStateException`)。 ## 1. ✅ 结构完全一致 —— 7 组,精确校验全部通过 结构(列+索引+排序规则)逐字节一致。子集证明**全部 0 行独有**,可直接丢副本。 | 表 | 保留 | 丢弃 | 行数(保留/丢弃) | |---|---|---|---| | `td_b_settlement` | `zt_order_n` | `zt_order` | 0 / 0 | | `td_b_settlement_org` | `zt_order_n` | `zt_order` | 9 / 1 | | `td_b_settlement_serial` | `zt_order_n` | `zt_order` | 4 / 0 | | `td_b_settlement_share_record` | `zt_order_n` | `zt_order` | 33 / 20 | | `td_p_parking_share_order_temp` | `zt_order_n` | `zt_order` | 0 / 0 | | `income_temp` | `zt_order_n` | `zt_park` | 353 / 353 | | `month_dchz` | `zt_order_n` | `zt_park` | 199 / 199 | | `table_15_free` | `zt_order_n` | `zt_park` | 344 / 344 | > 首版对 `td_b_settlement_share_record` / `income_temp` / `month_dchz` / `table_15_free` > 标的是"需抽样比对 / 取并集或查清差的 2 行"。精确校验后**四项都是 0 行独有**, > 无需并集:`table_15_free` 那个"差 2 行"是 `TABLE_ROWS` 估算误差,实测两侧都是 344。 > `month_dchz` / `table_15_free` **两侧都没有主键**,已改用业务键 > `time,pl_no` / `dateStr,pl_no` 判定(键已固化进 `check_subset.py`)。 ## 2. ⚠️ 结构不一致 —— 12 组 ### 2.1 可直接定:主份是超集且活跃(5 组) 主份**列是超集**,且子集证明 0 行独有 → 直接保留主份、丢副本。 | 表 | 保留 | 丢弃(行数) | 列差异 | 子集证明 | |---|---|---|---|---| | `parking_lot` | **`zt_park`** 3124 | `zt_bill` 1603、`zt_order` 1465、`zt_order_n` 1979 | 主份 47 列;副本 37/37/45 | 见下方专段 | | `park_in_out_record` | **`zt_park`** 97,980,455 | `zt_order` 1,242,863 | — | **✅ 0 行独有** | | `t_vip_card_cust_info` | **`zt_bill`** 108,583 | `zt_order` 1,912 | 主份多 `cust_name`/`cust_phone`/`parent_cust_card_no` | **✅ 0 行独有(按 `card_no`)** | | `tp_p_berths` | **`zt_park`** 32,750 | `zt_order_n` 20,554 | 主份多 `car_number_color` | 见下方专段 | | `tp_e_employee_charge` | **`zt_park`** 2,333 | `zt_order_n` 940 | 主份多 7 列 | **✅ 0 行独有** | > **`park_in_out_record`(原 §2.1 的抽样项,现已问死)** > 首版只做了"从副本抽 300 个 `order_id` 回主份查,命中 300/300"。 > 现在改跨库 `LEFT JOIN` 全量:副本 **1,242,863 行,独有 0 行**。 > 副本时间范围 `1970 ~ 2018-03-27`(同库 `td_b_order` 也冻结在 2018-04-08), > 印证它是那一代系统的历史切片。**可直接丢弃。** > **`tp_p_berths`(判定为"陈旧快照",可丢)** > 按**主键 `id`**:副本 20,554 行里 **31 行**的主份没有;但另有 **1,681 行同 `id` > 却指向了不同的 `(pl_no, berth_no)`** —— 即主份把这些车位号**重新分配**了。 > 按**业务键 `(pl_no, berth_no)`**:1,692 行"独有",涉及 54 个 `pl_no`, > 而这 54 个 `pl_no` **全部存在于 `zt_park.parking_lot`**(0 个是陌生停车场)。 > 且 `tp_p_berths` 是**车位实时状态表**(`car_no`/`is_occupy`/`order_id`/`pda_in_time`), > 不是静态资产台账——陈旧快照没有保留价值。 > 交叉验证:`grep -rl tp_p_berths zteits-order/src/main/` = **无引用**,order 代码根本不读它。 > **结论:丢。** #### `parking_lot`(4 库都有,最需要交代的一张) 保留 `zt_park`(3124 行,47 列 = 结构超集)。三份副本按 `pl_no` 的独有情况: | 副本 | 行数 | 按 `pl_no` 独有 | 这些"独有"是什么 | |---|---:|---:|---| | `zt_bill` | 1603 | **1** | `P1101020TH`(白米斜街立体停车场)→ 主份已有,是 `P1101020ZR`(pl_id 12407) | | `zt_order` | 1465 | **81** | 见下 | | `zt_order_n` | 1979 | **2** | `P1101020TH`(同上)、`003`(区域卡虚拟停车场)→ 主份都有同名行,换了号 | `zt_order` 那 81 个独有 `pl_no` 分两类: - **重编号的**:`A150402000 创业东路` → 主份 `P15040225Z`;`A150402001 百货东路` → `P15040225W`;`A150402005 嘉华东路` → `P15040225Y`。老 `A`/`C` 前缀系列是早期编号 - **剩余的**:赤峰市路侧停车(双桥东区/粮市中区/三中街东区…),形如 `P150402189~236` **三份副本都没有代码在读**(这是判定的关键): ``` grep -rl parking_lot zteits-bill/src/main/ → 无引用 grep -rl parking_lot zteits-pay/src/main/ → 只有注释("和 parking_lot 表中 source_type 保持一致") grep -rl parking_lot zteits-order/src/main/ → 1 处:TdBParkDurationRepSmapper.xml:376 的一条维护 UPDATE grep -rl parking_lot zteits-charge/src/main/ → 无引用 ``` 即:`parking_lot` **只有 park 模块在用**;bill/pay/charge 那三份是纯死数据, order 那份只被一条 `update td_b_park_duration_rep pdr, parking_lot pl` 维护语句碰到。 且副本是**结构子集**(缺 `area_type`/`pl_category`/`push_state`/`support_etc` 等列), 所以绝不可能"反向超集"。 **结论:三份副本全丢**;那 84 个独有 `pl_no` **先导出留痕**(见 §5), 不并进主份——并进去等于把已下线/已重编号的老停车场重新塞回前台列表。 另注 `zt_park` 自身有 2 个重复 `pl_no`(`P1101020SE`、`P320211004`,各 2 行, `data_status` 0/1 各一),合并时不阻塞,但**新代码按 `pl_no` 取单条的地方要留意**。 ### 2.2 🔴 主份缺列 / 反向差异(4 组) | 表 | 保留 | 丢弃 | 精确结论 | |---|---|---|---| | `sys_org` | **`zt_sys`** 242 | `zt_park` 20、`zt_order_n` 20、`zt_order` 0 | **✅ 三份都 0 行独有(按 `org_code`)→ 全丢** | | `td_c_cust_person` | **`zt_order`** 104,205(41 列,超集) | `zt_order_n` 28,844(38 列) | **🔴 29 行独有 → 取并集** | | `td_c_coupon_person` | **`zt_order`** 149,789 | `zt_order_n` 5,738 | **✅ 0 行独有 → 丢** | | `td_b_settlement_record` | **`zt_order_n`** 12,610 | `zt_order` 90 | **✅ 0 行独有 → 丢**(首版"建议并集"不必要) | > **`sys_org`**:首版的顾虑是"`zt_order` 版 `short_name` 是 NOT NULL、`zt_sys` 版可空, > 说明有代码按非空语义写"。现在两件事分开看: > ① 数据上三份副本**都没有 `zt_sys` 里不存在的 `org_code`** → 丢副本不丢数据; > ② 保留的是**可空**那一侧,即**约束变松**,不会让谁的写入开始失败。 > 所以是全丢。⚠️ 但 `org_code` 在 `zt_sys` 里**不唯一**,`sys_org` 只有 242 行却有重复 `org_code` > ——这是主份自身的数据质量问题,与合库无关,单独记一笔。 > **`td_c_cust_person`(必须并集)**:29 行独有(按 `cust_id`),其中 **26 行 `data_state=1`(有效)**, > 建于 2017-08 ~ 2019-04,名称如 `排水1`(×8)、`高自办月`(×8)、`队里`(×2)、`文化馆`、`测试`。 > 注意 order 侧**只有 javadoc 注释**提到该表(`OrderChargeInfo.java:37` 的字段说明), > **没有 SQL 读它**;真正读 `td_c_cust_person` 的是 **pay**(`TdBCustBindingSMapper` / > `TdCAcctPayoutSMapper` / `TdCCouponPersonSMapper` / `TdBParkCardCustSMapper`) > 以及保留侧 `zt_order`。也就是说这 29 个客户现在**只有直接查库才能看到**。 > > 处置:**补进保留侧**(29 行,`INSERT ... SELECT`)。理由:① 无损原则; > ② pay 侧那四个 mapper 若绑定了这些 `cust_id`,补进去是**修复**(之前 join 落空); > ③ 代价仅 29 行。⚠️ 这 29 行里有 `测试` 这类疑似测试数据,补进去会出现在客户列表—— > **这一点需要业务方认一下**(见 §5 待确认)。 ### 2.3 🟡 两侧都像配置(3 组) | 表 | 保留 | 丢弃 | 精确结论 | |---|---|---|---| | `td_b_wx_config` | **`zt_order_n`** 27 | `zt_order` 22 | **🔴 按 `appid` 独有 3 行 → 取并集** | | `tb_b_park_income_day` | **`zt_order_n`** 348,424 | `zt_order` 1,774 | **✅ 0 行独有 → 丢**(首版"1,796 行待定性"已定性) | | `td_b_settlement_serial` | 见 §1 | — | 结构差异(多 `settlement_month`)已在 §1 计为结构一致组;丢失侧 0 行 | > **`td_b_wx_config`(这张必须走 `INSERT ... SELECT`,不能手抄)** > 这是**含密钥支付配置表**(`app_secret`/`encoding_aes_key`/`pay_api_secret`), > 所以全程**只比 `appid`、不看任何别的列**。 > - 按 `org_id` 比会得到 18 行"独有"——**假警报**:老库那些行 `org_id` 是 NULL > - 按 `appid` 比:真正的独有只有 **3 行**,全是 2017 年建的、`org_id` 为空: > `wxc70ad87faf3c67b9`(2017-07-21)、`wx099b7a61d9867fe0`(2017-07-26)、 > `wxe98c8ee886986377`(2017-09-11) > - 两侧 `id` 空间不同(`zt_order.id=4` 的 `wx1e9001e5940605b1` 在 `zt_order_n` 是 `id=1`), > 所以**并集要按 `appid` 判重,不能按 `id` 插入**,否则主键冲突 > - 另有一处结构差异:`zt_order` 版**没有 `cert_name` 列**,`zt_order_n` 版有 > > 处置:把那 3 行补进保留侧(`INSERT ... SELECT`,密钥值随 SQL 走、 > 不出现在脚本或日志里)。若不补:`WxPayApiConfigKit` 按 appId 找不到配置会**直接抛异常**, > 而这三个 appId 已 8 年未动、`org_id` 为空,属早期试点。 > **建议补**(代价 3 行,避免支付链路隐患),但同样列入 §5 待确认。 ## 3. 与"跨模块直连"清单的交叉 `legacy-db-actual.md` §4.2 的副本表与 [`table-inventory.md`](./table-inventory.md) §2 的 13 张"跨模块直连表"高度重叠。合库时两份清单合并处理:**副本丢弃 + 跨模块调用改走 `parking-module-api`**,正是项目红线第 7 条(禁止跨模块直接访问数据库)的落地。 ⚠️ 本节新增两条**证据级**发现,都指向"副本其实没在被读": - `parking_lot` 在 `zt_bill`/`zt_order`/`zt_charge` 里**零引用**,`zt_order_n` 只有 1 处维护 UPDATE - `tp_p_berths` 在 `zteits-order` 里**零引用** - `td_c_cust_person` 在 `zteits-order` 里**只有 javadoc 注释**,真正在读的是 pay → 这三张的副本"丢弃"不仅数据安全,而且**不存在调用方需要一起改**, 比首版预估的改动面小。 ## 4. 执行顺序 1. **§1(7 组)**:结构一致且证明 0 独有 → 直接丢副本 2. **§2.1(5 组)**:保留主份、丢副本(`park_in_out_record` 的证明已完成) 3. **§2.2 / §2.3 的 3 处并集**:`td_c_cust_person` +29、`td_b_wx_config` +3 (`td_b_wx_config` 按 `appid` 判重插入)——**这是唯一需要搬数据的动作** 4. **留痕导出**:`parking_lot` 84 个独有 `pl_no` 先导出成文件再丢 > 全程遵循:**老库保留只读**,任何一份在确认无用之前都不删。 > 合并后 `check_subset.py` 应能全部退出 0(3 处并集做完后,"需人工处置"清零)。 ## 5. 精确校验怎么重跑 / 结果记在哪 ```bash cd parking-cloud python3 -I sql/tools/check_subset.py --env 私密/legacy-ro.env --sample 5 --timeout 900 ``` - 退出码 **0** = 所有待丢弃份都已被证明是保留份的子集 - 退出码 **1** = 仍有需人工处置的(当前应剩 **6 组**:`parking_lot`×3、`tp_p_berths`、 `td_c_cust_person`、`td_b_wx_config`)。**这 6 组是"待执行动作",不是"未定"**—— `parking_lot`×3 与 `tp_p_berths` 的判定为"丢"(§2.1 已给证据), 另两组的动作是"并集"(§2.2/§2.3) ### 仍需业务方点头的两处(数据无损,但会改变"看得见的内容") 1. `td_c_cust_person` 补进 29 行客户 —— 其中含 `测试`、`高自办月` 等疑似测试账号, 补进后**会出现在客户列表** 2. `td_b_wx_config` 补进 3 行 2017 年的微信支付配置 —— 补进后这三个 appId **从"查不到配置"变成"查得到"** > 两处都不做也不会丢"业务不可见"的数据(order 侧没有任何 SQL 读它们), > 但都不补就等于**默认删掉**这批历史行。按"先取并集"的保守方向默认补; > 若业务方确认是废弃数据,则改为"只导出留痕、不并库",并在本节记录决定。