feat(other): switch lucky gift to HyApp gateway
This commit is contained in:
parent
756cb1b6f9
commit
0f4e440f2b
@ -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) {
|
||||
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<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))
|
||||
|
||||
@ -72,6 +72,11 @@ public class GameLuckyGiftMsgCO extends ClientObject {
|
||||
*/
|
||||
private Integer multiple;
|
||||
|
||||
/**
|
||||
* 精确中奖倍率,1000000 表示 1 倍;保留 multiple 兼容旧客户端。
|
||||
*/
|
||||
private Long multiplierPpm;
|
||||
|
||||
/**
|
||||
* 中奖倍数类型.
|
||||
*/
|
||||
|
||||
@ -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='***'" +
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user