feat(permission): replace OA roles with post-bound roles and assignee posts

Unify effective roles via post-role bindings, remove dedicated OA role UI, and update OA assignee resolution plus related SQL/docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-04 17:41:04 +08:00
1 parent a3c73d76bb
commit 06db3bc966
66 files changed
+55337 -38647

No files matched your search

File diff suppressed because it is too large. Load diff
+10
View File
@@ -0,0 +1,10 @@
-- docs/sql/drop_oa_role.sql
-- 下线 OA 工作流角色:审批人选改为职位(sys_post / sys_user_post)
-- 同步软删菜单「工作流角色」
UPDATE `sys_menu`
SET `del_flag` = 1, `update_time` = NOW()
WHERE `id` = 'OA000000000000000000000000000102' AND `del_flag` = 0;
DROP TABLE IF EXISTS `oa_role_user`;
DROP TABLE IF EXISTS `oa_role`;
+19
View File
@@ -0,0 +1,19 @@
-- docs/sql/sys_post_role.sql
-- 职位-角色关联:职位可绑定多个角色,用户经职位继承角色权限(与直接角色取并集)
CREATE TABLE IF NOT EXISTS `sys_post_role` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`post_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '职位ID',
`role_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色ID',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_spr_post` (`tenant_no`,`post_id`,`del_flag`) USING BTREE,
KEY `idx_spr_role` (`tenant_no`,`role_id`,`del_flag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职位-角色关联';
@@ -0,0 +1,36 @@
# OA 审批应用复制 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:** 在审批管理页支持一键复制审批应用(含表单/流程草稿),名称为原名+「复制」。
**Architecture:** 后端 `POST /apps/{id}/copy` 原子复制 `oa_app` + 一份草稿版本;前端「更多」菜单调用并刷新列表。
**Tech Stack:** FastAPI + SQLAlchemy;Vue 3 + Element Plus
**Spec:** `docs/superpowers/specs/2026-07-31-oa-app-copy-design.md`
---
### Task 1: 后端 copy_app CRUD + API
**Files:**
- Modify: `backend/app/crud/oa_approval.py`
- Modify: `backend/app/api/oa_approval.py`
- [ ] **Step 1:** 在 `oa_approval.py` CRUD 增加 `copy_app`:查源 app → 选草稿或最新发布 → `create_app` 等价新建(名称加「复制」并截断)→ 用源 JSON 写入 draft 版本(覆盖 create 时空草稿)。
- [ ] **Step 2:** 增加路由 `POST /apps/{app_id}/copy`,返回 `_app_to_dict(..., has_draft=True)`。
### Task 2: 前端 API + 菜单
**Files:**
- Modify: `web-admin/src/api/oaApproval.ts`
- Modify: `web-admin/src/views/oa-approval/apps/index.vue`
- [ ] **Step 1:** 增加 `fetchCopyOaApp`
- [ ] **Step 2:** 「更多」增加「复制」;`handleCopy` 调用后 `loadData`
### Task 3: 验证
- [ ] 手工:复制有草稿 / 仅已发布 / 空应用;名称与未发布状态正确
- [ ] 更新 graph:`./scripts/update-graph.sh all`(或 backend + frontend)
@@ -0,0 +1,220 @@
# 职位关联角色(权限并集)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:** 职位可关联角色;用户权限 = 直接角色 ∪ 职位继承角色(菜单 + 数据权限);职位管理可配置角色,用户管理可多选职位。
**Architecture:** 新增 `sys_post_role`;抽 `get_effective_role_ids` 供 `web_auth_roles` 与 `data_scope` 共用;API 对齐现有 `GET/PUT /posts/{id}/users`;前端职位页「角色配置」、用户页补 `postIds`。
**Tech Stack:** FastAPI + SQLAlchemy + MySQL;Vue 3 + Element Plus;OpenSpec permission-conventions。
**Spec:** `docs/superpowers/specs/2026-08-02-post-role-permission-design.md`
---
## File map
| 文件 | 职责 |
|------|------|
| `docs/sql/sys_post_role.sql` | 建表 |
| `backend/app/models/models.py` | `SysPostRole` |
| `backend/app/crud/post.py` | 职位角色 list/sync;软删职位时软删关联 |
| `backend/app/services/web_auth_roles.py` | `get_effective_role_ids` + 改造菜单/角色名 |
| `backend/app/core/data_scope.py` | `_resolve_user_role_scopes` 用有效角色 |
| `backend/app/api/admin.py` | `GET/PUT /posts/{post_id}/roles` |
| `backend/tests/test_effective_roles.py` | 有效角色解析单测(可 mock Session 或轻量集成) |
| `web-admin/src/api/admin.ts` | `fetchPostBoundRoleIds` / `fetchUpdatePostBoundRoles` |
| `web-admin/src/views/system/post/index.vue` | 「角色配置」弹窗 |
| `web-admin/src/views/system/user/index.vue` | 职位多选 + 查看展示 |
| `docs/中科兴皖需求V0.3.md` §2.1.8 | 需求补充 |
| `openspec/.../permission-conventions/spec.md` | 并集语义扩展 |
---
### Task 1: SQL + Model
**Files:**
- Create: `docs/sql/sys_post_role.sql`
- Modify: `backend/app/models/models.py`(在 `SysUserPost` 后插入 `SysPostRole`)
- [ ] **Step 1: 写增量 SQL**
```sql
-- docs/sql/sys_post_role.sql
CREATE TABLE IF NOT EXISTS `sys_post_role` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`post_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '职位ID',
`role_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色ID',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_spr_post` (`tenant_no`,`post_id`,`del_flag`) USING BTREE,
KEY `idx_spr_role` (`tenant_no`,`role_id`,`del_flag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职位-角色关联';
```
- [ ] **Step 2: 增加 SQLAlchemy 模型**(字段与 `SysUserPost` 同风格,表名 `sys_post_role`,字段 `post_id`/`role_id`)
- [ ] **Step 3: 本地执行 SQL**(对当前开发库)
```bash
# 使用项目既有 DB 连接方式执行 docs/sql/sys_post_role.sql
```
---
### Task 2: 职位角色 CRUD
**Files:**
- Modify: `backend/app/crud/post.py`
- Modify: `backend/app/api/admin.py`(紧挨 posts users 路由之后)
- [ ] **Step 1: 在 `post.py` 增加**
```python
def list_role_ids_for_post(db, post_id: str, tenant_no: str) -> List[str]:
...
def sync_post_role_bindings(db, post_id, tenant_no, role_ids, updater_id) -> None:
"""全量替换:目标外软删,目标内恢复或新建(对齐 user 侧 set_user_* 模式)。"""
...
```
校验:每个 `role_id` 存在、同租户、`del_flag=0`(可选要求 `status=='1'` 才允许绑定——推荐允许绑停用角色但运行时不算有效,与用户绑停用角色一致即可)。
- [ ] **Step 2: `soft_delete_post` 时同步软删该职位下 `SysPostRole`(`del_flag=0` → 1)**
- [ ] **Step 3: API**
```python
class PostRolesUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
role_ids: List[str] = Field(default_factory=list, alias="roleIds")
@router.get("/posts/{post_id}/roles")
# require_menu_code("system:post:list") → {"roleIds": [...]}
@router.put("/posts/{post_id}/roles")
# require_admin_or_super_admin → sync_post_role_bindings
```
---
### Task 3: 有效角色解析
**Files:**
- Modify: `backend/app/services/web_auth_roles.py`
- Modify: `backend/app/core/data_scope.py`
- Create: `backend/tests/test_effective_roles.py`(纯函数级:若难起库,可用 SQLAlchemy 内存或对查询逻辑做小型集成;最低限度用注释说明手动验收场景)
- [ ] **Step 1: 实现 `get_effective_role_ids(db, user_id) -> list[str]`**
逻辑:
1. 直接:`SysUserRole` ⋈ `SysRole`(role status=1, del_flag=0)
2. 职位:`SysUserPost` ⋈ `SysPost`(status=1) ⋈ `SysPostRole` ⋈ `SysRole`(status=1)
3. 去重保序
- [ ] **Step 2: 改造 `_active_role_name_rows` / `iter_user_menu_perm_codes`**
用有效 role_id 查 `SysRole.role_name` / `SysRoleMenu`→`SysMenu`,不再仅 join `SysUserRole`。
- [ ] **Step 3: 改造 `data_scope._resolve_user_role_scopes`**
`from app.services.web_auth_roles import get_effective_role_ids`,按 ids 查 `SysRole` 与 detail。
- [ ] **Step 4: grep 防漏**
```bash
rg -n "SysUserRole" backend/app --glob '*.py'
```
凡「解析当前用户权限」路径必须改为有效角色;配置管理类(给角色绑用户等)保持只动 `sys_user_role`。
- [ ] **Step 5: 跑测试**
```bash
cd backend && source venv/bin/activate && pytest tests/test_effective_roles.py tests/test_api_crypto.py -v
```
---
### Task 4: 前端 API + 职位「角色配置」
**Files:**
- Modify: `web-admin/src/api/admin.ts`
- Modify: `web-admin/src/views/system/post/index.vue`
- [ ] **Step 1: API**
```ts
export function fetchPostBoundRoleIds(postId: string) {
return request.get<{ roleIds: string[] }>({ url: `/web/admin/posts/${postId}/roles` })
}
export function fetchUpdatePostBoundRoles(postId: string, roleIds: string[]) {
return request.put({
url: `/web/admin/posts/${postId}/roles`,
params: { roleIds },
showSuccessMessage: true
})
}
```
- [ ] **Step 2: 职位页**
操作列在「关联用户」旁加「角色配置」;弹窗用 `el-select` multiple + `fetchRoleList({ pageSize: 100 })` 加载选项;打开时 `fetchPostBoundRoleIds`;保存 `fetchUpdatePostBoundRoles`。
查看详情可增加「已关联角色」字段(可选)。
按钮须带 `el-icon`(如 `Key` / `Setting`)。
---
### Task 5: 用户管理职位多选
**Files:**
- Modify: `web-admin/src/views/system/user/index.vue`
- [ ] **Step 1:** `formData` 增加 `postIds: string[]`;表单项「职位」多选(`fetchPostList`);详情回填 `detail.postIds`;submit 带 `postIds`;查看 sections 展示职位名(可用 `postOptions` 映射)。
- [ ] **Step 2:** 确认后端 create/update 已传 `postIds`(`admin.py` UserCreate/UpdateBody 已有)。
---
### Task 6: 文档与图谱
**Files:**
- Modify: `docs/中科兴皖需求V0.3.md` §2.1.8
- Modify: `openspec/changes/zkxw-engineering-standards/specs/permission-conventions/spec.md`
- Run: `./scripts/update-graph.sh backend` 与 `frontend`(若改了前端)
- [ ] **Step 1:** 需求补充:职位可关联角色;用户菜单/数据权限 = 直接角色 ∪ 职位角色。
- [ ] **Step 2:** OpenSpec 在「多角色权限合并」下增加职位继承 Scenario。
- [ ] **Step 3:** 更新 graphify。
---
### Task 7: 手工验收清单
- [ ] 仅职位绑角色、无直接角色 → 侧栏有菜单、非 guest
- [ ] 直接角色 + 职位角色 → 菜单并集
- [ ] 停用职位 → 继承失效
- [ ] 用户改职位不影响直接角色;职位改角色不影响用户直接角色列表
- [ ] OA 职位控件仍可用
---
## Spec coverage check
| Spec 项 | Task |
|---------|------|
| `sys_post_role` | T1 |
| GET/PUT roles API | T2 |
| `get_effective_role_ids` + web_auth + data_scope | T3 |
| 职位角色配置 UI | T4 |
| 用户 postIds UI | T5 |
| 文档/OpenSpec | T6 |
| 停用语义 / 并集 | T3 + T7 |
| 非目标 OA | 不改 |
**Commits:** 按用户要求再提交;实现过程默认不自动 commit。
@@ -0,0 +1,16 @@
# OA 审批人:工作流角色 → 职位 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.
**Goal:** 流程审批/抄送用职位成员替代工作流角色;彻底删除 OA 角色模块并 DROP 表。
**Architecture:** `assigneeType: "posts"` + `postIds` → `sys_user_post` 展开用户;不碰 `sys_post_role`。
**Spec:** `docs/superpowers/specs/2026-08-04-oa-assignee-posts-replace-roles-design.md`
**Status:** 已完成(2026-08-04)
- [x] Task 1: 后端解析与发布校验
- [x] Task 2: 前端流程设计器
- [x] Task 3: 删除工作流角色模块 + drop SQL
- [x] Task 4: 验收(`pytest` 35 passed)
@@ -0,0 +1,62 @@
# OA 审批应用「复制」设计
| 项 | 内容 |
|----|------|
| 状态 | 已定稿 |
| 日期 | 2026-07-31 |
| 范围 | 审批管理页复制应用(基础信息 + 表单/流程/高级设置草稿) |
| 页面 | `/#/system-manage/oa-approval/apps` |
---
## 1. 目标与非目标
**目标**
- 在审批管理卡片「更多」中提供「复制」。
- 一键生成与源流程内容一致的新应用;名称 = 原名称 +「复制」。
- 内容来源:优先当前草稿;无草稿则用最新已发布版;皆无则空草稿。
- 副本始终为**未发布草稿**;复制成功后留在列表并刷新。
**非目标**
- 不自动发布副本。
- 不复制历史版本、实例、任务。
- 不跳转设计器。
---
## 2. 行为(SHALL)
1. 入口:卡片「更多」→「复制」(位于「编辑基础」与「删除」之间)。
2. 点击后直接执行(无二次确认);成功 Toast;刷新列表。
3. 新名称:`{appName}复制`。若超过 50 字:保留后缀「复制」,前缀截断使总长 ≤ 50。
4. 复制字段:
- 应用:`groupId`、`iconUrl`、`description`、`initiatorScope`、`initiatorUserIds`、`adminScope`、`adminUserIds`、`status`、`orderNum`、`remark`
- 版本草稿:`formSchemaJson`、`flowTreeJson`、`advancedSettingsJson`(深拷贝 JSON)
5. 新应用仅有 `version_no=1` 的 `draft`;`is_latest_published=0`;列表无已发布版本标签。
6. 租户隔离与创建应用权限一致(admin / super_admin)。
---
## 3. API
`POST /web/admin/oa-approval/apps/{app_id}/copy`
- 成功:返回新应用字典(与 create 一致),message「复制成功」。
- 源不存在:400「应用不存在」。
---
## 4. 前端
- `fetchCopyOaApp(id)` in `web-admin/src/api/oaApproval.ts`
- `apps/index.vue`:下拉项 + `handleCopy`;图标可用 `CopyDocument`
---
## 5. 自检
- [x] 无未决占位符
- [x] 与口头确认一致(草稿优先 / 未发布 / 留列表)
- [x] 范围仅应用复制,不涉及实例复制
@@ -0,0 +1,146 @@
# 职位纳入权限载体(职位关联角色 · 并集模型)
**日期:** 2026-08-02
**范围:** `sys_post` / `sys_post_role` / 用户有效角色解析 / 职位管理「角色配置」/ 用户管理职位多选
**方案:** 运行时并集(方案 1),不物化复制到 `sys_user_role`
## 已确认决策
| 项 | 结论 |
|----|------|
| 权限来源 | **并集**:用户直接角色 ∪ 职位继承角色 |
| 继承内容 | **菜单权限 + 数据权限**(与现有多角色并集语义一致) |
| 配置入口 | **职位管理**增加「角色配置」;用户管理补上职位多选 |
| 非目标 | 角色页反向关联职位;权限来源可视化;OA `OaRole` / 审批人解析;职位自带菜单树 |
## 背景与现状
- **角色**(`sys_role`)是权限载体:`sys_role_menu`(菜单)、`data_scope` + `sys_role_data_scope_detail`(数据范围)。
- **用户**通过 `sys_user_role` 绑定角色;登录后 `iter_user_menu_perm_codes`、`apply_data_scope`(`_resolve_user_role_scopes`)仅查直接角色。
- **职位**(`sys_post`)仅有 `sys_user_post` 关联;不参与 RBAC。OA 表单「职位」控件另用,本设计不改动。
- 用户 API 已支持 `postIds`,用户管理前端未暴露职位多选。
## 目标模型
```
用户 ──sys_user_role──────────────▶ 角色 ──▶ 菜单 / 数据权限
│ ▲
└──sys_user_post──▶ 职位 ──sys_post_role──┘
有效角色 = 启用直接角色 ∪(启用职位所关联的启用角色)
有效菜单码 / 数据范围 = 对有效角色取并集(与现规范一致)
```
## 数据模型
### 新表 `sys_post_role`
对齐 `sys_user_role` / `sys_user_post` 结构:
| 字段 | 说明 |
|------|------|
| `id` | VARCHAR(64) UUID PK |
| `post_id` | 职位 ID |
| `role_id` | 角色 ID |
| 公共字段 | `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` |
- 软删除:`del_flag=1`,查询默认排除。
- 业务唯一:同一租户下同一 `(post_id, role_id)` 仅一条有效关联(写入时软删旧行再插,或应用层去重;与现有 `sys_user_role` 替换策略一致)。
- SQL 增量:`docs/sql/sys_post_role.sql`;模型:`SysPostRole`(`backend/app/models/models.py`)。
既有表不变:`sys_post`、`sys_user_post`、`sys_user_role`、`sys_role*`。
## 后端权限解析
### 统一入口
在 `backend/app/services/web_auth_roles.py`(或抽出共享 helper,供 data_scope 复用)提供:
1. **`get_effective_role_ids(db, user_id) -> List[str]`**
- 来源 A:`sys_user_role` → `sys_role`(`del_flag=0`, `status='1'`, 租户一致)
- 来源 B:`sys_user_post` → `sys_post`(`del_flag=0`, `status='1'`)→ `sys_post_role` → `sys_role`(同上)
- 去重后返回 role id 列表。
2. 基于有效角色 ID 改造:
- `user_has_active_roles`:有效角色非空即为真(仅职位带角色、无直接角色也通过登录门禁)。
- `get_user_role_names` / `_active_role_name_rows` / `resolve_web_role_for_user`:按有效角色名称映射 `admin` / `super_admin`。
- `iter_user_menu_perm_codes`:`SysRoleMenu` 的 `role_id` 属于有效角色集合(可用 `role_id IN (...)` 或等价 join,避免只 join `SysUserRole`)。
3. **`data_scope._resolve_user_role_scopes`**:角色查询改为基于有效角色 ID,不再仅 join `SysUserRole`。
### 停用语义(显式)
| 状态 | 是否参与并集 |
|------|----------------|
| 职位 `status != '1'` 或已删 | 否(其下角色关联全部不计入) |
| 角色 `status != '1'` 或已删 | 否 |
| `sys_post_role` / `sys_user_post` / `sys_user_role` `del_flag=1` | 否 |
### API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/web/admin/posts/{postId}/roles` | 返回该职位已关联 `roleIds`(及可选角色摘要) |
| PUT | `/web/admin/posts/{postId}/roles` | body:`{ roleIds: string[] }`,全量替换关联(对齐 `PUT .../users`) |
- 权限/租户:与现有 posts CRUD 一致(当前用户租户,不信任客户端改租户)。
- 用户创建/更新:继续使用已有 `postIds`;确保 `_replace_user_posts` 与角色替换互不影响(已有行为保留)。
- 删除职位:现有软删职位逻辑下,有效角色解析因职位 `del_flag`/`status` 自动失效;可选同步软删 `sys_post_role`(与删职位时处理 `sys_user_post` 的方式对齐即可)。
路由注册:沿用 `admin` 路由模块,无需新 router 文件。
## 前端
### 职位管理 `web-admin/src/views/system/post/index.vue`
- 操作列增加 **「角色配置」**(与现有「关联用户」并列)。
- 弹窗:角色多选列表(`el-select` multiple 或勾选表格),加载全部启用角色 + 当前 `roleIds`,确定调用 `PUT .../roles`。
- UI 对齐 Fresh Mint / 现有职位用户配置弹窗;按钮带图标。
### 用户管理 `web-admin/src/views/system/user/index.vue`
- 新增/编辑表单:在「角色」旁增加 **「职位」** 多选(`postIds`),数据源 `fetchPostList`(或现有 posts API)。
- 提交时带上 `postIds`;详情回填;查看弹窗展示职位名称。
- 不移除用户直接绑角色。
### API 模块
- `web-admin/src/api/` 增加 `fetchPostRoleIds` / `fetchUpdatePostRoles`(命名与 posts users API 对称)。
### 列表展示(可选但建议)
- 职位列表可增加「关联角色数」或角色名摘要列(若实现成本低);非必须。
- 首期不做「有效权限预览」。
## 文档与规范同步
实现时同步更新(最小必要):
1. `docs/sql/sys_post_role.sql`(及视情况合并说明到基线 SQL 的维护方式与仓库惯例一致)。
2. `docs/中科兴皖需求V0.3.md` §2.1.8:补充职位可关联角色;用户权限 = 直接角色 ∪ 职位角色。
3. OpenSpec `permission-conventions`:多角色并集扩展为「直接角色 ∪ 经职位继承的角色」;菜单与数据权限均适用。
## 测试要点
1. 用户仅绑职位、职位绑角色 A → 拥有角色 A 的菜单码与数据范围;可登录(非 guest)。
2. 用户直接角色 B + 职位角色 A → 菜单/数据为 A∪B。
3. 停用职位或角色后,对应继承立即失效。
4. 职位角色配置全量替换;用户改 `postIds` 不影响 `roleIds`,反之亦然。
5. `super_admin` 映射:若职位关联的角色名命中超级管理员规则,JWT/web role 应为 `super_admin`。
6. OA 职位控件与审批流程不受影响。
## 风险与注意
- **接入点遗漏**:任何仍只 join `SysUserRole` 的权限路径都要改为有效角色;以 `web_auth_roles` + `data_scope._resolve_user_role_scopes` 为必改清单,实现前再 grep `SysUserRole` 防漏。
- **性能**:有效角色 ID 一次查出后复用;列表页高频 `apply_data_scope` 保持与现网同级查询次数(一次角色查询 + 明细)。
- **与 OA 角色混淆**:本设计仅扩展 `sys_role`;不触及 `oa_role` / `oa_role_user`。
## 实现顺序(建议)
1. SQL + Model `SysPostRole`
2. CRUD/API:`GET/PUT posts/{id}/roles`
3. `get_effective_role_ids` + 改造 `web_auth_roles` / `data_scope`
4. 前端职位「角色配置」
5. 前端用户「职位」多选
6. 文档 / OpenSpec 同步
7. `./scripts/update-graph.sh backend`(及改前端时 frontend)
@@ -0,0 +1,122 @@
# OA 审批人选:工作流角色 → 职位(硬切换)
**日期:** 2026-08-04
**范围:** 去掉 `oa_role` / `oa_role_user` 与【工作流角色】模块;流程审批人/抄送改为选【职位】,运行时按 `sys_user_post` 展开成员。
**方案:** 方案 1 — `assigneeType: "posts"` + `postIds`
## 已确认决策
| 项 | 结论 |
|----|------|
| 历史流程 | 系统未上线,无历史 `flow_tree` → **硬切换**,不做兼容层 |
| 预置 GM/CHAIRMAN | **不做**;设计器普通多选启用职位 |
| 下线范围 | **彻底去掉**:菜单/路由/管理页/API/`fetchOaRole*` |
| 数据表 | **一并 DROP** `oa_role_user`、`oa_role`(增量 SQL) |
| 审批人数据源 | **仅** `sys_user_post`(职位成员);**禁止**用 `sys_post_role` / `sys_role` |
## 目标模型
```
流程节点 approverConfig
assigneeType / assigneeTypes: "posts"
postIds: [sys_post.id, ...]
│
▼
sys_user_post(启用职位 + 启用用户)
│
▼
待办 / 抄送人 userId 列表
```
与权限链分离:
```
职位 → sys_post_role → sys_role → 菜单/数据权限 (登录 RBAC,本需求不改)
职位 → sys_user_post → 用户 (本需求:审批人选)
```
## Flow JSON(新约定)
审批节点示例:
```json
{
"assigneeType": "posts",
"postIds": ["<sys_post.id>"],
"signMode": "or"
}
```
抄送节点:`assigneeTypes` 可含 `"posts"`(与 `users` / `dept_leader` 等组合),成员来自同一套 `postIds`(或按现抄送结构对每种类型分字段;实现时与现 `oa_roles` 写法一一对应替换)。
**删除字段/枚举:** `oa_roles`、`oaRoleIds`、`oaRoleCodes`、`oa_role_ids`、`oa_role_codes`。
## 后端
### 新增/改造
| 位置 | 变更 |
|------|------|
| `crud/user.py` 或 `crud/post.py` | `list_user_ids_bound_to_posts(db, post_ids, tenant_no)`:多职位展开用户,过滤职位 `status=1`/`del_flag=0`、用户启用 |
| `oa_assignee.py` | `assigneeType == "posts"` 分支;`resolve_source: "posts"`;删除 `oa_roles` 分支 |
| `oa_publish.py` | 校验 `posts` 时 `postIds` 非空;引用校验查 `SysPost`;删除 `validate_oa_roles_referenced` |
| `oa_engine.py` | 展示标签改职位名称映射;删除 `OaRole` 查询 |
| `oa_approval.py` API | 删除整个工作流角色路由段 |
| `crud/oa_approval.py` | 删除 `PRESET_ROLES` / `ensure_presets` / 角色 CRUD / `list_oa_role_user_ids` |
| `models.py` | 删除 `OaRole`、`OaRoleUser`(与 drop SQL 同步) |
### SQL
创建 `docs/sql/drop_oa_role.sql`:
```sql
DROP TABLE IF EXISTS `oa_role_user`;
DROP TABLE IF EXISTS `oa_role`;
```
菜单:软删或删除「工作流角色」菜单项(`oa:roles:list`),与项目菜单维护方式一致。
### 错误语义
- 发布:未选职位 / 职位不存在或停用 → 明确错误
- 运行:职位下无有效用户 → 与现「工作流角色成员为空」同等失败(`AssigneeResolveError`)
## 前端
| 位置 | 变更 |
|------|------|
| `FlowDesigner.vue` | 选项「工作流角色」→「职位」;`fetchPostList` 加载(仅 `status==='1'`);`postIds` 多选 |
| `flow-tree/types.ts` | `AssigneeType` 含 `posts`;去掉 `oa_roles`;`summarizeApprover` 文案「职位」 |
| `OaProgressChain.vue` | `posts` →「职位」 |
| 删除 | `views/oa-approval/roles/index.vue`;路由 `oa-approval/roles`;`oaApproval.ts` 中全部 `fetchOaRole*` |
| 保留 | `OaPostSelect.vue`(表单控件,与审批人选独立) |
## 文档与测试
- 更新钉钉式 OA 设计文档中「工作流角色」章节为「职位」
- 需求/OpenSpec 若提及 OA 独立角色作审批人,改为职位
- 单测:`test_oa_assignee.py` 改为 posts;去掉「不读 sys_role」之外的 oa_role mocks;`test_oa_publish.py` 同步
- `./scripts/update-graph.sh backend`(及 frontend)
## 非目标
- 职位权限(`sys_post_role`)行为不变
- 不种子 GM/CHAIRMAN 职位
- 不做旧 JSON 兼容读取
- 不改审批人其它类型:`users` / `dept_leader` / `direct_supervisor` / `initiator_select`
## 实现顺序(建议)
1. 后端:`list_user_ids_bound_to_posts` + assignee/publish/engine 改 `posts`
2. 前端:FlowDesigner + types + 进度文案
3. 删除角色模块(页/路由/API/CRUD/模型)
4. drop SQL + 菜单清理 + 文档/单测
5. 图谱更新
## 验收
1. OA 菜单无「工作流角色」
2. 流程设计审批/抄送可选职位并保存 `posts`/`postIds`
3. 职位已关联用户时,发起后待办落到这些用户
4. 空成员职位无法正常完成解析(错误可读)
5. 代码库无 `oa_roles` / `fetchOaRole` / `OaRole` 残留(除历史设计文档归档说明外)
+6 -2
View File
@@ -387,12 +387,16 @@
### 2.1.8、职位管理
- 这个模块用于职位信息的维护管理,支持职位绑定用户,一个职位可以绑定多个用户,一个用户可以绑定多个职位
- 职位可关联角色:一个职位可绑定多个角色;用户通过职位继承这些角色的**菜单权限**与**数据权限**
- **特别注意:用户最终权限 = 用户直接绑定角色 ∪ 经启用职位继承的角色**(与 §2.1.5 多角色并集语义一致;停用职位或停用角色不参与并集)
- 职位表 sys_post,核心字段包括: 职位名称、职位编码、排序号、状态(1 正常 0 停用)
- 职位与用户关联表 sys_user_post, 核心字段: 用户id、职位id
- 职位表和职位与用户关联表的关系
- 职位与角色关联表 sys_post_role, 核心字段: 职位id、角色id
- 职位表和关联表的关系
- 职位表
- 职位与用户关联表
- 界面中展示职位信息,支持职位信息的新增、修改、删除、职位用户配置。点击【职位用户配置】按钮,弹出弹窗,弹窗中展示用户信息[2.1.4、用户管理],支持勾选多个用户
- 职位与角色关联表
- 界面中展示职位信息,支持职位信息的新增、修改、删除、职位用户配置、职位角色配置。点击【职位用户配置】按钮,弹出弹窗,弹窗中展示用户信息[2.1.4、用户管理],支持勾选多个用户;点击【角色配置】按钮,弹出弹窗支持勾选多个角色[2.1.5、角色管理]。用户管理支持为用户多选职位。
### 2.1.9、物品单位管理
- 记录物品单位的表,核心字段: 单位编码、单位名称 (例如:单位编码 g 、 单位名称 克)