Web 员工模块 — CUJ 关键用户旅程

Employees, Schedules, Time Clock & Payroll | 更新时间: 2026-02-09

TOUCHES (Pages - 12):
employees/page.tsx (员工列表), employees/[id]/page.tsx (员工详情 — 基本信息/服务能力/特殊权限), employees/schedules/page.tsx (排班总览 — List/Kanban 双视图), employees/schedules/[employeeId]/page.tsx (个人排班日历), employees/time-clock/page.tsx (打卡面板 — PIN/Click), employees/time-cards/page.tsx (工时卡 — 员工自助/管理员双模式), employees/tips/page.tsx (小费汇总), employees/payroll/page.tsx (工资单), admin/employees/page.tsx, admin/schedules/page.tsx, admin/permissions/roles/page.tsx (角色管理), admin/permissions/job-titles/page.tsx (职位管理)

TOUCHES (Components):
AddEmployeeModal.tsx, ScheduleKanbanBoard.tsx, AutoScheduleModal.tsx, GenerateScheduleModal.tsx, WeeklyTemplateModal.tsx, ScheduleEditor.tsx, QuickClockPanel.tsx, PinSetupModal.tsx, TimeCardTable.tsx, TimeCardDetailModal.tsx, MakeupPunchModal.tsx, PayrollTable.tsx, PayrollDetailModal.tsx

TOUCHES (Backend):
backend/api/time-clock.js (打卡/PIN/工时卡/补卡审批/设置), backend/api/payroll.js (工资单), backend/api/employee_availability.js, backend/api/daily_availability.js, backend/services/timeClockService.js, backend/services/timeCardService.js, backend/services/payrollService.js

架构概览

数据关系

flowchart TB
    EMP["employees\n基本信息/角色/门店"] --> SCH["employee_schedules\n每日排班 (date-specific)"]
    EMP --> WKS["employee_work_schedules\n周模板 (recurring)"]
    EMP --> CAP["employee_service_capabilities\n服务能力"]
    EMP --> PIN["employee_pins\nPIN 打卡码"]
    EMP --> TC["time_cards\n每日考勤卡"]
    TC --> TP["time_punches\n打卡记录"]
    EMP --> PR["payroll_reports\n工资单"]
    EMP --> APT["appointments\n被分配的预约"]

    WKS -->|"AutoSchedule\n模板生成"| SCH
    SCH -->|"影响"| AVAIL["可预约时段\nemployee_available_start_times"]
    TC -->|"汇总到"| PR

排班层级

层级数据表说明优先级
周模板employee_work_schedules周一到周日的固定模板(0=Sunday, 6=Saturday)最低 — 用于自动生成
每日排班employee_schedules具体日期的排班(date, start, end, breaks)最高 — 实际执行

打卡类型

punch_type说明触发条件
clock_in上班打卡当前未打卡
clock_out下班打卡当前已打卡且非休息中
break_start开始休息当前已打卡且非休息中
break_end结束休息当前处于休息中

目录

CUJ 总览与优先级矩阵

CUJ优先级描述触发点业务价值E2E 状态
CUJ-D1 P0 员工列表与详情管理 管理员进入员工页面 人事基础管理 已覆盖
CUJ-D2 P0 排班管理 管理员创建/编辑排班 排班 → 可预约时段 已覆盖
CUJ-D3 P0 PIN 打卡与考勤 员工上下班打卡 考勤 → 工资计算 已覆盖
CUJ-D4 P1 工时卡与补卡审批 管理员审核 / 员工查看 考勤异常处理 部分覆盖
CUJ-D5 P1 小费管理 查看/分配小费 技师收入管理 部分覆盖
CUJ-D6 P2 工资单 生成周期工资报表 薪酬管理 部分覆盖

CUJ-D1: 员工列表与详情管理

P0 人事基础 — 员工 CRUD、服务能力、特殊权限、门店调动

用户流程

flowchart TD
    A["侧边栏 → Employees"] --> B["员工列表\n筛选: 门店/职位/状态"]
    B --> C{操作?}
    C -->|"添加"| D["AddEmployeeModal\n姓名/邮箱/角色/门店"]
    D --> E["自动生成 employee ID"]
    C -->|"查看详情"| F["/employees/id"]
    F --> G["Tab 1: 基本信息\n姓名/邮箱/角色/职位/头像"]
    F --> H["Tab 2: 服务能力\n多选 checklist"]
    F --> I["Tab 3: 特殊权限\nGRANT/DENY (super_admin only)"]
    C -->|"停用"| J["is_active → false\n不再出现在排班/预约"]

    style B fill:#2196F3,stroke:#1565C0,color:#fff
    style F fill:#4CAF50,stroke:#2E7D32,color:#fff

