391 lines
14 KiB
Markdown
391 lines
14 KiB
Markdown
# 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 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 |
|
||
|
||
等价计算公式:
|
||
|
||
```text
|
||
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. 推荐数据表
|
||
|
||
```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 | 自动规则为 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. 后端保存事务
|
||
|
||
前端提交:
|
||
|
||
```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 计算 multiplier;AC2 校验人工 multiplier。
|
||
6. 计算 `Use = Night × Multiplier`。
|
||
7. 开启事务并锁定对应 entitlement period,同时检查 row version。
|
||
8. 校验余额足够。
|
||
9. 写入 Usage Record 和 ledger debit;Confirmation 通过 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 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 明确不预建的规则
|
||
|
||
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 写入路径。
|