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

20 KiB
Raw Blame History

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:租户管理员。

备注:代码侧已有 TenantUserRolemember/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.balancebigint可作为 balance_available 的第一版;建议新增 balance_frozenbigint并配套流水类型 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
  • typevideo/audio/image
  • storage_providerbucketobject_key(或 url
  • statusuploaded/processing/ready/failed/deleted
  • metaJSON时长/尺寸/码率/哈希等)
  • created_at/updated_at

5.2 Content内容

一条内容可以关联 0..N 个媒体资源(例如:视频+封面图+音频)。

最小字段建议:

  • id
  • tenant_id
  • user_id
  • titledescription
  • statusdraft/reviewing/published/unpublished/blocked
  • visibilitypublic/tenant_only/private可扩展
  • preview_seconds(默认 60
  • preview_downloadable(默认 false
  • published_at
  • created_at/updated_at

6. 定价与折扣

6.1 定价模型(建议)

价格以“最小货币单位”存储(例如分),避免浮点误差:

  • price_amountint64单位分
  • currency(本期固定 CNY多币种为后续扩展

6.2 折扣模型(建议最小集合)

折扣针对内容的成交价计算,可先实现“单一折扣规则”:

  • discount_type
    • none
    • percent(如 20% off
    • amount(立减)
  • discount_valuepercent(0-100) 或 amount(分)
  • discount_start_at / discount_end_at(可选)

计算规则:

  • percentfinal = price_amount * (100 - percent) / 100
  • amountfinal = 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
  • snapshotJSON下单时价格/折扣/内容标题等快照)
  • 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_idtenant_user_id(或 tenant_id+user_id
  • 所有入账/出账/退款都强制关联“业务单据”(订单/退款单)。

8.2 流水类型(建议)

  • debit_purchase:购买扣款
  • credit_refund:退款回滚
  • freeze / unfreeze:冻结/解冻(可选)
  • adjustment:人工调账(需强审计)

8.3 幂等与一致性

  • 流水表建议使用 idempotency_keybiz_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. 冻结余额:写入 ledgerfreezebalance_available -= amount_paidbalance_frozen += amount_paid
  5. 创建订单status=paid 或 created→paid
  6. 扣款落账:写入 ledgerdebit_purchasebalance_frozen -= amount_paid(表示冻结转为最终扣款)
  7. 写入“购买权益”(见 9.5

失败回滚(必须):

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

9.4 租户管理员退款

  1. tenant_admin 选中订单,校验可退款(状态/风控/时间窗)
  2. 创建退款记录可选订单状态→refunding
  3. 写入 ledgercredit_refund(金额=退款金额)
  4. 增加用户全局余额(可用余额)
  5. 订单状态→refunded记录 refunded_at 与操作者
  6. 收回/标记权益(若需要)

你已确认退款对权益的处理:

  • 退款成功后,将对应 content_access.status 置为 revoked(立即失效)。

9.5 购买权益(访问控制)

最小实现建议:记录 content_accesstenant_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
  • rolemember/tenant_admin
  • balance(可用余额,已存在)
  • (可选)balance_frozen
  • statusactive/disabled/pending 等)

13.2 media_assets

  • id, tenant_id, user_id, type
  • statusuploaded/processing/ready/failed/deleted
  • provider, bucket, object_key(或 url
  • metaJSONhash、duration、width、height、bitrate、codec...
  • created_at, updated_at

13.3 contents

  • id, tenant_id, user_id
  • title, description
  • statusdraft/reviewing/published/unpublished/blocked
  • visibilitypublic/tenant_only/private
  • published_at, created_at, updated_at

13.4 content_assets内容与媒体关联表

  • tenant_id, content_id, asset_id
  • rolemain/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
    • snapshotJSONidempotency_key(唯一)
    • created_at, paid_at, refunded_at
  • order_items
    • order_id, tenant_id, content_id
    • amount_line
    • snapshotJSON

13.7 balance_ledgers强烈建议新增

  • id, tenant_id, user_id(或 tenant_user_id
  • directioncredit/debit
  • typedebit_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
  • statusactive/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/refundamount?, 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/orderscontent_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、耗时、结果码。