feat: add Condo owner desk frontend and backend

This commit is contained in:
Wyndham ARR
2026-08-02 13:33:13 +08:00
commit ed6229c203
54 changed files with 18640 additions and 0 deletions

390
backend-data-model.md Normal file
View File

@@ -0,0 +1,390 @@
# 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 写入路径。