Appointment 数据库表结构详解

10 张核心表 · 预约 → 分组 → 账单 → 交易 → 退款 全链路

📅
预约层 (3 表)
appointments, sub_appointments, appointment_groups + members
🧾
账单层 (2 表)
invoices, invoice_items
💳
交易层 (4 表)
transactions, transaction_history, refunds, receipts

表间关系 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
    }
    
目录
  1. 1 appointments — 预约主表
  2. 2 sub_appointments — 子预约表
  3. 3 appointment_groups — 预约分组表
  4. 4 appointment_group_members — 分组成员表
  5. 5 invoices — 账单主表
  6. 6 invoice_items — 子账单表
  7. 7 transactions — 交易记录表
  8. 8 transaction_history — 交易状态历史表
  9. 9 refunds — 退款记录表
  10. 10 receipts — 收据记录表

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 格式。

关联字段

字段类型约束说明
idVARCHAR(255)PK预约唯一标识(兼容 Zenoti 外部 ID)
guest_idVARCHAR(255)NOT NULL FK→guests客户 ID,CASCADE 删除
service_idVARCHAR(255)NOT NULL FK→services服务项目 ID,RESTRICT 删除
employee_idVARCHAR(255)FK→employees技师 ID,ON DELETE SET NULL
center_idVARCHAR(255)NOT NULL FK→centers门店 ID,RESTRICT 删除

时间安排

字段类型约束说明
scheduled_dateDATENOT NULL预约日期
scheduled_timeTIMENOT NULL预约时间
scheduled_datetimeTIMESTAMPTZNOT NULL完整预约时间戳(含时区)
durationINTERVALNOT NULL预约时长
end_datetimeTIMESTAMPTZ预计结束时间(计算得出)

状态管理

字段类型约束说明
statusVARCHAR(50)NOT NULL DEFAULT 'pending'预约状态(见下方状态机)
payment_statusVARCHAR(50)DEFAULT 'pending'支付状态:pending / paid / refunded
payment_methodVARCHAR(50)支付方式:cash / card / online
状态流转 State Machine

pendingconfirmedchecked_inin_progresspending_paymentpaid

可跳转到终态: cancelled(从 confirmed/checked_in/in_progress) no_show(从 checked_in) void(从 paid,退款后)

价格信息

字段类型约束说明
quoted_priceDECIMAL(10,2)预约时报价
final_priceDECIMAL(10,2)最终实际价格
discount_amountDECIMAL(10,2)DEFAULT 0折扣金额
tip_amountDECIMAL(10,2)DEFAULT 0小费金额
total_amountDECIMAL(10,2)总金额

备注与业务标记

字段类型说明
special_requestsTEXT客户特殊要求
internal_notesTEXT内部备注(客户不可见)
cancellation_reasonTEXT取消原因
is_first_visitBOOLEAN是否首次到店(DEFAULT FALSE)
is_repeat_customerBOOLEAN是否回头客(DEFAULT FALSE)
referral_sourceVARCHAR(100)推荐来源
reminder_sentBOOLEAN是否已发提醒(DEFAULT FALSE)
confirmation_sentBOOLEAN是否已发确认(DEFAULT FALSE)

时间戳

字段类型说明
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间(触发器自动更新)
confirmed_atTIMESTAMPTZ确认时间
started_atTIMESTAMPTZ服务开始时间
completed_atTIMESTAMPTZ服务完成时间
cancelled_atTIMESTAMPTZ取消时间 Migration 添加

Migration 后续添加的字段

字段类型来源说明
sourceappointment_source_typeMigration 1771700000004预约来源(web / kiosk / phone / walk_in 等)
data_sourceVARCHARMigration 1771700000004数据来源(local / zenoti 等)
sync_statusVARCHARMigration 1771700000004同步状态

2 sub_appointments — 子预约表

当一个预约包含多个服务项目时,每个服务拆分为一条子预约。支持"一次预约多个服务、每个服务不同技师"的场景。

