yomi v4 integration

This commit is contained in:
tianfeng 2026-05-26 18:29:58 +08:00
parent 1931c70552
commit 71b2157b5b
8 changed files with 229 additions and 278 deletions

View File

@ -104,16 +104,12 @@ public class YomiGameRestController extends BaseController {
return YomiResponseWrapper.success(result);
} catch (ResponseException e) {
log.error("yomi changeBalance 业务异常: recordId={}, error={}",
e.getMessage(), e.getMessage());
log.error("yomi changeBalance 业务异常: error={}", e.getMessage());
if (Objects.equals(e.getErrorCode(), WalletErrorCode.INSUFFICIENT_BALANCE.getCode())) {
return YomiResponseWrapper.fail(
WalletErrorCode.INSUFFICIENT_BALANCE.getCode(),
WalletErrorCode.INSUFFICIENT_BALANCE.getMessage()
);
// v4协议余额不足返回10003
return YomiResponseWrapper.fail(10003, "gold not enough");
}
return YomiResponseWrapper.fail(500, e.getMessage());
return YomiResponseWrapper.fail(9998, e.getMessage());
} catch (Exception e) {
log.error("yomi changeBalance 系统异常: {}", e.getMessage(), e);
return YomiResponseWrapper.fail(500, e.getMessage());

View File

@ -47,8 +47,6 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@ -59,7 +57,10 @@ import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
public class YomiGameServiceImpl implements YomiGameService {
private static final String IDEMPOTENT_KEY_PREFIX = "yomi:change_balance:req:";
private static final long IDEMPOTENT_EXPIRE_SECONDS = 300L;
private final YomiConfig yomiConfig;
private final RedisTemplate<String, String> redisTemplate;
private final AuthClient authClient;
@ -75,129 +76,152 @@ public class YomiGameServiceImpl implements YomiGameService {
private final ActivityRechargeTicketService activityRechargeTicketService;
private final RoomDailyTaskProgressService roomDailyTaskProgressService;
private final RedisService redisService;
@Override
public YomiTokenCO getToken(YomiGetTokenCmd cmd) {
log.info("yomi获取token: userId={}, gameId={}", cmd.getUserId(), cmd.getGameId());
Long userId = Long.parseLong(cmd.getPlatUserId());
log.info("yomi获取token: platUserId={}, gameUid={}", userId, cmd.getGameUid());
try {
// 调用认证服务获取用户凭证
ResultResponse<UserCredential> response = authClient.getUserCredential(Long.parseLong(cmd.getUserId()));
UserCredential credential = response.getBody();
if (credential == null) {
ResultResponse<UserCredential> credResp = authClient.getUserCredential(userId);
if (credResp == null || !credResp.checkSuccess() || credResp.getBody() == null) {
throw new RuntimeException("用户不存在");
}
// 生成系统标准token
String token = credential.token();
// 过期时间(24小时后)
Long expireDate = System.currentTimeMillis() / 1000 + yomiConfig.getTokenExpireSeconds();
// 缓存token-userId映射
String cacheKey = buildTokenCacheKey(token);
redisTemplate.opsForValue().set(cacheKey, cmd.getUserId(),
yomiConfig.getTokenExpireSeconds(), TimeUnit.SECONDS);
UserProfile profile = userProfileGateway.getByUserId(userId);
if (profile == null) {
throw new RuntimeException("用户信息不存在");
}
ResultResponse<PennyAmount> balanceResp = walletGoldClient.getBalance(userId);
return new YomiTokenCO(token, expireDate);
String token = credResp.getBody().token();
long expireAt = System.currentTimeMillis() / 1000 + yomiConfig.getTokenExpireSeconds();
redisTemplate.opsForValue().set(buildTokenCacheKey(token), cmd.getPlatUserId(),
yomiConfig.getTokenExpireSeconds(), TimeUnit.SECONDS);
return YomiTokenCO.builder()
.platUserId(cmd.getPlatUserId())
.platUserOpenId("") // 项目暂无 openId 概念按协议传空
.nickname(profile.getUserNickname())
.avatar(profile.getUserAvatar())
.balance(balanceResp.getBody().getDollarAmount().longValue())
.platToken(token)
.expireAt(expireAt)
.build();
} catch (Exception e) {
log.error("yomi获取token失败: userId={}, error={}", cmd.getUserId(), e.getMessage(), e);
log.error("yomi获取token失败: platUserId={}, error={}", userId, e.getMessage(), e);
throw new RuntimeException("获取token失败: " + e.getMessage());
}
}
@Override
public YomiUserInfoCO getUserInfo(YomiUserInfoCmd cmd) {
String userId = cmd.getUserId();
log.info("yomi获取用户信息: userId={}, gameId={}", userId, cmd.getGameId());
Long userId = Long.parseLong(cmd.getPlatUserId());
try {
// 验证token
if (!UserCredential.checkToken(cmd.getToken(), Long.parseLong(userId))) {
if (!UserCredential.checkToken(cmd.getPlatToken(), userId)) {
throw new Exception("token与用户ID不匹配");
}
// 获取用户信息
UserProfile profile = userProfileGateway.getByUserId(Long.parseLong(userId));
UserProfile profile = userProfileGateway.getByUserId(userId);
if (profile == null) {
throw new Exception("用户信息不存在");
}
// 获取用户余额
ResultResponse<PennyAmount> response = walletGoldClient.getBalance(Long.parseLong(userId));
YomiUserInfoCO result = new YomiUserInfoCO(
userId,
profile.getUserNickname(),
profile.getUserAvatar(),
response.getBody().getDollarAmount().intValue()
);
return result;
ResultResponse<PennyAmount> response = walletGoldClient.getBalance(userId);
return YomiUserInfoCO.builder()
.platUserId(cmd.getPlatUserId())
.platUserOpenId("") // 项目暂无 openId 概念按协议传空
.nickname(profile.getUserNickname())
.avatar(profile.getUserAvatar())
.balance(response.getBody().getDollarAmount().longValue())
.build();
} catch (Exception e) {
log.error("yomi获取用户信息失败: userId={}, error={}", userId, e.getMessage(), e);
log.error("yomi获取用户信息失败: platUserId={}, error={}", userId, e.getMessage(), e);
throw new RuntimeException(e.getMessage());
}
}
@Override
public YomiBalanceCO changeBalance(YomiChangeBalanceCmd cmd) {
Long userId = Long.parseLong(cmd.getUserId());
Integer changeValue = cmd.getChangeValue();
log.info("yomi余额变动: userId={}, gameId={}, changeValue={}, changeCause={}, recordId={}",
userId, cmd.getGameId(), changeValue, cmd.getChangeCause(), cmd.getRecordId());
Long userId = Long.parseLong(cmd.getPlatUserId());
String gameIdStr = String.valueOf(cmd.getGameUid());
log.info("yomi余额变动: platUserId={}, gameUid={}, gold={}, reasonType={}, requestId={}",
userId, cmd.getGameUid(), cmd.getGold(), cmd.getReasonType(), cmd.getRequestId());
try {
// 验证token
String token = cmd.getToken();
String gameId = cmd.getGameId();
if (!UserCredential.checkToken(token, userId)) {
if (!UserCredential.checkToken(cmd.getPlatToken(), userId)) {
throw new Exception("token与用户ID不匹配");
}
WalletReceiptResDTO res = walletGoldClient.changeBalance(createReceipt(cmd)).getBody();
int currentBalance = res.getBalance().getDollarAmount().intValue();
// 获取游戏配置
String sysOrigin = UserCredential.parseToken(token).getSysOrigin();
GameListConfig gameConfig = gameListConfigService.getByGameId(gameId, GameOriginEnum.YOMI.name(), sysOrigin);
if (gameConfig == null) {
log.error("游戏配置不存在, gameId={}", gameId);
// 幂等检查requestId 已处理则直接返回当前余额
String idempotentKey = IDEMPOTENT_KEY_PREFIX + cmd.getRequestId();
String processed = redisTemplate.opsForValue().get(idempotentKey);
if (processed != null) {
log.info("yomi余额变动重复请求直接返回: requestId={}", cmd.getRequestId());
long currentBalance = walletGoldClient.getBalance(userId).getBody().getDollarAmount().longValue();
return new YomiBalanceCO(currentBalance);
}
// 发送广播通知
if (!isConsume(cmd)) {
sendBroadcast(userId, changeValue, gameConfig.getCover());
WalletReceiptResDTO res = walletGoldClient.changeBalance(createReceipt(cmd)).getBody();
long currentBalance = res.getBalance().getDollarAmount().longValue();
// 累计游戏排行榜
incGameRankingRecord(changeValue, gameConfig);
// 执行成功后写入幂等标记
redisTemplate.opsForValue().set(idempotentKey, "1", IDEMPOTENT_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 游戏参与打榜活动
incGameRankingActivity(userId, changeValue, gameConfig.getGameId());
String sysOrigin = UserCredential.parseToken(cmd.getPlatToken()).getSysOrigin();
GameListConfig gameConfig = gameListConfigService.getByGameId(gameIdStr, GameOriginEnum.YOMI.name(), sysOrigin);
if (gameConfig == null) {
log.error("游戏配置不存在, gameUid={}", cmd.getGameUid());
return new YomiBalanceCO(currentBalance);
}
//处理游戏获得任务
gameActivityService.handleGameSpendTask(userId, changeValue.longValue());
// 累加用户获得的金币并发送抽奖券
// activityRechargeTicketService.accumulatedRechargeAndAddTicket(userId, Math.abs(changeValue.longValue()), MonthlyRechargeType.UNDEFINED);
if (!isBet(cmd)) {
sendBroadcast(userId, cmd.getGold().intValue(), gameConfig.getCover());
incGameRankingRecord(cmd.getGold().intValue(), gameConfig);
incGameRankingActivity(userId, cmd.getGold().intValue(), gameIdStr);
gameActivityService.handleGameSpendTask(userId, cmd.getGold());
} else {
updateRoomDailyTask(changeValue.longValue(), userId);
updateRoomDailyTask(cmd.getGold(), userId);
}
return new YomiBalanceCO(currentBalance);
} catch (Exception e) {
log.error("yomi余额变动失败: userId={}, recordId={}, error={}",
userId, cmd.getRecordId(), e.getMessage(), e);
log.error("yomi余额变动失败: platUserId={}, requestId={}, error={}",
userId, cmd.getRequestId(), e.getMessage(), e);
throw new RuntimeException("余额变动失败: " + e.getMessage());
}
}
// ------------------------------------------------------------------ //
// 私有方法
// ------------------------------------------------------------------ //
private static boolean isBet(YomiChangeBalanceCmd cmd) {
return Integer.valueOf(1).equals(cmd.getReasonType());
}
private String buildTokenCacheKey(String token) {
return "yomi:token:" + token;
}
private GoldReceiptCmd createReceipt(YomiChangeBalanceCmd cmd) {
UserCredential userCredential = UserCredential.parseToken(cmd.getPlatToken());
String gameUidStr = String.valueOf(cmd.getGameUid());
return createGoldReceiptCmdBuilder(cmd.getReasonType())
.userId(Long.parseLong(cmd.getPlatUserId()))
.sysOrigin(userCredential.getSysOrigin())
.origin(GoldOrigin.YOMI_GAME.name() + "_" + gameUidStr)
.originDescribe(GoldOrigin.YOMI_GAME.getDesc() + "[" + gameUidStr + "]")
.eventId(String.valueOf(cmd.getGameRoundId()))
.amount(ArithmeticUtils.toBigDecimal(Math.abs(cmd.getGold())))
.build();
}
private GoldReceiptCmd.GoldReceiptCmdBuilder createGoldReceiptCmdBuilder(Integer reasonType) {
if (Integer.valueOf(1).equals(reasonType)) {
return GoldReceiptCmd.builder().appExpenditure();
}
if (Integer.valueOf(2).equals(reasonType)) {
return GoldReceiptCmd.builder().appIncome();
}
throw new IllegalArgumentException("yomi game param [reasonType] error: " + reasonType);
}
private void updateRoomDailyTask(Long coins, Long userId) {
RoomDailyTaskCode taskType = RoomDailyTaskCode.PERSONAL_GAME_CONSUME;
String redisKey = "room:daily:task:progress:" + taskType.name() + ":" + userId + ":" + ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate();
@ -211,92 +235,19 @@ public class YomiGameServiceImpl implements YomiGameService {
);
}
private static boolean isConsume(YomiChangeBalanceCmd cmd) {
return "bet".equals(cmd.getChangeCause());
}
/**
* 验证token有效性
*/
private String validateToken(String token) {
String cacheKey = buildTokenCacheKey(token);
String userId = redisTemplate.opsForValue().get(cacheKey);
if (userId == null) {
throw new RuntimeException("token已过期或无效");
}
return userId;
}
/**
* 构建token缓存key
*/
private String buildTokenCacheKey(String token) {
return "yomi:token:" + token;
}
private GoldReceiptCmd createReceipt(YomiChangeBalanceCmd cmd) {
UserCredential userCredential = UserCredential.parseToken(cmd.getToken());
return createGoldReceiptCmdBuilder(cmd.getChangeCause())
.userId(Long.parseLong(cmd.getUserId()))
.sysOrigin(userCredential.getSysOrigin())
.origin(GoldOrigin.YOMI_GAME.name() + "_" + cmd.getGameId())
.originDescribe(GoldOrigin.YOMI_GAME.getDesc() + "[" + cmd.getGameId() + "]")
.eventId(cmd.getRoundId())
.amount(ArithmeticUtils.toBigDecimal(Math.abs(cmd.getChangeValue())))
.build();
}
private GoldReceiptCmd.GoldReceiptCmdBuilder createGoldReceiptCmdBuilder(String changeCause) {
if ("bet".equals(changeCause)) {
return GoldReceiptCmd.builder().appExpenditure();
}
if ("awards".equals(changeCause)) {
return GoldReceiptCmd.builder().appIncome();
}
throw new IllegalArgumentException("yomi game param [changeCause] error: " + changeCause);
}
/**
* 构建游戏参数(用于广播和排行榜)
*/
private Map<String, Object> buildGameParam(YomiChangeBalanceCmd cmd, Long userId, GameListConfig gameConfig) {
Map<String, Object> param = new HashMap<>();
param.put("userId", userId);
param.put("gameId", cmd.getGameId());
param.put("roomId", cmd.getRoomId());
param.put("recordId", cmd.getRecordId());
param.put("roundId", cmd.getRoundId());
param.put("changeValue", cmd.getChangeValue());
param.put("changeCause", cmd.getChangeCause());
param.put("changeTime", cmd.getChangeTime());
param.put("gameConfig", gameConfig);
return param;
}
private void sendBroadcast(Long userId, Integer rewardAmount, String gameUrl) {
if (rewardAmount == null || rewardAmount <= 0) {
if (rewardAmount == null || rewardAmount <= 0) {
return;
}
BigDecimal cost = enumConfigCacheService.getValueBigDecimal(EnumConfigKey.GAME_BROADCAST_AMOUNT, SysOriginPlatformEnum.ATYOU.name()) ;
BigDecimal cost = enumConfigCacheService.getValueBigDecimal(EnumConfigKey.GAME_BROADCAST_AMOUNT, SysOriginPlatformEnum.ATYOU.name());
int thresholdAmount = Optional.ofNullable(cost).orElse(new BigDecimal("5000")).intValue();
if (rewardAmount < thresholdAmount) {
return;
}
UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO(userProfileGateway.getByUserId(userId));
Map<Object, Object> build = OfficialNoticeUtils.buildUserProfile(userProfile);
java.util.Map<Object, Object> build = OfficialNoticeUtils.buildUserProfile(userProfile);
build.put("gameUrl", gameUrl);
build.put("currencyDiff", rewardAmount);
imGroupClient.sendMessageBroadcast(
BroadcastGroupMsgBodyCmd.builder()
.toPlatform(SysOriginPlatformEnum.ATYOU)
@ -307,7 +258,6 @@ public class YomiGameServiceImpl implements YomiGameService {
}
private void incGameRankingRecord(Integer rewardAmount, GameListConfig gameListConfig) {
GameRankingIncrementCmd build = GameRankingIncrementCmd.builder()
.gameOrigin(GameOriginEnum.YOMI.name())
.gameId(gameListConfig.getGameId())
@ -321,7 +271,6 @@ public class YomiGameServiceImpl implements YomiGameService {
gameRankingGateway.incrementPrizeAmount(build);
}
private void incGameRankingActivity(Long userId, Integer rewardAmount, String gameId) {
RankingDataUpdateCmd receiverCmd = new RankingDataUpdateCmd();
receiverCmd.setUserId(userId);
@ -334,8 +283,6 @@ public class YomiGameServiceImpl implements YomiGameService {
receiverCmd.setBizNo(gameId);
receiverCmd.setReqSysOrigin(ReqSysOrigin.of(SysOriginPlatformEnum.ATYOU.name()));
rankingActivityService.accumulateRankingData(receiverCmd);
//游戏王日榜
gameActivityService.incKingGameDaily(userId, rewardAmount.longValue(), gameId);
}
}
}

View File

@ -1,21 +1,16 @@
package com.red.circle.other.app.dto.cmd.party3rd;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Yomi余额响应
* Yomi余额响应 (v4)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class YomiBalanceCO {
/**
* 当前余额
*/
@JsonProperty("current_balance")
private Integer currentBalance;
}
private Long balance;
}

View File

@ -1,39 +1,56 @@
package com.red.circle.other.app.dto.cmd.party3rd;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* Yomi用户余额变动请求
* Yomi用户余额变动请求 (v4)
*/
@Data
public class YomiChangeBalanceCmd {
@NotBlank(message = "游戏ID不能为空")
private String gameId;
@NotBlank(message = "用户ID不能为空")
private String userId;
@NotBlank(message = "token不能为空")
private String token;
@NotNull(message = "余额变化值不能为空")
private Integer changeValue;
@NotBlank(message = "变化原因不能为空")
private String changeCause;
@NotNull(message = "变化时间不能为空")
private Long changeTime;
@NotBlank(message = "房间ID不能为空")
private String roomId;
@NotBlank(message = "记录ID不能为空")
private String recordId;
@NotBlank(message = "轮次ID不能为空")
private String roundId;
}
/** 请求ID幂等标识 */
private Long requestId;
private String platUserId;
private String platToken;
/** 变动金额,取值范围[0~MaxBet]|[0~MaxReward] */
private Long gold;
private Integer gameUid;
/** 游戏场次 */
private Integer gameLevelUid;
/** 1=投注 2=返奖 */
private Integer reasonType;
/** 游戏轮局ID */
private Long gameRoundId;
/** 游戏发生时间戳 */
private Integer timestamp;
private String platPayload;
private String platRoomId;
@JsonProperty("game_infos")
private List<GameInfo> gameInfos;
@Data
public static class GameInfo {
@JsonProperty("are_id")
private Integer areId;
@JsonProperty("are_name")
private String areName;
@JsonProperty("are_rate")
private Integer areRate;
private Long bet;
private Long award;
}
}

View File

@ -1,23 +1,28 @@
package com.red.circle.other.app.dto.cmd.party3rd;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* Yomi获取token请求
* Yomi获取token请求 (v4)
*/
@Data
public class YomiGetTokenCmd {
@NotBlank(message = "游戏ID不能为空")
private String gameId;
@NotBlank(message = "房间ID不能为空")
private String roomId;
@NotBlank(message = "用户ID不能为空")
private String userId;
@NotBlank(message = "验证码不能为空")
private String code;
/** Yomi分配的GameUID */
private Integer gameUid;
/** 多语言 */
private String lang;
/** 商户code */
private String platAuthCode;
/** 用户ID */
private String platUserId;
/** 透传数据 */
private String platPayload;
/** 房间ID */
private String platRoomId;
}

View File

@ -1,26 +1,30 @@
package com.red.circle.other.app.dto.cmd.party3rd;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Yomi Token响应
* Yomi Token响应 (v4)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class YomiTokenCO {
/**
* token
*/
private String token;
/**
* 过期时间(秒级时间戳)
*/
@JsonProperty("expire_date")
private Long expireDate;
}
private String platUserId;
private String platUserOpenId;
private String nickname;
private String avatar;
private Long balance;
private String platToken;
private Long expireAt;
}

View File

@ -1,39 +1,26 @@
package com.red.circle.other.app.dto.cmd.party3rd;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Yomi用户信息响应
* Yomi用户信息响应 (v4)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class YomiUserInfoCO {
/**
* 用户ID
*/
@JsonProperty("user_id")
private String userId;
/**
* 用户昵称
*/
@JsonProperty("user_name")
private String userName;
/**
* 用户头像
*/
@JsonProperty("user_avatar")
private String userAvatar;
/**
* 用户余额
*/
@JsonProperty("balance")
private Integer balance;
}
private String platUserId;
private String platUserOpenId;
private String nickname;
private String avatar;
private Long balance;
}

View File

@ -1,20 +1,20 @@
package com.red.circle.other.app.dto.cmd.party3rd;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* Yomi获取用户信息请求
* Yomi获取用户信息请求 (v4)
*/
@Data
public class YomiUserInfoCmd {
@NotBlank(message = "游戏ID不能为空")
private String gameId;
@NotBlank(message = "用户ID不能为空")
private String userId;
@NotBlank(message = "token不能为空")
private String token;
private Integer gameUid;
private String platUserId;
private String platToken;
private String platPayload;
private String platRoomId;
}