14 KiB
CONDO 后端数据库模型
更新日期:2026-07-31
业务依据:当前前端页面、字段和运行规则
1. 设计原则
- 当前前端是生产数据库和 API 的业务规格。
- 生产表与本地导入预演分开;历史批次通过受控 importer 写入生产表,不把原始 Excel 作为数据库运行时依赖。
- 所有新对象仅位于现有
booking_test数据库的condonschema;不改动其他 schema 的对象、数据、角色或权限。 - 一个 Usage Record 代表一间房的一次使用。
- 每个业主房号独立管理权益余额。
- 所有 Use 和 Balance 都由后端计算,不能信任前端传来的结果。
- V1 只实现实时新增使用记录;取消、冲正、调整、账户状态和用户角色不预建。
2. 当前前端字段
2.1 Owner Account
- No.
- Transfer Date
- Name
- Room No.
- Room Type
- Unit No.
- Member No.
- Remaining stay privileges
2.2 Usage History
- Confirmation No.
- Check-in
- Check-out
- Night
- Use
- Balance
- Used Room Type
- Remark
关系为:
Night = Check-out - Check-inUse = Night × MultiplierBalance After = Balance Before - Use
历史导入另外保留 Room、Total 和来源定位字段;前端新建记录仍默认 Room=1,余额由后端维护。
3. 房型使用规则
3.0 酒店房型代码定义
| 代码 | 酒店定义 |
|---|---|
| RM1 | No balcony TWN(4+4 F) |
| RM2 | Superior King(6F) |
| RM3 | Superior TWN(4+4 F) |
| RM4 | Superior TWN(6+4 F) |
| UG1 | Deluxe King(6F) |
| UG2 | Deluxe TWN(6+4 F) |
| SU1 | Junior Suite King(6F) |
| SU2 | Junior Suite King(6F),Pool view |
| SU6 | Junior Suite TWN(4+4 F) |
| SU3 | Two bedroom / Family room(1 room King + 1 room TWN) |
| AC1 | Handicap King |
| AC2 | Handicap TWN |
历史源文件中的泛称(如 Superior Room、Deluxe Room、Junior Suite (One Bedroom))可能缺少 King/TWN/Pool view 信息,不能仅凭文字强制映射;历史原文应保留。当前已部署模型只初始化 AC2,AC1 是否加入生产代码及其扣减规则需单独确认。
3.1 房型分级
| Tier | 房型 |
|---|---|
| 1 | RM1、RM2、RM3、RM4、UG1、UG2 |
| 2 | SU1、SU2、SU6 |
| 3 | SU3 |
AC2 不属于自动分级,继续采用当前前端的人工倍数规则。
3.2 自动倍数矩阵
| 购买房型 | 使用 RM/UG | 使用 SU1/SU2/SU6 | 使用 SU3 |
|---|---|---|---|
| RM/UG | 1 | 2 | 3 |
| SU1/SU2/SU6 | 1 | 1 | 2 |
| SU3 | 1 | 1 | 1 |
等价计算公式:
Multiplier = max(1, Used Tier - Purchased Tier + 1)
后端必须根据 Owner Account 的 Purchased Room Type 和 Usage Record 的 Used Room Type 重新计算倍数。普通请求不能直接指定 multiplier。
购买或使用 AC2 时请求可以携带人工 multiplier,后端校验范围为当前前端支持的 1–3。
新业务 Usage Record 保存实际使用的 applied_multiplier 和 rule_version;legacy 导入记录使用 applied_multiplier=NULL、rule_version=legacy-source,以源 Use/Balance 为事实。
4. 推荐数据表
erDiagram
ROOM_TYPES ||--o{ OWNER_ACCOUNTS : purchased_as
OWNER_ACCOUNTS ||--o{ ENTITLEMENT_PERIODS : owns
BOOKINGS ||--o{ USAGE_RECORDS : contains
OWNER_ACCOUNTS ||--o{ USAGE_RECORDS : creates
ROOM_TYPES ||--o{ USAGE_RECORDS : used_as
ENTITLEMENT_PERIODS ||--o{ USAGE_RECORDS : applies_to
ENTITLEMENT_PERIODS ||--o{ ENTITLEMENT_LEDGER : records
USAGE_RECORDS ||--o| ENTITLEMENT_LEDGER : deducts
4.0 condon.bookings
一条 Confirmation 对应一条 booking;同一 booking 可以关联多条 usage_records。因此 usage_records.confirmation_no 不设唯一约束,而是引用本表主键。
| 字段 | 类型 | 说明 |
|---|---|---|
confirmation_no |
varchar(64) PK |
预订 Confirmation,当前新业务要求纯数字 |
created_at |
timestamptz |
首次出现时间 |
4.1 condon.room_types
| 字段 | 类型 | 说明 |
|---|---|---|
code |
varchar(16) PK |
RM3、SU1、AC2 等 |
entitlement_tier |
smallint nullable |
自动规则为 1–3;AC2 为空 |
requires_manual_multiplier |
boolean |
AC2 为 true,其他房型为 false |
created_at |
timestamptz |
创建时间 |
updated_at |
timestamptz |
更新时间 |
约束:
- 自动房型必须
entitlement_tier IN (1, 2, 3)且requires_manual_multiplier = false。 - 人工房型必须
entitlement_tier IS NULL且requires_manual_multiplier = true。 - 初始化代码仅包含 RM1、RM2、RM3、RM4、UG1、UG2、SU1、SU2、SU6、SU3、AC2。
4.2 condon.owner_accounts
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid PK |
内部不可变主键 |
account_no |
integer nullable |
前端 No. |
transfer_date |
date nullable |
Transfer Date |
owner_name |
text |
前端显示名称 |
room_no |
varchar(32) |
业主房号 |
purchased_room_type_code |
FK | 购买房型 |
unit_no |
varchar(32) |
Unit No. |
member_no |
varchar(32) |
Member No.,按文本保存 |
created_at |
timestamptz |
创建时间 |
updated_at |
timestamptz |
更新时间 |
约束:
account_no在非空时唯一。room_no唯一。purchased_room_type_code引用condon.room_types(code)。- 姓名、Unit No.、Member No. 不作为内部主键。
- 本表不保存余额;余额的单一权威来源是
condon.entitlement_periods.current_balance。
4.3 condon.entitlement_periods
每个 Owner Account 每个权益年度一条记录。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid PK |
权益期间 ID |
owner_account_id |
FK | 所属业主账户 |
period_year |
smallint |
权益年度 |
period_start |
date |
当前规则为 1 月 1 日 |
period_end |
date |
当前规则为 12 月 31 日 |
annual_grant |
integer |
当前规则为 15 晚 |
carry_forward |
integer |
上期结转 |
current_balance |
integer |
唯一权威余额缓存 |
row_version |
integer |
防止并发重复扣减 |
created_at |
timestamptz |
创建时间 |
updated_at |
timestamptz |
更新时间 |
约束:
(owner_account_id, period_year)唯一。period_start和period_end必须分别为该年的 1 月 1 日和 12 月 31 日。annual_grant >= 0。carry_forward >= 0。current_balance >= 0。row_version >= 1。
current_balance 用于快速显示;所有余额变化必须与 ledger 在同一事务内完成。
4.4 condon.usage_records
一行对应前端 Usage History 的一行。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid PK |
内部记录 ID |
owner_account_id |
FK | 所属 Owner Account |
entitlement_period_id |
FK | 扣减的权益年度 |
confirmation_no |
varchar(64) |
Confirmation No. |
check_in |
date |
Check-in |
check_out |
date |
Check-out |
night_count |
integer |
Night |
used_room_type_code |
FK nullable | 可映射到标准代码的 Used Room Type;历史泛称可为空 |
raw_used_room_type |
text |
源文件原始房型文本 |
applied_multiplier |
smallint nullable |
新记录为 1–3;legacy 历史统一为空 |
rule_version |
varchar(32) |
规则版本 |
use_nights |
integer |
Use |
balance_before |
integer |
扣减前余额 |
balance_after |
integer |
Balance |
room_count |
integer |
源历史 Room;当前最新批次均为 1 |
remark |
text |
Remark |
idempotency_key |
uuid |
防止重复请求 |
source_sheet |
varchar(128) nullable |
Excel 子表名 |
source_row |
integer nullable |
Excel 行号 |
source_sequence |
integer nullable |
子表 No.,保留源顺序 |
import_batch |
varchar(128) nullable |
导入批次标识 |
created_at |
timestamptz |
录入时间 |
约束:
confirmation_no引用condon.bookings,满足当前前端的纯数字规则;同一号允许多条 usage。check_out > check_in。night_count = check_out - check_in。- 新记录
applied_multiplier BETWEEN 1 AND 3,且use_nights = night_count × applied_multiplier。 - legacy 记录
applied_multiplier IS NULL,源Use、Total、Balance原样保存。 balance_before >= use_nights。balance_after = balance_before - use_nights。balance_after >= 0。idempotency_key唯一。- Check-in 和 Check-out 必须在同一个权益年度内;跨年度请求在 V1 明确拒绝。
4.5 condon.entitlement_ledger
保存每一次年度新增、结转和使用扣减。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid PK |
流水 ID |
entitlement_period_id |
FK | 所属权益期间 |
usage_record_id |
FK nullable | 使用扣减对应的记录 |
entry_type |
varchar(20) |
annual_grant、carry_forward、usage |
delta_nights |
integer |
增加为正,扣减为负 |
balance_before |
integer |
变动前余额 |
balance_after |
integer |
变动后余额 |
occurred_at |
timestamptz |
发生时间 |
约束:
balance_after = balance_before + delta_nights。balance_after >= 0。- 一条 Usage Record 只能产生一笔 usage debit。
- annual_grant 和非零 carry_forward 为正数,usage 为负数。
- usage 必须引用 Usage Record;annual_grant 和 carry_forward 不引用 Usage Record。
4.6 condon.schema_migrations
仅记录 condon 自身的迁移版本,不借用 public。
| 字段 | 类型 | 说明 |
|---|---|---|
version |
varchar(64) PK |
迁移版本 |
checksum |
varchar(64) |
SQL 内容校验值 |
applied_at |
timestamptz |
应用时间 |
导入批次和拒绝行保存在仓库内的本地审计产物中;数据库只保存已接受记录的 import_batch 与来源定位字段。
5. 后端保存事务
前端提交:
{
"ownerAccountId": "uuid",
"confirmationNo": "26090001",
"checkIn": "2026-09-01",
"checkOut": "2026-09-04",
"usedRoomType": "SU1",
"manualMultiplier": null,
"remark": "",
"idempotencyKey": "uuid"
}
后端处理:
- 校验 idempotency key 是否已处理。
- 校验 Confirmation No. 为纯数字且未使用。
- 读取 Owner Account、Purchased Room Type 和当前权益期间。
- 计算 Night。
- 根据购买/使用房型 tier 计算 multiplier;AC2 校验人工 multiplier。
- 计算
Use = Night × Multiplier。 - 开启事务并锁定对应 entitlement period,同时检查 row version。
- 校验余额足够。
- 写入 Usage Record 和 ledger debit;Confirmation 通过 booking 1:N 关系复用。
- 更新 current balance 和 row version。
- 提交事务并返回完整 Usage History 数据。
任一步失败都整体回滚,避免出现“余额已扣但记录未保存”或相反的情况。
前端传来的 Night、Use、Balance 和自动 multiplier 只用于即时预览,不能作为后端权威值。
数据库函数:
condon.calculate_multiplier(...):返回自动倍率或校验 AC2 手动倍率。condon.open_entitlement_period(...):按 15 晚年度新增和上一年度余额结转建立新期间,写入年度新增 ledger,并在结转大于 0 时写入结转 ledger。condon.create_usage_record(...):完成计算、行锁、余额验证、usage/ledger 写入和余额更新。
三个函数均使用显式 schema 名、固定安全 search_path 和调用者权限,不修改角色或全局权限。
6. API 读取模型
Owner Accounts
返回:
idaccountNotransferDatenameroomNopurchasedRoomTypeunitNomemberNoremainingStayPrivileges
Usage History
返回:
idconfirmationNoownerAccountIdownerNameownerRoomNocheckIncheckOutnightusebalanceusedRoomTyperemarkcreatedAt
这组返回值可以直接支持全局 Usage History、账户详情、Confirmation 搜索和当前排序规则。
7. Dashboard 计算
- Owner rooms:Owner Account 数量。
- Remaining privileges:当前 entitlement period 的
current_balance总和。 - Used this year:当前期间 Usage Record 的
use_nights总和。 - Room Type:按 Owner Account 的 purchased room type 计数。
- Used Room Type:按 Usage Record 的
night_count汇总实际房晚;一条记录代表一间房,所以不乘 Room 数量。 - 月度 Use:按 Check-in 月份汇总
use_nights。
所有 Dashboard 指标由同一权益期间和已保存的 Usage Record 生成,不能继续使用静态图表值。
8. 推荐索引
owner_accounts(account_no)唯一(允许多个 NULL)。owner_accounts(room_no)唯一。owner_accounts(member_no)。owner_accounts(purchased_room_type_code)。entitlement_periods(owner_account_id, period_year)唯一。bookings(confirmation_no)主键;usage 通过 Confirmation 外键关联。usage_records(import_batch, source_sheet, source_row)部分唯一索引。usage_records(owner_account_id, created_at, id)。usage_records(entitlement_period_id)。usage_records(used_room_type_code, check_in)。entitlement_ledger(entitlement_period_id, occurred_at, id)。entitlement_ledger(usage_record_id)唯一(允许 annual_grant/carry_forward 为 NULL)。
9. V1 明确不预建的规则
- Usage Record 取消、冲正和人工余额调整。
- 房屋转让时的余额归属与历史账户处理。
- 真实用户登录、角色和操作人追踪。
- 跨权益年度的一次使用;V1 返回明确错误。
- 新历史批次的业务纠正与房型映射;已确认的
(2)批次已导入,后续批次继续走预演流程。
这些能力以后通过独立迁移扩展,不在基础表中预先加入未经确认的字段。
10. 推荐实施顺序
- 只读盘点
booking_test,确认目标 schema 和业务表状态。 - 本地编写版本化迁移、精确回滚和 SQL allowlist 安全检查。
- 应用
001_create_condon_schema与002_legacy_import_and_bookings。 - 生成最新工作簿本地导入预演批次,排除无 Confirmation 行并逐条校验。
- 在空业务表中单事务导入 owner、权益期间、booking、legacy usage 和 ledger。
- 通过只读逐条比对及 API smoke 后再开放前端读取。
- 新增 usage 继续只调用数据库计算倍率的 v2 写入路径。