本项目站内资源源代码下载地址
项目类型:Web 应用开发 技术栈:Next.js 16 + React 19 + TypeScript 5 + Tailwind CSS 4
1. 概述

1.1 简介
在奶茶行业竞争日益激烈的今天,一套高效、稳定、易用的收银系统已成为门店运营的核心基础设施。本项目正是为此而生——一套面向中小型奶茶店的 POS(Point of Sale)收银管理系统。
系统的定位非常明确:轻量级、可快速部署、支持触屏与键盘双模式操作。无论是独立门店还是小型连锁,都能在短时间内完成上线,覆盖从顾客点单、厨房制作、收银结账到数据统计的完整营业闭环。
注 传统桌面 POS 软件存在部署繁琐、跨平台困难、维护成本高等痛点。而基于浏览器的 Web 应用天然具备跨平台优势——无论是 Windows 收银机、iPad 平板,还是普通的笔记本电脑,只要打开浏览器就能使用。Next.js 的 App Router 模式更支持服务端渲染(SSR)与客户端渲染(CSR)的灵活切换,让首屏加载更快、交互更流畅。
1.2 技术架构概览
| 前端框架 | Next.js 16 (App Router) + React 19 + TypeScript 5 | 最新 App Router 模式,原生支持 SSR/CSR 混合渲染;React 19 带来更优的并发特性;TypeScript 5 提供强类型安全保障 |
| 样式方案 | Tailwind CSS 4 + shadcn/ui (New York style) | 暖色茶饮主题,内置明暗模式切换;shadcn/ui 提供 40+ 可复用组件,开发效率极高 |
| 状态管理 | Zustand | 轻量级、无样板代码,极适合购物车这类局部状态管理 |
| 数据库 | Prisma ORM + SQLite | 零配置、轻量级本地存储,12 个数据模型完全满足业务需求 |
| 图表 | Recharts | 7 天趋势、支付分布、时段分析等数据可视化 |
| 动画 | Framer Motion | 商品卡片入场、购物车增减、收据弹出等微交互动画 |
| 声音 | Web Audio API | 程序化合成音效,无需加载任何音频文件,响应极速 |
| AI 图片 | z-ai-web-dev-sdk image-generation | 按分类自动生成产品配图,告别千篇一律的占位图 |
1.3 系统特性
系统围绕奶茶店日常营业场景,精心设计了 7 大功能 Tab:
| 🧋 收银 | 点单、选规格、加料、支付 | 顾客到店,店员快速录入订单 |
| 📋 订单 | 订单列表、状态管理、取消/退款 | 店长查看今日营业订单,处理异常单 |
| 🍳 厨房 | 制作队列、超时预警、叫号 | 后厨师傅按顺序制作,前台实时同步 |
| 📊 统计 | 营收趋势、热销分析、支付分布 | 打烊后查看今日营业数据 |
| 👑 会员 | 会员注册、储值、积分、折扣 | 老顾客充值享优惠,提升复购率 |
| ⚙️ 管理 | 商品/分类/规格/加料/操作员/日志 | 新品上架、价格调整、员工管理 |
| 👤 交接班 | 班次切换、收入汇总、对账 | 早班/晚班交接,清晰责任边界 |
核心数据指标:30 个 RESTful API 端点、12 个数据库模型、13 项细粒度权限、6 种交互音效、21 款默认商品、8 种加料选项。

