Pārlūkot izejas kodu

docs(spec): design OA history department filter

Add confirmed design for initiator-dept tree filter on the web-admin OA history list and export APIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 nedēļas atpakaļ
vecāks
revīzija
3ecd8f4a74

+ 147 - 0
docs/superpowers/specs/2026-10-16-oa-history-dept-filter-design.md

@@ -0,0 +1,147 @@
+# OA 历史记录 · 按部门检索设计
+
+**模块**: OA 审批 · 发起审批 · 历史记录  
+**日期**: 2026-10-16  
+**状态**: 已确认  
+**关联**: `docs/superpowers/specs/2026-08-28-oa-start-history-design.md`  
+**决策记录**:
+- 部门树展示当前租户**全部部门**
+- 选中部门后匹配 **本级 + 全部下级**(`initiator_dept_id IN (self ∪ descendants)`)
+- 匹配字段为实例快照 `initiator_dept_id`,不是发起人当前所属部门
+- 数据权限模型不变:仍仅由 `oa:start:history:all` 决定「本人 / 全员」;部门条件在该范围之上叠加
+- 列表与「导出全部」共用同一部门参数
+
+---
+
+## 1. 背景与目标
+
+【历史记录】页(`web-admin` `#/system-manage/oa-approval/history/:appId`)已有审批状态、审批 ID、发起/完成时间、以及全员权限下的「申请人」筛选。业务需要再按**发起部门**检索。
+
+**成功标准**:
+- 检索区可选择租户内任意部门并清空
+- 选中后列表仅含该部门及其下级发起的审批实例
+- 无全员权限时仍只见本人,部门条件在本人记录内生效
+- 「导出全部」沿用当前检索条件(含部门)
+- 后端强制过滤,前端无法伪造绕过
+
+---
+
+## 2. 范围
+
+### 2.1 在范围内
+
+| 项 | 说明 |
+|----|------|
+| 前端 | `web-admin/src/views/oa-approval/history/index.vue` 检索区「部门」 |
+| API | `GET .../instances/history`、`GET .../instances/history/export` 增加 `initiatorDeptId` |
+| 后端 | `list_instance_history` / `_history_export_query` / `export_instance_history` 按部门树展开过滤 |
+| 测试 | 扩展 `backend/tests/test_oa_instance_history.py` |
+
+### 2.2 非目标
+
+- 不改角色「数据范围」(本部门 / 本部门及以下等)模型
+- 不按当前审批人、抄送人部门过滤
+- 不在表格中新增「部门」列(导出已有「创建人部门」)
+- 不改移动端历史记录页(本期仅 web-admin)
+
+---
+
+## 3. 权限与可见性
+
+| 能力 | 规则 |
+|------|------|
+| 能否打开历史记录页 | 与现网一致(发起审批入口) |
+| 表格行可见范围 | 无 `oa:start:history:all` → 仅 `initiator_user_id = 当前用户`;有权限或 `super_admin` → 本租户该 `appId` 全部 |
+| 「部门」筛选项 | **始终显示**(与「申请人」不同;申请人仍仅全员权限显示) |
+| 部门条件与全员权限 | 先按全员权限收窄,再叠加部门 `IN` 过滤 |
+
+---
+
+## 4. 交互设计
+
+### 4.1 检索区
+
+在现有 `filter-grid` 中增加一项:
+
+- 标签:`部门`
+- 控件:`el-tree-select`,`check-strictly`、`filterable`、`clearable`
+- 数据源:`fetchDeptTree()`(当前租户全部部门)
+- 下拉节点文案:本级名称
+- 选中后输入框:全路径,分隔符 ` / `(复用 `buildTreePathLabelFromFlat` / 页内 path label 惯例)
+- 占位:`请选择部门`
+- 重置:清空 `initiatorDeptId` 与其它条件一并恢复
+
+### 4.2 查询与导出
+
+- 搜索 / 分页 / 导出全部:若已选部门,传 `initiatorDeptId=<deptId>`
+- 导出勾选:仍以勾选 ID 为准;导出参数可附带当前筛选(与现网 `buildExportParams` 行为一致,一并带上部门 ID)
+
+---
+
+## 5. API 设计
+
+### 5.1 新增查询参数
+
+| 参数 | 必填 | 说明 |
+|------|:----:|------|
+| `initiatorDeptId` | 否 | 发起部门 ID;空则不按部门过滤 |
+
+列表与导出两个端点均接收该参数。
+
+### 5.2 过滤语义
+
+1. `initiatorDeptId` 为空或仅空白 → 不加部门条件  
+2. 非空 → 在当前租户 `sys_dept`(`del_flag=0`)上解析 **自身 + 全部子孙** ID 集合  
+3. `OaInstance.initiator_dept_id IN 集合`  
+4. 若选中部门不存在或不属于当前租户 → 结果为空列表(`IN ()` 等价无匹配),不报 400  
+
+复用既有 `_get_child_dept_ids`(`app/core/data_scope.py`)或等价 BFS,避免重复实现。
+
+### 5.3 与现有筛选组合
+
+顺序保持:租户 + `appId` + 全员权限收窄 → 审批 ID / 申请人 / 时间 / 状态 → **部门** → 分页。  
+部门过滤与其它条件为 AND。
+
+---
+
+## 6. 实现要点
+
+### 6.1 后端
+
+- `oa_approval.py`:`list_oa_instance_history`、`export_oa_instance_history` 增加 `initiatorDeptId` Query,传入 engine  
+- `oa_engine.list_instance_history` / `_history_export_query` / `export_instance_history`:增加 `initiator_dept_id` 参数并应用 `IN` 过滤  
+- 不改 `_apply_instance_list_filters` 签名亦可:在 history 专用路径旁直接 filter,减少无关列表接口影响
+
+### 6.2 前端
+
+- `searchForm.initiatorDeptId`  
+- 挂载时加载部门树;`handleSearch` / `handleReset` / `loadList` / `buildExportParams` 透传参数  
+- `fetchOaInstanceHistory` / `fetchOaInstanceHistoryExport` 类型与 params 透传(若 API 封装为透传 Query 则仅页面改即可)
+
+### 6.3 测试
+
+在 `test_oa_instance_history.py` 增加:
+
+- 父部门过滤命中本级 + 子部门实例  
+- 选中子部门不命中仅属父部门的实例  
+- 无全员权限时部门过滤仍限制在本人发起记录内  
+
+需在测试库创建 `SysDept` 表及带 `initiator_dept_id` 的实例行。
+
+---
+
+## 7. 非功能
+
+- 最小改动;不重构历史页布局  
+- UI 对齐 Fresh Mint 筛选区与用户管理页部门树选惯例  
+- 完成后按需跑 `./scripts/update-graph.sh backend`(及若前端有显著符号变更则 `frontend`)
+
+---
+
+## 8. 验收清单
+
+1. 打开某流程历史记录,检索区可见「部门」,树含租户全部部门  
+2. 选中父部门 → 列表含本级与下级发起记录;清空 → 恢复原范围  
+3. 无 `oa:start:history:all` 账号:部门条件只影响本人可见行;仍不能看到他人  
+4. 「导出全部」结果与当前含部门条件的列表一致  
+5. 相关单测通过