This commit is contained in:
2025-07-14 18:28:54 +08:00
parent c09d2ad9b8
commit df4b8caf8b
8 changed files with 332 additions and 2 deletions

View File

@@ -511,6 +511,8 @@ public class Constant {
public static final String razer = "razer";
// give
public static final String give = "give";
// give
public static final String v5pay = "v5pay";
}

View File

@@ -1460,7 +1460,9 @@ public enum RedisKey {
first_charge_ip, //首充ip状态
first_charge_device, //首充设备状态
lock_user_pack; //礼包锁
lock_user_pack, //礼包锁
v5pay_lock, //v5pay支付锁
;
public String getKey() {

View File

@@ -0,0 +1,42 @@
package com.accompany.payment.v5pay;
import lombok.Data;
@Data
public class V5PayResponseVo {
// ============== 基础响应参数 ==============
/** 响应码4位 */
private String code;
/** 响应描述 */
private String message;
/** 页面支付地址 */
private String checkoutUrl;
/** 过期时间(时间戳) */
private Long expiresAt;
/** 签名32位 */
private String sign;
// ============== 换汇交易额外参数 ==============
/** 换汇前币种3位如 USD */
private String fromCurrency;
/** 换汇后币种3位如 EUR */
private String toCurrency;
/** 原始金额(换汇前,下单币种) */
private Number originPayinAmt;
/** 交易金额(换汇后,用户付款金额) */
private Number payinAmt;
/** 汇率 */
private Number exchangeRate;
/** 汇率时间(时间戳) */
private Long rateDateTime;
}

View File

@@ -0,0 +1,46 @@
package com.accompany.payment.strategy;
import com.accompany.common.constant.Constant;
import com.accompany.common.status.BusiStatus;
import com.accompany.core.base.UidContextHolder;
import com.accompany.core.exception.ServiceException;
import com.accompany.payment.annotation.PayChannelSupport;
import com.accompany.payment.constant.ChargeUserLimitConstant;
import com.accompany.payment.constant.PayConstant;
import com.accompany.payment.model.ChargeProd;
import com.accompany.payment.model.ChargeRecord;
import com.accompany.payment.service.ChargeUserLimitService;
import com.accompany.payment.v5pay.V5PayResponseVo;
import com.accompany.payment.v5pay.V5PayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
@PayChannelSupport(Constant.ChargeChannel.v5pay)
public class V5PayStrategy extends AbstractPayStrategy {
@Autowired
private ChargeUserLimitService chargeUserLimitService;
@Autowired
private V5PayService v5PayService;
@Override
public Object pay(PayContext context) throws Exception {
chargeUserLimitService.chargeLimitCheck(UidContextHolder.get(), ChargeUserLimitConstant.LIMIT_TYPE_OF_H5);
ChargeRecord chargeRecord = context.getChargeRecord();
ChargeProd chargeProd = context.getChargeProd();
V5PayResponseVo orderRes = v5PayService.createOrder(chargeRecord, chargeProd, context.getSuccessUrl());
if (!orderRes.getCode().equalsIgnoreCase("1000")) {
throw new ServiceException(BusiStatus.SERVERERROR);
}
Map<String, Object> appMap = new HashMap<>();
appMap.put(PayConstant.H5_PAY_URL_FIELD, orderRes.getCheckoutUrl());
appMap.put(PayConstant.H5_PAY_NICK_FIELD, context.getNick());
appMap.put(PayConstant.H5_PAY_ERBANNO_FIELD, context.getErbanNo());
return appMap;
}
}

View File

@@ -0,0 +1,18 @@
package com.accompany.payment.v5pay;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("v5pay")
@Data
public class V5PayConfig {
private String appKey;
private String secretKey;
private String merchantNo;
private String createUrl;
private String callbackUrl;
private String redirectUrl;
}

View File

@@ -0,0 +1,90 @@
package com.accompany.payment.v5pay;
import com.accompany.common.utils.HttpUtils;
import com.accompany.payment.model.ChargeProd;
import com.accompany.payment.model.ChargeRecord;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* https://api-doc.v5pay.com/#/zh-cn/common/payin-cashier
*/
@Slf4j
@Service
public class V5PayService {
@Autowired
private V5PayConfig v5PayConfig;
public V5PayResponseVo createOrder(ChargeRecord chargeRecord, ChargeProd chargeProd, String successUrl) {
try {
Map<String, Object> formMap = new HashMap<>();
formMap.put("merchantNo", v5PayConfig.getMerchantNo());
formMap.put("appKey", v5PayConfig.getAppKey());
formMap.put("sysCountryCode", chargeProd.getCountry());
formMap.put("currency", chargeRecord.getLocalCurrencyCode());
formMap.put("orderNo", chargeRecord.getChargeRecordId());
formMap.put("amount", String.valueOf(BigDecimal.valueOf(chargeRecord.getLocalAmount())
.divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.DOWN)));
formMap.put("sign", signature(buildPlainText(formMap)));
formMap.put("redirectUrl", successUrl);
formMap.put("callbackUrl", v5PayConfig.getCallbackUrl());
Map<String, String> headerMap = new HashMap<>();
headerMap.put("Content-Type", "application/json");
String jsonString = JSON.toJSONString(formMap);
log.info("v5pay-post :{}", jsonString);
String resultBody = HttpUtils.doPostForJson(v5PayConfig.getCreateUrl(), jsonString);
log.info("V5PayService.createOrder resultBody:{}" , resultBody);
JSONObject jsonObject = JSON.parseObject(resultBody);
Map<String, Object> resultObject = convertJsonToMapExcludeSign(jsonObject);
String responseSign = signature(buildPlainText(resultObject));
if (responseSign.equalsIgnoreCase(jsonObject.getString("sign"))) {
return JSONObject.parseObject(resultBody, V5PayResponseVo.class);
}
} catch (Exception e) {
log.error("V5PayService.createOrder:e:{}", e);
}
return new V5PayResponseVo();
}
public Map<String, Object> convertJsonToMapExcludeSign(JSONObject jsonObject) {
Map<String, Object> map = new HashMap<>();
for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
String key = entry.getKey();
if (!"sign".equals(key)) { // 排除 sign 字段
map.put(key, entry.getValue());
}
}
return map;
}
public String buildPlainText(Map<String, Object> data) {
return data.entrySet().stream()
// 过滤掉 value 为 null 或空字符串的键值对
.filter(entry -> entry.getValue() != null)
// 按 key 的字典序排序
.sorted(Map.Entry.comparingByKey())
// 拼接成 key=value 格式
.map(entry -> entry.getKey() + "=" + entry.getValue())
// 用 & 连接所有键值对
.collect(Collectors.joining("&"));
}
public String signature(String signStr) {
return DigestUtils.md5Hex(signStr + v5PayConfig.getSecretKey());
}
}