新增游戏上报接口

This commit is contained in:
tianfeng 2026-03-11 11:16:16 +08:00
parent 1cbc7d5d29
commit e55f3b909a
5 changed files with 341 additions and 0 deletions

View File

@ -1,6 +1,8 @@
package com.red.circle.other.adapter.app.game.party3rd;
import com.red.circle.component.game.baishun.GameBaishunService;
import com.red.circle.other.app.dto.cmd.game.GameBaishunReportCmd;
import com.red.circle.other.app.service.game.override.service.GameBaishunReportService;
import com.red.circle.component.game.baishun.request.BaishunChangeCurrencyRequest;
import com.red.circle.component.game.baishun.request.BaishunTokenRequest;
import com.red.circle.component.game.baishun.request.BaishunTokenUpdateRequest;
@ -32,6 +34,7 @@ import org.springframework.web.bind.annotation.RestController;
public class GameBaishunRestController extends BaseController {
private final GameBaishunService gameBaishunService;
private final GameBaishunReportService gameBaishunReportService;
/**
* 获取 SSToken: BAISHUN 服务器端获取调期令牌ss_token.
@ -69,4 +72,13 @@ public class GameBaishunRestController extends BaseController {
return gameBaishunService.changeBalance(request);
}
/**
* 游戏状态上报game_start / game_settle.
*/
@IgnoreResultResponse
@PostMapping("/report")
public BaishunResponse<Void> report(@RequestBody @Validated GameBaishunReportCmd cmd) {
return gameBaishunReportService.report(cmd);
}
}

View File

@ -0,0 +1,165 @@
package com.red.circle.other.app.command.game;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.red.circle.auth.inner.endpoint.AuthClient;
import com.red.circle.component.game.baishun.error.BaishunErrorCode;
import com.red.circle.component.game.baishun.props.GameBaishunProperties;
import com.red.circle.component.game.baishun.request.BaishunUserProfileRequest;
import com.red.circle.component.game.baishun.response.BaishunResponse;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.security.UserCredential;
import com.red.circle.other.app.dto.cmd.game.GameBaishunReportCmd;
import com.red.circle.other.app.dto.cmd.game.GameBaishunReportCmd.GameSettleInfo;
import com.red.circle.other.app.dto.cmd.game.GameBaishunReportCmd.GameStartInfo;
import com.red.circle.other.app.dto.cmd.game.GameBaishunReportCmd.PlayerResultInfo;
import com.red.circle.tool.core.parse.DataTypeUtils;
import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd;
import com.red.circle.wallet.inner.model.enums.GoldOrigin;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Baishun游戏状态上报处理.
*
* @author tf
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GameBaishunReportCmdExe {
private static final String REPORT_TYPE_START = "game_start";
private static final String REPORT_TYPE_SETTLE = "game_settle";
private final ObjectMapper objectMapper;
private final GameBaishunProperties gameBaishunProperties;
private final WalletGoldClient walletGoldClient;
private final AuthClient authClient;
public BaishunResponse<Void> execute(GameBaishunReportCmd cmd) {
if (!checkTokenStrict(cmd)) {
log.error("BaishunReportCmdExe execute checkTokenStrict fail !");
}
log.info("Baishun game report: type={}, userId={}", cmd.getReportType(), cmd.getUserId());
log.error("Baishun game report: {}", JSONObject.toJSONString(cmd));
/*switch (cmd.getReportType()) {
case REPORT_TYPE_START:
return handleGameStart(cmd);
case REPORT_TYPE_SETTLE:
return handleGameSettle(cmd);
default:
log.warn("Unknown report_type: {}", cmd.getReportType());
return BaishunResponse.fail(BaishunErrorCode.DATE_ERROR);
}*/
return BaishunResponse.success(null);
}
private BaishunResponse<Void> handleGameStart(GameBaishunReportCmd cmd) {
GameStartInfo startInfo = parseReportMsg(cmd.getReportMsg(), GameStartInfo.class);
if (startInfo == null) {
return BaishunResponse.fail(BaishunErrorCode.READ_DATE_ERROR);
}
// 扣除开局 bet非AI玩家
if (startInfo.getPlayers() != null) {
for (GameBaishunReportCmd.PlayerStartInfo player : startInfo.getPlayers()) {
if (Objects.equals(player.getIsAi(), 1) || player.getBet() == null || player.getBet() <= 0) {
continue;
}
try {
Long userId = DataTypeUtils.toLong(player.getUserId());
walletGoldClient.changeBalance(GoldReceiptCmd.builder()
.appExpenditure()
.userId(userId)
.sysOrigin(resolveTokenSysOrigin(cmd.getSsToken()))
.origin(GoldOrigin.BAISHUN_GAME.name())
.originDescribe(GoldOrigin.BAISHUN_GAME.getDesc())
.eventId(startInfo.getGameRoundId() + "_" + userId + "_start")
.amount(BigDecimal.valueOf(player.getBet()))
.build());
} catch (Exception e) {
log.error("Baishun game start deduct failed: userId={}, roundId={}", player.getUserId(), startInfo.getGameRoundId(), e);
}
}
}
return BaishunResponse.success(null);
}
private BaishunResponse<Void> handleGameSettle(GameBaishunReportCmd cmd) {
GameSettleInfo settleInfo = parseReportMsg(cmd.getReportMsg(), GameSettleInfo.class);
if (settleInfo == null) {
return BaishunResponse.fail(BaishunErrorCode.READ_DATE_ERROR);
}
List<PlayerResultInfo> players = settleInfo.getPlayers();
if (players == null || players.isEmpty()) {
return BaishunResponse.success(null);
}
for (PlayerResultInfo player : players) {
if (Objects.equals(player.getIsAi(), 1) || player.getReward() == null || player.getReward() <= 0) {
continue;
}
try {
Long userId = DataTypeUtils.toLong(player.getUserId());
walletGoldClient.changeBalance(GoldReceiptCmd.builder()
.appIncome()
.userId(userId)
.sysOrigin(resolveTokenSysOrigin(cmd.getSsToken()))
.origin(GoldOrigin.BAISHUN_GAME.name())
.originDescribe(GoldOrigin.BAISHUN_GAME.getDesc())
.eventId(settleInfo.getGameRoundId() + "_" + userId + "_settle")
.amount(BigDecimal.valueOf(player.getReward()))
.build());
} catch (Exception e) {
log.error("Baishun game settle reward failed: userId={}, roundId={}", player.getUserId(), settleInfo.getGameRoundId(), e);
}
}
return BaishunResponse.success(null);
}
private <T> T parseReportMsg(Object reportMsg, Class<T> clazz) {
try {
return objectMapper.convertValue(reportMsg, clazz);
} catch (Exception e) {
log.error("Failed to parse report_msg to {}: {}", clazz.getSimpleName(), reportMsg, e);
return null;
}
}
private String resolveTokenSysOrigin(String ssToken) {
try {
return com.red.circle.framework.core.security.UserCredential.parseToken(ssToken).getSysOrigin();
} catch (Exception e) {
return "LIKEI";
}
}
private boolean checkTokenStrict(GameBaishunReportCmd request) {
UserCredential accessToken = ResponseAssert.requiredSuccess(
authClient.getUserCredential(DataTypeUtils.toLong(request.getUserId())));
if (Objects.isNull(accessToken)) {
return false;
}
if (!Objects.equals(accessToken.getUserId(), DataTypeUtils.toLong(request.getUserId()))) {
return false;
}
return Objects.equals(accessToken.token(), request.getSsToken());
}
}

