hyapp-server/docs/资源组与礼物架构.md

25 KiB
Raw Blame History

Resource And Resource Group Architecture

本文定义资源、资源组、礼物配置和用户资源赠送的后端边界。资源类型固定为头像框、金币、座驾、气泡、徽章、飘屏、礼物;资源组是一组资源和钱包资产项的集合,可以一次性赠送给用户。实现必须以这里的服务归属、数据边界和 App/Admin 接口边界为准。

Goals

  • 建立统一资源库,头像框、金币、座驾、气泡、徽章、飘屏、礼物都从资源库中选择,不允许礼物配置直接填写散落的图片或动画 URL。
  • 资源组只表达“打包集合”,赠送资源组时按当时组内容展开成多条资源赠送事实;组内可以直接配置 COINDIAMOND 钱包资产数量。
  • 资源和资源组都可以通过后台命令赠送给用户,赠送必须可审计、可幂等、可追溯。
  • 金币仍由 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 直接配置 COINDIAMOND 数量。
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_idresource_id、展示名、排序、上下架状态、礼物类型、有效期、特效、客户端表现 metadata
礼物区域配置 wallet-service resource domain gift_id 和多个 region_id 的映射;region_id=0 表示 GLOBAL 兜底区域
礼物账务价格 wallet-service ledger domain gift_idprice_versioncharge_asset_typecoin_pricegift_point_amountheat_value、价格生效时间

礼物类型 code 固定为:

Code Label
normal 普通礼物
cp CP礼物
lucky 幸运礼物
super_lucky 超级幸运礼物
exclusive 专属礼物
noble 贵族礼物
flag 国旗礼物
activity 活动礼物
magic 魔法礼物
custom 定制礼物

礼物收费资产只允许 COINDIAMOND。字段名沿用 coin_price 表达“价格数值”,实际扣费资产由 charge_asset_type 决定。DebitGiftResponse.coin_spent 保留兼容旧调用方;新调用方应优先读取 charge_asset_typecharge_amount

礼物特效 code 固定为:

Code Label
animation 动画
music 音乐
global_broadcast 全局广播

gift_configs.effective_from_msgift_configs.effective_to_ms 表达礼物配置有效期。任一字段为 0 表示该方向不限制;两个字段都为 0 表示长期有效。App 礼物列表和 DebitGift 送礼扣费都必须过滤未开始或已结束的 active 礼物。后台列表可以展示全部礼物,便于运营提前配置或排查历史配置。

SendGift 仍然只接收 gift_id/gift_count。room-service 必须把房间 visible_region_id 传给 wallet-servicewallet-service 必须验证礼物 active、在有效期内且在该区域可用再使用服务端价格按 charge_asset_typeCOINDIAMOND,用服务端资源快照发房间消息。客户端不能提交资源 URL、礼物价格、收费资产、热度、主播积分、有效期、特效或礼物区域。

礼物绑定资源时的约束:

  • 一个 active 礼物必须绑定一个 active resource。
  • 礼物绑定的 resource 必须 resource_type=gift
  • 下架 resource 不自动删除历史礼物交易,只阻止新礼物上架或继续发送。
  • 修改礼物 resource 后,新发送使用新 resource历史礼物事件使用发送时快照不反查当前配置。
  • 钱包落账 metadata 必须保存 gift_idprice_versioncharge_asset_typecharge_amountcoin_pricegift_point_amountheat_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 只允许 COINDIAMOND,必须写 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

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=activegrantable=TRUEmanager_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 的单位是天,后台前端提交 durationDaysserver/admin 换算为 wallet-service RPC 的 duration_ms,数量固定为 1。
  • 资源组内 item_type=wallet_asset 的单位是数量,后台前端提交 assetTypeamountserver/admin 换算为 wallet-service RPC 的 wallet_asset_typewallet_asset_amountduration_ms=0quantity=1
  • 同一资源组内同一个 resource_id 不能重复,同一种钱包资产也不能重复。
  • active 资源组保存或启用时,组内资源项必须是 activegrantable=true 且不能是 coin 资源;金币和钻石必须使用 wallet_asset 项。
  • 礼物新增或编辑时必须选择礼物类型和收费类型;收费类型只允许 COINDIAMOND
  • 礼物有效期可以为空;填写时必须满足结束时间晚于开始时间。有效期只控制礼物配置可见和可发送,不重算历史礼物交易。
  • 礼物特效是枚举多选,只描述客户端表现能力;服务端不根据特效触发动画、音乐或广播,只在 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 只读资源配置和自己的资源权益,不直接改资源配置。

建议接口:

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_codecharge_asset_typeeffective_from_mseffective_to_mseffect_types,客户端只能用于展示和选择,不能在送礼时回传这些值作为结算依据。

装备资源的边界:

  • equip 只适用于头像框、资料卡、座驾、气泡、徽章、麦位声波等可装备资源。
  • floating_screengiftcoin 不能走 equip;金币只落钱包账本,礼物和飘屏后续通过使用命令扣减 remaining_quantity
  • badgemic_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 消费。