2. 系统架构
2.1 整体架构
系统采用经典的三层架构,各层职责边界清晰,便于独立演进与维护:
#mermaid-svg-kuRLb0mJzzDmDWrM{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kuRLb0mJzzDmDWrM .error-icon{fill:#552222;}#mermaid-svg-kuRLb0mJzzDmDWrM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kuRLb0mJzzDmDWrM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kuRLb0mJzzDmDWrM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kuRLb0mJzzDmDWrM .marker.cross{stroke:#333333;}#mermaid-svg-kuRLb0mJzzDmDWrM svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kuRLb0mJzzDmDWrM p{margin:0;}#mermaid-svg-kuRLb0mJzzDmDWrM .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-kuRLb0mJzzDmDWrM .cluster-label text{fill:#333;}#mermaid-svg-kuRLb0mJzzDmDWrM .cluster-label span{color:#333;}#mermaid-svg-kuRLb0mJzzDmDWrM .cluster-label span p{background-color:transparent;}#mermaid-svg-kuRLb0mJzzDmDWrM .label text,#mermaid-svg-kuRLb0mJzzDmDWrM span{fill:#333;color:#333;}#mermaid-svg-kuRLb0mJzzDmDWrM .node rect,#mermaid-svg-kuRLb0mJzzDmDWrM .node circle,#mermaid-svg-kuRLb0mJzzDmDWrM .node ellipse,#mermaid-svg-kuRLb0mJzzDmDWrM .node polygon,#mermaid-svg-kuRLb0mJzzDmDWrM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kuRLb0mJzzDmDWrM .rough-node .label text,#mermaid-svg-kuRLb0mJzzDmDWrM .node .label text,#mermaid-svg-kuRLb0mJzzDmDWrM .image-shape .label,#mermaid-svg-kuRLb0mJzzDmDWrM .icon-shape .label{text-anchor:middle;}#mermaid-svg-kuRLb0mJzzDmDWrM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kuRLb0mJzzDmDWrM .rough-node .label,#mermaid-svg-kuRLb0mJzzDmDWrM .node .label,#mermaid-svg-kuRLb0mJzzDmDWrM .image-shape .label,#mermaid-svg-kuRLb0mJzzDmDWrM .icon-shape .label{text-align:center;}#mermaid-svg-kuRLb0mJzzDmDWrM .node.clickable{cursor:pointer;}#mermaid-svg-kuRLb0mJzzDmDWrM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kuRLb0mJzzDmDWrM .arrowheadPath{fill:#333333;}#mermaid-svg-kuRLb0mJzzDmDWrM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kuRLb0mJzzDmDWrM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kuRLb0mJzzDmDWrM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kuRLb0mJzzDmDWrM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kuRLb0mJzzDmDWrM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kuRLb0mJzzDmDWrM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kuRLb0mJzzDmDWrM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kuRLb0mJzzDmDWrM .cluster text{fill:#333;}#mermaid-svg-kuRLb0mJzzDmDWrM .cluster span{color:#333;}#mermaid-svg-kuRLb0mJzzDmDWrM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kuRLb0mJzzDmDWrM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kuRLb0mJzzDmDWrM rect.text{fill:none;stroke-width:0;}#mermaid-svg-kuRLb0mJzzDmDWrM .icon-shape,#mermaid-svg-kuRLb0mJzzDmDWrM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kuRLb0mJzzDmDWrM .icon-shape p,#mermaid-svg-kuRLb0mJzzDmDWrM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kuRLb0mJzzDmDWrM .icon-shape .label rect,#mermaid-svg-kuRLb0mJzzDmDWrM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kuRLb0mJzzDmDWrM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kuRLb0mJzzDmDWrM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kuRLb0mJzzDmDWrM :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
💾 数据持久层
⚡ API 路由层
🖥️ 浏览器客户端层
HTTP /fetch
状态驱动 UI
程序化音效
SQL 查询
读写
Next.js App RouterReact 19 + TypeScript 5
Zustand 购物车状态
Web Audio API 音效
Framer Motion 动画
Recharts 数据图表
Next.js API Routes30 个 RESTful 端点
Prisma ORM 查询引擎
SQLite 数据库12 个数据模型
架构解读:
- 浏览器客户端层:负责所有用户交互与界面渲染。Next.js App Router 让页面路由与 API 路由共存于一个项目中,部署极为简便。Zustand 管理的购物车状态完全运行在客户端,即使网络短暂中断,点单操作也不会中断。
- API 路由层:30 个 RESTful API 端点按业务模块组织,通过 Prisma ORM 与数据库交互。Prisma 的 Schema 定义即代码,类型安全从数据库一直延伸到前端接口。
- 数据持久层:SQLite 作为嵌入式数据库,无需单独安装数据库服务,特别适合单门店部署。数据文件可直接备份为单个 .db 文件,迁移成本极低。
2.2 目录结构
📁 prisma/
└── schema.prisma # 数据库模型定义(12 个模型)
📁 src/
├── app/ # 页面路由 + API 路由(30 个端点)
│ ├── page.tsx # 主入口(POSClient)
│ ├── layout.tsx # 根布局(主题、字体、全局状态)
│ └── api/ # RESTful API 路由
│ ├── products/ # 商品管理
│ ├── orders/ # 订单管理
│ ├── members/ # 会员管理
│ ├── stats/ # 数据统计
│ ├── kitchen/ # 厨房显示
│ ├── shifts/ # 交接班
│ ├── operators/ # 操作员管理
│ ├── backup/ # 数据备份/恢复
│ └── …
│
├── components/
│ ├── pos/ # POSClient 主框架 + PosView 收银视图
│ ├── orders/ # 订单列表视图
│ ├── kitchen/ # 厨房显示视图
│ ├── dashboard/ # 数据统计视图
│ ├── members/ # 会员管理视图
│ ├── management/ # 后台管理视图
│ ├── shift/ # 交接班视图
│ └── ui/ # shadcn/ui 组件库(40+ 组件)
│
├── lib/ # 工具库
│ ├── db.ts # Prisma 数据库客户端
│ ├── cart-store.ts # Zustand 购物车状态
│ ├── sound.ts # Web Audio API 音效系统
│ └── seed.ts # 种子数据初始化
│
└── hooks/ # 自定义 Hooks
├── use-mobile.ts # 移动端设备检测
└── use-toast.ts # Toast 通知管理
📁 public/
└── images/ # AI 生成的产品图片
3. 数据库设计
3.1 核心实体关系
奶茶店业务的核心是商品 → 订单 → 支付这条主线。围绕这条主线,系统设计了 12 个数据模型。以下是核心实体关系图:
#mermaid-svg-pTxsqc910wfn886g{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pTxsqc910wfn886g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pTxsqc910wfn886g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pTxsqc910wfn886g .error-icon{fill:#552222;}#mermaid-svg-pTxsqc910wfn886g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pTxsqc910wfn886g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pTxsqc910wfn886g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pTxsqc910wfn886g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pTxsqc910wfn886g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pTxsqc910wfn886g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pTxsqc910wfn886g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pTxsqc910wfn886g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pTxsqc910wfn886g .marker.cross{stroke:#333333;}#mermaid-svg-pTxsqc910wfn886g svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pTxsqc910wfn886g p{margin:0;}#mermaid-svg-pTxsqc910wfn886g .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-pTxsqc910wfn886g .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-pTxsqc910wfn886g .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-pTxsqc910wfn886g .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-pTxsqc910wfn886g .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-pTxsqc910wfn886g .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-pTxsqc910wfn886g .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-pTxsqc910wfn886g .node rect,#mermaid-svg-pTxsqc910wfn886g .node circle,#mermaid-svg-pTxsqc910wfn886g .node ellipse,#mermaid-svg-pTxsqc910wfn886g .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pTxsqc910wfn886g .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-pTxsqc910wfn886g .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-pTxsqc910wfn886g .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pTxsqc910wfn886g .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-pTxsqc910wfn886g .edgeLabel .label text{fill:#333;}#mermaid-svg-pTxsqc910wfn886g :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
contains
has
contains
linked_via
ordered_as
contains
creates
belongs_to
works_in
independent
generates
Category
int
id
PK
string
name
string
icon
int
sortOrder
boolean
isEnabled
Product
int
id
PK
string
name
int
categoryId
FK
float
basePrice
string
barcode
string
imageUrl
boolean
isEnabled
int
sortOrder
ProductSpec
int
productId
PK,FK
int
specOptionId
PK,FK
SpecGroup
int
id
PK
string
name
boolean
isRequired
int
sortOrder
SpecOption
int
id
PK
int
groupId
FK
string
name
float
priceAdjust
int
sortOrder
OrderItem
int
id
PK
int
orderId
FK
int
productId
FK
string
productName
float
unitPrice
int
quantity
string
specs
string
addons
float
itemTotal
Order
int
id
PK
string
orderNo
UK
float
totalAmount
float
discountAmount
float
payAmount
string
payType
string
status
string
memberPhone
int
memberId
FK
string
remark
string
cancelReason
int
operatorId
FK
datetime
createdAt
datetime
completedAt
Operator
int
id
PK
string
name
string
role
string
pin
boolean
isActive
string
permissions
Member
int
id
PK
string
phone
UK
string
name
int
points
float
balance
float
totalSpent
float
totalTopup
float
discountRate
boolean
isEnabled
Shift
int
id
PK
int
operatorId
FK
datetime
startTime
datetime
endTime
float
cashAmount
float
onlineAmount
int
orderCount
string
note
Addon
int
id
PK
string
name
float
price
boolean
isEnabled
int
sortOrder
ActivityLog
int
id
PK
int
operatorId
string
action
string
detail
datetime
createdAt
关系解读:
- Category → Product:一对多。一个分类下有多款商品,如"经典奶茶"下有珍珠奶茶、芋泥奶茶等。
- Product ↔ SpecOption:多对多,通过 ProductSpec 关联表实现。这种设计非常灵活——同一规格选项(如"大杯")可以关联到多个商品,而每个商品也可以选择性地关联不同规格。
- SpecGroup → SpecOption:一对多。规格组是"糖度"、“冰度"这样的概念,选项是具体的"全糖”、"少冰"值。
- Order → OrderItem:一对多。订单主表记录支付信息、会员信息、操作员等;订单明细表记录每杯奶茶的规格、加料、小计金额。
- Order → Operator:多对一。明确每笔订单的责任人,便于交接班对账。
- Order → Member:多对零或一。非会员顾客无需关联会员,会员消费则记录手机号和会员 ID,支持余额支付和积分累积。
- Operator → Shift:一对多。记录每个操作员的班次历史,用于交接班时的收入汇总。
3.2 数据模型详细设计
3.2.1 Category(商品分类)
| id | Int | PK, AUTO_INCREMENT | 分类唯一标识 |
| name | String | NOT NULL | 分类名称,如"经典奶茶" |
| icon | String | DEFAULT ‘☕’ | 分类图标(Emoji),提升视觉识别度 |
| sortOrder | Int | DEFAULT 0 | 排序序号,控制分类在前端的显示顺序 |
| isEnabled | Boolean | DEFAULT true | 是否启用,停用后该分类商品不再显示 |
| createdAt | DateTime | DEFAULT now() | 创建时间 |
| updatedAt | DateTime | @updatedAt | 自动更新时间 |
设计思路:分类表是商品的组织骨架。通过 sortOrder 字段,运营人员可以自由调整分类在前端导航栏的排列顺序;isEnabled 则提供了软删除能力,避免直接删除分类导致历史订单数据断裂。
3.2.2 Product(商品)
| id | Int | PK, AUTO_INCREMENT | 商品唯一标识 |
| name | String | NOT NULL | 商品名称 |
| categoryId | Int | FK → Category.id | 所属分类 |
| basePrice | Float | NOT NULL | 基础价格(未加规格/加料的价格) |
| barcode | String? | – | 条形码,支持扫码枪快速录入 |
| imageUrl | String? | – | AI 生成的产品图片 URL |
| isEnabled | Boolean | DEFAULT true | 是否上架 |
| sortOrder | Int | DEFAULT 0 | 排序序号 |
| createdAt | DateTime | DEFAULT now() | 创建时间 |
| updatedAt | DateTime | @updatedAt | 更新时间 |
设计思路:basePrice 是商品的基准价,最终售价由 basePrice + 规格调价 + 加料价格 动态计算。barcode 字段为未来接入扫码枪预留扩展点。imageUrl 由 AI 按分类自动生成,确保每个分类都有视觉区分度。
3.2.3 SpecGroup(规格选项组)
| id | Int | PK, AUTO_INCREMENT | 规格组 ID |
| name | String | NOT NULL | 规格组名称,如"糖度"、“冰度”、“杯型” |
| isRequired | Boolean | DEFAULT true | 是否必选,如糖度通常必选,备注通常可选 |
| sortOrder | Int | DEFAULT 0 | 排序序号 |
设计思路:将规格抽象为"组"和"选项"两层,是应对奶茶店复杂规格需求的关键。例如"杯型"是一个组,“中杯/大杯/超大杯"是组内的选项。isRequired 字段让系统可以灵活支持"必选规格”(如糖度)和"可选规格"(如额外加料提示)。
3.2.4 SpecOption(规格选项值)
| id | Int | PK, AUTO_INCREMENT | 选项 ID |
| groupId | Int | FK → SpecGroup.id | 所属规格组 |
| name | String | NOT NULL | 选项名称,如"全糖"、“少冰”、“大杯” |
| priceAdjust | Float | DEFAULT 0 | 价格调整值,如大杯比中杯贵 3 元 |
| sortOrder | Int | DEFAULT 0 | 排序序号 |
设计思路:priceAdjust 是规格定价的核心机制。它表示相对于基础价格的增量,而非绝对价格。例如中杯 priceAdjust = 0,大杯 priceAdjust = 3,超大杯 priceAdjust = 5。这种设计让价格调整更直观,也避免了基础价格变动时逐个修改规格价格的麻烦。
3.2.5 ProductSpec(商品-规格关联)
| productId | Int | FK → Product.id, PK | 商品 ID(联合主键) |
| specOptionId | Int | FK → SpecOption.id, PK | 规格选项 ID(联合主键) |
设计思路:这是一张经典的关联表(Junction Table),将 Product 与 SpecOption 的多对多关系拆解为两张一对多关系。联合主键确保同一商品不会重复关联同一规格选项。这种设计的最大优势是灵活性——不同商品可以关联完全不同的规格组合。例如咖啡系列不需要"糖度"规格,而奶茶系列需要;季节限定商品可以拥有独特的"温度"规格。
3.2.6 Addon(加料)
| id | Int | PK, AUTO_INCREMENT | 加料 ID |
| name | String | NOT NULL | 加料名称,如"珍珠"、“布丁” |
| price | Float | NOT NULL | 加料价格 |
| isEnabled | Boolean | DEFAULT true | 是否启用 |
| sortOrder | Int | DEFAULT 0 | 排序序号 |
设计思路:加料是奶茶店提升客单价的重要手段。加料表独立存在,不与商品直接关联——这意味着任何商品都可以加任何料,运营人员无需为每种商品单独配置加料选项。price 是绝对价格,因为加料成本相对固定,不受商品基础价格影响。
3.2.7 Operator(操作员)
| id | Int | PK, AUTO_INCREMENT | 操作员 ID |
| name | String | NOT NULL | 姓名 |
| role | String | DEFAULT ‘cashier’ | 角色:admin(管理员)/ cashier(收银员) |
| pin | String | NOT NULL | 6 位数字 PIN 码,用于登录认证 |
| isActive | Boolean | DEFAULT true | 是否在职 |
| permissions | String | DEFAULT ‘…’ | 权限列表,逗号分隔的 13 项权限 |
| createdAt | DateTime | DEFAULT now() | 创建时间 |
权限枚举(13 项):
| 订单权限 | order:view | 查看订单 |
| order:create | 创建订单 | |
| order:cancel | 取消订单 | |
| order:refund | 退款处理 | |
| 会员权限 | member:view | 查看会员 |
| member:edit | 编辑会员 | |
| member:topup | 会员充值 | |
| 统计权限 | stats:view | 查看统计 |
| stats:export | 导出数据 | |
| 商品权限 | product:view | 查看商品 |
| product:edit | 编辑商品 | |
| 班次权限 | shift:view | 查看班次 |
| shift:manage | 管理交接班 |
设计思路:采用 RBAC(Role-Based Access Control) 的简化版。角色(admin/cashier)仅作为默认权限的快捷配置,真正的权限校验以 permissions 字段为准。这种设计兼顾了易用性(新员工直接选角色)和灵活性(老员工可以单独调整某项权限)。
3.2.8 Order(订单主表)
| id | Int | PK, AUTO_INCREMENT | 订单 ID |
| orderNo | String | UNIQUE | 订单号,20 位字符串 |
| totalAmount | Float | NOT NULL | 订单总金额(优惠前) |
| discountAmount | Float | DEFAULT 0 | 优惠金额(会员折扣、满减等) |
| payAmount | Float | NOT NULL | 实付金额 |
| payType | String | NOT NULL | 支付方式:Cash / WeChat / Alipay / Member |
| status | String | NOT NULL | 状态:Pending / Making / Completed / Cancelled |
| memberPhone | String? | – | 会员手机号(冗余字段,加速查询) |
| memberId | Int? | FK → Member.id | 会员 ID |
| remark | String? | – | 订单备注(如"少放糖"、“打包”) |
| cancelReason | String? | – | 取消原因 |
| operatorId | Int | FK → Operator.id | 操作员 ID |
| createdAt | DateTime | DEFAULT now() | 创建时间 |
| completedAt | DateTime? | – | 完成时间(制作完成或取消时填充) |
订单号生成规则:YYYYMMDDHHmmss + 4 位随机序号
例如:202603151430250001 表示 2026 年 3 月 15 日 14:30:25 的第 1 笔订单。
设计思路:订单表是系统的核心流水表。totalAmount 与 payAmount 分离,是为了清晰记录优惠轨迹;memberPhone 作为冗余字段,避免了查询订单时频繁 JOIN 会员表;status 字段驱动厨房显示系统的状态流转。
3.2.9 OrderItem(订单明细)
| id | Int | PK, AUTO_INCREMENT | 明细 ID |
| orderId | Int | FK → Order.id, CASCADE | 所属订单(级联删除) |
| productId | Int | FK → Product.id | 商品 ID |
| productName | String | NOT NULL | 商品名称(快照,防止商品改名后历史订单混乱) |
| unitPrice | Float | NOT NULL | 单价(含规格调价 + 加料) |
| quantity | Int | NOT NULL | 数量 |
| specs | String | DEFAULT ‘{}’ | 规格 JSON |
| addons | String | DEFAULT ‘[]’ | 加料 JSON |
| itemTotal | Float | NOT NULL | 小计金额 |
specs JSON 示例:
{
"糖度": "少糖",
"冰度": "去冰",
"杯型": "大杯"
}
addons JSON 示例:
[
{ "name": "珍珠", "price": 2 },
{ "name": "布丁", "price": 3 }
]
设计思路:productName 采用快照模式——记录下单时的商品名称,而非关联到 Product 表的实时名称。这确保了即使商品后来改名,历史订单小票上仍然显示当时的名称。specs 和 addons 使用 JSON 字符串存储,是因为规格和加料的结构灵活多变,关系型表的严格结构反而会成为束缚。
3.2.10 Shift(交接班记录)
| id | Int | PK, AUTO_INCREMENT | 班次 ID |
| operatorId | Int | FK → Operator.id | 操作员 ID |
| startTime | DateTime | NOT NULL | 开班时间 |
| endTime | DateTime? | – | 交班时间(NULL 表示当前班次未结束) |
| cashAmount | Float | DEFAULT 0 | 现金收入汇总 |
| onlineAmount | Float | DEFAULT 0 | 线上收入汇总(微信/支付宝/会员余额) |
| orderCount | Int | DEFAULT 0 | 订单数汇总 |
| note | String? | – | 备注 |
设计思路:交接班是奶茶店每日运营的关键节点。cashAmount 与 onlineAmount 分离,便于收银员与现金实物对账;endTime 为 NULL 表示这是当前正在进行的班次,系统通过此字段快速定位"当前班次"。
3.2.11 Member(会员)
| id | Int | PK, AUTO_INCREMENT | 会员 ID |
| phone | String | UNIQUE | 手机号(11 位,唯一标识) |
| name | String? | – | 姓名 |
| points | Int | DEFAULT 0 | 积分(可用于兑换或抵现) |
| balance | Float | DEFAULT 0 | 储值余额 |
| totalSpent | Float | DEFAULT 0 | 累计消费金额 |
| totalTopup | Float | DEFAULT 0 | 累计充值金额 |
| discountRate | Float | DEFAULT 1.0 | 折扣率:1.0 = 原价,0.9 = 9 折 |
| isEnabled | Boolean | DEFAULT true | 是否启用 |
| createdAt | DateTime | DEFAULT now() | 创建时间 |
| updatedAt | DateTime | @updatedAt | 更新时间 |
设计思路:会员系统采用手机号即会员号的极简设计,顾客无需携带实体卡,报手机号即可识别。discountRate 字段让不同等级会员享受不同折扣(如普通会员原价、金卡会员 9 折)。totalSpent 和 totalTopup 为运营分析提供数据基础。
3.2.12 ActivityLog(操作日志)
| id | Int | PK, AUTO_INCREMENT | 日志 ID |
| operatorId | Int | NOT NULL | 操作员 ID |
| action | String | NOT NULL | 操作类型 |
| detail | String? | – | 操作详情 |
| createdAt | DateTime | DEFAULT now() | 创建时间 |
action 枚举值:CreateOrder、CancelOrder、Refund、UpdateOrderStatus、ShiftStart、ShiftEnd、Login、Logout
设计思路:操作日志是安全审计的底线。记录操作员 ID 而非姓名(防止改名后无法追溯),记录操作类型便于后续按类型筛选。detail 字段存储 JSON 格式的操作详情,如取消订单时记录订单号和取消原因。
4. API 接口设计
系统共提供 30 个 RESTful API 端点,按业务模块划分为 11 个分组。所有 API 基于 Next.js API Routes 实现,通过 Prisma ORM 访问数据库。
4.1 商品管理
| GET | /api/products | 获取商品列表,?all=true 包含已停用商品 |
| POST | /api/products | 创建商品 |
| PUT | /api/products/[id] | 更新商品 |
| DELETE | /api/products/[id] | 删除商品 |
4.2 分类管理
| GET | /api/categories | 获取分类列表 |
| POST | /api/categories | 创建分类 |
| PUT | /api/categories/[id] | 更新分类 |
| DELETE | /api/categories/[id] | 删除分类 |
4.3 规格管理
| GET | /api/specs | 获取规格组列表(含嵌套选项) |
| POST | /api/specs | 创建规格组 |
| PUT | /api/specs/[id] | 更新规格组 |
| DELETE | /api/specs/[id] | 删除规格组 |
| POST | /api/specs/options | 创建规格选项 |
| PUT | /api/specs/options/[id] | 更新规格选项 |
| DELETE | /api/specs/options/[id] | 删除规格选项 |
4.4 加料管理
| GET | /api/addons | 获取加料列表 |
| POST | /api/addons | 创建加料 |
| PUT | /api/addons/[id] | 更新加料 |
| DELETE | /api/addons/[id] | 删除加料 |
4.5 订单管理
| GET | /api/orders | 获取订单列表,支持分页、状态过滤、日期范围 |
| POST | /api/orders | 创建订单(核心交易接口) |
| PATCH | /api/orders/[id]/status | 更新订单状态 |
订单状态流转:
#mermaid-svg-P7Hc7q9zATXhL7gi{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P7Hc7q9zATXhL7gi .error-icon{fill:#552222;}#mermaid-svg-P7Hc7q9zATXhL7gi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P7Hc7q9zATXhL7gi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P7Hc7q9zATXhL7gi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P7Hc7q9zATXhL7gi .marker.cross{stroke:#333333;}#mermaid-svg-P7Hc7q9zATXhL7gi svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P7Hc7q9zATXhL7gi p{margin:0;}#mermaid-svg-P7Hc7q9zATXhL7gi defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-P7Hc7q9zATXhL7gi g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-P7Hc7q9zATXhL7gi g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-P7Hc7q9zATXhL7gi g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-P7Hc7q9zATXhL7gi g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-P7Hc7q9zATXhL7gi g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-P7Hc7q9zATXhL7gi .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-P7Hc7q9zATXhL7gi .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-P7Hc7q9zATXhL7gi .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-P7Hc7q9zATXhL7gi .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-P7Hc7q9zATXhL7gi .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-P7Hc7q9zATXhL7gi .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-P7Hc7q9zATXhL7gi .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-P7Hc7q9zATXhL7gi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P7Hc7q9zATXhL7gi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P7Hc7q9zATXhL7gi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P7Hc7q9zATXhL7gi .edgeLabel .label text{fill:#333;}#mermaid-svg-P7Hc7q9zATXhL7gi .label div .edgeLabel{color:#333;}#mermaid-svg-P7Hc7q9zATXhL7gi .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-P7Hc7q9zATXhL7gi .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-P7Hc7q9zATXhL7gi .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-P7Hc7q9zATXhL7gi .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-P7Hc7q9zATXhL7gi .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-P7Hc7q9zATXhL7gi .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P7Hc7q9zATXhL7gi .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P7Hc7q9zATXhL7gi #statediagram-barbEnd{fill:#333333;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P7Hc7q9zATXhL7gi .cluster-label,#mermaid-svg-P7Hc7q9zATXhL7gi .nodeLabel{color:#131300;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-P7Hc7q9zATXhL7gi .note-edge{stroke-dasharray:5;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-note text{fill:black;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram-note .nodeLabel{color:black;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagram .edgeLabel{color:red;}#mermaid-svg-P7Hc7q9zATXhL7gi #dependencyStart,#mermaid-svg-P7Hc7q9zATXhL7gi #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-P7Hc7q9zATXhL7gi .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P7Hc7q9zATXhL7gi :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
创建订单
开始制作
制作完成
取消订单
取消订单
Pending
Making
Completed
Cancelled
待制作状态厨房显示左侧队列
制作中状态厨房显示右侧队列
需填写取消原因记录操作日志
4.6 厨房显示
| GET | /api/kitchen | 获取待制作(Pending)和制作中(Making)订单 |
4.7 数据统计
| GET | /api/stats | 获取统计数据:今日订单/收入/环比、支付分布、周趋势、时段分析、热销 TOP5、会员统计 |
4.8 会员管理
| GET | /api/members | 获取会员列表,?search 模糊搜索,?phone 精确查询 |
| GET | /api/members/[id] | 获取会员详情 |
| POST | /api/members | 创建会员 |
| PUT | /api/members/[id] | 更新会员 |
| DELETE | /api/members/[id] | 删除会员 |
| GET | /api/members/lookup | 按手机号快速查询(收银台会员支付时使用) |
| POST | /api/members/[id]/topup | 会员充值(余额或积分) |
4.9 操作员管理
| GET | /api/operators | 获取操作员列表 |
| POST | /api/operators | 创建操作员 |
| PUT | /api/operators/[id] | 更新操作员 |
| DELETE | /api/operators/[id] | 删除操作员 |
| PATCH | /api/operators/[id]/permissions | 更新权限 |
4.10 交接班
| GET | /api/shifts | 获取班次列表,?current=true 获取当前班次 |
| POST | /api/shifts | 交接班(结束当前班次 + 创建新班次) |
4.11 系统功能
| GET | /api/backup | 下载数据备份(JSON 格式) |
| POST | /api/backup | 恢复数据(清空 + 重导) |
| GET | /api/export | CSV 导出,?type=orders/stats |
| GET | /api/activity-logs | 操作日志查询,?page&limit&action |
| POST | /api/images/generate | 生成 AI 产品图片,?category |
5. 前端模块设计
5.1 POSClient(主框架)
POSClient 是整个系统的"总指挥",负责 Tab 导航、全局状态栏、键盘快捷键监听和操作员登录态管理。它就像一家奶茶店的"前台经理"——协调各个功能区域,确保信息在正确的时间出现在正确的位置。
核心状态:
| activeTab | String | 当前激活的 Tab,默认 ‘pos’ |
| isLoggedIn | Boolean | 操作员登录状态 |
| soundEnabled | Boolean | 声音开关,持久化到 localStorage |
| currentTime | DateTime | 实时时钟(每秒更新) |
| searchFocusTrigger | Number | 搜索框聚焦触发器(按 F9 或 / 时递增) |
| shortcutHelpOpen | Boolean | 快捷键帮助面板开关 |
Tab 配置:
| pos | 🧋 | 收银 | F1 | PosView |
| orders | 📋 | 订单 | F2 | OrdersView |
| kitchen | 🍳 | 厨房 | F3 | KitchenDisplay |
| dashboard | 📊 | 统计 | F4 | DashboardView |
| members | 👑 | 会员 | F5 | MemberView |
| management | ⚙️ | 管理 | F6 | ManagementView |
| shift | 👤 | 交接班 | F7 | ShiftView |
快捷键映射:
| F1 ~ F7 | 切换对应 Tab |
| F9 / / | 聚焦搜索框(收银台快速找商品) |
| ? | 打开快捷键帮助面板 |
| Esc | 关闭对话框 / 帮助面板 |
状态栏信息:操作员状态、班次开始时间、今日订单数、数据库状态、声音开关、实时时钟、登入/登出按钮。
5.2 PosView(收银视图)
PosView 是店员每天打交道最多的界面,采用经典的三栏布局:
#mermaid-svg-K3p64l2dZPxEKVv0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K3p64l2dZPxEKVv0 .error-icon{fill:#552222;}#mermaid-svg-K3p64l2dZPxEKVv0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K3p64l2dZPxEKVv0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K3p64l2dZPxEKVv0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K3p64l2dZPxEKVv0 .marker.cross{stroke:#333333;}#mermaid-svg-K3p64l2dZPxEKVv0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K3p64l2dZPxEKVv0 p{margin:0;}#mermaid-svg-K3p64l2dZPxEKVv0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-K3p64l2dZPxEKVv0 .cluster-label text{fill:#333;}#mermaid-svg-K3p64l2dZPxEKVv0 .cluster-label span{color:#333;}#mermaid-svg-K3p64l2dZPxEKVv0 .cluster-label span p{background-color:transparent;}#mermaid-svg-K3p64l2dZPxEKVv0 .label text,#mermaid-svg-K3p64l2dZPxEKVv0 span{fill:#333;color:#333;}#mermaid-svg-K3p64l2dZPxEKVv0 .node rect,#mermaid-svg-K3p64l2dZPxEKVv0 .node circle,#mermaid-svg-K3p64l2dZPxEKVv0 .node ellipse,#mermaid-svg-K3p64l2dZPxEKVv0 .node polygon,#mermaid-svg-K3p64l2dZPxEKVv0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K3p64l2dZPxEKVv0 .rough-node .label text,#mermaid-svg-K3p64l2dZPxEKVv0 .node .label text,#mermaid-svg-K3p64l2dZPxEKVv0 .image-shape .label,#mermaid-svg-K3p64l2dZPxEKVv0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-K3p64l2dZPxEKVv0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-K3p64l2dZPxEKVv0 .rough-node .label,#mermaid-svg-K3p64l2dZPxEKVv0 .node .label,#mermaid-svg-K3p64l2dZPxEKVv0 .image-shape .label,#mermaid-svg-K3p64l2dZPxEKVv0 .icon-shape .label{text-align:center;}#mermaid-svg-K3p64l2dZPxEKVv0 .node.clickable{cursor:pointer;}#mermaid-svg-K3p64l2dZPxEKVv0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-K3p64l2dZPxEKVv0 .arrowheadPath{fill:#333333;}#mermaid-svg-K3p64l2dZPxEKVv0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-K3p64l2dZPxEKVv0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-K3p64l2dZPxEKVv0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K3p64l2dZPxEKVv0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-K3p64l2dZPxEKVv0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K3p64l2dZPxEKVv0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-K3p64l2dZPxEKVv0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-K3p64l2dZPxEKVv0 .cluster text{fill:#333;}#mermaid-svg-K3p64l2dZPxEKVv0 .cluster span{color:#333;}#mermaid-svg-K3p64l2dZPxEKVv0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-K3p64l2dZPxEKVv0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-K3p64l2dZPxEKVv0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-K3p64l2dZPxEKVv0 .icon-shape,#mermaid-svg-K3p64l2dZPxEKVv0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K3p64l2dZPxEKVv0 .icon-shape p,#mermaid-svg-K3p64l2dZPxEKVv0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-K3p64l2dZPxEKVv0 .icon-shape .label rect,#mermaid-svg-K3p64l2dZPxEKVv0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K3p64l2dZPxEKVv0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-K3p64l2dZPxEKVv0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-K3p64l2dZPxEKVv0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
🛒 购物车区
🛍️ 商品网格区
📑 分类导航栏
点击筛选
点击加入
经典奶茶 🧋
果茶系列 🍵
冰沙系列 🧊
咖啡系列 ☕
小食点心 🍰
季节限定 🌸
珍珠奶茶 ¥15
芋泥奶茶 ¥18
黑糖鹿丸 ¥19
…
珍珠奶茶 大杯 少糖
+ 珍珠 + 布丁
¥21 × 1 = ¥21
—
总计: ¥47
去结算
左栏:6 个商品分类垂直排列,点击后中栏自动筛选对应商品。分类图标使用 Emoji,在触屏设备上具有极佳的识别度。
中栏:商品以卡片网格形式展示。每张卡片采用与分类对应的渐变色背景(见下方映射表),热门商品(basePrice ≥ ¥25)会显示"🔥 热门"徽章。
右栏:购物车实时展示已选商品,支持数量增减、删除、清空,底部显示总计金额和结算按钮。
子组件:
- SpecSelectionDialog:规格选择对话框。当商品关联了规格时,点击商品卡片会弹出此对话框。内部使用 RadioGroup 单选必选规格(如糖度、冰度),Checkbox 多选加料,配合数量步进器。
- PaymentDialog:支付对话框。支持 4 种支付方式(现金、微信、支付宝、会员余额),会员支付时可通过手机号查询会员信息并自动应用折扣,支持输入优惠金额和订单备注。
- ReceiptPreviewDialog:小票预览对话框。模拟热敏小票样式,显示门店信息、订单明细、支付信息,支持打印功能(调用浏览器打印 API)。
5.2.1 购物车状态(Zustand)
购物车状态是收银台的核心,采用 Zustand 管理:
interface CartItem {
id: string; // crypto.randomUUID() 生成
productId: number;
productName: string;
basePrice: number;
quantity: number;
specs: Record<string, string>; // { "糖度": "少糖", "杯型": "大杯" }
specPriceAdjust: number; // 规格调价总和
addons: { name: string; price: number }[];
unitPrice: number; // 计算后的单价
itemTotal: number; // 小计
}
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
incrementQuantity: (id: string) => void;
decrementQuantity: (id: string) => void;
clearCart: () => void;
totalAmount: number; // 计算属性:所有 itemTotal 之和
itemCount: number; // 计算属性:所有 quantity 之和
}
为什么用 Zustand 而非 React Context? 购物车状态更新极其频繁(加减数量、增删商品),Context 的渲染传播机制会导致大量不必要的重渲染。Zustand 采用订阅模式,只有真正依赖某个状态的组件才会更新,性能更优。同时 Zustand 的代码量极少,无需编写繁琐的 Provider 和 Reducer。
5.2.2 价格计算逻辑
价格计算遵循清晰的三层叠加模型:
unitPrice = basePrice + specPriceAdjust + Σ(addon.price)
itemTotal = unitPrice × quantity
totalAmount = Σ(item.itemTotal)
payAmount = max(0, totalAmount – discountAmount)
计算示例:一杯珍珠奶茶(basePrice ¥15),选大杯(specPriceAdjust +¥3),加珍珠(+¥2)和布丁(+¥3),数量 2 杯:
unitPrice = 15 + 3 + (2 + 3) = 23 元
itemTotal = 23 × 2 = 46 元
如果会员享受 9 折优惠(discountAmount = 46 × 0.1 = 4.6):
payAmount = max(0, 46 – 4.6) = 41.4 元
5.2.3 分类渐变色映射
每个分类拥有独特的渐变色背景,帮助店员在视觉上快速区分商品类别:
| 1 | 经典奶茶 | amber-50 → orange-50(温暖的琥珀色) |
| 2 | 果茶系列 | green-50 → emerald-50(清新的果绿色) |
| 3 | 冰沙系列 | yellow-50 → amber-50(明亮的冰沙黄) |
| 4 | 咖啡系列 | sky-50 → cyan-50(沉稳的咖啡蓝) |
| 5 | 小食点心 | pink-50 → rose-50(甜美的点心粉) |
| 6 | 季节限定 | purple-50 → violet-50(神秘的限定紫) |
5.3 OrdersView(订单视图)
OrdersView 是店长的"运营仪表盘",提供订单全生命周期的管理功能:
- 状态过滤:全部 / 待制作 / 制作中 / 已完成 / 已取消
- 日期范围:快速筛选今日、昨日、近 7 天、自定义范围
- 分页浏览:每页 20 条,大数据量下保持流畅
- 订单详情展开:Collapsible 折叠面板,点击展开查看明细
- 状态流转:Pending → Making → Completed,支持取消(需填写原因)
- 取消原因预设:顾客取消 / 商品售罄 / 操作错误 / 其他
- 自动刷新:每 30 秒自动拉取最新订单
- CSV 导出:一键导出当前筛选结果
PosView
SQLite
KitchenDisplay
OrdersView
PosView
PosView
SQLite
KitchenDisplay
OrdersView
PosView
#mermaid-svg-7VHsEInSMRIaamAJ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7VHsEInSMRIaamAJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7VHsEInSMRIaamAJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7VHsEInSMRIaamAJ .error-icon{fill:#552222;}#mermaid-svg-7VHsEInSMRIaamAJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7VHsEInSMRIaamAJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7VHsEInSMRIaamAJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7VHsEInSMRIaamAJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7VHsEInSMRIaamAJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7VHsEInSMRIaamAJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7VHsEInSMRIaamAJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7VHsEInSMRIaamAJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7VHsEInSMRIaamAJ .marker.cross{stroke:#333333;}#mermaid-svg-7VHsEInSMRIaamAJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7VHsEInSMRIaamAJ p{margin:0;}#mermaid-svg-7VHsEInSMRIaamAJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7VHsEInSMRIaamAJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7VHsEInSMRIaamAJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7VHsEInSMRIaamAJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7VHsEInSMRIaamAJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7VHsEInSMRIaamAJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7VHsEInSMRIaamAJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7VHsEInSMRIaamAJ .sequenceNumber{fill:white;}#mermaid-svg-7VHsEInSMRIaamAJ #sequencenumber{fill:#333;}#mermaid-svg-7VHsEInSMRIaamAJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7VHsEInSMRIaamAJ .messageText{fill:#333;stroke:none;}#mermaid-svg-7VHsEInSMRIaamAJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7VHsEInSMRIaamAJ .labelText,#mermaid-svg-7VHsEInSMRIaamAJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7VHsEInSMRIaamAJ .loopText,#mermaid-svg-7VHsEInSMRIaamAJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7VHsEInSMRIaamAJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7VHsEInSMRIaamAJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7VHsEInSMRIaamAJ .noteText,#mermaid-svg-7VHsEInSMRIaamAJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7VHsEInSMRIaamAJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7VHsEInSMRIaamAJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7VHsEInSMRIaamAJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7VHsEInSMRIaamAJ .actorPopupMenu{position:absolute;}#mermaid-svg-7VHsEInSMRIaamAJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7VHsEInSMRIaamAJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7VHsEInSMRIaamAJ .actor-man circle,#mermaid-svg-7VHsEInSMRIaamAJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7VHsEInSMRIaamAJ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
店员
录入订单并支付
POST /api/orders
新订单推送
新订单提醒
PATCH /api/orders/[id]/status → Making
制作完成
PATCH /api/orders/[id]/status → Completed
订单状态更新
店员
5.4 KitchenDisplay(厨房显示)
KitchenDisplay 是后厨的"作战指挥屏",采用全屏深色主题(gray-900 背景 + 白色文字),在明亮的厨房环境中依然清晰可见。
布局:左右分栏
- 左侧:待制作订单(Pending),黄色标记,按下单时间排序
- 右侧:制作中订单(Making),蓝色标记,显示预计剩余时间
核心特性:
- 超时预警:等待时间 > 5 分钟的订单显示红色脉冲边框,提醒优先处理
- 新订单音效:880Hz 双声调哔声,即使厨师背对屏幕也能感知到新单
- 自动刷新:每 10 秒拉取最新数据
- 全屏切换:F11 或点击按钮进入全屏,最大化利用屏幕空间
- 独立声音控制:可单独开关厨房提示音,不影响收银台音效
5.5 DashboardView(统计视图)
DashboardView 将枯燥的数字转化为直观的图表,帮助店长快速把握经营脉搏。
统计卡片(顶部):
| 今日订单 | 今日非取消订单数 | vs 昨日同时段 |
| 今日收入 | 今日非取消订单 payAmount 之和 | vs 昨日同时段 |
| 客单价 | 今日收入 ÷ 今日订单 | — |
| 活跃会员 | isEnabled = true 的会员总数 | — |
图表区域:
- 7 日营收趋势:BarChart,展示近 7 天每日收入走势
- 支付方式分布:PieChart 环形图,现金/微信/支付宝/会员余额占比
- 今日时段分析:BarChart(按小时),识别营业高峰时段
- 今日热销 TOP5:进度条 + 奖牌(🥇🥈🥉),直观展示爆款商品
自动刷新:每 60 秒自动更新数据,店长可以一直开着这个页面作为"营业看板"。
5.6 MemberView(会员视图)
MemberView 提供双视图模式,适应不同使用场景:
- 会员列表模式:表格展示所有会员,支持搜索、CRUD、充值操作
- 快速查询模式:手机号输入框 → 即时查询 → 一键跳转收银台(带会员信息)
会员统计卡片:总会员数 / 活跃会员 / 总积分 / 总余额 / 总充值
折扣率选项:原价(1.0)/ 95 折 / 9 折 / 85 折 / 8 折
充值类型:
- 积分充值:整数输入,用于积分兑换活动
- 余额充值:浮点数输入,直接存入储值余额,可用于支付订单
5.7 ManagementView(管理视图)
ManagementView 是后台管理的"控制中枢",包含 7 个子 Tab:
| 商品管理 | 商品表格 + 新增/编辑对话框(含规格关联选择) |
| 分类管理 | 分类表格 + 图标选择器 + 排序调整 |
| 规格管理 | 手风琴展开式(Accordion)+ 规格组/选项嵌套 CRUD |
| 加料管理 | 加料表格 + 价格编辑 |
| 操作日志 | 分页表格 + 操作类型过滤(8 种类型) |
| 数据备份 | 下载备份 JSON + 上传恢复 + 重置数据(三键操作) |
| 操作员管理 | 操作员表格 + 新增/编辑对话框 + 权限复选框分组 |
权限复选框分组展示:
| 订单权限 | view, create, cancel, refund |
| 会员权限 | view, edit, topup |
| 统计权限 | view, export |
| 商品权限 | view, edit |
| 班次权限 | view, manage |
默认权限:
- 管理员:全部 13 项权限
- 收银员:5 项基本权限(order:view、order:create、member:view、stats:view、shift:view)
5.8 ShiftView(交接班视图)
ShiftView 处理班次切换的完整流程,确保责任清晰、账目清楚。
当前班次信息:
- 操作员姓名
- 开班时间
- 运行时长(实时计算)
- 本班次收入统计(现金 / 线上 / 订单数)
交接班流程:
#mermaid-svg-Sib6lKjksTROGxv0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sib6lKjksTROGxv0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sib6lKjksTROGxv0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sib6lKjksTROGxv0 .error-icon{fill:#552222;}#mermaid-svg-Sib6lKjksTROGxv0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sib6lKjksTROGxv0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sib6lKjksTROGxv0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sib6lKjksTROGxv0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sib6lKjksTROGxv0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sib6lKjksTROGxv0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sib6lKjksTROGxv0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sib6lKjksTROGxv0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sib6lKjksTROGxv0 .marker.cross{stroke:#333333;}#mermaid-svg-Sib6lKjksTROGxv0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sib6lKjksTROGxv0 p{margin:0;}#mermaid-svg-Sib6lKjksTROGxv0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Sib6lKjksTROGxv0 .cluster-label text{fill:#333;}#mermaid-svg-Sib6lKjksTROGxv0 .cluster-label span{color:#333;}#mermaid-svg-Sib6lKjksTROGxv0 .cluster-label span p{background-color:transparent;}#mermaid-svg-Sib6lKjksTROGxv0 .label text,#mermaid-svg-Sib6lKjksTROGxv0 span{fill:#333;color:#333;}#mermaid-svg-Sib6lKjksTROGxv0 .node rect,#mermaid-svg-Sib6lKjksTROGxv0 .node circle,#mermaid-svg-Sib6lKjksTROGxv0 .node ellipse,#mermaid-svg-Sib6lKjksTROGxv0 .node polygon,#mermaid-svg-Sib6lKjksTROGxv0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Sib6lKjksTROGxv0 .rough-node .label text,#mermaid-svg-Sib6lKjksTROGxv0 .node .label text,#mermaid-svg-Sib6lKjksTROGxv0 .image-shape .label,#mermaid-svg-Sib6lKjksTROGxv0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Sib6lKjksTROGxv0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Sib6lKjksTROGxv0 .rough-node .label,#mermaid-svg-Sib6lKjksTROGxv0 .node .label,#mermaid-svg-Sib6lKjksTROGxv0 .image-shape .label,#mermaid-svg-Sib6lKjksTROGxv0 .icon-shape .label{text-align:center;}#mermaid-svg-Sib6lKjksTROGxv0 .node.clickable{cursor:pointer;}#mermaid-svg-Sib6lKjksTROGxv0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Sib6lKjksTROGxv0 .arrowheadPath{fill:#333333;}#mermaid-svg-Sib6lKjksTROGxv0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Sib6lKjksTROGxv0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Sib6lKjksTROGxv0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sib6lKjksTROGxv0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Sib6lKjksTROGxv0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sib6lKjksTROGxv0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Sib6lKjksTROGxv0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Sib6lKjksTROGxv0 .cluster text{fill:#333;}#mermaid-svg-Sib6lKjksTROGxv0 .cluster span{color:#333;}#mermaid-svg-Sib6lKjksTROGxv0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Sib6lKjksTROGxv0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Sib6lKjksTROGxv0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Sib6lKjksTROGxv0 .icon-shape,#mermaid-svg-Sib6lKjksTROGxv0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sib6lKjksTROGxv0 .icon-shape p,#mermaid-svg-Sib6lKjksTROGxv0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Sib6lKjksTROGxv0 .icon-shape .label rect,#mermaid-svg-Sib6lKjksTROGxv0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sib6lKjksTROGxv0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Sib6lKjksTROGxv0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Sib6lKjksTROGxv0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
从列表选择
无下一班
🖱️ 点击「交接班」按钮
📋 显示当前班次汇总
👥 选择下一班操作员
✏️ 填写交接备注
✅ 直接结束班次
🔁 确认交接
⏹️ 关闭当前班次endTime = now
🆕 创建新班次startTime = nowoperatorId = 下一班
🏪 跳转收银台
班次历史列表:展示所有历史班次,支持按操作员筛选,便于月末对账。
6. 声音反馈系统
在快节奏的收银场景中,声音反馈是不可或缺的交互维度。它让店员在视线不离开屏幕的情况下,确认操作已被系统接收。
6.1 音效定义(Web Audio API)
系统采用 Web Audio API 程序化合成音效,而非加载 MP3 文件。这样做有三个显著优势:
| 添加商品 | playAddToCart() | C5→E5 正弦波 | 商品加入购物车 | 轻快的上行双音,像水滴声 |
| 创建订单 | playOrderCreated() | C5→E5→G5 正弦波 | 订单创建成功 | 三音上行琶音,成就感 |
| 支付成功 | playPaymentSuccess() | C5→E5→G5→C6 正弦波 | 支付确认后 300ms | 四音上行,比创建订单更明亮 |
| 错误提示 | playError() | 200Hz 方波 | 订单创建失败 | 低沉短促的"嘟"声,警示感 |
| 取消操作 | playCancel() | 400→300Hz 正弦波 | 移除购物车商品 | 下行滑音,像撤销的"咻"声 |
| 新订单 | playNewOrder() | 880Hz×2 方波 | 厨房检测到新订单 | 高频双哔声,穿透厨房噪音 |
技术实现原理:Web Audio API 通过 AudioContext 创建振荡器(OscillatorNode),设置频率和波形类型(正弦波/方波),连接增益节点(GainNode)控制音量,最后输出到扬声器。整个过程在内存中完成,不涉及任何文件 I/O。
6.2 控制机制
- 全局声音开关:通过 localStorage 键 pos-sound-enabled 持久化,页面刷新后保持用户偏好
- AudioContext 自动恢复:浏览器出于安全考虑会暂停后台标签页的 AudioContext,系统监听 visibilitychange 事件,页面重新可见时自动恢复音频上下文
- 厨房独立控制:KitchenDisplay 组件拥有独立的声音开关状态,厨房可以关闭提示音而不影响收银台
7. 种子数据设计
系统内置丰富的种子数据,首次启动时自动初始化,让新用户立刻获得"开箱即用"的体验。
7.1 默认分类(6 个)
| 🧋 | 经典奶茶 | 0 |
| 🍵 | 果茶系列 | 1 |
| 🧊 | 冰沙系列 | 2 |
| ☕ | 咖啡系列 | 3 |
| 🍰 | 小食点心 | 4 |
| 🌸 | 季节限定 | 5 |
7.2 默认商品(21 个)
| 经典奶茶 | 珍珠奶茶 | ¥15 |
| 经典奶茶 | 芋泥奶茶 | ¥18 |
| 经典奶茶 | 红豆奶茶 | ¥16 |
| 经典奶茶 | 黑糖鹿丸奶茶 | ¥19 |
| 经典奶茶 | 椰香奶茶 | ¥17 |
| 果茶系列 | 杨枝甘露 | ¥20 |
| 果茶系列 | 百香果双响炮 | ¥18 |
| 果茶系列 | 柠檬养乐多 | ¥16 |
| 果茶系列 | 芒果冰沙 | ¥19 |
| 果茶系列 | 西瓜汁 | ¥14 |
| 冰沙系列 | 草莓冰沙 | ¥18 |
| 冰沙系列 | 蓝莓冰沙 | ¥18 |
| 冰沙系列 | 抹茶冰沙 | ¥17 |
| 咖啡系列 | 拿铁咖啡 | ¥22 |
| 咖啡系列 | 焦糖玛奇朵 | ¥25 |
| 咖啡系列 | 美式咖啡 | ¥18 |
| 小食点心 | 蛋挞 | ¥8 |
| 小食点心 | 薯条 | ¥12 |
| 小食点心 | 鸡块 | ¥15 |
| 季节限定 | 桂花酿奶茶 | ¥22 |
| 季节限定 | 樱花拿铁 | ¥25 |
7.3 默认规格
| 糖度 | 全糖 / 七分糖 / 半糖 / 三分糖 / 无糖 | ¥0 |
| 冰度 | 正常冰 / 少冰 / 去冰 / 常温 / 热饮 | ¥0 |
| 杯型 | 中杯 / 大杯 / 超大杯 | ¥0 / ¥3 / ¥5 |
7.4 默认加料(8 个)
| 珍珠 | ¥2 |
| 芋圆 | ¥2 |
| 椰果 | ¥2 |
| 红豆 | ¥2 |
| 布丁 | ¥3 |
| 仙草 | ¥2 |
| 芝士奶盖 | ¥5 |
| 燕麦 | ¥2 |
7.5 默认操作员
| 店长 | admin | 1234 | 全部 13 项权限 |
| 收银员 | cashier | 0000 | 5 项基本权限(order:view、order:create、member:view、stats:view、shift:view) |
8. 数据备份与恢复
8.1 备份格式
备份文件采用 JSON 格式,结构清晰,便于人工阅读和第三方工具处理:
{
"version": "1.1.0",
"exportedAt": "2026-03-15T14:30:00.000Z",
"data": {
"categories": […],
"products": […],
"specGroups": […],
"specOptions": […],
"productSpecs": […],
"addons": […],
"operators": […],
"orders": […],
"orderItems": […],
"shifts": […],
"members": […],
"activityLogs": […]
}
}
8.2 恢复流程
数据恢复采用**“清空 + 重导”**策略,确保恢复后的数据与备份完全一致,避免新旧数据混杂。
关键设计点:
- 依赖逆序清空:先删除被引用的表(如 OrderItem),再删除引用表(如 Order),避免外键约束冲突
- 依赖正序恢复:先创建基础数据(Category、Operator),再创建依赖它们的数据(Product、Order),确保外键始终有效
- 恢复期间不可用:清空操作会短暂导致数据为空,建议在业务低峰期(如深夜)操作
- 版本兼容性:version 字段为未来版本升级预留,不同版本的数据结构可以通过迁移逻辑处理
9. 安全与权限
9.1 操作员认证
系统采用 PIN 码登录机制,操作员输入 6 位数字 PIN 码完成身份验证。登录状态显示在全局状态栏,支持随时登出。
SQLite
/api/operators
POSClient
SQLite
/api/operators
POSClient
#mermaid-svg-FIezf861VQ1OGMzl{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FIezf861VQ1OGMzl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FIezf861VQ1OGMzl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FIezf861VQ1OGMzl .error-icon{fill:#552222;}#mermaid-svg-FIezf861VQ1OGMzl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FIezf861VQ1OGMzl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FIezf861VQ1OGMzl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FIezf861VQ1OGMzl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FIezf861VQ1OGMzl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FIezf861VQ1OGMzl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FIezf861VQ1OGMzl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FIezf861VQ1OGMzl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FIezf861VQ1OGMzl .marker.cross{stroke:#333333;}#mermaid-svg-FIezf861VQ1OGMzl svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FIezf861VQ1OGMzl p{margin:0;}#mermaid-svg-FIezf861VQ1OGMzl .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FIezf861VQ1OGMzl text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-FIezf861VQ1OGMzl .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FIezf861VQ1OGMzl .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-FIezf861VQ1OGMzl .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-FIezf861VQ1OGMzl .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-FIezf861VQ1OGMzl #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-FIezf861VQ1OGMzl .sequenceNumber{fill:white;}#mermaid-svg-FIezf861VQ1OGMzl #sequencenumber{fill:#333;}#mermaid-svg-FIezf861VQ1OGMzl #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-FIezf861VQ1OGMzl .messageText{fill:#333;stroke:none;}#mermaid-svg-FIezf861VQ1OGMzl .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FIezf861VQ1OGMzl .labelText,#mermaid-svg-FIezf861VQ1OGMzl .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-FIezf861VQ1OGMzl .loopText,#mermaid-svg-FIezf861VQ1OGMzl .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-FIezf861VQ1OGMzl .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FIezf861VQ1OGMzl .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FIezf861VQ1OGMzl .noteText,#mermaid-svg-FIezf861VQ1OGMzl .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-FIezf861VQ1OGMzl .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FIezf861VQ1OGMzl .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FIezf861VQ1OGMzl .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FIezf861VQ1OGMzl .actorPopupMenu{position:absolute;}#mermaid-svg-FIezf861VQ1OGMzl .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-FIezf861VQ1OGMzl .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FIezf861VQ1OGMzl .actor-man circle,#mermaid-svg-FIezf861VQ1OGMzl line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-FIezf861VQ1OGMzl :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
店员
输入 PIN 码
POST 验证请求
查询匹配的操作员
返回操作员信息
返回 token + 权限列表
写入登录状态
记录 Login 日志
显示操作员姓名 + 角色
店员
登录/登出操作均记录在 ActivityLog 中,便于安全审计。
9.2 权限控制
权限控制分为前端 UI 层和后端 API 层两个维度:
#mermaid-svg-gpe9Vw0eAi9kN155{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gpe9Vw0eAi9kN155 .error-icon{fill:#552222;}#mermaid-svg-gpe9Vw0eAi9kN155 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gpe9Vw0eAi9kN155 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gpe9Vw0eAi9kN155 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gpe9Vw0eAi9kN155 .marker.cross{stroke:#333333;}#mermaid-svg-gpe9Vw0eAi9kN155 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gpe9Vw0eAi9kN155 p{margin:0;}#mermaid-svg-gpe9Vw0eAi9kN155 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-gpe9Vw0eAi9kN155 .cluster-label text{fill:#333;}#mermaid-svg-gpe9Vw0eAi9kN155 .cluster-label span{color:#333;}#mermaid-svg-gpe9Vw0eAi9kN155 .cluster-label span p{background-color:transparent;}#mermaid-svg-gpe9Vw0eAi9kN155 .label text,#mermaid-svg-gpe9Vw0eAi9kN155 span{fill:#333;color:#333;}#mermaid-svg-gpe9Vw0eAi9kN155 .node rect,#mermaid-svg-gpe9Vw0eAi9kN155 .node circle,#mermaid-svg-gpe9Vw0eAi9kN155 .node ellipse,#mermaid-svg-gpe9Vw0eAi9kN155 .node polygon,#mermaid-svg-gpe9Vw0eAi9kN155 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gpe9Vw0eAi9kN155 .rough-node .label text,#mermaid-svg-gpe9Vw0eAi9kN155 .node .label text,#mermaid-svg-gpe9Vw0eAi9kN155 .image-shape .label,#mermaid-svg-gpe9Vw0eAi9kN155 .icon-shape .label{text-anchor:middle;}#mermaid-svg-gpe9Vw0eAi9kN155 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gpe9Vw0eAi9kN155 .rough-node .label,#mermaid-svg-gpe9Vw0eAi9kN155 .node .label,#mermaid-svg-gpe9Vw0eAi9kN155 .image-shape .label,#mermaid-svg-gpe9Vw0eAi9kN155 .icon-shape .label{text-align:center;}#mermaid-svg-gpe9Vw0eAi9kN155 .node.clickable{cursor:pointer;}#mermaid-svg-gpe9Vw0eAi9kN155 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gpe9Vw0eAi9kN155 .arrowheadPath{fill:#333333;}#mermaid-svg-gpe9Vw0eAi9kN155 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gpe9Vw0eAi9kN155 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gpe9Vw0eAi9kN155 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gpe9Vw0eAi9kN155 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gpe9Vw0eAi9kN155 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gpe9Vw0eAi9kN155 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gpe9Vw0eAi9kN155 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gpe9Vw0eAi9kN155 .cluster text{fill:#333;}#mermaid-svg-gpe9Vw0eAi9kN155 .cluster span{color:#333;}#mermaid-svg-gpe9Vw0eAi9kN155 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gpe9Vw0eAi9kN155 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gpe9Vw0eAi9kN155 rect.text{fill:none;stroke-width:0;}#mermaid-svg-gpe9Vw0eAi9kN155 .icon-shape,#mermaid-svg-gpe9Vw0eAi9kN155 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gpe9Vw0eAi9kN155 .icon-shape p,#mermaid-svg-gpe9Vw0eAi9kN155 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gpe9Vw0eAi9kN155 .icon-shape .label rect,#mermaid-svg-gpe9Vw0eAi9kN155 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gpe9Vw0eAi9kN155 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gpe9Vw0eAi9kN155 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gpe9Vw0eAi9kN155 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
⚠️ 后端 API 层
API 请求
当前: 无权限校验
直接执行操作
🎨 前端 UI 层
有权限
无权限
操作员登录
获取 permissions 列表
检查权限
显示功能按钮/菜单
隐藏或禁用按钮
前端层:根据操作员 permissions 字段控制功能按钮和菜单的显示。例如没有 product:edit 权限的收银员,在管理视图中看不到"编辑商品"按钮。
后端层:当前版本尚未实现权限校验中间件,这是已知的安全风险(详见第 10 章)。任何登录用户理论上均可调用所有 API,前端隐藏按钮只是"防君子不防小人"的 UI 层保护。
9.3 删除保护
系统实施了删除保护机制:不允许删除最后一个管理员角色的操作员。这确保了系统始终至少有一个管理员账户可用,避免因误操作导致系统陷入"无管理员可管理"的死锁状态。
10. 已知限制与风险
| 1 | 后端 API 未做操作员权限校验 | 任何登录用户可调用所有 API,存在越权风险 | 🔴 高 | 在 API 路由层增加权限校验中间件,参照前端 permissions 字段进行校验 |
| 2 | 会员余额不足仍允许会员支付订单 | 可能造成负余额,财务数据混乱 | 🟡 中 | 在创建订单时增加余额校验逻辑,余额不足时拒绝 Member 支付方式 |
| 3 | 产品图片按分类共享 | 同分类产品使用相同图片,辨识度不足 | 🟢 低 | V1.1 规划为每个产品独立生成 AI 图片 |
| 4 | 数据恢复操作清空再导入 | 恢复期间系统数据不可用,影响营业 | 🟡 中 | 增加恢复预览功能,或采用事务包裹实现原子性恢复 |
| 5 | 沙箱环境内存限制 | 开发服务器偶尔 OOM(Out of Memory) | ⚪ 仅开发 | 生产环境部署到独立服务器,不受沙箱限制 |
11. 未来规划
| 1 | 每产品独立 AI 图片 | 21 个产品各生成一张独特图片,提升视觉体验 | 品牌感提升 |
| 2 | 后端权限校验中间件 | API 层面强制执行操作员权限,补齐安全短板 | 安全性提升 |
| 3 | 移动端响应式优化 | 适配手机/平板触屏操作,支持移动收银 | 场景扩展 |
| 4 | 月度统计对比 | 月度同比/环比分析,识别长期趋势 | 决策支持 |
| 5 | 在线支付集成 | 微信/支付宝支付接口模拟,真实收款闭环 | 功能完善 |
| 6 | ESC/POS 打印 | 热敏小票打印机适配,支持物理小票打印 | 硬件集成 |
| 7 | 订单备注语音输入 | ASR 语音转文字,解放双手快速录入 | 效率提升 |
| 8 | AI 推荐系统 | 根据销售数据推荐热门搭配,提升客单价 | 智能营销 |
| 9 | 会员生日提醒 | 营销推送功能,自动发送生日优惠券 | 客户关怀 |
| 10 | 多店数据同步 | 云端备份与多门店数据同步,支持连锁经营 | 规模扩展 |

