任务配置
This commit is contained in:
parent
0439da10f8
commit
2ed7b6bf78
File diff suppressed because it is too large
Load Diff
@ -261,6 +261,19 @@ message CronBatchResponse {
|
||||
bool has_more = 5;
|
||||
}
|
||||
|
||||
// TaskRewardItem 是任务保存时从 wallet 资源目录解析出的展示快照。
|
||||
// resource 奖励只给客户端封面和资源语义;名称不进入任务 H5,避免运营改名影响任务卡片。
|
||||
message TaskRewardItem {
|
||||
string item_type = 1;
|
||||
int64 resource_id = 2;
|
||||
string resource_type = 3;
|
||||
string cover_url = 4;
|
||||
int64 quantity = 5;
|
||||
int64 duration_ms = 6;
|
||||
string wallet_asset_type = 7;
|
||||
int64 wallet_asset_amount = 8;
|
||||
}
|
||||
|
||||
// TaskItem 是 App 每日任务/专属任务列表里的单个可展示任务快照。
|
||||
message TaskItem {
|
||||
string task_id = 1;
|
||||
@ -288,6 +301,12 @@ message TaskItem {
|
||||
string action_payload_json = 23;
|
||||
string dimension_filter_json = 24;
|
||||
string reward_asset_type = 25;
|
||||
string reward_type = 26;
|
||||
int64 reward_resource_id = 27;
|
||||
int64 reward_resource_group_id = 28;
|
||||
int64 reward_quantity = 29;
|
||||
int64 reward_duration_ms = 30;
|
||||
repeated TaskRewardItem reward_items = 31;
|
||||
}
|
||||
|
||||
// TaskSection 固定表达 App 任务页的 daily/exclusive 两个分区。
|
||||
@ -333,6 +352,11 @@ message ClaimTaskRewardResponse {
|
||||
int64 granted_at_ms = 8;
|
||||
bool claimed = 9;
|
||||
string reward_asset_type = 10;
|
||||
string reward_type = 11;
|
||||
int64 reward_resource_id = 12;
|
||||
int64 reward_resource_group_id = 13;
|
||||
string wallet_resource_grant_id = 14;
|
||||
repeated TaskRewardItem reward_items = 15;
|
||||
}
|
||||
|
||||
// ConsumeTaskEventRequest 是异步 outbox consumer 投递给任务系统的事实事件。
|
||||
@ -481,6 +505,12 @@ message TaskDefinition {
|
||||
string action_payload_json = 24;
|
||||
string dimension_filter_json = 25;
|
||||
string reward_asset_type = 26;
|
||||
string reward_type = 27;
|
||||
int64 reward_resource_id = 28;
|
||||
int64 reward_resource_group_id = 29;
|
||||
int64 reward_quantity = 30;
|
||||
int64 reward_duration_ms = 31;
|
||||
repeated TaskRewardItem reward_items = 32;
|
||||
}
|
||||
|
||||
// ListTaskDefinitionsRequest 是后台任务配置列表筛选条件。
|
||||
@ -524,6 +554,11 @@ message UpsertTaskDefinitionRequest {
|
||||
string action_payload_json = 21;
|
||||
string dimension_filter_json = 22;
|
||||
string reward_asset_type = 23;
|
||||
string reward_type = 24;
|
||||
int64 reward_resource_id = 25;
|
||||
int64 reward_resource_group_id = 26;
|
||||
int64 reward_quantity = 27;
|
||||
int64 reward_duration_ms = 28;
|
||||
}
|
||||
|
||||
message UpsertTaskDefinitionResponse {
|
||||
|
||||
@ -144,7 +144,7 @@ curl -X GET 'http://127.0.0.1:13000/api/v1/app/explore-tabs' \
|
||||
| `tab` | string | App 展示的 tab 文案。 |
|
||||
| `h5_url` | string | 点击 tab 后加载的 H5 地址。 |
|
||||
| `enabled` | bool | 是否启用。App 接口只返回 `true` 的记录。 |
|
||||
| `sort_order` | int | 展示排序,数值越小越靠前。 |
|
||||
| `sort_order` | int | 展示排序,数值越大越靠前。 |
|
||||
| `updated_at_ms` | int | 更新时间,Unix epoch milliseconds。 |
|
||||
|
||||
### 空列表响应
|
||||
|
||||
@ -55,7 +55,7 @@ X-App-Platform: android
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `items` | 当前可展示的开屏配置列表,已按 `sort_order ASC, id DESC` 排序。 |
|
||||
| `items` | 当前可展示的开屏配置列表,已按 `sort_order DESC, id DESC` 排序。 |
|
||||
| `cover_url` | 开屏图片地址。 |
|
||||
| `type` | `h5` 或 `app`。 |
|
||||
| `param` | `type=h5` 时是 H5 链接;`type=app` 时由客户端解释。 |
|
||||
|
||||
@ -50,7 +50,7 @@ X-App-Code: lalu
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `items` | 当前可展示的弹窗配置列表,已按 `sort_order ASC, id DESC` 排序。 |
|
||||
| `items` | 当前可展示的弹窗配置列表,已按 `sort_order DESC, id DESC` 排序。 |
|
||||
| `code` | 弹窗编码,同一个 App 下唯一,用于 Flutter 本地频控和埋点识别。 |
|
||||
| `name` | 弹窗名字。 |
|
||||
| `image_url` | 弹窗图片地址,可能为空。 |
|
||||
|
||||
@ -144,7 +144,7 @@ flowchart LR
|
||||
| 指标类型 | 是 | 按分类收敛,例如 `mic_online_minutes`、`gift_spend_coin` |
|
||||
| 目标值 | 是 | 必须大于 0 |
|
||||
| 奖励金币 | 是 | 必须大于 0 |
|
||||
| 排序值 | 是 | 小值靠前 |
|
||||
| 排序值 | 是 | 大值靠前 |
|
||||
| 生效/结束时间 | 否 | 控制任务上下架窗口 |
|
||||
| 状态 | 是 | draft 保存草稿,active 对 App 生效 |
|
||||
|
||||
|
||||
@ -404,7 +404,7 @@ Response:
|
||||
规则:
|
||||
|
||||
- 只返回 `enabled=true` 的国家。
|
||||
- 按 `sort_order ASC, country_code ASC` 排序。
|
||||
- 按 `sort_order DESC, country_code ASC` 排序。
|
||||
- `country_code` 是注册页提交给 `CompleteOnboarding` 的唯一国家值。
|
||||
- `phone_country_code` 只用于注册页展示或后续手机号能力预填,不参与国家归属判断。
|
||||
- 该接口可以由 gateway 短 TTL 缓存,但必须在后台启用/禁用国家后尽快失效。
|
||||
|
||||
@ -205,7 +205,7 @@ def fetch_resources(conn, app_code: str) -> list[dict[str, Any]]:
|
||||
sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||
FROM resources
|
||||
WHERE app_code = %s AND status = 'active'
|
||||
ORDER BY sort_order ASC, resource_id ASC
|
||||
ORDER BY sort_order DESC, resource_id ASC
|
||||
""",
|
||||
(app_code,),
|
||||
)
|
||||
|
||||
@ -38,8 +38,13 @@ type taskRequest struct {
|
||||
DimensionFilterJSON string `json:"dimension_filter_json"`
|
||||
TargetValue int64 `json:"target_value"`
|
||||
TargetUnit string `json:"target_unit"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||
RewardAssetType string `json:"reward_asset_type" binding:"required,oneof=COIN POINT"`
|
||||
RewardAssetType string `json:"reward_asset_type"`
|
||||
RewardResourceID int64 `json:"reward_resource_id"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
RewardQuantity int64 `json:"reward_quantity"`
|
||||
RewardDurationMS int64 `json:"reward_duration_ms"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
EffectiveFromMS int64 `json:"effective_from_ms"`
|
||||
@ -66,8 +71,14 @@ type taskDTO struct {
|
||||
DimensionFilterJSON string `json:"dimension_filter_json"`
|
||||
TargetValue int64 `json:"target_value"`
|
||||
TargetUnit string `json:"target_unit"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||
RewardAssetType string `json:"reward_asset_type"`
|
||||
RewardResourceID int64 `json:"reward_resource_id"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
RewardQuantity int64 `json:"reward_quantity"`
|
||||
RewardDurationMS int64 `json:"reward_duration_ms"`
|
||||
RewardItems []taskRewardItemDTO `json:"reward_items"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
Version int64 `json:"version"`
|
||||
@ -79,6 +90,17 @@ type taskDTO struct {
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
type taskRewardItemDTO struct {
|
||||
ItemType string `json:"item_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
CoverURL string `json:"cover_url"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
WalletAssetType string `json:"wallet_asset_type"`
|
||||
WalletAssetAmount int64 `json:"wallet_asset_amount"`
|
||||
}
|
||||
|
||||
// ListTaskDefinitions 返回活动管理 > 每日任务页的 daily/exclusive 配置列表。
|
||||
func (h *Handler) ListTaskDefinitions(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
@ -190,6 +212,11 @@ func (r taskRequest) toProto(c *gin.Context, taskID string, meta *activityv1.Req
|
||||
TargetUnit: strings.TrimSpace(r.TargetUnit),
|
||||
RewardCoinAmount: r.RewardCoinAmount,
|
||||
RewardAssetType: strings.TrimSpace(r.RewardAssetType),
|
||||
RewardType: strings.TrimSpace(r.RewardType),
|
||||
RewardResourceId: r.RewardResourceID,
|
||||
RewardResourceGroupId: r.RewardResourceGroupID,
|
||||
RewardQuantity: r.RewardQuantity,
|
||||
RewardDurationMs: r.RewardDurationMS,
|
||||
Status: strings.TrimSpace(r.Status),
|
||||
SortOrder: r.SortOrder,
|
||||
EffectiveFromMs: r.EffectiveFromMS,
|
||||
@ -237,6 +264,12 @@ func taskFromProto(item *activityv1.TaskDefinition) taskDTO {
|
||||
TargetUnit: item.GetTargetUnit(),
|
||||
RewardCoinAmount: item.GetRewardCoinAmount(),
|
||||
RewardAssetType: item.GetRewardAssetType(),
|
||||
RewardType: item.GetRewardType(),
|
||||
RewardResourceID: item.GetRewardResourceId(),
|
||||
RewardResourceGroupID: item.GetRewardResourceGroupId(),
|
||||
RewardQuantity: item.GetRewardQuantity(),
|
||||
RewardDurationMS: item.GetRewardDurationMs(),
|
||||
RewardItems: taskRewardItemsFromProto(item.GetRewardItems()),
|
||||
Status: item.GetStatus(),
|
||||
SortOrder: item.GetSortOrder(),
|
||||
Version: item.GetVersion(),
|
||||
@ -249,6 +282,26 @@ func taskFromProto(item *activityv1.TaskDefinition) taskDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func taskRewardItemsFromProto(items []*activityv1.TaskRewardItem) []taskRewardItemDTO {
|
||||
result := make([]taskRewardItemDTO, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, taskRewardItemDTO{
|
||||
ItemType: item.GetItemType(),
|
||||
ResourceID: item.GetResourceId(),
|
||||
ResourceType: item.GetResourceType(),
|
||||
CoverURL: item.GetCoverUrl(),
|
||||
Quantity: item.GetQuantity(),
|
||||
DurationMS: item.GetDurationMs(),
|
||||
WalletAssetType: item.GetWalletAssetType(),
|
||||
WalletAssetAmount: item.GetWalletAssetAmount(),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func firstQuery(c *gin.Context, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := c.Query(key); value != "" {
|
||||
|
||||
@ -91,7 +91,7 @@ func TestRoomRPSConfigRequestDefaultsOpenConfig(t *testing.T) {
|
||||
if got.GetStatus() != "active" || got.GetChallengeTimeoutMs() != 600000 || got.GetRevealCountdownMs() != 3000 {
|
||||
t.Fatalf("room rps default config mismatch: %+v", got)
|
||||
}
|
||||
if got.GetStakeGifts()[3].GetSortOrder() != 4 {
|
||||
if got.GetStakeGifts()[0].GetSortOrder() != 4 || got.GetStakeGifts()[3].GetSortOrder() != 1 {
|
||||
t.Fatalf("default sort order mismatch: %+v", got.GetStakeGifts())
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,8 +247,8 @@ func (r roomRPSConfigRequest) toProto() (*gamev1.RoomRPSConfig, error) {
|
||||
seen[giftID] = struct{}{}
|
||||
sortOrder := gift.SortOrder
|
||||
if sortOrder <= 0 {
|
||||
// 未显式给排序时按提交顺序落库,保证前端拖拽顺序和运行时展示顺序一致。
|
||||
sortOrder = int32(index + 1)
|
||||
// 未显式给排序时按提交顺序分配递减权重,保证大值优先后仍与前端从上到下的顺序一致。
|
||||
sortOrder = int32(len(r.StakeGifts) - index)
|
||||
}
|
||||
gifts = append(gifts, &gamev1.RoomRPSStakeGift{
|
||||
GiftId: legacyRoomRPSGiftID(giftID),
|
||||
|
||||
@ -328,7 +328,7 @@ func leaderCCCatalogItems(platformCode string, providerItems []leaderCCGameListI
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(providerItems), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterLeaderCCV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -389,7 +389,7 @@ func zeeoneCatalogItems(platformCode string, config zeeoneSyncConfig, req syncGa
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(keys), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterZeeOneV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -469,7 +469,7 @@ func vivaGamesCatalogItems(platformCode string, config vivaGamesSyncConfig, req
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(keys), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterVivaGamesV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -605,7 +605,7 @@ func reyouCatalogItems(platformCode string, config reyouSyncConfig, req syncGame
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(keys), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterReyouV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -641,7 +641,7 @@ func zgameCatalogItems(platformCode string, config zgameSyncConfig, req syncGame
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(keys), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterZGameV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -677,7 +677,7 @@ func amgCatalogItems(platformCode string, config amgSyncConfig, req syncGamesReq
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(keys), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterAMGV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -730,7 +730,7 @@ func yomiCatalogItems(platformCode string, config yomiSyncConfig, req syncGamesR
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(keys), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterYomiV4}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -986,7 +986,7 @@ func baishunCatalogItems(platformCode string, providerItems []baishunGameListIte
|
||||
SafeHeight: int32FromJSONValue(providerItem.SafeHeight),
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
SortOrder: defaultCatalogSortOrder(len(providerItems), index),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterBaishunV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
@ -1175,3 +1175,11 @@ func defaulted(value string, fallback string) string {
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func defaultCatalogSortOrder(total int, index int) int32 {
|
||||
// 厂商列表或稳定 key 列表已经给出默认展示顺序;大值优先后必须从高到低赋权,避免每次同步都把游戏倒序。
|
||||
if total <= 0 || index < 0 || index >= total {
|
||||
return 0
|
||||
}
|
||||
return int32((total - index) * 10)
|
||||
}
|
||||
|
||||
@ -1444,7 +1444,7 @@ func (r *Reader) ListCoinSellerSalaryRates(ctx context.Context, regionID int64)
|
||||
SELECT region_id, min_usd_minor, max_usd_minor, coin_per_usd, status, sort_order, updated_at_ms
|
||||
FROM coin_seller_salary_exchange_rate_tiers
|
||||
WHERE app_code = ? AND region_id = ?
|
||||
ORDER BY sort_order ASC, min_usd_minor ASC, tier_id ASC
|
||||
ORDER BY sort_order DESC, min_usd_minor ASC, tier_id ASC
|
||||
`, appctx.FromContext(ctx), regionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1506,7 +1506,8 @@ func (r *Reader) ReplaceCoinSellerSalaryRates(ctx context.Context, regionID int6
|
||||
}
|
||||
sortOrder := tier.SortOrder
|
||||
if sortOrder == 0 {
|
||||
sortOrder = (index + 1) * 10
|
||||
// 请求数组是后台从上到下的档位顺序;缺省排序按递减权重持久化,读回时保持原顺序。
|
||||
sortOrder = (len(tiers) - index) * 10
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO coin_seller_salary_exchange_rate_tiers (
|
||||
|
||||
@ -214,7 +214,8 @@ func (s *Service) ReplaceCoinSellerSalaryRates(ctx context.Context, actorID int6
|
||||
}
|
||||
sortOrder := item.SortOrder
|
||||
if sortOrder == 0 {
|
||||
sortOrder = (index + 1) * 10
|
||||
// 大值优先后,表单首行必须获得最高缺省权重,避免保存一次就把兑换档位倒置。
|
||||
sortOrder = (len(req.Tiers) - index) * 10
|
||||
}
|
||||
tiers = append(tiers, CoinSellerSalaryRateTier{
|
||||
RegionID: regionID,
|
||||
|
||||
@ -489,7 +489,7 @@ func (h *Handler) listMoneyRegions(ctx context.Context, access repository.MoneyA
|
||||
ON rc.app_code = r.app_code AND rc.region_id = r.region_id AND rc.status = 'active'
|
||||
WHERE r.status = 'active' AND `+where+`
|
||||
GROUP BY r.app_code, r.region_id, r.region_code, r.name, r.status, r.sort_order
|
||||
ORDER BY r.app_code ASC, r.sort_order ASC, r.name ASC`, args...)
|
||||
ORDER BY r.app_code ASC, r.sort_order DESC, r.name ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -522,7 +522,7 @@ func (h *Handler) listMoneyCountries(ctx context.Context, access repository.Mone
|
||||
LEFT JOIN region_countries rc
|
||||
ON rc.app_code = c.app_code AND rc.country_code = c.country_code AND rc.status = 'active'
|
||||
WHERE c.enabled = TRUE AND `+where+`
|
||||
ORDER BY c.app_code ASC, c.sort_order ASC, c.country_name ASC`, args...)
|
||||
ORDER BY c.app_code ASC, c.sort_order DESC, c.country_name ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -287,7 +287,8 @@ func legacyRegionDocumentsToMoneyData(sourceConfig config.MoneyRegionSourceConfi
|
||||
RegionCode: regionCode,
|
||||
Name: regionName,
|
||||
Status: "active",
|
||||
SortOrder: index + 1,
|
||||
// 旧文档数组本身是展示顺序;分配递减权重后仍满足全后台“大值靠前”的统一契约。
|
||||
SortOrder: len(documents) - index,
|
||||
Countries: countryCodes,
|
||||
})
|
||||
for countryIndex, countryCode := range countryCodes {
|
||||
@ -298,7 +299,7 @@ func legacyRegionDocumentsToMoneyData(sourceConfig config.MoneyRegionSourceConfi
|
||||
CountryDisplayName: countryCode,
|
||||
Enabled: true,
|
||||
RegionID: regionID,
|
||||
SortOrder: countryIndex + 1,
|
||||
SortOrder: len(countryCodes) - countryIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,7 +357,7 @@ func (h *Handler) listRechargeBillRegions(ctx context.Context, appCode string) (
|
||||
SELECT region_id, region_code, name
|
||||
FROM regions
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY sort_order ASC, name ASC`, appCode)
|
||||
ORDER BY sort_order DESC, name ASC`, appCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ func (s *Service) List(ctx context.Context, appCode string) ([]itemDTO, error) {
|
||||
status, created_at_ms, updated_at_ms
|
||||
FROM point_withdrawal_coin_seller_configs
|
||||
WHERE app_code = ?
|
||||
ORDER BY sort_order ASC, seller_user_id ASC`, normalizeAppCode(appCode))
|
||||
ORDER BY sort_order DESC, seller_user_id ASC`, normalizeAppCode(appCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -404,7 +404,7 @@ func (s *Service) resolveRegionScope(ctx context.Context, appCode string, scope
|
||||
SELECT region_id
|
||||
FROM regions
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY sort_order ASC, region_id ASC`, appCode)
|
||||
ORDER BY sort_order DESC, region_id ASC`, appCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -396,7 +396,7 @@ func resolveCountryCode(ctx context.Context, querier countryCodeQuerier, appCode
|
||||
OR UPPER(country_code) = ?
|
||||
OR UPPER(COALESCE(iso_alpha3, '')) = ?
|
||||
)
|
||||
ORDER BY enabled DESC, sort_order ASC, country_id ASC
|
||||
ORDER BY enabled DESC, sort_order DESC, country_id ASC
|
||||
LIMIT 1
|
||||
`, appCode, keyword, keyword, upperKeyword, upperKeyword).Scan(&countryCode)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
||||
@ -244,7 +244,8 @@ func (r cycleRequest) toProto() *activityv1.WeeklyStarCycle {
|
||||
}
|
||||
} else {
|
||||
for index, giftID := range r.GiftIDs {
|
||||
gifts = append(gifts, &activityv1.WeeklyStarGift{GiftId: strings.TrimSpace(giftID), SortOrder: int32(index + 1)})
|
||||
// 兼容旧 gift_ids 请求:数组首项是页面首项,因此在大值优先语义下分配最高默认权重。
|
||||
gifts = append(gifts, &activityv1.WeeklyStarGift{GiftId: strings.TrimSpace(giftID), SortOrder: int32(len(r.GiftIDs) - index)})
|
||||
}
|
||||
}
|
||||
rewards := make([]*activityv1.WeeklyStarReward, 0, 3)
|
||||
|
||||
@ -38,7 +38,7 @@ func (s *Store) ListAppBanners(options AppBannerListOptions) ([]model.AppBanner,
|
||||
like := "%" + strings.TrimSpace(options.Keyword) + "%"
|
||||
query = query.Where("(description LIKE ? OR param LIKE ? OR country_code LIKE ? OR display_scope LIKE ? OR room_small_image_url LIKE ?)", like, like, like, like, like)
|
||||
}
|
||||
err := query.Order("sort_order ASC, id DESC").Find(&items).Error
|
||||
err := query.Order("sort_order DESC, id DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
|
||||
@ -277,7 +277,7 @@ func (s *Store) ListAppExploreTabs(options AppExploreTabListOptions) ([]model.Ap
|
||||
}
|
||||
|
||||
var items []model.AppExploreTab
|
||||
err := db.Order("sort_order ASC, id DESC").Find(&items).Error
|
||||
err := db.Order("sort_order DESC, id DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ func (s *Store) ListAppPopups(options AppPopupListOptions) ([]model.AppPopup, er
|
||||
like := "%" + strings.TrimSpace(options.Keyword) + "%"
|
||||
query = query.Where("(code LIKE ? OR name LIKE ? OR jump_url LIKE ? OR image_url LIKE ?)", like, like, like, like)
|
||||
}
|
||||
err := query.Order("sort_order ASC, id DESC").Find(&items).Error
|
||||
err := query.Order("sort_order DESC, id DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ func (s *Store) ListAppSplashScreens(options AppSplashScreenListOptions) ([]mode
|
||||
// 关键字只查人工可识别的字段,避免把封面 URL 这类长字段带进模糊查询拖慢列表。
|
||||
query = query.Where("(description LIKE ? OR param LIKE ? OR country_code LIKE ?)", like, like, like)
|
||||
}
|
||||
err := query.Order("sort_order ASC, id DESC").Find(&items).Error
|
||||
err := query.Order("sort_order DESC, id DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
|
||||
func (s *Store) MenusForPermissions(permissionCodes []string) ([]model.Menu, error) {
|
||||
var menus []model.Menu
|
||||
query := s.db.Where("visible = ?", true).Order("sort ASC, id ASC")
|
||||
query := s.db.Where("visible = ?", true).Order("sort DESC, id ASC")
|
||||
if len(permissionCodes) > 0 {
|
||||
query = query.Where("permission_code = '' OR permission_code IN ?", permissionCodes)
|
||||
} else {
|
||||
@ -23,7 +23,7 @@ func (s *Store) MenusForPermissions(permissionCodes []string) ([]model.Menu, err
|
||||
|
||||
func (s *Store) ListMenus() ([]model.Menu, error) {
|
||||
var menus []model.Menu
|
||||
err := s.db.Order("sort ASC, id ASC").Find(&menus).Error
|
||||
err := s.db.Order("sort DESC, id ASC").Find(&menus).Error
|
||||
return menus, err
|
||||
}
|
||||
|
||||
|
||||
@ -23,10 +23,10 @@ func (s *Store) ListTeams(keyword string) ([]model.Team, error) {
|
||||
var teams []model.Team
|
||||
err := query.
|
||||
Group("admin_teams.id, admin_teams.parent_id, admin_teams.name, admin_teams.description, admin_teams.sort, admin_teams.created_at_ms, admin_teams.updated_at_ms, parent_team.name").
|
||||
Order("CASE WHEN admin_teams.parent_id IS NULL THEN admin_teams.sort ELSE COALESCE(parent_team.sort, admin_teams.sort) END ASC").
|
||||
Order("CASE WHEN admin_teams.parent_id IS NULL THEN admin_teams.sort ELSE COALESCE(parent_team.sort, admin_teams.sort) END DESC").
|
||||
Order("COALESCE(admin_teams.parent_id, admin_teams.id) ASC").
|
||||
Order("CASE WHEN admin_teams.parent_id IS NULL THEN 0 ELSE 1 END ASC").
|
||||
Order("admin_teams.sort ASC, admin_teams.id ASC").
|
||||
Order("admin_teams.sort DESC, admin_teams.id ASC").
|
||||
Scan(&teams).Error
|
||||
return teams, err
|
||||
}
|
||||
@ -96,7 +96,7 @@ func (s *Store) TeamAndChildIDs(id uint) ([]uint, error) {
|
||||
}
|
||||
ids := []uint{id}
|
||||
var childIDs []uint
|
||||
if err := s.db.Model(&model.Team{}).Where("parent_id = ?", id).Order("sort ASC, id ASC").Pluck("id", &childIDs).Error; err != nil {
|
||||
if err := s.db.Model(&model.Team{}).Where("parent_id = ?", id).Order("sort DESC, id ASC").Pluck("id", &childIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, childIDs...)
|
||||
|
||||
@ -53,7 +53,7 @@ func TestTeamAndChildIDsReturnsParentAndChildren(t *testing.T) {
|
||||
store, mock, closeStore := newRepositorySQLMock(t)
|
||||
defer closeStore()
|
||||
|
||||
mock.ExpectQuery("SELECT `id` FROM `admin_teams` WHERE parent_id = \\? ORDER BY sort ASC, id ASC").
|
||||
mock.ExpectQuery("SELECT `id` FROM `admin_teams` WHERE parent_id = \\? ORDER BY sort DESC, id ASC").
|
||||
WithArgs(model.TeamOperationsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(11).AddRow(12))
|
||||
|
||||
|
||||
@ -225,8 +225,14 @@ CREATE TABLE IF NOT EXISTS task_definitions (
|
||||
target_unit VARCHAR(32) NOT NULL COMMENT '目标单位',
|
||||
reward_coin_amount BIGINT NOT NULL COMMENT '奖励金币数量',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型:COIN/POINT',
|
||||
reward_type VARCHAR(32) NOT NULL DEFAULT 'wallet_asset' COMMENT '奖励类型:wallet_asset/resource/resource_group',
|
||||
reward_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励 ID',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源组奖励 ID',
|
||||
reward_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励数量',
|
||||
reward_duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励有效期毫秒,0 为永久',
|
||||
reward_items_json JSON NULL COMMENT '钱包资源目录解析出的奖励展示快照',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
version BIGINT NOT NULL COMMENT '版本号',
|
||||
current_task_version_id BIGINT NOT NULL DEFAULT 0 COMMENT '当前任务版本 ID',
|
||||
effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间,UTC epoch ms',
|
||||
@ -262,6 +268,12 @@ CREATE TABLE IF NOT EXISTS user_task_progress (
|
||||
target_value BIGINT NOT NULL COMMENT '目标值',
|
||||
reward_coin_amount BIGINT NOT NULL COMMENT '奖励金币数量',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT',
|
||||
reward_type VARCHAR(32) NOT NULL DEFAULT 'wallet_asset' COMMENT '奖励类型快照',
|
||||
reward_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励 ID 快照',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源组奖励 ID 快照',
|
||||
reward_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励数量快照',
|
||||
reward_duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励有效期快照',
|
||||
reward_items_json JSON NULL COMMENT '奖励展示快照',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
completed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '完成时间,UTC epoch ms',
|
||||
claimed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '领取时间,UTC epoch ms',
|
||||
@ -300,8 +312,15 @@ CREATE TABLE IF NOT EXISTS task_reward_claims (
|
||||
cycle_key VARCHAR(32) NOT NULL COMMENT '周期键',
|
||||
reward_coin_amount BIGINT NOT NULL COMMENT '奖励金币数量',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT',
|
||||
reward_type VARCHAR(32) NOT NULL DEFAULT 'wallet_asset' COMMENT '奖励类型快照',
|
||||
reward_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励 ID 快照',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源组奖励 ID 快照',
|
||||
reward_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励数量快照',
|
||||
reward_duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励有效期快照',
|
||||
reward_items_json JSON NULL COMMENT '奖励展示快照',
|
||||
wallet_command_id VARCHAR(128) NOT NULL COMMENT '钱包命令 ID',
|
||||
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||
wallet_resource_grant_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包资源赠送 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
failure_reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
@ -381,7 +400,7 @@ CREATE TABLE IF NOT EXISTS first_recharge_reward_tiers (
|
||||
discount_percent INT NOT NULL DEFAULT 0 COMMENT '展示优惠百分比,不参与结算',
|
||||
resource_group_id BIGINT NOT NULL COMMENT '奖励资源分组 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
UNIQUE KEY uk_first_recharge_reward_tier_code (app_code, tier_code),
|
||||
@ -438,7 +457,7 @@ CREATE TABLE IF NOT EXISTS cumulative_recharge_reward_tiers (
|
||||
threshold_usd_minor BIGINT NOT NULL COMMENT '达标美元金额,美分',
|
||||
resource_group_id BIGINT NOT NULL COMMENT '奖励资源分组 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
UNIQUE KEY uk_cumulative_recharge_reward_tier_code (app_code, tier_code),
|
||||
@ -583,7 +602,7 @@ CREATE TABLE IF NOT EXISTS growth_level_tracks (
|
||||
track VARCHAR(32) NOT NULL COMMENT '赛道',
|
||||
name VARCHAR(96) NOT NULL COMMENT '名称',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
display_config_json JSON NULL COMMENT '展示配置 JSON 配置或快照',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
@ -599,7 +618,7 @@ CREATE TABLE IF NOT EXISTS growth_level_rules (
|
||||
name VARCHAR(96) NOT NULL COMMENT '名称',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '奖励资源分组 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
display_config_json JSON NULL COMMENT '展示配置 JSON 配置或快照',
|
||||
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建管理员 ID',
|
||||
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新管理员 ID',
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 三张任务热表只在末尾追加定长字段和一个可空 JSON,MySQL 8 可使用 INSTANT/LOCK=NONE,
|
||||
-- 避免迁移期间复制整表或阻塞任务事件、列表和领奖写入。
|
||||
ALTER TABLE task_definitions
|
||||
ADD COLUMN reward_type VARCHAR(32) NOT NULL DEFAULT 'wallet_asset' COMMENT '奖励类型:wallet_asset/resource/resource_group',
|
||||
ADD COLUMN reward_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励 ID',
|
||||
ADD COLUMN reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源组奖励 ID',
|
||||
ADD COLUMN reward_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励数量',
|
||||
ADD COLUMN reward_duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励有效期毫秒,0 为永久',
|
||||
ADD COLUMN reward_items_json JSON NULL COMMENT '钱包资源目录解析出的奖励展示快照',
|
||||
ALGORITHM=INSTANT,
|
||||
LOCK=NONE;
|
||||
|
||||
ALTER TABLE user_task_progress
|
||||
ADD COLUMN reward_type VARCHAR(32) NOT NULL DEFAULT 'wallet_asset' COMMENT '奖励类型快照',
|
||||
ADD COLUMN reward_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励 ID 快照',
|
||||
ADD COLUMN reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源组奖励 ID 快照',
|
||||
ADD COLUMN reward_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励数量快照',
|
||||
ADD COLUMN reward_duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励有效期快照',
|
||||
ADD COLUMN reward_items_json JSON NULL COMMENT '奖励展示快照',
|
||||
ALGORITHM=INSTANT,
|
||||
LOCK=NONE;
|
||||
|
||||
ALTER TABLE task_reward_claims
|
||||
ADD COLUMN reward_type VARCHAR(32) NOT NULL DEFAULT 'wallet_asset' COMMENT '奖励类型快照',
|
||||
ADD COLUMN reward_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励 ID 快照',
|
||||
ADD COLUMN reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源组奖励 ID 快照',
|
||||
ADD COLUMN reward_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励数量快照',
|
||||
ADD COLUMN reward_duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '单资源奖励有效期快照',
|
||||
ADD COLUMN reward_items_json JSON NULL COMMENT '奖励展示快照',
|
||||
ADD COLUMN wallet_resource_grant_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包资源赠送 ID',
|
||||
ALGORITHM=INSTANT,
|
||||
LOCK=NONE;
|
||||
@ -55,8 +55,27 @@ const (
|
||||
|
||||
RewardAssetCoin = "COIN"
|
||||
RewardAssetPoint = "POINT"
|
||||
|
||||
RewardTypeWalletAsset = "wallet_asset"
|
||||
RewardTypeResource = "resource"
|
||||
RewardTypeResourceGroup = "resource_group"
|
||||
|
||||
RewardItemTypeResource = "resource"
|
||||
RewardItemTypeWalletAsset = "wallet_asset"
|
||||
)
|
||||
|
||||
// RewardItem 是任务定义在保存时固化的奖励展示快照;H5 只依赖封面和数量,不从名称反推资源语义。
|
||||
type RewardItem struct {
|
||||
ItemType string `json:"item_type"`
|
||||
ResourceID int64 `json:"resource_id,omitempty"`
|
||||
ResourceType string `json:"resource_type,omitempty"`
|
||||
CoverURL string `json:"cover_url,omitempty"`
|
||||
Quantity int64 `json:"quantity,omitempty"`
|
||||
DurationMS int64 `json:"duration_ms,omitempty"`
|
||||
WalletAssetType string `json:"wallet_asset_type,omitempty"`
|
||||
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
|
||||
}
|
||||
|
||||
// Definition 是 task_definitions 的当前版本读模型;App 查询只读这张表和用户进度表。
|
||||
type Definition struct {
|
||||
AppCode string
|
||||
@ -75,8 +94,14 @@ type Definition struct {
|
||||
DimensionFilterJSON string
|
||||
TargetValue int64
|
||||
TargetUnit string
|
||||
RewardType string
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
RewardResourceID int64
|
||||
RewardResourceGroupID int64
|
||||
RewardQuantity int64
|
||||
RewardDurationMS int64
|
||||
RewardItemsJSON string
|
||||
Status string
|
||||
SortOrder int32
|
||||
Version int64
|
||||
@ -98,8 +123,14 @@ type Progress struct {
|
||||
CycleKey string
|
||||
ProgressValue int64
|
||||
TargetValue int64
|
||||
RewardType string
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
RewardResourceID int64
|
||||
RewardResourceGroupID int64
|
||||
RewardQuantity int64
|
||||
RewardDurationMS int64
|
||||
RewardItemsJSON string
|
||||
Status string
|
||||
CompletedAtMS int64
|
||||
ClaimedAtMS int64
|
||||
@ -159,8 +190,14 @@ type DefinitionCommand struct {
|
||||
DimensionFilterJSON string
|
||||
TargetValue int64
|
||||
TargetUnit string
|
||||
RewardType string
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
RewardResourceID int64
|
||||
RewardResourceGroupID int64
|
||||
RewardQuantity int64
|
||||
RewardDurationMS int64
|
||||
RewardItemsJSON string
|
||||
Status string
|
||||
SortOrder int32
|
||||
EffectiveFromMS int64
|
||||
@ -204,10 +241,17 @@ type Claim struct {
|
||||
TaskID string
|
||||
TaskType string
|
||||
CycleKey string
|
||||
RewardType string
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
RewardResourceID int64
|
||||
RewardResourceGroupID int64
|
||||
RewardQuantity int64
|
||||
RewardDurationMS int64
|
||||
RewardItemsJSON string
|
||||
WalletCommandID string
|
||||
WalletTransactionID string
|
||||
WalletResourceGrantID string
|
||||
Status string
|
||||
FailureReason string
|
||||
CreatedAtMS int64
|
||||
|
||||
@ -656,7 +656,7 @@ func normalizeTemplate(item domain.Template) domain.Template {
|
||||
if item.Gifts[i].SortOrder == item.Gifts[j].SortOrder {
|
||||
return item.Gifts[i].GiftID < item.Gifts[j].GiftID
|
||||
}
|
||||
return item.Gifts[i].SortOrder < item.Gifts[j].SortOrder
|
||||
return item.Gifts[i].SortOrder > item.Gifts[j].SortOrder
|
||||
})
|
||||
for index := range item.Tasks {
|
||||
item.Tasks[index].TaskKey = strings.ToLower(strings.TrimSpace(item.Tasks[index].TaskKey))
|
||||
@ -666,7 +666,7 @@ func normalizeTemplate(item domain.Template) domain.Template {
|
||||
if item.Tasks[i].SortOrder == item.Tasks[j].SortOrder {
|
||||
return item.Tasks[i].TaskKey < item.Tasks[j].TaskKey
|
||||
}
|
||||
return item.Tasks[i].SortOrder < item.Tasks[j].SortOrder
|
||||
return item.Tasks[i].SortOrder > item.Tasks[j].SortOrder
|
||||
})
|
||||
for index := range item.RankRewards {
|
||||
item.RankRewards[index].BoardType = strings.ToLower(strings.TrimSpace(item.RankRewards[index].BoardType))
|
||||
@ -696,7 +696,7 @@ func normalizeTemplate(item domain.Template) domain.Template {
|
||||
if item.Assets[i].Locale != item.Assets[j].Locale {
|
||||
return item.Assets[i].Locale < item.Assets[j].Locale
|
||||
}
|
||||
return item.Assets[i].SortOrder < item.Assets[j].SortOrder
|
||||
return item.Assets[i].SortOrder > item.Assets[j].SortOrder
|
||||
})
|
||||
return item
|
||||
}
|
||||
|
||||
@ -249,7 +249,7 @@ func normalizeTiers(tiers []domain.Tier) []domain.Tier {
|
||||
if items[i].SortOrder == items[j].SortOrder {
|
||||
return items[i].ThresholdUSDMinor < items[j].ThresholdUSDMinor
|
||||
}
|
||||
return items[i].SortOrder < items[j].SortOrder
|
||||
return items[i].SortOrder > items[j].SortOrder
|
||||
})
|
||||
return items
|
||||
}
|
||||
@ -350,7 +350,7 @@ func activeTiers(tiers []domain.Tier) []domain.Tier {
|
||||
if items[i].SortOrder == items[j].SortOrder {
|
||||
return items[i].ThresholdUSDMinor < items[j].ThresholdUSDMinor
|
||||
}
|
||||
return items[i].SortOrder < items[j].SortOrder
|
||||
return items[i].SortOrder > items[j].SortOrder
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
@ -219,7 +219,7 @@ func normalizeTiers(tiers []domain.Tier) []domain.Tier {
|
||||
if items[i].SortOrder == items[j].SortOrder {
|
||||
return items[i].USDMinorAmount < items[j].USDMinorAmount
|
||||
}
|
||||
return items[i].SortOrder < items[j].SortOrder
|
||||
return items[i].SortOrder > items[j].SortOrder
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
@ -364,7 +364,7 @@ func normalizeTiers(tiers []domain.Tier) []domain.Tier {
|
||||
if items[i].SortOrder == items[j].SortOrder {
|
||||
return thresholdValue(items[i]) < thresholdValue(items[j])
|
||||
}
|
||||
return items[i].SortOrder < items[j].SortOrder
|
||||
return items[i].SortOrder > items[j].SortOrder
|
||||
}
|
||||
return items[i].RewardType < items[j].RewardType
|
||||
})
|
||||
|
||||
@ -525,7 +525,7 @@ func sortTiersForDisplay(tiers []domain.Tier) []domain.Tier {
|
||||
if items[i].SortOrder == items[j].SortOrder {
|
||||
return items[i].ThresholdCoinSpent < items[j].ThresholdCoinSpent
|
||||
}
|
||||
return items[i].SortOrder < items[j].SortOrder
|
||||
return items[i].SortOrder > items[j].SortOrder
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ type Repository interface {
|
||||
UpsertTaskDefinition(ctx context.Context, command taskdomain.DefinitionCommand, nowMS int64) (taskdomain.Definition, bool, error)
|
||||
SetTaskDefinitionStatus(ctx context.Context, taskID string, status string, operatorAdminID int64, nowMS int64) (taskdomain.Definition, error)
|
||||
PrepareTaskClaim(ctx context.Context, command taskdomain.ClaimCommand, nowMS int64) (taskdomain.Claim, error)
|
||||
MarkTaskClaimGranted(ctx context.Context, claimID string, walletTransactionID string, grantedAtMS int64) (taskdomain.Claim, error)
|
||||
MarkTaskClaimGranted(ctx context.Context, claimID string, walletTransactionID string, walletResourceGrantID string, grantedAtMS int64) (taskdomain.Claim, error)
|
||||
MarkTaskClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error
|
||||
}
|
||||
|
||||
@ -34,16 +34,29 @@ type WalletClient interface {
|
||||
CreditTaskReward(ctx context.Context, req *walletv1.CreditTaskRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error)
|
||||
}
|
||||
|
||||
// ResourceWalletClient 单独声明资源目录和赠送能力,保留只测试金币链路的轻量 fake。
|
||||
// 生产 wallet gRPC client 同时实现两个接口;资源任务在缺少该能力时明确失败,不能退化为金币入账。
|
||||
type ResourceWalletClient interface {
|
||||
GetResource(ctx context.Context, req *walletv1.GetResourceRequest, opts ...grpc.CallOption) (*walletv1.GetResourceResponse, error)
|
||||
GetResourceGroup(ctx context.Context, req *walletv1.GetResourceGroupRequest, opts ...grpc.CallOption) (*walletv1.GetResourceGroupResponse, error)
|
||||
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
|
||||
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
|
||||
}
|
||||
|
||||
// Service 承载任务查询、事件消费、领奖和后台配置用例。
|
||||
type Service struct {
|
||||
repository Repository
|
||||
wallet WalletClient
|
||||
resourceWallet ResourceWalletClient
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// New 创建任务服务;daily 周期固定按 UTC 自然日切分,不读取机器或客户端时区。
|
||||
func New(repository Repository, wallet WalletClient) *Service {
|
||||
return &Service{repository: repository, wallet: wallet, now: time.Now}
|
||||
service := &Service{repository: repository, wallet: wallet, now: time.Now}
|
||||
// 生成的 WalletServiceClient 具备完整资源能力;拆分类型断言只为让纯资产测试和降级配置保持最小依赖。
|
||||
service.resourceWallet, _ = wallet.(ResourceWalletClient)
|
||||
return service
|
||||
}
|
||||
|
||||
// SetClock 给测试注入稳定时间;生产路径始终使用 time.Now。
|
||||
@ -135,14 +148,28 @@ func (s *Service) ClaimTaskReward(ctx context.Context, command taskdomain.ClaimC
|
||||
if claim.Status == taskdomain.ClaimStatusGranted {
|
||||
return claim, nil
|
||||
}
|
||||
if s.wallet == nil {
|
||||
return taskdomain.Claim{}, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
}
|
||||
|
||||
reason := "daily_task_reward"
|
||||
if command.TaskType == taskdomain.TypeExclusive {
|
||||
reason = "exclusive_task_reward"
|
||||
}
|
||||
walletTransactionID, walletResourceGrantID, grantedAtMS, err := s.grantClaimReward(ctx, claim, reason)
|
||||
if err != nil {
|
||||
// 钱包资产或资源发放失败时都不能把任务进度改成 claimed;同一 command_id 重试会复用 claim 和 wallet_command_id。
|
||||
_ = s.repository.MarkTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UnixMilli())
|
||||
return taskdomain.Claim{}, err
|
||||
}
|
||||
if grantedAtMS <= 0 {
|
||||
grantedAtMS = s.now().UnixMilli()
|
||||
}
|
||||
return s.repository.MarkTaskClaimGranted(ctx, claim.ClaimID, walletTransactionID, walletResourceGrantID, grantedAtMS)
|
||||
}
|
||||
|
||||
func (s *Service) grantClaimReward(ctx context.Context, claim taskdomain.Claim, reason string) (string, string, int64, error) {
|
||||
switch normalizeRewardType(claim.RewardType) {
|
||||
case taskdomain.RewardTypeWalletAsset:
|
||||
if s.wallet == nil {
|
||||
return "", "", 0, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
}
|
||||
resp, err := s.wallet.CreditTaskReward(ctx, &walletv1.CreditTaskRewardRequest{
|
||||
CommandId: claim.WalletCommandID,
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
@ -155,15 +182,60 @@ func (s *Service) ClaimTaskReward(ctx context.Context, command taskdomain.ClaimC
|
||||
Reason: reason,
|
||||
})
|
||||
if err != nil {
|
||||
// 钱包失败时不能把任务进度改成 claimed;同一 command_id 重试会复用 claim 和 wallet_command_id。
|
||||
_ = s.repository.MarkTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UnixMilli())
|
||||
return taskdomain.Claim{}, err
|
||||
return "", "", 0, err
|
||||
}
|
||||
grantedAtMS := resp.GetGrantedAtMs()
|
||||
return resp.GetTransactionId(), "", resp.GetGrantedAtMs(), nil
|
||||
case taskdomain.RewardTypeResource:
|
||||
if s.resourceWallet == nil {
|
||||
return "", "", 0, xerr.New(xerr.Unavailable, "wallet resource client is not configured")
|
||||
}
|
||||
resp, err := s.resourceWallet.GrantResource(ctx, &walletv1.GrantResourceRequest{
|
||||
CommandId: claim.WalletCommandID,
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
TargetUserId: claim.UserID,
|
||||
ResourceId: claim.RewardResourceID,
|
||||
Quantity: claim.RewardQuantity,
|
||||
DurationMs: claim.RewardDurationMS,
|
||||
Reason: reason,
|
||||
OperatorUserId: claim.UserID,
|
||||
GrantSource: "daily_task",
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
return resourceGrantReceipt(resp)
|
||||
case taskdomain.RewardTypeResourceGroup:
|
||||
if s.resourceWallet == nil {
|
||||
return "", "", 0, xerr.New(xerr.Unavailable, "wallet resource client is not configured")
|
||||
}
|
||||
resp, err := s.resourceWallet.GrantResourceGroup(ctx, &walletv1.GrantResourceGroupRequest{
|
||||
CommandId: claim.WalletCommandID,
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
TargetUserId: claim.UserID,
|
||||
GroupId: claim.RewardResourceGroupID,
|
||||
Reason: reason,
|
||||
OperatorUserId: claim.UserID,
|
||||
GrantSource: "daily_task",
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
return resourceGrantReceipt(resp)
|
||||
default:
|
||||
return "", "", 0, xerr.New(xerr.InvalidArgument, "task reward_type is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func resourceGrantReceipt(resp *walletv1.ResourceGrantResponse) (string, string, int64, error) {
|
||||
if resp == nil || resp.GetGrant() == nil || strings.TrimSpace(resp.GetGrant().GetGrantId()) == "" {
|
||||
return "", "", 0, xerr.New(xerr.Unavailable, "wallet resource grant receipt is empty")
|
||||
}
|
||||
grant := resp.GetGrant()
|
||||
grantedAtMS := grant.GetUpdatedAtMs()
|
||||
if grantedAtMS <= 0 {
|
||||
grantedAtMS = s.now().UnixMilli()
|
||||
grantedAtMS = grant.GetCreatedAtMs()
|
||||
}
|
||||
return s.repository.MarkTaskClaimGranted(ctx, claim.ClaimID, resp.GetTransactionId(), grantedAtMS)
|
||||
return "", grant.GetGrantId(), grantedAtMS, nil
|
||||
}
|
||||
|
||||
// ConsumeTaskEvent 把服务端事实事件归属到 occurred_at_ms 所在的任务周期。
|
||||
@ -312,14 +384,15 @@ func (s *Service) UpsertTaskDefinition(ctx context.Context, command taskdomain.D
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return taskdomain.Definition{}, false, err
|
||||
}
|
||||
// 奖励资产属于任务定义本身;必须由调用方显式携带,避免旧客户端编辑 POINT 任务时用空字段静默覆盖存量资产。
|
||||
if strings.TrimSpace(command.RewardAssetType) == "" {
|
||||
return taskdomain.Definition{}, false, xerr.New(xerr.InvalidArgument, "reward_asset_type is required")
|
||||
}
|
||||
command = normalizeDefinitionCommand(command)
|
||||
if err := validateDefinitionCommand(ctx, command); err != nil {
|
||||
return taskdomain.Definition{}, false, err
|
||||
}
|
||||
var err error
|
||||
command, err = s.resolveRewardItems(ctx, command)
|
||||
if err != nil {
|
||||
return taskdomain.Definition{}, false, err
|
||||
}
|
||||
return s.repository.UpsertTaskDefinition(ctx, command, s.now().UnixMilli())
|
||||
}
|
||||
|
||||
@ -350,13 +423,22 @@ func (s *Service) itemFromDefinition(def taskdomain.Definition, progress taskdom
|
||||
status = progress.Status
|
||||
progressValue = progress.ProgressValue
|
||||
def.TargetValue = progress.TargetValue
|
||||
def.RewardType = normalizeRewardType(progress.RewardType)
|
||||
def.RewardCoinAmount = progress.RewardCoinAmount
|
||||
def.RewardAssetType = normalizeRewardAssetType(progress.RewardAssetType)
|
||||
def.RewardResourceID = progress.RewardResourceID
|
||||
def.RewardResourceGroupID = progress.RewardResourceGroupID
|
||||
def.RewardQuantity = progress.RewardQuantity
|
||||
def.RewardDurationMS = progress.RewardDurationMS
|
||||
def.RewardItemsJSON = progress.RewardItemsJSON
|
||||
}
|
||||
if status == "" {
|
||||
status = taskdomain.StatusInProgress
|
||||
}
|
||||
def.RewardType = normalizeRewardType(def.RewardType)
|
||||
if def.RewardType == taskdomain.RewardTypeWalletAsset {
|
||||
def.RewardAssetType = normalizeRewardAssetType(def.RewardAssetType)
|
||||
}
|
||||
return taskdomain.Item{
|
||||
Definition: def,
|
||||
ProgressValue: progressValue,
|
||||
@ -392,7 +474,7 @@ func sortTaskItems(items []taskdomain.Item) {
|
||||
return leftRank < rightRank
|
||||
}
|
||||
if items[i].SortOrder != items[j].SortOrder {
|
||||
return items[i].SortOrder < items[j].SortOrder
|
||||
return items[i].SortOrder > items[j].SortOrder
|
||||
}
|
||||
return items[i].TaskID < items[j].TaskID
|
||||
})
|
||||
@ -429,6 +511,7 @@ func normalizeDefinitionCommand(command taskdomain.DefinitionCommand) taskdomain
|
||||
command.ActionPayloadJSON = normalizeJSONForCommand(command.ActionPayloadJSON)
|
||||
command.DimensionFilterJSON = normalizeJSONForCommand(command.DimensionFilterJSON)
|
||||
command.TargetUnit = strings.ToLower(strings.TrimSpace(command.TargetUnit))
|
||||
command.RewardType = normalizeRewardType(command.RewardType)
|
||||
command.RewardAssetType = normalizeRewardAssetType(command.RewardAssetType)
|
||||
command.Status = strings.TrimSpace(command.Status)
|
||||
if command.Status == "" {
|
||||
@ -470,12 +553,29 @@ func validateDefinitionCommand(ctx context.Context, command taskdomain.Definitio
|
||||
if _, err := normalizeObjectJSON(command.DimensionFilterJSON); err != nil {
|
||||
return xerr.New(xerr.InvalidArgument, "dimension_filter_json is invalid")
|
||||
}
|
||||
if command.TargetValue <= 0 || command.RewardCoinAmount <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "target_value and reward_coin_amount must be positive")
|
||||
if command.TargetValue <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "target_value must be positive")
|
||||
}
|
||||
switch command.RewardType {
|
||||
case taskdomain.RewardTypeWalletAsset:
|
||||
// 旧 Admin 请求没有 reward_type 时归一为 wallet_asset;这条分支仍要求显式资产,避免编辑 POINT 任务时空值覆盖。
|
||||
if strings.TrimSpace(command.RewardAssetType) == "" || command.RewardCoinAmount <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "reward_asset_type and reward_coin_amount are required")
|
||||
}
|
||||
if !validRewardAssetType(command.RewardAssetType) {
|
||||
return xerr.New(xerr.InvalidArgument, "reward_asset_type is invalid")
|
||||
}
|
||||
case taskdomain.RewardTypeResource:
|
||||
if command.RewardResourceID <= 0 || command.RewardQuantity <= 0 || command.RewardDurationMS < 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "resource reward is incomplete")
|
||||
}
|
||||
case taskdomain.RewardTypeResourceGroup:
|
||||
if command.RewardResourceGroupID <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "reward_resource_group_id is required")
|
||||
}
|
||||
default:
|
||||
return xerr.New(xerr.InvalidArgument, "reward_type is invalid")
|
||||
}
|
||||
if !validTargetUnit(command.TargetUnit) {
|
||||
return xerr.New(xerr.InvalidArgument, "target_unit is invalid")
|
||||
}
|
||||
@ -493,8 +593,14 @@ func validateDefinitionCommand(ctx context.Context, command taskdomain.Definitio
|
||||
|
||||
func normalizeRewardAssetType(value string) string {
|
||||
value = strings.ToUpper(strings.TrimSpace(value))
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeRewardType(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
if value == "" {
|
||||
return taskdomain.RewardAssetCoin
|
||||
// 没有 reward_type 的历史请求和历史数据都属于原有 COIN/POINT 钱包资产奖励。
|
||||
return taskdomain.RewardTypeWalletAsset
|
||||
}
|
||||
return value
|
||||
}
|
||||
@ -509,6 +615,130 @@ func validRewardAssetType(value string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) resolveRewardItems(ctx context.Context, command taskdomain.DefinitionCommand) (taskdomain.DefinitionCommand, error) {
|
||||
switch command.RewardType {
|
||||
case taskdomain.RewardTypeWalletAsset:
|
||||
// 非当前奖励类型字段必须清空,避免任务从资源切回金币后旧 ID 继续出现在 Admin/App 响应中。
|
||||
command.RewardResourceID = 0
|
||||
command.RewardResourceGroupID = 0
|
||||
command.RewardQuantity = 0
|
||||
command.RewardDurationMS = 0
|
||||
return withRewardItems(command, []taskdomain.RewardItem{{
|
||||
ItemType: taskdomain.RewardItemTypeWalletAsset,
|
||||
WalletAssetType: command.RewardAssetType,
|
||||
WalletAssetAmount: command.RewardCoinAmount,
|
||||
}})
|
||||
case taskdomain.RewardTypeResource:
|
||||
if s.resourceWallet == nil {
|
||||
return taskdomain.DefinitionCommand{}, xerr.New(xerr.Unavailable, "wallet resource client is not configured")
|
||||
}
|
||||
resp, err := s.resourceWallet.GetResource(ctx, &walletv1.GetResourceRequest{
|
||||
RequestId: rewardCatalogRequestID(command),
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
ResourceId: command.RewardResourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return taskdomain.DefinitionCommand{}, err
|
||||
}
|
||||
item, err := rewardItemFromResource(resp.GetResource(), command.RewardQuantity, command.RewardDurationMS)
|
||||
if err != nil {
|
||||
return taskdomain.DefinitionCommand{}, err
|
||||
}
|
||||
command.RewardCoinAmount = 0
|
||||
command.RewardAssetType = ""
|
||||
command.RewardResourceGroupID = 0
|
||||
return withRewardItems(command, []taskdomain.RewardItem{item})
|
||||
case taskdomain.RewardTypeResourceGroup:
|
||||
if s.resourceWallet == nil {
|
||||
return taskdomain.DefinitionCommand{}, xerr.New(xerr.Unavailable, "wallet resource client is not configured")
|
||||
}
|
||||
resp, err := s.resourceWallet.GetResourceGroup(ctx, &walletv1.GetResourceGroupRequest{
|
||||
RequestId: rewardCatalogRequestID(command),
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
GroupId: command.RewardResourceGroupID,
|
||||
})
|
||||
if err != nil {
|
||||
return taskdomain.DefinitionCommand{}, err
|
||||
}
|
||||
group := resp.GetGroup()
|
||||
if group == nil || group.GetGroupId() != command.RewardResourceGroupID || strings.ToLower(strings.TrimSpace(group.GetStatus())) != "active" {
|
||||
return taskdomain.DefinitionCommand{}, xerr.New(xerr.Conflict, "reward resource group is unavailable")
|
||||
}
|
||||
if len(group.GetItems()) == 0 {
|
||||
return taskdomain.DefinitionCommand{}, xerr.New(xerr.InvalidArgument, "reward resource group has no items")
|
||||
}
|
||||
items := make([]taskdomain.RewardItem, 0, len(group.GetItems()))
|
||||
for _, groupItem := range group.GetItems() {
|
||||
if strings.ToLower(strings.TrimSpace(groupItem.GetItemType())) == taskdomain.RewardItemTypeWalletAsset {
|
||||
if groupItem.GetWalletAssetType() == "" || groupItem.GetWalletAssetAmount() <= 0 {
|
||||
return taskdomain.DefinitionCommand{}, xerr.New(xerr.InvalidArgument, "reward resource group contains an invalid wallet asset")
|
||||
}
|
||||
items = append(items, taskdomain.RewardItem{
|
||||
ItemType: taskdomain.RewardItemTypeWalletAsset,
|
||||
WalletAssetType: strings.ToUpper(strings.TrimSpace(groupItem.GetWalletAssetType())),
|
||||
WalletAssetAmount: groupItem.GetWalletAssetAmount(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
item, itemErr := rewardItemFromResource(groupItem.GetResource(), groupItem.GetQuantity(), groupItem.GetDurationMs())
|
||||
if itemErr != nil {
|
||||
return taskdomain.DefinitionCommand{}, itemErr
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
command.RewardCoinAmount = 0
|
||||
command.RewardAssetType = ""
|
||||
command.RewardResourceID = 0
|
||||
command.RewardQuantity = 0
|
||||
command.RewardDurationMS = 0
|
||||
return withRewardItems(command, items)
|
||||
default:
|
||||
return taskdomain.DefinitionCommand{}, xerr.New(xerr.InvalidArgument, "reward_type is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func rewardItemFromResource(resource *walletv1.Resource, quantity int64, durationMS int64) (taskdomain.RewardItem, error) {
|
||||
if resource == nil || resource.GetResourceId() <= 0 || strings.ToLower(strings.TrimSpace(resource.GetStatus())) != "active" || !resource.GetGrantable() {
|
||||
return taskdomain.RewardItem{}, xerr.New(xerr.Conflict, "reward resource is unavailable")
|
||||
}
|
||||
coverURL := strings.TrimSpace(resource.GetPreviewUrl())
|
||||
if coverURL == "" {
|
||||
coverURL = strings.TrimSpace(resource.GetAssetUrl())
|
||||
}
|
||||
if coverURL == "" {
|
||||
// H5 的奖励区明确只展示封面,不展示名称;没有可渲染封面的资源不能被保存为任务奖励。
|
||||
return taskdomain.RewardItem{}, xerr.New(xerr.InvalidArgument, "reward resource cover is required")
|
||||
}
|
||||
if quantity <= 0 || durationMS < 0 {
|
||||
return taskdomain.RewardItem{}, xerr.New(xerr.InvalidArgument, "reward resource quantity or duration is invalid")
|
||||
}
|
||||
return taskdomain.RewardItem{
|
||||
ItemType: taskdomain.RewardItemTypeResource,
|
||||
ResourceID: resource.GetResourceId(),
|
||||
ResourceType: strings.ToLower(strings.TrimSpace(resource.GetResourceType())),
|
||||
CoverURL: coverURL,
|
||||
Quantity: quantity,
|
||||
DurationMS: durationMS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func withRewardItems(command taskdomain.DefinitionCommand, items []taskdomain.RewardItem) (taskdomain.DefinitionCommand, error) {
|
||||
payload, err := json.Marshal(items)
|
||||
if err != nil {
|
||||
return taskdomain.DefinitionCommand{}, err
|
||||
}
|
||||
command.RewardItemsJSON = string(payload)
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func rewardCatalogRequestID(command taskdomain.DefinitionCommand) string {
|
||||
taskID := strings.TrimSpace(command.TaskID)
|
||||
if taskID == "" {
|
||||
taskID = "new"
|
||||
}
|
||||
return "task-reward-config-" + taskID
|
||||
}
|
||||
|
||||
func roomGiftTaskDimensions(envelope *roomeventsv1.EventEnvelope, gift *roomeventsv1.RoomGiftSent) (string, error) {
|
||||
// 维度只记录任务匹配和排障需要的事实字段;后台 dimension_filter_json 可以按 gift_id、pool_id、room_id 等字段精确限定任务。
|
||||
payload := map[string]any{
|
||||
|
||||
@ -2,6 +2,7 @@ package task_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -405,6 +406,161 @@ func TestExplicitPointTaskDefinitionDoesNotDependOnApplicationName(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupTaskDisplaysEveryCoverAndGrantsGroup(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
wallet := &fakeWalletClient{
|
||||
group: &walletv1.ResourceGroup{
|
||||
GroupId: 9901,
|
||||
Status: "active",
|
||||
Items: []*walletv1.ResourceGroupItem{
|
||||
{
|
||||
ItemType: "resource",
|
||||
ResourceId: 8801,
|
||||
Quantity: 1,
|
||||
Resource: &walletv1.Resource{
|
||||
ResourceId: 8801,
|
||||
ResourceType: "avatar_frame",
|
||||
Status: "active",
|
||||
Grantable: true,
|
||||
PreviewUrl: "https://cdn.example.com/rewards/frame.webp",
|
||||
},
|
||||
},
|
||||
{
|
||||
ItemType: "resource",
|
||||
ResourceId: 8802,
|
||||
Quantity: 2,
|
||||
Resource: &walletv1.Resource{
|
||||
ResourceId: 8802,
|
||||
ResourceType: "badge",
|
||||
Status: "active",
|
||||
Grantable: true,
|
||||
AssetUrl: "https://cdn.example.com/rewards/badge.webp",
|
||||
},
|
||||
},
|
||||
{
|
||||
ItemType: "wallet_asset",
|
||||
WalletAssetType: "COIN",
|
||||
WalletAssetAmount: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := taskservice.New(repository, wallet)
|
||||
now := time.Date(2026, 5, 9, 10, 0, 0, 0, time.UTC)
|
||||
svc.SetClock(func() time.Time { return now })
|
||||
|
||||
definition, created, err := svc.UpsertTaskDefinition(context.Background(), taskdomain.DefinitionCommand{
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
Category: "gift",
|
||||
MetricType: taskdomain.MetricGiftSendCount,
|
||||
Title: "send gift for resource group",
|
||||
TargetValue: 1,
|
||||
TargetUnit: "count",
|
||||
RewardType: taskdomain.RewardTypeResourceGroup,
|
||||
RewardResourceGroupID: 9901,
|
||||
Status: taskdomain.StatusActive,
|
||||
OperatorAdminID: 90001,
|
||||
})
|
||||
if err != nil || !created {
|
||||
t.Fatalf("create resource group task failed: created=%v err=%v", created, err)
|
||||
}
|
||||
var rewardItems []taskdomain.RewardItem
|
||||
if err := json.Unmarshal([]byte(definition.RewardItemsJSON), &rewardItems); err != nil {
|
||||
t.Fatalf("decode reward items failed: %v", err)
|
||||
}
|
||||
if len(rewardItems) != 3 ||
|
||||
rewardItems[0].CoverURL != "https://cdn.example.com/rewards/frame.webp" ||
|
||||
rewardItems[1].CoverURL != "https://cdn.example.com/rewards/badge.webp" ||
|
||||
rewardItems[2].WalletAssetAmount != 50 {
|
||||
t.Fatalf("resource group display snapshot mismatch: %+v", rewardItems)
|
||||
}
|
||||
|
||||
if _, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{
|
||||
EventID: "evt-resource-group-task",
|
||||
EventType: "RoomGiftSent",
|
||||
SourceService: "room-service",
|
||||
UserID: 10001,
|
||||
MetricType: taskdomain.MetricGiftSendCount,
|
||||
Value: 1,
|
||||
OccurredAtMS: now.UnixMilli(),
|
||||
}); err != nil {
|
||||
t.Fatalf("complete resource group task failed: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{
|
||||
UserID: 10001,
|
||||
TaskID: definition.TaskID,
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
CycleKey: "2026-05-09",
|
||||
CommandID: "cmd-resource-group-task",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("claim resource group task failed: %v", err)
|
||||
}
|
||||
if claim.WalletResourceGrantID != "grant-task-1" || wallet.groupGrantCalls != 1 ||
|
||||
wallet.lastGroupGrant.GetGroupId() != 9901 {
|
||||
t.Fatalf("resource group claim mismatch: claim=%+v wallet=%+v", claim, wallet.lastGroupGrant)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleResourceTaskGrantsConfiguredQuantityAndDuration(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
wallet := &fakeWalletClient{resource: &walletv1.Resource{
|
||||
ResourceId: 8701,
|
||||
ResourceType: "vehicle",
|
||||
Status: "active",
|
||||
Grantable: true,
|
||||
PreviewUrl: "https://cdn.example.com/rewards/vehicle.webp",
|
||||
}}
|
||||
svc := taskservice.New(repository, wallet)
|
||||
now := time.Date(2026, 5, 10, 10, 0, 0, 0, time.UTC)
|
||||
svc.SetClock(func() time.Time { return now })
|
||||
const durationMS = int64(7 * 24 * time.Hour / time.Millisecond)
|
||||
|
||||
definition, _, err := svc.UpsertTaskDefinition(context.Background(), taskdomain.DefinitionCommand{
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
Category: "game",
|
||||
MetricType: taskdomain.MetricGameDicePlayCount,
|
||||
Title: "play for vehicle",
|
||||
TargetValue: 1,
|
||||
TargetUnit: "count",
|
||||
RewardType: taskdomain.RewardTypeResource,
|
||||
RewardResourceID: 8701,
|
||||
RewardQuantity: 2,
|
||||
RewardDurationMS: durationMS,
|
||||
Status: taskdomain.StatusActive,
|
||||
OperatorAdminID: 90001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create resource task failed: %v", err)
|
||||
}
|
||||
if _, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{
|
||||
EventID: "evt-single-resource-task",
|
||||
EventType: "GameOrderSettled",
|
||||
SourceService: "game-service",
|
||||
UserID: 10002,
|
||||
MetricType: taskdomain.MetricGameDicePlayCount,
|
||||
Value: 1,
|
||||
OccurredAtMS: now.UnixMilli(),
|
||||
}); err != nil {
|
||||
t.Fatalf("complete resource task failed: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{
|
||||
UserID: 10002,
|
||||
TaskID: definition.TaskID,
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
CycleKey: "2026-05-10",
|
||||
CommandID: "cmd-single-resource-task",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("claim resource task failed: %v", err)
|
||||
}
|
||||
if claim.WalletResourceGrantID != "grant-task-resource-1" || wallet.resourceGrantCalls != 1 ||
|
||||
wallet.lastResourceGrant.GetResourceId() != 8701 || wallet.lastResourceGrant.GetQuantity() != 2 ||
|
||||
wallet.lastResourceGrant.GetDurationMs() != durationMS {
|
||||
t.Fatalf("single resource grant mismatch: claim=%+v request=%+v", claim, wallet.lastResourceGrant)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyRewardAssetUpdateDoesNotOverwriteStoredPoint(t *testing.T) {
|
||||
repository := &fakeTaskDefinitionRepository{storedAsset: taskdomain.RewardAssetPoint}
|
||||
svc := taskservice.New(repository, &fakeWalletClient{})
|
||||
@ -517,6 +673,12 @@ func assertRecordedTaskEvents(t *testing.T, got []taskdomain.Event, want []expec
|
||||
type fakeWalletClient struct {
|
||||
calls int
|
||||
last *walletv1.CreditTaskRewardRequest
|
||||
resource *walletv1.Resource
|
||||
group *walletv1.ResourceGroup
|
||||
resourceGrantCalls int
|
||||
groupGrantCalls int
|
||||
lastResourceGrant *walletv1.GrantResourceRequest
|
||||
lastGroupGrant *walletv1.GrantResourceGroupRequest
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) CreditTaskReward(_ context.Context, req *walletv1.CreditTaskRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) {
|
||||
@ -533,3 +695,31 @@ func (f *fakeWalletClient) CreditTaskReward(_ context.Context, req *walletv1.Cre
|
||||
Balance: &walletv1.AssetBalance{AssetType: assetType, AvailableAmount: req.GetAmount()},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) GetResource(_ context.Context, _ *walletv1.GetResourceRequest, _ ...grpc.CallOption) (*walletv1.GetResourceResponse, error) {
|
||||
return &walletv1.GetResourceResponse{Resource: f.resource}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) GetResourceGroup(_ context.Context, _ *walletv1.GetResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.GetResourceGroupResponse, error) {
|
||||
return &walletv1.GetResourceGroupResponse{Group: f.group}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) GrantResource(_ context.Context, req *walletv1.GrantResourceRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
|
||||
f.resourceGrantCalls++
|
||||
f.lastResourceGrant = req
|
||||
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{
|
||||
GrantId: "grant-task-resource-1",
|
||||
CreatedAtMs: 1778292000000,
|
||||
UpdatedAtMs: 1778292000000,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
|
||||
f.groupGrantCalls++
|
||||
f.lastGroupGrant = req
|
||||
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{
|
||||
GrantId: "grant-task-1",
|
||||
CreatedAtMs: 1778292000000,
|
||||
UpdatedAtMs: 1778292000000,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
@ -306,10 +306,12 @@ func normalizeCycle(cycle domain.Cycle) domain.Cycle {
|
||||
seenGifts[gift.GiftID] = true
|
||||
gifts = append(gifts, gift)
|
||||
}
|
||||
sort.SliceStable(gifts, func(i, j int) bool { return gifts[i].SortOrder < gifts[j].SortOrder })
|
||||
// sort_order 是运营权重而不是数组下标;全后台统一使用大值优先,避免周星礼物与普通礼物列表语义相反。
|
||||
sort.SliceStable(gifts, func(i, j int) bool { return gifts[i].SortOrder > gifts[j].SortOrder })
|
||||
for index := range gifts {
|
||||
if gifts[index].SortOrder <= 0 {
|
||||
gifts[index].SortOrder = int32(index + 1)
|
||||
// 零值兼容旧请求;按当前数组顺序补递减权重,重新读取时不会因 DESC 反转。
|
||||
gifts[index].SortOrder = int32(len(gifts) - index)
|
||||
}
|
||||
}
|
||||
cycle.Gifts = gifts
|
||||
|
||||
@ -526,7 +526,7 @@ func attachActivityTemplateChildren(ctx context.Context, q activityTemplateQueri
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `
|
||||
SELECT gift_id, sort_order, name, icon_url, coin_price, price_version
|
||||
FROM activity_template_gifts WHERE app_code = ? AND template_id = ? ORDER BY sort_order, gift_id`, item.AppCode, item.TemplateID)
|
||||
FROM activity_template_gifts WHERE app_code = ? AND template_id = ? ORDER BY sort_order DESC, gift_id`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -547,7 +547,7 @@ func attachActivityTemplateChildren(ctx context.Context, q activityTemplateQueri
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `
|
||||
SELECT task_key, task_type, target_value, reward_resource_group_id, sort_order
|
||||
FROM activity_template_tasks WHERE app_code = ? AND template_id = ? ORDER BY sort_order, task_key`, item.AppCode, item.TemplateID)
|
||||
FROM activity_template_tasks WHERE app_code = ? AND template_id = ? ORDER BY sort_order DESC, task_key`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -589,7 +589,7 @@ func attachActivityTemplateChildren(ctx context.Context, q activityTemplateQueri
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `
|
||||
SELECT asset_key, locale, url, media_type, sort_order
|
||||
FROM activity_template_assets WHERE app_code = ? AND template_id = ? ORDER BY asset_key, locale, sort_order`, item.AppCode, item.TemplateID)
|
||||
FROM activity_template_assets WHERE app_code = ? AND template_id = ? ORDER BY asset_key, locale, sort_order DESC`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -454,7 +454,7 @@ func (r *Repository) getCumulativeRechargeRewardConfigForUpdate(ctx context.Cont
|
||||
func (r *Repository) listCumulativeRechargeRewardTiers(ctx context.Context, tx *sql.Tx) ([]domain.Tier, error) {
|
||||
query := cumulativeRechargeRewardTierSelectSQL() + `
|
||||
WHERE app_code = ?
|
||||
ORDER BY sort_order ASC, threshold_usd_minor ASC, tier_id ASC`
|
||||
ORDER BY sort_order DESC, threshold_usd_minor ASC, tier_id ASC`
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
if tx != nil {
|
||||
@ -472,7 +472,7 @@ func (r *Repository) listCumulativeRechargeRewardTiers(ctx context.Context, tx *
|
||||
func (r *Repository) listCumulativeRechargeRewardTiersForUpdate(ctx context.Context, tx *sql.Tx) ([]domain.Tier, error) {
|
||||
rows, err := tx.QueryContext(ctx, cumulativeRechargeRewardTierSelectSQL()+`
|
||||
WHERE app_code = ?
|
||||
ORDER BY sort_order ASC, threshold_usd_minor ASC, tier_id ASC
|
||||
ORDER BY sort_order DESC, threshold_usd_minor ASC, tier_id ASC
|
||||
FOR UPDATE`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
|
||||
@ -349,13 +349,13 @@ func (r *Repository) listFirstRechargeRewardTiers(ctx context.Context, tx *sql.T
|
||||
if tx != nil {
|
||||
rows, err = tx.QueryContext(ctx, firstRechargeRewardTierSelectSQL()+`
|
||||
WHERE app_code = ?
|
||||
ORDER BY sort_order ASC, usd_minor_amount ASC, tier_id ASC`,
|
||||
ORDER BY sort_order DESC, usd_minor_amount ASC, tier_id ASC`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
} else {
|
||||
rows, err = r.db.QueryContext(ctx, firstRechargeRewardTierSelectSQL()+`
|
||||
WHERE app_code = ?
|
||||
ORDER BY sort_order ASC, usd_minor_amount ASC, tier_id ASC`,
|
||||
ORDER BY sort_order DESC, usd_minor_amount ASC, tier_id ASC`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
}
|
||||
@ -369,7 +369,7 @@ func (r *Repository) listFirstRechargeRewardTiers(ctx context.Context, tx *sql.T
|
||||
func (r *Repository) listFirstRechargeRewardTiersForUpdate(ctx context.Context, tx *sql.Tx) ([]domain.Tier, error) {
|
||||
rows, err := tx.QueryContext(ctx, firstRechargeRewardTierSelectSQL()+`
|
||||
WHERE app_code = ?
|
||||
ORDER BY sort_order ASC, usd_minor_amount ASC, tier_id ASC
|
||||
ORDER BY sort_order DESC, usd_minor_amount ASC, tier_id ASC
|
||||
FOR UPDATE`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
|
||||
@ -1110,7 +1110,7 @@ func (r *Repository) listActiveLevelTracks(ctx context.Context, q queryer) ([]do
|
||||
SELECT app_code, track, name, status, sort_order, COALESCE(CAST(display_config_json AS CHAR), '{}'), created_at_ms, updated_at_ms
|
||||
FROM growth_level_tracks
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY sort_order ASC, track ASC`,
|
||||
ORDER BY sort_order DESC, track ASC`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
@ -1135,7 +1135,7 @@ func (r *Repository) listLevelTracks(ctx context.Context, q queryer, track strin
|
||||
SELECT app_code, track, name, status, sort_order, COALESCE(CAST(display_config_json AS CHAR), '{}'), created_at_ms, updated_at_ms
|
||||
FROM growth_level_tracks
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY sort_order ASC, track ASC`, args...)
|
||||
ORDER BY sort_order DESC, track ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -740,7 +740,7 @@ func (r *Repository) listInviteActivityRewardTiers(ctx context.Context, tx *sql.
|
||||
}
|
||||
rows, err := queryer.QueryContext(ctx, inviteActivityRewardTierSelectSQL()+`
|
||||
`+where+`
|
||||
ORDER BY reward_type ASC, sort_order ASC, tier_id ASC`, args...)
|
||||
ORDER BY reward_type ASC, sort_order DESC, tier_id ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -483,7 +483,7 @@ func scanRoomTurnoverRewardConfig(scanner roomTurnoverRewardConfigScanner) (doma
|
||||
func (r *Repository) listRoomTurnoverRewardTiers(ctx context.Context, tx *sql.Tx, regionID int64) ([]domain.Tier, error) {
|
||||
query := roomTurnoverRewardTierSelectSQL() + `
|
||||
WHERE app_code = ? AND region_id = ?
|
||||
ORDER BY sort_order ASC, threshold_coin_spent ASC, tier_id ASC`
|
||||
ORDER BY sort_order DESC, threshold_coin_spent ASC, tier_id ASC`
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
if tx != nil {
|
||||
|
||||
@ -26,14 +26,16 @@ func (r *Repository) ListVisibleDefinitions(ctx context.Context, nowMS int64) ([
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, reward_type,
|
||||
reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), status, sort_order, version,
|
||||
current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
|
||||
updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM task_definitions
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
AND (effective_from_ms = 0 OR effective_from_ms <= ?)
|
||||
AND (effective_to_ms = 0 OR effective_to_ms > ?)
|
||||
ORDER BY sort_order ASC, task_id ASC`,
|
||||
ORDER BY sort_order DESC, task_id ASC`,
|
||||
appcode.FromContext(ctx), nowMS, nowMS,
|
||||
)
|
||||
if err != nil {
|
||||
@ -59,7 +61,9 @@ func (r *Repository) ListUserProgress(ctx context.Context, userID int64, cycleKe
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT app_code, user_id, task_id, task_version_id, cycle_key, progress_value,
|
||||
target_value, reward_coin_amount, reward_asset_type, status, completed_at_ms, claimed_at_ms, updated_at_ms
|
||||
target_value, reward_coin_amount, reward_asset_type, reward_type,
|
||||
reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), status, completed_at_ms, claimed_at_ms, updated_at_ms
|
||||
FROM user_task_progress
|
||||
WHERE app_code = ? AND user_id = ? AND cycle_key IN (`+placeholders+`)`, args...)
|
||||
if err != nil {
|
||||
@ -171,11 +175,13 @@ func (r *Repository) ListTaskDefinitions(ctx context.Context, query taskdomain.D
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, reward_type,
|
||||
reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), status, sort_order, version,
|
||||
current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
|
||||
updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM task_definitions `+where+`
|
||||
ORDER BY sort_order ASC, updated_at_ms DESC, task_id ASC
|
||||
ORDER BY sort_order DESC, updated_at_ms DESC, task_id ASC
|
||||
LIMIT ? OFFSET ?`, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@ -229,8 +235,14 @@ func (r *Repository) UpsertTaskDefinition(ctx context.Context, command taskdomai
|
||||
next.DimensionFilterJSON = command.DimensionFilterJSON
|
||||
next.TargetValue = command.TargetValue
|
||||
next.TargetUnit = command.TargetUnit
|
||||
next.RewardType = command.RewardType
|
||||
next.RewardCoinAmount = command.RewardCoinAmount
|
||||
next.RewardAssetType = command.RewardAssetType
|
||||
next.RewardResourceID = command.RewardResourceID
|
||||
next.RewardResourceGroupID = command.RewardResourceGroupID
|
||||
next.RewardQuantity = command.RewardQuantity
|
||||
next.RewardDurationMS = command.RewardDurationMS
|
||||
next.RewardItemsJSON = command.RewardItemsJSON
|
||||
next.Status = command.Status
|
||||
next.SortOrder = command.SortOrder
|
||||
next.EffectiveFromMS = command.EffectiveFromMS
|
||||
@ -250,14 +262,18 @@ func (r *Repository) UpsertTaskDefinition(ctx context.Context, command taskdomai
|
||||
SET task_type = ?, category = ?, metric_type = ?, title = ?, description = ?,
|
||||
audience_type = ?, icon_key = ?, icon_url = ?, action_type = ?, action_param = ?,
|
||||
action_payload_json = CAST(? AS JSON), dimension_filter_json = CAST(? AS JSON),
|
||||
target_value = ?, target_unit = ?, reward_coin_amount = ?, reward_asset_type = ?, status = ?, sort_order = ?,
|
||||
target_value = ?, target_unit = ?, reward_coin_amount = ?, reward_asset_type = ?, reward_type = ?,
|
||||
reward_resource_id = ?, reward_resource_group_id = ?, reward_quantity = ?, reward_duration_ms = ?,
|
||||
reward_items_json = CAST(? AS JSON), status = ?, sort_order = ?,
|
||||
version = ?, current_task_version_id = ?, effective_from_ms = ?, effective_to_ms = ?,
|
||||
updated_by_admin_id = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND task_id = ?`,
|
||||
next.TaskType, next.Category, next.MetricType, next.Title, next.Description,
|
||||
next.AudienceType, next.IconKey, next.IconURL, next.ActionType, next.ActionParam,
|
||||
next.ActionPayloadJSON, next.DimensionFilterJSON,
|
||||
next.TargetValue, next.TargetUnit, next.RewardCoinAmount, next.RewardAssetType, next.Status, next.SortOrder,
|
||||
next.TargetValue, next.TargetUnit, next.RewardCoinAmount, next.RewardAssetType, next.RewardType,
|
||||
next.RewardResourceID, next.RewardResourceGroupID, next.RewardQuantity, next.RewardDurationMS,
|
||||
next.RewardItemsJSON, next.Status, next.SortOrder,
|
||||
next.Version, next.CurrentVersionID, next.EffectiveFromMS, next.EffectiveToMS,
|
||||
next.UpdatedByAdminID, next.UpdatedAtMS, appcode.FromContext(ctx), next.TaskID,
|
||||
); err != nil {
|
||||
@ -358,8 +374,14 @@ func (r *Repository) PrepareTaskClaim(ctx context.Context, command taskdomain.Cl
|
||||
TaskID: command.TaskID,
|
||||
TaskType: command.TaskType,
|
||||
CycleKey: command.CycleKey,
|
||||
RewardType: progress.RewardType,
|
||||
RewardCoinAmount: progress.RewardCoinAmount,
|
||||
RewardAssetType: progress.RewardAssetType,
|
||||
RewardResourceID: progress.RewardResourceID,
|
||||
RewardResourceGroupID: progress.RewardResourceGroupID,
|
||||
RewardQuantity: progress.RewardQuantity,
|
||||
RewardDurationMS: progress.RewardDurationMS,
|
||||
RewardItemsJSON: progress.RewardItemsJSON,
|
||||
Status: taskdomain.ClaimStatusPending,
|
||||
CreatedAtMS: nowMS,
|
||||
UpdatedAtMS: nowMS,
|
||||
@ -368,10 +390,14 @@ func (r *Repository) PrepareTaskClaim(ctx context.Context, command taskdomain.Cl
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO task_reward_claims (
|
||||
app_code, claim_id, command_id, user_id, task_id, task_type, cycle_key,
|
||||
reward_coin_amount, reward_asset_type, wallet_command_id, status, failure_reason, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?)`,
|
||||
reward_coin_amount, reward_asset_type, reward_type, reward_resource_id, reward_resource_group_id,
|
||||
reward_quantity, reward_duration_ms, reward_items_json, wallet_command_id, status,
|
||||
failure_reason, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, '', ?, ?)`,
|
||||
appcode.FromContext(ctx), claim.ClaimID, claim.CommandID, claim.UserID, claim.TaskID, claim.TaskType,
|
||||
claim.CycleKey, claim.RewardCoinAmount, claim.RewardAssetType, claim.WalletCommandID, claim.Status, claim.CreatedAtMS, claim.UpdatedAtMS,
|
||||
claim.CycleKey, claim.RewardCoinAmount, claim.RewardAssetType, claim.RewardType, claim.RewardResourceID,
|
||||
claim.RewardResourceGroupID, claim.RewardQuantity, claim.RewardDurationMS, claim.RewardItemsJSON,
|
||||
claim.WalletCommandID, claim.Status, claim.CreatedAtMS, claim.UpdatedAtMS,
|
||||
); err != nil {
|
||||
return taskdomain.Claim{}, err
|
||||
}
|
||||
@ -381,8 +407,8 @@ func (r *Repository) PrepareTaskClaim(ctx context.Context, command taskdomain.Cl
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
// MarkTaskClaimGranted 把钱包成功回执和用户任务 claimed 状态放在同一 activity 事务里。
|
||||
func (r *Repository) MarkTaskClaimGranted(ctx context.Context, claimID string, walletTransactionID string, grantedAtMS int64) (taskdomain.Claim, error) {
|
||||
// MarkTaskClaimGranted 把钱包资产交易或资源赠送回执和用户任务 claimed 状态放在同一 activity 事务里。
|
||||
func (r *Repository) MarkTaskClaimGranted(ctx context.Context, claimID string, walletTransactionID string, walletResourceGrantID string, grantedAtMS int64) (taskdomain.Claim, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return taskdomain.Claim{}, err
|
||||
@ -401,9 +427,10 @@ func (r *Repository) MarkTaskClaimGranted(ctx context.Context, claimID string, w
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE task_reward_claims
|
||||
SET status = 'granted', wallet_transaction_id = ?, failure_reason = '', updated_at_ms = ?
|
||||
SET status = 'granted', wallet_transaction_id = ?, wallet_resource_grant_id = ?,
|
||||
failure_reason = '', updated_at_ms = ?
|
||||
WHERE app_code = ? AND claim_id = ?`,
|
||||
walletTransactionID, grantedAtMS, appcode.FromContext(ctx), claimID,
|
||||
walletTransactionID, walletResourceGrantID, grantedAtMS, appcode.FromContext(ctx), claimID,
|
||||
); err != nil {
|
||||
return taskdomain.Claim{}, err
|
||||
}
|
||||
@ -420,6 +447,7 @@ func (r *Repository) MarkTaskClaimGranted(ctx context.Context, claimID string, w
|
||||
}
|
||||
claim.Status = taskdomain.ClaimStatusGranted
|
||||
claim.WalletTransactionID = walletTransactionID
|
||||
claim.WalletResourceGrantID = walletResourceGrantID
|
||||
claim.UpdatedAtMS = grantedAtMS
|
||||
return claim, nil
|
||||
}
|
||||
@ -474,14 +502,16 @@ func (r *Repository) listDefinitionsForEvent(ctx context.Context, tx *sql.Tx, me
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, reward_type,
|
||||
reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), status, sort_order, version,
|
||||
current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
|
||||
updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM task_definitions
|
||||
WHERE app_code = ? AND status = 'active' AND metric_type = ?
|
||||
AND (effective_from_ms = 0 OR effective_from_ms <= ?)
|
||||
AND (effective_to_ms = 0 OR effective_to_ms > ?)
|
||||
ORDER BY sort_order ASC, task_id ASC
|
||||
ORDER BY sort_order DESC, task_id ASC
|
||||
FOR UPDATE`,
|
||||
appcode.FromContext(ctx), metricType, occurredAtMS, occurredAtMS,
|
||||
)
|
||||
@ -498,7 +528,7 @@ func (r *Repository) applyTaskProgressDelta(ctx context.Context, tx *sql.Tx, use
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return false, err
|
||||
}
|
||||
// 首次命中任务时把配置版本、目标值和奖励金额一起快照到用户进度,后续后台改配置不会改变已产生进度的领取口径。
|
||||
// 首次命中任务时把配置版本、目标值和完整奖励一起快照到用户进度,后续后台改配置不会改变已产生进度的领取口径。
|
||||
status := taskdomain.StatusInProgress
|
||||
completedAtMS := int64(0)
|
||||
if delta >= definition.TargetValue {
|
||||
@ -508,10 +538,14 @@ func (r *Repository) applyTaskProgressDelta(ctx context.Context, tx *sql.Tx, use
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO user_task_progress (
|
||||
app_code, user_id, task_id, task_version_id, cycle_key, progress_value,
|
||||
target_value, reward_coin_amount, reward_asset_type, status, completed_at_ms, claimed_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
||||
target_value, reward_coin_amount, reward_asset_type, reward_type, reward_resource_id,
|
||||
reward_resource_group_id, reward_quantity, reward_duration_ms, reward_items_json,
|
||||
status, completed_at_ms, claimed_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, 0, ?)`,
|
||||
appcode.FromContext(ctx), userID, definition.TaskID, definition.CurrentVersionID, cycleKey, delta,
|
||||
definition.TargetValue, definition.RewardCoinAmount, definition.RewardAssetType, status, completedAtMS, nowMS,
|
||||
definition.TargetValue, definition.RewardCoinAmount, definition.RewardAssetType, definition.RewardType,
|
||||
definition.RewardResourceID, definition.RewardResourceGroupID, definition.RewardQuantity,
|
||||
definition.RewardDurationMS, definition.RewardItemsJSON, status, completedAtMS, nowMS,
|
||||
)
|
||||
return err == nil, err
|
||||
}
|
||||
@ -554,8 +588,14 @@ func (r *Repository) createTaskDefinition(ctx context.Context, tx *sql.Tx, comma
|
||||
DimensionFilterJSON: command.DimensionFilterJSON,
|
||||
TargetValue: command.TargetValue,
|
||||
TargetUnit: command.TargetUnit,
|
||||
RewardType: command.RewardType,
|
||||
RewardCoinAmount: command.RewardCoinAmount,
|
||||
RewardAssetType: command.RewardAssetType,
|
||||
RewardResourceID: command.RewardResourceID,
|
||||
RewardResourceGroupID: command.RewardResourceGroupID,
|
||||
RewardQuantity: command.RewardQuantity,
|
||||
RewardDurationMS: command.RewardDurationMS,
|
||||
RewardItemsJSON: command.RewardItemsJSON,
|
||||
Status: command.Status,
|
||||
SortOrder: command.SortOrder,
|
||||
Version: 1,
|
||||
@ -575,15 +615,19 @@ func (r *Repository) createTaskDefinition(ctx context.Context, tx *sql.Tx, comma
|
||||
INSERT INTO task_definitions (
|
||||
app_code, task_id, task_type, category, metric_type, title, description,
|
||||
audience_type, icon_key, icon_url, action_type, action_param, action_payload_json, dimension_filter_json,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, reward_type, reward_resource_id,
|
||||
reward_resource_group_id, reward_quantity, reward_duration_ms, reward_items_json,
|
||||
status, sort_order, version,
|
||||
current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
|
||||
updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
definition.AppCode, definition.TaskID, definition.TaskType, definition.Category, definition.MetricType,
|
||||
definition.Title, definition.Description, definition.AudienceType, definition.IconKey, definition.IconURL,
|
||||
definition.ActionType, definition.ActionParam, definition.ActionPayloadJSON, definition.DimensionFilterJSON,
|
||||
definition.TargetValue, definition.TargetUnit,
|
||||
definition.RewardCoinAmount, definition.RewardAssetType, definition.Status, definition.SortOrder, definition.Version,
|
||||
definition.RewardCoinAmount, definition.RewardAssetType, definition.RewardType, definition.RewardResourceID,
|
||||
definition.RewardResourceGroupID, definition.RewardQuantity, definition.RewardDurationMS, definition.RewardItemsJSON,
|
||||
definition.Status, definition.SortOrder, definition.Version,
|
||||
definition.CurrentVersionID, definition.EffectiveFromMS, definition.EffectiveToMS,
|
||||
definition.CreatedByAdminID, definition.UpdatedByAdminID, definition.CreatedAtMS, definition.UpdatedAtMS,
|
||||
)
|
||||
@ -617,7 +661,9 @@ func (r *Repository) getTaskDefinition(ctx context.Context, taskID string) (task
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, reward_type,
|
||||
reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), status, sort_order, version,
|
||||
current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
|
||||
updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM task_definitions
|
||||
@ -633,7 +679,9 @@ func (r *Repository) getTaskDefinitionForUpdate(ctx context.Context, tx *sql.Tx,
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, reward_type,
|
||||
reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), status, sort_order, version,
|
||||
current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
|
||||
updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM task_definitions
|
||||
@ -651,7 +699,9 @@ func (r *Repository) getTaskDefinitionForUpdate(ctx context.Context, tx *sql.Tx,
|
||||
func (r *Repository) getProgressForUpdate(ctx context.Context, tx *sql.Tx, userID int64, taskID string, cycleKey string) (taskdomain.Progress, error) {
|
||||
row := tx.QueryRowContext(ctx, `
|
||||
SELECT app_code, user_id, task_id, task_version_id, cycle_key, progress_value,
|
||||
target_value, reward_coin_amount, reward_asset_type, status, completed_at_ms, claimed_at_ms, updated_at_ms
|
||||
target_value, reward_coin_amount, reward_asset_type, reward_type,
|
||||
reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), status, completed_at_ms, claimed_at_ms, updated_at_ms
|
||||
FROM user_task_progress
|
||||
WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ?
|
||||
FOR UPDATE`,
|
||||
@ -733,6 +783,12 @@ func scanDefinition(row rowScanner) (taskdomain.Definition, error) {
|
||||
&definition.TargetUnit,
|
||||
&definition.RewardCoinAmount,
|
||||
&definition.RewardAssetType,
|
||||
&definition.RewardType,
|
||||
&definition.RewardResourceID,
|
||||
&definition.RewardResourceGroupID,
|
||||
&definition.RewardQuantity,
|
||||
&definition.RewardDurationMS,
|
||||
&definition.RewardItemsJSON,
|
||||
&definition.Status,
|
||||
&definition.SortOrder,
|
||||
&definition.Version,
|
||||
@ -759,6 +815,12 @@ func scanProgress(row rowScanner) (taskdomain.Progress, error) {
|
||||
&progress.TargetValue,
|
||||
&progress.RewardCoinAmount,
|
||||
&progress.RewardAssetType,
|
||||
&progress.RewardType,
|
||||
&progress.RewardResourceID,
|
||||
&progress.RewardResourceGroupID,
|
||||
&progress.RewardQuantity,
|
||||
&progress.RewardDurationMS,
|
||||
&progress.RewardItemsJSON,
|
||||
&progress.Status,
|
||||
&progress.CompletedAtMS,
|
||||
&progress.ClaimedAtMS,
|
||||
@ -778,8 +840,15 @@ func scanClaim(row rowScanner) (taskdomain.Claim, error) {
|
||||
&claim.CycleKey,
|
||||
&claim.RewardCoinAmount,
|
||||
&claim.RewardAssetType,
|
||||
&claim.RewardType,
|
||||
&claim.RewardResourceID,
|
||||
&claim.RewardResourceGroupID,
|
||||
&claim.RewardQuantity,
|
||||
&claim.RewardDurationMS,
|
||||
&claim.RewardItemsJSON,
|
||||
&claim.WalletCommandID,
|
||||
&claim.WalletTransactionID,
|
||||
&claim.WalletResourceGrantID,
|
||||
&claim.Status,
|
||||
&claim.FailureReason,
|
||||
&claim.CreatedAtMS,
|
||||
@ -790,7 +859,9 @@ func scanClaim(row rowScanner) (taskdomain.Claim, error) {
|
||||
|
||||
func taskClaimSelectSQL() string {
|
||||
return `SELECT claim_id, command_id, user_id, task_id, task_type, cycle_key, reward_coin_amount, reward_asset_type,
|
||||
wallet_command_id, wallet_transaction_id, status, failure_reason, created_at_ms, updated_at_ms
|
||||
reward_type, reward_resource_id, reward_resource_group_id, reward_quantity, reward_duration_ms,
|
||||
COALESCE(CAST(reward_items_json AS CHAR), '[]'), wallet_command_id, wallet_transaction_id,
|
||||
wallet_resource_grant_id, status, failure_reason, created_at_ms, updated_at_ms
|
||||
FROM task_reward_claims `
|
||||
}
|
||||
|
||||
@ -827,7 +898,13 @@ func taskDefinitionNeedsVersion(current taskdomain.Definition, next taskdomain.D
|
||||
current.TargetValue != next.TargetValue ||
|
||||
current.TargetUnit != next.TargetUnit ||
|
||||
current.RewardCoinAmount != next.RewardCoinAmount ||
|
||||
current.RewardAssetType != next.RewardAssetType
|
||||
current.RewardAssetType != next.RewardAssetType ||
|
||||
current.RewardType != next.RewardType ||
|
||||
current.RewardResourceID != next.RewardResourceID ||
|
||||
current.RewardResourceGroupID != next.RewardResourceGroupID ||
|
||||
current.RewardQuantity != next.RewardQuantity ||
|
||||
current.RewardDurationMS != next.RewardDurationMS ||
|
||||
current.RewardItemsJSON != next.RewardItemsJSON
|
||||
}
|
||||
|
||||
func dimensionFilterMatches(filterJSON string, dimensionsJSON string) (bool, error) {
|
||||
|
||||
@ -136,7 +136,8 @@ func (r *Repository) UpsertWeeklyStarCycle(ctx context.Context, command domain.C
|
||||
for index, gift := range cycle.Gifts {
|
||||
sortOrder := gift.SortOrder
|
||||
if sortOrder <= 0 {
|
||||
sortOrder = int32(index + 1)
|
||||
// repository 仍兼容绕过 service 的调用方;数组首项补最高权重,保证读回的 DESC 顺序不反转。
|
||||
sortOrder = int32(len(cycle.Gifts) - index)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO weekly_star_cycle_gifts (app_code, cycle_id, gift_id, sort_order, created_at_ms)
|
||||
@ -777,7 +778,7 @@ func (r *Repository) listWeeklyStarGifts(ctx context.Context, cycleID string) ([
|
||||
SELECT app_code, cycle_id, gift_id, sort_order
|
||||
FROM weekly_star_cycle_gifts
|
||||
WHERE app_code = ? AND cycle_id = ?
|
||||
ORDER BY sort_order ASC, gift_id ASC`,
|
||||
ORDER BY sort_order DESC, gift_id ASC`,
|
||||
appcode.FromContext(ctx), cycleID,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@ -2,6 +2,7 @@ package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
@ -125,6 +126,11 @@ func (s *AdminTaskServer) UpsertTaskDefinition(ctx context.Context, req *activit
|
||||
TargetUnit: req.GetTargetUnit(),
|
||||
RewardCoinAmount: req.GetRewardCoinAmount(),
|
||||
RewardAssetType: req.GetRewardAssetType(),
|
||||
RewardType: req.GetRewardType(),
|
||||
RewardResourceID: req.GetRewardResourceId(),
|
||||
RewardResourceGroupID: req.GetRewardResourceGroupId(),
|
||||
RewardQuantity: req.GetRewardQuantity(),
|
||||
RewardDurationMS: req.GetRewardDurationMs(),
|
||||
Status: req.GetStatus(),
|
||||
SortOrder: req.GetSortOrder(),
|
||||
EffectiveFromMS: req.GetEffectiveFromMs(),
|
||||
@ -194,6 +200,12 @@ func itemToProto(item taskdomain.Item) *activityv1.TaskItem {
|
||||
ProgressValue: item.ProgressValue,
|
||||
RewardCoinAmount: item.RewardCoinAmount,
|
||||
RewardAssetType: item.RewardAssetType,
|
||||
RewardType: item.RewardType,
|
||||
RewardResourceId: item.RewardResourceID,
|
||||
RewardResourceGroupId: item.RewardResourceGroupID,
|
||||
RewardQuantity: item.RewardQuantity,
|
||||
RewardDurationMs: item.RewardDurationMS,
|
||||
RewardItems: rewardItemsToProto(item.RewardItemsJSON),
|
||||
Status: item.UserStatus,
|
||||
Claimable: item.Claimable,
|
||||
TaskDay: item.TaskDay,
|
||||
@ -212,6 +224,11 @@ func claimToProto(claim taskdomain.Claim) *activityv1.ClaimTaskRewardResponse {
|
||||
TaskDay: claim.CycleKey,
|
||||
RewardCoinAmount: claim.RewardCoinAmount,
|
||||
RewardAssetType: claim.RewardAssetType,
|
||||
RewardType: claim.RewardType,
|
||||
RewardResourceId: claim.RewardResourceID,
|
||||
RewardResourceGroupId: claim.RewardResourceGroupID,
|
||||
WalletResourceGrantId: claim.WalletResourceGrantID,
|
||||
RewardItems: rewardItemsToProto(claim.RewardItemsJSON),
|
||||
Status: claim.Status,
|
||||
WalletTransactionId: claim.WalletTransactionID,
|
||||
GrantedAtMs: claim.UpdatedAtMS,
|
||||
@ -238,6 +255,12 @@ func definitionToProto(item taskdomain.Definition) *activityv1.TaskDefinition {
|
||||
TargetUnit: item.TargetUnit,
|
||||
RewardCoinAmount: item.RewardCoinAmount,
|
||||
RewardAssetType: item.RewardAssetType,
|
||||
RewardType: item.RewardType,
|
||||
RewardResourceId: item.RewardResourceID,
|
||||
RewardResourceGroupId: item.RewardResourceGroupID,
|
||||
RewardQuantity: item.RewardQuantity,
|
||||
RewardDurationMs: item.RewardDurationMS,
|
||||
RewardItems: rewardItemsToProto(item.RewardItemsJSON),
|
||||
Status: item.Status,
|
||||
SortOrder: item.SortOrder,
|
||||
Version: item.Version,
|
||||
@ -249,3 +272,25 @@ func definitionToProto(item taskdomain.Definition) *activityv1.TaskDefinition {
|
||||
UpdatedAtMs: item.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func rewardItemsToProto(snapshotJSON string) []*activityv1.TaskRewardItem {
|
||||
var items []taskdomain.RewardItem
|
||||
if err := json.Unmarshal([]byte(snapshotJSON), &items); err != nil {
|
||||
// 旧任务没有展示快照时返回空数组,gateway/H5 会继续使用 reward_coin_amount 的兼容展示。
|
||||
return []*activityv1.TaskRewardItem{}
|
||||
}
|
||||
result := make([]*activityv1.TaskRewardItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, &activityv1.TaskRewardItem{
|
||||
ItemType: item.ItemType,
|
||||
ResourceId: item.ResourceID,
|
||||
ResourceType: item.ResourceType,
|
||||
CoverUrl: item.CoverURL,
|
||||
Quantity: item.Quantity,
|
||||
DurationMs: item.DurationMS,
|
||||
WalletAssetType: item.WalletAssetType,
|
||||
WalletAssetAmount: item.WalletAssetAmount,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ CREATE TABLE IF NOT EXISTS game_platforms (
|
||||
callback_secret_ciphertext VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '回调密钥密文',
|
||||
callback_ip_whitelist JSON NULL COMMENT '回调IP白名单',
|
||||
adapter_config JSON NULL COMMENT '适配器配置',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY(app_code, platform_code)
|
||||
@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS game_catalog (
|
||||
min_coin BIGINT NOT NULL DEFAULT 0 COMMENT '最小金币',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
whitelist_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否仅对白名单用户展示并允许启动',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
tags JSON NULL COMMENT '标签',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS game_display_rules (
|
||||
platform VARCHAR(16) NOT NULL DEFAULT '' COMMENT '平台',
|
||||
visible TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可见',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY(app_code, rule_id),
|
||||
|
||||
@ -272,7 +272,7 @@ func NormalizeStakeOptions(options []StakeOption) []StakeOption {
|
||||
if out[left].SortOrder == out[right].SortOrder {
|
||||
return out[left].StakeCoin < out[right].StakeCoin
|
||||
}
|
||||
return out[left].SortOrder < out[right].SortOrder
|
||||
return out[left].SortOrder > out[right].SortOrder
|
||||
})
|
||||
if len(out) == 0 {
|
||||
return NormalizeStakeOptions(nil)
|
||||
|
||||
@ -236,11 +236,11 @@ func (s *Service) GetBridgeScript(ctx context.Context, appCode string) (BridgeSc
|
||||
if err != nil {
|
||||
return BridgeScriptResult{}, err
|
||||
}
|
||||
// admin 会把通用桥接脚本同步写入所有平台;这里仍按 sort/code 排序后读取第一份有效配置,
|
||||
// admin 会把通用桥接脚本同步写入所有平台;这里仍按“大 sort 优先、code 稳定兜底”读取第一份有效配置,
|
||||
// 避免数据库返回顺序变化导致 App 在相同配置之间来回切换。
|
||||
sort.SliceStable(platforms, func(left, right int) bool {
|
||||
if platforms[left].SortOrder != platforms[right].SortOrder {
|
||||
return platforms[left].SortOrder < platforms[right].SortOrder
|
||||
return platforms[left].SortOrder > platforms[right].SortOrder
|
||||
}
|
||||
return strings.Compare(platforms[left].PlatformCode, platforms[right].PlatformCode) < 0
|
||||
})
|
||||
|
||||
@ -141,7 +141,7 @@ func TestGetBridgeScriptUsesFirstActivePlatformConfig(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetBridgeScript failed: %v", err)
|
||||
}
|
||||
if result.URL != "https://cdn.example.test/bridge.js" || result.Version != "20260608.1" || result.SHA256 != "abc" || result.ServerTimeMS != 1700000000000 {
|
||||
if result.URL != "https://cdn.example.test/bridge-viva.js" || result.Version != "20260608.2" || result.SHA256 != "def" || result.ServerTimeMS != 1700000000000 {
|
||||
t.Fatalf("bridge script mismatch: %+v", result)
|
||||
}
|
||||
if repo.lastListPlatformsAppCode != "lalu" || repo.lastListPlatformsStatus != gamedomain.StatusActive {
|
||||
|
||||
@ -755,7 +755,8 @@ func (s *Service) normalizeConfig(ctx context.Context, app string, config *gamev
|
||||
}
|
||||
sortOrder := gift.GetSortOrder()
|
||||
if sortOrder <= 0 {
|
||||
sortOrder = int32(index + 1)
|
||||
// 提交数组已经代表后台从上到下的顺序;大值优先后,越靠前的缺省档位必须获得越大的权重。
|
||||
sortOrder = int32(len(config.GetStakeGifts()) - index)
|
||||
}
|
||||
if _, ok := seenSort[sortOrder]; ok {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "room rps gift sort_order is duplicated")
|
||||
@ -795,7 +796,8 @@ func (s *Service) normalizeConfig(ctx context.Context, app string, config *gamev
|
||||
normalized.GiftPriceCoin = int64(normalized.GetSortOrder()) * 100
|
||||
}
|
||||
}
|
||||
sort.Slice(gifts, func(i, j int) bool { return gifts[i].GetSortOrder() < gifts[j].GetSortOrder() })
|
||||
// 猜拳礼物同样遵循后台统一权重语义;档位身份由礼物和金额表达,不能再把小排序值当作优先级。
|
||||
sort.Slice(gifts, func(i, j int) bool { return gifts[i].GetSortOrder() > gifts[j].GetSortOrder() })
|
||||
return &gamev1.RoomRPSConfig{
|
||||
AppCode: app,
|
||||
GameId: GameID,
|
||||
|
||||
@ -39,14 +39,14 @@ func TestUpdateConfigEnrichesStakeGiftsFromWalletCatalog(t *testing.T) {
|
||||
t.Fatalf("UpdateConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if got := config.GetStakeGifts()[0]; got.GetGiftName() != "Rose" || got.GetGiftIconUrl() != "https://cdn.example.test/rose.png" || got.GetGiftAnimationUrl() != "https://cdn.example.test/rose.mp4" || got.GetGiftPriceCoin() != 100 {
|
||||
t.Fatalf("first gift was not enriched from wallet catalog: %+v", got)
|
||||
if got := config.GetStakeGifts()[0]; got.GetGiftName() != "Cup" || got.GetGiftIconUrl() != "https://cdn.example.test/cup.png" || got.GetGiftPriceCoin() != 1000 || got.GetSortOrder() != 4 {
|
||||
t.Fatalf("highest-sort gift was not enriched from wallet catalog: %+v", got)
|
||||
}
|
||||
if got := config.GetStakeGifts()[1]; got.GetGiftIconUrl() != "https://cdn.example.test/bell-preview.png" || got.GetGiftAnimationUrl() != "https://cdn.example.test/bell.svga" {
|
||||
t.Fatalf("second gift should use preview_url as icon and animation_url as animation: %+v", got)
|
||||
if got := config.GetStakeGifts()[2]; got.GetGiftIconUrl() != "https://cdn.example.test/bell-preview.png" || got.GetGiftAnimationUrl() != "https://cdn.example.test/bell.svga" {
|
||||
t.Fatalf("sort-2 gift should use preview_url as icon and animation_url as animation: %+v", got)
|
||||
}
|
||||
if got := config.GetStakeGifts()[2]; got.GetGiftIconUrl() != "" || got.GetGiftAnimationUrl() != "https://cdn.example.test/crown.svga" {
|
||||
t.Fatalf("third gift should not use animation_url as display icon: %+v", got)
|
||||
if got := config.GetStakeGifts()[1]; got.GetGiftIconUrl() != "" || got.GetGiftAnimationUrl() != "https://cdn.example.test/crown.svga" {
|
||||
t.Fatalf("sort-3 gift should not use animation_url as display icon: %+v", got)
|
||||
}
|
||||
if len(catalog.requests) != 4 {
|
||||
t.Fatalf("expected one exact catalog lookup per missing gift, got %d", len(catalog.requests))
|
||||
@ -54,7 +54,7 @@ func TestUpdateConfigEnrichesStakeGiftsFromWalletCatalog(t *testing.T) {
|
||||
if req := catalog.requests[0]; req.GetAppCode() != "lalu" || req.GetKeyword() != "2001" || req.GetPage() != 1 || req.GetPageSize() != 100 {
|
||||
t.Fatalf("catalog lookup request mismatch: %+v", req)
|
||||
}
|
||||
if got := config.GetStakeGifts()[0]; got.GetGiftIdText() != "2001" || got.GetGiftId() != 2001 {
|
||||
if got := config.GetStakeGifts()[0]; got.GetGiftIdText() != "2004" || got.GetGiftId() != 2004 {
|
||||
t.Fatalf("numeric gift should keep string primary ID and legacy number: %+v", got)
|
||||
}
|
||||
}
|
||||
@ -244,7 +244,7 @@ func TestRoomRPSConfigPersistsAcrossServiceInstances(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfig() after restart error = %v", err)
|
||||
}
|
||||
if got := config.GetStakeGifts()[0]; got.GetGiftIdText() != "persist-1" || got.GetGiftPriceCoin() != 11 {
|
||||
if got := config.GetStakeGifts()[0]; got.GetGiftIdText() != "persist-4" || got.GetGiftPriceCoin() != 44 {
|
||||
t.Fatalf("persistent config was not reused after restart: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@ -699,10 +699,11 @@ func isBuiltinSelfGameID(gameID string) bool {
|
||||
|
||||
func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) {
|
||||
// dice/rock 需要保留 catalog 行给专用匹配入口做可玩性校验,但 App 普通游戏列表只展示三方 H5 游戏,避免自研入口混进运营列表后被误删。
|
||||
// 同一游戏命中多条区域/语言展示规则时取最大运营权重;0 仍表示沿用目录权重,最终按大值优先返回。
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
||||
c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status,
|
||||
MIN(CASE WHEN r.sort_order = 0 THEN c.sort_order ELSE r.sort_order END) AS display_sort
|
||||
MAX(CASE WHEN r.sort_order = 0 THEN c.sort_order ELSE r.sort_order END) AS display_sort
|
||||
FROM game_catalog c
|
||||
JOIN game_platforms p ON p.app_code = c.app_code AND p.platform_code = c.platform_code
|
||||
JOIN game_display_rules r ON r.app_code = c.app_code AND r.game_id = c.game_id
|
||||
@ -723,7 +724,7 @@ func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQ
|
||||
))
|
||||
GROUP BY c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
||||
c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status, c.sort_order, p.sort_order
|
||||
ORDER BY display_sort ASC, p.sort_order ASC, c.sort_order ASC, c.game_id ASC`,
|
||||
ORDER BY display_sort DESC, p.sort_order DESC, c.sort_order DESC, c.game_id ASC`,
|
||||
appcode.Normalize(query.AppCode), query.Scene, query.RegionID, query.Language, query.ClientPlatform, query.UserID,
|
||||
)
|
||||
if err != nil {
|
||||
@ -755,7 +756,7 @@ func (r *Repository) ListRecentGames(ctx context.Context, query gameservice.List
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
||||
c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status,
|
||||
MIN(CASE WHEN r.sort_order = 0 THEN c.sort_order ELSE r.sort_order END) AS display_sort,
|
||||
MAX(CASE WHEN r.sort_order = 0 THEN c.sort_order ELSE r.sort_order END) AS display_sort,
|
||||
recent.last_played_at_ms
|
||||
FROM (
|
||||
SELECT game_id, MAX(created_at_ms) AS last_played_at_ms
|
||||
@ -785,7 +786,7 @@ func (r *Repository) ListRecentGames(ctx context.Context, query gameservice.List
|
||||
GROUP BY c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
||||
c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status,
|
||||
c.sort_order, p.sort_order, recent.last_played_at_ms
|
||||
ORDER BY recent.last_played_at_ms DESC, display_sort ASC, p.sort_order ASC, c.sort_order ASC, c.game_id ASC
|
||||
ORDER BY recent.last_played_at_ms DESC, display_sort DESC, p.sort_order DESC, c.sort_order DESC, c.game_id ASC
|
||||
LIMIT ?`,
|
||||
appcode.Normalize(query.AppCode), query.UserID, query.Scene,
|
||||
appcode.Normalize(query.AppCode), query.Scene, query.RegionID, query.Language, query.ClientPlatform, query.UserID, pageSize,
|
||||
@ -1601,7 +1602,7 @@ func (r *Repository) ListPlatforms(ctx context.Context, appCode string, status s
|
||||
query += " AND status = ?"
|
||||
args = append(args, strings.TrimSpace(status))
|
||||
}
|
||||
query += " ORDER BY sort_order ASC, platform_code ASC"
|
||||
query += " ORDER BY sort_order DESC, platform_code ASC"
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1665,7 +1666,7 @@ func (r *Repository) ListCatalog(ctx context.Context, query gameservice.ListCata
|
||||
sqlText += " AND status = ?"
|
||||
args = append(args, strings.TrimSpace(query.Status))
|
||||
}
|
||||
sqlText += " ORDER BY sort_order ASC, game_id ASC"
|
||||
sqlText += " ORDER BY sort_order DESC, game_id ASC"
|
||||
if query.PageSize > 0 {
|
||||
sqlText += " LIMIT ?"
|
||||
args = append(args, query.PageSize)
|
||||
|
||||
@ -49,7 +49,7 @@ const listExploreTabsSQL = `
|
||||
SELECT id, app_code, tab, h5_url, enabled, sort_order, updated_at_ms
|
||||
FROM admin_app_explore_tabs
|
||||
WHERE app_code = ? AND enabled = TRUE
|
||||
ORDER BY sort_order ASC, id DESC`
|
||||
ORDER BY sort_order DESC, id DESC`
|
||||
const listAppBannersSQL = `
|
||||
SELECT id, app_code, cover_url, room_small_image_url, banner_type, display_scope, param, platform, sort_order, region_id, country_code, description, starts_at_ms, ends_at_ms, updated_at_ms
|
||||
FROM admin_app_banners
|
||||
@ -59,7 +59,7 @@ const listAppBannersSQL = `
|
||||
AND (country_code = '' OR country_code = ?)
|
||||
AND (starts_at_ms = 0 OR starts_at_ms <= ?)
|
||||
AND (ends_at_ms = 0 OR ends_at_ms > ?)
|
||||
ORDER BY sort_order ASC, id DESC`
|
||||
ORDER BY sort_order DESC, id DESC`
|
||||
const listSplashScreensSQL = `
|
||||
SELECT id, app_code, cover_url, splash_type, param, platform, sort_order, region_id, country_code, description, display_duration_ms, starts_at_ms, ends_at_ms, updated_at_ms
|
||||
FROM admin_app_splash_screens
|
||||
@ -69,14 +69,14 @@ const listSplashScreensSQL = `
|
||||
AND (country_code = '' OR country_code = ?)
|
||||
AND (starts_at_ms = 0 OR starts_at_ms <= ?)
|
||||
AND (ends_at_ms = 0 OR ends_at_ms > ?)
|
||||
ORDER BY sort_order ASC, id DESC`
|
||||
ORDER BY sort_order DESC, id DESC`
|
||||
const listPopupsSQL = `
|
||||
SELECT id, app_code, code, name, image_url, jump_type, jump_url, display_period_days, sort_order, starts_at_ms, ends_at_ms, updated_at_ms
|
||||
FROM admin_app_popups
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
AND (starts_at_ms = 0 OR starts_at_ms <= ?)
|
||||
AND (ends_at_ms = 0 OR ends_at_ms > ?)
|
||||
ORDER BY sort_order ASC, id DESC`
|
||||
ORDER BY sort_order DESC, id DESC`
|
||||
const latestAppVersionSQL = `
|
||||
SELECT id, app_code, platform, version, build_number, force_update, download_url, COALESCE(description, ''), updated_at_ms
|
||||
FROM admin_app_versions
|
||||
|
||||
@ -37,6 +37,12 @@ type taskItemData struct {
|
||||
ProgressValue int64 `json:"progress_value"`
|
||||
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||
RewardAssetType string `json:"reward_asset_type"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardResourceID int64 `json:"reward_resource_id"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
RewardQuantity int64 `json:"reward_quantity"`
|
||||
RewardDurationMS int64 `json:"reward_duration_ms"`
|
||||
RewardItems []taskRewardItemData `json:"reward_items"`
|
||||
Status string `json:"status"`
|
||||
Claimable bool `json:"claimable"`
|
||||
TaskDay string `json:"task_day"`
|
||||
@ -53,12 +59,28 @@ type taskClaimData struct {
|
||||
TaskDay string `json:"task_day"`
|
||||
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||
RewardAssetType string `json:"reward_asset_type"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardResourceID int64 `json:"reward_resource_id"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
RewardItems []taskRewardItemData `json:"reward_items"`
|
||||
Status string `json:"status"`
|
||||
WalletTransactionID string `json:"wallet_transaction_id"`
|
||||
WalletResourceGrantID string `json:"wallet_resource_grant_id"`
|
||||
GrantedAtMS int64 `json:"granted_at_ms"`
|
||||
Claimed bool `json:"claimed"`
|
||||
}
|
||||
|
||||
type taskRewardItemData struct {
|
||||
ItemType string `json:"item_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
CoverURL string `json:"cover_url"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
WalletAssetType string `json:"wallet_asset_type"`
|
||||
WalletAssetAmount int64 `json:"wallet_asset_amount"`
|
||||
}
|
||||
|
||||
type taskClaimRequestBody struct {
|
||||
TaskID string `json:"task_id"`
|
||||
TaskIDCamel string `json:"taskId"`
|
||||
@ -170,8 +192,13 @@ func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Requ
|
||||
TaskDay: resp.GetTaskDay(),
|
||||
RewardCoinAmount: resp.GetRewardCoinAmount(),
|
||||
RewardAssetType: resp.GetRewardAssetType(),
|
||||
RewardType: resp.GetRewardType(),
|
||||
RewardResourceID: resp.GetRewardResourceId(),
|
||||
RewardResourceGroupID: resp.GetRewardResourceGroupId(),
|
||||
RewardItems: taskRewardItemsFromProto(resp.GetRewardItems()),
|
||||
Status: resp.GetStatus(),
|
||||
WalletTransactionID: resp.GetWalletTransactionId(),
|
||||
WalletResourceGrantID: resp.GetWalletResourceGrantId(),
|
||||
GrantedAtMS: resp.GetGrantedAtMs(),
|
||||
Claimed: resp.GetClaimed(),
|
||||
})
|
||||
@ -199,6 +226,12 @@ func taskItemFromProto(item *activityv1.TaskItem) taskItemData {
|
||||
ProgressValue: item.GetProgressValue(),
|
||||
RewardCoinAmount: item.GetRewardCoinAmount(),
|
||||
RewardAssetType: item.GetRewardAssetType(),
|
||||
RewardType: item.GetRewardType(),
|
||||
RewardResourceID: item.GetRewardResourceId(),
|
||||
RewardResourceGroupID: item.GetRewardResourceGroupId(),
|
||||
RewardQuantity: item.GetRewardQuantity(),
|
||||
RewardDurationMS: item.GetRewardDurationMs(),
|
||||
RewardItems: taskRewardItemsFromProto(item.GetRewardItems()),
|
||||
Status: item.GetStatus(),
|
||||
Claimable: item.GetClaimable(),
|
||||
TaskDay: item.GetTaskDay(),
|
||||
@ -209,6 +242,26 @@ func taskItemFromProto(item *activityv1.TaskItem) taskItemData {
|
||||
}
|
||||
}
|
||||
|
||||
func taskRewardItemsFromProto(items []*activityv1.TaskRewardItem) []taskRewardItemData {
|
||||
result := make([]taskRewardItemData, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, taskRewardItemData{
|
||||
ItemType: item.GetItemType(),
|
||||
ResourceID: item.GetResourceId(),
|
||||
ResourceType: item.GetResourceType(),
|
||||
CoverURL: item.GetCoverUrl(),
|
||||
Quantity: item.GetQuantity(),
|
||||
DurationMS: item.GetDurationMs(),
|
||||
WalletAssetType: item.GetWalletAssetType(),
|
||||
WalletAssetAmount: item.GetWalletAssetAmount(),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func taskJSONPayload(value string) map[string]any {
|
||||
payload := map[string]any{}
|
||||
if strings.TrimSpace(value) == "" {
|
||||
|
||||
@ -383,7 +383,7 @@ CREATE TABLE IF NOT EXISTS countries (
|
||||
phone_country_code VARCHAR(8) NULL COMMENT '电话国家编码',
|
||||
flag VARCHAR(16) NOT NULL DEFAULT '' COMMENT '国旗',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '启用',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_by_user_id BIGINT NULL COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NULL COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
@ -401,7 +401,7 @@ CREATE TABLE IF NOT EXISTS regions (
|
||||
region_code VARCHAR(64) NOT NULL COMMENT '区域编码',
|
||||
name VARCHAR(128) NOT NULL COMMENT '名称',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_by_user_id BIGINT NULL COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NULL COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
|
||||
@ -140,7 +140,7 @@ func (r *Repository) ListAvailablePrettyDisplayIDs(ctx context.Context, query us
|
||||
AND pdi.source = ?
|
||||
AND pool.status = ?
|
||||
`+levelWhere+`
|
||||
ORDER BY pool.sort_order ASC, pdi.created_at_ms ASC, pdi.pretty_id ASC
|
||||
ORDER BY pool.sort_order DESC, pdi.created_at_ms ASC, pdi.pretty_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`, selectArgs...)
|
||||
if err != nil {
|
||||
@ -696,7 +696,7 @@ func (r *Repository) ListPrettyDisplayIDPools(ctx context.Context, query userdom
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM pretty_display_id_pools
|
||||
`+whereSQL+`
|
||||
ORDER BY sort_order ASC, created_at_ms DESC, pool_id ASC
|
||||
ORDER BY sort_order DESC, created_at_ms DESC, pool_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`, append(args, int(pageSize), int((page-1)*pageSize))...)
|
||||
if err != nil {
|
||||
|
||||
@ -91,7 +91,7 @@ func listCountries(ctx context.Context, q queryer, filter userdomain.CountryFilt
|
||||
if len(conditions) > 0 {
|
||||
query += " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
query += " ORDER BY sort_order ASC, country_code ASC"
|
||||
query += " ORDER BY sort_order DESC, country_code ASC"
|
||||
rows, err := queryRows(ctx, q, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -121,7 +121,7 @@ func (r *Repository) ListRegions(ctx context.Context, filter userdomain.RegionFi
|
||||
args = append(args, userdomain.GlobalRegionCode)
|
||||
conditions = append(conditions, "region_code NOT IN ("+retiredRegionCodeSQLList+")")
|
||||
query += " WHERE " + strings.Join(conditions, " AND ")
|
||||
query += " ORDER BY sort_order ASC, region_code ASC"
|
||||
query += " ORDER BY sort_order DESC, region_code ASC"
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -286,7 +286,7 @@ CREATE TABLE IF NOT EXISTS wallet_policy_instances (
|
||||
CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
seller_user_id BIGINT NOT NULL COMMENT '币商用户 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序,越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序,越大越靠前',
|
||||
point_amount BIGINT NOT NULL COMMENT '兑换比例中的 POINT 分母',
|
||||
seller_coin_amount BIGINT NOT NULL COMMENT '兑换比例中的币商金币分子',
|
||||
service_country_codes JSON NOT NULL COMMENT '服务国家码数组;空数组表示不限国家',
|
||||
@ -909,7 +909,7 @@ CREATE TABLE IF NOT EXISTS wallet_recharge_products (
|
||||
resource_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '资源资产类型',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否在 App 普通充值列表展示;不影响 Google Play 验单',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_by_user_id BIGINT NULL COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NULL COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
@ -1294,7 +1294,7 @@ CREATE TABLE IF NOT EXISTS resources (
|
||||
preview_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '预览 URL',
|
||||
animation_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '动画 URL',
|
||||
metadata_json JSON NULL COMMENT '扩展元数据 JSON',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
@ -1357,7 +1357,7 @@ CREATE TABLE IF NOT EXISTS resource_groups (
|
||||
name VARCHAR(128) NOT NULL COMMENT '名称',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
description VARCHAR(512) NOT NULL DEFAULT '' COMMENT '描述信息',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
@ -1376,7 +1376,7 @@ CREATE TABLE IF NOT EXISTS resource_group_items (
|
||||
wallet_asset_amount BIGINT NOT NULL DEFAULT 0 COMMENT '钱包资产数量',
|
||||
quantity BIGINT NOT NULL DEFAULT 1 COMMENT '数量',
|
||||
duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '持续时长,毫秒',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
UNIQUE KEY uk_resource_group_items_member (app_code, group_id, item_type, resource_id, wallet_asset_type),
|
||||
@ -1410,7 +1410,7 @@ CREATE TABLE IF NOT EXISTS resource_shop_items (
|
||||
coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '该售卖天数的独立金币价格;未自定义时复制资源列表价格',
|
||||
effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间,UTC epoch ms,0 表示立即生效',
|
||||
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效结束时间,UTC epoch ms,0 表示长期有效',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
@ -1449,7 +1449,7 @@ CREATE TABLE IF NOT EXISTS gift_type_configs (
|
||||
name VARCHAR(64) NOT NULL COMMENT '后台展示名称',
|
||||
tab_key VARCHAR(32) NOT NULL COMMENT 'App 礼物面板 tab 展示 key',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT 'tab 状态,active 才在 App 面板展示',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT 'tab 排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT 'tab 排序权重,数值越大越靠前',
|
||||
created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
@ -1466,7 +1466,7 @@ CREATE TABLE IF NOT EXISTS gift_configs (
|
||||
resource_id BIGINT NOT NULL COMMENT '资源 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
name VARCHAR(128) NOT NULL COMMENT '名称',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
presentation_json JSON NULL COMMENT '展示配置 JSON 配置或快照',
|
||||
gift_type_code VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '礼物类型编码',
|
||||
vip_level INT NOT NULL DEFAULT 0 COMMENT 'VIP/贵族礼物对应的 VIP 等级,非 VIP 礼物为 0',
|
||||
@ -1614,7 +1614,7 @@ CREATE TABLE IF NOT EXISTS user_gift_wall (
|
||||
last_price_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '最后价格版本',
|
||||
first_received_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '首次收到时间,UTC epoch ms',
|
||||
last_received_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最后收到时间,UTC epoch ms',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, user_id, gift_id),
|
||||
@ -1738,7 +1738,7 @@ CREATE TABLE IF NOT EXISTS wallet_diamond_exchange_rules (
|
||||
to_asset_type VARCHAR(32) NOT NULL COMMENT '目标资产类型',
|
||||
from_amount BIGINT NOT NULL COMMENT '源数量',
|
||||
to_amount BIGINT NOT NULL COMMENT '目标数量',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
UNIQUE KEY uk_diamond_exchange_rule (app_code, exchange_type, from_amount, to_asset_type),
|
||||
@ -1929,7 +1929,7 @@ CREATE TABLE IF NOT EXISTS vip_levels (
|
||||
duration_ms BIGINT NOT NULL COMMENT '持续时长,毫秒',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '奖励资源分组 ID',
|
||||
required_recharge_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '兼容旧 VIP 累充门槛字段,当前不参与购买资格',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越大越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, level),
|
||||
|
||||
@ -80,7 +80,7 @@ func CanonicalResourceGroupJSON(group ResourceGroup) ([]byte, error) {
|
||||
}
|
||||
sort.SliceStable(canonical.Items, func(left, right int) bool {
|
||||
if canonical.Items[left].SortOrder != canonical.Items[right].SortOrder {
|
||||
return canonical.Items[left].SortOrder < canonical.Items[right].SortOrder
|
||||
return canonical.Items[left].SortOrder > canonical.Items[right].SortOrder
|
||||
}
|
||||
return canonical.Items[left].GroupItemID < canonical.Items[right].GroupItemID
|
||||
})
|
||||
|
||||
@ -14,21 +14,20 @@ type v5PayProductSyncCountry struct {
|
||||
CountryCode string
|
||||
CountryName string
|
||||
CurrencyCode string
|
||||
SortBase int32
|
||||
}
|
||||
|
||||
var v5PayProductSyncCountries = []v5PayProductSyncCountry{
|
||||
{CountryCode: "PH", CountryName: "Philippines", CurrencyCode: "PHP", SortBase: 2000},
|
||||
{CountryCode: "SA", CountryName: "Saudi Arabia", CurrencyCode: "SAR", SortBase: 2300},
|
||||
{CountryCode: "EG", CountryName: "Egypt", CurrencyCode: "EGP", SortBase: 2400},
|
||||
{CountryCode: "AE", CountryName: "United Arab Emirates", CurrencyCode: "AED", SortBase: 2500},
|
||||
{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", SortBase: 2600},
|
||||
{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", SortBase: 2800},
|
||||
{CountryCode: "PK", CountryName: "Pakistan", CurrencyCode: "PKR", SortBase: 2900},
|
||||
{CountryCode: "TR", CountryName: "Turkey", CurrencyCode: "TRY", SortBase: 3000},
|
||||
{CountryCode: "MX", CountryName: "Mexico", CurrencyCode: "MXN", SortBase: 3100},
|
||||
{CountryCode: "VN", CountryName: "Vietnam", CurrencyCode: "VND", SortBase: 3200},
|
||||
{CountryCode: "BR", CountryName: "Brazil", CurrencyCode: "BRL", SortBase: 3300},
|
||||
{CountryCode: "PH", CountryName: "Philippines", CurrencyCode: "PHP"},
|
||||
{CountryCode: "SA", CountryName: "Saudi Arabia", CurrencyCode: "SAR"},
|
||||
{CountryCode: "EG", CountryName: "Egypt", CurrencyCode: "EGP"},
|
||||
{CountryCode: "AE", CountryName: "United Arab Emirates", CurrencyCode: "AED"},
|
||||
{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR"},
|
||||
{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR"},
|
||||
{CountryCode: "PK", CountryName: "Pakistan", CurrencyCode: "PKR"},
|
||||
{CountryCode: "TR", CountryName: "Turkey", CurrencyCode: "TRY"},
|
||||
{CountryCode: "MX", CountryName: "Mexico", CurrencyCode: "MXN"},
|
||||
{CountryCode: "VN", CountryName: "Vietnam", CurrencyCode: "VND"},
|
||||
{CountryCode: "BR", CountryName: "Brazil", CurrencyCode: "BRL"},
|
||||
}
|
||||
|
||||
// SyncThirdPartyPaymentMethods 从支付商实时拉取已开通方式并落本地配置表;当前只有 V5Pay 提供 productTypes 能力。
|
||||
@ -76,7 +75,7 @@ func (s *Service) fetchV5PayPaymentMethodsForSync(ctx context.Context) ([]ledger
|
||||
issues := make([]ledger.ThirdPartyPaymentMethodSyncIssue, 0)
|
||||
skipped := 0
|
||||
seen := map[string]struct{}{}
|
||||
for _, country := range v5PayProductSyncCountries {
|
||||
for countryIndex, country := range v5PayProductSyncCountries {
|
||||
// V5Pay 的 productTypes 接口必须逐国家/币种查询,没有“全部国家”接口;候选集覆盖当前种子国家和后台已开通截图里的新增国家。
|
||||
products, err := s.v5Pay.ListProductTypes(ctx, V5PayProductTypesRequest{
|
||||
CountryCode: country.CountryCode,
|
||||
@ -86,8 +85,10 @@ func (s *Service) fetchV5PayPaymentMethodsForSync(ctx context.Context) ([]ledger
|
||||
issues = append(issues, v5PayProductSyncIssue(country, err))
|
||||
continue
|
||||
}
|
||||
// 国家数组与支付商产品数组都是默认展示顺序;分桶后从高到低赋权,确保同步不会反转任一层级。
|
||||
countrySortBase := int32((len(v5PayProductSyncCountries) - countryIndex) * 1000)
|
||||
for index, product := range products.PayinList {
|
||||
method, ok := v5PayProductToPaymentMethod(country, product, index)
|
||||
method, ok := v5PayProductToPaymentMethod(country, product, countrySortBase, index, len(products.PayinList))
|
||||
if !ok {
|
||||
skipped++
|
||||
continue
|
||||
@ -104,7 +105,7 @@ func (s *Service) fetchV5PayPaymentMethodsForSync(ctx context.Context) ([]ledger
|
||||
return methods, skipped, issues
|
||||
}
|
||||
|
||||
func v5PayProductToPaymentMethod(country v5PayProductSyncCountry, product V5PayProductType, index int) (ledger.ThirdPartyPaymentMethod, bool) {
|
||||
func v5PayProductToPaymentMethod(country v5PayProductSyncCountry, product V5PayProductType, countrySortBase int32, index int, total int) (ledger.ThirdPartyPaymentMethod, bool) {
|
||||
productType := strings.ToUpper(strings.TrimSpace(product.ProductType))
|
||||
currency := strings.ToUpper(strings.TrimSpace(product.Currency))
|
||||
if currency == "" {
|
||||
@ -128,7 +129,7 @@ func v5PayProductToPaymentMethod(country v5PayProductSyncCountry, product V5PayP
|
||||
LogoURL: strings.TrimSpace(product.Logo),
|
||||
Status: ledger.ThirdPartyPaymentStatusActive,
|
||||
USDToCurrencyRate: "0",
|
||||
SortOrder: country.SortBase + int32(index+1)*10,
|
||||
SortOrder: countrySortBase + int32(total-index)*10,
|
||||
}, true
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||
resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
|
||||
@ -39,6 +40,11 @@ func (s *Service) CreateResource(ctx context.Context, command resourcedomain.Res
|
||||
return resourcedomain.Resource{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
}
|
||||
command.AppCode = appcode.Normalize(command.AppCode)
|
||||
// 资源编码是系统业务键,不再要求后台运营手工维护;仅在创建请求未提供编码时生成,
|
||||
// 以兼容批量导入、初始化脚本等仍需指定稳定编码的内部调用方。
|
||||
if strings.TrimSpace(command.ResourceCode) == "" {
|
||||
command.ResourceCode = idgen.New("resource")
|
||||
}
|
||||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||||
return s.repository.CreateResource(ctx, command)
|
||||
}
|
||||
@ -104,6 +110,11 @@ func (s *Service) CreateResourceGroup(ctx context.Context, command resourcedomai
|
||||
return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
}
|
||||
command.AppCode = appcode.Normalize(command.AppCode)
|
||||
// 资源组编码由 wallet owner 生成,随机熵与数据库唯一键共同保证并发创建安全;
|
||||
// 显式编码仍保留给初始化和跨租户映射流程使用。
|
||||
if strings.TrimSpace(command.GroupCode) == "" {
|
||||
command.GroupCode = idgen.New("resource_group")
|
||||
}
|
||||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||||
return s.repository.CreateResourceGroup(ctx, command)
|
||||
}
|
||||
|
||||
@ -357,7 +357,7 @@ func (r *Repository) ListCoinSellerSalaryExchangeRateTiers(ctx context.Context,
|
||||
SELECT region_id, min_usd_minor, max_usd_minor, coin_per_usd, status, sort_order, updated_at_ms
|
||||
FROM coin_seller_salary_exchange_rate_tiers
|
||||
WHERE app_code = ? AND region_id = ? %s
|
||||
ORDER BY sort_order ASC, min_usd_minor ASC, tier_id ASC`, statusClause)
|
||||
ORDER BY sort_order DESC, min_usd_minor ASC, tier_id ASC`, statusClause)
|
||||
rows, err := r.db.QueryContext(ctx, query, appcode.FromContext(ctx), regionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -27,8 +27,9 @@ func (r *Repository) ListGiftConfigs(ctx context.Context, query resourcedomain.L
|
||||
`+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// 后台列表与 App 礼物面板共用此顺序,必须按大排序值优先,避免两端看到不同礼物次序。
|
||||
rows, err := r.db.QueryContext(ctx, giftConfigSelectSQL()+where+`
|
||||
ORDER BY gc.sort_order ASC, gc.created_at_ms DESC
|
||||
ORDER BY gc.sort_order DESC, gc.created_at_ms DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
|
||||
)
|
||||
|
||||
@ -357,7 +357,7 @@ func (r *Repository) resolveCoinSellerSalaryExchangeRateTier(ctx context.Context
|
||||
FROM coin_seller_salary_exchange_rate_tiers
|
||||
WHERE app_code = ? AND region_id = ? AND status = 'active'
|
||||
AND min_usd_minor <= ? AND max_usd_minor >= ?
|
||||
ORDER BY sort_order ASC, min_usd_minor ASC, tier_id ASC
|
||||
ORDER BY sort_order DESC, min_usd_minor ASC, tier_id ASC
|
||||
LIMIT 1
|
||||
FOR UPDATE`,
|
||||
appcode.FromContext(ctx),
|
||||
|
||||
@ -24,7 +24,7 @@ func (r *Repository) ListGiftTypeConfigs(ctx context.Context, query resourcedoma
|
||||
query = normalizeGiftTypeConfigListQuery(query)
|
||||
where, args := giftTypeConfigWhereSQL(query)
|
||||
rows, err := r.db.QueryContext(ctx, giftTypeConfigSelectSQL()+where+`
|
||||
ORDER BY sort_order ASC, type_code ASC`, args...)
|
||||
ORDER BY sort_order DESC, type_code ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ func (r *Repository) ListThirdPartyPaymentChannels(ctx context.Context, query le
|
||||
SELECT app_code, provider_code, provider_name, status, sort_order
|
||||
FROM third_party_payment_channels
|
||||
`+where+`
|
||||
ORDER BY sort_order ASC, provider_code ASC`, args...)
|
||||
ORDER BY sort_order DESC, provider_code ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -247,7 +247,7 @@ func (r *Repository) listH5ThirdPartyPaymentMethods(ctx context.Context, appCode
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, thirdPartyPaymentMethodSelectSQL()+`
|
||||
`+where+`
|
||||
ORDER BY c.sort_order ASC, CASE WHEN m.country_code = 'GLOBAL' THEN 1 ELSE 0 END ASC, m.sort_order ASC, m.method_id ASC`, args...)
|
||||
ORDER BY c.sort_order DESC, CASE WHEN m.country_code = 'GLOBAL' THEN 1 ELSE 0 END ASC, m.sort_order DESC, m.method_id ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -278,7 +278,7 @@ func (r *Repository) listThirdPartyPaymentMethods(ctx context.Context, appCode s
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, thirdPartyPaymentMethodSelectSQL()+`
|
||||
`+where+`
|
||||
ORDER BY CASE WHEN m.country_code = 'GLOBAL' THEN 1 ELSE 0 END ASC, m.sort_order ASC, m.method_id ASC`, args...)
|
||||
ORDER BY CASE WHEN m.country_code = 'GLOBAL' THEN 1 ELSE 0 END ASC, m.sort_order DESC, m.method_id ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ func (r *Repository) ListPointWithdrawalCoinSellers(ctx context.Context, appCode
|
||||
status, created_at_ms, updated_at_ms
|
||||
FROM point_withdrawal_coin_seller_configs
|
||||
WHERE `+where+`
|
||||
ORDER BY sort_order ASC, seller_user_id ASC`, args...)
|
||||
ORDER BY sort_order DESC, seller_user_id ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ func (r *Repository) listRedPacketCountTiers(ctx context.Context, querier redPac
|
||||
SELECT packet_count
|
||||
FROM red_packet_count_tiers
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY sort_order ASC, packet_count ASC`,
|
||||
ORDER BY sort_order DESC, packet_count ASC`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
@ -164,7 +164,7 @@ func (r *Repository) listRedPacketAmountTiers(ctx context.Context, querier redPa
|
||||
SELECT total_amount
|
||||
FROM red_packet_amount_tiers
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY sort_order ASC, total_amount ASC`,
|
||||
ORDER BY sort_order DESC, total_amount ASC`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@ -37,9 +37,10 @@ func (r *Repository) ListResources(ctx context.Context, query resourcedomain.Lis
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// sort_order 是运营展示权重,统一按大值优先;resource_id 只负责同权重下的稳定分页。
|
||||
rows, err := r.db.QueryContext(ctx, resourceSelectSQL()+`
|
||||
`+where+`
|
||||
ORDER BY sort_order ASC, resource_id DESC
|
||||
ORDER BY sort_order DESC, resource_id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
|
||||
)
|
||||
|
||||
@ -31,7 +31,7 @@ func (r *Repository) ListResourceGroups(ctx context.Context, query resourcedomai
|
||||
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||
FROM resource_groups
|
||||
`+where+`
|
||||
ORDER BY sort_order ASC, group_id DESC
|
||||
ORDER BY sort_order DESC, group_id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
|
||||
)
|
||||
@ -292,7 +292,7 @@ func (r *Repository) listResourceGroupItemsWithQuery(ctx context.Context, querie
|
||||
FROM resource_group_items i
|
||||
LEFT JOIN resources r ON r.app_code = i.app_code AND r.resource_id = i.resource_id
|
||||
WHERE i.app_code = ? AND i.group_id = ?
|
||||
ORDER BY i.sort_order ASC, i.group_item_id ASC`
|
||||
ORDER BY i.sort_order DESC, i.group_item_id ASC`
|
||||
rows, err := querier.QueryContext(ctx, query, appcode.FromContext(ctx), groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -314,7 +314,7 @@ func (r *Repository) lockResourceGroupItemRows(ctx context.Context, tx *sql.Tx,
|
||||
SELECT group_item_id
|
||||
FROM resource_group_items
|
||||
WHERE app_code = ? AND group_id = ?
|
||||
ORDER BY sort_order ASC, group_item_id ASC
|
||||
ORDER BY sort_order DESC, group_item_id ASC
|
||||
FOR UPDATE`,
|
||||
appcode.FromContext(ctx), groupID,
|
||||
)
|
||||
|
||||
@ -33,8 +33,9 @@ func (r *Repository) ListResourceShopItems(ctx context.Context, query resourcedo
|
||||
`+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// 商店配置和 App 购买列表共用同一权重语义:sort_order 越大越靠前。
|
||||
rows, err := r.db.QueryContext(ctx, resourceShopItemSelectSQL()+where+`
|
||||
ORDER BY si.sort_order ASC, si.shop_item_id DESC
|
||||
ORDER BY si.sort_order DESC, si.shop_item_id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
|
||||
)
|
||||
@ -594,7 +595,7 @@ func (r *Repository) listResourceShopItemsByResourceIDs(ctx context.Context, res
|
||||
args := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(resourceIDs)...)
|
||||
rows, err := r.db.QueryContext(ctx, resourceShopItemSelectSQL()+`
|
||||
WHERE si.app_code = ? AND si.resource_id IN (`+placeholders(len(resourceIDs))+`)
|
||||
ORDER BY si.sort_order ASC, si.shop_item_id DESC`,
|
||||
ORDER BY si.sort_order DESC, si.shop_item_id DESC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@ -558,7 +558,7 @@ func ensurePointWithdrawalCoinSellerSchema(ctx context.Context, db *sql.DB) erro
|
||||
CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
seller_user_id BIGINT NOT NULL COMMENT '币商用户 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序,越小越靠前',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序,越大越靠前',
|
||||
point_amount BIGINT NOT NULL COMMENT '兑换比例中的 POINT 分母',
|
||||
seller_coin_amount BIGINT NOT NULL COMMENT '兑换比例中的币商金币分子',
|
||||
service_country_codes JSON NOT NULL COMMENT '服务国家码数组;空数组表示不限国家',
|
||||
|
||||
@ -525,7 +525,7 @@ func (r *Repository) listVipLevelBenefits(ctx context.Context, querier vipBenefi
|
||||
query += ` AND b.status = ?`
|
||||
args = append(args, ledger.VipStatusActive)
|
||||
}
|
||||
query += ` ORDER BY b.sort_order ASC, b.benefit_code ASC`
|
||||
query += ` ORDER BY b.sort_order DESC, b.benefit_code ASC`
|
||||
rows, err := querier.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -113,7 +113,7 @@ func (r *Repository) listVipLevelsFromSnapshot(ctx context.Context, querier vipS
|
||||
required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
|
||||
FROM vip_levels
|
||||
WHERE app_code = ? AND status = ? AND level BETWEEN 1 AND ?
|
||||
ORDER BY sort_order ASC, level ASC`,
|
||||
ORDER BY sort_order DESC, level ASC`,
|
||||
appcode.FromContext(ctx), ledger.VipStatusActive, config.LevelCount,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@ -63,7 +63,7 @@ func (r *Repository) GetUserGiftWall(ctx context.Context, userID int64) (ledger.
|
||||
charge_asset_type, last_price_version, first_received_at_ms, last_received_at_ms, sort_order
|
||||
FROM user_gift_wall
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
ORDER BY sort_order ASC, total_value DESC, last_received_at_ms DESC, gift_id ASC`,
|
||||
ORDER BY sort_order DESC, total_value DESC, last_received_at_ms DESC, gift_id ASC`,
|
||||
appCode, userID,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user