Appointment 数据库表结构详解
10 张核心表 · 预约 → 分组 → 账单 → 交易 → 退款 全链路
← 返回笔记首页
表间关系 ER 图
erDiagram
appointment_groups ||--o{ appointment_group_members : "has members"
appointment_group_members }o--|| appointments : "links to"
appointments ||--o{ sub_appointments : "has sub-services"
appointments ||--o| invoices : "generates"
invoices ||--o{ invoice_items : "splits into"
invoices ||--o{ transactions : "paid via"
transactions ||--o{ transaction_history : "status log"
transactions ||--o{ refunds : "refunded via"
transactions ||--o{ receipts : "receipt for"
appointments {
VARCHAR id PK
VARCHAR guest_id FK
VARCHAR service_id FK
VARCHAR employee_id FK
VARCHAR center_id FK
VARCHAR status
DECIMAL total_amount
}
appointment_groups {
SERIAL id PK
VARCHAR group_code UK
VARCHAR center_id
DATE scheduled_date
}
appointment_group_members {
SERIAL id PK
INTEGER group_id FK
VARCHAR appointment_id UK
VARCHAR guest_id
BOOLEAN is_primary
}
invoices {
UUID id PK
VARCHAR invoice_number UK
VARCHAR appointment_id FK
ENUM invoice_type
DECIMAL total_amount
JSONB discount_details
}
transactions {
UUID id PK
VARCHAR order_id UK
UUID invoice_id FK
ENUM payment_method
DECIMAL total_amount
DECIMAL refunded_amount
}
ENUM 类型一览
payment_method_type
credit_card
debit_card
cash
online_card
payment_status_type
pending
processing
completed
failed
cancelled
refunded
partial_refunded
invoice_type
single
group
invoice_status
draft
unpaid
partial
paid
void
invoice_item_status
pending
processing
paid
skipped
void
refund_status_type
pending
processing
completed
failed
refund_type_enum
card
cash
gift_card
receipt_type
print
email
sms
Layer 1 — 预约层
1 appointments — 预约主表
每条记录代表一个客人的一次服务预约。是整个系统的核心表,连接客户、服务、员工、店铺。
设计要点
id 使用
VARCHAR(255) 而非 UUID,因为历史上需要兼容 Zenoti 外部系统的预约 ID 格式。
关联字段
| 字段 | 类型 | 约束 | 说明 |
id | VARCHAR(255) | PK | 预约唯一标识(兼容 Zenoti 外部 ID) |
guest_id | VARCHAR(255) | NOT NULL FK→guests | 客户 ID,CASCADE 删除 |
service_id | VARCHAR(255) | NOT NULL FK→services | 服务项目 ID,RESTRICT 删除 |
employee_id | VARCHAR(255) | FK→employees | 技师 ID,ON DELETE SET NULL |
center_id | VARCHAR(255) | NOT NULL FK→centers | 门店 ID,RESTRICT 删除 |
时间安排
| 字段 | 类型 | 约束 | 说明 |
scheduled_date | DATE | NOT NULL | 预约日期 |
scheduled_time | TIME | NOT NULL | 预约时间 |
scheduled_datetime | TIMESTAMPTZ | NOT NULL | 完整预约时间戳(含时区) |
duration | INTERVAL | NOT NULL | 预约时长 |
end_datetime | TIMESTAMPTZ | | 预计结束时间(计算得出) |
状态管理
| 字段 | 类型 | 约束 | 说明 |
status | VARCHAR(50) | NOT NULL DEFAULT 'pending' | 预约状态(见下方状态机) |
payment_status | VARCHAR(50) | DEFAULT 'pending' | 支付状态:pending / paid / refunded |
payment_method | VARCHAR(50) | | 支付方式:cash / card / online |
状态流转 State Machine
pending →
confirmed →
checked_in →
in_progress →
pending_payment →
paid
可跳转到终态:
cancelled(从 confirmed/checked_in/in_progress)
no_show(从 checked_in)
void(从 paid,退款后)
价格信息
| 字段 | 类型 | 约束 | 说明 |
quoted_price | DECIMAL(10,2) | | 预约时报价 |
final_price | DECIMAL(10,2) | | 最终实际价格 |
discount_amount | DECIMAL(10,2) | DEFAULT 0 | 折扣金额 |
tip_amount | DECIMAL(10,2) | DEFAULT 0 | 小费金额 |
total_amount | DECIMAL(10,2) | | 总金额 |
备注与业务标记
| 字段 | 类型 | 说明 |
special_requests | TEXT | 客户特殊要求 |
internal_notes | TEXT | 内部备注(客户不可见) |
cancellation_reason | TEXT | 取消原因 |
is_first_visit | BOOLEAN | 是否首次到店(DEFAULT FALSE) |
is_repeat_customer | BOOLEAN | 是否回头客(DEFAULT FALSE) |
referral_source | VARCHAR(100) | 推荐来源 |
reminder_sent | BOOLEAN | 是否已发提醒(DEFAULT FALSE) |
confirmation_sent | BOOLEAN | 是否已发确认(DEFAULT FALSE) |
时间戳
| 字段 | 类型 | 说明 |
created_at | TIMESTAMPTZ | 创建时间 |
updated_at | TIMESTAMPTZ | 更新时间(触发器自动更新) |
confirmed_at | TIMESTAMPTZ | 确认时间 |
started_at | TIMESTAMPTZ | 服务开始时间 |
completed_at | TIMESTAMPTZ | 服务完成时间 |
cancelled_at | TIMESTAMPTZ | 取消时间 Migration 添加 |
Migration 后续添加的字段
| 字段 | 类型 | 来源 | 说明 |
source | appointment_source_type | Migration 1771700000004 | 预约来源(web / kiosk / phone / walk_in 等) |
data_source | VARCHAR | Migration 1771700000004 | 数据来源(local / zenoti 等) |
sync_status | VARCHAR | Migration 1771700000004 | 同步状态 |
2 sub_appointments — 子预约表
当一个预约包含多个服务项目时,每个服务拆分为一条子预约。支持"一次预约多个服务、每个服务不同技师"的场景。
| 字段 | 类型 | 约束 | 说明 |
id | SERIAL | PK | 自增主键 |
appointment_id | VARCHAR(255) | NOT NULL FK→appointments | 所属主预约,CASCADE 删除 |
service_id | VARCHAR(255) | NOT NULL FK→services | 服务项目 ID |
provider_id | VARCHAR(255) | FK→employees | 该服务的技师,SET NULL 删除 |
sub_duration | INTERVAL | NOT NULL | 该项服务时长 |
sequence_order | INT | DEFAULT 1 | 服务顺序(1, 2, 3...) |
sub_price | DECIMAL(10,2) | NOT NULL DEFAULT 0 | 该项服务价格 |
special_notes | TEXT | | 该服务的特殊备注 |
created_at | TIMESTAMPTZ | | 创建时间 |
updated_at | TIMESTAMPTZ | | 更新时间 |
唯一约束
UNIQUE(appointment_id, sequence_order) — 同一主预约下,服务顺序号不可重复。
3 appointment_groups — 预约分组表
记录"多人一起来"的 Group Appointment。一个 group 对应多个 appointment。
| 字段 | 类型 | 约束 | 说明 |
id | SERIAL | PK | 自增主键 |
group_code | VARCHAR(50) | NOT NULL UNIQUE | 组标识码,如 grp_1706800000_abc123 |
center_id | VARCHAR(50) | NOT NULL | 门店 ID |
scheduled_date | DATE | NOT NULL | 预约日期 |
is_active | BOOLEAN | DEFAULT TRUE | 是否有效 |
created_at | TIMESTAMPTZ | | 创建时间 |
created_by | VARCHAR(50) | | 创建人 ID |
notes | TEXT | | 备注 |
4 appointment_group_members — 分组成员表
将多个 appointment 关联到同一个 group 的桥接表。
| 字段 | 类型 | 约束 | 说明 |
id | SERIAL | PK | 自增主键 |
group_id | INTEGER | NOT NULL FK→appointment_groups | 分组 ID,CASCADE 删除 |
appointment_id | VARCHAR(50) | NOT NULL UNIQUE | 预约 ID(一个预约最多属于一个 group) |
guest_id | VARCHAR(50) | NOT NULL | 客户 ID |
is_primary | BOOLEAN | DEFAULT FALSE | 是否为主预约人(通知发送对象) |
created_at | TIMESTAMPTZ | | 创建时间 |
设计决策:为什么不直接在 appointments 上加 group_id?
采用独立桥接表的"间接关联"设计,好处是 group 关系是可选的,不会污染基础 appointments 表结构。appointment_id 的 UNIQUE 约束确保一个预约最多属于一个 group。
Layer 2 — 账单层
5 invoices — 账单主表
每个 appointment(或 group)生成一张 invoice,作为支付的"总账"。这是折扣和支付金额的 Source of Truth。
| 字段 | 类型 | 约束 | 说明 |
id | UUID | PK | 账单唯一 ID |
invoice_number | VARCHAR(50) | NOT NULL UNIQUE | 账单号 QQ_INV_YYYYMMDD_NNN |
store_id | VARCHAR(255) | NOT NULL FK→centers | 门店 ID,RESTRICT 删除 |
appointment_id | VARCHAR(255) | FK→appointments | 关联的预约 ID,SET NULL 删除 |
invoice_type | invoice_type | NOT NULL ENUM DEFAULT 'single' | single(单人)/ group(多人) |
金额字段
| 字段 | 类型 | 约束 | 说明 |
subtotal | DECIMAL(10,2) | NOT NULL DEFAULT 0 | 服务小计(税前) |
discount_amount | DECIMAL(10,2) | NOT NULL DEFAULT 0 | 折扣金额(税前) |
tax_amount | DECIMAL(10,2) | NOT NULL DEFAULT 0 | 税费金额 |
tip_amount | DECIMAL(10,2) | NOT NULL DEFAULT 0 | 小费金额 |
total_amount | DECIMAL(10,2) | NOT NULL DEFAULT 0 | 总金额 |
discount_details | JSONB | JSONB DEFAULT '[]' Migration 添加 | 折扣详情 [{type, name, amount, baseAmount}] |
支付追踪
| 字段 | 类型 | 约束 | 说明 |
payment_status | invoice_status | NOT NULL ENUM DEFAULT 'draft' | draft / unpaid / partial / paid / void |
paid_amount | DECIMAL(10,2) | NOT NULL DEFAULT 0 | 已支付金额 |
item_count | INTEGER | NOT NULL DEFAULT 1 | 子账单总数(Group Invoice 用) |
paid_item_count | INTEGER | NOT NULL DEFAULT 0 | 已支付子账单数量 |
创建 / 作废 / 时间戳
| 字段 | 类型 | 说明 |
created_by | VARCHAR(255) | 创建人(FK→employees) |
voided_by | VARCHAR(255) | 作废人(FK→employees) |
voided_at | TIMESTAMPTZ | 作废时间 |
void_reason | TEXT | 作废原因 |
created_at | TIMESTAMPTZ | 创建时间 |
updated_at | TIMESTAMPTZ | 更新时间 |
discount_details 字段的由来
最初折扣信息只存在 transactions.discount_details 上。但页面刷新后,前端重新加载时如果 DiscountSection 被隐藏,就会导致折扣信息丢失。Migration 1771500000001 将 discount_details 添加到了 invoices 表,使其成为折扣信息的 Source of Truth。
6 invoice_items — 子账单表
Group Invoice 下,每个客人拆出一个 invoice_item。支持"各付各的"的分人付款模式。
| 字段 | 类型 | 约束 | 说明 |
id | UUID | PK | 子账单 ID |
invoice_id | UUID | NOT NULL FK→invoices | 主账单 ID,CASCADE 删除 |
item_number | INTEGER | NOT NULL | 子账单序号 |
guest_id | VARCHAR(255) | FK→guests | 客人 ID |
guest_name | VARCHAR(100) | | 客人姓名(快照,防改名影响历史) |
services | JSONB | NOT NULL JSONB | 服务项目列表(见下方格式) |
subtotal | DECIMAL(10,2) | NOT NULL | 服务小计 |
discount_amount | DECIMAL(10,2) | DEFAULT 0 | 折扣金额 |
tax_amount | DECIMAL(10,2) | DEFAULT 0 | 税费 |
tip_amount | DECIMAL(10,2) | DEFAULT 0 | 小费 |
total_amount | DECIMAL(10,2) | NOT NULL | 总金额 |
payment_status | invoice_item_status | ENUM DEFAULT 'pending' | pending / processing / paid / skipped / void |
transaction_id | UUID | FK→transactions | 关联交易 ID(支付后填入) |
payment_method | payment_method_type | ENUM | 支付方式 |
paid_at | TIMESTAMPTZ | | 支付时间 |
skipped_at | TIMESTAMPTZ | | 跳过时间 |
skip_reason | VARCHAR(50) | | customer_request / card_declined / timeout |
created_at | TIMESTAMPTZ | | 创建时间 |
updated_at | TIMESTAMPTZ | | 更新时间 |
services JSONB 格式
[
{
"service_id": "uuid-string",
"service_name": "Gel Manicure",
"employee_id": "uuid-string",
"employee_name": "Alice",
"price": 35.00,
"quantity": 1
}
]
唯一约束
UNIQUE(invoice_id, item_number) — 同一账单下序号不重复。
Layer 3 — 交易层
7 transactions — 交易记录表
每次支付动作产生一条 transaction。一个 invoice 可以有多条 transaction(分次付款、退款等)。
标识与关联
| 字段 | 类型 | 约束 | 说明 |
id | UUID | PK | 交易 ID |
order_id | VARCHAR(50) | NOT NULL UNIQUE | 业务订单号 QQ_ORD_YYYYMMDD_NNN |
appointment_id | VARCHAR(255) | FK→appointments | 预约 ID |
invoice_id | UUID | | 主账单 ID |
invoice_item_id | UUID | | 子账单 ID(分人付款时) |
store_id | VARCHAR(255) | NOT NULL FK→centers | 门店 ID |
customer_id | VARCHAR(255) | FK→guests | 客户 ID |
employee_id | VARCHAR(255) | FK→employees | 处理员工 ID |
金额信息
| 字段 | 类型 | 约束 | 说明 |
subtotal | DECIMAL(10,2) | NOT NULL | 服务小计 |
discount_amount | DECIMAL(10,2) | DEFAULT 0 | 折扣金额 |
tax_amount | DECIMAL(10,2) | DEFAULT 0 | 税费金额 |
tip_amount | DECIMAL(10,2) | DEFAULT 0 | 小费金额 |
total_amount | DECIMAL(10,2) | NOT NULL | 总金额(含税含折扣含小费) |
refunded_amount | DECIMAL(10,2) | DEFAULT 0 | 已退款金额 |
支付方式与状态
| 字段 | 类型 | 约束 | 说明 |
payment_method | payment_method_type | NOT NULL ENUM | credit_card / debit_card / cash / online_card |
payment_status | payment_status_type | ENUM DEFAULT 'pending' | pending / processing / completed / failed / cancelled / refunded / partial_refunded |
CodePay 支付网关字段
| 字段 | 类型 | 说明 |
codepay_order_id | VARCHAR(100) | CodePay 订单号 |
codepay_trans_no | VARCHAR(100) | CodePay 交易号 |
card_type | VARCHAR(20) | 卡类型(VISA / MASTERCARD / AMEX) |
card_last_four | VARCHAR(4) | 卡号后四位 |
terminal_sn | VARCHAR(50) | 终端序列号 |
现金支付 & 其他
| 字段 | 类型 | 说明 |
cash_received | DECIMAL(10,2) | 现金收款金额 |
cash_change | DECIMAL(10,2) | 找零金额 |
discount_details | JSONB | 折扣明细 JSON(冗余快照,invoice 上的为准) |
items | JSONB | 服务项目列表 JSON |
idempotency_key | VARCHAR(100) | 幂等键(UNIQUE),防止重复扣款 |
notes | TEXT | 备注 Migration 添加 |
错误与会话(Migration 添加)
| 字段 | 类型 | 说明 |
error_code | VARCHAR(50) | 支付失败错误代码 |
error_message | TEXT | 支付失败错误信息 |
cancel_reason | TEXT | 取消原因 |
cash_drawer_session_id | UUID | 关联的收银会话 ID(FK→cash_drawer_sessions) |
时间戳
| 字段 | 类型 | 说明 |
created_at | TIMESTAMPTZ | 创建时间 |
paid_at | TIMESTAMPTZ | 支付完成时间 |
updated_at | TIMESTAMPTZ | 更新时间 |
idempotency_key 的作用
这是支付安全的关键字段。当 CodePay 回调可能重复到达时(网络重试),系统用 idempotency_key 做去重,防止同一笔支付被记录两次。前端每次发起支付都会生成唯一的 key。
8 transaction_history — 交易状态变更历史
每次 transaction 状态变化都记录一条历史,完整的审计日志。
| 字段 | 类型 | 约束 | 说明 |
id | UUID | PK | 历史记录 ID |
transaction_id | UUID | NOT NULL FK→transactions | 交易 ID,CASCADE 删除 |
status | VARCHAR(30) | NOT NULL | 状态值(如 pending → processing → completed) |
message | TEXT | | 状态说明 |
raw_response | JSONB | JSONB | CodePay 或其他渠道的原始响应 |
changed_by | VARCHAR(255) | FK→employees | 操作人员 ID |
created_at | TIMESTAMPTZ | | 记录时间 |
9 refunds — 退款记录表
支持全额和部分退款,支持刷卡 / 现金 / 礼品卡三种退款方式。
核心字段
| 字段 | 类型 | 约束 | 说明 |
id | UUID | PK | 退款 ID |
refund_number | VARCHAR(50) | NOT NULL UNIQUE | 退款单号 QQ_REF_YYYYMMDD_NNN |
transaction_id | UUID | NOT NULL FK→transactions | 原交易 ID(RESTRICT,不可删除有退款的交易) |
refund_amount | DECIMAL(10,2) | NOT NULL | 退款金额 |
refund_reason | TEXT | | 退款原因 |
refund_status | refund_status_type | ENUM DEFAULT 'pending' | pending / processing / completed / failed |
refund_type | refund_type_enum | ENUM DEFAULT 'card' Migration 添加 | card / cash / gift_card |
is_cash_refund | BOOLEAN | DEFAULT FALSE | 旧字段,新系统用 refund_type |
CodePay 退款
| 字段 | 类型 | 说明 |
codepay_refund_no | VARCHAR(100) | CodePay 退款号 |
终端信息(Migration 添加 — 刷卡退款)
| 字段 | 类型 | 说明 |
terminal_id | UUID | 终端 ID(FK→terminals) |
terminal_sn | VARCHAR(100) | 终端序列号 |
card_type | VARCHAR(20) | 卡类型 |
card_last_four | VARCHAR(4) | 卡号后四位 |
礼品卡退款(Migration 添加)
| 字段 | 类型 | 说明 |
gift_card_id | UUID | 礼品卡 ID(FK→gift_cards) |
gift_card_number | VARCHAR(50) | 礼品卡号 |
处理人 & 时间戳
| 字段 | 类型 | 说明 |
processed_by | VARCHAR(255) | 处理人(FK→employees) |
approved_by | VARCHAR(255) | 审批人(FK→employees,manager 级别) |
created_at | TIMESTAMPTZ | 创建时间 |
completed_at | TIMESTAMPTZ | 完成时间 |
10 receipts — 收据记录表
支持打印 / 邮件 / 短信三种收据方式。
| 字段 | 类型 | 约束 | 说明 |
id | UUID | PK | 收据 ID |
receipt_number | VARCHAR(50) | NOT NULL UNIQUE | 收据号 QQ_RCP_YYYYMMDD_NNN |
transaction_id | UUID | NOT NULL FK→transactions | 交易 ID,CASCADE 删除 |
receipt_type | receipt_type | NOT NULL ENUM | print / email / sms |
content | JSONB | NOT NULL JSONB | 收据内容(店铺信息、服务明细、金额等) |
sent_to | VARCHAR(255) | | 发送目标(email 或 phone) |
sent_at | TIMESTAMPTZ | | 发送时间 |
send_status | VARCHAR(20) | DEFAULT 'pending' | pending / sent / failed |
send_error | TEXT | | 发送失败原因 |
created_at | TIMESTAMPTZ | | 创建时间 |
典型数据流
单人预约支付流程
sequenceDiagram
participant F as Frontend
participant B as Backend
participant DB as Database
F->>B: POST /api/payment/checkout
B->>DB: 查找/创建 invoice (type=single)
B->>DB: 创建 transaction (status=pending)
B->>DB: 创建 transaction_history
B-->>F: {orderId, transactionId}
Note over F,B: CodePay 支付...
B->>DB: UPDATE transaction (status=completed)
B->>DB: UPDATE invoice (paid_amount, status=paid)
B->>DB: UPDATE appointment (status=paid)
B->>DB: 创建 transaction_history
B->>DB: 创建 receipt
B-->>F: 支付成功
Group 预约支付流程
sequenceDiagram
participant F as Frontend
participant B as Backend
participant DB as Database
Note over F: Group: Guest A + Guest B
F->>B: POST /api/payment/checkout (Guest A)
B->>DB: 查找/创建 invoice (type=group)
B->>DB: 创建 invoice_item #1 (Guest A)
B->>DB: 创建 transaction #1
B-->>F: 支付 Guest A 成功
Note over DB: invoice: partial (1/2 paid)
F->>B: POST /api/payment/checkout (Guest B)
B->>DB: 更新已有 invoice
B->>DB: 创建 invoice_item #2 (Guest B)
B->>DB: 创建 transaction #2
B-->>F: 支付 Guest B 成功
Note over DB: invoice: paid (2/2 paid)
退款流程
sequenceDiagram
participant F as Frontend
participant B as Backend
participant DB as Database
participant CP as CodePay
F->>B: POST /api/payment/refund
B->>DB: 创建 refund (status=pending)
B->>DB: 创建 transaction_history
alt 刷卡退款
B->>CP: 发起退款请求
CP-->>B: 退款结果
else 现金退款
Note over B: 直接完成
else 礼品卡退款
B->>DB: 增加 gift_card 余额
end
B->>DB: UPDATE refund (status=completed)
B->>DB: UPDATE transaction (refunded_amount)
B->>DB: UPDATE invoice (paid_amount)
B->>DB: 创建 transaction_history
B-->>F: 退款成功
Celoria Appointment Database Schema · 更新于 2026-02-16
数据来源:database/schema/tables/ + backend/database/migrations/