9.4 KiB
PR #7 合并记录与权限号段修正
合并时间:2026-09-12 合并提交:
4413644(merge: 合并 ZSY 的客服 Agent 接入(访客身份、画像候选、转人工工单)—— PR #7) 来源:origin/ZSY_develop=f68b052→qyqy_develop规模:90 文件、+7897 / −73 门禁:全部通过(见 §2),数据库侧无迁移、表结构未变
1. 合并内容
ZSY 的客服 Agent 接入线,含三块新能力与一次合规清理:
| 块 | 内容 |
|---|---|
| 访客身份 | VisitorTokenIssuer(/api/v1/auth/visitor-tokens)、security.py 的 visitor:true 分支、auth.py 跳过 IdentityService.resolve() |
| 画像候选(二期) | 候选生成 Worker、用户确认 / 管理员审核四个端点、Milvus + Neo4j 投影适配器、memory_sync_outbox 消费者 |
| 转人工工单 | 工单只读管理端点、转接上下文构造、会话凭据最小化(conversation_privacy.py) |
合规清理(9aaacc2) |
删除生产死代码 knowledge_tool_service.py 与硬编码 Milvus 字段名的 milvus_knowledge_adapter.py(9 文件 +189/−483) |
评审往返见 docs/33(v1 回复)、docs/34(v2 回复)、docs/35(v3 回执核实)。
2. 合并后门禁实测
| 项 | 结果 |
|---|---|
git merge f68b052 --no-ff |
✅ 无冲突 |
ruff check app tests tools |
✅ 干净 |
mypy app |
✅ 244 文件 0 错(基线 228,+16 为新模块) |
python tools/check_authoritative_docs.py |
✅ 49 份文档无撞号 |
pytest tests/unit tests/contract |
✅ 1275 passed, 2 skipped, 0 failed(基线 1207,+68 为新用例) |
alembic current |
✅ 已在 20260911_merge_adv_risk_heads(head),upgrade head 无操作 → 无新迁移 |
python tools/audit_schema.py |
✅ 89 business tables, no missing or unexpected tables |
说明:
f68b052单独一棵树跑文档守卫会报21/22撞车 —— 那是我方基线的问题 (投顾那两份的改名在我方5018f11,不在 ZSY 的基线上)。合并后消失:21/22归 风控与四大 Agent,投顾挪到30/31。不是 ZSY 引入的。
3. 合并时顺手修掉的两处
3.1 tests/unit/core/test_security.py 的密钥路径漂移(2 行)
第 48、108 行原本硬编码 Path("config/jwt/jwt-private.pem"),漏了 dev/:
tools/generate_jwt_keys.py:107的默认--out-dir是config/jwt/dev;app/core/config.py:26-27的默认值也是config/jwt/dev/...;- 同一个文件第 13 行已定义
DEV_KEY_DIR = Path("config/jwt/dev"),注释写着 「路径只在这里定义一次……避免多处硬编码各自漂移」。
后果:在没有 config/jwt/jwt-private.pem 的环境里必红 2 个用例(本机即如此)。
已改为 (DEV_KEY_DIR / "jwt-private.pem"),与第 132 行写法一致。全仓这种写法只此 2 处,
其余 12 处引用都是 config/jwt/dev/。
3.2 权限号段冲突(我方环境的历史遗留,ZSY 不受影响)
问题:库里有一批 9020-9035,是本方 tools/grant_advisor_role.py 用旧号段建的;
而投顾线(bbf623a)后来把 9020-9034(15 个)写进了公共种子 seed_test_rbac.py。
两套 id→code 映射不同:
| id | 种子(投顾线) | 库里(旧 grant_advisor_role.py) |
|---|---|---|
| 9020 | investment-goal:write:self |
asset-allocation:generate:self |
| 9023 | product-recommendation:generate:self |
investment-goal:review |
| 9031 | product-recommendation:review |
product-governance:read |
后果:种子的清理是 DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099,
但角色绑定只清 role_id IN (9001,9002,9003) —— advisor(9004) 的绑定不在清理范围内。
所以跑一次种子会把 9020-9034 换成种子的语义,而 advisor 的绑定仍指向这些 id ⇒
advisor 会拿到错误的权限组合,且不会报错。
处置(已改代码,见下):权限号段整体上移到 9041-9046,与种子的 9001-9034 和
库里的旧数据都不冲突;并把"权限码的定义以种子为准"写进了三个文件的文档字符串。
ZSY 侧不受影响:他的库只有
9001-9018,没有这批旧数据。
4. 代码改动清单
| 文件 | 改动 |
|---|---|
tests/unit/core/test_security.py |
2 行改用 DEV_KEY_DIR(§3.1) |
tools/seed_test_rbac.py |
PERMISSIONS 增 9041-9046(投顾治理类 3 个 + 客服二期 3 个);CUSTOMER_PERMISSIONS 增 9044;文档字符串写清「DELETE 重建」的两个后果 |
tools/grant_advisor_role.py |
ADVISOR_PERMISSIONS 由 16 个收敛为种子里缺的 3 个(9041-9043);文档字符串记录号段冲突与处置顺序 |
tools/grant_customer_service_phase2_permissions.py |
权限 id 9036-9038 → 9044-9046;文档字符串改为「已并入种子,本脚本仅作幂等补齐」 |
新增的 6 个权限码:
| id | 权限码 | 授给 |
|---|---|---|
| 9041 | product-governance:read |
admin(治理类) |
| 9042 | product-governance:review |
admin |
| 9043 | product-governance:sync |
admin |
| 9044 | memory:candidate:confirm |
customer(确认自己的候选)+ admin |
| 9045 | memory:candidate:review |
admin |
| 9046 | handover:read |
admin |
一致性自检(只读):种子 40 条权限、id 唯一、无重复;两个 grant_*.py 的每一条
(id, code) 都与种子逐字一致;CUSTOMER_PERMISSIONS 引用的 id 全部存在。
5. 本机 RBAC 对齐:最终执行记录
分两轮完成。第一轮原计划是"种子 + 三个 grant 脚本",但种子当场跑不通(见 §5.1), 于是先改用不依赖种子的方案把功能补齐;随后按项目方决定修好种子,第二轮完整跑通。
5.1 第一轮撞到的破坏:seed_test_rbac.py 在这台环境上跑不通(已修)
sqlalchemy.exc.IntegrityError: (1451, 'Cannot delete or update a parent row: a foreign key
constraint fails (`jr`.`advisor_profile_tag`, CONSTRAINT `fk_advisor_profile_tag_customer`
FOREIGN KEY (`customer_id`) REFERENCES `sys_user` (`id`))')
[SQL: DELETE FROM sys_user WHERE id IN (9001,9002,9003)]
投顾线引入的 advisor_profile_tag 有 FK 指向 sys_user,而库里有数据引用 9001-9003,
于是种子最后那步 DELETE FROM sys_user 被外键拒绝。因为 commit() 在最后,种子是原子的
(失败即完整回滚,已实测:权限仍 38 条、三个用户密码完好),但任何人在这台环境、
或任何有投顾数据的环境上跑种子都会失败 —— 而且外部表现只是"什么都没发生"。
修法(已落地):把 DELETE FROM sys_user 换成「存在则 UPDATE 非密码字段、不存在才 INSERT」,
且不覆盖 password_hash。附带好处:重跑种子不再弄丢演示密码,也不需要事后补
set_user_password.py —— 实测两轮之后 9001/9002/9003/9020 的 bcrypt 密码都还在。
考虑过但未采用:① 种子完全不碰 sys_user(把用户创建交给 create_test_user.py,代价是
"新建环境多一步");② 给该 FK 加 ON DELETE CASCADE(改已有约束,牵涉规则 4,不做)。
5.2 对齐后的最终实测状态
| 项 | 值 |
|---|---|
| 权限总数 | 45 条 |
| 绑定数 | admin 44 / customer 20 / risk_operator 10 / advisor 10 / operator 1 |
原先缺的 9018 knowledge:query |
✅ 已补(种子重建带入) |
原先缺的 9019 knowledge:manage |
✅ 已补 |
9041-9043 投顾治理类 |
✅ 就位 |
9044-9046 客服二期 |
✅ 就位;实测 memory:candidate:confirm → customer + admin,另两个 → admin |
| 演示密码 | ✅ 四个账号 bcrypt 密码完好,未重设 |
pytest tests/integration/test_auth_login_mysql.py tests/integration/test_rbac_read_mysql.py |
✅ 19 passed |
pytest tests/unit tests/contract |
✅ 1275 passed, 2 skipped |
执行顺序(有依赖,不能颠倒):
清 advisor(9004) 旧绑定 → seed_test_rbac.py → grant_advisor_role.py
→ grant_customer_service_phase2_permissions.py → grant_risk_permissions.py
第一步必须做,原因见 §3.2;后两个 grant_*.py 里风控那 4 个权限是必要的幂等补齐 ——
种子不重建 1986... 号段的风控权限,却会清掉 risk_operator/admin 对它们的绑定(本次实测补了 8 条)。
6. 遗留事项
| 项 | 归属 | 状态 |
|---|---|---|
品牌名(COMPANY 南方科技 → 奶龙基金责任有限公司) |
ZSY 上报项目方 | 走 (b):保持现状,定后单行 revert,须与 customer_service_rules.py:116/129 一起处理 |
| 候选流程缺走 RBAC 的集成用例(服务层测试测不出"权限码不存在") | ZSY | 他认账,承诺补 |
| 只读真机冒烟(用真实令牌打 §19 新增端点,403 当失败) | 待 ZSY 回话 | 已提议 |
| 环境数据不可跨环境 | 全员 | 第 4 次踩坑,已写进种子文档字符串 |
docs/05 §19 端点编号 |
已解决 | A039/A040,全表 62 个编号唯一 |
seed_test_rbac.py 被投顾 FK 挡住 |
✅ 已修 | 改 sys_user 为 UPSERT 且不覆盖密码,见 §5.1;重跑种子不再弄丢演示密码 |
本机缺 9018 knowledge:query / 9019 knowledge:manage |
✅ 已补 | 见 §5.2,客服检索工具的必需权限已就位 |