# Spec 01：多租户媒体发布平台（余额隔离 / 订单与退款 / 内容定价）

> 目标：把“同一用户属于多个租户、租户可为用户充值、余额仅能在当前租户消费、租户管理员可查看订单并退款、管理员发布内容可配置价格与折扣”等需求落到可实现的业务规格，作为后续数据模型/API/权限/流程实现依据。

## 1. 背景与范围

### 1.1 背景
- 平台支持视频/音频/图片的媒体内容发布与售卖（或付费访问）。
- 平台面向多租户（Tenant）：不同租户之间的数据、资金、内容必须严格隔离。
- 同一用户（User）可加入多个租户；在每个租户内拥有独立的“租户内余额”用于消费该租户内的业务。
- 租户管理员（Tenant Admin）能对订单进行查看、发起退款等操作；管理员发布的内容可配置价格与折扣策略。

### 1.2 本 spec 覆盖
- 多租户身份与权限：同一用户多租户归属、租户内角色。
- 余额体系：租户为用户充值、租户内余额隔离、消费与退款回滚。
- 商品与订单：内容定价、折扣、下单、扣款、退款、订单审计。
- 媒体发布：内容/媒体资源的基础生命周期（上传、审核/上架、购买/访问）。

### 1.3 明确不做（暂定）
- 广告分发、推荐算法、复杂分账（创作者分成）、第三方支付直连（如微信/支付宝）细节。
- 版权确权、内容风控/涉政涉黄自动审核体系（仅预留状态与接口）。
- 跨租户资产迁移、跨租户合并结算。

## 2. 角色与核心术语

### 2.1 角色（Actors）
- 平台超级管理员（Super Admin）：平台级别的租户管理/风控/对账（可选，视项目现状）。
- 租户管理员（Tenant Admin）：租户内运营角色；发布内容、设置价格与折扣、查看订单与退款。
- 租户成员（Member）：属于某租户的普通用户；可消费该租户内容、可发布内容（若租户开放）。
- 游客/未加入租户用户：只能浏览公开内容（若允许），不可使用租户余额。

### 2.2 核心术语
- Tenant（租户）：逻辑隔离域，拥有自己的内容、订单、余额账本规则。
- TenantUser（租户成员关系）：User 在某个 Tenant 下的身份载体（含 role、balance、状态等）。
- Balance（余额）：以 TenantUser 为维度的可用余额；只可在当前 Tenant 内消费与退款回滚。
- Ledger（账本/流水）：所有余额变动必须落到可审计流水（增、减、冻结/解冻、退款）。
- Content（内容）：一条可出售/可访问的媒体内容实体（可关联多媒体资源）。
- MediaAsset（媒体资源）：视频/音频/图片的文件对象（含转码/封面/时长/尺寸等元数据）。
- Price（价格）：内容在某租户内的定价；折扣可作用于价格形成最终成交价。
- Order（订单）：用户在租户内对内容的购买/消费记录；支持退款。

## 3. 多租户与权限模型

### 3.1 多租户原则
- 所有业务数据必须带 `tenant_id`（或可推导的 tenant 归属），并在查询/写入时强制校验租户边界。
- 同一 user 在不同 tenant 下的余额、订单、内容访问权限互不影响。

### 3.2 租户内角色（最小集合）
- `member`：默认角色。
- `tenant_admin`：租户管理员。

> 备注：代码侧已有 `TenantUserRole`（member/tenant_admin），可以作为对齐基准。

### 3.3 权限矩阵（建议）
- member：
  - 可查看本租户可见内容（公开/已购买/订阅等策略见后文）。
  - 可用本租户余额购买内容/消费服务。
  - 可查看自己的订单与余额流水。
- tenant_admin：
  - 拥有 member 的所有权限。
  - 可创建/编辑/下架内容；配置价格与折扣。
  - 可查看租户内订单（按条件检索/导出）。
  - 可发起退款（遵循退款规则/风控规则）。
  - 可为租户内用户充值（如果业务允许“租户给用户发放额度”）。
- super admin（可选）：
  - 可查看全平台租户与全局审计（不在本 spec 强制实现，但建议预留）。

## 4. 余额体系（Tenant 内隔离）

### 4.1 账户维度
- 余额账户 = `TenantUser` 维度（tenant_id + user_id）。
- `balance_available`（可用余额）：可直接消费。
- `balance_frozen`（冻结余额，可选）：用于“待支付/待确认/争议期”等场景，避免并发重复扣款。

> 你已确认需要冻结机制（5.B）。当前项目已有 `tenant_users.balance`（bigint），可作为 `balance_available` 的第一版；建议新增 `balance_frozen`（bigint）并配套流水类型 `freeze/unfreeze`。

### 4.2 充值（租户为用户充值）
定义：租户向其成员发放/充值额度，用户只能在该租户内使用。

规则建议：
- 充值必须产生一条“充值订单”或“余额流水”（建议两者都存在：订单做业务视角，流水做账本视角）。
- 充值需支持幂等：相同外部业务单号/请求幂等键重复请求，不可重复入账。
- 充值来源（可枚举）：
  - `tenant_grant`：租户后台人工/批量发放额度。
  - `user_pay`：用户实际支付购买额度（本期不做，仅预留）。

### 4.3 消费（余额扣减）
定义：用户在租户内使用余额购买内容（或支付发布/服务费用）。

你已确认“消费=购买租户内付费内容”（1.A），本 spec 将仅覆盖 `content_purchase`，发布收费等其他计费暂不纳入本期范围。

关键规则：
- 订单必须绑定 `tenant_id`，扣款只能操作该 `tenant_id` 下的 `TenantUser` 余额。
- 扣款以“最终成交价”为准，成交价由“基础价 + 折扣/优惠”计算。
- 余额不足则拒绝下单/支付。
- 需要防止并发超卖/重复扣款：建议在创建支付时使用冻结余额或基于数据库事务 + 行锁。

### 4.4 退款（订单退款回滚余额）
定义：租户管理员可对订单发起退款，退款金额回到原租户余额账户。

规则建议：
- 退款必须可追溯：关联原订单、原扣款流水。
- 支持两种策略（二选一或都做）：
  1) 全额退款：仅允许对未消费/未解锁的内容退款。
  2) 部分退款：按已消费比例/争议裁决退款（需要更复杂的计费与权益计算）。
- 你已确认仅做“全额退款 + 限时间窗”（4.A）：
  - 默认规则：`refundable_until = paid_at + 24h`；
  - 管理侧强制退款：`tenant_admin` 可忽略时间窗强制退款（必须写明原因并强审计）。
- 退款结果必须幂等：同一笔退款请求不可重复入账。
- 退款权限：仅 `tenant_admin`（或更高）可操作；member 只能发起“退款申请”。

## 5. 内容与媒体模型

### 5.1 MediaAsset（媒体资源）
支持类型：
- video：原始文件 + 转码产物 + 封面 + 时长 + 分辨率 + 编码信息
- audio：原始文件 + 转码产物 + 时长 + 码率
- image：原始文件 + 缩略图 + 尺寸

最小字段建议：
- `id`
- `tenant_id`
- `user_id`
- `type`（video/audio/image）
- `storage_provider`、`bucket`、`object_key`（或 url）
- `status`（uploaded/processing/ready/failed/deleted）
- `meta`（JSON：时长/尺寸/码率/哈希等）
- `created_at/updated_at`

### 5.2 Content（内容）
一条内容可以关联 0..N 个媒体资源（例如：视频+封面图+音频）。

最小字段建议：
- `id`
- `tenant_id`
- `user_id`
- `title`、`description`
- `status`（draft/reviewing/published/unpublished/blocked）
- `visibility`（public/tenant_only/private；可扩展）
- `preview_seconds`（默认 60）
- `preview_downloadable`（默认 false）
- `published_at`
- `created_at/updated_at`

## 6. 定价与折扣

### 6.1 定价模型（建议）
价格以“最小货币单位”存储（例如分），避免浮点误差：
- `price_amount`（int64，单位分）
- `currency`（本期固定 CNY；多币种为后续扩展）

### 6.2 折扣模型（建议最小集合）
折扣针对内容的成交价计算，可先实现“单一折扣规则”：
- `discount_type`：
  - `none`
  - `percent`（如 20% off）
  - `amount`（立减）
- `discount_value`：percent(0-100) 或 amount(分)
- `discount_start_at` / `discount_end_at`（可选）

计算规则：
- percent：`final = price_amount * (100 - percent) / 100`
- amount：`final = max(0, price_amount - amount)`
- 必须记录下单时的“成交快照”（避免事后改价影响历史订单）。

### 6.3 谁能设置
- 仅 `tenant_admin` 可为其发布的内容设置/修改价格与折扣。
- 如果未来允许“作者发布但管理员定价”，需要在权限上增加“作者/运营”区分。

## 7. 订单模型（购买内容）

### 7.1 订单类型（建议）
为后续扩展预留 `order_type`：
- `content_purchase`：购买内容（本 spec 核心）
- `topup`：充值订单（租户发放/用户支付）
> 你已确认本期只做 1.A + 2.A，因此 `content_purchase` + `topup(tenant_grant)` 为主，`user_pay`/`service_fee` 仅作为未来扩展位保留。

### 7.2 订单状态（建议）
以余额支付为例（不接三方）：
- `created`：已创建待支付（可选，若立即扣款可跳过）
- `paid`：已扣款/支付成功
- `refunding`：退款处理中
- `refunded`：已退款
- `canceled`：已取消（未扣款）
- `failed`：失败（扣款失败/规则校验失败）

冻结机制下的状态约束建议：
- 进入 `paid` 前如发生错误（例如订单落库失败），必须执行 `unfreeze` 回滚冻结余额，并将订单标记为 `failed` 或不落库（但需保证幂等返回一致）。
  - 你已确认：订单创建成功但写 `debit_purchase` 失败时，不保留该订单记录；幂等返回“失败 + 已回滚冻结”。

### 7.3 订单字段建议
- `id`
- `tenant_id`
- `buyer_user_id`
- `order_type`
- `amount_original`（原价）
- `amount_discount`（优惠金额）
- `amount_paid`（实付）
- `currency`
- `status`
- `snapshot`（JSON：下单时价格/折扣/内容标题等快照）
- `created_at/paid_at/refunded_at`
> 订单操作者如需审计，建议使用 `operator_user_id`（例如后台代下单/代退款），或在 `snapshot` 中记录。

### 7.4 订单明细（OrderItem，建议）
内容购买通常 1 单 1 内容，但用明细便于扩展：
- `order_id`
- `content_id`
- `content_owner_user_id`（可选，用于后续分成/对账）
- `amount_line`（该行实付）
- `snapshot`（内容快照）

## 8. 余额流水（账本 Ledger）

### 8.1 设计原则
- 余额的每一次变化都必须有流水记录，可审计、可对账、可回放。
- 流水必须带 `tenant_id` 与 `tenant_user_id`（或 tenant_id+user_id）。
- 所有入账/出账/退款都强制关联“业务单据”（订单/退款单）。

### 8.2 流水类型（建议）
- `credit_topup`：充值入账
- `debit_purchase`：购买扣款
- `credit_refund`：退款回滚
- `freeze` / `unfreeze`：冻结/解冻（可选）
- `adjustment`：人工调账（需强审计）

### 8.3 幂等与一致性
- 流水表建议使用 `idempotency_key` 或 `biz_ref_type + biz_ref_id` 做唯一约束，防止重复入账。
- 扣款与订单状态更新必须在一个事务内完成（或使用可靠消息最终一致）。

## 9. 关键流程（建议版）

### 9.1 加入租户
1) user 通过邀请/申请加入 tenant
2) 创建 `TenantUser(tenant_id,user_id,role=member,balance=0)`
3) tenant_admin 可提升为 `tenant_admin`

### 9.2 租户为用户充值
1) tenant_admin 在后台选择 tenant_user、输入金额、填写备注/原因
2) 创建 `topup` 订单（或 topup 记录）
3) 写入 ledger：`credit_topup`
4) 增加 tenant_user.balance
5) 返回充值结果与可用余额

### 9.3 用户购买内容（余额支付）
1) buyer 选择 tenant 下某 content
2) 系统计算成交价（读取当前 price+discount），生成订单快照
3) 校验余额足够、内容可售、用户在该 tenant 下有效
4) 冻结余额：写入 ledger：`freeze`，`balance_available -= amount_paid`，`balance_frozen += amount_paid`
5) 创建订单（status=paid 或 created→paid）
6) 扣款落账：写入 ledger：`debit_purchase`，`balance_frozen -= amount_paid`（表示冻结转为最终扣款）
7) 写入“购买权益”（见 9.5）

失败回滚（必须）：
- 若步骤 4 成功而步骤 5/6 失败：必须写入 `unfreeze` 并回滚余额（`balance_available += amount_paid`，`balance_frozen -= amount_paid`），同时保证幂等键再次请求能返回最终一致结果。
- 若步骤 5 成功但步骤 6 失败：执行 `unfreeze` 回滚后不落库订单，并对该幂等键固定返回“失败+已回滚”（不可重复创建新订单）。

### 9.4 租户管理员退款
1) tenant_admin 选中订单，校验可退款（状态/风控/时间窗）
2) 创建退款记录（可选），订单状态→refunding
3) 写入 ledger：`credit_refund`（金额=退款金额）
4) 增加 tenant_user.balance（可用余额）
5) 订单状态→refunded，记录 refunded_at 与操作者
6) 收回/标记权益（若需要）

你已确认退款对权益的处理：
- 退款成功后，将对应 `content_access.status` 置为 `revoked`（立即失效）。

### 9.5 购买权益（访问控制）
最小实现建议：记录 `content_access`（tenant_id, content_id, user_id, order_id, status, created_at）。
- 访问内容时，校验：
  - content 是否公开（public）或
  - user 是否拥有 `content_access` 且有效

你已确认需要“试看/预览”（3.B），建议增加：
- `content_assets.role=preview`（或单独字段），允许未购买用户访问 preview 资源；
- 正片资源（main）仍需 `content_access` 校验；
- 订单快照中记录“当时预览策略”，避免策略变更导致争议。

预览边界建议（最小可用）：
- preview 资源必须与 main 资源彻底区分（不同 object_key / 不同转码模板），避免客户端绕过。
- preview 可以额外加频控/防盗链（后续），但不影响本期的租户隔离与权益校验。

你已确认的试看策略：
- 固定时长试看：默认前 `60s`（仅 streaming，不允许下载）。

## 10. 查询与后台能力（Tenant Admin）

### 10.1 订单查询
筛选条件建议：
- 时间范围（created_at/paid_at）
- buyer_user_id / 用户关键字
- content_id / 内容标题关键字
- 订单状态、订单类型
- 金额范围

### 10.2 退款操作
输入项建议：
- 退款金额（默认=实付）
- 退款原因（枚举 + 备注）
- 是否立即生效（余额体系通常立即入账）

输出与审计：
- 记录操作者、操作时间、原订单快照、退款快照、对应流水 id

## 11. 已确认的关键决策（用于锁定实现）

你已确认：
- 消费=购买租户内付费内容（1.A）
- 充值来源=仅租户后台发放额度（2.A）
- 内容支持试看/预览（3.B）：固定时长前 `60s`，不允许下载
- 退款=全额退款 + 默认时间窗 `paid_at + 24h`，且租户管理侧允许强制退款（需强审计）
- 余额需要冻结机制（5.B），并要求“失败回滚 + 幂等返回一致”
- 金额=仅 CNY（分）（6.A）

## 12. 里程碑拆分（建议）
- M1：TenantUser 余额隔离 + 充值入账 + 余额支付下单 + 订单查询 + 全额退款。
- M2：内容发布全链路（media asset/processing 状态）、内容访问权益、折扣生效与订单快照。
- M3：冻结/部分退款/风控规则/对账导出/批量充值。

## 13. 数据模型草案（供对齐）

> 注：字段名为建议，最终以现有项目的命名/生成器约束为准。金额统一用 `int64`（分）。

### 13.1 tenant_users（已存在，建议扩展方向）
- `tenant_id`, `user_id`
- `role`（member/tenant_admin）
- `balance`（可用余额，已存在）
- （可选）`balance_frozen`
- `status`（active/disabled/pending 等）

### 13.2 media_assets
- `id`, `tenant_id`, `user_id`, `type`
- `status`（uploaded/processing/ready/failed/deleted）
- `provider`, `bucket`, `object_key`（或 `url`）
- `meta`（JSON：hash、duration、width、height、bitrate、codec...）
- `created_at`, `updated_at`

### 13.3 contents
- `id`, `tenant_id`, `user_id`
- `title`, `description`
- `status`（draft/reviewing/published/unpublished/blocked）
- `visibility`（public/tenant_only/private）
- `published_at`, `created_at`, `updated_at`

