Files
group_fqcd_jr/docs/36-PR7合并记录与权限号段修正.md
T

9.4 KiB
Raw Blame History

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,客服检索工具的必需权限已就位