merge-conflict-plan.md 14.2 KB

六库合一:重名表冲突处理表

方案 §4.4 第 1 步要求的交付物。数据来源见 <code>legacy-db-actual.md</code>。

行数分两层,后一层取代前一层:

  1. information_schema.TABLES.TABLE_ROWS 估算(首版;TABLE_ROWS 是采样值,不准)
  2. COUNT(*) 精确值 + 跨库 LEFT JOIN 子集证明 —— 由 <code>tools/check_subset.py</code> 产出,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 的副本表与 <code>table-inventory.md</code> §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. 精确校验怎么重跑 / 结果记在哪

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 读它们), 但都不补就等于默认删掉这批历史行。按"先取并集"的保守方向默认补; 若业务方确认是废弃数据,则改为"只导出留痕、不并库",并在本节记录决定。