# 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 写入路径。