chore(scratch): 补账号错位排查探针与修复脚本
两个只读探针 + 一份待确认的修复脚本,接在 2026-09-16 两库合并的留档后面:
- AccountLookup.java:按登录账号 / 手机号在两个库里查同一个实体,并把它同主键的邻居打出来——
两库合并是按主键只插不改,撞车时「同 id 不同实体」就是这么被发现的。
- TenantDataDiff.java:同一归属 id(talent_id / merchant_id)在两个库各有多少行、差在哪些表,
用于判断「某个账号的数据是不是全在」。
- repair-onboardp-talent.sql:只针对 onboardp001 / 19934036931 这组错位的修复语句,尚未执行,
文件里写明事实、依据与验收语句,执行前需备份并按事务跑。
| New file |
| | |
| | | import java.sql.*; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Read-only: where do the merchant login `onboardp001` and the talent `19934036931` live, and what do |
| | | * their neighbours at the same primary keys look like (the two-database merge was insert-only by primary |
| | | * key, so a collision would leave an unrelated row in place). Never prints password hashes. |
| | | */ |
| | | public class AccountLookup { |
| | | private static final String ACCOUNT = "onboardp001"; |
| | | private static final String MOBILE = "19934036931"; |
| | | |
| | | public static void main(String[] args) throws Exception { |
| | | for (String url : args) { |
| | | System.out.println(); |
| | | System.out.println("================ " + url); |
| | | try (Connection connection = DriverManager.getConnection(url, "root", "123456")) { |
| | | System.out.println("server=" + one(connection, "select @@hostname, @@port, database()")); |
| | | query(connection, """ |
| | | select id, merchant_id, account, display_name, role_code, status, deleted, last_login_at, created_at, |
| | | length(password_hash) as pwd_len, left(password_hash, 8) as pwd_head |
| | | from merchant_operator where account like '%""" + ACCOUNT + "%'", "商户登录账号"); |
| | | query(connection, """ |
| | | select id, merchant_name, contact_mobile, merchant_type_code, status, deleted, created_at |
| | | from merchant where id in (630, 631, 632, 633, 634) |
| | | order by id""", "merchant 表 id 630-634(看同 id 是不是同一实体)"); |
| | | query(connection, """ |
| | | select merchant_id, count(*) rows_count, min(id) min_id, max(id) max_id |
| | | from merchant_operator where merchant_id in (630, 631, 632, 633, 634) group by merchant_id |
| | | order by merchant_id""", "这些商户各自有几个登录账号"); |
| | | query(connection, """ |
| | | select id, name, mobile, status, deleted, created_at from talent_user where mobile = '""" + MOBILE + "'", |
| | | "人才账号(按手机号)"); |
| | | query(connection, """ |
| | | select id, name, mobile, status, deleted, created_at from talent_user where id in (444, 445, 446, 447) |
| | | order by id""", "talent_user 表 id 444-447"); |
| | | query(connection, """ |
| | | select id, order_no, order_type, order_status, verify_status, refund_status, pay_amount, merchant_id, talent_id, created_at |
| | | from benefit_order where id in (520, 521, 522, 523, 524) order by id""", "订单 id 520-524"); |
| | | } catch (SQLException exception) { |
| | | System.out.println("查询失败: " + exception.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static String one(Connection connection, String sql) throws SQLException { |
| | | try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(sql)) { |
| | | if (!rows.next()) return ""; |
| | | StringBuilder text = new StringBuilder(); |
| | | for (int index = 1; index <= rows.getMetaData().getColumnCount(); index++) { |
| | | text.append(rows.getString(index)).append(' '); |
| | | } |
| | | return text.toString().trim(); |
| | | } |
| | | } |
| | | |
| | | private static void query(Connection connection, String sql, String title) throws SQLException { |
| | | System.out.println(); |
| | | System.out.println("-- " + title); |
| | | try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(sql)) { |
| | | ResultSetMetaData meta = rows.getMetaData(); |
| | | List<String> header = new ArrayList<>(); |
| | | for (int index = 1; index <= meta.getColumnCount(); index++) header.add(meta.getColumnLabel(index)); |
| | | System.out.println(String.join(" | ", header)); |
| | | int count = 0; |
| | | while (rows.next()) { |
| | | List<String> line = new ArrayList<>(); |
| | | for (int index = 1; index <= meta.getColumnCount(); index++) { |
| | | Object value = rows.getObject(index); |
| | | line.add(value == null ? "NULL" : String.valueOf(value)); |
| | | } |
| | | System.out.println(String.join(" | ", line)); |
| | | count++; |
| | | } |
| | | if (count == 0) System.out.println("(无行)"); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import java.sql.*; |
| | | import java.util.*; |
| | | |
| | | /** |
| | | * Read-only: 同一个归属 id(talent_id / merchant_id)在两个库里各有多少行、差在哪些表。 |
| | | * 用于两库合并后核对「某个账号的数据是不是全在」,以及定位主键撞车影响的表。 |
| | | */ |
| | | public class TenantDataDiff { |
| | | public static void main(String[] args) throws Exception { |
| | | String column = args[0]; |
| | | long id = Long.parseLong(args[1]); |
| | | String left = args[2]; |
| | | String right = args[3]; |
| | | try (Connection a = DriverManager.getConnection(left, "root", "123456"); |
| | | Connection b = DriverManager.getConnection(right, "root", "123456")) { |
| | | System.out.println("=== " + column + " = " + id); |
| | | System.out.println("左 " + one(a, "select concat(@@hostname, '/', database())")); |
| | | System.out.println("右 " + one(b, "select concat(@@hostname, '/', database())")); |
| | | System.out.println(); |
| | | System.out.format("%-46s %8s %8s %8s%n", "表", "左", "右", "差"); |
| | | for (String table : referencingTables(a, column)) { |
| | | if (!exists(b, table)) { |
| | | System.out.format("%-46s %8s %8s %8s%n", table, count(a, table, column, id), "-", "右库无此表"); |
| | | continue; |
| | | } |
| | | long leftCount = count(a, table, column, id); |
| | | long rightCount = count(b, table, column, id); |
| | | if (leftCount == 0 && rightCount == 0) continue; |
| | | System.out.format("%-46s %8d %8d %8d%s%n", table, leftCount, rightCount, leftCount - rightCount, |
| | | leftCount == rightCount ? "" : " <-- 不一致"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static List<String> referencingTables(Connection connection, String column) throws SQLException { |
| | | List<String> tables = new ArrayList<>(); |
| | | try (PreparedStatement statement = connection.prepareStatement( |
| | | "select table_name from information_schema.columns where table_schema = database() and column_name = ? order by table_name")) { |
| | | statement.setString(1, column); |
| | | try (ResultSet rows = statement.executeQuery()) { |
| | | while (rows.next()) tables.add(rows.getString(1)); |
| | | } |
| | | } |
| | | return tables; |
| | | } |
| | | |
| | | private static boolean exists(Connection connection, String table) throws SQLException { |
| | | try (PreparedStatement statement = connection.prepareStatement( |
| | | "select count(*) from information_schema.tables where table_schema = database() and table_name = ?")) { |
| | | statement.setString(1, table); |
| | | try (ResultSet rows = statement.executeQuery()) { |
| | | return rows.next() && rows.getLong(1) > 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static long count(Connection connection, String table, String column, long id) throws SQLException { |
| | | // 列名与表名都来自 information_schema,不是外部输入。 |
| | | try (Statement statement = connection.createStatement(); |
| | | ResultSet rows = statement.executeQuery("select count(*) from `" + table + "` where `" + column + "` = " + id)) { |
| | | return rows.next() ? rows.getLong(1) : 0; |
| | | } |
| | | } |
| | | |
| | | private static String one(Connection connection, String sql) throws SQLException { |
| | | try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(sql)) { |
| | | return rows.next() ? rows.getString(1) : ""; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | -- 修复 2026-09-16 两库合并留下的账号错位(只针对 onboardp001 / 19934036931,勿全表照搬)。 |
| | | -- |
| | | -- 事实(2026-09-17 核对,只读): |
| | | -- 服务器库 talent_user id 446 = 「账号434 / 13540211634」(8-03 的种子账号), |
| | | -- 本机开发库 talent_user id 446 = 「验证用户 / 19934036931」(本人的账号,9-13 注册)。 |
| | | -- 合并规则是「只插不改」,446 在服务器已被占用 → 本人那条**没有搬过去**; |
| | | -- 合并后重新登录(9-16 19:02)由微信登录新建了 id 1031「普通游客」(openid 在它身上)。 |
| | | -- 本人的业务数据(13 笔订单 / 4 笔退款 / 12 条支付流水 / 5 条核销记录)当初按原主键搬过去了, |
| | | -- 全部挂在 talent_id = 446 上——所以数据在,只是账号认不出来。账号434 自己没有任何业务数据 |
| | | -- (它名下最早的订单是 9-14 10:57,全是本人的)。 |
| | | -- |
| | | -- 本脚本选择「把身份还原到数据所在的 446」,原因是改动最小:所有业务行已经指向 446, |
| | | -- 只需处理身份行、人才卡,以及被撞车的 card_id 引用。 |
| | | -- |
| | | -- 执行前请先备份:mysqldump 该库(.scratch/db-merge/README.md 里有 SchemaDump 的用法), |
| | | -- 然后在事务里跑,最后按文件末尾的验收语句核对。 |
| | | |
| | | start transaction; |
| | | |
| | | -- 0) 先看清现状(可选,人工确认再往下) |
| | | select id, name, mobile, status, deleted from talent_user where id in (446, 1031); |
| | | |
| | | -- 1) 撤掉重新登录时新建的那条游客账号:实测只有 1 条会话、0 笔订单、0 张卡。 |
| | | -- openid 是唯一键,必须先从它身上挪走(见第 2 步)。 |
| | | delete from talent_session where talent_id = 1031; |
| | | delete from talent_card where talent_id = 1031; |
| | | delete from talent_user where id = 1031; |
| | | |
| | | -- 2) 把身份还原到 446:openid 从 1031 挪过来(人才端靠 openid 找人登录),手机号与姓名也还原。 |
| | | -- 账号434 是种子账号、无业务数据,其身份由此被本人身份取代。 |
| | | update talent_user |
| | | set openid = 'oWVhp3cvkFLyILiXC4EyP7A3BFwo', |
| | | name = '验证用户', |
| | | mobile = '19934036931', |
| | | status = 1, |
| | | deleted = 0 |
| | | where id = 446; |
| | | |
| | | -- 3) 人才卡:服务器上 446 名下那张是种子账号的遗留(card_no=0434、卡等级=普通游客)。 |
| | | -- 本机本人那张(card 240:SEEDCARD92348450290200 / EXCELLENT / 2026-09-12~2027-09-13) |
| | | -- 因为 id 240 在服务器被别的人才占用,合并时整行没搬过来(服务器 card 240 属于 talent 240)。 |
| | | -- 这里把 446 名下这张的内容换成本人那张;card_no 已确认在服务器未被占用。 |
| | | update talent_card |
| | | set card_no = 'SEEDCARD92348450290200', |
| | | card_type = 'PERSONAL', |
| | | card_level = 'EXCELLENT', |
| | | valid_start = '2026-09-12', |
| | | valid_end = '2027-09-13', |
| | | status = 1, |
| | | deleted = 0 |
| | | where id = 446; |
| | | |
| | | -- 4) 跨实体引用:本人订单原本指向本机 card 240,而服务器上 240 是**别的人才**的卡 |
| | | -- (talent 240 的 0228/卓越人才卡)。不改指的话,本人订单会挂到别人的卡上。 |
| | | update benefit_order set card_id = 446 where talent_id = 446 and card_id = 240; |
| | | update order_refund set card_id = 446 where talent_id = 446 and card_id = 240; |
| | | update verify_record set card_id = 446 where talent_id = 446 and card_id = 240; |
| | | update talent_dynamic_code set card_id = 446 where talent_id = 446 and card_id = 240; |
| | | update order_dynamic_verification_code set card_id = 446 where talent_id = 446 and card_id = 240; |
| | | |
| | | -- 刻意不动的东西:talent_session / talent_dynamic_code / order_dynamic_verification_code 的 |
| | | -- 历史行(本机有 17/121/55 行没搬过去)。这些是登录会话与 60 秒一换的核销码,运行时自动重建, |
| | | -- 补历史没有业务意义,也避免再引入一批 id 撞车。 |
| | | |
| | | commit; |
| | | |
| | | -- 验收(应为:446 是本人身份;订单 13 笔;有效卡是 EXCELLENT): |
| | | -- select id, name, mobile from talent_user where id = 446; |
| | | -- select count(*) from benefit_order where talent_id = 446; |
| | | -- select card_no, card_level, valid_end from talent_card where talent_id = 446 and status = 1 and deleted = 0; |
| | | -- select count(*) from benefit_order where talent_id = 446 and card_id = 240; -- 应为 0 |