From fa2ab4f3d6f495dc1c360c827fd56f95aa76e07d Mon Sep 17 00:00:00 2001 From: zhx Date: Sat, 18 Jul 2026 13:12:15 +0800 Subject: [PATCH] fix: restrict game king to game expenditures --- .../sql/20260717_aslan_game_king.sql | 12 +- .../sql/20260717_aslan_game_king.sql | 12 +- .../api/AslanGameKingManageClientApi.java | 2 - .../activity/aslan/AslanGameKingModels.java | 15 ++- .../cmd/UserGoldRunningWaterBackQryCmd.java | 6 + .../AslanGameKingBackRestController.java | 8 +- .../docs/Aslan游戏王活动接口.md | 25 ++-- .../other-application/pom.xml | 7 + .../listener/AslanGameKingWalletListener.java | 4 +- .../aslan/AslanGameKingServiceImpl.java | 122 +++++++----------- .../activity/aslan/AslanGameKingService.java | 3 +- .../activity/aslan/AslanGameKingRows.java | 4 +- .../AslanGameKingManageClientEndpoint.java | 6 +- .../impl/WalletGoldClientServiceImpl.java | 29 ++++- 14 files changed, 138 insertions(+), 117 deletions(-) diff --git a/.deploy/prod-deploy/sql/20260717_aslan_game_king.sql b/.deploy/prod-deploy/sql/20260717_aslan_game_king.sql index 0a366077..d2d58ba3 100644 --- a/.deploy/prod-deploy/sql/20260717_aslan_game_king.sql +++ b/.deploy/prod-deploy/sql/20260717_aslan_game_king.sql @@ -1,4 +1,4 @@ --- Aslan Game King: dedicated consume-to-draw campaign, ranking and settlement. +-- Aslan Game King: fixed third-party game expenditure-to-draw campaign, ranking and settlement. -- -- Performance review (DDL only; this migration must not run ad-hoc against production): -- 1. Wallet hot path inserts one ledger row by uq_activity_asset, then updates one overall PK row @@ -18,6 +18,9 @@ -- idempotent menu rows; it does not backfill wallet history or lock existing business tables. -- 7. Unlimited prize stock (-1) is never updated at draw time, avoiding a global hot-row lock; -- only finite stock uses the activity/prize primary-key update. +-- 8. Historical wallet backfill is not executed by this migration. Application queries bind +-- sys_origin + EXPENDITURE and four escaped left-anchored game prefixes, so the existing +-- wallet index (sys_origin,event_type,create_time) remains usable instead of scanning all spend. SET NAMES utf8mb4; @@ -33,7 +36,8 @@ CREATE TABLE IF NOT EXISTS aslan_game_king_activity ( settlement_delay_minutes INT NOT NULL DEFAULT 30, settlement_time DATETIME(3) NOT NULL, coin_per_draw BIGINT NOT NULL, - event_types VARCHAR(2000) NOT NULL, + event_types VARCHAR(2000) NOT NULL DEFAULT 'GAME_EXPENDITURE' + COMMENT 'compatibility sentinel; event scope is fixed in service code', ranking_type VARCHAR(32) NOT NULL DEFAULT 'TYCOON', ranking_period VARCHAR(32) NOT NULL DEFAULT 'OVERALL', enabled TINYINT(1) NOT NULL DEFAULT 0, @@ -88,7 +92,7 @@ CREATE TABLE IF NOT EXISTS aslan_game_king_wallet_ledger ( sys_origin VARCHAR(32) NOT NULL, event_type VARCHAR(64) NOT NULL, event_id VARCHAR(128) NULL, - receipt_type TINYINT NOT NULL COMMENT '0 income/winning, 1 expenditure/consumption', + receipt_type TINYINT NOT NULL COMMENT 'fixed 1: third-party game expenditure', amount DECIMAL(24,2) NOT NULL, event_time DATETIME(3) NOT NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), @@ -96,7 +100,7 @@ CREATE TABLE IF NOT EXISTS aslan_game_king_wallet_ledger ( UNIQUE KEY uq_aslan_gk_activity_asset (activity_id, asset_record_id), KEY idx_aslan_gk_ledger_user (activity_id, user_id, event_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='Idempotent eligible game expenditure and winning ledger'; + COMMENT='Idempotent eligible third-party game expenditure ledger'; CREATE TABLE IF NOT EXISTS aslan_game_king_user ( activity_id BIGINT NOT NULL, diff --git a/.deploy/test-deploy/sql/20260717_aslan_game_king.sql b/.deploy/test-deploy/sql/20260717_aslan_game_king.sql index 0a366077..d2d58ba3 100644 --- a/.deploy/test-deploy/sql/20260717_aslan_game_king.sql +++ b/.deploy/test-deploy/sql/20260717_aslan_game_king.sql @@ -1,4 +1,4 @@ --- Aslan Game King: dedicated consume-to-draw campaign, ranking and settlement. +-- Aslan Game King: fixed third-party game expenditure-to-draw campaign, ranking and settlement. -- -- Performance review (DDL only; this migration must not run ad-hoc against production): -- 1. Wallet hot path inserts one ledger row by uq_activity_asset, then updates one overall PK row @@ -18,6 +18,9 @@ -- idempotent menu rows; it does not backfill wallet history or lock existing business tables. -- 7. Unlimited prize stock (-1) is never updated at draw time, avoiding a global hot-row lock; -- only finite stock uses the activity/prize primary-key update. +-- 8. Historical wallet backfill is not executed by this migration. Application queries bind +-- sys_origin + EXPENDITURE and four escaped left-anchored game prefixes, so the existing +-- wallet index (sys_origin,event_type,create_time) remains usable instead of scanning all spend. SET NAMES utf8mb4; @@ -33,7 +36,8 @@ CREATE TABLE IF NOT EXISTS aslan_game_king_activity ( settlement_delay_minutes INT NOT NULL DEFAULT 30, settlement_time DATETIME(3) NOT NULL, coin_per_draw BIGINT NOT NULL, - event_types VARCHAR(2000) NOT NULL, + event_types VARCHAR(2000) NOT NULL DEFAULT 'GAME_EXPENDITURE' + COMMENT 'compatibility sentinel; event scope is fixed in service code', ranking_type VARCHAR(32) NOT NULL DEFAULT 'TYCOON', ranking_period VARCHAR(32) NOT NULL DEFAULT 'OVERALL', enabled TINYINT(1) NOT NULL DEFAULT 0, @@ -88,7 +92,7 @@ CREATE TABLE IF NOT EXISTS aslan_game_king_wallet_ledger ( sys_origin VARCHAR(32) NOT NULL, event_type VARCHAR(64) NOT NULL, event_id VARCHAR(128) NULL, - receipt_type TINYINT NOT NULL COMMENT '0 income/winning, 1 expenditure/consumption', + receipt_type TINYINT NOT NULL COMMENT 'fixed 1: third-party game expenditure', amount DECIMAL(24,2) NOT NULL, event_time DATETIME(3) NOT NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), @@ -96,7 +100,7 @@ CREATE TABLE IF NOT EXISTS aslan_game_king_wallet_ledger ( UNIQUE KEY uq_aslan_gk_activity_asset (activity_id, asset_record_id), KEY idx_aslan_gk_ledger_user (activity_id, user_id, event_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='Idempotent eligible game expenditure and winning ledger'; + COMMENT='Idempotent eligible third-party game expenditure ledger'; CREATE TABLE IF NOT EXISTS aslan_game_king_user ( activity_id BIGINT NOT NULL, diff --git a/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/activity/api/AslanGameKingManageClientApi.java b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/activity/api/AslanGameKingManageClientApi.java index 02a60bdd..4e99d514 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/activity/api/AslanGameKingManageClientApi.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/activity/api/AslanGameKingManageClientApi.java @@ -75,8 +75,6 @@ public interface AslanGameKingManageClientApi { @PostMapping("/backfill/{id}") ResultResponse backfill(@PathVariable("id") Long id, - @RequestParam("eventType") String eventType, - @RequestParam("receiptType") Integer receiptType, @RequestParam(value = "lastId", required = false) Long lastId, @RequestParam(value = "limit", required = false) Integer limit); } diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/activity/aslan/AslanGameKingModels.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/activity/aslan/AslanGameKingModels.java index 4d7b2f81..1c8c1d80 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/activity/aslan/AslanGameKingModels.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/activity/aslan/AslanGameKingModels.java @@ -47,6 +47,11 @@ public final class AslanGameKingModels { private Integer settlementDelayMinutes; private Long settlementTime; private Long coinPerDraw; + /** + * @deprecated 游戏王只统计服务端固定识别的第三方游戏金币消耗;该字段仅保留旧客户端兼容, + * 响应恒为 {@code [GAME_EXPENDITURE]},不能作为运营配置使用。 + */ + @Deprecated private List eventTypes = new ArrayList<>(); private String rankingType; private String rankingPeriod; @@ -311,9 +316,11 @@ public final class AslanGameKingModels { @Positive(message = "coinPerDraw must be greater than zero.") private Long coinPerDraw; - @NotNull(message = "eventTypes required.") - @Size(min = 1, max = 30, message = "eventTypes size must be between 1 and 30.") - private List<@NotBlank String> eventTypes; + /** + * @deprecated 兼容旧版 webconsole 请求体;服务端忽略传值并写入固定语义哨兵,避免旧客户端升级期间报错。 + */ + @Deprecated + private List eventTypes; private String rankingType = "TYCOON"; private String rankingPeriod = "OVERALL"; @@ -374,7 +381,7 @@ public final class AslanGameKingModels { private static final long serialVersionUID = 1L; private String eventType; - /** 0=收入(VICTORIOUS),1=支出(TYCOON 与抽奖次数)。 */ + /** 固定为 1(EXPENDITURE);历史补数不允许调用方切换收入/支出口径。 */ private Integer receiptType; private Integer scanned; private Integer accepted; diff --git a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/cmd/UserGoldRunningWaterBackQryCmd.java b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/cmd/UserGoldRunningWaterBackQryCmd.java index 2e11b35b..32c8a7f3 100644 --- a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/cmd/UserGoldRunningWaterBackQryCmd.java +++ b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/cmd/UserGoldRunningWaterBackQryCmd.java @@ -59,6 +59,12 @@ public class UserGoldRunningWaterBackQryCmd extends Command { */ private String origin; + /** + * 仅供服务端活动补数:强制查询金币支出,并把来源限制为四类第三方游戏前缀。 + * 调用方不能通过 type/origin 放宽此口径,避免补数误扫礼物、转账等非游戏流水。 + */ + private Boolean queryThirdPartyGameExpenditure; + /** * 创建时间. */ diff --git a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/activity/AslanGameKingBackRestController.java b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/activity/AslanGameKingBackRestController.java index eedd68a9..ec999305 100644 --- a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/activity/AslanGameKingBackRestController.java +++ b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/activity/AslanGameKingBackRestController.java @@ -118,13 +118,11 @@ public class AslanGameKingBackRestController extends BaseController { } /** - * 按精确 eventType + receiptType 分页补数;两种收支类型各自维护 lastId,避免合并游标跳流水。 + * 服务端固定补算第三方游戏金币支出;页面只维护分页游标,不能手工扩大事件或切换收支口径。 */ @PostMapping("/backfill/{id}") - public BackfillResult backfill(@PathVariable Long id, @RequestParam String eventType, - @RequestParam Integer receiptType, - @RequestParam(required = false) Long lastId, + public BackfillResult backfill(@PathVariable Long id, @RequestParam(required = false) Long lastId, @RequestParam(required = false) Integer limit) { - return client.backfill(id, eventType, receiptType, lastId, limit).getBody(); + return client.backfill(id, lastId, limit).getBody(); } } diff --git a/rc-service/rc-service-other/docs/Aslan游戏王活动接口.md b/rc-service/rc-service-other/docs/Aslan游戏王活动接口.md index 37d760de..8112da32 100644 --- a/rc-service/rc-service-other/docs/Aslan游戏王活动接口.md +++ b/rc-service/rc-service-other/docs/Aslan游戏王活动接口.md @@ -42,7 +42,7 @@ Query: `body` 为 `Detail`: -- `activity`:活动配置,主要字段为 `id`、`activityCode`、`activityName`、`activityDesc`、`sysOrigin`、`timeZone`、`startTime`、`endTime`、`settlementDelayMinutes`、`settlementTime`、`coinPerDraw`、`eventTypes`、`rankingType`、`rankingPeriod`、`enabled`、`settlementStatus`。 +- `activity`:活动配置,主要字段为 `id`、`activityCode`、`activityName`、`activityDesc`、`sysOrigin`、`timeZone`、`startTime`、`endTime`、`settlementDelayMinutes`、`settlementTime`、`coinPerDraw`、`rankingType`、`rankingPeriod`、`enabled`、`settlementStatus`。兼容字段 `eventTypes` 恒为 `["GAME_EXPENDITURE"]`,不代表可配置白名单。 - `prizes`:7 个奖项;主要字段为 `id`、`prizeName`、`prizeImage`、`weight`、`stock`、`resourceGroupId`、`sortOrder`、`rewardItems`。 - `rankRewards`:固定档位 `1`、`2`、`3`、`4-7`、`8-10`、`11-30` 及其 `resourceGroupId`、`rewardName`、`rewardItems`。 - `supportedRankingTypes`:固定为 `TYCOON`、`VICTORIOUS`。 @@ -89,7 +89,7 @@ Body: | Query 参数 | 必填 | 说明 | | --- | --- | --- | | `activityId` | 否 | 活动 ID。 | -| `rankingType` | 否 | `TYCOON`(游戏支出)或 `VICTORIOUS`(游戏收入);省略时使用活动配置。 | +| `rankingType` | 否 | `TYCOON`(游戏支出)或兼容维度 `VICTORIOUS`;省略时使用活动配置。本活动事件采集只接收支出,因此运营配置固定使用 `TYCOON`。 | | `period` | 否 | `DAILY` 或 `OVERALL`;省略时使用活动配置。 | | `limit` | 否 | 默认 30,服务端限制到 1~100。 | @@ -128,7 +128,7 @@ Body: | `POST /settlement/{id}` | 活动 ID | 到达 `settlementTime` 后冻结 TYCOON/OVERALL Top 30 并发奖。 | | `POST /settlement/retry/{recordId}` | 结算记录 ID | 仅重试已核实可重试的 `FAILED` 记录,不处理 `UNKNOWN`。 | | `POST /delivery-item/resolve/{itemId}` | Query `delivered=true/false` | 只处理 `UNKNOWN` 单项;语义见下文。 | -| `POST /backfill/{id}` | `eventType`、`receiptType` 必填;`lastId`、`limit` 可选 | 分页补算钱包流水,返回 `BackfillResult`。 | +| `POST /backfill/{id}` | `lastId`、`limit` 可选 | 服务端按固定第三方游戏支出口径分页补算钱包流水,返回 `BackfillResult`。 | ### `SaveCommand` 关键字段与约束 @@ -143,13 +143,13 @@ Body: | `startTime`、`endTime` | 必填毫秒时间戳,且 `startTime < endTime`。 | | `settlementDelayMinutes` | 0~1440,默认 30;`settlementTime = endTime + settlementDelayMinutes`。 | | `coinPerDraw` | 必填,正整数金币数。 | -| `eventTypes` | 必填,1~30 个非空字符串;按钱包 `eventType` 精确匹配,单值不能含逗号。 | +| `eventTypes` | 已废弃且可不传;旧客户端传值也会被忽略,服务端统一保存 `GAME_EXPENDITURE` 兼容哨兵。 | | `rankingType` | `TYCOON` 或 `VICTORIOUS`,默认 `TYCOON`。 | | `rankingPeriod` | `DAILY` 或 `OVERALL`,默认 `OVERALL`。 | | `enabled` | 默认 `false`。首次以 `true` 保存时,`startTime` 必须晚于当前时间。 | | `prizes`、`rankRewards` | 可随活动一起保存,也可通过独立接口保存。启用前两者都必须完整。 | -同一 `sysOrigin` 的已启用活动时间窗不得重叠。活动开始后,统计时间、系统、时区、结算延时、`coinPerDraw`、启用状态和 `eventTypes` 等统计口径会锁定。 +同一 `sysOrigin` 的已启用活动时间窗不得重叠。活动开始后,统计时间、系统、时区、结算延时、`coinPerDraw` 和启用状态等统计口径会锁定。事件资格由服务端固定分类器决定,不允许 webconsole 手工配置。 奖项必须恰好 7 个且全部 `enabled=true`,`sortOrder` 为不重复的 1~7,`prizeName`/`prizeImage` 非空,`weight > 0`,`stock >= -1`,并配置 `resourceGroupId`。`stock=-1` 表示无限库存,0 表示耗尽。 @@ -157,9 +157,9 @@ Body: ### 补数返回 -`POST /backfill/{id}` 的 `receiptType`:`0=INCOME/VICTORIOUS`,`1=EXPENDITURE/TYCOON 与抽奖机会`。`limit` 默认 200、最大 500;每个 `eventType + receiptType` 必须分别维护自己的 `lastId`,直到返回 `finished=true`。 +`POST /backfill/{id}` 不接收 `eventType` 或 `receiptType`。`limit` 默认 200、最大 500;调用方只需把本次 `nextLastId` 作为下次 `lastId` 继续请求,直到返回 `finished=true`。钱包侧会强制 `EXPENDITURE`,并以四个转义后的左锚定游戏前缀查询,避免扫描活动时间窗内的全部非游戏支出。 -`BackfillResult` 字段:`eventType`、`receiptType`、`scanned`、`accepted`、`nextLastId`、`finished`。补数查询边界同实时统计,为 `[startTime,endTime)`;调用钱包历史接口时实现为 `endTime - 1ms`。 +`BackfillResult` 字段:`eventType`(恒为 `GAME_EXPENDITURE`)、`receiptType`(恒为 `1/EXPENDITURE`)、`scanned`、`accepted`、`nextLastId`、`finished`。补数查询边界同实时统计,为 `[startTime,endTime)`;调用钱包历史接口时实现为 `endTime - 1ms`。 ## 钱包事件统计契约 @@ -167,15 +167,16 @@ Body: - tag:`wallet_sync_gold_v2`。 - 独立 consumer group:`ASLAN_GAME_KING_WALLET_SYNC_V1`。不得和钱包落库消费者共用 group,否则会竞争消费。 - 事件模型:`WalletReceiptSyncEvent`。使用字段 `assetRecordId`、`userId`、`sysOrigin`、`eventType`、`eventId`、`receiptType`、`bizType`、`amount`、`createTime`。 -- 仅接收 `bizType=1`、金额非 0、`sysOrigin` 与活动相同、`eventType` 在活动白名单中、`createTime` 位于 `[startTime,endTime)` 的事件。 -- `receiptType=EXPENDITURE(1)` 累计 TYCOON 与抽奖机会;`receiptType=INCOME(0)` 累计 VICTORIOUS。 +- 唯一有效分类条件为:`bizType=WalletBizType.GOLD.getType()`、`receiptType=EXPENDITURE(1)`,并且 `eventType` 以 `HOT_GAME_`、`HKYS_GAME_`、`BAISHUN_GAME_`、`YOMI_GAME_` 之一开头;此外金额必须非 0、`sysOrigin` 必须与活动相同、`createTime` 必须位于 `[startTime,endTime)`。 +- 分类器使用严格 `startsWith`,且前缀后必须有具体游戏 ID/标识;裸 `HOT_GAME_` 等前缀不会计入。禁止仅判断“支出”、`contains("GAME")` 或 `GoldOriginMapping`,这些宽泛条件会误收礼物/奖励等流水或遗漏 YOMI。 +- 有效流水只累计 TYCOON 与抽奖机会;`INCOME` 不进入该活动统计。 - 实时 MQ、MQ 重投和后台补数共用唯一幂等键 `(activity_id, asset_record_id)`。 - DAILY 按活动 `timeZone` 落日表;OVERALL 使用全活动累计。活动结算奖励固定按 TYCOON/OVERALL,而不是当前 H5 正在查看的榜单维度。 ## 结算截点与发奖核账 - 默认在 `endTime + 30 分钟` 到达结算截点;后台可通过 `settlementDelayMinutes` 配置 0~1440 分钟。定时任务每 5 分钟扫描一次到期活动。 -- 统计有效时间仍只到 `endTime`(不含);30 分钟只是给异步消息到达留出的缓冲,不代表 RocketMQ 已提供“无更早消息”的 watermark。要求严格完整时,应在截点前按全部配置的 `eventType × receiptType` 完成补数,再执行结算。 +- 统计有效时间仍只到 `endTime`(不含);30 分钟只是给异步消息到达留出的缓冲,不代表 RocketMQ 已提供“无更早消息”的 watermark。要求严格完整时,应在截点前按 `nextLastId` 连续调用固定口径补数直至 `finished=true`,再执行结算。 - 首次结算会在活动行排他锁内冻结 TYCOON/OVERALL Top 30;已进入统计事务会先完成,冻结后到达的 MQ 或补数会被拒绝,排名快照不会再漂移。 - 每个奖励组拆成 `deliveryItems` 逐项发放。普通失败可标为 `FAILED` 并在运营核实后走 retry;外部适配器抛错,或 `PROCESSING` 超过 10 分钟租期时,结果不能判定是否已到账,因此标为 `UNKNOWN`。 - `UNKNOWN` 永远不会被定时任务或普通 retry 盲目补发。运营核对下游账目后调用 `/delivery-item/resolve/{itemId}`:`delivered=true` 表示确认已到账,仅把该项记为成功且不再次发送;`delivered=false` 表示确认未到账,原子解锁父记录与该项后再补发。 @@ -185,6 +186,6 @@ Body: - 测试环境:`.deploy/test-deploy/sql/20260717_aslan_game_king.sql` - 生产环境:`.deploy/prod-deploy/sql/20260717_aslan_game_king.sql` -- 两份 SQL 内容应保持字节一致。迁移创建 9 张独立活动表,并登记 webconsole 菜单/资源。 -- SQL 文件开头已记录性能评估:实时写路径使用唯一键/主键聚合,榜单直接读取维度索引,恢复与到期扫描均有状态时间索引;迁移不回填钱包流水,也不扫描更新既有业务大表。 +- 两份 SQL 内容应保持字节一致。迁移创建 9 张独立活动表,并登记 webconsole 菜单/资源。活动表 `event_types` 只为兼容旧服务保留,默认且只写 `GAME_EXPENDITURE`,真实流水的 `ledger.event_type` 仍保留用于审计。 +- SQL 文件开头已记录性能评估:实时写路径使用唯一键/主键聚合,榜单直接读取维度索引,恢复与到期扫描均有状态时间索引;补数绑定 `sys_origin + EXPENDITURE` 并使用四个转义后的左锚定前缀,利用钱包 `(sys_origin,event_type,create_time)` 索引。迁移不回填钱包流水,也不扫描更新既有业务大表。 - 当前交付只新增了迁移文件,**尚未在测试或生产数据库执行**。执行前仍需按环境发布流程复核目标库、变更窗口和执行计划。 diff --git a/rc-service/rc-service-other/other-application/pom.xml b/rc-service/rc-service-other/other-application/pom.xml index 80f1eead..0571f848 100644 --- a/rc-service/rc-service-other/other-application/pom.xml +++ b/rc-service/rc-service-other/other-application/pom.xml @@ -23,6 +23,13 @@ component-game + + + com.red.circle + wallet-domain + ${project.version} + + com.red.circle live-inner-model diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/AslanGameKingWalletListener.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/AslanGameKingWalletListener.java index 430ba4bd..98ec129d 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/AslanGameKingWalletListener.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/AslanGameKingWalletListener.java @@ -12,7 +12,7 @@ import com.red.circle.other.app.service.activity.aslan.AslanGameKingServiceImpl; import lombok.RequiredArgsConstructor; /** - * Aslan 游戏王钱包收支监听器。 + * Aslan 游戏王第三方游戏金币消费监听器。 * *

使用独立 consumer group 订阅 wallet_sync_gold_v2,不能与钱包流水落库消费者共用 * group;否则两者会竞争消息,导致活动漏统计。业务层仍以活动+资产流水唯一键兜底 MQ 重投。

@@ -31,7 +31,7 @@ public class AslanGameKingWalletListener implements MessageListener { public Action consume(ConsumerMessage message) { return messageEventProcess.consume( MessageEventProcessDescribe.builder() - .logTag("Aslan游戏王收支统计") + .logTag("Aslan游戏王第三方游戏消费统计") .consumeMsgTimeoutMinute(60) .repeatConsumeMinute(30) .repeatConsumeTag("AslanGameKingWalletSyncV1") diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingServiceImpl.java index c4f8e301..d8a0aa60 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingServiceImpl.java @@ -42,6 +42,7 @@ import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.other.inner.model.dto.material.PropsActivityRewardConfigInfoDTO; import com.red.circle.tool.core.sequence.IdWorkerUtils; import com.red.circle.tool.core.text.StringUtils; +import com.red.circle.wallet.domain.wallet.WalletBizType; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; import com.red.circle.wallet.inner.model.cmd.UserGoldRunningWaterBackQryCmd; import com.red.circle.wallet.inner.model.dto.UserGoldRunningWaterHistoryDTO; @@ -53,10 +54,8 @@ import java.time.LocalDate; import java.time.ZoneId; import java.time.DateTimeException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -85,6 +84,10 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { private static final String RANKING_VICTORIOUS = "VICTORIOUS"; private static final String PERIOD_DAILY = "DAILY"; private static final String PERIOD_OVERALL = "OVERALL"; + /** 对外兼容字段与数据库列只保存此哨兵;运营不能自行扩大钱包事件口径。 */ + private static final String FIXED_EVENT_SCOPE = "GAME_EXPENDITURE"; + private static final List THIRD_PARTY_GAME_EVENT_PREFIXES = List.of( + "HOT_GAME_", "HKYS_GAME_", "BAISHUN_GAME_", "YOMI_GAME_"); private static final String DEFAULT_TIME_ZONE = "Asia/Riyadh"; private static final int DEFAULT_SETTLEMENT_DELAY_MINUTES = 30; private static final int MAX_SETTLEMENT_DELAY_MINUTES = 1440; @@ -238,11 +241,9 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { Timestamp eventTime = new Timestamp(event.getCreateTime().getTime()); BigDecimal amount = event.getAmount().getDollarAmount().abs(); for (ActivityRow activity : mapper.findActiveActivities(event.getSysOrigin(), eventTime)) { - if (!splitEventTypes(activity.getEventTypes()).contains(event.getEventType())) { - continue; - } recordConsumption(activity, event.getAssetRecordId(), event.getUserId(), event.getSysOrigin(), - event.getEventType(), event.getEventId(), event.getReceiptType(), amount, eventTime); + event.getBizType(), event.getEventType(), event.getEventId(), event.getReceiptType(), + amount, eventTime); } } @@ -255,21 +256,32 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { && event.getAmount().getDollarAmount() != null && event.getAmount().getDollarAmount().signum() != 0 && event.getSysOrigin() != null - && event.getEventType() != null - // wallet_sync_gold_v2 当前由金币网关发布;bizType=1 再做一次防御,避免未来复用 topic 时误计。 - && Objects.equals(event.getBizType(), 1) - && (ReceiptType.EXPENDITURE.eq(event.getReceiptType()) - || ReceiptType.INCOME.eq(event.getReceiptType())); + && isThirdPartyGameExpenditure(event.getBizType(), event.getReceiptType(), + event.getEventType()); + } + + /** + * 唯一权威的游戏消费分类器。三个条件必须同时成立:金币业务、支出单据、四个第三方 + * 游戏目录之一。禁止 contains("GAME") 或 GoldOriginMapping,因为二者会把奖励/礼物等 + * 非下注流水混入活动,且旧映射并不完整包含 YOMI。 + */ + private boolean isThirdPartyGameExpenditure(Integer bizType, ReceiptType receiptType, + String eventType) { + return Objects.equals(bizType, WalletBizType.GOLD.getType()) + && ReceiptType.EXPENDITURE.eq(receiptType) + && StringUtils.isNotBlank(eventType) + // 裸前缀不是具体游戏事件,必须在前缀后继续包含游戏 ID/标识。 + && THIRD_PARTY_GAME_EVENT_PREFIXES.stream() + .anyMatch(prefix -> eventType.length() > prefix.length() && eventType.startsWith(prefix)); } private boolean recordConsumption(ActivityRow activity, Long assetRecordId, Long userId, - String sysOrigin, String eventType, String eventId, ReceiptType receiptType, + String sysOrigin, Integer bizType, String eventType, String eventId, ReceiptType receiptType, BigDecimal amount, Timestamp eventTime) { if (assetRecordId == null || userId == null || eventTime == null || amount == null || amount.signum() <= 0 || !Objects.equals(activity.getSysOrigin(), sysOrigin) - || !splitEventTypes(activity.getEventTypes()).contains(eventType) - || eventTime.before(activity.getStartTime()) || !eventTime.before(activity.getEndTime()) - || !(ReceiptType.EXPENDITURE.eq(receiptType) || ReceiptType.INCOME.eq(receiptType))) { + || !isThirdPartyGameExpenditure(bizType, receiptType, eventType) + || eventTime.before(activity.getStartTime()) || !eventTime.before(activity.getEndTime())) { return false; } Date statDate = Date.valueOf(eventTime.toInstant() @@ -290,17 +302,10 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { } mapper.ensureUser(activity.getId(), userId); mapper.ensureDailyUser(activity.getId(), statDate, userId); - if (ReceiptType.EXPENDITURE.eq(receiptType)) { - ResponseAssert.isTrue(CommonErrorCode.UPDATE_FAILURE, - mapper.incrementUserConsumption(activity.getId(), userId, amount, eventTime) == 1 - && mapper.incrementDailyConsumption(activity.getId(), statDate, userId, amount, - eventTime) == 1); - } else { - ResponseAssert.isTrue(CommonErrorCode.UPDATE_FAILURE, - mapper.incrementUserWinning(activity.getId(), userId, amount, eventTime) == 1 - && mapper.incrementDailyWinning(activity.getId(), statDate, userId, amount, - eventTime) == 1); - } + ResponseAssert.isTrue(CommonErrorCode.UPDATE_FAILURE, + mapper.incrementUserConsumption(activity.getId(), userId, amount, eventTime) == 1 + && mapper.incrementDailyConsumption(activity.getId(), statDate, userId, amount, + eventTime) == 1); return true; }); return Boolean.TRUE.equals(inserted); @@ -582,22 +587,13 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { } @Override - public BackfillResult backfill(Long activityId, String eventType, Integer receiptType, - Long lastId, Integer limit) { + public BackfillResult backfill(Long activityId, Long lastId, Integer limit) { ActivityRow activity = requireActivity(activityId); - ResponseAssert.isTrue(CommonErrorCode.REQUIRED_FIELDS_CANNOT_BE_IGNORED, - StringUtils.isNotBlank(eventType)); - ResponseAssert.isTrue(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE, - splitEventTypes(activity.getEventTypes()).contains(eventType)); - ResponseAssert.isTrue(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE, - Objects.equals(receiptType, ReceiptType.EXPENDITURE.getType()) - || Objects.equals(receiptType, ReceiptType.INCOME.getType())); - ReceiptType requestedReceiptType = ReceiptType.toEnum(receiptType); int safeLimit = clamp(limit, 200, MAX_BACKFILL_LIMIT); UserGoldRunningWaterBackQryCmd query = new UserGoldRunningWaterBackQryCmd() .setSysOrigin(activity.getSysOrigin()) - .setType(requestedReceiptType.getType()) - .setOrigin(eventType) + .setType(ReceiptType.EXPENDITURE.getType()) + .setQueryThirdPartyGameExpenditure(true) .setStartTime(activity.getStartTime().getTime()) // 钱包历史接口使用 <= endTime;减 1ms 与实时监听的 [start,end) 边界保持一致。 .setEndTime(Math.subtractExact(activity.getEndTime().getTime(), 1L)) @@ -608,17 +604,24 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { .orElse(Collections.emptyList()); int accepted = 0; for (UserGoldRunningWaterHistoryDTO record : records) { + ReceiptType recordReceiptType = Objects.equals(record.getType(), + ReceiptType.EXPENDITURE.getType()) ? ReceiptType.EXPENDITURE : null; + // 钱包查询已用索引前缀收窄,这里仍复用实时 MQ 的分类器做第二道防线,防止查询契约漂移。 + if (!isThirdPartyGameExpenditure(WalletBizType.GOLD.getType(), recordReceiptType, + record.getOrigin())) { + continue; + } Timestamp eventTime = new Timestamp(record.getCreateTime()); if (recordConsumption(activity, record.getId(), record.getUserId(), record.getSysOrigin(), - record.getOrigin(), record.getTrackId(), requestedReceiptType, + WalletBizType.GOLD.getType(), record.getOrigin(), record.getTrackId(), recordReceiptType, record.getQuantity().abs(), eventTime)) { accepted++; } } Long nextLastId = records.isEmpty() ? null : records.get(records.size() - 1).getId(); return new BackfillResult() - .setEventType(eventType) - .setReceiptType(requestedReceiptType.getType()) + .setEventType(FIXED_EVENT_SCOPE) + .setReceiptType(ReceiptType.EXPENDITURE.getType()) .setScanned(records.size()) .setAccepted(accepted) .setNextLastId(nextLastId) @@ -813,7 +816,6 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { || RANKING_VICTORIOUS.equals(cmd.getRankingType())) && (PERIOD_OVERALL.equals(Optional.ofNullable(cmd.getRankingPeriod()) .orElse(PERIOD_OVERALL)) || PERIOD_DAILY.equals(cmd.getRankingPeriod()))); - normalizeEventTypes(cmd.getEventTypes()); if (cmd.getPrizes() != null) { validatePrizes(cmd.getPrizes(), true, cmd.getSysOrigin()); } @@ -826,7 +828,8 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { if (existing == null || existing.getStartTime().after(Timestamp.from(Instant.now()))) { return; } - // 活动一旦开始,资格分母、统计起点、系统与精确事件白名单均不可改变,否则历史流水无法重算。 + // 活动一旦开始,资格分母、统计起点和系统不可改变,否则已累计的消费流水无法重算。 + // 事件口径由服务端分类器固定,不再比较旧库 CSV;保存时会把兼容列收敛为统一哨兵。 boolean lockedFieldsSame = Objects.equals(existing.getStartTime().getTime(), cmd.getStartTime()) && Objects.equals(existing.getEndTime().getTime(), cmd.getEndTime()) && Objects.equals(existing.getCoinPerDraw(), cmd.getCoinPerDraw()) @@ -834,9 +837,7 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { && Objects.equals(existing.getTimeZone(), normalizeTimeZone(cmd.getTimeZone())) && Objects.equals(existing.getSettlementDelayMinutes(), normalizeSettlementDelay(cmd.getSettlementDelayMinutes())) - && Objects.equals(existing.getEnabled(), Boolean.TRUE.equals(cmd.getEnabled())) - && Objects.equals(existing.getEventTypes(), String.join(",", - normalizeEventTypes(cmd.getEventTypes()))); + && Objects.equals(existing.getEnabled(), Boolean.TRUE.equals(cmd.getEnabled())); ResponseAssert.isTrue(CommonErrorCode.STATE_ERROR, lockedFieldsSame); } @@ -1036,7 +1037,7 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { .setSettlementDelayMinutes(row.getSettlementDelayMinutes()) .setSettlementTime(row.getSettlementTime().getTime()) .setCoinPerDraw(row.getCoinPerDraw()) - .setEventTypes(splitEventTypes(row.getEventTypes())) + .setEventTypes(List.of(FIXED_EVENT_SCOPE)) .setRankingType(row.getRankingType()) .setRankingPeriod(row.getRankingPeriod()) .setEnabled(row.getEnabled()) @@ -1059,7 +1060,7 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { .setSettlementDelayMinutes(settlementDelayMinutes) .setSettlementTime(buildSettlementTime(cmd.getEndTime(), settlementDelayMinutes)) .setCoinPerDraw(cmd.getCoinPerDraw()) - .setEventTypes(String.join(",", normalizeEventTypes(cmd.getEventTypes()))) + .setEventTypes(FIXED_EVENT_SCOPE) .setRankingType(Optional.ofNullable(cmd.getRankingType()).orElse(RANKING_TYCOON)) .setRankingPeriod(Optional.ofNullable(cmd.getRankingPeriod()).orElse(PERIOD_OVERALL)) .setEnabled(Boolean.TRUE.equals(cmd.getEnabled())) @@ -1218,31 +1219,6 @@ public class AslanGameKingServiceImpl implements AslanGameKingService { .orElseThrow(() -> new IllegalStateException("Missing rank reward for rank " + rank)); } - private List normalizeEventTypes(List eventTypes) { - ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR, - eventTypes != null && !eventTypes.isEmpty()); - LinkedHashSet normalized = new LinkedHashSet<>(); - for (String eventType : eventTypes) { - String value = eventType == null ? "" : eventType.trim(); - // CSV 是内部存储格式,因此拒绝逗号;其余字符保持原样以做钱包 eventType 精确匹配。 - ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR, - StringUtils.isNotBlank(value) && value.length() <= 64 && !value.contains(",")); - normalized.add(value); - } - ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR, normalized.size() == eventTypes.size()); - return new ArrayList<>(normalized); - } - - private List splitEventTypes(String eventTypes) { - if (StringUtils.isBlank(eventTypes)) { - return Collections.emptyList(); - } - return Arrays.stream(eventTypes.split(",")) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .toList(); - } - private String normalizeTimeZone(String timeZone) { return StringUtils.isBlank(timeZone) ? DEFAULT_TIME_ZONE : timeZone.trim(); } diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingService.java index d7df5c4c..8b5796c7 100644 --- a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingService.java +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/aslan/AslanGameKingService.java @@ -63,6 +63,5 @@ public interface AslanGameKingService { void resolveUnknownDeliveryItem(Long itemId, boolean delivered); - BackfillResult backfill(Long activityId, String eventType, Integer receiptType, Long lastId, - Integer limit); + BackfillResult backfill(Long activityId, Long lastId, Integer limit); } diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/model/activity/aslan/AslanGameKingRows.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/model/activity/aslan/AslanGameKingRows.java index 7e1abdd6..5f4c26fc 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/model/activity/aslan/AslanGameKingRows.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/model/activity/aslan/AslanGameKingRows.java @@ -8,8 +8,8 @@ import lombok.experimental.Accessors; /** * Aslan 游戏王活动数据库行模型。 * - *

这些类型只在持久层使用,尤其把事件白名单保留为规范化 CSV;对外契约仍使用 - * List,防止存储格式泄漏到 H5 与 webconsole。

+ *

这些类型只在持久层使用。eventTypes 列仅保存固定的 GAME_EXPENDITURE 兼容哨兵; + * 真实事件资格由应用层按钱包业务类型、收支类型和第三方游戏前缀统一判定,不能由运营配置。

*/ public final class AslanGameKingRows { diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/activity/AslanGameKingManageClientEndpoint.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/activity/AslanGameKingManageClientEndpoint.java index f2130661..e89cbc6b 100644 --- a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/activity/AslanGameKingManageClientEndpoint.java +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/activity/AslanGameKingManageClientEndpoint.java @@ -119,9 +119,7 @@ public class AslanGameKingManageClientEndpoint implements AslanGameKingManageCli } @Override - public ResultResponse backfill(Long id, String eventType, Integer receiptType, - Long lastId, Integer limit) { - return ResultResponse.success( - aslanGameKingService.backfill(id, eventType, receiptType, lastId, limit)); + public ResultResponse backfill(Long id, Long lastId, Integer limit) { + return ResultResponse.success(aslanGameKingService.backfill(id, lastId, limit)); } } diff --git a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/wallet/impl/WalletGoldClientServiceImpl.java b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/wallet/impl/WalletGoldClientServiceImpl.java index 483a7911..b34d4d1e 100644 --- a/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/wallet/impl/WalletGoldClientServiceImpl.java +++ b/rc-service/rc-service-wallet/wallet-inner-endpoint/src/main/java/com/red/circle/wallet/inner/service/wallet/impl/WalletGoldClientServiceImpl.java @@ -2,6 +2,7 @@ package com.red.circle.wallet.inner.service.wallet.impl; import com.google.common.collect.Lists; import com.red.circle.common.business.core.enums.OpUserType; +import com.red.circle.common.business.core.enums.ReceiptType; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; import com.red.circle.component.tencent.cls.request.ClsLogSearchQry; import com.red.circle.component.tencent.cls.response.ClsLogSearchResult; @@ -61,6 +62,14 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class WalletGoldClientServiceImpl implements WalletGoldClientService { + /** + * MySQL LIKE 中下划线本身是单字符通配符,所以这里必须先用反斜线转义;MyBatis-Plus + * 再追加右侧百分号后得到形如 HOT\_GAME\_% 的左锚定范围。该条件能继续利用 + * (sys_origin,event_type,create_time) 复合索引,避免活动补数退化成全平台支出扫描。 + */ + private static final List THIRD_PARTY_GAME_ORIGIN_LIKE_PREFIXES = List.of( + "HOT\\_GAME\\_", "HKYS\\_GAME\\_", "BAISHUN\\_GAME\\_", "YOMI\\_GAME\\_"); + private final ClsService clsService; private final EnvProperties envProperties; private final WalletGoldGateway walletGoldGateway; @@ -149,6 +158,11 @@ public class WalletGoldClientServiceImpl implements WalletGoldClientService { @Override public List listWater(UserGoldRunningWaterBackQryCmd cmd) { List querySysOrigin = listWaterQuerySysOrigin(cmd); + boolean queryThirdPartyGameExpenditure = + Boolean.TRUE.equals(cmd.getQueryThirdPartyGameExpenditure()); + // 固定模式忽略调用方传入的 type/origin,服务端强制收窄为游戏金币支出,不能被请求放宽。 + Integer queryType = queryThirdPartyGameExpenditure + ? ReceiptType.EXPENDITURE.getType() : cmd.getType(); LambdaQueryWrapperChain queryChain = walletGoldAssetRecordService.query(); @@ -165,12 +179,21 @@ public class WalletGoldClientServiceImpl implements WalletGoldClientService { .eq(Objects.nonNull(cmd.getId()), WalletGoldAssetRecord::getId, cmd.getId()) .eq(Objects.nonNull(cmd.getQueryBackOperationUser()), WalletGoldAssetRecord::getOpUserType, cmd.getQueryBackOperationUser()) - .eq(Objects.nonNull(cmd.getType()), WalletGoldAssetRecord::getType, cmd.getType()) + .eq(Objects.nonNull(queryType), WalletGoldAssetRecord::getType, queryType) .eq(Objects.nonNull(cmd.getUserId()), WalletGoldAssetRecord::getUserId, cmd.getUserId()) .eq(StringUtils.isNotBlank(cmd.getTrackId()), WalletGoldAssetRecord::getEventId, cmd.getTrackId()) - .eq(StringUtils.isNotBlank(cmd.getOrigin()), WalletGoldAssetRecord::getEventType, - cmd.getOrigin()) + .eq(!queryThirdPartyGameExpenditure && StringUtils.isNotBlank(cmd.getOrigin()), + WalletGoldAssetRecord::getEventType, cmd.getOrigin()) + .and(queryThirdPartyGameExpenditure, origins -> origins + .likeRight(WalletGoldAssetRecord::getEventType, + THIRD_PARTY_GAME_ORIGIN_LIKE_PREFIXES.get(0)) + .or().likeRight(WalletGoldAssetRecord::getEventType, + THIRD_PARTY_GAME_ORIGIN_LIKE_PREFIXES.get(1)) + .or().likeRight(WalletGoldAssetRecord::getEventType, + THIRD_PARTY_GAME_ORIGIN_LIKE_PREFIXES.get(2)) + .or().likeRight(WalletGoldAssetRecord::getEventType, + THIRD_PARTY_GAME_ORIGIN_LIKE_PREFIXES.get(3))) .lt(Objects.nonNull(cmd.getLastId()), WalletGoldAssetRecord::getId, cmd.getLastId()) .orderByDesc(WalletGoldAssetRecord::getId) .last(Objects.nonNull(cmd.getLimit()) && cmd.getLimit() > 0 ? PageConstant.formatLimit(