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());
}
}

View File

@@ -312,7 +312,8 @@ public class ChargeService extends BaseService {
List<String> h5PayChannels = Arrays.asList(Constant.ChargeChannel.payermax,
Constant.ChargeChannel.my_card,
Constant.ChargeChannel.start_pay,
Constant.ChargeChannel.razer);
Constant.ChargeChannel.razer,
Constant.ChargeChannel.v5pay);
Long amountLong = h5PayChannels.contains(chargeRecord.getChannel()) ?
chargeRecord.getLocalAmount() : chargeRecord.getAmount();
String chargeRecordProdId = chargeRecord.getChargeProdId();

View File

@@ -0,0 +1,129 @@
package com.accompany.business.controller.apppay;
import com.accompany.business.service.ChargeService;
import com.accompany.common.constant.Constant;
import com.accompany.common.status.BusiStatus;
import com.accompany.core.exception.ServiceException;
import com.accompany.payment.model.ChargeRecord;
import com.accompany.payment.service.ChargeRecordService;
import com.accompany.payment.v5pay.V5PayService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.accompany.common.redis.RedisKey.v5pay_lock;
/**
* 0 未支付
* 1 支付处理中
* 2 支付成功
* 3 支付失败
* 4 退款处理中
* 5 退款成功
* 6 退款失败
*/
@RestController
@RequestMapping("/payment/v5pay")
@Slf4j
public class V5PayController {
@Autowired
private ChargeRecordService chargeRecordService;
@Autowired
private ChargeService chargeService;
@Autowired
private V5PayService v5PayService;
@Autowired
private RedissonClient redissonClient;
@PostMapping(value = "/callback")
public String callback(HttpServletRequest request) {
RLock lock = null;
boolean isLocked = false;
try {
// 1. 读取请求体中的 JSON 数据
BufferedReader reader = request.getReader();
StringBuilder jsonStr = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
jsonStr.append(line);
}
log.info("V5PayController:params:{}" , jsonStr);
// 2. 解析为 JSONObject
JSONObject jsonObject = JSON.parseObject(jsonStr.toString());
String chargeRecordId = jsonObject.getString("orderNo");
lock = redissonClient.getLock(v5pay_lock.getKey(chargeRecordId));
isLocked = lock.tryLock(5, TimeUnit.SECONDS);
if (!isLocked) {
throw new ServiceException(BusiStatus.SERVERBUSY);
}
Map<String, Object> resultObject = v5PayService.convertJsonToMapExcludeSign(jsonObject);
String responseSign = v5PayService.signature(v5PayService.buildPlainText(resultObject));
if (!responseSign.equalsIgnoreCase(jsonObject.getString("sign"))) {
log.info("V5PayController,sign-error,responseSign:{},:json:{}" , responseSign, jsonStr);
return "success";
}
ChargeRecord chargeRecordById = chargeRecordService.getChargeRecordById(chargeRecordId);
if (chargeRecordById == null) {
log.info("V5PayController empty charge, chargeId:{}" , chargeRecordId);
return "success";
}
if (Constant.ChargeRecordStatus.finish.equals(chargeRecordById.getChargeStatus())) {
log.info("V5PayController charge finish, chargeId:{}" , chargeRecordId);
return "success";
}
if (!Constant.ChargeRecordStatus.create.equals(chargeRecordById.getChargeStatus())) {
log.info("V5PayController charge_status fail, charge:{}" , JSONObject.toJSONString(chargeRecordById));
return "success";
}
String pinngxxId = jsonObject.getString("transactionId");
chargeRecordById.setPingxxChargeId(pinngxxId);
String paymentStatusCode = jsonObject.getString("status");
if ("2".equals(paymentStatusCode)) {
String currencyCode = jsonObject.getString("currency");
Double amountValue = Double.valueOf(jsonObject.getString("amount")) * 100;
if (chargeRecordById.getLocalAmount().doubleValue() != amountValue || !chargeRecordById.getLocalCurrencyCode().equals(currencyCode)) {
log.info("V5PayController amount fail, chargeRecordId:{},chargeLocalAmount:{}, callBackAmound:{}" ,
chargeRecordId, chargeRecordById.getLocalAmount(), amountValue);
return "success";
}
String paymentStatusDate = jsonObject.getString("successTime");
chargeRecordById.setUpdateTime(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").parse(paymentStatusDate));
chargeService.updateAppPayData(chargeRecordById);
} else {
chargeRecordById.setChargeStatus(Constant.ChargeRecordStatus.error);
chargeRecordById.setChargeDesc(jsonObject.getString("message"));
chargeRecordService.updateChargeRecord(chargeRecordById);
}
log.info("end handle v5pay notification");
} catch (Exception e) {
log.info("V5PayController,e:{}" , e.getMessage(), e);
throw new ServiceException(BusiStatus.SERVERBUSY);
} finally {
if (lock != null && isLocked) {
lock.unlock();
}
}
return "success";
}
}