Files
wyndham-Condon/backend-data-model.md
2026-08-02 13:33:13 +08:00

391 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CONDO 后端数据库模型
更新日期2026-07-31
业务依据:当前前端页面、字段和运行规则
## 1. 设计原则
- 当前前端是生产数据库和 API 的业务规格。
- 生产表与本地导入预演分开;历史批次通过受控 importer 写入生产表,不把原始 Excel 作为数据库运行时依赖。
- 所有新对象仅位于现有 `booking_test` 数据库的 `condon` schema不改动其他 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
1. Confirmation No.
2. Check-in
3. Check-out
4. Night
5. Use
6. Balance
7. Used Room Type
8. Remark
关系为:
- `Night = Check-out - Check-in`
- `Use = Night × Multiplier`
- `Balance After = Balance Before - Use`
历史导入另外保留 `Room``Total` 和来源定位字段;前端新建记录仍默认 Room=1余额由后端维护。
## 3. 房型使用规则
### 3.0 酒店房型代码定义
| 代码 | 酒店定义 |
| --- | --- |
| RM1 | No balcony TWN4+4 F |
| RM2 | Superior King6F |
| RM3 | Superior TWN4+4 F |
| RM4 | Superior TWN6+4 F |
| UG1 | Deluxe King6F |
| UG2 | Deluxe TWN6+4 F |
| SU1 | Junior Suite King6F |
| SU2 | Junior Suite King6FPool view |
| SU6 | Junior Suite TWN4+4 F |
| SU3 | Two bedroom / Family room1 room King + 1 room TWN |
| AC1 | Handicap King |
| AC2 | Handicap TWN |
历史源文件中的泛称(如 `Superior Room``Deluxe Room``Junior Suite (One Bedroom)`)可能缺少 King/TWN/Pool view 信息,不能仅凭文字强制映射;历史原文应保留。当前已部署模型只初始化 AC2AC1 是否加入生产代码及其扣减规则需单独确认。
### 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 |
等价计算公式:
```text
Multiplier = max(1, Used Tier - Purchased Tier + 1)
```
后端必须根据 Owner Account 的 Purchased Room Type 和 Usage Record 的 Used Room Type 重新计算倍数。普通请求不能直接指定 multiplier。
购买或使用 AC2 时请求可以携带人工 multiplier后端校验范围为当前前端支持的 13。
新业务 Usage Record 保存实际使用的 `applied_multiplier``rule_version`legacy 导入记录使用 `applied_multiplier=NULL``rule_version=legacy-source`,以源 Use/Balance 为事实。
## 4. 推荐数据表
```mermaid
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 | 自动规则为 13AC2 为空 |
| `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` | 新记录为 13legacy 历史统一为空 |
| `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 Recordannual_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. 后端保存事务
前端提交:
```json
{
"ownerAccountId": "uuid",
"confirmationNo": "26090001",
"checkIn": "2026-09-01",
"checkOut": "2026-09-04",
"usedRoomType": "SU1",
"manualMultiplier": null,
"remark": "",
"idempotencyKey": "uuid"
}
```
后端处理:
1. 校验 idempotency key 是否已处理。
2. 校验 Confirmation No. 为纯数字且未使用。
3. 读取 Owner Account、Purchased Room Type 和当前权益期间。
4. 计算 Night。
5. 根据购买/使用房型 tier 计算 multiplierAC2 校验人工 multiplier。
6. 计算 `Use = Night × Multiplier`
7. 开启事务并锁定对应 entitlement period同时检查 row version。
8. 校验余额足够。
9. 写入 Usage Record 和 ledger debitConfirmation 通过 booking 1:N 关系复用。
10. 更新 current balance 和 row version。
11. 提交事务并返回完整 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
返回:
- `id`
- `accountNo`
- `transferDate`
- `name`
- `roomNo`
- `purchasedRoomType`
- `unitNo`
- `memberNo`
- `remainingStayPrivileges`
### Usage History
返回:
- `id`
- `confirmationNo`
- `ownerAccountId`
- `ownerName`
- `ownerRoomNo`
- `checkIn`
- `checkOut`
- `night`
- `use`
- `balance`
- `usedRoomType`
- `remark`
- `createdAt`
这组返回值可以直接支持全局 Usage History、账户详情、Confirmation 搜索和当前排序规则。
## 7. Dashboard 计算
- Owner roomsOwner 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 明确不预建的规则
1. Usage Record 取消、冲正和人工余额调整。
2. 房屋转让时的余额归属与历史账户处理。
3. 真实用户登录、角色和操作人追踪。
4. 跨权益年度的一次使用V1 返回明确错误。
5. 新历史批次的业务纠正与房型映射;已确认的 `(2)` 批次已导入,后续批次继续走预演流程。
这些能力以后通过独立迁移扩展,不在基础表中预先加入未经确认的字段。
## 10. 推荐实施顺序
1. 只读盘点 `booking_test`,确认目标 schema 和业务表状态。
2. 本地编写版本化迁移、精确回滚和 SQL allowlist 安全检查。
3. 应用 `001_create_condon_schema``002_legacy_import_and_bookings`
4. 生成最新工作簿本地导入预演批次,排除无 Confirmation 行并逐条校验。
5. 在空业务表中单事务导入 owner、权益期间、booking、legacy usage 和 ledger。
6. 通过只读逐条比对及 API smoke 后再开放前端读取。
7. 新增 usage 继续只调用数据库计算倍率的 v2 写入路径。