字段类型约束说明
idSERIALPK自增主键
appointment_idVARCHAR(255)NOT NULL FK→appointments所属主预约,CASCADE 删除
service_idVARCHAR(255)NOT NULL FK→services服务项目 ID
provider_idVARCHAR(255)FK→employees该服务的技师,SET NULL 删除
sub_durationINTERVALNOT NULL该项服务时长
sequence_orderINTDEFAULT 1服务顺序(1, 2, 3...)
sub_priceDECIMAL(10,2)NOT NULL DEFAULT 0该项服务价格
special_notesTEXT该服务的特殊备注
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间
唯一约束
UNIQUE(appointment_id, sequence_order) — 同一主预约下,服务顺序号不可重复。

3 appointment_groups — 预约分组表

记录"多人一起来"的 Group Appointment。一个 group 对应多个 appointment。

字段类型约束说明
idSERIALPK自增主键
group_codeVARCHAR(50)NOT NULL UNIQUE组标识码,如 grp_1706800000_abc123
center_idVARCHAR(50)NOT NULL门店 ID
scheduled_dateDATENOT NULL预约日期
is_activeBOOLEANDEFAULT TRUE是否有效
created_atTIMESTAMPTZ创建时间
created_byVARCHAR(50)创建人 ID
notesTEXT备注

4 appointment_group_members — 分组成员表

将多个 appointment 关联到同一个 group 的桥接表。

字段类型约束说明
idSERIALPK自增主键
group_idINTEGERNOT NULL FK→appointment_groups分组 ID,CASCADE 删除
appointment_idVARCHAR(50)NOT NULL UNIQUE预约 ID(一个预约最多属于一个 group)
guest_idVARCHAR(50)NOT NULL客户 ID
is_primaryBOOLEANDEFAULT FALSE是否为主预约人(通知发送对象)
created_atTIMESTAMPTZ创建时间
设计决策:为什么不直接在 appointments 上加 group_id?

采用独立桥接表的"间接关联"设计,好处是 group 关系是可选的,不会污染基础 appointments 表结构。appointment_id 的 UNIQUE 约束确保一个预约最多属于一个 group。

Layer 2 — 账单层

5 invoices — 账单主表

每个 appointment(或 group)生成一张 invoice,作为支付的"总账"。这是折扣和支付金额的 Source of Truth

字段类型约束说明
idUUIDPK账单唯一 ID
invoice_numberVARCHAR(50)NOT NULL UNIQUE账单号 QQ_INV_YYYYMMDD_NNN
store_idVARCHAR(255)NOT NULL FK→centers门店 ID,RESTRICT 删除
appointment_idVARCHAR(255)FK→appointments关联的预约 ID,SET NULL 删除
invoice_typeinvoice_typeNOT NULL ENUM DEFAULT 'single'single(单人)/ group(多人)

金额字段

字段类型约束说明
subtotalDECIMAL(10,2)NOT NULL DEFAULT 0服务小计(税前)
discount_amountDECIMAL(10,2)NOT NULL DEFAULT 0折扣金额(税前)
tax_amountDECIMAL(10,2)NOT NULL DEFAULT 0税费金额
tip_amountDECIMAL(10,2)NOT NULL DEFAULT 0小费金额
total_amountDECIMAL(10,2)NOT NULL DEFAULT 0总金额
discount_detailsJSONBJSONB DEFAULT '[]' Migration 添加折扣详情 [{type, name, amount, baseAmount}]

支付追踪

字段类型约束说明
payment_statusinvoice_statusNOT NULL ENUM DEFAULT 'draft'draft / unpaid / partial / paid / void
paid_amountDECIMAL(10,2)NOT NULL DEFAULT 0已支付金额
item_countINTEGERNOT NULL DEFAULT 1子账单总数(Group Invoice 用)
paid_item_countINTEGERNOT NULL DEFAULT 0已支付子账单数量

创建 / 作废 / 时间戳

