diff --git a/docs/superpowers/specs/2026-08-17-duty-staff-role-design.md b/docs/superpowers/specs/2026-08-17-duty-staff-role-design.md new file mode 100644 index 000000000..9c9ed9ab1 --- /dev/null +++ b/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`(无角色拒绝抢班/指定;有角色可通过列表过滤)