PDEPARTS 废弃 + 头像同步修复 + 用户查询回归:递归 CTE 上溯
导读
6/22 暴露的 PDEPARTS 下线问题,今天给出完整修复:废弃所有依赖 PDEPARTS 的解析,改用部门 UUID 沿
sys_organization.parent_id递归上溯查sys_park_organization桥表(MySQL 8 CTE)。一个 commit 改了 8 个文件、+167/-33,把昨天识别出的 4 个 Bug 全部修完。同时顺手把头像同步 bug(setFacePic误写为setFacePicAuditStatus)修了,并补充face_pic='2'历史脏数据的处理策略。
🎧 文章导读
🎵 背景音乐
前言
6/22 链路测试报告里挖出来的 4 个 Bug,今天一次性收尾。核心是 PDEPARTS 替代方案:用递归 CTE 沿部门 UUID 上溯查桥表,这是 MySQL 8 才支持的语法(项目里其他 SQL 都很”老派”,没想到会用到 CTE)。

图1:递归 CTE 上溯
一、核心修复:废弃 PDEPARTS,改用部门 UUID 上溯
方案
1 | WITH RECURSIVE org_chain AS ( |
改动文件
| 文件 | 改动 |
|---|---|
UserSyncServiceImpl.java |
ADD/UPDATE 分支:getCompanyIdFromPDeparts() → findParkByOrgWalkUp(getDepartment());部门列表从 PDEPARTS 数组改为单值构造 |
ParkOrganizationServiceImpl.java |
新增 findParkByOrgWalkUp 实现 |
ParkOrganizationMapper.xml |
新增 CTE 递归 SQL |
IParkOrganizationService.java |
接口定义 |
数据依赖
sys_user_organization.org_id 从公司级改为存用户直接部门 UUID。桥表 sys_park_organization 需要有公司级 org_id → park_id 映射(广蓄/清蓄已补)。
防环机制
AND c.depth < 20 是防环保险——理论上 sys_organization.parent_id 应该是有向无环树,但万一数据脏了(人为乱改 parent_id)会无限递归。20 层足够业务穿透(公司级通常 3-5 层)。
二、回归修复:用户查询接口
PDEPARTS 修复后 sys_user_organization.org_id 语义变更(公司级→部门级),导致两个用户查询接口返回 0 条。
Bug 1:/user/pagingOrgAndPark → listByOrgIdsAndParkId
- 现象:带 orgId+parkId 搜用户返回 0 条
- 根因:SQL 通过
uo.org_id = po.org_idJOIN 桥表,部门级 ≠ 公司级 → JOIN 失效 - 修复(UserMapper.xml):去掉桥表 JOIN,
sys_user_park直连sys_user_organization
Bug 2:/user/page → listByConditionExt
- 现象:带 orgId+parkId 搜用户返回 0 条,只搜 parkId 能搜到 138 条
- 根因:
uo.org_id = #{orgId}精确匹配部门级 ≠ 公司级 - 修复(UserMapperExt.xml):orgId 过滤改用
urp.org_id(sys_user_park 公司级),urp 子查询加MIN(org_id)

图2:组织树修复
三、组织树修复
Bug 3:selectCurParkOrgTree 返回空
- 现象:广蓄园区组织树返回
[] - 根因:递归从
parentId=""起步,UUID 组织树根节点parent_id=NULL,equalsIgnoreCase(null, "")= false → 根进不来 - 修复(OrganizationServiceImpl.java):
loadCurParkTree和loadOrganizationTree的匹配条件兼容 null 和 “” 两种根写法
额外改进
selectOrganizationTree强制delete_flag='0'(原来不过滤)getCompanyList改为两级树(一级公司 + 二级子节点)saveCompanyBind去掉orgIds非空校验

图3:头像同步策略表
四、钉钉头像同步
问题 1:setFacePic 误写为 setFacePicAuditStatus
UserSyncServiceImpl.java 第 129 行 bug:
1 | // 错误代码(bug) |
导致所有钉钉同步用户 face_pic = '2'(这是 audit_status 的”拒绝”状态,不是 face_pic 的值)。
问题 2:Dataphin 返回的 avatar 完全没用
PROFILEPHOTOURL(avatar 字段)包含钉钉头像 URL,但同步代码完全没用这个字段。
修复策略
| 用户类型 | face_pic 现状 | 同步后行为 |
|---|---|---|
| 新用户 | — | 下载钉钉头像 → upload aided OSS → /fs/hikFacePic/... |
老用户(脏值"2") |
"2" |
有头像→/fs/...;无头像→清空 |
| 老用户(空值) | 空/NULL | 不回填(避免批量下载几千个头像占事务) |
老用户(正常/fs/...) |
正常路径 | 不覆盖 |
关键设计
uploadDingAvatar整段 try/catch,Feign 超时 / aided 宕机不影响同步事务shouldBackfillFacePic第一版只匹配脏值"2",空值留给后续离线任务
为什么不同步回填空值
如果对所有空值用户都触发头像下载:
- 15000 用户 × 头像下载(每张 ~2MB)= 30GB 网络流量
- 单个事务内长时间 IO 阻塞
- 失败重试成本高
主动放弃空值回填——这是和 pass_user 字段同步同样的折中思路:高频场景做,低频场景留给后续离线任务。
五、其他发现(未修复,记录备查)
| 问题 | 说明 |
|---|---|
sys_user_organization.id 列长不够 |
varchar(64) → UUID 时代 36+36=72 > 64,生产需 ALTER TABLE sys_user_organization MODIFY COLUMN id varchar(128) |
| 桥表数据 | 广蓄/清蓄需 INSERT UUID 映射(生产 UUID 和 dev 不同,需先查) |
| dev OSS 未配置 | config.enabled: false,需在 bootstrap-dev.yml 加 oss 配置(provider:local, bucketName:Windows 路径) |
face_pic='2' 历史脏数据 |
~160 条(全软删),5 条未删的全是测试账号 |
六、验证结果
- mock 端到端测试:admin 侧 sys_user + sys_user_organization + sys_user_park ✅,through 侧 pass_user dist_sync_status=PENDING ✅
/user/pageMCP 验证:带 orgId+parkId 改前 0 条 → 改后 3 条 ✅/user/pagingOrgAndParkMCP 验证:改前 0 条 → 改后 3 条 ✅selectCurParkOrgTree递归 null 兼容:修复后返回树结构 ✅- Java 8 编译通过 ✅
提交概览
提交: f78fbe0be (8 files, +167 -33)
分支: 618zh
| 类别 | 文件数 | 行数 |
|---|---|---|
| 核心修复(PDEPARTS) | 4 | +98 |
| 回归修复(查询接口) | 2 | +22 |
| 组织树修复 | 1 | +15 |
| 头像同步修复 | 1 | +27 |
| 其他杂项 | 0 | +5 |
经验总结
递归 CTE 的实战价值
MySQL 8 的 WITH RECURSIVE 解决了”沿树结构上溯直到命中条件”这类问题,比 Java 里写递归函数简洁 100 倍:
1 | // Java 递归方案(伪代码) |
1 | -- SQL CTE 方案 |
BaseResultMap 错映射是 MyBatis-Plus 隐形坑
第 2 个回归 bug 的根因:UserParkMapper.xml 的 BaseResultMap 把 org_id 和 park_id 都映射到 parkId 字段。没有配置 mapUnderscoreToCamelCase,裸 resultType 也不行。最终采用显式 AS 别名的方式绕过:
1 | <select id="listActiveByUserIds" resultType="...UserParkDTO"> |
教训:MyBatis-Plus 的 BaseResultMap 自动生成不是万能的,手写 resultType 时必须显式 AS 别名。
主动放弃低频场景是工程纪律
头像回填同步只覆盖”脏值 "2"“和”新用户”,空值留给离线任务:
- 高频场景(新用户入库):立即做
- 低频场景(存量空值回填):数据迁移期单独跑
这与之前 pass_user 字段同步”换部门放弃”的思路一致:折中方案的纪律是分清高频/低频,专注高频场景。
数据迁移期要列详细清单
5 个未修复的发现都是”未来生产要处理”的:
sys_user_organization.id列长不够——生产环境必须 ALTER- 桥表数据(广蓄/清蓄 UUID)——需要从生产 dump 提取
- dev OSS 配置——本地开发才能验证头像链路
face_pic='2'历史脏数据——需要在迁移脚本里清理
这些不能”先上线再说”——必须先列出清单,每条 owner + 截止日期。
关联
- [[2026-06-22 工作记录]]
- [[钉钉同步园区失败-PDEPARTS字段下线/问题分析与修复]]
- [[用户同步园区下发完整链路]]
- [[2026-06-22 钉钉同步真实格式链路测试报告]]