BDD 场景

场景 D1.1: 查看员工列表 — 筛选与搜索

Given 用户已登录并拥有对应功能所需权限与范围(scope),已登录
When 导航到 /employees
Then 显示当前门店的员工列表(分页,20条/页)
  And 支持按姓名/邮箱搜索
  And 支持按职位筛选、按状态筛选 (active/inactive)
  And 显示"今日排班状态"标签 (working/off/no schedule)

测试: employees.spec.ts

场景 D1.2: 添加新员工

Given 用户已登录并拥有对应功能所需权限与范围(scope),在员工列表页
When 点击"添加员工"打开 AddEmployeeModal
  And 填写姓名 "New Tech"、邮箱 "new@test.com"、角色 "employee"
  And 选择门店和职位
  And 保存
Then 系统自动生成 employee ID (emp_001, emp_002...)
  And 新员工出现在列表中
  And 初始 must_change_password = true

测试: full-flow/02c-employee-crud.spec.ts

场景 D1.3: 管理服务能力

Given 用户已登录并拥有对应功能所需权限与范围(scope),在员工 Alice 的详情页 Tab 2
When 勾选 "Gel Manicure" 和 "Pedicure"
  And 取消勾选 "Acrylic Nails"
  And 保存
Then Alice 的服务能力更新
  And 在线预约时,Alice 只出现在 Gel Manicure 和 Pedicure 的可选技师中

场景 D1.4: 管理特殊权限 (super_admin only)

Given 用户已登录并拥有对应功能所需权限与范围(scope), 在员工 Bob 的详情页 Tab 3
When 为 Bob 添加 GRANT: reports:view
  And 添加 DENY: employees:delete
Then Bob 可以查看报表(即使角色无此权限)
  And Bob 不能删除员工(即使角色有此权限)

场景 D1.5: 停用/终止员工

Given 员工 Charlie 要离职
When 用户在详情页将 is_active 切换为 false
Then Charlie 不再出现在排班表和技师选择列表中
  And Charlie 的现有预约不受影响
  And Charlie 无法登录

CUJ-D2: 排班管理

P0 运营基础 — 技师排班直接影响 employee_available_start_times,进而影响可预约时段

用户流程

flowchart TD
    A["/employees/schedules\n排班总览"] --> B{视图模式?}
    B -->|"List"| C["表格视图\n排班统计/今日状态"]
    B -->|"Kanban"| D["ScheduleKanbanBoard\n周视图所有技师"]
    D --> E{操作?}
    E -->|"手动"| F["点击空白格子\n设置时间段"]
    E -->|"模板"| G["WeeklyTemplateModal\n编辑周模板"]
    E -->|"自动生成"| H["GenerateScheduleModal\n按模板批量生成N周"]
    E -->|"智能排班"| I["AutoScheduleModal\n规则自动生成"]

    C --> J["点击员工\n→ /schedules/employeeId"]
    J --> K["个人日历视图\n月度排班详情"]

    style A fill:#2196F3,stroke:#1565C0,color:#fff
    style K fill:#4CAF50,stroke:#2E7D32,color:#fff

BDD 场景

场景 D2.1: 手动创建排班

Given 用户已登录并拥有对应功能所需权限与范围(scope),在排班 Kanban 视图
When 点击 Alice 周一的空白格子
  And 设置 9:00 AM - 6:00 PM,类型 regular
  And 添加休息时间 12:00 - 1:00 PM
  And 保存
Then employee_schedules 新增记录
  And Alice 周一的可预约时段自动更新(排除休息时间)

测试: full-flow/02d-schedule-crud.spec.ts

场景 D2.2: 使用模板批量生成排班

Given 已有周模板 "Standard Week"(周一到周五 9-6,周六 10-3)
When 用户打开 GenerateScheduleModal
  And 选择模板 + 日期范围(未来 4 周)
  And 点击"生成"
Then 4 周排班按模板自动填充到 employee_schedules
  And 已存在的排班不被覆盖

测试: schedule-templates/schedule-generation.spec.ts

场景 D2.3: 设置休假

Given Alice 下周三要请假
When 用户编辑 Alice 周三的排班
  And 设置 schedule_type = vacation,is_available = false
Then Alice 周三的可预约时段清零
  And Kanban 视图显示"休假"标签

场景 D2.4: 查看个人排班日历

