From 14f3811c3518ea8989aa608c41f5588c4d20a91d Mon Sep 17 00:00:00 2001 From: tianfeng <769204422@qq.com> Date: Tue, 20 Jan 2026 14:52:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eyomi=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wallet/inner/model/enums/GoldOrigin.java | 2 + .../game/party3rd/YomiGameRestController.java | 147 ++++++++ .../app/service/game/HotGameServiceImpl.java | 2 +- .../app/service/game/YomiGameServiceImpl.java | 315 ++++++++++++++++++ .../override/service/YomiGameService.java | 24 ++ .../app/dto/cmd/party3rd/YomiBalanceCO.java | 19 ++ .../cmd/party3rd/YomiChangeBalanceCmd.java | 39 +++ .../app/dto/cmd/party3rd/YomiGetTokenCmd.java | 23 ++ .../dto/cmd/party3rd/YomiResponseWrapper.java | 37 ++ .../app/dto/cmd/party3rd/YomiTokenCO.java | 24 ++ .../app/dto/cmd/party3rd/YomiUserInfoCO.java | 34 ++ .../app/dto/cmd/party3rd/YomiUserInfoCmd.java | 20 ++ .../circle/other/infra/config/YomiConfig.java | 24 ++ .../circle/other/infra/utils/AesUtils.java | 127 +++++++ 14 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/party3rd/YomiGameRestController.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/override/service/YomiGameService.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiBalanceCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiChangeBalanceCmd.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiGetTokenCmd.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiResponseWrapper.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiTokenCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCmd.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/YomiConfig.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/utils/AesUtils.java diff --git a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java index 542e9c2a..9e2da475 100644 --- a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java +++ b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java @@ -564,6 +564,8 @@ public enum GoldOrigin { */ BAISHUN_GAME("Baishun game"), + YOMI_GAME("Yomi game"), + /** * 房间贡献活动奖励. */ diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/party3rd/YomiGameRestController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/party3rd/YomiGameRestController.java new file mode 100644 index 00000000..6ea0ac8a --- /dev/null +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/party3rd/YomiGameRestController.java @@ -0,0 +1,147 @@ +package com.red.circle.other.adapter.app.game.party3rd; + +import com.alibaba.fastjson.JSON; +import com.red.circle.component.game.hotgame.response.HotGameCoin; +import com.red.circle.component.game.hotgame.response.HotGameErrorEnum; +import com.red.circle.component.game.hotgame.response.HotGameResponse; +import com.red.circle.framework.core.exception.ResponseException; +import com.red.circle.framework.web.controller.BaseController; +import com.red.circle.other.app.dto.cmd.party3rd.*; +import com.red.circle.other.app.service.game.override.service.YomiGameService; +import com.red.circle.other.infra.config.YomiConfig; +import com.red.circle.other.infra.utils.AesUtils; +import com.red.circle.wallet.inner.error.WalletErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * Yomi游戏REST接口 + * + * @eo.name Yomi游戏接口 + */ +@Slf4j +@RestController +@RequestMapping("/game/yomi") +@RequiredArgsConstructor +public class YomiGameRestController extends BaseController { + + private final YomiGameService yomiGameService; + private final YomiConfig yomiConfig; + + /** + * 获取token + * + * @eo.name 获取token + * @eo.url /api/gettoken + * @eo.method post + * @eo.request-type text/plain + */ + @PostMapping("/api/gettoken") + public YomiResponseWrapper getToken(HttpServletRequest request) { + try { + String encryptedBody = readRequestBody(request); + String decryptedJson = AesUtils.decryptAESWithBase64(encryptedBody, yomiConfig.getAesKey()); + + // 解析JSON为Cmd对象 + YomiGetTokenCmd cmd = JSON.parseObject(decryptedJson, YomiGetTokenCmd.class); + + // 调用Service处理 + YomiTokenCO result = yomiGameService.getToken(cmd); + + return YomiResponseWrapper.success(result); + + } catch (Exception e) { + log.error("yomi getToken 业务异常: {}", e.getMessage()); + return YomiResponseWrapper.fail(10005, e.getMessage()); + } + } + + /** + * 获取用户信息 + * + * @eo.name 获取用户信息 + * @eo.url /api/userinfo + * @eo.method post + * @eo.request-type text/plain + */ + @PostMapping("/api/userinfo") + public YomiResponseWrapper getUserInfo(HttpServletRequest request) { + try { + String encryptedBody = readRequestBody(request); + String decryptedJson = AesUtils.decryptAESWithBase64(encryptedBody, yomiConfig.getAesKey()); + + // 解析JSON为Cmd对象 + YomiUserInfoCmd cmd = JSON.parseObject(decryptedJson, YomiUserInfoCmd.class); + + // 调用Service处理 + YomiUserInfoCO result = yomiGameService.getUserInfo(cmd); + + return YomiResponseWrapper.success(result); + + } catch (Exception e) { + log.error("yomi getUserInfo 业务异常: {}", e.getMessage()); + return YomiResponseWrapper.fail(10005, e.getMessage()); + } + } + + /** + * 用户余额变动 + * + * @eo.name 用户余额变动 + * @eo.url /api/change_balance + * @eo.method post + * @eo.request-type text/plain + */ + @PostMapping("/api/change_balance") + public YomiResponseWrapper changeBalance(HttpServletRequest request) { + try { + // 读取加密的请求body + String encryptedBody = readRequestBody(request); + log.info("yomi changeBalance 加密请求: {}", encryptedBody); + + // AES解密 + String decryptedJson = AesUtils.decryptAESWithBase64(encryptedBody, yomiConfig.getAesKey()); + log.info("yomi changeBalance 解密后: {}", decryptedJson); + + // 解析JSON为Cmd对象 + YomiChangeBalanceCmd cmd = JSON.parseObject(decryptedJson, YomiChangeBalanceCmd.class); + + // 调用Service处理 + YomiBalanceCO result = yomiGameService.changeBalance(cmd); + + return YomiResponseWrapper.success(result); + + } catch (ResponseException e) { + log.error("yomi changeBalance 业务异常: recordId={}, error={}", + e.getMessage(), e.getMessage()); + + if (Objects.equals(e.getErrorCode(), WalletErrorCode.INSUFFICIENT_BALANCE.getCode())) { + return YomiResponseWrapper.fail(10003, e.getMessage()); + } + } catch (Exception e) { + log.error("yomi changeBalance 系统异常: {}", e.getMessage(), e); + return YomiResponseWrapper.fail(9999, "系统错误,联系技术解决"); + } + return null; + } + + /** + * 读取请求body + */ + private String readRequestBody(HttpServletRequest request) { + try { + byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream()); + return new String(bodyBytes, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException("读取请求body失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java index a85b67cb..6eec4a61 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java @@ -316,7 +316,7 @@ public class HotGameServiceImpl implements HotGameService { incGameRankingActivity(param); //FruitParty - incGameRankingTmpActivity(param); +// incGameRankingTmpActivity(param); //处理游戏消费任务 if (param.getType() == 1) { diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java new file mode 100644 index 00000000..cd318f37 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java @@ -0,0 +1,315 @@ +package com.red.circle.other.app.service.game; + +import com.red.circle.auth.inner.endpoint.AuthClient; +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.component.game.hotgame.request.HotGameUserCoinUpdate; +import com.red.circle.external.inner.endpoint.message.ImGroupClient; +import com.red.circle.external.inner.model.cmd.message.BroadcastGroupMsgBodyCmd; +import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum; +import com.red.circle.framework.core.dto.ReqSysOrigin; +import com.red.circle.framework.core.security.UserCredential; +import com.red.circle.framework.dto.ResultResponse; +import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; +import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd; +import com.red.circle.other.app.dto.cmd.party3rd.*; +import com.red.circle.other.app.enums.violation.GameOriginEnum; +import com.red.circle.other.app.service.activity.RankingActivityService; +import com.red.circle.other.app.service.game.override.service.YomiGameService; +import com.red.circle.other.app.util.OfficialNoticeUtils; +import com.red.circle.other.domain.game.GameRankingIncrementCmd; +import com.red.circle.other.domain.gateway.game.GameRankingGateway; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.other.domain.ranking.RankingActivityType; +import com.red.circle.other.domain.ranking.RankingCycleType; +import com.red.circle.other.domain.ranking.RankingDimension; +import com.red.circle.other.infra.config.YomiConfig; +import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService; +import com.red.circle.other.infra.database.rds.entity.sys.GameListConfig; +import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService; +import com.red.circle.other.inner.enums.config.EnumConfigKey; +import com.red.circle.other.inner.model.dto.user.UserProfileDTO; +import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; +import com.red.circle.tool.core.num.ArithmeticUtils; +import com.red.circle.tool.core.parse.DataTypeUtils; +import com.red.circle.tool.core.tuple.PennyAmount; +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.dto.WalletReceiptResDTO; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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; + +/** + * Yomi游戏服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class YomiGameServiceImpl implements YomiGameService { + + private final YomiConfig yomiConfig; + private final RedisTemplate redisTemplate; + private final AuthClient authClient; + private final UserProfileGateway userProfileGateway; + private final WalletGoldClient walletGoldClient; + private final GameListConfigService gameListConfigService; + private final EnumConfigCacheService enumConfigCacheService; + private final ImGroupClient imGroupClient; + private final UserProfileAppConvertor userProfileAppConvertor; + private final RankingActivityService rankingActivityService; + private final GameActivityService gameActivityService; + private final GameRankingGateway gameRankingGateway; + + @Override + public YomiTokenCO getToken(YomiGetTokenCmd cmd) { + log.info("yomi获取token: userId={}, gameId={}", cmd.getUserId(), cmd.getGameId()); + + try { + // 调用认证服务获取用户凭证 + ResultResponse response = authClient.getUserCredential(Long.parseLong(cmd.getUserId())); + UserCredential credential = response.getBody(); + + if (credential == 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); + + return new YomiTokenCO(token, expireDate); + + } catch (Exception e) { + log.error("yomi获取token失败: userId={}, error={}", cmd.getUserId(), 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()); + + try { + // 验证token + if (!UserCredential.checkToken(cmd.getToken(), Long.parseLong(userId))) { + throw new Exception("token与用户ID不匹配"); + } + + // 获取用户信息 + UserProfile profile = userProfileGateway.getByUserId(Long.parseLong(userId)); + + if (profile == null) { + throw new Exception("用户信息不存在"); + } + + // 获取用户余额 + ResultResponse response = walletGoldClient.getBalance(Long.parseLong(userId)); + + YomiUserInfoCO result = new YomiUserInfoCO( + userId, + profile.getUserNickname(), + profile.getUserAvatar(), + response.getBody().getDollarAmount().intValue() + ); + return result; + + } catch (Exception e) { + log.error("yomi获取用户信息失败: userId={}, error={}", userId, e.getMessage(), e); + } + return new YomiUserInfoCO(); + } + + @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()); + + try { + // 验证token + String token = cmd.getToken(); + String gameId = cmd.getGameId(); + + if (!UserCredential.checkToken(token, 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.HOTGAME.name(), sysOrigin); + if (gameConfig == null) { + log.error("游戏配置不存在, gameId={}", gameId); + return new YomiBalanceCO(currentBalance); + } + + // 发送广播通知 + if (!isConsume(cmd)) { + sendBroadcast(userId, changeValue, gameConfig.getCover()); + + // 累计游戏排行榜 + incGameRankingRecord(changeValue, gameConfig); + + // 游戏参与打榜活动 + incGameRankingActivity(userId, changeValue, gameConfig.getGameId()); + } + + //处理游戏消费任务 + if (isConsume(cmd)) { + gameActivityService.handleGameSpendTask(userId, changeValue.longValue()); + } + + return new YomiBalanceCO(currentBalance); + + } catch (Exception e) { + log.error("yomi余额变动失败: userId={}, recordId={}, error={}", + userId, cmd.getRecordId(), e.getMessage(), e); + throw new RuntimeException("余额变动失败: " + e.getMessage()); + } + } + + 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(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 buildGameParam(YomiChangeBalanceCmd cmd, Long userId, GameListConfig gameConfig) { + Map 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) { + return; + } + + BigDecimal cost = enumConfigCacheService.getValueBigDecimal(EnumConfigKey.GAME_BROADCAST_AMOUNT, SysOriginPlatformEnum.LIKEI.name()) ; + int thresholdAmount = Optional.ofNullable(cost).orElse(new BigDecimal("5000")).intValue(); + if (rewardAmount < thresholdAmount) { + return; + } + + UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO(userProfileGateway.getByUserId(userId)); + + Map build = OfficialNoticeUtils.buildUserProfile(userProfile); + build.put("gameUrl", gameUrl); + build.put("currencyDiff", rewardAmount); + + imGroupClient.sendMessageBroadcast( + BroadcastGroupMsgBodyCmd.builder() + .toPlatform(SysOriginPlatformEnum.LIKEI) + .type(GroupMessageTypeEnum.GAME_BAISHUN_WIN) + .data(build) + .build() + ); + } + + private void incGameRankingRecord(Integer rewardAmount, GameListConfig gameListConfig) { + + GameRankingIncrementCmd build = GameRankingIncrementCmd.builder() + .gameOrigin(GameOriginEnum.HOTGAME.name()) + .gameId(gameListConfig.getGameId()) + .gameName(gameListConfig.getName()) + .gameCover(gameListConfig.getCover()) + .gameUrl(gameListConfig.getGameCode()) + .sysOrigin(gameListConfig.getSysOrigin()) + .amount(rewardAmount.longValue()) + .periodType("ALL") + .build(); + gameRankingGateway.incrementPrizeAmount(build); + } + + + private void incGameRankingActivity(Long userId, Integer rewardAmount, String gameId) { + RankingDataUpdateCmd receiverCmd = new RankingDataUpdateCmd(); + receiverCmd.setUserId(userId); + receiverCmd.setUserSex(1); + receiverCmd.setActivityType(RankingActivityType.KING_GAMES.getCode()); + receiverCmd.setCycleType(RankingCycleType.WEEKLY.getCode()); + receiverCmd.setCycleKey(ZonedDateTimeAsiaRiyadhUtils.nowWeekMondayToInt().toString()); + receiverCmd.setDimension(RankingDimension.GAME_WIN.getCode()); + receiverCmd.setIncrementQuantity(rewardAmount.longValue()); + receiverCmd.setBizNo(gameId); + receiverCmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI")); + rankingActivityService.accumulateRankingData(receiverCmd); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/override/service/YomiGameService.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/override/service/YomiGameService.java new file mode 100644 index 00000000..b825f084 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/override/service/YomiGameService.java @@ -0,0 +1,24 @@ +package com.red.circle.other.app.service.game.override.service; + +import com.red.circle.other.app.dto.cmd.party3rd.*; + +/** + * Yomi游戏服务接口 + */ +public interface YomiGameService { + + /** + * 获取token + */ + YomiTokenCO getToken(YomiGetTokenCmd cmd); + + /** + * 获取用户信息 + */ + YomiUserInfoCO getUserInfo(YomiUserInfoCmd cmd); + + /** + * 用户余额变动 + */ + YomiBalanceCO changeBalance(YomiChangeBalanceCmd cmd); +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiBalanceCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiBalanceCO.java new file mode 100644 index 00000000..655e48cf --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiBalanceCO.java @@ -0,0 +1,19 @@ +package com.red.circle.other.app.dto.cmd.party3rd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Yomi余额响应 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class YomiBalanceCO { + + /** + * 当前余额 + */ + private Integer currentBalance; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiChangeBalanceCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiChangeBalanceCmd.java new file mode 100644 index 00000000..616f3b8b --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiChangeBalanceCmd.java @@ -0,0 +1,39 @@ +package com.red.circle.other.app.dto.cmd.party3rd; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * Yomi用户余额变动请求 + */ +@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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiGetTokenCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiGetTokenCmd.java new file mode 100644 index 00000000..39bad499 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiGetTokenCmd.java @@ -0,0 +1,23 @@ +package com.red.circle.other.app.dto.cmd.party3rd; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * Yomi获取token请求 + */ +@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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiResponseWrapper.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiResponseWrapper.java new file mode 100644 index 00000000..857bb7bc --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiResponseWrapper.java @@ -0,0 +1,37 @@ +package com.red.circle.other.app.dto.cmd.party3rd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Yomi统一响应包装 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class YomiResponseWrapper { + + /** + * 状态码 200=成功 + */ + private Integer code; + + /** + * 消息 + */ + private String message; + + /** + * 数据 + */ + private T data; + + public static YomiResponseWrapper success(T data) { + return new YomiResponseWrapper<>(200, "success", data); + } + + public static YomiResponseWrapper fail(Integer code, String message) { + return new YomiResponseWrapper<>(code, message, null); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiTokenCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiTokenCO.java new file mode 100644 index 00000000..be2667e6 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiTokenCO.java @@ -0,0 +1,24 @@ +package com.red.circle.other.app.dto.cmd.party3rd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Yomi Token响应 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class YomiTokenCO { + + /** + * token + */ + private String token; + + /** + * 过期时间(秒级时间戳) + */ + private Long expireDate; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCO.java new file mode 100644 index 00000000..d8888f14 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCO.java @@ -0,0 +1,34 @@ +package com.red.circle.other.app.dto.cmd.party3rd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Yomi用户信息响应 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class YomiUserInfoCO { + + /** + * 用户ID + */ + private String userId; + + /** + * 用户昵称 + */ + private String userName; + + /** + * 用户头像 + */ + private String userAvatar; + + /** + * 用户余额 + */ + private Integer balance; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCmd.java new file mode 100644 index 00000000..f235c985 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/party3rd/YomiUserInfoCmd.java @@ -0,0 +1,20 @@ +package com.red.circle.other.app.dto.cmd.party3rd; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * Yomi获取用户信息请求 + */ +@Data +public class YomiUserInfoCmd { + + @NotBlank(message = "游戏ID不能为空") + private String gameId; + + @NotBlank(message = "用户ID不能为空") + private String userId; + + @NotBlank(message = "token不能为空") + private String token; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/YomiConfig.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/YomiConfig.java new file mode 100644 index 00000000..87dcb1e1 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/YomiConfig.java @@ -0,0 +1,24 @@ +package com.red.circle.other.infra.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * Yomi游戏配置 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "yomi") +public class YomiConfig { + + /** + * AES加密密钥 + */ + private String aesKey; + + /** + * Token过期时间(秒) 默认24小时 + */ + private Integer tokenExpireSeconds = 86400; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/utils/AesUtils.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/utils/AesUtils.java new file mode 100644 index 00000000..663adb7b --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/utils/AesUtils.java @@ -0,0 +1,127 @@ +package com.red.circle.other.infra.utils; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.SecureRandom; +import java.util.Base64; + +public class AesUtils { + private static final Base64.Encoder base64Encoder = Base64.getUrlEncoder(); + private static final Base64.Decoder base64Decoder = Base64.getUrlDecoder(); + private static final int IV_LENGTH = 12; + private static final int GCM_TAG_LENGTH = 16; // GCM tag length in bytes + + public static String generateAESKey() throws Exception { + final int keyLength = 32; // 256-bit key (AES-256) + final String charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + final int charSetLength = charSet.length(); + + SecureRandom random = new SecureRandom(); + StringBuilder keyBuilder = new StringBuilder(keyLength); + for (int i = 0; i < keyLength; i++) { + int index = random.nextInt(charSetLength); + keyBuilder.append(charSet.charAt(index)); + } + + return keyBuilder.toString(); + } + + // 验证并转换字符串密钥为字节数组 + private static SecretKeySpec validateKey(String key) throws Exception { + int keyLength = key.length(); + if (keyLength != 16 && keyLength != 24 && keyLength != 32) { + throw new IllegalArgumentException("Invalid AES key size (must be 16, 24, or 32 bytes)"); + } + return new SecretKeySpec(key.getBytes(), "AES"); + } + + // 使用AES加密并返回Base64编码结果 + public static String encryptAESWithBase64(byte[] data, String key) throws Exception { + // 验证并转换密钥 + SecretKeySpec keyBytes = validateKey(key); + + // 创建AES加密块 + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + + // 生成随机IV(Nonce),长度为GCM推荐的NonceSize + byte[] iv = new byte[IV_LENGTH]; + SecureRandom random = new SecureRandom(); + random.nextBytes(iv); + + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); + + // 初始化加密器 + cipher.init(Cipher.ENCRYPT_MODE, keyBytes, gcmParameterSpec); + + // 加密数据 + byte[] ciphertext = cipher.doFinal(data); + + // 拼接IV和密文 + byte[] finalData = new byte[iv.length + ciphertext.length]; + System.arraycopy(iv, 0, finalData, 0, iv.length); + System.arraycopy(ciphertext, 0, finalData, iv.length, ciphertext.length); + + // 返回Base64编码字符串 + return base64Encoder.encodeToString(finalData); + } + + // 使用AES解密Base64编码数据 + public static String decryptAESWithBase64(String base64Data, String key) throws Exception { + System.out.println("解码原数据:"+ base64Data); + // 验证并转换密钥 + SecretKeySpec keyBytes = validateKey(key); + + // 解码Base64字符串 + byte[] encryptedData = base64Decoder.decode(base64Data); + + // 创建AES加密块 + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + + // 检查数据长度是否足够(至少包含一个Nonce) + if (encryptedData.length < GCM_TAG_LENGTH) { + throw new IllegalArgumentException("Encrypted data too short"); + } + + // 提取IV和密文 + byte[] iv = new byte[IV_LENGTH]; + System.arraycopy(encryptedData, 0, iv, 0, iv.length); + byte[] ciphertext = new byte[encryptedData.length - iv.length]; + System.arraycopy(encryptedData, iv.length, ciphertext, 0, ciphertext.length); + + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); + + // 初始化解密器 + cipher.init(Cipher.DECRYPT_MODE, keyBytes, gcmParameterSpec); + + // 解密数据 + String decryptStr = new String(cipher.doFinal(ciphertext)); + System.out.println("解密后数据:"+ decryptStr); + return decryptStr; + } + + public static void main(String[] args) { + try { + // 生成AES密钥 + String key = "bDyeXhfYPAadK7DzxuMpZDHsPBZjbEzS"; + // 密文 + String encryptedData = "ni-P7cw9mPhm8SndkwvxAl_6o1I_jLc930uHJLXuYNe2On1vmKz1--5dvWFpJoMlKnXOrhax03DU5wrR82l_ukRo9ZMqk8AX3kENM816doDcGIS2xcotqrjaN202Lcj2_IFCy-TZdP4WjA=="; + + // 解密 + String decryptedData = decryptAESWithBase64(encryptedData, key); + System.out.println("Decrypted Data: " + decryptedData); + + System.out.println(); + + String encryptedData2 = encryptAESWithBase64(decryptedData.getBytes(), key); + System.out.println("Encrypt Data: " + encryptedData2); + + // 解密2 + String decryptedData2 = decryptAESWithBase64(encryptedData2, key); + System.out.println("Decrypted Data2: " + decryptedData2); + + } catch (Exception e) { + e.printStackTrace(); + } + } +}