docs: add duty shift dict, work plan, and time range design

Capture confirmed approach for customizable shift types and work plans via sys_dict, slot time fields with morning/evening defaults, migration, and public H5 sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-20 20:36:33 +08:00
1 parent 09a57a7af9
commit 361e923396
1 file changed
+187
@@ -0,0 +1,187 @@
# 值班排班 · 自定义类型 / 工作安排字典 / 值班时段 — 设计文档
> **日期**: 2026-08-20
> **状态**: 已确认
> **仓库**: shanxia2.0(`backend/` + `web-admin/`)
> **方案**: 方案 1 — 复用 `sys_dict` + `duty_slot` 扩字段
> **前置**: `docs/superpowers/specs/2026-08-14-duty-schedule-design.md`(V1 排班)及后续值班人员角色、月历筛选等增量
> **影响页面**: 管理端 `/#/duty/schedule`;公开移动端 `/#/public/duty/{token}`
## 背景
值班排班 V1 将班次类型写死为 `morning` / `evening` / `duty`(早班 / 晚班 / 值班),工作安排落在 `duty_slot.remark` 自由文本,且无值班时间段。业务需要:
1. **值班类型可自定义**:输入新类型后写入字典,下次可选;新建某日默认仍带出三种预置类型,同日可追加其它类型。
2. **工作安排改为多选下拉**:预置如配酸、装卸;可新建并入库;与备注分离。
3. **增加值班时间段**:可改;早班默认 08:00–18:00,晚班默认 18:00–次日 07:00;「值班」暂不设默认。
4. **公开抢班 H5 同步展示**类型名、时段、工作安排、备注。
## 已确认决策
| # | 决策 |
|---|------|
| D1 | 采用方案 1:复用系统字典 `sys_dict`,两类字典;新建值写入 `sys_dict_data` |
| D2 | 新建某日默认三条预置类型;同日支持追加多个其它类型;**同日同类型最多一条 slot**(与 V1 一致) |
| D3 | 工作安排 **多选**;存逗号分隔的 dict_value;每条 slot 另有独立 **备注** |
| D4 | 早班默认 08:00–18:00(可改);晚班默认 18:00–次日 07:00(可改);值班及其他自定义类型默认时段为空 |
| D5 | `shift_type` 继续存 **dict_value**(预置保持 `morning`/`evening`/`duty`),不改写历史编码 |
| D6 | 字典类型用系统字典页维护即可;排班页 `allow-create` 只追加 dict_data |
| D7 | 管理端与公开移动端同一数据源,H5 必须同步改展示与抢班校验 |
| D8 | 月历统计:预置三列保留;自定义类型计入「其他」(轻量) |
## 方案选择
**采用方案 1(已确认)**:字典编码稳定 + `duty_slot` 扩字段。
未采用:业务表只存中文标签(历史编码难兼容);工作安排 JSON 数组(与仓库逗号分隔习惯不一致)。
## 架构与边界
```
sys_dict_type / sys_dict_data
duty_shift_type → 值班类型选项
duty_work_plan → 工作安排选项
duty_day
└── duty_slot(shift_type = dict_value,同日唯一)
├── work_plans / remark / start_time / end_time / cross_day
└── duty_assignment × M
```
| 端 | 职责 |
|----|------|
| **web-admin 排班** | 配置日、字典下拉+创建、时段、多选工作安排、备注;月历/筛选/统计适配 |
| **公开 H5** | 展示 label / 时段 / 工作安排 / 备注;按 `shiftType` 抢班 |
| **backend** | slot 字段读写;保存时 upsert 字典项;公开 API 返回展示字段;校验放宽为「当日存在该 slot」 |
| **系统字典页** | 可启停、改 label;不必新建独立「值班类型」业务页 |
## 数据模型
### 字典种子
| dict_type | dict_label | dict_value | 说明 |
|-----------|------------|------------|------|
| `duty_shift_type` | 早班 | `morning` | 默认时段 08:00–18:00,`cross_day=0` |
| `duty_shift_type` | 晚班 | `evening` | 默认时段 18:00–07:00,`cross_day=1` |
| `duty_shift_type` | 值班 | `duty` | 无默认时段 |
| `duty_work_plan` | 配酸 | `peisuan` | |
| `duty_work_plan` | 装卸 | `zhuangxie` | |
新建自定义类型 / 工作安排时:`dict_label` 为用户输入文案(去首尾空白);`dict_value` 优先用拼音小写(非法字符剔除),冲突则追加短后缀(如 `_` + 4 位 hex),租户内同 `dict_type` 下 `dict_value` 唯一;写入当前 `tenant_no`。若已存在相同 `dict_label`(未删除),复用该条,不新建。
### `duty_slot` 变更
| 字段 | 变更 | 说明 |
|------|------|------|
| `shift_type` | 语义放宽 | 存 dict_value;不再限死三枚举;同日唯一(未删除) |
| `work_plans` | **新增** | `varchar(500)`,多选 dict_value 逗号分隔,如 `peisuan,zhuangxie` |
| `remark` | **语义变更** | 自由备注;不再表示工作安排 |
| `start_time` | **新增** | `TIME`,可空 |
| `end_time` | **新增** | `TIME`,可空 |
| `cross_day` | **新增** | `smallint`,默认 0;1 表示结束时间在次日 |
字段顺序遵守:`id` → 业务字段 → 公共字段。SQL 变更同步 `docs/sql/` 与 `backend/scripts/`。
### API 载荷(增量)
Slot 读写字段(camelCase):
- `shiftType`、`shiftLabel`(只读展示)
- `workPlans`: `string[]`(API 进出均为数组;库内逗号拼接 dict_value)
- `workPlanLabels`: `string[]` 只读,按字典解析;缺字典时回退为 value 本身
- `remark`: 备注
- `startTime` / `endTime`: `"HH:mm"` 或 `null`
- `crossDay`: `boolean`(库内 0/1)
- `timeLabel`: 只读,如 `08:00–18:00`、`18:00–次日07:00`;无时段时为 `null`
公开:
- `GET /api/public/duty/{token}`:slots 含上述展示字段
- `POST .../grab`:`shiftType` 改为任意非空字符串;校验当日存在该 slot、开链、未满员(不再 `in SHIFT_LABELS`)
管理端保存:若传入的类型 label/工作安排值在字典中不存在,则 upsert `sys_dict_data` 后再落 slot。
复用:`fetchDictDataOptions('duty_shift_type' | 'duty_work_plan')`。
## Web 管理端
### 配置某日弹窗
- 未配置日打开:默认三条(早/晚/值班),早/晚带默认时段,值班时段空;均可取消启用。
- 「添加班次」:从 `duty_shift_type` 选择(filterable + allow-create);新建写入字典;已占用类型不可再选。
- 每条启用的 slot:名额、时段(起止;`cross_day` 可由结束&lt;开始推断或显式)、工作安排多选(allow-create 入库)、备注、值班安排(选人,规则不变)。
- 校验:同日同 `shiftType` 唯一;缩容 / 有人不可删班次等规则保持 V1。
### 月历 / 筛选 / 统计
- 图例与筛选读字典(预置优先);自定义类型用中性色点。
- 按人统计:预置 morning/evening/duty 三列 +「其他」。
视觉:Fresh Mint;`label-position="top"`;弹窗无背景模糊。
## 公开移动端 H5(必须同步)
路径:`/#/public/duty/{token}`(`web-admin/src/views/public/duty/index.vue`)。
| 区域 | 行为 |
|------|------|
| 班次标题 | `shiftLabel`;未知类型中性样式 |
| 时段 | 有则展示 `timeLabel`;无则隐藏 |
| 工作安排 | `workPlanLabels`,顿号或 chip;无则隐藏 |
| 备注 | 有 `remark` 时展示「备注:…」 |
| 抢班 | body 仍传该 slot 的 `shiftType` |
| 成功页 | 建议带时段与工作安排摘要 |
视觉:三墙 Token(`#0a9f6d`),不与管理端色值混用。
## 存量迁移
上线顺序:
1. 插入 `sys_dict_type` + 预置 `sys_dict_data`(两类)。
2. `ALTER TABLE duty_slot` 增加 `work_plans`、`start_time`、`end_time`、`cross_day`。
3. 数据回填:
- `remark` 去空白后整段等于「配酸」→ `work_plans=peisuan`,清空 `remark`;等于「装卸」→ `zhuangxie`;若同时需支持历史里逗号拼接的「配酸,装卸」可按分隔拆分映射。
- 其它非空 `remark` → **保留为备注**(不迁入 work_plans)。
- `shift_type=morning` → `start_time=08:00`、`end_time=18:00`、`cross_day=0`。
- `shift_type=evening` → `18:00`、`07:00`、`cross_day=1`。
- `duty` 与未知类型 → 时间字段保持 NULL。
4. 发布后端,再发布 web-admin(排班 + 公开 H5)。
5. 回归:旧 token 可打开;历史早/晚显示默认时段;曾填配酸/装卸显示为工作安排标签。
`access_token` 不变;不改写历史 `shift_type` 编码。
## 错误处理
| 场景 | 行为 |
|------|------|
| 同日重复 shiftType | 保存拒绝,明确文案 |
| 字典写入重名 / 冲突 | 提示失败;优先复用已有同 label 项 |
| 缩容过大 / 删仍有人的班次 | 保持 V1 |
| H5 抢不存在的类型 | 拒绝并刷新 |
| 字典项停用 | 历史按存库 value 解析 label(或缺省显示 value);新排班下拉仅启用项 |
## 测试要点(验收)
1. 新建日默认三类型;可追加自定义类型且入库,下次可选。
2. 工作安排多选 + 新建入库;与备注互不覆盖。
3. 早/晚默认时段可改;值班可空。
4. 同日同类型不可重复。
5. 月历/筛选/统计不因自定义类型报错。
6. 公开链接展示类型、时段、工作安排、备注,并可抢班成功。
7. 迁移后历史展示正确,token 不变,租户隔离仍成立。
## 明确不做(本迭代)
- 同日同类型多条 slot
- 独立「值班类型」业务维护页(用系统字典)
- 改写历史 `morning` / `evening` / `duty` 编码
- 批量按日期范围建排班、员工自助取消、重置 token(仍按 V1)
## 相关文件(实现时)
- 模型 / 服务 / API:`backend/app/models/duty_models.py`、`services/duty_schedule.py`、`schemas/duty.py`、`api/duty.py`、`api/public_duty.py`
- 管理端:`web-admin/src/views/duty/schedule/index.vue`、`web-admin/src/api/duty.ts`
- 公开 H5:`web-admin/src/views/public/duty/index.vue`、`web-admin/src/api/public-duty.ts`
- 字典:现有 `sys_dict_*` CRUD 与 `fetchDictDataOptions`
- 单测:`backend/tests/test_duty_schedule.py`