Given 用户已登录并拥有对应功能所需权限与范围(scope),在排班总览点击 Alice
When 进入 /employees/schedules/[aliceId]
Then 显示月度日历视图
  And 每日格子显示排班时间段
  And 支持拖拽编辑 (react-dnd)

CUJ-D3: PIN 打卡与考勤

P0 考勤管理 — PIN/Click 两种打卡方式,自动计算迟到/早退

用户流程

flowchart TD
    A["/employees/time-clock\nQuickClockPanel"] --> B{打卡方式?}
    B -->|"PIN 打卡"| C["输入 4-6 位 PIN"]
    B -->|"Click 打卡"| D["选择员工\n点击打卡按钮"]
    C --> E{PIN 正确?}
    E -->|"否"| F["错误提示\n失败次数+1"]
    F --> G{5次失败?}
    G -->|"是"| H["PIN 锁定 30 分钟"]
    G -->|"否"| C
    E -->|"是"| I{当前状态?}
    D --> I
    I -->|"未打卡"| J["Clock In\n记录上班时间"]
    I -->|"已打卡"| K["Clock Out\n记录下班时间"]
    I -->|"休息中"| L["Break End\n结束休息"]
    J --> M["对比排班计算\n迟到分钟数"]
    K --> N["对比排班计算\n早退分钟数"]

    style A fill:#2196F3,stroke:#1565C0,color:#fff
    style J fill:#4CAF50,stroke:#2E7D32,color:#fff
    style K fill:#FF9800,stroke:#E65100,color:#fff

BDD 场景

场景 D3.1: PIN 打卡上班

Given 员工 Alice 今天尚未打卡
  And 排班 9:00 AM - 6:00 PM
When 在 QuickClockPanel 输入 Alice 的 4 位 PIN
Then 识别员工 Alice
  And 记录 clock_in, punch_method = pin
  And 自动创建当天 time_card
  And 如果9:05打卡,显示"迟到 5 分钟"

测试: time-clock.spec.ts

场景 D3.2: PIN 打卡下班

Given Alice 已打卡上班
When 再次输入 Alice 的 PIN
Then 记录 clock_out
  And 如果5:30打卡(排班6:00),显示"早退 30 分钟"
  And time_card 状态变为 completed

场景 D3.3: 休息打卡

Given Alice 已打卡上班,非休息状态
When 点击 "Start Break"
Then 记录 break_start
  And 状态显示 "On Break"
When 点击 "End Break"
Then 记录 break_end
  And 休息时长计入 breakMinutes

场景 D3.4: PIN 锁定 — 5 次失败

Given 已连续输入4次错误 PIN
When 第5次输入错误 PIN
Then PIN 被锁定 30 分钟
  And 显示 "PIN locked, contact manager"
  And 管理员可通过 POST /time-clock/pin/unlock 手动解锁

场景 D3.5: PIN 设置

Given 新员工尚未设置 PIN
When 用户打开 PinSetupModal
  And 输入 4 位 PIN "1234"
Then PIN 用 bcrypt 哈希后存入 employee_pins
  And 员工可以使用该 PIN 打卡

测试: time-clock.spec.ts (部分覆盖)

CUJ-D4: 工时卡与补卡审批

P1 考勤审核 — 双模式查看工时,补卡需管理员审批

用户流程

flowchart TD
    A["/employees/time-cards"] --> B{用户角色?}
    B -->|"员工"| C["My Time Cards\n只看自己的记录"]
    B -->|"管理员"| D["All Time Cards\n所有员工 + 筛选"]
    D --> E["Tab: cards / pending"]
    E -->|"cards"| F["工时卡列表\n汇总: 总天数/总工时/迟到数/早退数"]
    E -->|"pending"| G["待审批补卡列表"]
    G --> H{审批操作}
    H -->|"批准"| I["approveMakeupPunch\n补卡写入工时卡"]
    H -->|"拒绝"| J["rejectMakeupPunch\n填写拒绝原因"]

    C --> K["查看详情\nTimeCardDetailModal"]
    F --> K
    K --> L["打卡明细\nclock_in/out/break 时间线"]

    style A fill:#2196F3,stroke:#1565C0,color:#fff
    style I fill:#4CAF50,stroke:#2E7D32,color:#fff
    style J fill:#f44336,stroke:#c62828,color:#fff

BDD 场景

场景 D4.1: 员工查看自己的工时卡

