顾客选服务设计:到店 Kiosk(浏览 / 选卡项 / 下单)
状态:已实施(K1–K6)。
kiosk/端(浏览网格 / 下单 / 成功回执 / 空闲清篮)、服务端 kiosk 接口与订单管理均已落地并逐项实证;与候诊区展示屏(平板展示)是两台设备两个工程——展示屏只播不录,本机是顾客自助录入入口。
界面原型(选服务下单页,设计稿 · 数据为虚构演示):

定位与场景
kiosk/ 是摆在前台/大堂的顾客自助 iPad 网页:到店顾客在大屏上浏览服务项目,自己勾选想做的卡项,留手机号提交,前台在管理端接单。三步:
- 浏览:大字网格展示启用中的卡项(封面 + 名称 + 价格),触摸友好;
- 选服务:点选加入购物篮,可多选、可增删;
- 下单:填手机号(可填称呼)提交,屏幕回执单号与合计;前台在管理端「订单管理」看到新单并接待。
不做支付(无任何收款代码,下单即到店待接待);不做会员价/优惠计算(订单金额按下单时刻卡项价格快照)。
顾客(kiosk/ 大屏点选) ──► server kiosk 接口(浏览只读 + 下单)
│
前台(web dash 订单管理)◄── 新单状态流转(接单 → 完成 / 取消)数据模型(复用既有表,新增字段)
items — 卡项补三个字段
现有卡项表只有 name / price / description,Kiosk 浏览需要展示与上架控制,补:
| 字段 | 类型 | 含义 |
|---|---|---|
| enabled | Integer | 1 上架、0 下架(Kiosk 只展示上架项;存量行默认 1) |
| cover | String | 封面图相对 url(可空;复用现有媒体上传与 /media 托管) |
| sort | Integer | 展示顺序,小者在前 |
ddl-auto: update 自动加列,不影响存量数据。管理端卡项页(已有)补这三个字段的编辑。
orders — 状态语义定档
表已存在(user_id / item_id / staff_id / status / price),此前接口全是空壳。现在把状态定档并写死:
| status | 含义 |
|---|---|
| 0 | 已下单(Kiosk 提交后的初始态) |
| 1 | 已确认(前台接待中) |
| 2 | 已完成 |
| 9 | 已取消 |
user_id存顾客 id(customers表,业务外键);item_id存卡项 id;staff_id接单时可填(Kiosk 单初始为空);price是下单时刻的卡项价格快照(Integer,分为单位与卡项表一致——卡项 price 当前即按元存整数,如实沿用);- 一个卡项一条订单(多选 = 多条订单,同一次提交共享同一顾客),不加购物车表。
顾客关联
customers.phone 唯一。Kiosk 下单按手机号查顾客,查无则建档:name 取称呼输入(不填默认「到店客人」),phone 必填且要求 11 位手机号格式。
API 面(服务端新增 /api/v1/kiosk,免登录)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/kiosk/items | Kiosk 浏览:enabled=1 的卡项按 sort,name 升序,返回 id / name / price / cover / description(不含审计字段) |
| POST | /api/v1/kiosk/orders | Kiosk 下单:{ phone, name?, itemIds: [卡项id…] };校验手机号格式与卡项存在且上架;按手机号找/建顾客;逐项写订单(status=0、price=卡项现价);返回 { customerId, customerName, orders: [{ id, itemId, itemName, price }], totalFee } |
免登录边界(如实):Kiosk 是内网设备,浏览接口纯只读;下单接口是唯一写入口,写范围限定「一条顾客档案(按手机号幂等)+ N 条 status=0 订单」,不暴露任何查询/删除能力。如暴露公网需另加验证码或屏 token。
管理端订单接口:复用 OrderService(save/findById 已有),新增列表查询与状态流转,走需登录的 /api/v1/order 组:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/order/page | 分页(current、pageSize、status 可选),联出顾客姓名与卡项名 |
| PUT | /api/v1/order/status | { id, status },只允许 0→1→2 正向流转与 0/1→9 取消 |
| DELETE | /api/v1/order/{id} | 沿用既有删除(仅 9 已取消可删) |
现有 POST/PUT /api/v1/order/、GET /api/v1/order/{id} 等空壳接口一并替换为真实实现(OrderView 字段与实体对齐:customerId/itemId/staffId/status/price,去掉实体里不存在的 extOrderId/payType/payment)。
手机查卡(小程序「我的卡项」,2026-10-14 裁定)
隐私红线:不做裸手机号查卡——无身份绑定即拒绝。身份绑定两条路径,按后台配置 sinomed.phone-lookup.mode 三态开关(env PHONE_LOOKUP_MODE)选择,默认 off(关闭):
| mode | 行为 |
|---|---|
off | 默认。card-lookup、sms-code、openid-session、openid-bind、my-appointments 与 appointment-cancel 一律 400 拒绝,小程序页面透出「在线查卡已关闭 / 在线自助查询已关闭,请到店前台或致电门店查询」 |
openid_only | 登录态换 openid:小程序 wx.login → 服务端 POST /api/v1/kiosk/openid-session 经微信 jscode2session 换 openid(需 WECHAT_APP_ID/WECHAT_APP_SECRET,未配置 400 明确拒绝),按 customers.openid 查绑定顾客;缺 openid / 未绑定均拒绝,不回退手机号;sms-code 在此模式仅服务自助绑定(发码放行,查卡仍只走 openid);my-appointments / appointment-cancel 在此模式 400 拒绝 |
sms_verified | 手机号 + 短信验证码双因子:先 sms-code 发码,再带 smsCode 查卡(card-lookup)、查自己的待到店预约(my-appointments,跨设备)或取消自己的待到店预约(appointment-cancel,2026-10-16 代决);验证码 5 分钟有效、一次性,错/过期/已用均拒绝;错试计数,连续错满 5 次烧码(used=1,须重新取码);openid-session / openid-bind 在此模式 400 拒绝 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/v1/kiosk/sms-code | { phone };sms_verified 查卡、openid_only 绑定共用(off 拒绝),发 6 位码落 sms_codes(5 分钟有效、一次性)。本系统无短信网关,发码为服务端桩:码只落库与日志(SMS_CODE_DEMO phone=… code=…),不对外泄露 |
| POST | /api/v1/kiosk/card-lookup | { phone?, openid?, smsCode? };按 mode 走 openid 或双因子,命中顾客返回 { customerId, customerName, cards: [{ cardId, itemId, itemName, totalTimes, remainingTimes, status, validUntil }] }(新卡在前,含停用与过期卡由前端标态);未命中/被拒不返回任何卡项 |
| POST | /api/v1/kiosk/openid-bind | { openid, phone, smsCode };openid_only 模式专用(见下) |
openid 自助绑定(2026-10-15 代决,用户可推翻):绑定路径拍板为小程序自助——openid-bind 用一次性短信验证码自证手机号后,把登录 openid 绑到该手机号对应的顾客档案,免桌面免前台;与查卡 sms_verified 同一验证码通道、同一审计口径。同档案重复绑定幂等成功;openid 已绑其他档案 400 拒绝(不静默改绑,需前台处理)。此前 customers.openid 只能落库直写、无登记入口,此为唯一登记入口。激活仍缺微信侧凭据(正式 appid + AppSecret,同 openid-session「先建后激活」先例)。
审计:每次尝试(成功与拒绝)都落 card_lookup_logs 一条——查了谁的卡(phone / customer_id)、谁查的(operator:openid 路径=openid,短信路径=手机号,绑定路径=openid)、什么模式(mode)、结果(result:ok / disabled / missing_openid / no_binding / missing_code / bad_code / expired_code / used_code / no_customer / bind_ok / bind_conflict / missing_appointment / not_owner / not_cancelable / cancel_ok)、时间(create_time)。各路径同口径,off 模式的拒绝也留痕。
自助取消预约(2026-10-16 代决,用户可推翻):app-booking.md 拍板 2(2026-10-10)裁「顾客侧取消不做」,前提是「取消不核验身份,无法阻止恶意取消他人约」;该前提已因 2026-10-14/15 手机号+一次性验证码双因子与 openid 绑定落地而失效,故恢复顾客侧取消——POST /api/v1/kiosk/appointment-cancel({ phone, smsCode, appointmentId })核验身份后只允许取消本人 status=0 且未过期的预约(0→9),非本人/已受理/已过号/已取消/已过期/未知预约均 400 不静默;取消后该时段按「每时段限约 1 位」自动释放、当日频控随之解除(与 ensureSlotFree 的 status IN (0,1) 计数同一口径);与 my-appointments 同一套一次性验证码通道(查预约用过的码取消须重新取码)与审计口径。管理端取消(0→9)保留并行。
小程序侧(app/src/pages/card):手机号 + 验证码表单,服务端 400 的 errorMessage 原样透出;命中后列持卡与余次。weapp 登录换取 openid(仅存内存不落存储),openid 带上即可查卡;未绑定时填手机号+验证码点「绑定微信」走自助绑定(绑定成功直接以 openid 查一次卡);H5 无 wx.login 只走短信路径。
交互链路
Kiosk 首页(卡项网格,30s 无操作自动清篮回首页)
→ 点选卡项 → 底部购物篮(数量合并、可删)
→ 「去下单」→ 手机号 + 称呼(可选)→ 提交
→ 成功页(单号列表 + 合计金额)→ 10s 自动回首页
前台 dash「订单管理」:新单列表(状态角标)
→ 接单(0→1)→ 服务完成(1→2);未接待可取消(→9)目录落位
kiosk/ # monorepo 新增目录(与 tablet/ 平级,同一套 Vite + React + TS 约定)
├── src/
│ ├── net/api.ts # items 拉取与下单提交
│ ├── menu/ # 卡项网格与购物篮
│ └── App.tsx # 首页 / 下单 / 成功三态切换、空闲超时回首页
├── index.html # 触摸大字布局基准(768–1080 宽竖屏为主)
├── package.json # name: sinomed-kiosk(Node.js 24 / pnpm)
└── .nvmrc
web/src/pages/Order/ # dash:订单管理(替换现存的顾客列表复制品页面)
server/ …/controller/KioskController.java、OrderController.java(真实实现)部署落位:Nginx 增加 /kiosk/(kiosk/dist);卡项封面沿用 /media/ 托管,无需新增静态段。
todo 原子项
| # | 事项 | 验收 | 状态 |
|---|---|---|---|
| K1 | items 补 enabled / cover / sort 三字段并透出到 VO;dash 卡项页补编辑 | dash 可上架/下架、传封面、排序 | ✅ 存量行默认上架、表单三字段浏览器实证 |
| K2 | server KioskController:GET /kiosk/items + POST /kiosk/orders(免登录、手机号建档幂等) | curl 浏览只读、下单落库、重复手机号不重复建档 | ✅ 下架项排除、双单合计与同号归同顾客 curl 实证 |
| K3 | dash 订单管理页:分页列表(联顾客/卡项名)+ 状态流转,替换空壳 OrderController | kiosk 下的单在 dash 可见、可接单/完成/取消 | ✅ 7 项 curl + 浏览器接单流转实证 |
| K4 | kiosk/ 脚手架 + 卡项网格浏览页 | 浏览器打开即见上架卡项大字网格 | ✅ 竖屏实证:上架 2 项、下架排除、点选汇总 |
| K5 | kiosk 购物篮 + 下单 + 成功回执 + 空闲回首页 | 全流程触摸走通,重复手机号二单归同一顾客 | ✅ 手机号校验/回执/10s 自动返回/30s 清篮,二单 customerId 相同 |
| K6 | 端到端验收 + 文档同步(data-model / api / deploy / index) | 设计稿改已实施,docs build 过 | ✅ 文档四件套同步,docs build 过 |