重复订单

This commit is contained in:
zhx 2026-05-21 18:30:55 +08:00
parent 5e0e02cd2a
commit 8dc76890a6
19 changed files with 1017 additions and 28 deletions

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// protoc v5.27.3
// source: proto/wallet/v1/wallet.proto
package walletv1
@ -1069,6 +1069,9 @@ type Resource struct {
CreatedAtMs int64 `protobuf:"varint,19,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
UpdatedAtMs int64 `protobuf:"varint,20,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
ManagerGrantEnabled bool `protobuf:"varint,21,opt,name=manager_grant_enabled,json=managerGrantEnabled,proto3" json:"manager_grant_enabled,omitempty"`
PriceType string `protobuf:"bytes,22,opt,name=price_type,json=priceType,proto3" json:"price_type,omitempty"`
CoinPrice int64 `protobuf:"varint,23,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"`
GiftPointAmount int64 `protobuf:"varint,24,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1250,6 +1253,27 @@ func (x *Resource) GetManagerGrantEnabled() bool {
return false
}
func (x *Resource) GetPriceType() string {
if x != nil {
return x.PriceType
}
return ""
}
func (x *Resource) GetCoinPrice() int64 {
if x != nil {
return x.CoinPrice
}
return 0
}
func (x *Resource) GetGiftPointAmount() int64 {
if x != nil {
return x.GiftPointAmount
}
return 0
}
type ResourceGroupItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
GroupItemId int64 `protobuf:"varint,1,opt,name=group_item_id,json=groupItemId,proto3" json:"group_item_id,omitempty"`
@ -2614,6 +2638,9 @@ type CreateResourceRequest struct {
SortOrder int32 `protobuf:"varint,16,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"`
OperatorUserId int64 `protobuf:"varint,17,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"`
ManagerGrantEnabled *bool `protobuf:"varint,18,opt,name=manager_grant_enabled,json=managerGrantEnabled,proto3,oneof" json:"manager_grant_enabled,omitempty"`
PriceType string `protobuf:"bytes,19,opt,name=price_type,json=priceType,proto3" json:"price_type,omitempty"`
CoinPrice int64 `protobuf:"varint,20,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"`
GiftPointAmount int64 `protobuf:"varint,21,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -2774,6 +2801,27 @@ func (x *CreateResourceRequest) GetManagerGrantEnabled() bool {
return false
}
func (x *CreateResourceRequest) GetPriceType() string {
if x != nil {
return x.PriceType
}
return ""
}
func (x *CreateResourceRequest) GetCoinPrice() int64 {
if x != nil {
return x.CoinPrice
}
return 0
}
func (x *CreateResourceRequest) GetGiftPointAmount() int64 {
if x != nil {
return x.GiftPointAmount
}
return 0
}
type UpdateResourceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
@ -2795,6 +2843,9 @@ type UpdateResourceRequest struct {
SortOrder int32 `protobuf:"varint,17,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"`
OperatorUserId int64 `protobuf:"varint,18,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"`
ManagerGrantEnabled *bool `protobuf:"varint,19,opt,name=manager_grant_enabled,json=managerGrantEnabled,proto3,oneof" json:"manager_grant_enabled,omitempty"`
PriceType string `protobuf:"bytes,20,opt,name=price_type,json=priceType,proto3" json:"price_type,omitempty"`
CoinPrice int64 `protobuf:"varint,21,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"`
GiftPointAmount int64 `protobuf:"varint,22,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -2962,6 +3013,27 @@ func (x *UpdateResourceRequest) GetManagerGrantEnabled() bool {
return false
}
func (x *UpdateResourceRequest) GetPriceType() string {
if x != nil {
return x.PriceType
}
return ""
}
func (x *UpdateResourceRequest) GetCoinPrice() int64 {
if x != nil {
return x.CoinPrice
}
return 0
}
func (x *UpdateResourceRequest) GetGiftPointAmount() int64 {
if x != nil {
return x.GiftPointAmount
}
return 0
}
type SetResourceStatusRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
@ -8961,7 +9033,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x17recharge_policy_version\x18\b \x01(\tR\x15rechargePolicyVersion\x12=\n" +
"\x1brecharge_policy_coin_amount\x18\t \x01(\x03R\x18rechargePolicyCoinAmount\x12F\n" +
" recharge_policy_usd_minor_amount\x18\n" +
" \x01(\x03R\x1crechargePolicyUsdMinorAmount\"\xfd\x05\n" +
" \x01(\x03R\x1crechargePolicyUsdMinorAmount\"\xe7\x06\n" +
"\bResource\x12\x19\n" +
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x1f\n" +
"\vresource_id\x18\x02 \x01(\x03R\n" +
@ -8987,7 +9059,12 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x12updated_by_user_id\x18\x12 \x01(\x03R\x0fupdatedByUserId\x12\"\n" +
"\rcreated_at_ms\x18\x13 \x01(\x03R\vcreatedAtMs\x12\"\n" +
"\rupdated_at_ms\x18\x14 \x01(\x03R\vupdatedAtMs\x122\n" +
"\x15manager_grant_enabled\x18\x15 \x01(\bR\x13managerGrantEnabled\"\xc7\x03\n" +
"\x15manager_grant_enabled\x18\x15 \x01(\bR\x13managerGrantEnabled\x12\x1d\n" +
"\n" +
"price_type\x18\x16 \x01(\tR\tpriceType\x12\x1d\n" +
"\n" +
"coin_price\x18\x17 \x01(\x03R\tcoinPrice\x12*\n" +
"\x11gift_point_amount\x18\x18 \x01(\x03R\x0fgiftPointAmount\"\xc7\x03\n" +
"\x11ResourceGroupItem\x12\"\n" +
"\rgroup_item_id\x18\x01 \x01(\x03R\vgroupItemId\x12\x19\n" +
"\bgroup_id\x18\x02 \x01(\x03R\agroupId\x12\x1f\n" +
@ -9144,7 +9221,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\vresource_id\x18\x03 \x01(\x03R\n" +
"resourceId\"L\n" +
"\x13GetResourceResponse\x125\n" +
"\bresource\x18\x01 \x01(\v2\x19.hyapp.wallet.v1.ResourceR\bresource\"\xaf\x05\n" +
"\bresource\x18\x01 \x01(\v2\x19.hyapp.wallet.v1.ResourceR\bresource\"\x99\x06\n" +
"\x15CreateResourceRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
@ -9167,8 +9244,13 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\n" +
"sort_order\x18\x10 \x01(\x05R\tsortOrder\x12(\n" +
"\x10operator_user_id\x18\x11 \x01(\x03R\x0eoperatorUserId\x127\n" +
"\x15manager_grant_enabled\x18\x12 \x01(\bH\x00R\x13managerGrantEnabled\x88\x01\x01B\x18\n" +
"\x16_manager_grant_enabled\"\xd0\x05\n" +
"\x15manager_grant_enabled\x18\x12 \x01(\bH\x00R\x13managerGrantEnabled\x88\x01\x01\x12\x1d\n" +
"\n" +
"price_type\x18\x13 \x01(\tR\tpriceType\x12\x1d\n" +
"\n" +
"coin_price\x18\x14 \x01(\x03R\tcoinPrice\x12*\n" +
"\x11gift_point_amount\x18\x15 \x01(\x03R\x0fgiftPointAmountB\x18\n" +
"\x16_manager_grant_enabled\"\xba\x06\n" +
"\x15UpdateResourceRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
@ -9193,7 +9275,12 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\n" +
"sort_order\x18\x11 \x01(\x05R\tsortOrder\x12(\n" +
"\x10operator_user_id\x18\x12 \x01(\x03R\x0eoperatorUserId\x127\n" +
"\x15manager_grant_enabled\x18\x13 \x01(\bH\x00R\x13managerGrantEnabled\x88\x01\x01B\x18\n" +
"\x15manager_grant_enabled\x18\x13 \x01(\bH\x00R\x13managerGrantEnabled\x88\x01\x01\x12\x1d\n" +
"\n" +
"price_type\x18\x14 \x01(\tR\tpriceType\x12\x1d\n" +
"\n" +
"coin_price\x18\x15 \x01(\x03R\tcoinPrice\x12*\n" +
"\x11gift_point_amount\x18\x16 \x01(\x03R\x0fgiftPointAmountB\x18\n" +
"\x16_manager_grant_enabled\"\xb7\x01\n" +
"\x18SetResourceStatusRequest\x12\x1d\n" +
"\n" +

View File

@ -149,6 +149,9 @@ message Resource {
int64 created_at_ms = 19;
int64 updated_at_ms = 20;
bool manager_grant_enabled = 21;
string price_type = 22;
int64 coin_price = 23;
int64 gift_point_amount = 24;
}
message ResourceGroupItem {
@ -320,6 +323,9 @@ message CreateResourceRequest {
int32 sort_order = 16;
int64 operator_user_id = 17;
optional bool manager_grant_enabled = 18;
string price_type = 19;
int64 coin_price = 20;
int64 gift_point_amount = 21;
}
message UpdateResourceRequest {
@ -342,6 +348,9 @@ message UpdateResourceRequest {
int32 sort_order = 17;
int64 operator_user_id = 18;
optional bool manager_grant_enabled = 19;
string price_type = 20;
int64 coin_price = 21;
int64 gift_point_amount = 22;
}
message SetResourceStatusRequest {

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// - protoc v5.27.3
// source: proto/wallet/v1/wallet.proto
package walletv1

View File

@ -0,0 +1,588 @@
# 幸运礼物 Flutter 对接
本文描述 Flutter App 对接幸运礼物所需的 HTTP 接口、请求参数、返回值、错误处理和腾讯云 IM 自定义消息。幸运礼物支持多个互相独立的奖池App 只负责传入 `pool_id`,不计算 RTP、倍率概率、奖池水位或中奖结果。
## 基础约定
本地开发地址:
```text
http://127.0.0.1:13000
```
业务接口统一响应 envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
| 字段 | 说明 |
| --- | --- |
| `code` | 成功固定 `OK`;失败为稳定错误码。 |
| `message` | 短错误文案,不用于客户端分支判断。 |
| `request_id` | 服务端链路追踪 ID客户端日志要记录。 |
| `data` | 成功业务数据;失败时通常不存在。 |
通用请求头:
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>`。 |
| `X-App-Code` | 否 | App 编码,默认 `lalu`;登录态接口最终以 token 内 app_code 为准。 |
| `Content-Type` | POST 必填 | `application/json`。 |
所有 `*_ms` 都是 Unix epoch milliseconds。金额单位都是整数金币。
## 核心概念
| 概念 | 说明 |
| --- | --- |
| `gift_id` | 用户实际送出的幸运礼物 ID例如某个 100 金币、5000 金币或 500000 金币礼物。 |
| `pool_id` | 后台配置的幸运礼物奖池 ID例如 `pool_95``pool_98`。不同 `pool_id` 的 RTP、奖池、风控和用户阶段完全隔离。 |
| `command_id` | 本次送礼动作幂等键。用户同一次点击产生一个值HTTP 超时或重试必须复用。 |
| `coin_spent` | 服务端按礼物价格和数量计算的实际扣费金额App 不上传该字段。 |
| `multiplier_ppm` | 倍率的 ppm 表达,`1x=1000000``0.5x=500000``100x=100000000`。 |
| `base_reward_coins` | 基础 RTP 返奖,计入平台成本控制。 |
| `effective_reward_coins` | 用户可见总奖励,等于基础返奖、房间气氛奖励、活动补贴之和。 |
App 不要展示后台 RTP 数值,不要展示实时概率,不要本地推算“下一次该中”。客户端只展示服务端返回的结果。
## 当前实现状态
当前仓库已经具备:
| 能力 | 状态 |
| --- | --- |
| `POST /api/v1/rooms/gift/send` 支持 `pool_id` 入参 | 已有 |
| 房间送礼 IM `room_gift_sent` 可携带 `pool_id` | 已有 |
| `activity-service` 内部 gRPC `CheckLuckyGift` / `ExecuteLuckyGiftDraw` | 已有 |
| 后台按 `pool_id` 配置多奖池 | 已有 |
Flutter 完整体验还需要后端补齐:
| 能力 | 建议 |
| --- | --- |
| App 查询某个奖池是否可抽 | 新增 HTTP `POST /api/v1/activities/lucky-gifts/check` |
| 送礼响应直接返回抽奖结果 | 在 `/rooms/gift/send` 成功 `data` 中增加 `lucky_gift` |
| 中奖房间表现 | 新增房间 IM `lucky_gift_drawn` |
| 返奖到账 | 钱包侧新增幸运礼物返奖入账后继续发 `WalletBalanceChanged` |
下面文档按最终 Flutter 契约编写;已存在字段可直接对接,标注为“建议新增”的字段需要后端继续落地。
## 客户端流程
1. 进入房间后调用 `GET /api/v1/rooms/{room_id}/gift-panel` 获取礼物面板、金币余额和收礼人。
2. 找到 `gift_type_code=lucky``gift_type_code=super_lucky` 的礼物,读取该礼物对应的 `pool_id`。当前可从 `presentation_json.lucky_pool_id` 下发,后续建议提升为顶层字段 `lucky_pool_id`
3. 用户点击幸运礼物前,可调用 `POST /api/v1/activities/lucky-gifts/check` 判断奖池是否启用。
4. 用户确认送礼时调用 `POST /api/v1/rooms/gift/send`,请求体带上 `pool_id`
5. 成功响应里如果有 `lucky_gift`,立即播放中奖或未中奖反馈。
6. 房间内监听 `room_gift_sent``lucky_gift_drawn` IM给其他用户同步展示。
7. 金币余额以 `WalletBalanceChanged` 私有 IM 或 `GET /api/v1/wallet/me/balances` 刷新为准,不在本地推算。
## 获取礼物面板
```http
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
X-App-Code: lalu
```
路径参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前语音房 ID。 |
成功响应节选:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"coin_balance": 1000000,
"recipients": [],
"tabs": [],
"gifts": [
{
"gift_id": "lucky_star_100",
"name": "幸运星",
"gift_type_code": "lucky",
"charge_asset_type": "COIN",
"coin_price": 100,
"presentation_json": "{\"lucky_pool_id\":\"pool_95\"}",
"resource": {
"asset_url": "https://cdn.example/lucky_star.png",
"animation_url": "https://cdn.example/lucky_star.svga"
}
}
],
"quantity_presets": [1, 9, 99, 999]
}
}
```
幸运礼物识别规则:
| 字段 | 说明 |
| --- | --- |
| `gift_type_code=lucky` | 普通幸运礼物。 |
| `gift_type_code=super_lucky` | 超级幸运礼物。 |
| `presentation_json.lucky_pool_id` | 建议下发的奖池 ID为空时 App 传 `default` 或不传,由服务端按默认奖池处理。 |
| `coin_price` | 单个礼物价格;实际扣费仍以服务端钱包价格为准。 |
## 检查幸运礼物状态
建议新增 App HTTP 接口:
```http
POST /api/v1/activities/lucky-gifts/check
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求体:
```json
{
"room_id": "lalu_xxx",
"gift_id": "lucky_star_100",
"pool_id": "pool_95"
}
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `gift_id` | string | 是 | 用户准备送出的幸运礼物 ID。 |
| `pool_id` | string | 否 | 奖池 ID为空时服务端使用 `default`。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"enabled": true,
"reason": "enabled",
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"rule_version": 12,
"experience_pool": "novice",
"server_time_ms": 1779259000000
}
}
```
返回字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 是否允许当前用户在当前房间抽该奖池。 |
| `reason` | string | 服务端原因,客户端只做日志或灰度排查。 |
| `pool_id` | string | 实际使用的奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `rule_version` | int64 | 当前奖池规则版本。 |
| `experience_pool` | string | `novice``intermediate``advanced`,只影响展示文案或动效强度。 |
| `server_time_ms` | int64 | 服务端当前时间。 |
客户端处理:
| 场景 | 处理 |
| --- | --- |
| `enabled=true` | 允许点击送礼。 |
| `enabled=false` | 置灰幸运抽奖入口,但普通礼物展示可继续。 |
| 接口失败 | 不在本地兜底开奖;可以提示稍后重试或隐藏幸运玩法。 |
## 送礼并抽奖
已存在 App HTTP 入口:
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求体:
```json
{
"room_id": "lalu_xxx",
"command_id": "gift_1779259000000_10001_01",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "lucky_star_100",
"gift_count": 1,
"pool_id": "pool_95"
}
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `command_id` | string | 是 | 本次用户动作幂等键;同一次重试必须复用。 |
| `target_type` | string | 否 | 当前主流程使用 `user`;为空按 `user`。 |
| `target_user_ids` | int64[] | 是 | 收礼用户 ID`target_type=user` 时必须且只能 1 个。 |
| `target_user_id` | int64 | 否 | 单目标兼容字段;新代码优先用 `target_user_ids`。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `gift_count` | int32 | 是 | 礼物数量,必须大于 0。 |
| `pool_id` | string | 否 | 幸运礼物奖池 ID不同奖池完全隔离。普通礼物可不传。 |
成功响应节选:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"result": {
"applied": true,
"room_version": 14,
"server_time_ms": 1779259000000
},
"billing_receipt_id": "br_xxx",
"room_heat": 123456,
"gift_rank": [],
"room": {},
"treasure": {},
"lucky_gift": {
"enabled": true,
"draw_id": "lucky_draw_xxx",
"command_id": "gift_1779259000000_10001_01",
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"base_reward_coins": 200,
"room_atmosphere_reward_coins": 0,
"activity_subsidy_coins": 0,
"effective_reward_coins": 200,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000
}
}
}
```
`lucky_gift` 字段为建议新增。普通礼物或奖池未启用时可以为空。
`data` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否实际落地;幂等重放可能为 `false`。 |
| `result.room_version` | int64 | 房间版本。 |
| `result.server_time_ms` | int64 | 服务端时间。 |
| `billing_receipt_id` | string | 钱包扣费回执;存在表示送礼扣费成功。 |
| `room_heat` | int64 | 房间热度。 |
| `gift_rank` | array | 房间礼物榜快照。 |
| `room` | object | 房间快照。 |
| `treasure` | object | 宝箱状态;如果房间宝箱开启可直接更新。 |
| `lucky_gift` | object | 幸运礼物抽奖结果;建议新增。 |
`lucky_gift` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 本次是否执行了幸运礼物抽奖。 |
| `draw_id` | string | 抽奖记录 ID。 |
| `command_id` | string | 和送礼请求一致的幂等键。 |
| `pool_id` | string | 本次使用的奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `rule_version` | int64 | 抽奖时使用的规则版本。 |
| `experience_pool` | string | `novice``intermediate``advanced`。 |
| `selected_tier_id` | string | 服务端选中的奖档 ID。 |
| `multiplier_ppm` | int64 | 建议新增;用于直接展示倍率。后端暂未返回时客户端不要本地解析倍率。 |
| `base_reward_coins` | int64 | 基础 RTP 返奖。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛补贴奖励。 |
| `activity_subsidy_coins` | int64 | 活动补贴奖励。 |
| `effective_reward_coins` | int64 | 用户可见总奖励。 |
| `reward_status` | string | `pending``granted``failed`。 |
| `stage_feedback` | bool | 是否触发阶段反馈。可能没有金币奖励。 |
| `high_multiplier` | bool | 是否属于高倍结果,可用于强化动效。 |
| `created_at_ms` | int64 | 抽奖结果生成时间。 |
展示规则:
| 场景 | 处理 |
| --- | --- |
| `lucky_gift` 为空 | 按普通礼物成功展示。 |
| `effective_reward_coins=0``stage_feedback=false` | 展示未中奖或普通送礼效果。 |
| `stage_feedback=true` | 展示阶段进度、幸运值或轻量动画;不要本地加金币。 |
| `effective_reward_coins>0``reward_status=granted` | 展示中奖金额和到账效果;余额仍以钱包 IM 为准。 |
| `effective_reward_coins>0``reward_status=pending` | 展示“奖励处理中”,等待钱包 IM 或余额刷新。 |
| `reward_status=failed` | 不本地加金币;提示稍后刷新或走补偿查询。 |
| `high_multiplier=true` | 播放高倍中奖动效,并等待房间 IM 同步其他用户。 |
## 错误处理
通用错误:
| HTTP | `code` | 说明 | Flutter 处理 |
| --- | --- | --- | --- |
| 400 | `INVALID_JSON` | JSON 解析失败。 | 修正客户端请求体。 |
| 400 | `INVALID_ARGUMENT` | 参数不合法,例如缺 `room_id``gift_id``command_id`。 | 不重试,提示操作失败并打点。 |
| 401 | `UNAUTHORIZED` | token 无效或过期。 | 走登录刷新或重新登录。 |
| 403 | `PROFILE_REQUIRED` | 用户未完成资料。 | 引导补全资料。 |
| 403 | `PERMISSION_DENIED` | 没有权限。 | 提示不可操作。 |
| 409 | `INSUFFICIENT_BALANCE` | 金币不足。 | 弹充值或余额不足提示。 |
| 409 | `ROOM_CLOSED` | 房间已关闭。 | 退出或刷新房间状态。 |
| 409 | `RULE_NOT_ACTIVE` | 幸运礼物奖池未启用。 | 置灰幸运玩法,不本地开奖。 |
| 409 | `IDEMPOTENCY_CONFLICT` | 同一 `command_id` 对应的请求内容不一致。 | 生成新的 `command_id` 重新发起新动作;旧动作不再重放。 |
| 409 | `CONFLICT` | 规则、奖池、水位或风控状态冲突。 | 提示稍后再试,必要时刷新面板。 |
| 502 | `UPSTREAM_ERROR` | room、wallet、activity 等依赖暂不可用。 | 可保留同一个 `command_id` 重试同一次送礼。 |
| 500 | `INTERNAL_ERROR` | 服务端内部异常。 | 记录 `request_id`,提示稍后重试。 |
幂等和重试规则:
| 场景 | 处理 |
| --- | --- |
| HTTP 超时、断网、502 | 使用同一个 `command_id` 重试。 |
| 409 `IDEMPOTENCY_CONFLICT` | 不要复用该 `command_id`;说明客户端把同一个幂等键用于不同请求。 |
| 收到成功响应且有 `billing_receipt_id` | 扣费已成功,不要再次生成新命令补发。 |
| 成功响应丢失但钱包余额已变化 | 以钱包余额和房间 IM 为准,避免重复扣费。 |
| App 进程重启 | 本地持久化最近未完成的 `command_id`,优先用同一键查询或重试。 |
## IM房间送礼消息
已存在房间 IM
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `room_gift_sent` |
| `Ext` / `extension` | `room_system_message` |
| 群 ID | 当前 `room_id` |
| `CloudCustomData` | 与 `TIMCustomElem.Data` 相同的 JSON 字符串 |
Payload 示例:
```json
{
"event_id": "evt_xxx",
"room_id": "lalu_xxx",
"event_type": "room_gift_sent",
"actor_user_id": 10001,
"target_user_id": 10002,
"gift_value": 100,
"room_heat": 123456,
"room_version": 14,
"attributes": {
"gift_id": "lucky_star_100",
"pool_id": "pool_95",
"gift_count": "1",
"billing_receipt_id": "br_xxx",
"coin_spent": "100",
"gift_point_added": "10",
"price_version": "pv_xxx"
}
}
```
处理规则:
| 场景 | 处理 |
| --- | --- |
| `Ext` 不是 `room_system_message` | 忽略。 |
| `event_type` 不是 `room_gift_sent` | 交给其他房间消息处理器。 |
| `event_id` 已处理 | 忽略。 |
| `room_version` 小于本地当前版本 | 忽略或只展示轻量动画,不回滚房间状态。 |
| `attributes.pool_id` 为空 | 按普通礼物处理。 |
| `attributes.pool_id` 非空 | 展示幸运礼物送出效果,但中奖结果仍等 `lucky_gift_drawn` 或 HTTP `lucky_gift`。 |
## IM幸运礼物开奖结果
建议新增房间 IM
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `lucky_gift_drawn` |
| `Ext` / `extension` | `room_system_message` |
| 群 ID | 当前 `room_id` |
| `CloudCustomData` | 与 `TIMCustomElem.Data` 相同的 JSON 字符串 |
Payload 示例:
```json
{
"event_id": "lucky_draw_event_xxx",
"room_id": "lalu_xxx",
"event_type": "lucky_gift_drawn",
"room_version": 14,
"draw_id": "lucky_draw_xxx",
"command_id": "gift_1779259000000_10001_01",
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"gift_count": 1,
"sender_user_id": 10001,
"target_user_id": 10002,
"coin_spent": 100,
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"effective_reward_coins": 200,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | IM 事件幂等键。 |
| `event_type` | string | 固定 `lucky_gift_drawn`。 |
| `room_id` | string | 房间 ID。 |
| `room_version` | int64 | 房间版本。 |
| `draw_id` | string | 抽奖记录 ID。 |
| `command_id` | string | 送礼幂等键。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `gift_count` | int32 | 礼物数量。 |
| `sender_user_id` | int64 | 送礼用户。 |
| `target_user_id` | int64 | 收礼用户。 |
| `coin_spent` | int64 | 本次实际扣费。 |
| `selected_tier_id` | string | 中奖奖档。 |
| `multiplier_ppm` | int64 | 中奖倍率,建议新增。 |
| `effective_reward_coins` | int64 | 用户可见奖励金币。 |
| `reward_status` | string | `pending``granted``failed`。 |
| `stage_feedback` | bool | 是否阶段反馈。 |
| `high_multiplier` | bool | 是否高倍结果。 |
| `created_at_ms` | int64 | 事件创建时间。 |
展示规则:
| 场景 | 处理 |
| --- | --- |
| 当前用户是 `sender_user_id` | 如果 HTTP 已展示过同一 `draw_id`,只更新状态,不重复播中奖动画。 |
| 当前用户不是 `sender_user_id` | 展示房间内中奖横幅、飘屏或公屏提示。 |
| `effective_reward_coins=0` | 不展示大奖飘屏,只保留普通送礼效果或阶段反馈。 |
| `high_multiplier=true` | 展示高倍动画。 |
| `reward_status=pending` | 文案使用“奖励处理中”,不要展示已到账。 |
| JSON 解析失败或字段缺失 | 忽略并记录客户端日志。 |
## IM钱包余额变更
幸运礼物扣费和返奖都应通过钱包私有 IM 同步余额。该 IM 已在钱包文档中定义。
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `WalletBalanceChanged` |
| `Ext` / `extension` | `wallet_notice` |
| 通道 | C2C 私有消息 |
Payload 示例:
```json
{
"event_type": "WalletBalanceChanged",
"event_id": "wev_xxx",
"app_code": "lalu",
"transaction_id": "tx_xxx",
"command_id": "gift_1779259000000_10001_01",
"user_id": "10001",
"asset_type": "COIN",
"available_delta": -100,
"frozen_delta": 0,
"available_after": 999900,
"frozen_after": 0,
"balance_version": 18,
"created_at_ms": 1779259000000,
"source_created_at_ms": 1779259000000,
"metadata": {
"biz_type": "gift_debit",
"gift_id": "lucky_star_100",
"pool_id": "pool_95"
}
}
```
返奖到账时 `available_delta` 为正数,`metadata.biz_type` 建议使用 `lucky_gift_reward`
处理规则:
| 场景 | 处理 |
| --- | --- |
| `Ext` 不是 `wallet_notice` | 忽略。 |
| `event_type` 不是 `WalletBalanceChanged` | 忽略。 |
| `event_id` 已处理 | 忽略。 |
| 本地 `asset_type` 的版本 `>= balance_version` | 忽略。 |
| 长时间没有收到余额 IM | 调 `GET /api/v1/wallet/me/balances` 兜底刷新。 |
## IM高倍全站或区域播报
建议新增播报 IM用于高倍中奖或运营配置的大额中奖展示。
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `lucky_gift_big_win` |
| `Ext` / `extension` | `im_broadcast` |
| 群 ID | 全站播报群或区域播报群 |
Payload 示例:
```json
{
"event_id": "lucky_big_win_xxx",
"broadcast_type": "lucky_gift_big_win",
"scope": "region",
"app_code": "lalu",
"region_id": 1001,
"room_id": "lalu_xxx",
"sender_user_id": 10001,
"target_user_id": 10002,
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"multiplier_ppm": 100000000,
"effective_reward_coins": 10000,
"sent_at_ms": 1779259000000,
"action": {
"type": "enter_room",
"room_id": "lalu_xxx"
}
}
```
处理规则:
| 场景 | 处理 |
| --- | --- |
| 当前用户已在该房间且已收到 `lucky_gift_drawn` | 可以只展示一次,避免重复刷屏。 |
| 当前用户不在该房间 | 展示全站或区域飘屏,点击 `action` 进房。 |
| `event_id` 已处理 | 忽略。 |
## App 最小对接顺序
1. 登录后调用 `GET /api/v1/im/usersig`,登录腾讯云 IM。
2. 进入房间后加入房间 IM 群,并调用 `GET /api/v1/rooms/{room_id}/gift-panel`
3. 从礼物面板识别幸运礼物和对应 `pool_id`
4. 用户点击送礼时生成 `command_id`,调用 `POST /api/v1/rooms/gift/send`
5. 使用 HTTP 响应里的 `lucky_gift` 做当前用户即时反馈。
6. 监听房间 IM `room_gift_sent``lucky_gift_drawn`,同步其他用户看到的表现。
7. 监听 C2C IM `WalletBalanceChanged`,更新金币余额。
8. 任意 IM 丢失、页面重进或余额不一致时,重新拉取礼物面板和钱包余额纠正状态。

View File

@ -14,6 +14,9 @@ type resourceDTO struct {
GrantStrategy string `json:"grantStrategy"`
WalletAssetType string `json:"walletAssetType"`
WalletAssetAmount int64 `json:"walletAssetAmount"`
PriceType string `json:"priceType"`
CoinPrice int64 `json:"coinPrice"`
GiftPointAmount int64 `json:"giftPointAmount"`
UsageScopes []string `json:"usageScopes"`
AssetURL string `json:"assetUrl"`
PreviewURL string `json:"previewUrl"`
@ -172,6 +175,9 @@ func resourceFromProto(item *walletv1.Resource) resourceDTO {
GrantStrategy: item.GetGrantStrategy(),
WalletAssetType: item.GetWalletAssetType(),
WalletAssetAmount: item.GetWalletAssetAmount(),
PriceType: item.GetPriceType(),
CoinPrice: item.GetCoinPrice(),
GiftPointAmount: item.GetGiftPointAmount(),
UsageScopes: item.GetUsageScopes(),
AssetURL: item.GetAssetUrl(),
PreviewURL: item.GetPreviewUrl(),

View File

@ -74,6 +74,10 @@ func (h *Handler) CreateResource(c *gin.Context) {
response.BadRequest(c, "资源参数不正确")
return
}
if err := validateResourcePricing(req); err != nil {
response.BadRequest(c, err.Error())
return
}
resp, err := h.wallet.CreateResource(c.Request.Context(), req.createProto(c))
if err != nil {
response.BadRequest(c, err.Error())
@ -170,6 +174,10 @@ func (h *Handler) UpdateResource(c *gin.Context) {
response.BadRequest(c, "资源参数不正确")
return
}
if err := validateResourcePricing(req); err != nil {
response.BadRequest(c, err.Error())
return
}
protoReq := req.updateProto(c, resourceID)
resp, err := h.wallet.UpdateResource(c.Request.Context(), protoReq)
if err != nil {

View File

@ -21,6 +21,11 @@ const (
resourceTypeEmojiPack = "emoji_pack"
)
const (
resourcePriceTypeCoin = "coin"
resourcePriceTypeFree = "free"
)
const (
defaultEmojiPackCategory = "默认"
emojiPackPricingFree = "free"
@ -33,6 +38,9 @@ type resourceRequest struct {
Name string `json:"name"`
Status string `json:"status"`
Amount int64 `json:"amount"`
PriceType string `json:"priceType"`
CoinPrice int64 `json:"coinPrice"`
GiftPointAmount int64 `json:"giftPointAmount"`
ManagerGrantEnabled *bool `json:"managerGrantEnabled"`
AssetURL string `json:"assetUrl"`
PreviewURL string `json:"previewUrl"`
@ -121,6 +129,7 @@ type grantGroupRequest struct {
func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceRequest {
resourceType := normalizeResourceType(r.ResourceType)
walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice)
return &walletv1.CreateResourceRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
@ -133,6 +142,9 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
GrantStrategy: resourceGrantStrategy(resourceType),
WalletAssetType: walletAssetType,
WalletAssetAmount: walletAssetAmount,
PriceType: priceType,
CoinPrice: coinPrice,
GiftPointAmount: giftPointAmount,
UsageScopes: []string{resourceType},
AssetUrl: strings.TrimSpace(r.AssetURL),
PreviewUrl: strings.TrimSpace(r.PreviewURL),
@ -146,6 +158,7 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1.UpdateResourceRequest {
resourceType := normalizeResourceType(r.ResourceType)
walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice)
return &walletv1.UpdateResourceRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
@ -159,6 +172,9 @@ func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1
GrantStrategy: resourceGrantStrategy(resourceType),
WalletAssetType: walletAssetType,
WalletAssetAmount: walletAssetAmount,
PriceType: priceType,
CoinPrice: coinPrice,
GiftPointAmount: giftPointAmount,
UsageScopes: []string{resourceType},
AssetUrl: strings.TrimSpace(r.AssetURL),
PreviewUrl: strings.TrimSpace(r.PreviewURL),
@ -340,6 +356,29 @@ func resourceWalletAsset(resourceType string, amount int64) (string, int64) {
return "", 0
}
func resourcePricing(priceType string, coinPrice int64) (string, int64, int64) {
priceType = strings.ToLower(strings.TrimSpace(priceType))
if priceType == resourcePriceTypeFree {
return resourcePriceTypeFree, 0, 0
}
return resourcePriceTypeCoin, coinPrice, coinPrice
}
func validateResourcePricing(req resourceRequest) error {
priceType := strings.ToLower(strings.TrimSpace(req.PriceType))
switch priceType {
case resourcePriceTypeCoin:
if req.CoinPrice <= 0 {
return fmt.Errorf("请输入资源价格")
}
case resourcePriceTypeFree:
return nil
default:
return fmt.Errorf("请选择资源价格类型")
}
return nil
}
func resourceGrantStrategy(resourceType string) string {
switch resourceType {
case resourceTypeCoin:

View File

@ -179,6 +179,11 @@ func (s *Service) handleLeaderCCChangeCoin(ctx context.Context, app string, plat
raw, contentType := leaderccJSON(code, message, map[string]any{"coin": balance})
return raw, contentType, strconv.Itoa(code), true, providerOrderID, nil
}
if result.IdempotentReplay {
// 灵仙要求“成功订单的重复通知”也显式返回 10003这里不重复改账只回历史余额。
raw, contentType := leaderccJSON(leaderccCodeDuplicatedOrder, leaderccMessageFromError(xerr.New(xerr.IdempotencyConflict, "duplicated order")), map[string]any{"coin": result.BalanceAfter})
return raw, contentType, strconv.Itoa(leaderccCodeDuplicatedOrder), true, providerOrderID, nil
}
raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{"coin": result.BalanceAfter})
return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil
}

View File

@ -309,6 +309,20 @@ func TestHandleLeaderCCChangeCoinAndRepairOrder(t *testing.T) {
t.Fatalf("wallet command mismatch: %+v", wallet.lastApply)
}
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-change-replay", AppCode: "lalu"},
PlatformCode: "lingxian",
Operation: "change_coin",
RawBody: []byte(`{"orderId":"order_1","gameId":"101","roundId":"round_1","uid":"42","coin":120,"type":1,"rewardType":2,"token":"` + token + `","winId":"","roomId":"room_1","sign":"` + changeSign + `"}`),
RemoteAddr: "3.1.174.194:443",
})
if err != nil {
t.Fatalf("duplicate HandleCallback failed: %v", err)
}
if !strings.Contains(string(raw), `"errorCode":10003`) || !strings.Contains(string(raw), `"code":10003`) || !strings.Contains(string(raw), `"coin":1560`) {
t.Fatalf("leadercc duplicate change response mismatch: %s", raw)
}
repairSign := leaderccTestSign(key, "repair_1", "101", "round_2", "42", "50", "2", "", "room_1")
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-repair", AppCode: "lalu"},

View File

@ -260,7 +260,8 @@ func (s *Service) handleYomiRepairOrder(ctx context.Context, app string, platfor
}
type yomiCoinChangeResult struct {
BalanceAfter int64
BalanceAfter int64
IdempotentReplay bool
}
func (s *Service) applyYomiCoinChange(ctx context.Context, app string, req *gamev1.CallbackRequest, session gamedomain.LaunchSession, providerOrderID string, providerRoundID string, opType string, amount int64, roomID string, requestHash string) (yomiCoinChangeResult, error) {
@ -286,7 +287,7 @@ func (s *Service) applyYomiCoinChange(ctx context.Context, app string, req *game
}
if exists && order.Status == gamedomain.OrderStatusSucceeded {
// 已成功订单按幂等重放处理,直接返回历史余额,不能再次调用钱包。
return yomiCoinChangeResult{BalanceAfter: order.WalletBalanceAfter}, nil
return yomiCoinChangeResult{BalanceAfter: order.WalletBalanceAfter, IdempotentReplay: true}, nil
}
if amount == 0 {
// Yomi 文档允许投注/返奖金额为 0这类订单仍需标记成功但不能产生钱包流水。

View File

@ -32,6 +32,9 @@ type resourceData struct {
GrantStrategy string `json:"grant_strategy"`
WalletAssetType string `json:"wallet_asset_type,omitempty"`
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
PriceType string `json:"price_type,omitempty"`
CoinPrice int64 `json:"coin_price,omitempty"`
GiftPointAmount int64 `json:"gift_point_amount,omitempty"`
UsageScopes []string `json:"usage_scopes"`
AssetURL string `json:"asset_url"`
PreviewURL string `json:"preview_url"`
@ -328,6 +331,9 @@ func resourceFromProto(item *walletv1.Resource) resourceData {
GrantStrategy: item.GetGrantStrategy(),
WalletAssetType: item.GetWalletAssetType(),
WalletAssetAmount: item.GetWalletAssetAmount(),
PriceType: item.GetPriceType(),
CoinPrice: item.GetCoinPrice(),
GiftPointAmount: item.GetGiftPointAmount(),
UsageScopes: item.GetUsageScopes(),
AssetURL: item.GetAssetUrl(),
PreviewURL: item.GetPreviewUrl(),

View File

@ -12,6 +12,9 @@ type resourceData struct {
GrantStrategy string `json:"grant_strategy"`
WalletAssetType string `json:"wallet_asset_type,omitempty"`
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
PriceType string `json:"price_type,omitempty"`
CoinPrice int64 `json:"coin_price,omitempty"`
GiftPointAmount int64 `json:"gift_point_amount,omitempty"`
UsageScopes []string `json:"usage_scopes"`
AssetURL string `json:"asset_url"`
PreviewURL string `json:"preview_url"`
@ -106,6 +109,9 @@ func resourceFromProto(item *walletv1.Resource) resourceData {
GrantStrategy: item.GetGrantStrategy(),
WalletAssetType: item.GetWalletAssetType(),
WalletAssetAmount: item.GetWalletAssetAmount(),
PriceType: item.GetPriceType(),
CoinPrice: item.GetCoinPrice(),
GiftPointAmount: item.GetGiftPointAmount(),
UsageScopes: item.GetUsageScopes(),
AssetURL: item.GetAssetUrl(),
PreviewURL: item.GetPreviewUrl(),

View File

@ -21,6 +21,9 @@ type resourceData struct {
GrantStrategy string `json:"grant_strategy"`
WalletAssetType string `json:"wallet_asset_type,omitempty"`
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
PriceType string `json:"price_type,omitempty"`
CoinPrice int64 `json:"coin_price,omitempty"`
GiftPointAmount int64 `json:"gift_point_amount,omitempty"`
UsageScopes []string `json:"usage_scopes"`
AssetURL string `json:"asset_url"`
PreviewURL string `json:"preview_url"`
@ -53,6 +56,9 @@ func resourceFromProto(item *walletv1.Resource) resourceData {
GrantStrategy: item.GetGrantStrategy(),
WalletAssetType: item.GetWalletAssetType(),
WalletAssetAmount: item.GetWalletAssetAmount(),
PriceType: item.GetPriceType(),
CoinPrice: item.GetCoinPrice(),
GiftPointAmount: item.GetGiftPointAmount(),
UsageScopes: item.GetUsageScopes(),
AssetURL: item.GetAssetUrl(),
PreviewURL: item.GetPreviewUrl(),

View File

@ -197,6 +197,9 @@ CREATE TABLE IF NOT EXISTS resources (
grant_strategy VARCHAR(32) NOT NULL COMMENT '发放策略',
wallet_asset_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '钱包资产类型',
wallet_asset_amount BIGINT NOT NULL DEFAULT 0 COMMENT '钱包资产数量',
price_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '资源价格类型coin/free',
coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '资源金币价格',
gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT '资源礼物积分',
usage_scope_json JSON NOT NULL COMMENT '使用范围 JSON 配置或快照',
asset_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '资产 URL',
preview_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '预览 URL',
@ -221,6 +224,33 @@ PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND COLUMN_NAME = 'price_type') = 0,
'ALTER TABLE resources ADD COLUMN price_type VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''资源价格类型coin/free'' AFTER wallet_asset_amount',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND COLUMN_NAME = 'coin_price') = 0,
'ALTER TABLE resources ADD COLUMN coin_price BIGINT NOT NULL DEFAULT 0 COMMENT ''资源金币价格'' AFTER price_type',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND COLUMN_NAME = 'gift_point_amount') = 0,
'ALTER TABLE resources ADD COLUMN gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT ''资源礼物积分'' AFTER coin_price',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND INDEX_NAME = 'idx_resources_manager_grant') = 0,
'ALTER TABLE resources ADD KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, grantable, sort_order)',
@ -570,22 +600,22 @@ INSERT IGNORE INTO wallet_gift_prices (
INSERT IGNORE INTO resources (
app_code, resource_code, resource_type, name, status, grantable, grant_strategy,
wallet_asset_type, wallet_asset_amount, usage_scope_json, asset_url, preview_url,
wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount, usage_scope_json, asset_url, preview_url,
animation_url, metadata_json, sort_order, created_by_user_id, updated_by_user_id,
created_at_ms, updated_at_ms
) VALUES
('lalu', 'gift_rose', 'gift', 'Rose', 'active', TRUE, 'increase_quantity', '', 0, JSON_ARRAY('gift'), '', '', '', JSON_OBJECT(), 10, 0, 0, 0, 0),
('lalu', 'gift_rocket', 'gift', 'Rocket', 'active', TRUE, 'increase_quantity', '', 0, JSON_ARRAY('gift'), '', '', '', JSON_OBJECT(), 20, 0, 0, 0, 0);
('lalu', 'gift_rose', 'gift', 'Rose', 'active', TRUE, 'increase_quantity', '', 0, 'coin', 10, 10, JSON_ARRAY('gift'), '', '', '', JSON_OBJECT(), 10, 0, 0, 0, 0),
('lalu', 'gift_rocket', 'gift', 'Rocket', 'active', TRUE, 'increase_quantity', '', 0, 'coin', 100, 100, JSON_ARRAY('gift'), '', '', '', JSON_OBJECT(), 20, 0, 0, 0, 0);
INSERT IGNORE INTO resources (
app_code, resource_code, resource_type, name, status, grantable, grant_strategy,
wallet_asset_type, wallet_asset_amount, usage_scope_json, asset_url, preview_url,
wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount, usage_scope_json, asset_url, preview_url,
animation_url, metadata_json, sort_order, created_by_user_id, updated_by_user_id,
created_at_ms, updated_at_ms
) VALUES
('lalu', 'vip1_avatar_frame', 'avatar_frame', 'VIP1 Avatar Frame', 'active', TRUE, 'set_active_flag', '', 0, JSON_ARRAY('profile', 'room'), '', '', '', JSON_OBJECT(), 101, 0, 0, 0, 0),
('lalu', 'vip2_avatar_frame', 'avatar_frame', 'VIP2 Avatar Frame', 'active', TRUE, 'set_active_flag', '', 0, JSON_ARRAY('profile', 'room'), '', '', '', JSON_OBJECT(), 102, 0, 0, 0, 0),
('lalu', 'vip3_vehicle', 'vehicle', 'VIP3 Vehicle', 'active', TRUE, 'set_active_flag', '', 0, JSON_ARRAY('room_entry'), '', '', '', JSON_OBJECT(), 103, 0, 0, 0, 0);
('lalu', 'vip1_avatar_frame', 'avatar_frame', 'VIP1 Avatar Frame', 'active', TRUE, 'set_active_flag', '', 0, 'free', 0, 0, JSON_ARRAY('profile', 'room'), '', '', '', JSON_OBJECT(), 101, 0, 0, 0, 0),
('lalu', 'vip2_avatar_frame', 'avatar_frame', 'VIP2 Avatar Frame', 'active', TRUE, 'set_active_flag', '', 0, 'free', 0, 0, JSON_ARRAY('profile', 'room'), '', '', '', JSON_OBJECT(), 102, 0, 0, 0, 0),
('lalu', 'vip3_vehicle', 'vehicle', 'VIP3 Vehicle', 'active', TRUE, 'set_active_flag', '', 0, 'free', 0, 0, JSON_ARRAY('room_entry'), '', '', '', JSON_OBJECT(), 103, 0, 0, 0, 0);
INSERT IGNORE INTO resource_groups (
app_code, group_code, name, status, description, sort_order,

View File

@ -68,6 +68,9 @@ const (
GiftEffectAnimation = "animation"
GiftEffectMusic = "music"
GiftEffectGlobalBroadcast = "global_broadcast"
PriceTypeCoin = "coin"
PriceTypeFree = "free"
)
type Resource struct {
@ -81,6 +84,9 @@ type Resource struct {
GrantStrategy string
WalletAssetType string
WalletAssetAmount int64
PriceType string
CoinPrice int64
GiftPointAmount int64
UsageScopes []string
AssetURL string
PreviewURL string
@ -229,6 +235,9 @@ type ResourceCommand struct {
GrantStrategy string
WalletAssetType string
WalletAssetAmount int64
PriceType string
CoinPrice int64
GiftPointAmount int64
UsageScopes []string
AssetURL string
PreviewURL string
@ -472,6 +481,19 @@ func NormalizeWalletAssetType(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
func NormalizePriceType(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func ValidPriceType(value string) bool {
switch NormalizePriceType(value) {
case PriceTypeCoin, PriceTypeFree:
return true
default:
return false
}
}
func NormalizeGiftTypeCode(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
value = strings.ReplaceAll(value, "-", "_")

View File

@ -764,6 +764,99 @@ func TestGiftConfigSupportsDiamondChargeAndEffectiveRange(t *testing.T) {
}
}
func TestGiftConfigSyncsConfiguredResourcePrice(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)
ctx := context.Background()
paidResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "gift_price_sync",
ResourceType: resourcedomain.TypeGift,
Name: "Price Sync Gift",
Status: resourcedomain.StatusActive,
Grantable: true,
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
PriceType: resourcedomain.PriceTypeCoin,
CoinPrice: 77,
UsageScopes: []string{"gift"},
OperatorUserID: 90001,
})
if err != nil {
t.Fatalf("create priced gift resource failed: %v", err)
}
if paidResource.GiftPointAmount != 77 {
t.Fatalf("resource gift point should sync coin price: %+v", paidResource)
}
gift, err := svc.CreateGiftConfig(ctx, resourcedomain.GiftConfigCommand{
GiftID: "price-sync",
ResourceID: paidResource.ResourceID,
Status: resourcedomain.StatusActive,
Name: "Price Sync Gift",
PriceVersion: "v1",
ChargeAssetType: ledger.AssetDiamond,
CoinPrice: 1,
GiftPointAmount: 1,
HeatValue: 3,
GiftTypeCode: resourcedomain.GiftTypeNormal,
OperatorUserID: 90001,
RegionIDs: []int64{0},
})
if err != nil {
t.Fatalf("create synced gift config failed: %v", err)
}
if gift.ChargeAssetType != ledger.AssetCoin || gift.CoinPrice != 77 || gift.GiftPointAmount != 77 {
t.Fatalf("gift price should be overridden by resource price: %+v", gift)
}
freeResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "gift_free_sync",
ResourceType: resourcedomain.TypeGift,
Name: "Free Sync Gift",
Status: resourcedomain.StatusActive,
Grantable: true,
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
PriceType: resourcedomain.PriceTypeFree,
UsageScopes: []string{"gift"},
OperatorUserID: 90001,
})
if err != nil {
t.Fatalf("create free gift resource failed: %v", err)
}
freeGift, err := svc.CreateGiftConfig(ctx, resourcedomain.GiftConfigCommand{
GiftID: "free-sync",
ResourceID: freeResource.ResourceID,
Status: resourcedomain.StatusActive,
Name: "Free Sync Gift",
PriceVersion: "v1",
CoinPrice: 9,
GiftPointAmount: 9,
HeatValue: 0,
GiftTypeCode: resourcedomain.GiftTypeNormal,
OperatorUserID: 90001,
RegionIDs: []int64{0},
})
if err != nil {
t.Fatalf("create free synced gift config failed: %v", err)
}
if freeGift.CoinPrice != 0 || freeGift.GiftPointAmount != 0 {
t.Fatalf("free gift price should sync to zero: %+v", freeGift)
}
receipt, err := svc.DebitGift(ctx, ledger.DebitGiftCommand{
CommandID: "cmd-free-sync",
RoomID: "room-free",
SenderUserID: 53001,
TargetUserID: 53002,
GiftID: "free-sync",
GiftCount: 1,
})
if err != nil {
t.Fatalf("free gift debit should not require existing balance: %v", err)
}
if receipt.ChargeAmount != 0 || receipt.GiftPointAdded != 0 {
t.Fatalf("free gift receipt mismatch: %+v", receipt)
}
}
func TestGiftTypeConfigDefaultsAndUpsert(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)

View File

@ -130,7 +130,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
return ledger.Receipt{}, err
}
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, price.ChargeAssetType, false, nowMs)
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, price.ChargeAssetType, chargeAmount == 0, nowMs)
if err != nil {
return ledger.Receipt{}, err
}
@ -990,7 +990,7 @@ func (r *Repository) resolveGiftPrice(ctx context.Context, tx *sql.Tx, giftID st
}
return giftPrice{}, err
}
if price.CoinPrice <= 0 || price.GiftPointAmount < 0 || price.HeatValue < 0 {
if price.CoinPrice < 0 || price.GiftPointAmount < 0 || price.HeatValue < 0 {
return giftPrice{}, xerr.New(xerr.Internal, "gift price is invalid")
}
price.ChargeAssetType = ledger.NormalizeGiftChargeAssetType(price.ChargeAssetType)

View File

@ -117,10 +117,10 @@ func (r *Repository) CreateResource(ctx context.Context, command resourcedomain.
result, err := tx.ExecContext(ctx, `
INSERT INTO resources (
app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled, grant_strategy,
wallet_asset_type, wallet_asset_amount, usage_scope_json, asset_url, preview_url,
wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount, usage_scope_json, asset_url, preview_url,
animation_url, metadata_json, sort_order, created_by_user_id, updated_by_user_id,
created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
command.AppCode,
command.ResourceCode,
command.ResourceType,
@ -131,6 +131,9 @@ func (r *Repository) CreateResource(ctx context.Context, command resourcedomain.
command.GrantStrategy,
command.WalletAssetType,
command.WalletAssetAmount,
command.PriceType,
command.CoinPrice,
command.GiftPointAmount,
usageJSON,
command.AssetURL,
command.PreviewURL,
@ -194,6 +197,7 @@ func (r *Repository) UpdateResource(ctx context.Context, command resourcedomain.
UPDATE resources
SET resource_code = ?, resource_type = ?, name = ?, status = ?, grantable = ?, manager_grant_enabled = ?,
grant_strategy = ?, wallet_asset_type = ?, wallet_asset_amount = ?,
price_type = ?, coin_price = ?, gift_point_amount = ?,
usage_scope_json = ?, asset_url = ?, preview_url = ?, animation_url = ?,
metadata_json = ?, sort_order = ?, updated_by_user_id = ?, updated_at_ms = ?
WHERE app_code = ? AND resource_id = ?`,
@ -206,6 +210,9 @@ func (r *Repository) UpdateResource(ctx context.Context, command resourcedomain.
command.GrantStrategy,
command.WalletAssetType,
command.WalletAssetAmount,
command.PriceType,
command.CoinPrice,
command.GiftPointAmount,
usageJSON,
command.AssetURL,
command.PreviewURL,
@ -959,8 +966,8 @@ func (r *Repository) upsertGiftConfig(ctx context.Context, command resourcedomai
ctx = contextWithCommandApp(ctx, command.AppCode)
command.AppCode = appcode.FromContext(ctx)
command = normalizeGiftConfigCommand(command)
if err := validateGiftConfigCommand(command); err != nil {
return resourcedomain.GiftConfig{}, err
if command.ResourceID <= 0 {
return resourcedomain.GiftConfig{}, xerr.New(xerr.InvalidArgument, "gift config command is incomplete")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
@ -975,6 +982,13 @@ func (r *Repository) upsertGiftConfig(ctx context.Context, command resourcedomai
if resource.ResourceType != resourcedomain.TypeGift {
return resourcedomain.GiftConfig{}, xerr.New(xerr.InvalidArgument, "gift config must select gift resource")
}
command = applyResourcePricingToGiftCommand(command, resource)
if resourcedomain.NormalizePriceType(resource.PriceType) == "" && command.CoinPrice <= 0 {
return resourcedomain.GiftConfig{}, xerr.New(xerr.InvalidArgument, "gift price is invalid")
}
if err := validateGiftConfigCommand(command); err != nil {
return resourcedomain.GiftConfig{}, err
}
if command.Status == resourcedomain.StatusActive && resource.Status != resourcedomain.StatusActive {
return resourcedomain.GiftConfig{}, xerr.New(xerr.Conflict, "gift resource is disabled")
}
@ -1766,6 +1780,9 @@ func scanResource(scanner scanTarget) (resourcedomain.Resource, error) {
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
&resource.PriceType,
&resource.CoinPrice,
&resource.GiftPointAmount,
&scopesJSON,
&resource.AssetURL,
&resource.PreviewURL,
@ -1823,6 +1840,9 @@ func scanResourceGroupItem(scanner scanTarget) (resourcedomain.ResourceGroupItem
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
&resource.PriceType,
&resource.CoinPrice,
&resource.GiftPointAmount,
&scopesJSON,
&resource.AssetURL,
&resource.PreviewURL,
@ -1878,6 +1898,9 @@ func scanGiftConfig(scanner scanTarget) (resourcedomain.GiftConfig, error) {
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
&resource.PriceType,
&resource.CoinPrice,
&resource.GiftPointAmount,
&scopesJSON,
&resource.AssetURL,
&resource.PreviewURL,
@ -1942,6 +1965,9 @@ func scanUserResourceEntitlement(scanner scanTarget) (resourcedomain.UserResourc
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
&resource.PriceType,
&resource.CoinPrice,
&resource.GiftPointAmount,
&scopesJSON,
&resource.AssetURL,
&resource.PreviewURL,
@ -1990,7 +2016,7 @@ func resourceColumnsWithAlias(alias string) string {
}
return prefix + `app_code, ` + prefix + `resource_id, ` + prefix + `resource_code, ` + prefix + `resource_type, ` +
prefix + `name, ` + prefix + `status, ` + prefix + `grantable, ` + prefix + `manager_grant_enabled, ` + prefix + `grant_strategy, ` +
prefix + `wallet_asset_type, ` + prefix + `wallet_asset_amount, COALESCE(CAST(` + prefix + `usage_scope_json AS CHAR), '[]'), ` +
prefix + `wallet_asset_type, ` + prefix + `wallet_asset_amount, ` + prefix + `price_type, ` + prefix + `coin_price, ` + prefix + `gift_point_amount, COALESCE(CAST(` + prefix + `usage_scope_json AS CHAR), '[]'), ` +
prefix + `asset_url, ` + prefix + `preview_url, ` + prefix + `animation_url, COALESCE(CAST(` + prefix + `metadata_json AS CHAR), '{}'), ` +
prefix + `sort_order, ` + prefix + `created_by_user_id, ` + prefix + `updated_by_user_id, ` + prefix + `created_at_ms, ` + prefix + `updated_at_ms`
}
@ -2003,7 +2029,7 @@ func nullableResourceColumnsWithAlias(alias string) string {
return `COALESCE(` + prefix + `app_code, ''), COALESCE(` + prefix + `resource_id, 0), COALESCE(` + prefix + `resource_code, ''), ` +
`COALESCE(` + prefix + `resource_type, ''), COALESCE(` + prefix + `name, ''), COALESCE(` + prefix + `status, ''), ` +
`COALESCE(` + prefix + `grantable, FALSE), COALESCE(` + prefix + `manager_grant_enabled, FALSE), COALESCE(` + prefix + `grant_strategy, ''), COALESCE(` + prefix + `wallet_asset_type, ''), ` +
`COALESCE(` + prefix + `wallet_asset_amount, 0), COALESCE(CAST(` + prefix + `usage_scope_json AS CHAR), '[]'), ` +
`COALESCE(` + prefix + `wallet_asset_amount, 0), COALESCE(` + prefix + `price_type, ''), COALESCE(` + prefix + `coin_price, 0), COALESCE(` + prefix + `gift_point_amount, 0), COALESCE(CAST(` + prefix + `usage_scope_json AS CHAR), '[]'), ` +
`COALESCE(` + prefix + `asset_url, ''), COALESCE(` + prefix + `preview_url, ''), COALESCE(` + prefix + `animation_url, ''), ` +
`COALESCE(CAST(` + prefix + `metadata_json AS CHAR), '{}'), COALESCE(` + prefix + `sort_order, 0), ` +
`COALESCE(` + prefix + `created_by_user_id, 0), COALESCE(` + prefix + `updated_by_user_id, 0), ` +
@ -2196,6 +2222,13 @@ func normalizeResourceCommand(command resourcedomain.ResourceCommand) resourcedo
command.Status = resourcedomain.NormalizeStatus(command.Status)
command.GrantStrategy = resourcedomain.NormalizeGrantStrategy(command.GrantStrategy)
command.WalletAssetType = resourcedomain.NormalizeWalletAssetType(command.WalletAssetType)
command.PriceType = resourcedomain.NormalizePriceType(command.PriceType)
if command.PriceType == resourcedomain.PriceTypeFree {
command.CoinPrice = 0
command.GiftPointAmount = 0
} else if command.PriceType == resourcedomain.PriceTypeCoin {
command.GiftPointAmount = command.CoinPrice
}
command.AssetURL = strings.TrimSpace(command.AssetURL)
command.PreviewURL = strings.TrimSpace(command.PreviewURL)
command.AnimationURL = strings.TrimSpace(command.AnimationURL)
@ -2221,10 +2254,23 @@ func validateResourceCommand(command resourcedomain.ResourceCommand) error {
if command.GrantStrategy != resourcedomain.GrantStrategyWalletCredit || command.WalletAssetType != ledger.AssetCoin || command.WalletAssetAmount <= 0 {
return xerr.New(xerr.InvalidArgument, "coin resource must credit COIN")
}
} else if command.GrantStrategy == resourcedomain.GrantStrategyWalletCredit || command.WalletAssetType != "" || command.WalletAssetAmount != 0 {
return xerr.New(xerr.InvalidArgument, "non-coin resource cannot use wallet credit")
}
if command.PriceType == "" {
if command.CoinPrice != 0 || command.GiftPointAmount != 0 {
return xerr.New(xerr.InvalidArgument, "resource price type is required")
}
return nil
}
if command.GrantStrategy == resourcedomain.GrantStrategyWalletCredit || command.WalletAssetType != "" || command.WalletAssetAmount != 0 {
return xerr.New(xerr.InvalidArgument, "non-coin resource cannot use wallet credit")
if !resourcedomain.ValidPriceType(command.PriceType) {
return xerr.New(xerr.InvalidArgument, "resource price type is invalid")
}
if command.PriceType == resourcedomain.PriceTypeCoin && command.CoinPrice <= 0 {
return xerr.New(xerr.InvalidArgument, "resource coin price is invalid")
}
if command.CoinPrice < 0 || command.GiftPointAmount < 0 || command.GiftPointAmount != command.CoinPrice {
return xerr.New(xerr.InvalidArgument, "resource price is invalid")
}
return nil
}
@ -2302,6 +2348,20 @@ func validGroupWalletAssetType(assetType string) bool {
}
}
func applyResourcePricingToGiftCommand(command resourcedomain.GiftConfigCommand, resource resourcedomain.Resource) resourcedomain.GiftConfigCommand {
switch resourcedomain.NormalizePriceType(resource.PriceType) {
case resourcedomain.PriceTypeCoin:
command.ChargeAssetType = ledger.AssetCoin
command.CoinPrice = resource.CoinPrice
command.GiftPointAmount = resource.GiftPointAmount
case resourcedomain.PriceTypeFree:
command.ChargeAssetType = ledger.AssetCoin
command.CoinPrice = 0
command.GiftPointAmount = 0
}
return command
}
func normalizeGiftConfigCommand(command resourcedomain.GiftConfigCommand) resourcedomain.GiftConfigCommand {
command.GiftID = strings.TrimSpace(command.GiftID)
command.Status = resourcedomain.NormalizeStatus(command.Status)
@ -2342,7 +2402,7 @@ func validateGiftConfigCommand(command resourcedomain.GiftConfigCommand) error {
if !resourcedomain.ValidStatus(command.Status) {
return xerr.New(xerr.InvalidArgument, "status is invalid")
}
if command.CoinPrice <= 0 || command.GiftPointAmount < 0 || command.HeatValue < 0 {
if command.CoinPrice < 0 || command.GiftPointAmount < 0 || command.HeatValue < 0 {
return xerr.New(xerr.InvalidArgument, "gift price is invalid")
}
if !resourcedomain.ValidGiftTypeCode(command.GiftTypeCode) {

View File

@ -298,6 +298,9 @@ func resourceCommandFromCreate(req *walletv1.CreateResourceRequest) resourcedoma
GrantStrategy: req.GetGrantStrategy(),
WalletAssetType: req.GetWalletAssetType(),
WalletAssetAmount: req.GetWalletAssetAmount(),
PriceType: req.GetPriceType(),
CoinPrice: req.GetCoinPrice(),
GiftPointAmount: req.GetGiftPointAmount(),
UsageScopes: req.GetUsageScopes(),
AssetURL: req.GetAssetUrl(),
PreviewURL: req.GetPreviewUrl(),
@ -320,6 +323,9 @@ func resourceCommandFromUpdate(req *walletv1.UpdateResourceRequest) resourcedoma
GrantStrategy: req.GetGrantStrategy(),
WalletAssetType: req.GetWalletAssetType(),
WalletAssetAmount: req.GetWalletAssetAmount(),
PriceType: req.GetPriceType(),
CoinPrice: req.GetCoinPrice(),
GiftPointAmount: req.GetGiftPointAmount(),
UsageScopes: req.GetUsageScopes(),
AssetUrl: req.GetAssetUrl(),
PreviewUrl: req.GetPreviewUrl(),
@ -439,6 +445,9 @@ func resourceToProto(resource resourcedomain.Resource) *walletv1.Resource {
GrantStrategy: resource.GrantStrategy,
WalletAssetType: resource.WalletAssetType,
WalletAssetAmount: resource.WalletAssetAmount,
PriceType: resource.PriceType,
CoinPrice: resource.CoinPrice,
GiftPointAmount: resource.GiftPointAmount,
UsageScopes: resource.UsageScopes,
AssetUrl: resource.AssetURL,
PreviewUrl: resource.PreviewURL,