25 KiB
Resource And Resource Group Architecture
本文定义资源、资源组、礼物配置和用户资源赠送的后端边界。资源类型固定为头像框、金币、座驾、气泡、徽章、飘屏、礼物;资源组是一组资源和钱包资产项的集合,可以一次性赠送给用户。实现必须以这里的服务归属、数据边界和 App/Admin 接口边界为准。
Goals
- 建立统一资源库,头像框、金币、座驾、气泡、徽章、飘屏、礼物都从资源库中选择,不允许礼物配置直接填写散落的图片或动画 URL。
- 资源组只表达“打包集合”,赠送资源组时按当时组内容展开成多条资源赠送事实;组内可以直接配置
COIN和DIAMOND钱包资产数量。 - 资源和资源组都可以通过后台命令赠送给用户,赠送必须可审计、可幂等、可追溯。
- 金币仍由
wallet-service资产账本管理;资源库里的金币资源只是对COIN入账规则的配置包装。 - 礼物价格、收费资产、礼物热度、主播积分和有效期仍由
wallet-service结算,客户端不能提交价格、收费类型、热度或积分。 - 按
app_code隔离资源、资源组、礼物配置、赠送和用户持有状态。
Non-Goals
- 首版不新增独立
resource-service微服务。资源 catalog、资源组、礼物绑定、用户资源背包和赠送命令放在wallet-serviceresource 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
资源和资源组都可以直接赠送给用户。赠送必须是服务端命令,不允许客户端自助拼装。
赠送资源:
server/admin鉴权并透传操作端前端按单次赠送动作生成的command_id。wallet-service锁定资源配置,确认 active、grantable、app_code 匹配。- 如果是
coin,在同一个钱包事务中写wallet_transactions/wallet_entries/wallet_outbox。 - 如果是非金币资源,写
resource_grants/resource_grant_items/user_resource_entitlements/resource_outbox。 - 返回 grant 结果和每个资源项结果。
赠送资源组:
- 锁定 active
resource_groups和对应的resource_group_items。 - 按当时组内容展开资源项,并保存
group_snapshot_json。 item_type=resource按资源自身 grant 规则处理。item_type=wallet_asset只允许COIN或DIAMOND,必须写wallet_transactions/wallet_entries/wallet_outbox。- 任一资源项失败,整个资源组赠送失败并回滚;不能出现只发了一半的资源组。
幂等规则:
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=activeeffective_at_ms <= nowexpires_at_ms IS NULL OR expires_at_ms > now- 数量型资源必须
remaining_quantity > 0
资源过期不需要物理删除。展示和使用时按时间过滤,后台任务可以异步把过期权益标记为 expired。
Data Model Sketch
字段名称使用 snake_case,所有业务表必须带 app_code。
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 建议:
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_codecommand_id- 操作人
- 目标用户
- 资源或资源组
- 数量、有效期、原因
- 请求 IP、路径、request_id
- wallet-service 返回的
grant_id或错误
App API Boundary
App 只读资源配置和自己的资源权益,不直接改资源配置。
建议接口:
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-serviceresource 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
- 在
wallet-service新增资源 catalog、资源组、礼物配置、用户权益和赠送表。 - 新增 wallet-service 管理 RPC:资源 CRUD、资源组 CRUD、礼物配置、赠送资源、赠送资源组。
- admin-server 增加资源、资源组、礼物和赠送页面接口,只做鉴权和审计,调用 wallet-service。
- gateway 增加 App 资源列表、礼物列表、我的资源接口。
- room-service SendGift 读取礼物资源快照,房间礼物消息携带资源快照。
- 增加用户装备接口和房间用户资料资源快照。
- 最后补批量赠送、过期扫描、缓存刷新和资源变更 outbox 消费。