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

193 lines
11 KiB
Markdown
Raw Normal View History

# PR #7 合并记录与权限号段修正
> **合并时间**:2026-09-12
> **合并提交**:`4413644`(`merge: 合并 ZSY 的客服 Agent 接入(访客身份、画像候选、转人工工单)—— PR #7`)
> **来源**:`origin/ZSY_develop` = `f68b052` → `qyqy_develop`
> **规模**:90 文件、+7897 / −73
> **门禁**:全部通过(见 §2),数据库侧无迁移、表结构未变
>
> ---
> ## ⚠️ 口径修正(2026-09-14):本文的"权限条数"是**当时快照**,现已增长
>
> 本文正文出现的 **40 条 / 45 条 / 38 条**都是 **2026-09-12 当天**的实测值,不是当前值。
> 现状(直接查 `tools/seed_test_rbac.py` 的 `PERMISSIONS`):
>
> | 口径 | 值 |
> |---|---|
> | **权限号段** | **`9001`–`9065`(共 65 条)** |
> | 区间归属 | `9001-9017` 一期公共;`9018-9034` 客服二期/投顾;`9041-9046` 产品治理与候选审核;`9047-9050` 风控告警;`9051-9056` 推广/NL2SQL/探针;`9057-9059` 投顾客户范围;`9060-9065` 账户与交易看板 |
> | **从未存在** | `9035-9040`、`4041-4046`(后者是 `9041-9046` 的笔误) |
>
> ⚠️ 本文 §4 的号段表到 `9046` 结束是正确的(那正是当时的上界),**但不要据此认为 9046 是终点**。
> 权威出处是 `tools/seed_test_rbac.py` 的 `PERMISSIONS`(该脚本是
> `DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099` 的**重建**语义:
> 没并进种子的权限码,重建一次就没了,表现是"接口突然 403"而无报错线索)。
> 一致性由 `python tools/check_rbac_seed_consistency.py` 及其单测守着。
---
## 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 全部存在。
(⚠️ 2026-09-14:现为 **65 条**,号段 `9001-9065`。)
---
## 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 对齐后的最终实测状态
> ⚠️ **下表是 2026-09-12 当天的实测快照**。权限总数与绑定数此后已随新增权限码变动
> (现共 65 条,见文件头 ⚠️ 块);测试用例数也已增长(`tests/unit tests/contract` 不再只有 1275 项)。
| 项 | 值(2026-09-12) |
|---|---|
| 权限总数 | ~~**45 条**~~ → 现 **65 条** |
| 绑定数 | `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,客服检索工具的必需权限已就位 |