字段类型说明
created_byVARCHAR(255)创建人(FK→employees)
voided_byVARCHAR(255)作废人(FK→employees)
voided_atTIMESTAMPTZ作废时间
void_reasonTEXT作废原因
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间
discount_details 字段的由来

最初折扣信息只存在 transactions.discount_details 上。但页面刷新后,前端重新加载时如果 DiscountSection 被隐藏,就会导致折扣信息丢失。Migration 1771500000001discount_details 添加到了 invoices 表,使其成为折扣信息的 Source of Truth。

6 invoice_items — 子账单表

Group Invoice 下,每个客人拆出一个 invoice_item。支持"各付各的"的分人付款模式。

字段类型约束说明
idUUIDPK子账单 ID
invoice_idUUIDNOT NULL FK→invoices主账单 ID,CASCADE 删除
item_numberINTEGERNOT NULL子账单序号
guest_idVARCHAR(255)FK→guests客人 ID
guest_nameVARCHAR(100)客人姓名(快照,防改名影响历史)
servicesJSONBNOT NULL JSONB服务项目列表(见下方格式)
subtotalDECIMAL(10,2)NOT NULL服务小计
discount_amountDECIMAL(10,2)DEFAULT 0折扣金额
tax_amountDECIMAL(10,2)DEFAULT 0税费
tip_amountDECIMAL(10,2)DEFAULT 0小费
total_amountDECIMAL(10,2)NOT NULL总金额
payment_statusinvoice_item_statusENUM DEFAULT 'pending'pending / processing / paid / skipped / void
transaction_idUUIDFK→transactions关联交易 ID(支付后填入)
payment_methodpayment_method_typeENUM支付方式
paid_atTIMESTAMPTZ支付时间
skipped_atTIMESTAMPTZ跳过时间
skip_reasonVARCHAR(50)customer_request / card_declined / timeout
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间

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(分次付款、退款等)。

标识与关联

字段类型约束说明
idUUIDPK交易 ID
order_idVARCHAR(50)NOT NULL UNIQUE业务订单号 QQ_ORD_YYYYMMDD_NNN
appointment_idVARCHAR(255)FK→appointments预约 ID
invoice_idUUID主账单 ID
invoice_item_idUUID子账单 ID(分人付款时)
store_idVARCHAR(255)NOT NULL FK→centers门店 ID
customer_idVARCHAR(255)FK→guests客户 ID
employee_idVARCHAR(255)FK→employees处理员工 ID

金额信息

字段类型约束说明
subtotalDECIMAL(10,2)NOT NULL服务小计
discount_amountDECIMAL(10,2)DEFAULT 0折扣金额
tax_amountDECIMAL(10,2)DEFAULT 0税费金额
tip_amountDECIMAL(10,2)DEFAULT 0小费金额
total_amountDECIMAL(10,2)NOT NULL总金额(含税含折扣含小费)
refunded_amountDECIMAL(10,2)DEFAULT 0已退款金额

支付方式与状态

字段类型约束说明
payment_methodpayment_method_typeNOT NULL ENUMcredit_card / debit_card / cash / online_card
payment_statuspayment_status_typeENUM DEFAULT 'pending'pending / processing / completed / failed / cancelled / refunded / partial_refunded

CodePay 支付网关字段

字段类型说明
codepay_order_idVARCHAR(100)CodePay 订单号
codepay_trans_noVARCHAR(100)CodePay 交易号
card_typeVARCHAR(20)卡类型(VISA / MASTERCARD / AMEX)
card_last_fourVARCHAR(4)卡号后四位
terminal_snVARCHAR(50)终端序列号

现金支付 & 其他

字段类型说明
cash_receivedDECIMAL(10,2)现金收款金额
cash_changeDECIMAL(10,2)找零金额
discount_detailsJSONB折扣明细 JSON(冗余快照,invoice 上的为准)
itemsJSONB服务项目列表 JSON
idempotency_keyVARCHAR(100)幂等键(UNIQUE),防止重复扣款
notesTEXT备注 Migration 添加