Given 技师 Alice 已登录(权限: time_clock:view_own
When 导航到 /employees/time-cards
Then 标题显示 "My Time Cards"
  And 只显示 Alice 自己的打卡记录
  And 没有员工筛选器

场景 D4.2: 管理员查看所有工时卡

Given 用户已登录并拥有 time_clock:view_all,且具备对应范围(scope)
When 导航到 /employees/time-cards
Then 显示所有员工的工时卡
  And 汇总卡片: 总打卡天数、总工时、迟到次数、早退次数
  And 支持按员工/日期/状态筛选

场景 D4.3: 提交补卡申请

Given Alice 昨天忘记打下班卡
When 用户打开 MakeupPunchModal
  And 选择 Alice、昨天、下班时间 6:00 PM
  And 填写原因 "忘记打卡"
  And 提交
Then 补卡记录状态为 pending
  And 出现在管理员的 "Pending Approvals" 标签页

场景 D4.4: 审批补卡 — 批准

Given 用户已登录并拥有对应功能所需权限与范围(scope),在 Pending 标签页看到 Alice 的补卡申请
When 点击"批准"
Then 补卡状态变为 approved
  And 打卡记录写入 Alice 昨天的工时卡
  And 标记为"管理员补卡"

场景 D4.5: 导出工时卡 CSV

Given 用户已登录并拥有对应功能所需权限与范围(scope),在工时卡列表
When 点击"导出"
Then GET /time-clock/time-cards/export 生成 CSV
  And 包含: 员工、日期、上班时间、下班时间、工时、迟到、早退

CUJ-D5: 小费管理

P1 收入管理 — 查看技师小费明细

BDD 场景

场景 D5.1: 查看小费汇总

Given 用户已登录并拥有对应功能所需权限与范围(scope),导航到 /employees/tips
When 选择本周日期范围
Then 显示每位技师的小费明细: 现金小费、卡小费、总计

测试: employees/tips.spec.ts

场景 D5.2: 员工查看自己的小费

Given 技师 Alice 已登录(权限: tips:view level=view)
When 导航到 /employees/tips
Then 只显示 Alice 自己的小费记录
  And 看不到其他技师的小费
小费与日结的关系: 小费分配在日结流程的 Step 2 (TipsSettlementStep) 中完成,分配算法有两种: equal(平分)和 by_service_price(按服务价格比例)+ 门店佣金扣除。详见 CUJ-C1 Step 2

CUJ-D6: 工资单

P2 薪酬管理 — 生成和管理员工工资单

工资单状态流转

flowchart LR
    A["draft\n草稿"] -->|"finalizePayroll()"| B["finalized\n已确认"]
    B -->|"markAsPaid()"| C["paid\n已发放"]

BDD 场景

场景 D6.1: 生成工资单

Given 用户已登录并拥有对应功能所需权限与范围(scope),在 /employees/payroll
When 选择工资周期 2026-01-01 至 2026-01-15
  And 选择员工(可选全部)
  And 点击"生成"
Then 系统计算每位员工: 总工时、加班工时(>40h/周)、基本工资、小费、扣除、净工资
  And 生成 draft 状态的工资报表

测试: payroll.spec.ts (部分覆盖)

场景 D6.2: 确认并发放工资

Given 工资单处于 draft 状态
When 用户审核后点击"确认"
Then 状态变为 finalized
When 实际发放后标记"已发放"
Then 状态变为 paid

跨模块链接

从 D 模块关联 CUJ关系
D2 排班CUJ-G1 在线预约排班 → employee_available_start_times → 可选时段
D1 服务能力CUJ-G1 技师选择employee_service_capabilities → 可选技师列表
D3 打卡CUJ-C1 Step 1考勤数据用于日结 OrderStatus 验证
D5 小费CUJ-C1 Step 2小费在日结 TipsSettlement 中分配
D6 工资CUJ-H 报表payroll_reports 数据用于员工报表
D1 特殊权限CUJ-A5 RBACGRANT/DENY 特殊权限覆盖角色默认

关键业务规则

排班 → 可预约时段 链路: employee_work_schedules(周模板)→ AutoSchedule 生成 → employee_schedules(每日排班)→ 系统计算 → employee_available_start_times(可预约时段)→ 在线预约 /slots API 返回。排班变更会自动触发可预约时段重算。
PIN 安全:
打卡自动创建工时卡: 当天第一次 clock_in 时,系统自动创建 time_cards 记录。后续所有打卡(clock_out, break_start, break_end)都关联到该 time_card。
迟到/早退计算: 打卡时自动对比 employee_schedules 中的排班时间。clock_in > shift_start 则计算迟到分钟;clock_out < shift_end 则计算早退分钟。无排班记录则标记为 isUnscheduled
权限分级: