Files
quyun-v2/backend/specs/spec01.md

428 lines
20 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.
# 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”特性用户余额为全局属性`users.balance`),可在加入的任意租户内消费。
### 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 流水类型(建议)
- `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 (已移除)租户为用户充值
本项目不支持“租户管理员为用户充值”。余额为 users 全局余额,用户可在已加入租户内共享消费。
### 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) 增加用户全局余额(可用余额)
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并要求“失败回滚 + 幂等返回一致”
- 金额=仅 CNY6.A
## 12. 里程碑拆分(建议)
- M1TenantUser 余额隔离 + 充值入账 + 余额支付下单 + 订单查询 + 全额退款。
- 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`JSONhash、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`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
- 订单查询:
- `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、耗时、结果码。