错误与会话(Migration 添加)

字段类型说明
error_codeVARCHAR(50)支付失败错误代码
error_messageTEXT支付失败错误信息
cancel_reasonTEXT取消原因
cash_drawer_session_idUUID关联的收银会话 ID(FK→cash_drawer_sessions)

时间戳

字段类型说明
created_atTIMESTAMPTZ创建时间
paid_atTIMESTAMPTZ支付完成时间
updated_atTIMESTAMPTZ更新时间
idempotency_key 的作用

这是支付安全的关键字段。当 CodePay 回调可能重复到达时(网络重试),系统用 idempotency_key 做去重,防止同一笔支付被记录两次。前端每次发起支付都会生成唯一的 key。

8 transaction_history — 交易状态变更历史

每次 transaction 状态变化都记录一条历史,完整的审计日志。

字段类型约束说明
idUUIDPK历史记录 ID
transaction_idUUIDNOT NULL FK→transactions交易 ID,CASCADE 删除
statusVARCHAR(30)NOT NULL状态值(如 pending → processing → completed)
messageTEXT状态说明
raw_responseJSONBJSONBCodePay 或其他渠道的原始响应
changed_byVARCHAR(255)FK→employees操作人员 ID
created_atTIMESTAMPTZ记录时间

9 refunds — 退款记录表

支持全额和部分退款,支持刷卡 / 现金 / 礼品卡三种退款方式。

核心字段

字段类型约束说明
idUUIDPK退款 ID
refund_numberVARCHAR(50)NOT NULL UNIQUE退款单号 QQ_REF_YYYYMMDD_NNN
transaction_idUUIDNOT NULL FK→transactions原交易 ID(RESTRICT,不可删除有退款的交易)
refund_amountDECIMAL(10,2)NOT NULL退款金额
refund_reasonTEXT退款原因
refund_statusrefund_status_typeENUM DEFAULT 'pending'pending / processing / completed / failed
refund_typerefund_type_enumENUM DEFAULT 'card' Migration 添加card / cash / gift_card
is_cash_refundBOOLEANDEFAULT FALSE旧字段,新系统用 refund_type

CodePay 退款

字段类型说明
codepay_refund_noVARCHAR(100)CodePay 退款号

终端信息(Migration 添加 — 刷卡退款)

字段类型说明
terminal_idUUID终端 ID(FK→terminals)
terminal_snVARCHAR(100)终端序列号
card_typeVARCHAR(20)卡类型
card_last_fourVARCHAR(4)卡号后四位

礼品卡退款(Migration 添加)

字段类型说明
gift_card_idUUID礼品卡 ID(FK→gift_cards)
gift_card_numberVARCHAR(50)礼品卡号

处理人 & 时间戳

字段类型说明
processed_byVARCHAR(255)处理人(FK→employees)
approved_byVARCHAR(255)审批人(FK→employees,manager 级别)
created_atTIMESTAMPTZ创建时间
completed_atTIMESTAMPTZ完成时间

10 receipts — 收据记录表

支持打印 / 邮件 / 短信三种收据方式。

字段类型约束说明
idUUIDPK收据 ID
receipt_numberVARCHAR(50)NOT NULL UNIQUE收据号 QQ_RCP_YYYYMMDD_NNN
transaction_idUUIDNOT NULL FK→transactions交易 ID,CASCADE 删除
receipt_typereceipt_typeNOT NULL ENUMprint / email / sms
contentJSONBNOT NULL JSONB收据内容(店铺信息、服务明细、金额等)
sent_toVARCHAR(255)发送目标(email 或 phone)
sent_atTIMESTAMPTZ发送时间
send_statusVARCHAR(20)DEFAULT 'pending'pending / sent / failed
send_errorTEXT发送失败原因
created_atTIMESTAMPTZ创建时间
典型数据流

单人预约支付流程

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/