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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-17 18:15:58 +08:00
1 parent dea4159636
commit 2942d176bd
1 file changed
+113
@@ -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`(无角色拒绝抢班/指定;有角色可通过列表过滤)