feat(other): switch lucky gift to HyApp gateway

This commit is contained in:
zhx 2026-07-15 21:09:31 +08:00
parent 756cb1b6f9
commit 0f4e440f2b
3 changed files with 143 additions and 140 deletions

View File

@ -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<String> 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<String> 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();
}
Map<String, Object> 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 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());
// 生成签名
String signature = generateSignature(requestData, apiConfig.getAppKey());
requestData.put("signature", signature);
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("external_order_id", requestId);
metadata.put("gift_id", param.getGiftId().toString());
metadata.put("room_id", param.getRoomId().toString());
log.info("调用第三方幸运礼物API请求参数{}", JSON.toJSONString(requestData));
Map<String, Object> 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);
// 特别记录order_id的编码情况
log.debug("order_id原始值: {}", JSON.toJSONString(orderIds));
log.debug("order_id URL编码后: {}", URLEncoder.encode(JSON.toJSONString(orderIds), StandardCharsets.UTF_8));
log.info("调用 HyApp 幸运礼物接口, requestId={}, appCode={}, userId={}, giftId={}, "
+ "giftCount={}, unitAmount={}",
requestId, appCode, param.getUserId(), param.getGiftId(),
param.getQuantity(), unitAmount);
// 发送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) {
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 (data == null) {
log.error("第三方幸运礼物API返回data为空");
if (Objects.isNull(data)) {
log.error("HyApp 幸运礼物接口成功响应缺少 data, requestId={}", requestId);
return createFailedResult();
}
JSONArray orderList = data.getJSONArray("OrderList");
if (orderList == null || orderList.isEmpty()) {
log.error("第三方幸运礼物API返回订单列表为空");
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();
}
ThirdPartyLotteryResult result = new ThirdPartyLotteryResult();
result.setSuccess(true);
result.setOrderResults(new ArrayList<>());
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);
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"));
return createFailedResult();
}
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<String, Object> params, String appKey) {
try {
// 按键名升序排列参数
TreeMap<String, Object> sortedParams = new TreeMap<>(params);
// 构建签名字符串
StringBuilder signBuilder = new StringBuilder();
for (Map.Entry<String, Object> 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))

View File

@ -72,6 +72,11 @@ public class GameLuckyGiftMsgCO extends ClientObject {
*/
private Integer multiple;
/**
* 精确中奖倍率1000000 表示 1 保留 multiple 兼容旧客户端
*/
private Long multiplierPpm;
/**
* 中奖倍数类型.
*/

View File

@ -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='***'" +