Przeglądaj źródła

docs: add duty staff role filter design for schedule selection.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 miesięcy temu
rodzic
commit
2942d176bd

+ 113 - 0
docs/superpowers/specs/2026-08-17-duty-staff-role-design.md

@@ -0,0 +1,113 @@
+# 值班排班 ·「值班人员」角色筛选 — 设计文档
+
+> **日期**: 2026-08-17  
+> **状态**: 已确认  
+> **仓库**: shanxia2.0(`backend/` + `web-admin/`)  
+> **前置**: `docs/superpowers/specs/2026-08-14-duty-schedule-design.md`(已实现)  
+> **方案**: 方案 1 — 固定「值班人员」角色 + 值班专用人员 options 接口
+
+## 背景
+
+值班排班配置弹窗与公开抢班链接的人员下拉,当前返回本租户全部启用系统用户。业务需要收窄为:仅「值班人员」角色下的用户可被新指定 / 新抢班。
+
+## 已确认决策
+
+| # | 决策 |
+|---|------|
+| D1 | 新建专用角色,展示名 **「值班人员」** |
+| D2 | 角色不挂任何菜单,仅作「可排班名单」标签;在用户管理中给用户勾选即可 |
+| D3 | 管理端「直接安排」与公开页「选择姓名」均只展示该角色下的启用用户 |
+| D4 | 历史 assignment 仍展示;不强制清理未赋角色的历史人员 |
+| D5 | 新指定 / 新抢班在后端强制校验角色,防止绕过下拉 |
+| D6 | 不改全局 `/web/admin/users/options`;值班模块使用专用 options 接口 |
+| D7 | 不自动给现有用户赋该角色;不做可配置「值班角色」设置页 |
+
+## 方案选择
+
+**采用方案 1(已确认)**:迁移创建固定 UUID 的「值班人员」角色;`GET /web/admin/duty/users/options` 供配置弹窗;公开页 `_list_public_users` 与 `grab` / `save_day` 共用同一角色判定。
+
+未采用:给全局用户 options 加 `roleName` 参数(业务耦合到通用接口);可配置值班角色设置页(当前无需求)。
+
+## 角色与数据
+
+### 迁移
+
+- 脚本:`backend/scripts/migrate_20260817_duty_staff_role.sql`(幂等插入)
+- 角色:
+  - `id`:固定 UUID,例如 `R65CAAE057914CCCAE2A81BE627A5101`
+  - `role_name`:`值班人员`
+  - `status`:`1`(启用)
+  - `data_scope`:`4`(仅本人即可;本角色不用于数据权限)
+  - `tenant_no`:`shanxia2.0`(与现有业务角色一致;多租户若后续需要再按租户补插)
+  - **不**写入 `sys_role_menu`
+  - **不**预绑定任何 `sys_user_role`
+- SQL 快照:按仓库约定同步维护 `docs/sql/`(或至少保证迁移脚本可在目标库执行)
+
+### 代码识别
+
+- 常量:`DUTY_STAFF_ROLE_ID`(固定 id)
+- 解析顺序:先按 id + 租户 + 未删除查找角色;找不到再按 `role_name == '值班人员'` 兜底(同租户)
+- 用户是否可排班:本租户 + `SysUser.del_flag=0` + `status=1` + 存在有效 `sys_user_role` 指向该角色
+
+## 接口与校验
+
+### 管理端
+
+| 能力 | 说明 |
+|------|------|
+| `GET /web/admin/duty/users/options` | 返回可排班用户:`[{ label, value }]`(label=昵称优先,否则账号;value=用户 id) |
+| `POST /web/admin/duty/day` | 保存时对每个 `source=assign` 且带 `userId` 的项校验「值班人员」角色;失败返回明确文案(如「仅可安排值班人员角色的用户」) |
+
+配置页人员下拉改调值班专用 options;其它业务继续用全局 `/web/admin/users/options`。
+
+### 公开(免登录)
+
+| 能力 | 说明 |
+|------|------|
+| `GET /api/public/duty/{token}` | 响应中 `users` 仅含「值班人员」角色用户 |
+| `POST /api/public/duty/{token}/grab` | 所选 `userId` 必须具备该角色,否则拒绝 |
+
+### 历史兼容
+
+- 月历摘要、日详情、公开页已报名列表:**照常展示**已有 assignment(含当时无该角色的人)
+- 仅限制新写入:管理端新指定、公开新抢班
+
+## 前端
+
+| 位置 | 改动 |
+|------|------|
+| `web-admin/src/views/duty/schedule/index.vue` | 加载人员 options 改为值班专用 API;文案可注明仅显示「值班人员」 |
+| `web-admin/src/api/duty.ts` | 新增 `fetchDutyUserOptions` |
+| `web-admin/src/views/public/duty/index.vue` | 交互不变;依赖后端 `users` 过滤 |
+| 用户管理 | 不改;管理员在角色勾选中赋「值班人员」 |
+
+## 错误处理
+
+| 场景 | 行为 |
+|------|------|
+| 指定非值班人员用户 | 保存失败,提示需具备「值班人员」角色 |
+| 公开抢班选非值班人员(或伪造 id) | 抢班失败,提示用户无效或不具备值班资格 |
+| 租户内尚无该角色记录 | options 返回空列表;保存/抢班校验失败并提示角色未配置 |
+
+## 测试要点
+
+1. 迁移后角色列表出现「值班人员」,无菜单权限。
+2. 未赋角色用户:配置下拉与公开页下拉均不可见。
+3. 赋角色后:两边可选,指定与抢班成功。
+4. 伪造无角色 `userId`:管理端保存与公开抢班均失败。
+5. 历史上无角色人员仍显示在日历/详情中。
+6. 其它页面全局用户下拉不受影响。
+
+## 不在范围内
+
+- 不自动给现有用户赋「值班人员」角色
+- 不修改全局 `/web/admin/users/options` 行为
+- 不做系统设置里可配置「值班角色」
+- 不清理历史 assignment
+
+## 实现落点(摘要)
+
+- Backend:`duty_schedule.py` 抽取 `list_duty_staff_users` / `assert_duty_staff`;`duty.py` 注册 options;公开路径复用同一过滤
+- Frontend:排班页改用 `fetchDutyUserOptions`
+- SQL:`migrate_20260817_duty_staff_role.sql`
+- 单测:扩展 `backend/tests/test_duty_schedule.py`(无角色拒绝抢班/指定;有角色可通过列表过滤)