### 13.4 content_assets（内容与媒体关联表）
- `tenant_id`, `content_id`, `asset_id`
- `role`（main/cover/preview 等）
- `sort`

### 13.5 content_prices（内容定价；或直接放 contents）
- `tenant_id`, `user_id`, `content_id`
- `currency`
- `price_amount`
- `discount_type`, `discount_value`
- `discount_start_at`, `discount_end_at`
- `updated_at`

### 13.6 orders / order_items
- orders：
  - `id`, `tenant_id`, `buyer_user_id`, `order_type`, `status`
  - `amount_original`, `amount_discount`, `amount_paid`, `currency`
  - `snapshot`（JSON），`idempotency_key`（唯一）
  - `created_at`, `paid_at`, `refunded_at`
- order_items：
  - `order_id`, `tenant_id`, `content_id`
  - `amount_line`
  - `snapshot`（JSON）

### 13.7 balance_ledgers（强烈建议新增）
- `id`, `tenant_id`, `user_id`（或 `tenant_user_id`）
- `direction`（credit/debit）
- `type`（credit_topup/debit_purchase/credit_refund/...）
- `amount`（正数）
- `balance_before`, `balance_after`（可选但强审计）
- `biz_ref_type`, `biz_ref_id`（唯一约束，幂等）
- `operator_user_id`（谁触发：admin/buyer/system）
- `note`, `created_at`

### 13.8 content_access（购买权益）
- `tenant_id`, `content_id`, `user_id`, `order_id`
- `status`（active/revoked/expired）
- `created_at`, `revoked_at`

## 14. API 草案（只描述意图，不锁死路径）

### 14.1 租户侧（Tenant Admin）
- 充值：
  - `POST /tenants/:tenant_id/users/:user_id/topup`（amount, note, idempotency_key）
- 订单查询：
  - `GET /tenants/:tenant_id/orders`（分页+筛选）
  - `GET /tenants/:tenant_id/orders/:id`
- 退款：
  - `POST /tenants/:tenant_id/orders/:id/refund`（amount?, reason, idempotency_key）
- 内容管理：
  - `POST /tenants/:tenant_id/contents`（草稿）
  - `PATCH /tenants/:tenant_id/contents/:id`（编辑/上架/下架）
  - `PUT /tenants/:tenant_id/contents/:id/price`（价格+折扣）

### 14.2 用户侧（Member）
- 浏览内容：
  - `GET /tenants/:tenant_id/contents`（可见列表）
  - `GET /tenants/:tenant_id/contents/:id`
- 购买：
  - `POST /tenants/:tenant_id/orders`（content_id, idempotency_key）
- 我的订单/余额：
  - `GET /tenants/:tenant_id/me/orders`
  - `GET /tenants/:tenant_id/me/balance`
  - `GET /tenants/:tenant_id/me/ledgers`

## 15. 边界条件与非功能要求（落地时容易踩坑）

### 15.1 并发与一致性
- 同一用户并发下单：必须避免“余额被扣成负数”（事务行锁/冻结机制/乐观锁三选一）。
- 同一幂等键重复请求：返回同一订单/同一结果，不重复扣款/入账。
- 价格/折扣被修改：历史订单不受影响，必须依赖订单快照。

### 15.2 多租户隔离
- 任何带 `order_id/content_id/asset_id` 的 API，都必须校验其 `tenant_id` 属于当前上下文租户。
- 后台“按用户查询订单/余额”也必须限定到租户维度，避免跨租户泄露。

### 15.3 金额与舍入
- 金额统一使用整数分；percent 折扣的舍入规则需要固定（建议向下取整），并写入订单快照。

### 15.4 审计与追责
- 充值、退款、调账必须记录 `operator_user_id`、原因、时间、请求来源。
- 建议为后台敏感操作增加二次确认/权限分级（后续）。

### 15.6 试看与禁下载（3.B 已确认）
- 必须从“资源形态”上实现禁下载：preview 仅提供 60s 的独立转码产物，不复用 main 资源。
- 媒体访问接口仅下发短时效播放凭证/地址（例如签名 URL/Token），避免返回可长期复用的直链。
- 客户端不提供“下载”入口不构成安全措施，服务端与存储策略必须生效。

### 15.5 可观测性
- 关键链路（下单扣款/退款入账/上传处理）打点日志：tenant_id、user_id、biz_ref、耗时、结果码。
