4.2 员工端:排班与接单

场景:美甲师不是桌椅板凳

档期表把一天切成时段格子,但格子是死的,美甲师是活的。开业第一个月,系统按纯时段容量放号:一个时段六把椅子,约满即止。结果有一天两位美甲师同时请假,剩下的人根本做不完六个同时段的单子,客人在店里等得冒火。林姐说:你们这系统算的是椅子,不是人。

问题的本质是产能和人员的解耦与再耦合:档期表管物理容量,排班表管谁在岗,接单表管派给谁。三层各自独立又能串起来,是这一节的设计主线。

林姐的原话更形象:椅子是死的,人是活的,系统要两头都看得见。这一节从三张表讲到企微里的三个页面,看起来都是小工具,撑起的却是门店每天最早开始运转的那件事:今天谁来、谁干什么。

顺带一个观察:整个员工端我们没写任何算法,但做出了全店最受欢迎的系统。有时候工具的价值不在智能,在于把每天重复的烦人事做掉。

方案:三张表一台戏

三张表的分工是这样的:员工表存人,排班表存人岗匹配,接单流水存结果。

表 管什么 关键字段
t_staff 员工档案 shop_id、姓名、职级
t_staff_daily 每日排班 staff_id、日期、班次
t_assign 接单流水 staff_id、book_id、状态

排班表是核心。每天一条记录,班次直接用门店的时段模板对齐:早班 9 到 16 点(540 到 960 分钟),晚班 13 到 21 点。排班和时段容量之间是软约束关系:系统不阻止超卖,但会在排班少于容量时提醒店长。

CREATE TABLE t_staff_daily (
  daily_id  BIGINT PRIMARY KEY AUTO_INCREMENT,
  staff_id  INT NOT NULL,
  duty_date DATE NOT NULL,
  shop_duty INT NOT NULL COMMENT '当天在哪个店上班',
  start_min INT NOT NULL,
  end_min   INT NOT NULL,
  state     TINYINT NOT NULL COMMENT '0 正常 1 请假 2 调班',
  UNIQUE KEY uk_staff_date (staff_id, duty_date)
);

唯一键防止同一员工同一天被排两条班,state 字段让请假换班有记录可查。这张表是接单校验和业绩统计的共同数据源,全店每天的生活都压缩在这一行行里。

接单的派发逻辑我们做得克制:预约核销时由前台或店长手工指派给在岗的美甲师,不做自动派单。原因是美甲项目因技师手艺不同,客人也常有指定的技师,自动派单听起来酷但实际添乱。系统只做一件事:指派后在员工端推送一条提醒。

func assignStaff(bookID, staffID int) error {
    if !onDuty(staffID, bookingDate, bookingStartMin) {
        return errors.New("该员工当天不在班")
    }
    saveAssign(bookID, staffID)
    pushWorkNotice(staffID, "新接单:" + bookingDetail)
    return nil
}

onDuty 检查是接单的第一道闸门,所有派单请求都先过它。曾经有店长给休假员工派单成功,客人到店找不到人,从那以后这个校验从应用层挪到了数据库约束层。

派单成功的另一个动作是推送。推送内容带预约详情和核销码截图按钮,美甲师点开就能看到客人要做什么、有没有备注。我们故意把推送做得信息完整:门店每天最怕的就是美甲师到岗了才知道今天要做什么,提前看一眼能早做准备。

func pushWorkNotice(staffID int, text string) {
    msg := map[string]interface{}{
        "touser":  staffWxID(staffID),
        "msgtype": "text",
        "agentid": qyAgentID,
        "text":    map[string]string{"content": text},
    }
    postJSON(qySendURL+"?access_token="+qyToken(), msg)
}

企业微信的发送接口和公众号同源,token 管理复用同一套缓存,没花多少额外功夫。

这里还有一个取舍值得记录:推送发企微还是发短信?企微免费但只在上班时间被看到,短信必达但要钱。最终规则是工作消息一律企微,只有客人侧的关键消息才动用短信。通道的克制是另一种省钱。

