492 lines
25 KiB
Markdown
492 lines
25 KiB
Markdown
# Resource And Resource Group Architecture
|
||
|
||
本文定义资源、资源组、礼物配置和用户资源赠送的后端边界。资源类型固定为头像框、金币、座驾、气泡、徽章、飘屏、礼物;资源组是一组资源和钱包资产项的集合,可以一次性赠送给用户。实现必须以这里的服务归属、数据边界和 App/Admin 接口边界为准。
|
||
|
||
## Goals
|
||
|
||
- 建立统一资源库,头像框、金币、座驾、气泡、徽章、飘屏、礼物都从资源库中选择,不允许礼物配置直接填写散落的图片或动画 URL。
|
||
- 资源组只表达“打包集合”,赠送资源组时按当时组内容展开成多条资源赠送事实;组内可以直接配置 `COIN` 和 `DIAMOND` 钱包资产数量。
|
||
- 资源和资源组都可以通过后台命令赠送给用户,赠送必须可审计、可幂等、可追溯。
|
||
- 金币仍由 `wallet-service` 资产账本管理;资源库里的金币资源只是对 `COIN` 入账规则的配置包装。
|
||
- 礼物价格、收费资产、礼物热度、主播积分和有效期仍由 `wallet-service` 结算,客户端不能提交价格、收费类型、热度或积分。
|
||
- 按 `app_code` 隔离资源、资源组、礼物配置、赠送和用户持有状态。
|
||
|
||
## Non-Goals
|
||
|
||
- 首版不新增独立 `resource-service` 微服务。资源 catalog、资源组、礼物绑定、用户资源背包和赠送命令放在 `wallet-service` resource domain 中。
|
||
- 首版不做资源交易市场、用户之间转赠、资源回收兑换、资源合成。
|
||
- 首版不做资源组嵌套资源组,避免递归、循环引用和赠送展开的不确定性。
|
||
- 首版不让客户端直接提交资源 URL、资源组内容、金币数量或资源有效期。
|
||
- 首版不把资源事实写入 `hyapp_admin` 库;后台库只保存操作审计。
|
||
|
||
## Service Ownership
|
||
|
||
| Service | Owns | Does Not Own |
|
||
| --- | --- | --- |
|
||
| `wallet-service` | 资源 catalog、资源组、资源组成员、礼物绑定、用户资源持有、资源/资源组赠送、金币资源入账、资源赠送 outbox | 用户资料、房间状态、后台账号权限 |
|
||
| `room-service` | 房间礼物命令、房间热度、本地榜、房间礼物事件 | 礼物价格、礼物资源配置、用户资源背包 |
|
||
| `gateway-service` | App HTTP envelope、鉴权、把当前用户和 `app_code` 传给内部服务 | 资源配置事实、赠送落账 |
|
||
| `server/admin` | 后台鉴权、权限、表单校验、操作审计、调用 wallet-service 管理 RPC | 资源 catalog 权威事实、用户资源余额或背包事实 |
|
||
| `user-service` | 用户主数据、区域、账号状态 | 用户资源背包、金币资产、礼物配置 |
|
||
|
||
后台管理资源或资源组时,`server/admin` 必须先做后台权限判断并写 `admin_operation_logs`,然后调用 `wallet-service` 管理命令。不要把资源配置和赠送结果落到 admin DB,也不要让 `server/admin` 直接修改用户背包表。
|
||
|
||
## Core Concepts
|
||
|
||
| Concept | Meaning |
|
||
| --- | --- |
|
||
| `resource` | 可配置、可引用、可赠送的最小资源。头像框、资料卡、金币、座驾、气泡、徽章、飘屏、礼物都属于资源。 |
|
||
| `resource_group` | 一组资源和钱包资产项的集合。赠送资源组时展开为组内每个资源或资产赠送项。 |
|
||
| `resource_group_item` | 资源组成员。`item_type=resource` 引用资源库资源;`item_type=wallet_asset` 直接配置 `COIN` 或 `DIAMOND` 数量。 |
|
||
| `gift_config` | 礼物配置是一个绑定了 `resource_type=gift` 资源的业务配置,并额外拥有礼物类型、有效期、特效、价格、收费资产、热度和主播积分规则。 |
|
||
| `user_resource` | 用户持有的非金币资源权益,例如某个头像框 30 天、某个徽章永久。 |
|
||
| `wallet_asset_resource` | 会转成钱包资产入账的资源,例如金币。实际余额仍写 `wallet_accounts`。 |
|
||
| `grant` | 后台或系统向用户发放资源或资源组的命令事实。 |
|
||
|
||
资源类型固定为枚举,不允许后台随意输入新字符串:
|
||
|
||
| Type | Example | Grant Result |
|
||
| --- | --- | --- |
|
||
| `avatar_frame` | 头像框 | 写入 `user_resource_entitlements` |
|
||
| `profile_card` | 资料卡 | 写入 `user_resource_entitlements` |
|
||
| `coin` | 金币包 | `wallet_accounts.asset_type=COIN` 入账 |
|
||
| `vehicle` | 座驾 | 写入 `user_resource_entitlements` |
|
||
| `chat_bubble` | 气泡 | 写入 `user_resource_entitlements` |
|
||
| `badge` | 徽章 | 写入 `user_resource_entitlements` |
|
||
| `floating_screen` | 飘屏 | 写入 `user_resource_entitlements`,使用时由服务端触发飘屏展示 |
|
||
| `gift` | 礼物 | 写入 `user_resource_entitlements`;礼物配置只能绑定这类资源 |
|
||
|
||
## Gift Boundary
|
||
|
||
礼物不是独立上传资源。新增礼物必须从 `resources` 中选择一个 `resource_type=gift` 的资源,然后配置礼物类型、有效期、特效、价格、收费资产和结算值。
|
||
|
||
礼物拆成三层:
|
||
|
||
| Layer | Owner | Fields |
|
||
| --- | --- | --- |
|
||
| 礼物配置 | `wallet-service` resource domain | `gift_id`、`resource_id`、展示名、排序、上下架状态、礼物类型、有效期、特效、客户端表现 metadata |
|
||
| 礼物区域配置 | `wallet-service` resource domain | `gift_id` 和多个 `region_id` 的映射;`region_id=0` 表示 GLOBAL 兜底区域 |
|
||
| 礼物账务价格 | `wallet-service` ledger domain | `gift_id`、`price_version`、`charge_asset_type`、`coin_price`、`gift_point_amount`、`heat_value`、价格生效时间 |
|
||
|
||
礼物类型 code 固定为:
|
||
|
||
| Code | Label |
|
||
| --- | --- |
|
||
| `normal` | 普通礼物 |
|
||
| `cp` | CP礼物 |
|
||
| `lucky` | 幸运礼物 |
|
||
| `super_lucky` | 超级幸运礼物 |
|
||
| `exclusive` | 专属礼物 |
|
||
| `noble` | 贵族礼物 |
|
||
| `flag` | 国旗礼物 |
|
||
| `activity` | 活动礼物 |
|
||
| `magic` | 魔法礼物 |
|
||
| `custom` | 定制礼物 |
|
||
|
||
礼物收费资产只允许 `COIN` 和 `DIAMOND`。字段名沿用 `coin_price` 表达“价格数值”,实际扣费资产由 `charge_asset_type` 决定。`DebitGiftResponse.coin_spent` 保留兼容旧调用方;新调用方应优先读取 `charge_asset_type` 和 `charge_amount`。
|
||
|
||
礼物特效 code 固定为:
|
||
|
||
| Code | Label |
|
||
| --- | --- |
|
||
| `animation` | 动画 |
|
||
| `music` | 音乐 |
|
||
| `global_broadcast` | 全局广播 |
|
||
|
||
`gift_configs.effective_from_ms` 和 `gift_configs.effective_to_ms` 表达礼物配置有效期。任一字段为 `0` 表示该方向不限制;两个字段都为 `0` 表示长期有效。App 礼物列表和 `DebitGift` 送礼扣费都必须过滤未开始或已结束的 active 礼物。后台列表可以展示全部礼物,便于运营提前配置或排查历史配置。
|
||
|
||
`SendGift` 仍然只接收 `gift_id/gift_count`。room-service 必须把房间 `visible_region_id` 传给 wallet-service,wallet-service 必须验证礼物 active、在有效期内且在该区域可用,再使用服务端价格按 `charge_asset_type` 扣 `COIN` 或 `DIAMOND`,用服务端资源快照发房间消息。客户端不能提交资源 URL、礼物价格、收费资产、热度、主播积分、有效期、特效或礼物区域。
|
||
|
||
礼物绑定资源时的约束:
|
||
|
||
- 一个 active 礼物必须绑定一个 active resource。
|
||
- 礼物绑定的 resource 必须 `resource_type=gift`。
|
||
- 下架 resource 不自动删除历史礼物交易,只阻止新礼物上架或继续发送。
|
||
- 修改礼物 resource 后,新发送使用新 resource;历史礼物事件使用发送时快照,不反查当前配置。
|
||
- 钱包落账 metadata 必须保存 `gift_id`、`price_version`、`charge_asset_type`、`charge_amount`、`coin_price`、`gift_point_amount`、`heat_value`。房间礼物事件必须保存必要的 `resource_snapshot_json` 供客户端展示和审计。
|
||
|
||
## Grant Boundary
|
||
|
||
资源和资源组都可以直接赠送给用户。赠送必须是服务端命令,不允许客户端自助拼装。
|
||
|
||
赠送资源:
|
||
|
||
1. `server/admin` 鉴权并透传操作端前端按单次赠送动作生成的 `command_id`。
|
||
2. `wallet-service` 锁定资源配置,确认 active、grantable、app_code 匹配。
|
||
3. 如果是 `coin`,在同一个钱包事务中写 `wallet_transactions/wallet_entries/wallet_outbox`。
|
||
4. 如果是非金币资源,写 `resource_grants/resource_grant_items/user_resource_entitlements/resource_outbox`。
|
||
5. 返回 grant 结果和每个资源项结果。
|
||
|
||
赠送资源组:
|
||
|
||
1. 锁定 active `resource_groups` 和对应的 `resource_group_items`。
|
||
2. 按当时组内容展开资源项,并保存 `group_snapshot_json`。
|
||
3. `item_type=resource` 按资源自身 grant 规则处理。
|
||
4. `item_type=wallet_asset` 只允许 `COIN` 或 `DIAMOND`,必须写 `wallet_transactions/wallet_entries/wallet_outbox`。
|
||
5. 任一资源项失败,整个资源组赠送失败并回滚;不能出现只发了一半的资源组。
|
||
|
||
幂等规则:
|
||
|
||
- `resource_grants.command_id` 在同一 `app_code` 下唯一。
|
||
- 同一 `command_id` 重试且 payload 完全一致,返回原 grant 结果。
|
||
- 同一 `command_id` 携带不同目标用户、资源、资源组、数量或有效期,返回冲突。
|
||
- 资源组赠送的幂等 payload 必须包含展开前的 `group_id` 和后台提交参数;组内容快照以第一次成功提交为准。
|
||
|
||
## Expiration And Stacking
|
||
|
||
资源有效期不能由客户端提交。后台或系统命令可以选择服务端允许的 `duration_ms`,最终策略由资源配置决定。
|
||
|
||
建议资源配置 `grant_strategy`:
|
||
|
||
| Strategy | Meaning |
|
||
| --- | --- |
|
||
| `wallet_credit` | 转成钱包资产入账,只适用于 `coin` 等资产资源。 |
|
||
| `new_entitlement` | 每次发放生成一条独立权益,适合限时体验券。 |
|
||
| `extend_expiry` | 同一用户同一资源合并延长有效期,适合头像框、气泡、座驾。 |
|
||
| `increase_quantity` | 增加数量,适合礼物、飘屏等消耗型资源。 |
|
||
| `set_active_flag` | 发放后永久持有,适合徽章。 |
|
||
|
||
首版 App 使用资源时,只读取当前有效权益:
|
||
|
||
- `status=active`
|
||
- `effective_at_ms <= now`
|
||
- `expires_at_ms IS NULL OR expires_at_ms > now`
|
||
- 数量型资源必须 `remaining_quantity > 0`
|
||
|
||
资源过期不需要物理删除。展示和使用时按时间过滤,后台任务可以异步把过期权益标记为 `expired`。
|
||
|
||
## Data Model Sketch
|
||
|
||
字段名称使用 snake_case,所有业务表必须带 `app_code`。
|
||
|
||
```sql
|
||
resources(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
resource_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
resource_code VARCHAR(96) NOT NULL,
|
||
resource_type VARCHAR(32) NOT NULL,
|
||
name VARCHAR(128) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
grantable BOOLEAN NOT NULL DEFAULT TRUE,
|
||
manager_grant_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
grant_strategy VARCHAR(32) NOT NULL,
|
||
wallet_asset_type VARCHAR(32) NULL,
|
||
wallet_asset_amount BIGINT NOT NULL DEFAULT 0,
|
||
usage_scope_json JSON NOT NULL,
|
||
asset_url VARCHAR(512) NOT NULL DEFAULT '',
|
||
preview_url VARCHAR(512) NOT NULL DEFAULT '',
|
||
animation_url VARCHAR(512) NOT NULL DEFAULT '',
|
||
metadata_json JSON NULL,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_by_user_id BIGINT NOT NULL,
|
||
updated_by_user_id BIGINT NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
UNIQUE KEY uk_resources_code (app_code, resource_code),
|
||
KEY idx_resources_type_status (app_code, resource_type, status, sort_order),
|
||
KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, grantable, sort_order)
|
||
);
|
||
|
||
resource_groups(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
group_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
group_code VARCHAR(96) NOT NULL,
|
||
name VARCHAR(128) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
description VARCHAR(512) NOT NULL DEFAULT '',
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_by_user_id BIGINT NOT NULL,
|
||
updated_by_user_id BIGINT NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
UNIQUE KEY uk_resource_groups_code (app_code, group_code),
|
||
KEY idx_resource_groups_status (app_code, status, sort_order)
|
||
);
|
||
|
||
resource_group_items(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
group_item_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
group_id BIGINT NOT NULL,
|
||
item_type VARCHAR(32) NOT NULL DEFAULT 'resource',
|
||
resource_id BIGINT NOT NULL DEFAULT 0,
|
||
wallet_asset_type VARCHAR(32) NOT NULL DEFAULT '',
|
||
wallet_asset_amount BIGINT NOT NULL DEFAULT 0,
|
||
quantity BIGINT NOT NULL DEFAULT 1,
|
||
duration_ms BIGINT NOT NULL DEFAULT 0,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
UNIQUE KEY uk_resource_group_items_member (app_code, group_id, item_type, resource_id, wallet_asset_type),
|
||
KEY idx_resource_group_items_group (app_code, group_id, sort_order)
|
||
);
|
||
|
||
gift_configs(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
gift_id VARCHAR(96) NOT NULL,
|
||
resource_id BIGINT NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
name VARCHAR(128) NOT NULL,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
presentation_json JSON NULL,
|
||
gift_type_code VARCHAR(32) NOT NULL DEFAULT 'normal',
|
||
effective_from_ms BIGINT NOT NULL DEFAULT 0,
|
||
effective_to_ms BIGINT NOT NULL DEFAULT 0,
|
||
effect_types_json JSON NULL,
|
||
created_by_user_id BIGINT NOT NULL,
|
||
updated_by_user_id BIGINT NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, gift_id),
|
||
KEY idx_gift_configs_resource (app_code, resource_id),
|
||
KEY idx_gift_configs_status_sort (app_code, status, sort_order),
|
||
KEY idx_gift_configs_effective (app_code, status, effective_from_ms, effective_to_ms)
|
||
);
|
||
|
||
wallet_gift_prices(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
gift_id VARCHAR(96) NOT NULL,
|
||
price_version VARCHAR(64) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
charge_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN',
|
||
coin_price BIGINT NOT NULL,
|
||
gift_point_amount BIGINT NOT NULL,
|
||
heat_value BIGINT NOT NULL,
|
||
effective_at_ms BIGINT NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, gift_id, price_version),
|
||
KEY idx_wallet_gift_prices_effective (app_code, gift_id, status, effective_at_ms)
|
||
);
|
||
|
||
gift_config_regions(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
gift_id VARCHAR(96) NOT NULL,
|
||
region_id BIGINT NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, gift_id, region_id),
|
||
KEY idx_gift_config_regions_region (app_code, region_id, gift_id)
|
||
);
|
||
|
||
resource_grants(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
grant_id VARCHAR(96) NOT NULL,
|
||
command_id VARCHAR(128) NOT NULL,
|
||
target_user_id BIGINT NOT NULL,
|
||
grant_source VARCHAR(32) NOT NULL,
|
||
grant_subject_type VARCHAR(32) NOT NULL,
|
||
grant_subject_id VARCHAR(96) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
request_hash VARCHAR(128) NOT NULL,
|
||
group_snapshot_json JSON NULL,
|
||
reason VARCHAR(256) NOT NULL,
|
||
operator_user_id BIGINT NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, grant_id),
|
||
UNIQUE KEY uk_resource_grants_command (app_code, command_id),
|
||
KEY idx_resource_grants_target_time (app_code, target_user_id, created_at_ms)
|
||
);
|
||
|
||
resource_grant_items(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
grant_item_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
grant_id VARCHAR(96) NOT NULL,
|
||
resource_id BIGINT NOT NULL,
|
||
resource_snapshot_json JSON NOT NULL,
|
||
quantity BIGINT NOT NULL DEFAULT 1,
|
||
duration_ms BIGINT NULL,
|
||
result_type VARCHAR(32) NOT NULL,
|
||
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
|
||
entitlement_id VARCHAR(96) NOT NULL DEFAULT '',
|
||
created_at_ms BIGINT NOT NULL,
|
||
KEY idx_resource_grant_items_grant (app_code, grant_id)
|
||
);
|
||
|
||
user_resource_entitlements(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
entitlement_id VARCHAR(96) NOT NULL,
|
||
user_id BIGINT NOT NULL,
|
||
resource_id BIGINT NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
quantity BIGINT NOT NULL DEFAULT 1,
|
||
remaining_quantity BIGINT NOT NULL DEFAULT 1,
|
||
effective_at_ms BIGINT NOT NULL,
|
||
expires_at_ms BIGINT NULL,
|
||
source_grant_id VARCHAR(96) NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, entitlement_id),
|
||
KEY idx_user_resource_active (app_code, user_id, resource_id, status, expires_at_ms),
|
||
KEY idx_user_resource_user_type (app_code, user_id, status, updated_at_ms)
|
||
);
|
||
|
||
user_resource_equipment(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
user_id BIGINT NOT NULL,
|
||
resource_type VARCHAR(32) NOT NULL,
|
||
entitlement_id VARCHAR(96) NOT NULL,
|
||
resource_id BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, user_id, resource_type),
|
||
KEY idx_user_resource_equipment_user (app_code, user_id, updated_at_ms)
|
||
);
|
||
```
|
||
|
||
`manager_grant_enabled` 只控制经理中心 H5 是否可赠送该资源。后台仍可按后台权限管理资源;App 普通资源列表不暴露这个字段。`grant_source='manager_center'` 的 `GrantResource` 必须在 wallet-service 事务内重新校验 `status=active`、`grantable=TRUE` 和 `manager_grant_enabled=TRUE`,不能只信 gateway 的列表过滤。
|
||
|
||
`wallet_gift_prices` 保持账务字段,但必须和 `gift_configs.gift_id` 对齐。后续实现可以在 `wallet_gift_prices` 增加 `gift_config_version` 或在账务 metadata 中保存 `gift_configs` 快照;不要把礼物资源字段塞进账务价格表。
|
||
|
||
## Admin API Boundary
|
||
|
||
后台 API 建议:
|
||
|
||
```http
|
||
GET /api/v1/admin/resources
|
||
POST /api/v1/admin/resources
|
||
GET /api/v1/admin/resources/{resource_id}
|
||
PUT /api/v1/admin/resources/{resource_id}
|
||
POST /api/v1/admin/resources/{resource_id}/enable
|
||
POST /api/v1/admin/resources/{resource_id}/disable
|
||
|
||
GET /api/v1/admin/resource-groups
|
||
POST /api/v1/admin/resource-groups
|
||
PUT /api/v1/admin/resource-groups/{group_id}
|
||
PUT /api/v1/admin/resource-groups/{group_id}/items
|
||
POST /api/v1/admin/resource-groups/{group_id}/enable
|
||
POST /api/v1/admin/resource-groups/{group_id}/disable
|
||
|
||
GET /api/v1/admin/gifts
|
||
POST /api/v1/admin/gifts
|
||
PUT /api/v1/admin/gifts/{gift_id}
|
||
POST /api/v1/admin/gifts/{gift_id}/enable
|
||
POST /api/v1/admin/gifts/{gift_id}/disable
|
||
|
||
POST /api/v1/admin/resource-grants/resource
|
||
POST /api/v1/admin/resource-grants/group
|
||
GET /api/v1/admin/resource-grants
|
||
```
|
||
|
||
后台资源管理页面边界:
|
||
|
||
- 资源列表每行支持编辑资源名称、编码、类型、封面 URL、素材 URL、上下架状态和金币数量配置。
|
||
- 资源组列表支持新增资源组;资源组成员可以选择资源库资源,也可以直接添加金币或钻石资产项。
|
||
- 资源组内 `item_type=resource` 的单位是天,后台前端提交 `durationDays`;`server/admin` 换算为 wallet-service RPC 的 `duration_ms`,数量固定为 1。
|
||
- 资源组内 `item_type=wallet_asset` 的单位是数量,后台前端提交 `assetType` 和 `amount`;`server/admin` 换算为 wallet-service RPC 的 `wallet_asset_type`、`wallet_asset_amount`,`duration_ms=0`、`quantity=1`。
|
||
- 同一资源组内同一个 `resource_id` 不能重复,同一种钱包资产也不能重复。
|
||
- active 资源组保存或启用时,组内资源项必须是 `active`、`grantable=true` 且不能是 `coin` 资源;金币和钻石必须使用 `wallet_asset` 项。
|
||
- 礼物新增或编辑时必须选择礼物类型和收费类型;收费类型只允许 `COIN` 或 `DIAMOND`。
|
||
- 礼物有效期可以为空;填写时必须满足结束时间晚于开始时间。有效期只控制礼物配置可见和可发送,不重算历史礼物交易。
|
||
- 礼物特效是枚举多选,只描述客户端表现能力;服务端不根据特效触发动画、音乐或广播,只在 App 礼物列表和房间礼物消息相关链路中透传必要配置。
|
||
|
||
权限建议:
|
||
|
||
| Permission | Kind | Usage |
|
||
| --- | --- | --- |
|
||
| `resource:view` | `menu` | 查看资源列表和详情 |
|
||
| `resource:create` | `button` | 新增资源 |
|
||
| `resource:update` | `button` | 编辑资源、启用、禁用 |
|
||
| `resource-group:view` | `menu` | 查看资源组列表和详情 |
|
||
| `resource-group:create` | `button` | 新增资源组 |
|
||
| `resource-group:update` | `button` | 编辑资源组、替换组成员、启用、禁用 |
|
||
| `gift:view` | `menu` | 查看礼物列表 |
|
||
| `gift:create` | `button` | 新增礼物并选择资源 |
|
||
| `gift:update` | `button` | 编辑礼物和价格 |
|
||
| `gift:status` | `button` | 上架、下架 |
|
||
| `resource-grant:view` | `menu` | 查看资源赠送记录 |
|
||
| `resource-grant:create` | `button` | 给用户赠送资源或资源组 |
|
||
|
||
后台写命令必须记录:
|
||
|
||
- `app_code`
|
||
- `command_id`
|
||
- 操作人
|
||
- 目标用户
|
||
- 资源或资源组
|
||
- 数量、有效期、原因
|
||
- 请求 IP、路径、request_id
|
||
- wallet-service 返回的 `grant_id` 或错误
|
||
|
||
## App API Boundary
|
||
|
||
App 只读资源配置和自己的资源权益,不直接改资源配置。
|
||
|
||
建议接口:
|
||
|
||
```http
|
||
GET /api/v1/resources?resource_type=avatar_frame
|
||
GET /api/v1/resource-groups/{group_id}
|
||
GET /api/v1/gifts?region_id=1001
|
||
GET /api/v1/users/me/resources
|
||
POST /api/v1/users/me/resources/{resource_id}/equip
|
||
```
|
||
|
||
礼物列表按区域和有效期过滤:传入 `region_id=N` 时返回绑定到 `N` 的礼物和 `region_id=0` 的 GLOBAL 兜底礼物;不传 `region_id` 时只返回 GLOBAL 兜底礼物。只返回 `status=active`、资源 active、当前时间落在有效期内的礼物。响应会包含 `gift_type_code`、`charge_asset_type`、`effective_from_ms`、`effective_to_ms` 和 `effect_types`,客户端只能用于展示和选择,不能在送礼时回传这些值作为结算依据。
|
||
|
||
装备资源的边界:
|
||
|
||
- `equip` 只适用于头像框、资料卡、座驾、气泡、徽章、麦位声波等可装备资源。
|
||
- `floating_screen`、`gift` 和 `coin` 不能走 `equip`;金币只落钱包账本,礼物和飘屏后续通过使用命令扣减 `remaining_quantity`。
|
||
- `badge`、`mic_seat_animation` 发放成功后默认写入当前佩戴。
|
||
- `user-service.users.avatar` 仍然只是头像 URL,不承载头像框。
|
||
- 当前装备状态放在 `wallet-service` resource domain 的 `user_resource_equipment` 表,避免污染用户主数据。
|
||
- 房间展示用户资料时需要读已装备资源快照,不能让客户端自报头像框、气泡或徽章。
|
||
|
||
## Validation Rules
|
||
|
||
| Rule | Error |
|
||
| --- | --- |
|
||
| resource_code empty or duplicated in same app | `INVALID_ARGUMENT` / `CONFLICT` |
|
||
| unknown resource_type | `INVALID_ARGUMENT` |
|
||
| `coin` resource missing `wallet_asset_type=COIN` or amount <= 0 | `INVALID_ARGUMENT` |
|
||
| non-coin resource uses `wallet_credit` | `INVALID_ARGUMENT` |
|
||
| gift config selects resource_type != `gift` | `INVALID_ARGUMENT` |
|
||
| gift config has negative region_id | `INVALID_ARGUMENT` |
|
||
| SendGift gift not configured for room visible_region_id | `NOT_FOUND` |
|
||
| disabled resource selected for new gift or grant | `CONFLICT` |
|
||
| resource group has no items | `INVALID_ARGUMENT` |
|
||
| resource group resource item missing positive `resource_id` or day duration | `INVALID_ARGUMENT` |
|
||
| resource group wallet asset item is not `COIN` or `DIAMOND`, or amount <= 0 | `INVALID_ARGUMENT` |
|
||
| resource group contains duplicate resource or duplicate wallet asset | `INVALID_ARGUMENT` |
|
||
| resource group contains disabled resource on grant | `CONFLICT` |
|
||
| duplicate command_id with different payload | `LEDGER_CONFLICT` or `CONFLICT` |
|
||
| target user not found or app_code mismatch | `NOT_FOUND` |
|
||
| equip resource without active entitlement, or entitlement expired | `NOT_FOUND` |
|
||
| equip non-equipable resource type | `INVALID_ARGUMENT` |
|
||
|
||
## Event And Outbox
|
||
|
||
资源相关事件必须和资源赠送或配置事务同库提交:
|
||
|
||
| Event | When |
|
||
| --- | --- |
|
||
| `ResourceChanged` | 资源创建、编辑、启用、禁用 |
|
||
| `ResourceGroupChanged` | 资源组创建、编辑、替换成员、启用、禁用 |
|
||
| `GiftConfigChanged` | 礼物创建、编辑、上下架、资源绑定变更 |
|
||
| `ResourceGranted` | 单资源赠送成功 |
|
||
| `ResourceGroupGranted` | 资源组展开赠送成功 |
|
||
| `UserResourceChanged` | 用户权益新增、延长、数量变化、过期或装备变更 |
|
||
|
||
消费者只能把事件作为缓存刷新、消息通知或分析输入。用户当前是否拥有某资源,必须以 `user_resource_entitlements` 和钱包余额事实为准。
|
||
|
||
## Important Edge Cases
|
||
|
||
- 修改资源组后,不影响已经赠送出去的资源组;历史赠送以 `group_snapshot_json` 为准。
|
||
- 禁用资源不删除用户已持有权益,除非后续单独设计“强制召回”命令。
|
||
- 礼物已经发送成功后,即使绑定的 `gift` 资源后来被禁用,历史房间事件仍按发送时快照展示。
|
||
- 资源组不能嵌套资源组,避免赠送展开循环。
|
||
- 金币资源赠送必须走钱包账本,不能只写资源赠送记录。
|
||
- 资源组里的金币和钻石资产项也必须走钱包账本;不能写成用户资源权益。
|
||
- 用户资源过期不能依赖客户端隐藏;服务端查询和装备校验必须过滤过期权益。
|
||
- 后台批量赠送必须拆成可追踪的命令,不能用一个不可恢复的大事务吞掉所有用户结果。
|
||
|
||
## Implementation Order
|
||
|
||
1. 在 `wallet-service` 新增资源 catalog、资源组、礼物配置、用户权益和赠送表。
|
||
2. 新增 wallet-service 管理 RPC:资源 CRUD、资源组 CRUD、礼物配置、赠送资源、赠送资源组。
|
||
3. admin-server 增加资源、资源组、礼物和赠送页面接口,只做鉴权和审计,调用 wallet-service。
|
||
4. gateway 增加 App 资源列表、礼物列表、我的资源接口。
|
||
5. room-service SendGift 读取礼物资源快照,房间礼物消息携带资源快照。
|
||
6. 增加用户装备接口和房间用户资料资源快照。
|
||
7. 最后补批量赠送、过期扫描、缓存刷新和资源变更 outbox 消费。
|