关系表存 A 系统 ID、查时传 B 系统 ID——静默返回空的隐形杀手
某次上线后,某后台管理系统的「会员标签」功能突然对所有人都显示「暂无标签」。去数据库手查,标签数据分明存在;API 返回 200,body 里是空数组;前端没有报错,只有一片空白。整整排查了两轮,才发现问题出在一个谁都没注意过的地方:关系表里存的是微信 openid,但代码查询时传的是 POS 系统的会员编号。
现象
- UI 显示「暂无标签」,但手动
SELECT * FROM user_tags WHERE openid = 'oD4jk5...'能查到几十条记录 SELECT * FROM user_tags WHERE openid = '901097009'→ 0 行- API 全程 200 OK,没有任何异常日志
- 自动打标任务跑了多次,写入记录持续增加,但前端依然空白
根因
系统由两套子系统拼合而成:一套是微信生态(用 openid 标识用户),另一套是线下 POS/CRM(用会员编号标识用户)。关系表建表时只考虑了微信侧,openid 列存的是形如 oD4jk5CpI7Is1k26dyT1_VvTXbLU 的字符串。
然而,后台管理 API 接收的是从 POS 侧传来的会员编号(如 901097009),代码直接用它查 openid 列:
// 列名叫 openid,变量却是 memberCode——两个完全不同的值域
->where('openid', $memberCode)两套标识符互不相交,查询永远返回 0 行。更隐蔽的是,有些地方还加了字段别名掩盖了不对称:
->field('ft.openid as member_code') // alias 不等于语义等价openid as member_code 只是改了输出列名,不代表这两列存的是同一套 ID。跨系统集成时,列名往往沿用了建表时某一侧的语义,随着业务扩展另一侧的调用方开始复用这张表,悄悄埋下不匹配。
最危险的是:查询不会报错,只会返回空——开发者会先去怀疑「同步没跑到」「数据没写进去」,绕很大一圈才想到键域不匹配。
正解
不要每次查询都做实时 JOIN 翻译,而是在关系表里反范式冗余存储第二个系统的 ID,一劳永逸。
第一步:加列加索引
ALTER TABLE user_tags
ADD COLUMN member_code VARCHAR(50) DEFAULT NULL COMMENT 'POS会员编号(冗余)'
AFTER openid;
ALTER TABLE user_tags ADD INDEX idx_member_code (member_code);第二步:存量数据回填
通过身份映射表(存储 openid ↔ member_code 对应关系)回填:
UPDATE user_tags ut
INNER JOIN identity_mapping im
ON ut.openid = im.openid
AND im.member_code IS NOT NULL
SET ut.member_code = im.member_code
WHERE ut.member_code IS NULL;SSH 执行时的小坑:条件里尽量用
IS NOT NULL而不是!= '',空引号经过多层 shell 转义后可能被静默吃掉,导致部分行漏更新。跑完后建议再验一次:
-- 应返回 0,否则说明有漏网之鱼
SELECT COUNT(*) FROM user_tags ut
INNER JOIN identity_mapping im ON ut.openid = im.openid
AND im.member_code IS NOT NULL
WHERE ut.member_code IS NULL;第三步:修所有查询点
把用错键的地方全部改过来,写入也不能漏:
// 读:改用新列
->where('member_code', $memberCode)
// 写:两个键都要落库
$db->table('user_tags')->insert([
'openid' => $openid, // 保留,兼容旧查询
'member_code' => $memberCode, // 新增,供管理侧查询
'tag_id' => $tagId,
]);第四步:修上游写入服务
所有异步创建关系记录的服务(自动打标、同步引擎等)也要同时写两列,否则增量数据仍然残缺:
function applyTag(string $openid, string $memberCode, int $tagId): void {
$db->name('user_tags')->insert([
'openid' => $openid,
'member_code' => $memberCode, // 必须
'tag_id' => $tagId,
]);
}两套系统共享关系表时,最稳的做法是在表里同时存两个系统的用户标识。读写都命中本系统的列,不依赖运行时 JOIN,也不怕某一侧的 ID 变更(比如微信重新绑定后 openid 变了,但 member_code 不变,标签依然存活)。
如果需要批量给两套系统的用户打标,优先从绑定了两侧 ID 的较小集合迭代,而非全量扫大表。例如:全量 POS 会员 130 万条、已绑定微信的只有 7.5 万条——从 7.5 万侧迭代,每条都有 openid 可用,效率提升约 17 倍,避免 120 万次空查询。
一句话外卖
两套系统共用一张关系表时,先问「这张表里存的是哪个系统的 ID」,再问「调用方传来的是哪个系统的 ID」——不对齐就是定时炸弹,且爆炸时没有任何报错。