From 0f4e440f2b696f5c38f5bd76f58553846f6ae24e Mon Sep 17 00:00:00 2001 From: zhx Date: Wed, 15 Jul 2026 21:09:31 +0800 Subject: [PATCH] feat(other): switch lucky gift to HyApp gateway --- .../app/common/gift/GameLuckyGiftCommon.java | 257 ++++++++---------- .../clientobject/game/GameLuckyGiftMsgCO.java | 5 + .../infra/config/LuckyGiftApiConfig.java | 21 ++ 3 files changed, 143 insertions(+), 140 deletions(-) diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java index 903dcff7..a9870bed 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java @@ -1,10 +1,8 @@ package com.red.circle.other.app.common.gift; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -44,7 +42,6 @@ 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.LuckyGiftApiConfig; -import com.red.circle.other.infra.config.LuckyGiftApiConfig.ApiAccount; import com.red.circle.other.infra.database.cache.entity.game.luckgift.GameLuckyGiftConfigCache; import com.red.circle.other.infra.database.cache.entity.game.luckgift.GameLuckyGiftInventoryCache; import com.red.circle.other.infra.database.cache.entity.game.luckgift.LuckyGiftInventoryTemplateCache; @@ -87,7 +84,6 @@ import java.io.Serial; import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; -import java.security.MessageDigest; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -249,10 +245,12 @@ public class GameLuckyGiftCommon { List orderIds = List.of("LG_" + param.getId().toString()); ThirdPartyLotteryResult apiResult = callThirdPartyLotteryApi(param, orderIds); - Integer totalAwardAmount = apiResult.getOrderResults().stream().mapToInt(OrderResult::getRewardNum).sum(); + long totalAwardAmount = apiResult.getOrderResults().stream() + .mapToLong(OrderResult::getRewardNum) + .sum(); // 保存幸运礼物流水 - Long rewardAmount = Long.valueOf(String.valueOf(totalAwardAmount)); + Long rewardAmount = totalAwardAmount; saveLuckGiftCount(param, rewardAmount); if (totalAwardAmount <= 0) { @@ -264,7 +262,11 @@ public class GameLuckyGiftCommon { // 发送中奖通知 int multiple = getRewardMultiple(param, rewardAmount); - sendMsg(param, multiple, balance, rewardAmount); + long multiplierPpm = apiResult.getOrderResults().stream() + .findFirst() + .map(OrderResult::getMultiplierPpm) + .orElse((long) multiple * 1_000_000L); + sendMsg(param, multiple, multiplierPpm, balance, rewardAmount); // 魔法礼物中奖后处理 GiftConfigDTO giftConfigDTO = getGiftConfigDTO(cmd.getGiftId()); @@ -782,156 +784,119 @@ public class GameLuckyGiftCommon { } /** - * 调用第三方幸运礼物API. + * 调用 HyApp 幸运礼物外接接口。 + * + * requestId 直接使用 MQ 中稳定的抽奖记录 ID,网络重试和重复消费都会命中同一结果, + * 不会因为重新生成请求号而重复抽奖。 */ private ThirdPartyLotteryResult callThirdPartyLotteryApi(GameLuckyGiftParam param, List orderIds) { try { - // 根据 standardId 获取对应的API配置 - ApiAccount apiConfig = luckyGiftApiConfig.getConfigByStandardId(param.getStandardId()); - log.info("使用API配置, standardId: {}, config: {}", param.getStandardId(), apiConfig); + if (CollectionUtils.isEmpty(orderIds) || StringUtils.isBlank(orderIds.get(0))) { + log.error("调用 HyApp 幸运礼物接口失败:request_id 为空"); + return createFailedResult(); + } + + String requestId = orderIds.get(0); + String configuredAppCode = luckyGiftApiConfig.getAppCode(); + if (StringUtils.isBlank(configuredAppCode)) { + log.error("调用 HyApp 幸运礼物接口失败:app_code 为空, requestId={}", requestId); + return createFailedResult(); + } + String appCode = configuredAppCode.trim().toLowerCase(Locale.ROOT); + long unitAmount = param.getGift().getGiftCandy().longValue(); + long totalAmount = Math.multiplyExact(unitAmount, param.getQuantity().longValue()); + + Map metadata = new LinkedHashMap<>(); + metadata.put("external_order_id", requestId); + metadata.put("gift_id", param.getGiftId().toString()); + metadata.put("room_id", param.getRoomId().toString()); + + Map requestData = new LinkedHashMap<>(); + requestData.put("app_code", appCode); + requestData.put("request_id", requestId); + requestData.put("external_user_id", param.getUserId().toString()); + requestData.put("gift_count", param.getQuantity()); + requestData.put("unit_amount", unitAmount); + requestData.put("currency", "COIN"); + requestData.put("metadata", metadata); + + log.info("调用 HyApp 幸运礼物接口, requestId={}, appCode={}, userId={}, giftId={}, " + + "giftCount={}, unitAmount={}", + requestId, appCode, param.getUserId(), param.getGiftId(), + param.getQuantity(), unitAmount); - Map requestData = new HashMap<>(); - requestData.put("room_id", param.getRoomId().toString()); - requestData.put("user_id", param.getUserId().toString()); - requestData.put("order_id", orderIds); - requestData.put("app_id", apiConfig.getAppId()); - requestData.put("app_channel", apiConfig.getAppChannel()); - requestData.put("gift_id", param.getGiftId().intValue()); - requestData.put("gift_price", param.getGift().getGiftCandy().intValue()); - requestData.put("gift_num", param.getQuantity()); - requestData.put("gift_is_free", 0); // 0收费 1免费 - requestData.put("timestamp", System.currentTimeMillis() / 1000); - - // 生成签名 - String signature = generateSignature(requestData, apiConfig.getAppKey()); - requestData.put("signature", signature); - - log.info("调用第三方幸运礼物API,请求参数:{}", JSON.toJSONString(requestData)); - - // 特别记录order_id的编码情况 - log.debug("order_id原始值: {}", JSON.toJSONString(orderIds)); - log.debug("order_id URL编码后: {}", URLEncoder.encode(JSON.toJSONString(orderIds), StandardCharsets.UTF_8)); - - // 发送HTTP请求 - 使用Hutool的HttpUtil String requestBody = JSON.toJSONString(requestData); - String response = HttpUtil.createPost(apiConfig.getUrl()) + int httpStatus; + String response; + try (HttpResponse httpResponse = HttpUtil.createPost(luckyGiftApiConfig.getEndpoint()) .header("Content-Type", "application/json") .header("Accept", "application/json") - .timeout(10000) + .header("X-Request-Id", requestId) + .timeout(luckyGiftApiConfig.getTimeoutMs()) .body(requestBody) - .execute() - .body(); + .execute()) { + httpStatus = httpResponse.getStatus(); + response = httpResponse.body(); + } if (StringUtils.isBlank(response)) { - log.error("第三方幸运礼物API返回空内容"); + log.error("HyApp 幸运礼物接口返回空内容, requestId={}, httpStatus={}", + requestId, httpStatus); return createFailedResult(); } - - log.info("第三方幸运礼物API响应:{}", response); - - // 解析响应 + JSONObject responseJson = JSON.parseObject(response); - if (responseJson.getInteger("code") == 200) { - JSONObject data = responseJson.getJSONObject("data"); - if (data == null) { - log.error("第三方幸运礼物API返回data为空"); - return createFailedResult(); - } - - JSONArray orderList = data.getJSONArray("OrderList"); - if (orderList == null || orderList.isEmpty()) { - log.error("第三方幸运礼物API返回订单列表为空"); - return createFailedResult(); - } - - ThirdPartyLotteryResult result = new ThirdPartyLotteryResult(); - result.setSuccess(true); - result.setOrderResults(new ArrayList<>()); - - for (int i = 0; i < orderList.size(); i++) { - JSONObject order = orderList.getJSONObject(i); - OrderResult orderResult = new OrderResult(); - orderResult.setOrderId(order.getString("order_id")); - orderResult.setIsWin(order.getInteger("is_win")); - orderResult.setRewardNum(order.getInteger("reward_num")); - result.getOrderResults().add(orderResult); - } - - return result; - } else { - log.error("第三方幸运礼物API返回错误,code:{},docs:{}", - responseJson.getInteger("code"), responseJson.getString("docs")); + if (httpStatus != 200 || !Objects.equals(responseJson.getInteger("code"), 0)) { + log.error("HyApp 幸运礼物接口返回失败, requestId={}, httpStatus={}, code={}, message={}", + requestId, httpStatus, responseJson.getInteger("code"), + responseJson.getString("message")); return createFailedResult(); } + + JSONObject data = responseJson.getJSONObject("data"); + if (Objects.isNull(data)) { + log.error("HyApp 幸运礼物接口成功响应缺少 data, requestId={}", requestId); + return createFailedResult(); + } + + Long responseGiftCount = data.getLong("gift_count"); + Long responseUnitAmount = data.getLong("unit_amount"); + Long responseTotalAmount = data.getLong("total_amount"); + // 幂等命中会返回首次请求结果,必须校验关键扣费事实,防止错误结果进入 Aslan 钱包。 + if (!Objects.equals(requestId, data.getString("request_id")) + || !Objects.equals(appCode, data.getString("app_code")) + || !Objects.equals(param.getUserId().toString(), data.getString("external_user_id")) + || !Objects.equals(param.getQuantity().longValue(), responseGiftCount) + || !Objects.equals(unitAmount, responseUnitAmount) + || !Objects.equals(totalAmount, responseTotalAmount) + || !Objects.equals("granted", data.getString("reward_status")) + || StringUtils.isBlank(data.getString("draw_id"))) { + log.error("HyApp 幸运礼物接口返回的幂等事实不匹配, requestId={}, data={}", + requestId, data.toJSONString()); + return createFailedResult(); + } + + long rewardAmount = Optional.ofNullable(data.getLong("reward_amount")).orElse(0L); + long multiplierPpm = Optional.ofNullable(data.getLong("multiplier_ppm")).orElse(0L); + OrderResult orderResult = new OrderResult() + .setOrderId(requestId) + .setDrawId(data.getString("draw_id")) + .setIsWin(rewardAmount > 0 ? 1 : 0) + .setRewardNum(rewardAmount) + .setMultiplierPpm(multiplierPpm); + + log.info("HyApp 幸运礼物接口调用成功, requestId={}, drawId={}, rewardAmount={}, " + + "multiplierPpm={}", + requestId, orderResult.getDrawId(), rewardAmount, multiplierPpm); + return new ThirdPartyLotteryResult() + .setSuccess(true) + .setOrderResults(List.of(orderResult)); } catch (Exception e) { - log.error("调用第三方幸运礼物API异常", e); + log.error("调用 HyApp 幸运礼物接口异常", e); return createFailedResult(); } } - /** - * 生成API签名. - * - * @param params 请求参数 - * @param appKey 应用密钥 - * @return 签名字符串 - */ - private String generateSignature(Map params, String appKey) { - try { - // 按键名升序排列参数 - TreeMap sortedParams = new TreeMap<>(params); - - // 构建签名字符串 - StringBuilder signBuilder = new StringBuilder(); - for (Map.Entry entry : sortedParams.entrySet()) { - if (!"signature".equals(entry.getKey())) { - if (!signBuilder.isEmpty()) { - signBuilder.append("&"); - } - - String key = entry.getKey(); - Object value = entry.getValue(); - String valueStr; - - // 特殊处理order_id字段,需要URL编码 - if ("order_id".equals(key) && value instanceof List) { - // 将List转为JSON字符串然后URL编码 - String jsonStr = JSON.toJSONString(value); - valueStr = URLEncoder.encode(jsonStr, StandardCharsets.UTF_8); - } else { - valueStr = String.valueOf(value); - } - - signBuilder.append(key).append("=").append(valueStr); - } - } - signBuilder.append("&app_key=").append(appKey); - - log.debug("签名原始字符串: {}", signBuilder.toString()); - - // MD5加密 - MessageDigest md = MessageDigest.getInstance("MD5"); - byte[] digest = md.digest(signBuilder.toString().getBytes("UTF-8")); - - // 转换为16进制字符串 - StringBuilder hexString = new StringBuilder(); - for (byte b : digest) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) { - hexString.append('0'); - } - hexString.append(hex); - } - - String signature = hexString.toString(); - log.debug("生成的签名: {}", signature); - return signature; - } catch (Exception e) { - log.error("生成签名异常", e); - return ""; - } - } - /** * 创建失败的结果. */ @@ -1023,7 +988,8 @@ public class GameLuckyGiftCommon { if (firstOrder.getIsWin() == 1 && firstOrder.getRewardNum() > 0) { // 第三方返回的奖励数量转换为倍数 // 根据礼物价格计算倍数:奖励金额 / 礼物价格 - rewardMultiple = firstOrder.getRewardNum() / param.getGift().getGiftCandy().intValue(); + rewardMultiple = Math.toIntExact( + firstOrder.getRewardNum() / param.getGift().getGiftCandy().longValue()); if (rewardMultiple <= 0) { rewardMultiple = 1; } @@ -1239,8 +1205,10 @@ public class GameLuckyGiftCommon { private static final long serialVersionUID = 1L; private String orderId; + private String drawId; private Integer isWin; // 0未中 1中奖 - private Integer rewardNum; // 奖励数量 + private Long rewardNum; // 奖励数量 + private Long multiplierPpm; // 奖励倍率,1000000 表示 1 倍 } /** @@ -1277,6 +1245,14 @@ public class GameLuckyGiftCommon { * @param balance 余额 */ private void sendMsg(GameLuckyGiftParam param, Integer multiple, BigDecimal balance, Long awardAmount) { + sendMsg(param, multiple, (long) multiple * 1_000_000L, balance, awardAmount); + } + + /** + * 发送中奖通知,同时保留新外接平台返回的 ppm 精确倍率。 + */ + private void sendMsg(GameLuckyGiftParam param, Integer multiple, Long multiplierPpm, + BigDecimal balance, Long awardAmount) { GameLuckyGiftMsgCO giftMsg = new GameLuckyGiftMsgCO() .setSysOrigin(param.getSysOrigin()) @@ -1289,6 +1265,7 @@ public class GameLuckyGiftCommon { .setAvatarFrameCover(param.getAvatarFrameCover()) .setAvatarFrameSvg(param.getAvatarFrameSvg()) .setMultiple(multiple) + .setMultiplierPpm(multiplierPpm) .setAwardAmount(awardAmount) .setGiftId(param.getGiftId()) .setMultipleType(getRewardMultipleType(multiple)) diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/game/GameLuckyGiftMsgCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/game/GameLuckyGiftMsgCO.java index 01ca358b..b55b7888 100644 --- a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/game/GameLuckyGiftMsgCO.java +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/game/GameLuckyGiftMsgCO.java @@ -72,6 +72,11 @@ public class GameLuckyGiftMsgCO extends ClientObject { */ private Integer multiple; + /** + * 精确中奖倍率,1000000 表示 1 倍;保留 multiple 兼容旧客户端。 + */ + private Long multiplierPpm; + /** * 中奖倍数类型. */ diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/LuckyGiftApiConfig.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/LuckyGiftApiConfig.java index d4c1cc17..81905d72 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/LuckyGiftApiConfig.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/config/LuckyGiftApiConfig.java @@ -16,6 +16,23 @@ import org.springframework.stereotype.Component; @RefreshScope public class LuckyGiftApiConfig { + /** + * HyApp 幸运礼物外接网关地址。 + * 使用独立字段,避免线上旧供应商的 lucky.gift.api.url 覆盖新接口。 + */ + private String endpoint = + "https://lucky-api.global-interaction.com/api/v1/lucky-gifts/send"; + + /** + * Aslan 在 HyApp 幸运礼物网关中的应用编码。 + */ + private String appCode = "aslan"; + + /** + * HTTP 调用超时,必须大于外接网关内部 3 秒处理超时。 + */ + private int timeoutMs = 10000; + /** * 多账号配置列表 */ @@ -124,6 +141,10 @@ public class LuckyGiftApiConfig { @Override public String toString() { return "LuckyGiftApiConfig{" + + "endpoint='" + endpoint + '\'' + + ", appCode='" + appCode + '\'' + + ", timeoutMs=" + timeoutMs + + ", " + "configs=" + configs + ", url='" + url + '\'' + ", appKey='***'" +