配置:员工端形态

员工端没有做独立 App。美甲师平均年龄三十五岁,教她们装新 App 的成本高于教她们用企业微信。最终形态是企业微信自建应用:排班、接单提醒、个人业绩三个页面,H5 嵌进企微工作台。打开率比我们预期高得多——美甲师每天进店第一件事就是打卡顺便看排班。

功能 形态 更新频率
我的排班 H5 页面 实时
接单提醒 企微消息 即时
个人业绩 H5 图表 每日刷新
请假申请 企微审批 按需

企微审批流串起了请假和排班:美甲师提交请假,店长审批通过后打卡记录同步写进排班表,当天该员工的接单提醒自动摘除,时段容量提示同步给店长。一条审批三个系统联动,员工只用点一下。

早会上林姐指着企微说,以前请假要在群里发红包求人换班,现在系统一条申请全搞定。员工满意度的提升有时候不来自技术多先进,而来自一个流程让人有尊严。

踩坑:排班现场的四个坑

第一个坑是排班班次和时段对不齐。早班写 9 点到 16 点,但时段模板从 10 点开始排,结果早班员工有一小时闲着却显示在岗。后来规定排班粒度必须与时段模板同格(30 分钟),班次边界必须是格子边界。

第二个坑是调班没走系统。两个美甲师私下换班,系统里排班还是旧的,接单校验把新班次的人全挡在外面。修复方案是允许员工在企微里发起换班申请,对方确认后写排班表,私下换班成了不可能完成的任务。

换班上线第一个月发生过一次温馨的意外:一位美甲师想换班去接孩子放学,系统里发起申请后秒批。她后来跟林姐说,这是她在这家店六年第一次觉得请假不是件难事。系统的温度有时候就藏在一个按钮里。

同样务实的决定还有排班的批量操作。每周日晚店长要排下周的班,逐条加记录能把人点到手酸。我们做了模板班次:选定员工和周期,一键复制上周排班,再针对调班单独改。这一个功能给店长每周省下四十分钟,她原话是终于有时间好好喝杯茶了。

第三个坑是业绩统计口径。财务按核销算提成,美甲师看自己页面按接单算,两个数字对不上,每月发工资必吵一次。统一口径为以核销为准后,接单表里增加核销回写,两个页面一个真相:

SELECT staff_id, COUNT(*) AS done_cnt, SUM(price) AS revenue
FROM t_assign a JOIN t_booking b ON a.book_id = b.book_id
WHERE a.state = 1 AND b.status = 2
  AND b.verify_at BETWEEN ? AND ?
GROUP BY staff_id;

一条 SQL 就是工资单的底稿,财务和美甲师看到的是同一个数字。截止到上线当月,工资纠纷从平均每月三起到半年零起。

第四个坑是跨店支援的排班。A 店缺人时 B 店员工过去支援,排班表没有跨店记录,该员工在 A 店接单被拒。后来排班表加了 shop_duty 字段,一天可以在不同店各排一段,权限按排班店走而不是归属店。

跨店支援这块还有个尾巴:支援员工的业绩归支援店还是归属店,财务纠结了一周。最后的决定是业绩归支援店(客人服务和钱都在那边发生),归属店只看考勤。规则简单到不用解释,是它能活下来的原因。

小结

员工端的设计哲学是克制:不自动派单、不上复杂算法、不造新 App。把排班做准、把校验做硬、把入口做进员工已经在用的工具里。最高赞的功能不是任何炫技,而是我的排班页面让美甲师第一次能提前规划自己的生活——上两个月她们排班靠的是店长的心情。

一个数字结尾:系统上线半年,员工主动换班申请 240 次,零纠纷。技术最好的价值是让人不用吵架。

再补一组数字:排班与接单联动之后,门店早班的平均准备时间从 22 分钟压到 8 分钟——美甲师到岗前已经知道今天的第一单做什么、客人是谁。这 14 分钟全是客人少等的 14 分钟,也是林姐愿意续费服务器的 14 分钟。

延伸阅读