View File

@ -0,0 +1,25 @@
package com.red.circle.other.app.service.game;
import com.red.circle.component.game.baishun.response.BaishunResponse;
import com.red.circle.other.app.command.game.GameBaishunReportCmdExe;
import com.red.circle.other.app.dto.cmd.game.GameBaishunReportCmd;
import com.red.circle.other.app.service.game.override.service.GameBaishunReportService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author tf
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class GameBaishunReportServiceImpl implements GameBaishunReportService {
private final GameBaishunReportCmdExe gameBaishunReportCmdExe;
@Override
public BaishunResponse<Void> report(GameBaishunReportCmd cmd) {
return gameBaishunReportCmdExe.execute(cmd);
}
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.app.service.game.override.service;
import com.red.circle.other.app.dto.cmd.game.GameBaishunReportCmd;
import com.red.circle.component.game.baishun.response.BaishunResponse;
/**
* Baishun游戏状态上报服务.
*
* @author tf
*/
public interface GameBaishunReportService {
BaishunResponse<Void> report(GameBaishunReportCmd cmd);
}

View File

@ -0,0 +1,125 @@
package com.red.circle.other.app.dto.cmd.game;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import lombok.Data;
/**
* Baishun游戏状态上报.
*
* @author tf
*/
@Data
public class GameBaishunReportCmd {
/** 上报类型: game_start / game_settle */
@NotBlank
@JsonProperty("report_type")
private String reportType;
/** 上报数据体(对应 game_start_info 或 game_settle_info */
@NotNull
@JsonProperty("report_msg")
private Object reportMsg;
/** 用户ID */
@NotBlank
@JsonProperty("user_id")
private String userId;
/** 用户ss_token */
@NotBlank
@JsonProperty("ss_token")
private String ssToken;
// -------------------------------------------------------------------------
// 内嵌数据结构
// -------------------------------------------------------------------------
/** 游戏开始上报数据 */
@Data
public static class GameStartInfo {
@JsonProperty("game_id")
private Integer gameId;
@JsonProperty("room_id")
private String roomId;
@JsonProperty("game_round_id")
private String gameRoundId;
/** 开始时间戳(毫秒) */
@JsonProperty("start_at")
private Long startAt;
private List<PlayerStartInfo> players;
/** 货币类型app定义非必填 */
@JsonProperty("currency_type")
private Integer currencyType;
}
/** 游戏结算上报数据 */
@Data
public static class GameSettleInfo {
@JsonProperty("game_id")
private Integer gameId;
@JsonProperty("room_id")
private String roomId;
@JsonProperty("game_round_id")
private String gameRoundId;
/** 开始时间戳(毫秒) */
@JsonProperty("start_at")
private Long startAt;
/** 结束时间戳(毫秒) */
@JsonProperty("end_at")
private Long endAt;
private List<PlayerResultInfo> players;
/** 货币类型app定义非必填 */
@JsonProperty("currency_type")
private Integer currencyType;
}
/** 开局玩家信息 */
@Data
public static class PlayerStartInfo {
@JsonProperty("user_id")
private String userId;
/** 0: 玩家, 1: 机器人 */
@JsonProperty("is_ai")
private Integer isAi;
/** 扣除数量 */
private Integer bet;
}
/** 结算玩家信息 */
@Data
public static class PlayerResultInfo {
@JsonProperty("user_id")
private String userId;
/** 0: 玩家, 1: 机器人 */
@JsonProperty("is_ai")
private Integer isAi;
private Integer rank;
private Integer reward;
private Integer score;
}
}