v5pay
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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";
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user