# 业务模块数据权限运行时过滤 — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 `apply_data_scope` 从「系统管理 + 门户工作流 + 请假 Demo」扩展到 CRM、PL、BX、DB 及系统管理辅助接口,使角色 `data_scope` 在业务列表/详情/统计查询中一致生效。 **Architecture:** 在 `app/core/data_scope.py` 增强通用能力与 CRM 负责人字段适配;新增 `assert_row_in_data_scope` 供详情/CUD 校验;各模块 CRUD/Service 列表函数统一增加 `user_info` 参数并在查询末尾调用过滤;API 层传入当前用户上下文。PL/DB 通过 `DbUserContext.to_user_info()` 对齐门户 JWT 字段。公开 token 接口(`/public/pl/*`、`/public/db/*`)不在范围内。 **Tech Stack:** FastAPI · SQLAlchemy · Pydantic v2 · pytest · 现有 `sys_role.data_scope` / `sys_role_data_scope_detail` **依据:** OpenSpec `permission-conventions` · 现状扫描(2026-06-28) --- ## 现状摘要 | 状态 | 模块 | 端点约数 | |------|------|----------| | ✅ 列表已接入 | 系统管理 9 类主列表、门户工作流 5 类列表、请假列表 | ~20 | | ⚠️ 部分接入 | 系统管理辅助下拉/详情、工作流写操作/详情 | ~15 | | ❌ 未接入 | CRM、PL、BX、DB、系统监控 | ~385 | **过滤依据字段(默认):** `create_dept_id`(部门范围)+ `create_by`(仅本人) **CRM 额外字段:** 客户/线索/商机/合同/报价等含 `owner_user_id`,「仅本人/自定义用户」应同时匹配 `owner_user_id`。 --- ## 文件结构(新增/核心修改) | 文件 | 职责 | |------|------| | `backend/app/core/data_scope.py` | 扩展 `apply_data_scope`、新增 `assert_row_in_data_scope`、`build_user_info_from_context` | | `backend/app/core/db_auth.py` | `DbUserContext.to_user_info()` 供 PL/DB 复用 | | `backend/tests/test_data_scope.py` | 数据权限单元测试(新建) | | `backend/app/crud/crm/*.py` | CRM 各域 list/query 接入 | | `backend/app/api/crm*.py` | 传入 `user_info`;详情/CUD 校验 | | `backend/app/services/pl_*.py` / `backend/app/api/pl/*.py` | PL 列表/详情接入 | | `backend/app/services/bx/*.py` / `backend/app/api/bx/*.py` | BX 列表/详情接入 | | `backend/app/api/db/*.py` | DB 列表/详情接入 | | `backend/app/api/admin.py` | 补全辅助接口与详情校验 | | `backend/app/crud/monitor/oper_log.py` | 操作日志列表过滤(可选按 `oper_name`) | --- ## 设计决策(实施前确认) ### D1 — CRM「仅本人」语义 - **规则:** `data_scope=4` 时,CRM 主实体(客户/线索/商机/合同/报价)过滤条件为 `(owner_user_id = 当前用户) OR (create_by = 当前用户)` 子表(跟进记录、产品行等)仍按 `create_by`。 - **实现:** `apply_data_scope(query, model, user_info, db, tenant_no, owner_field="owner_user_id")` 可选参数。 ### D2 — 公海/池规则/产品主数据 | 实体 | 策略 | |------|------| | 公海客户 `list_pool_customers` | 不按 owner 过滤;全员可见公海(仅租户隔离) | | 公海规则 `pool_rule` | 管理配置,`create_by` 过滤 | | 产品/产品分类 | 主数据,建议 `data_scope=1` 角色可见全部;列表仍走 `apply_data_scope` | | 统计接口 | 与对应 list 使用相同 base_query 再聚合 | ### D3 — BX「我的/待办/全部」 | 接口类型 | 策略 | |----------|------| | `/reimburses/my`、`/advances/my` | 在「我的」语义上 **AND** `apply_data_scope`(取交集,防止越权) | | `/pending-approval` | 审批人维度为主,**不**叠加 data_scope(审批可见性独立) | | `/reimburses` 管理列表 | 必须 `apply_data_scope` | ### D4 — PL/DB 用户上下文 扩展 `DbUserContext.to_user_info()`: ```python def to_user_info(self) -> dict: return { "user_id": self.id, "role": self.role, "tenant_no": self.tenant_no, "dept_id": self.dept_id, "region_id": self.dept_id, } ``` API 层:`user_info = user.to_user_info()` 传入 service。 ### D5 — 详情/CUD 校验 列表过滤不足以防 ID 枚举。新增: ```python def assert_row_in_data_scope(db, row, model, user_info, tenant_no, owner_field=None) -> None: """不在范围内则 raise HTTPException(403)""" ``` 所有 `get_by_id` / `update` / `delete` 在业务校验后调用。 --- ## Phase 0 — 基础设施(必须先完成) ### Task 0.1: 扩展 data_scope 核心 **Files:** - Modify: `backend/app/core/data_scope.py` - Create: `backend/tests/test_data_scope.py` - [ ] **Step 1: 编写失败测试** — `tests/test_data_scope.py` ```python import pytest from unittest.mock import MagicMock, patch from app.core.data_scope import apply_data_scope, assert_row_in_data_scope class _FakeModel: create_dept_id = "dept-a" create_by = "user-1" owner_user_id = "user-2" def test_scope4_filters_by_create_by_when_no_owner_field(): query = MagicMock() user_info = {"user_id": "user-1", "role": "admin", "dept_id": "dept-a"} with patch("app.core.data_scope._resolve_user_role_scopes", return_value=[{"data_scope": "4", "details": []}]): apply_data_scope(query, _FakeModel, user_info, MagicMock(), "shanxia2.0") assert query.filter.called def test_super_admin_bypass(): query = MagicMock() result = apply_data_scope(query, _FakeModel, {"role": "super_admin"}, MagicMock(), "shanxia2.0") assert result is query query.filter.assert_not_called() ``` - [ ] **Step 2: 运行测试确认 FAIL** ```bash cd backend && source venv/bin/activate && pytest tests/test_data_scope.py -v ``` - [ ] **Step 3: 实现扩展** 在 `apply_data_scope` 签名增加 `owner_field: str | None = None`;scope=4 且 model 有 owner_field 时: ```python if scope == "4": user_ids.add(user_id) if owner_field and hasattr(model, owner_field): conditions.append(getattr(model, owner_field).in_(user_ids)) elif has_user: conditions.append(model.create_by.in_(user_ids)) ``` 新增 `assert_row_in_data_scope`:复用 scope 解析,对单行做 membership 检查。 - [ ] **Step 4: 测试 PASS** - [ ] **Step 5: Commit** — `feat: extend data_scope for owner field and row assert` --- ### Task 0.2: DbUserContext 对齐 **Files:** - Modify: `backend/app/core/db_auth.py` - [ ] **Step 1:** 新增 `to_user_info()`(见 D4),保留 `to_tenant_user_dict()` 兼容旧调用。 - [ ] **Step 2: Commit** — `feat: DbUserContext.to_user_info for data scope` --- ### Task 0.3: CRUD 接入模板(文档化约定) **约定(后续所有 Task 遵循):** 1. 列表函数增加 `user_info=None` 参数(已有则跳过)。 2. 租户过滤 → 业务条件 → **`if user_info: q = apply_data_scope(q, Model, user_info, db, tenant_no[, owner_field=...])`** → count/paginate。 3. API 路由将 `current_user` / `DbUserContext.to_user_info()` 传入 CRUD。 4. 详情/更新/删除:取 row 后 `assert_row_in_data_scope(...)`。 --- ## Phase 1 — 系统管理补全(⚠️ 部分改造) **预估:** 0.5 人日 ### Task 1.1: 辅助接口 **Files:** - Modify: `backend/app/api/admin.py` - Modify: `backend/app/crud/user.py`(如需) | 接口 | 改造 | |------|------| | `GET /users/options` | 查询后 `apply_data_scope(SysUser, ...)` | | `GET /roles/options` | 已有 tenant;可选 `apply_data_scope(SysRole, ...)` | | `GET /dict-data/options` | `list_dict_data_options` 增加 `user_info` | | `GET /dict-data/all` | 改为走 `list_dict_data` + user_info | - [ ] 逐项改造并手动验证下拉数据量随角色变化。 ### Task 1.2: 详情接口校验 **Files:** - Modify: `backend/app/api/admin.py` - [ ] `GET /users/{id}`: 取 user 后 `assert_row_in_data_scope(db, row, SysUser, user, tenant_no)`,403 替代仅 tenant 检查。 - [ ] **Commit** — `feat: data scope on admin auxiliary and user detail` --- ## Phase 2 — CRM(最高优先级) **预估:** 5–8 人日 · ~228 端点 · 按域分批 PR ### Task 2.1: 客户域(试点) **Files:** - Modify: `backend/app/crud/crm/customer.py` - Modify: `backend/app/crud/crm/customer_pool.py` - Modify: `backend/app/crud/crm/customer_follow.py` - Modify: `backend/app/crud/crm/pool_rule.py` - Modify: `backend/app/api/crm.py`(customer / pool / follow / pool/rule 相关路由) - [ ] `list_customers`: 增加 `user_info`,`owner_field="owner_user_id"`(**排除** `list_pool_customers` 不过 data_scope)。 - [ ] `get_customer` / update / delete: `assert_row_in_data_scope`。 - [ ] API `customer/list`、`customer/detail` 等传入 `_user` 的 user_info。 - [ ] **保留** `customer/mine` 等接口原有 owner 过滤,与 data_scope **AND**。 - [ ] 手工用例:角色 data_scope=4 仅能看到本人负责客户;=2 能看到本部门。 ### Task 2.2: 线索域 **Files:** - Modify: `backend/app/crud/crm/clue.py`, `clue_follow.py`, `clue_statistics.py` - Modify: `backend/app/api/crm.py`(clue 段) - [ ] 同 Task 2.1 模式,`owner_field="owner_user_id"`。 - [ ] 统计 `clue_statistics/*`: base query 先 apply_data_scope 再聚合。 ### Task 2.3: 商机域 **Files:** - Modify: `backend/app/crud/crm/opportunity.py`, `opportunity_follow.py`, `opportunity_product.py` - Modify: `backend/app/api/crm.py`(opportunity 段) ### Task 2.4: 联系人 / 供应商 / 开票 **Files:** - Modify: `backend/app/crud/crm/contact.py`, `supplier_info.py`, `invoice_info.py` - Modify: `backend/app/api/crm.py` - [ ] 联系人按 `customer_id` 关联时:先校验客户 in scope,或 contact 自身 `create_by`。 ### Task 2.5: 产品域 **Files:** - Modify: `backend/app/crud/crm/product.py` - Modify: `backend/app/api/crm.py`(product / product-categories) - [ ] 仅 `create_by` / `create_dept_id`,无 owner_field。 ### Task 2.6: 报价域 **Files:** - Modify: `backend/app/crud/crm/quote.py`, `quote_product.py`, `quote_approval.py`, `quote_share_log.py` - Modify: `backend/app/api/crm_quote_routes.py` - [ ] `owner_field="owner_user_id"`(报价负责人)。 ### Task 2.7: 合同域 **Files:** - Modify: `backend/app/crud/crm/contract.py`, `contract_product.py`, `contract_approval.py` - Modify: `backend/app/api/crm_contract_routes.py`, `crm_contract_sub_routes.py` - [ ] 合同主表 `owner_field="owner_user_id"`;变更/模板/审批子模块按 `create_by`。 - [ ] **Phase 2 验收:** CRM 各 list 在 Postman 用不同 data_scope 角色对比 total;detail 越权 ID 返回 403。 --- ## Phase 3 — 运输计划 PL **预估:** 3–4 人日 · ~60 端点(不含 public) ### Task 3.1: 基础数据 **Files:** - Modify: `backend/app/api/pl/base_data.py`(carrier / vehicle / personnel / driver 列表查询段) - Modify: `backend/app/services/pl_personnel_service.py`(如有独立 query) - [ ] 各 `GET` 列表:`user_info = user.to_user_info()` + `apply_data_scope`。 - [ ] 详情/改/删:`assert_row_in_data_scope`。 ### Task 3.2: 计划 / 运输 / 运费 / 盘存 **Files:** - Modify: `backend/app/api/pl/plan.py` - Modify: `backend/app/api/pl/transport.py` - Modify: `backend/app/api/pl/freight.py` - Modify: `backend/app/services/pl_freight_settlement_service.py` - Modify: `backend/app/services/pl_plan_service.py` - Modify: `backend/app/api/pl/inventory.py` - [ ] 优先改造:`plan` 列表、`freight` 结算单列表、`transport` 列表。 - [ ] 审批/待办类接口:按产品确认是否叠加 scope(默认:列表加,待办不加)。 - [ ] **排除:** `public_site.py`, `public_transport.py`(无 JWT)。 --- ## Phase 4 — 报销 BX **预估:** 2–3 人日 · ~64 端点 ### Task 4.1: 费用项 + 流程配置 **Files:** - Modify: `backend/app/services/bx/expense_item.py`, `bx/workflow.py` - Modify: `backend/app/api/bx/expense_item.py`, `bx/workflow.py` ### Task 4.2: 备用金 + 报销 + 支付 **Files:** - Modify: `backend/app/services/bx/advance.py`, `bx/reimburse.py`, `bx/payment.py` - Modify: `backend/app/api/bx/advance.py`, `bx/reimburse.py`, `bx/payment.py` - [ ] 管理列表 `/advances`、`/reimburses`(非 my):`apply_data_scope`。 - [ ] `/my` 系列:`(create_by = me) AND apply_data_scope`。 - [ ] `/pending-approval`:保持审批人逻辑,不叠加 scope(见 D3)。 ### Task 4.3: 附件 **Files:** - Modify: `backend/app/services/bx/attachment.py`, `backend/app/api/bx/attachment.py` - [ ] 按关联业务单校验 scope(下载/删除时 assert 主单 in scope)。 --- ## Phase 5 — 地磅 DB **预估:** 2 人日 · ~28 端点(不含 public) ### Task 5.1: 出入库 / 计划 / 过磅 / 看板 **Files:** - Modify: `backend/app/api/db/inventory.py` - Modify: `backend/app/api/db/plans.py` - Modify: `backend/app/api/db/weighing.py` - Modify: `backend/app/api/db/dashboard.py` - [ ] 统一 `user.to_user_info()` 传入查询。 - [ ] Dashboard 统计基于已过滤子查询。 - [ ] **排除:** `public_inventory.py`。 --- ## Phase 6 — 系统监控 + 门户工作流补全 **预估:** 1 人日 ### Task 6.1: 操作日志 **Files:** - Modify: `backend/app/crud/monitor/oper_log.py` - Modify: `backend/app/api/monitor.py` - [ ] 若日志表无 `create_dept_id`:按 `oper_user_id` 映射为等效 `create_by` 过滤,或文档声明「监控仅 super_admin」。 ### Task 6.2: 工作流详情/写操作 **Files:** - Modify: `backend/app/api/workflow.py` - Modify: `backend/app/crud/workflow.py` - [ ] `GET /instances/{id}`、`POST /tasks/{id}/approve` 前校验实例/任务关联业务或 `create_by` in scope。 --- ## Phase 7 — 测试与回归 ### Task 7.1: 自动化测试 **Files:** - Create/Expand: `backend/tests/test_data_scope.py` - [ ] 覆盖:scope 1/2/3/4/5、多角色并集、super_admin 放行、CRM owner_field、空 scope 返回空集。 ```bash cd backend && pytest tests/test_data_scope.py -v ``` ### Task 7.2: 手工回归矩阵 | 角色 data_scope | 模块 | 预期 | |-----------------|------|------| | 1 全部 | CRM 客户 list | 租户内全量 | | 4 仅本人 | CRM 客户 list | 仅 owner/create_by=本人 | | 2 本部门 | PL 计划 list | 仅本部门 create_dept_id | | 5 自定义部门 | 系统用户 list | 自定义部门用户 | | super_admin | 任意 | 不过滤 | ### Task 7.3: 字段一致性(可选) ```bash cd backend && python scripts/check_field_consistency.py ``` --- ## 排期建议 | 阶段 | 内容 | 人日 | 依赖 | |------|------|------|------| | Phase 0 | 基础设施 + 测试 | 1 | — | | Phase 1 | 系统管理补全 | 0.5 | Phase 0 | | Phase 2 | CRM 全域 | 5–8 | Phase 0 | | Phase 3 | PL | 3–4 | Phase 0 | | Phase 4 | BX | 2–3 | Phase 0 | | Phase 5 | DB | 2 | Phase 0 | | Phase 6 | 监控 + 工作流补全 | 1 | Phase 0 | | Phase 7 | 测试回归 | 1 | All | | **合计** | | **15–20** | | **推荐 PR 切分(便于评审):** 1. `feat/data-scope-core` — Phase 0 + 1 2. `feat/data-scope-crm-customer-clue` — Phase 2.1–2.2 3. `feat/data-scope-crm-opp-quote-contract` — Phase 2.3–2.7 4. `feat/data-scope-pl` — Phase 3 5. `feat/data-scope-bx` — Phase 4 6. `feat/data-scope-db-monitor-wf` — Phase 5–6 --- ## 风险与缓解 | 风险 | 缓解 | |------|------| | CRM `/mine` 与 data_scope 双重过滤导致结果为空 | 集成测试 + 产品确认:mine = owner 条件 AND scope | | 历史数据 `create_dept_id` 为空 | 迁移脚本补全;空 dept 记录仅 create_by 命中 | | PL/DB service 层多处裸 query | 优先抽 `build_scoped_query(model, user_info)` 减少遗漏 | | 性能(scope=3 递归部门) | 已有 `_get_child_dept_ids`;大数据量时可缓存部门树 | | 公海/审批/待办语义冲突 | 按 D2/D3 写死规则并在 API docstring 注明 | --- ## 不在本次范围 - 小程序端菜单/数据权限(独立鉴权体系) - 公开 token 接口(`/public/pl/*`、`/public/db/*`) - 前端改造(后端过滤即可;前端无需改 unless 403 提示优化) - 新角色类型或新 data_scope 枚举值 --- ## 完成定义(Definition of Done) - [ ] `backend/app/core/data_scope.py` 含 owner_field + assert_row_in_data_scope - [ ] CRM / PL / BX / DB 所有 **列表/统计** 查询均调用 apply_data_scope(公开接口除外) - [ ] 各模块 **详情/更新/删除** 均调用 assert_row_in_data_scope - [ ] 系统管理辅助接口补全 - [ ] `pytest tests/test_data_scope.py` 通过 - [ ] 手工回归矩阵至少覆盖 CRM + PL 各 1 条 - [ ] `./scripts/update-graph.sh backend` 已执行