Commit d3430f5e by Jony.L

新支付功能测试

parent da69e2e2
......@@ -39,6 +39,14 @@ public class AppResourceOrderController {
return success(respVO);
}
@PostMapping("/create-wpgj")
@Operation(summary = "创建算力资源订单(旺铺聚合支付)")
public CommonResult<AppResourceOrderCreateRespVO> createResourceOrderWithWpgj(@Valid @RequestBody AppResourceOrderCreateReqVO createReqVO) {
Long userId = getLoginUserId();
AppResourceOrderCreateRespVO respVO = resourceOrderService.createUserResourceOrderWithWpgj(userId, createReqVO);
return success(respVO);
}
@PutMapping("/cancel")
@Operation(summary = "取消算力资源订单")
public CommonResult<Boolean> cancelResourceOrder(@RequestParam("orderId") Long orderId) {
......
......@@ -74,6 +74,15 @@ public interface ResourceOrderService {
AppResourceOrderCreateRespVO createUserResourceOrder(Long userId, @Valid AppResourceOrderCreateReqVO createReqVO);
/**
* 用户创建算力资源订单(旺铺聚合支付)
*
* @param userId 用户ID
* @param createReqVO 创建信息
* @return 创建响应
*/
AppResourceOrderCreateRespVO createUserResourceOrderWithWpgj(Long userId, @Valid AppResourceOrderCreateReqVO createReqVO);
/**
* 更新订单为已支付(支付回调使用)
*
* @param orderId 订单ID
......
......@@ -49,6 +49,10 @@ import static com.luhu.computility.module.compute.enums.ErrorCodeConstants.*;
import static com.luhu.computility.module.compute.enums.ResourceOrderRefundStatus.NOT_REFUND;
import static com.luhu.computility.module.compute.enums.ResourceOrderInvoiceStatus.UNINVOICE;
import com.luhu.computility.module.compute.dal.redis.no.ResourceOrderNoRedisDAO;
import com.luhu.computility.module.pay.service.order.PayOrderService;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum;
/**
* 算力资源订单 Service 实现类
......@@ -76,6 +80,9 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
private PayOrderApi payOrderApi;
@Resource
private PayOrderService payOrderService;
@Resource
private ResourceOrderProperties resourceOrderProperties;
@Resource
......@@ -434,4 +441,64 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public AppResourceOrderCreateRespVO createUserResourceOrderWithWpgj(Long userId, AppResourceOrderCreateReqVO createReqVO) {
// 1. 构建订单
ResourceOrderDO order = buildResourceOrder(userId, createReqVO);
// 2. 保存订单
resourceOrderMapper.insert(order);
// 3. 创建旺铺聚合支付订单
if (order.getPaymentPrice() > 0) {
createPayOrderWithWpgj(order);
}
// 4. 返回结果
AppResourceOrderCreateRespVO respVO = new AppResourceOrderCreateRespVO();
respVO.setId(order.getId());
respVO.setOrderNo(order.getOrderNo());
respVO.setPayOrderId(order.getPayOrderId());
return respVO;
}
/**
* 创建旺铺聚合支付订单
*/
private void createPayOrderWithWpgj(ResourceOrderDO order) {
try {
// 1. 创建支付订单(用于数据库记录)
PayOrderCreateReqDTO payOrderCreateReqDTO = new PayOrderCreateReqDTO()
.setAppKey(resourceOrderProperties.getPayAppKey()).setUserIp(order.getUserIp());
payOrderCreateReqDTO.setMerchantOrderId(String.valueOf(order.getId()));
String subject = order.getSpuName() + " - " + getDurationDaysFromSku(order.getSkuId()) + "天";
subject = StrUtils.maxLength(subject, PayOrderCreateReqDTO.SUBJECT_MAX_LENGTH);
payOrderCreateReqDTO.setSubject(subject);
payOrderCreateReqDTO.setBody(subject);
payOrderCreateReqDTO.setPrice(order.getPaymentPrice().intValue())
.setExpireTime(addTime(resourceOrderProperties.getPayExpireTime()));
Long payOrderId = payOrderApi.createOrder(payOrderCreateReqDTO);
// 2. 提交支付订单(使用旺铺聚合支付)
AppPayOrderSubmitReqVO submitReqDTO = new AppPayOrderSubmitReqVO();
submitReqDTO.setId(payOrderId);
submitReqDTO.setChannelCode("wpgj_dynamic");
AppPayOrderSubmitRespVO submitRespVO = payOrderService.submitWpgjOrder(submitReqDTO, order.getUserIp());
// 3. 更新到算力资源订单上
resourceOrderMapper.updateById(new ResourceOrderDO().setId(order.getId()).setPayOrderId(payOrderId));
order.setPayOrderId(payOrderId);
log.info("[createPayOrderWithWpgj] 旺铺聚合支付订单创建成功,订单ID: {}, 支付订单ID: {}", order.getId(), payOrderId);
} catch (Exception e) {
log.error("[createPayOrderWithWpgj] 创建旺铺聚合支付订单失败", e);
throw new ServiceException("创建支付订单失败: " + e.getMessage());
}
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.controller.admin.notify;
import com.luhu.computility.framework.common.pojo.CommonResult;
import com.luhu.computility.framework.common.util.json.JsonUtils;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjCryptoUtils;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjPayProperties;
import com.luhu.computility.module.pay.service.order.PayOrderService;
import cn.hutool.json.JSONUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
import static com.luhu.computility.framework.common.pojo.CommonResult.success;
/**
* WPGJ旺铺聚合支付回调Controller
*
* @author jonyl
*/
@Tag(name = "管理后台 - WPGJ旺铺聚合支付回调")
@RestController
@RequestMapping("/pay/wpgj")
@Slf4j
public class AdminWpgjPayController {
@Resource
private PayOrderService payOrderService;
@Resource
private WpgjPayProperties wpgjPayProperties;
@PostMapping("/notify")
@Operation(summary = "WPGJ支付异步回调通知")
public CommonResult<String> notifyWpgjPay(@RequestBody String encryptedData) {
try {
log.info("[notifyWpgjPay] 收到WPGJ支付回调,加密数据: {}", encryptedData);
// 1. 解密回调数据
String decryptedJson = WpgjCryptoUtils.decryptResponse(encryptedData, wpgjPayProperties.getPrivateKey());
Map<String, Object> decryptedData = JSONUtil.parseObj(decryptedJson);
log.info("[notifyWpgjPay] 解密后的回调数据: {}", JsonUtils.toJsonString(decryptedData));
// 2. 解析回调数据,构造PayOrderRespDTO
PayOrderRespDTO notify = parseWpgjNotifyData(decryptedData);
// 3. 处理支付结果通知
// 由于WPGJ是特殊渠道,直接调用内部处理方法
payOrderService.notifyOrder(-1L, notify); // 使用-1表示WPGJ特殊渠道
log.info("[notifyWpgjPay] WPGJ支付回调处理完成");
return success("success");
} catch (Exception e) {
log.error("[notifyWpgjPay] WPGJ支付回调处理失败", e);
return success("fail");
}
}
/**
* 解析WPGJ回调数据为PayOrderRespDTO
*/
private PayOrderRespDTO parseWpgjNotifyData(Map<String, Object> data) {
PayOrderRespDTO notify = new PayOrderRespDTO();
// 根据WPGJ回调文档解析字段
notify.setOutTradeNo((String) data.get("out_trade_no")); // 商户订单号
notify.setChannelOrderNo((String) data.get("transaction_id")); // 第三方交易号
notify.setStatus(parseWpgjStatus((String) data.get("trade_state"))); // 交易状态
// 设置支付成功时间
if ("SUCCESS".equals(data.get("trade_state"))) {
notify.setSuccessTime(java.time.LocalDateTime.now());
}
// 设置其他必要的字段
notify.setChannelCode("wpgj_dynamic");
notify.setChannelErrorCode(null); // 成功时没有错误码
notify.setChannelErrorMsg(null);
notify.setRawData(JsonUtils.toJsonString(data)); // 保存原始数据
return notify;
}
/**
* 解析WPGJ交易状态
*/
private Integer parseWpgjStatus(String tradeState) {
if ("SUCCESS".equals(tradeState)) {
return com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum.SUCCESS.getStatus();
} else if ("CLOSED".equals(tradeState) || "REVOKED".equals(tradeState)) {
return com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum.CLOSED.getStatus();
} else {
return com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum.WAITING.getStatus();
}
}
}
\ No newline at end of file
......@@ -30,6 +30,11 @@ public class PayOrderRespDTO {
private String outTradeNo;
/**
* 支付渠道编码
*/
private String channelCode;
/**
* 支付渠道编号
*/
private String channelOrderNo;
......
......@@ -19,6 +19,7 @@ import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* 旺铺聚合支付加解密工具类
......@@ -223,6 +224,7 @@ public class WpgjCryptoUtils {
}
}
/**
* 生成序列号
*/
......
package com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luhu.computility.module.pay.enums.PayChannelEnum;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.AbstractPayClient;
import com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum;
import com.luhu.computility.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 旺铺聚合支付的 PayClient 实现类
*
* @author generated
*/
@Slf4j
public class WpgjPayClient extends AbstractPayClient<WpgjPayClientConfig> {
public WpgjPayClient(Long channelId, String channelCode, WpgjPayClientConfig config) {
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
}
@Override
protected void doInit() {
// 旺铺支付不需要特殊初始化
}
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable {
try {
// 构建业务数据
JSONObject businessData = buildBusinessData(reqDTO);
// 构建完整请求数据
String requestData = WpgjCryptoUtils.buildRequestData(businessData, config.getOrganizNo());
// 加密请求数据
String encryptedRequest = WpgjCryptoUtils.encryptRequest(requestData, config.getPublicKey());
// 发送HTTP请求
String apiUrl = config.getApiUrl() + "/industrial/payment/dynamic";
HttpResponse response = HttpUtil.createPost(apiUrl)
.body(encryptedRequest)
.contentType("application/json")
.timeout(30000)
.execute();
if (!response.isOk()) {
log.error("[doUnifiedOrder] 旺铺支付请求失败: status={}, body={}", response.getStatus(), response.body());
return PayOrderRespDTO.closedOf("HTTP_ERROR", "请求失败: " + response.getStatus(),
reqDTO.getOutTradeNo(), response.body());
}
// 解析响应
String responseBody = response.body();
JSONObject responseJson = JSONUtil.parseObj(responseBody);
// 检查响应状态
String code = responseJson.getStr("code");
if (!"0000".equals(code)) {
String msg = responseJson.getStr("msg", "未知错误");
log.error("[doUnifiedOrder] 旺铺支付返回错误: code={}, msg={}", code, msg);
return PayOrderRespDTO.closedOf(code, msg, reqDTO.getOutTradeNo(), responseBody);
}
// 解密响应数据获取payUrl
String decryptedData = WpgjCryptoUtils.decryptResponse(responseBody, config.getPrivateKey());
JSONObject dataJson = JSONUtil.parseObj(decryptedData);
String payUrl = dataJson.getStr("payUrl");
if (StrUtil.isBlank(payUrl)) {
log.error("[doUnifiedOrder] 旺铺支付响应中缺少payUrl");
return PayOrderRespDTO.closedOf("MISSING_PAY_URL", "响应中缺少支付链接",
reqDTO.getOutTradeNo(), responseBody);
}
// 返回成功结果
Map<String, Object> rawData = new HashMap<>();
rawData.put("serialNo", responseJson.getStr("serialNo"));
rawData.put("payUrl", payUrl);
rawData.put("response", responseBody);
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), payUrl,
reqDTO.getOutTradeNo(), rawData);
} catch (Exception e) {
log.error("[doUnifiedOrder] 旺铺支付统一下单异常", e);
return PayOrderRespDTO.closedOf("SYSTEM_ERROR", e.getMessage(),
reqDTO.getOutTradeNo(), e.getMessage());
}
}
@Override
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws Throwable {
try {
// 解密回调数据
String decryptedData = WpgjCryptoUtils.decryptResponse(body, config.getPrivateKey());
JSONObject dataJson = JSONUtil.parseObj(decryptedData);
// 获取订单状态和相关信息
String orderId = dataJson.getStr("order_id");
String orderStatus = dataJson.getStr("order_status");
String tradeNo = dataJson.getStr("trade_no");
String tradeTime = dataJson.getStr("trade_time");
// 判断支付状态
Integer status;
if ("1".equals(orderStatus)) {
status = PayOrderStatusEnum.SUCCESS.getStatus();
} else {
status = PayOrderStatusEnum.CLOSED.getStatus();
}
// 解析交易时间
LocalDateTime successTime = null;
if (StrUtil.isNotBlank(tradeTime)) {
try {
successTime = LocalDateTime.parse(tradeTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
log.warn("[doParseOrderNotify] 解析交易时间失败: {}", tradeTime, e);
}
}
// 构建原始数据
Map<String, Object> rawData = new HashMap<>();
rawData.put("decryptedData", decryptedData);
rawData.put("rawBody", body);
rawData.put("dataJson", dataJson);
return PayOrderRespDTO.of(status, tradeNo, null, successTime,
orderId, rawData);
} catch (Exception e) {
log.error("[doParseOrderNotify] 解析旺铺支付回调异常", e);
throw e;
}
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
// 旺铺支付暂时不支持主动查询订单状态
// 可以根据需要实现订单查询接口
log.warn("[doGetOrder] 旺铺支付暂不支持订单查询: {}", outTradeNo);
return null;
}
/**
* 构建业务数据
*
* @param reqDTO 统一下单请求
* @return 业务数据
*/
private JSONObject buildBusinessData(PayOrderUnifiedReqDTO reqDTO) {
JSONObject data = new JSONObject();
// 必填字段
data.set("organiz_no", config.getOrganizNo());
data.set("mer_no", config.getMerNo());
data.set("mer_code", config.getMerCode());
data.set("term_code", config.getTermCode());
// 金额转换:分转元
BigDecimal amountYuan = new BigDecimal(reqDTO.getPrice()).divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
data.set("order_amt", amountYuan.toString());
// 订单信息
data.set("mer_order_id", reqDTO.getOutTradeNo());
data.set("notifyurl", reqDTO.getNotifyUrl());
// 可选字段
if (StrUtil.isNotBlank(reqDTO.getReturnUrl())) {
data.set("return_url", reqDTO.getReturnUrl());
}
// 设置默认关闭时间30分钟
data.set("close_time", "30");
// 设置订单标题
String subject = reqDTO.getSubject();
if (StrUtil.isBlank(subject)) {
subject = "商品支付";
}
data.set("order_title", subject);
return data;
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj;
import com.luhu.computility.framework.common.util.validation.ValidationUtils;
import com.luhu.computility.module.pay.framework.pay.core.client.PayClientConfig;
import lombok.Data;
import javax.validation.ConstraintValidator;
import javax.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 旺铺聚合支付的 PayClientConfig 实现类
* 旺铺聚合支付配置属性
*
* @author generated
* @author jonyl
*/
@Data
public class WpgjPayClientConfig implements PayClientConfig {
@Component
@ConfigurationProperties(prefix = "computility.pay")
public class WpgjPayProperties {
/**
* 合作机构渠道号
* 测试环境:105549
* 组织编号
*/
@NotBlank(message = "合作机构渠道号不能为空")
private String organizNo;
/**
* 旺铺内部商户号,进件入网后返回
* 测试环境:99911325651RE1R
* 商户编号
*/
@NotBlank(message = "旺铺内部商户号不能为空")
private String merNo;
/**
* 商户号,进件入网后返回
* 测试环境:K20241200111267
* 商户代码
*/
@NotBlank(message = "商户号不能为空")
private String merCode;
/**
* 终端号,进件入网后返回
* 测试环境:1011215692596
* 终端编号
*/
@NotBlank(message = "终端号不能为空")
private String termCode;
/**
* 旺铺公钥,用于加密AES密钥
* API地址
*/
@NotBlank(message = "旺铺公钥不能为空")
private String publicKey;
private String apiUrl;
/**
* 机构私钥,用于解密回调数据中的AES密钥
* 回调地址
*/
@NotBlank(message = "机构私钥不能为空")
private String privateKey;
private String notifyUrl;
/**
* API地址
* 测试环境:https://stg5-qr.wpgjcs.com
* 公钥
*/
@NotBlank(message = "API地址不能为空")
private String apiUrl;
@Override
public void validate(ConstraintValidator validator) {
ValidationUtils.validate(validator, this);
}
private String publicKey;
/**
* 私钥
*/
private String privateKey;
}
\ No newline at end of file
......@@ -7,6 +7,8 @@ import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderExportR
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.dal.dataobject.order.PayOrderDO;
import com.luhu.computility.module.pay.dal.dataobject.order.PayOrderExtensionDO;
......@@ -92,6 +94,17 @@ public interface PayOrderService {
@NotEmpty(message = "提交 IP 不能为空") String userIp);
/**
* 提交旺铺聚合支付订单
* 此时,会发起旺铺聚合支付渠道的调用
*
* @param reqVO 提交请求
* @param userIp 提交 IP
* @return 提交结果
*/
AppPayOrderSubmitRespVO submitWpgjOrder(@Valid AppPayOrderSubmitReqVO reqVO,
@NotEmpty(message = "提交 IP 不能为空") String userIp);
/**
* 通知支付单成功
*
* @param channelId 渠道编号
......
......@@ -4,18 +4,26 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luhu.computility.framework.common.pojo.PageResult;
import com.luhu.computility.framework.common.util.date.LocalDateTimeUtils;
import com.luhu.computility.framework.common.util.number.MoneyUtils;
import com.luhu.computility.module.pay.framework.pay.core.client.PayClient;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjCryptoUtils;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjPayProperties;
import com.luhu.computility.framework.tenant.core.util.TenantUtils;
import com.luhu.computility.module.pay.api.order.dto.PayOrderCreateReqDTO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.convert.order.PayOrderConvert;
import com.luhu.computility.module.pay.dal.dataobject.app.PayAppDO;
import com.luhu.computility.module.pay.dal.dataobject.channel.PayChannelDO;
......@@ -74,6 +82,9 @@ public class PayOrderServiceImpl implements PayOrderService {
@Resource
private PayNotifyService notifyService;
@Resource
private WpgjPayProperties wpgjPayProperties;
@Override
public PayOrderDO getOrder(Long id) {
return payOrderMapper.selectById(id);
......@@ -181,6 +192,127 @@ public class PayOrderServiceImpl implements PayOrderService {
return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp);
}
/**
* 提交旺铺聚合支付订单
*
* @param reqVO 支付提交请求
* @param userIp 用户 IP
* @return 支付提交响应
*/
// 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
public AppPayOrderSubmitRespVO submitWpgjOrder(AppPayOrderSubmitReqVO reqVO, String userIp) {
// 1.1 获得 PayOrderDO ,并校验其是否存在
PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
// 2. 插入 PayOrderExtensionDO
String no = noRedisDAO.generate(payProperties.getOrderNoPrefix());
PayOrderExtensionDO orderExtension = PayOrderConvert.INSTANCE.convert(reqVO, userIp)
.setOrderId(order.getId()).setNo(no)
.setChannelId(-1L).setChannelCode("wpgj_dynamic") // 使用-1表示WPGJ特殊渠道,避免在validateOrderActuallyPaid中查找PayClient
.setStatus(PayOrderStatusEnum.WAITING.getStatus());
orderExtensionMapper.insert(orderExtension);
// 3. 直接调用WPGJ API
PayOrderRespDTO unifiedOrderResp = callWpgjApi(order, orderExtension, reqVO, userIp);
// 4. 构建返回结果
AppPayOrderSubmitRespVO appRespVO = new AppPayOrderSubmitRespVO();
if (unifiedOrderResp != null) {
appRespVO.setStatus(PayOrderStatusEnum.WAITING.getStatus());
appRespVO.setDisplayMode(unifiedOrderResp.getDisplayMode());
appRespVO.setDisplayContent(unifiedOrderResp.getDisplayContent());
} else {
appRespVO.setStatus(order.getStatus());
}
return appRespVO;
}
/**
* 调用WPGJ支付API
*/
private PayOrderRespDTO callWpgjApi(PayOrderDO order, PayOrderExtensionDO orderExtension,
AppPayOrderSubmitReqVO reqVO, String userIp) {
try {
log.info("[callWpgjApi] 开始调用WPGJ支付API,订单号: {}", orderExtension.getNo());
// 1. 构建业务数据
JSONObject businessData = new JSONObject();
businessData.set("mer_no", wpgjPayProperties.getMerNo());
businessData.set("mer_code", wpgjPayProperties.getMerCode());
businessData.set("term_code", wpgjPayProperties.getTermCode());
businessData.set("out_trade_no", orderExtension.getNo());
businessData.set("pay_amount", order.getPrice());
businessData.set("goods_name", order.getSubject());
businessData.set("goods_desc", order.getBody());
businessData.set("notify_url", wpgjPayProperties.getNotifyUrl());
businessData.set("return_url", reqVO.getReturnUrl());
businessData.set("time_expire", order.getExpireTime());
// 2. 构建完整请求数据
String requestData = WpgjCryptoUtils.buildRequestData(businessData, wpgjPayProperties.getOrganizNo());
String encryptedRequest = WpgjCryptoUtils.encryptRequest(requestData, wpgjPayProperties.getPublicKey());
// 3. 发送HTTP请求
HttpResponse response = HttpRequest.post(wpgjPayProperties.getApiUrl())
.header("Content-Type", "application/json")
.body(encryptedRequest)
.timeout(30000)
.execute();
// 4. 处理响应
if (!response.isOk()) {
log.error("[callWpgjApi] WPGJ API调用失败,HTTP状态码: {}, 响应: {}",
response.getStatus(), response.body());
throw exception(PAY_ORDER_SUBMIT_CHANNEL_ERROR, "HTTP_ERROR", "支付网关调用失败");
}
String encryptedResponse = response.body();
String decryptedResponse = WpgjCryptoUtils.decryptResponse(encryptedResponse, wpgjPayProperties.getPrivateKey());
JSONObject responseData = JSONUtil.parseObj(decryptedResponse);
log.info("[callWpgjApi] WPGJ API调用成功,响应: {}", decryptedResponse);
// 5. 解析响应结果
return parseWpgjApiResponse(responseData, orderExtension.getNo());
} catch (Exception e) {
log.error("[callWpgjApi] WPGJ API调用异常,订单号: {}", orderExtension.getNo(), e);
throw exception(PAY_ORDER_SUBMIT_CHANNEL_ERROR, "API_ERROR", "支付网关调用异常: " + e.getMessage());
}
}
/**
* 解析WPGJ API响应
*/
private PayOrderRespDTO parseWpgjApiResponse(JSONObject response, String outTradeNo) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
// 根据WPGJ API文档解析响应字段
respDTO.setOutTradeNo(outTradeNo);
respDTO.setChannelCode("wpgj_dynamic");
respDTO.setStatus(PayOrderStatusEnum.WAITING.getStatus());
// 解析二维码信息(假设WPGJ返回二维码URL)
if (response.containsKey("qr_code")) {
respDTO.setDisplayMode("qr_code");
respDTO.setDisplayContent(response.getStr("qr_code"));
} else if (response.containsKey("pay_url")) {
respDTO.setDisplayMode("pay_url");
respDTO.setDisplayContent(response.getStr("pay_url"));
}
// 检查是否有错误码
if (response.containsKey("resp_code") && !"00".equals(response.getStr("resp_code"))) {
respDTO.setChannelErrorCode(response.getStr("resp_code"));
respDTO.setChannelErrorMsg(response.getStr("resp_msg"));
}
respDTO.setRawData(JSONUtil.toJsonStr(response));
return respDTO;
}
private PayOrderDO validateOrderCanSubmit(Long id) {
PayOrderDO order = payOrderMapper.selectById(id);
if (order == null) { // 是否存在
......@@ -219,6 +351,11 @@ public class PayOrderServiceImpl implements PayOrderService {
// 情况二:调用三方接口,查询支付单状态,是不是已支付
PayClient<?> payClient = channelService.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
// WPGJ特殊渠道,没有PayClient,跳过查询
if (orderExtension.getChannelId() == -1L && "wpgj_dynamic".equals(orderExtension.getChannelCode())) {
log.debug("[validateOrderCanSubmit][WPGJ渠道({}) 跳过支付状态查询]", orderExtension.getChannelId());
return;
}
log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return;
}
......@@ -256,13 +393,133 @@ public class PayOrderServiceImpl implements PayOrderService {
@Override
public void notifyOrder(Long channelId, PayOrderRespDTO notify) {
// 校验支付渠道是否有效
// WPGJ特殊渠道处理
if (channelId == -1L && "wpgj_dynamic".equals(notify.getChannelCode())) {
log.info("[notifyOrder][WPGJ特殊渠道] 处理支付通知: {}", JSONUtil.toJsonStr(notify));
getSelf().notifyWpgjOrder(notify);
return;
}
// 原有渠道处理
PayChannelDO channel = channelService.validPayChannel(channelId);
// 更新支付订单为已支付
TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyOrder(channel, notify));
}
/**
* 处理WPGJ旺铺聚合支付通知
*
* @param notify 支付结果通知
*/
@Transactional(rollbackFor = Exception.class)
public void notifyWpgjOrder(PayOrderRespDTO notify) {
log.info("[notifyWpgjOrder] 开始处理WPGJ支付通知: {}", JSONUtil.toJsonStr(notify));
// 情况一:支付成功的回调
if (PayOrderStatusEnum.isSuccess(notify.getStatus())) {
notifyWpgjOrderSuccess(notify);
return;
}
// 情况二:支付失败的回调
if (PayOrderStatusEnum.isClosed(notify.getStatus())) {
notifyWpgjOrderClosed(notify);
}
// 情况三:WAITING:无需处理
log.info("[notifyWpgjOrder] WPGJ支付通知处理完成,状态: {}", notify.getStatus());
}
/**
* 处理WPGJ支付成功通知
*/
private void notifyWpgjOrderSuccess(PayOrderRespDTO notify) {
// 1. 更新 PayOrderExtensionDO 支付成功
PayOrderExtensionDO orderExtension = updateOrderSuccess(notify);
// 2. 更新 PayOrderDO 支付成功(使用WPGJ特殊渠道参数)
updateWpgjOrderSuccess(orderExtension, notify);
// 3. 插入支付通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.ORDER.getType(),
orderExtension.getOrderId());
log.info("[notifyWpgjOrderSuccess] WPGJ支付成功通知处理完成,订单ID: {}", orderExtension.getOrderId());
}
/**
* 处理WPGJ支付关闭通知
*/
private void notifyWpgjOrderClosed(PayOrderRespDTO notify) {
updateWpgjOrderClosed(notify);
log.info("[notifyWpgjOrderClosed] WPGJ支付关闭通知处理完成");
}
/**
* 更新WPGJ PayOrderDO 支付成功
*/
private void updateWpgjOrderSuccess(PayOrderExtensionDO orderExtension, PayOrderRespDTO notify) {
// 1. 判断 PayOrderDO 是否处于待支付
PayOrderDO order = payOrderMapper.selectById(orderExtension.getOrderId());
if (order == null) {
throw exception(PAY_ORDER_NOT_FOUND);
}
if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
&& Objects.equals(order.getExtensionId(), orderExtension.getId())) {
log.info("[updateWpgjOrderSuccess][order({}) 已经是已支付,无需更新]", order.getId());
return;
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 2. 更新 PayOrderDO(使用WPGJ固定参数)
int updateCounts = payOrderMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus())
.channelId(-1L).channelCode("wpgj_dynamic")
.successTime(notify.getSuccessTime()).extensionId(orderExtension.getId()).no(orderExtension.getNo())
.channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId())
.channelFeeRate(0.0) // WPGJ手续费暂时设为0
.channelFeePrice(0)
.build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
log.info("[updateWpgjOrderSuccess][order({}) 更新为已支付]", order.getId());
}
/**
* 更新WPGJ PayOrderExtensionDO 关闭
*/
private void updateWpgjOrderClosed(PayOrderRespDTO notify) {
// 1. 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
if (orderExtension == null) {
log.warn("[updateWpgjOrderClosed] 找不到PayOrderExtensionDO,订单号: {}", notify.getOutTradeNo());
return;
}
if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新
log.info("[updateWpgjOrderClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId());
return;
}
// 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
log.info("[updateWpgjOrderClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
return;
}
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
log.warn("[updateWpgjOrderClosed][orderExtension({}) 状态不是待支付,无需更新: {}", orderExtension.getId(), orderExtension.getStatus());
return;
}
// 2. 更新 PayOrderExtensionDO
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
PayOrderExtensionDO.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify))
.channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
log.warn("[updateWpgjOrderClosed][orderExtension({}) 更新失败,可能状态已变更]", orderExtension.getId());
return;
}
log.info("[updateWpgjOrderClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId());
}
/**
* 通知并更新订单的支付结果
*
* @param channel 支付渠道
......
......@@ -165,9 +165,55 @@ computility:
captcha:
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
pay:
order-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
refund-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
transfer-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
# order-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
# refund-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
# transfer-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
# WPGJ旺铺聚合支付配置
organiz-no: 105549
mer-no: 99911325651RE1R
mer-code: K20241200111267
term-code: 1011215692596
api-url: https://stg5-qr.wpgjcs.com
notify-url: https://phslgld.hnluchuan.com/admin-api/pay/wpgj/notify
public-key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4985PvG6DGbmIia0VV2I
wLWj5mYe2gfeDSAqV0YrGAG0MFrw6Cak/anNDNgDeMT3V3tbvCnPw1T5Th0Z3PAW
Wixdk6HyLkSAtHwpGG5O4uHKTMOnRuOjSMFiJm2RWyYbD0bfGJ4OVWadyrLIA4JA
MBkfVS1XWfJG5ZX26DjUk0EahoKgmIwrXVGbN1wQMtB6v5D0XER6WzgAdWwsKHKq
GGIxO/t2B23Y8JkfosPPMCm5jxbvg65ACaciyxJ8qVR8I/4yHxCOKDdyRxfrWkPE
v5tYnfdJIXHUmJMisn39F+651sVw5V8nRZqshm9KbfXWzZA0uYUfu4GItlvXaOWb
gwIDAQAB
-----END PUBLIC KEY-----
private-key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCthyDY1HX4+Ls3
I7IAUG5PuerDsvUkCjrZpx+OQ8TtDnGDlhNKtH3MtFu3In4txQnjyGc3wv0k/QGU
TYKHy8dWfaOts2v+5ycTTLs5OvtMsGHp00jYC/4444IA3atIn2qrYfAhBwv1UnCM
DxodYSeF8ZFnGDBjsx6v2VGVX9fpHwm/w1OGQtqbeyOO/CWkYHXDCgK4iMeDm81d
u31t3C6babloomkU+ZZ61FGSN8h3Ua2NLrJrzpIs4x48bc2QwvWDbTXjyage803U
momO7ZMv+w06xyZTGnT7j21ZxR1pYlcIcyd7SYpzxQTbTj7HAQsH5ncjoayxpldn
MfIwodWHAgMBAAECggEAeEv6SHcZhc0hNHuJddBMePbBOoQAj8iLvcGvDZgLl+9w
YDGVFDAGGfhasJ237ZDwZznR3IVPqum+w4nsWKm7eklFRDDjSGYE38tM68Lo3eRV
LJ1tnWDLD7Mgt+r0SDdD4qXVq3/QGXTaUy5RA1cL6D5sd5T8TtGpD21PsocsDg5z
HxzhGaxK1fyu2DUQSvnlWbIE8l36N6S+CZoiUOL8htFNG6C/IUGDwLvRPljHUs2y
tcgKq6o3nTrjnhcJre5B3oUB6Y/tjFy/4siqUDKQ2XRLAaW3dwIANEOLaxfMlxNC
SVf1uJ4sdIKimbPdzUzSMJhri4VF6qhaZKdw7Okj4QKBgQDTkT72Fe4ityBQBAQ7
rbMThYkHCzc1G5peHo+bPpIf7boSjglNUgH0x3DkvnqR4+wxSomfK/lGFPYmzesy
5eaXSgrm+RPYsiG20PHnUR7of/hVfoYpuvWXBFMWA0dLQVG/QOG5icGIcYuzbf1b
jtChTUZfARaprw9h/lO8EZACVwKBgQDR+LebAXdxIsi1gUnffn/9L0qGqk9mNcXS
UEhvU9WEzJZv4eg1ldR4o7WilLiIWqZH/YxadNKAORDh/euNHE9ECRQkRTtpzsrt
pmVsQDPkskTx7jq0Em+wS5aQt76jaXegXnl1juzVkG5j8j8aCcuckuz5EkKE7dpC
RDoH3rioUQKBgCu1wnBNL5n0g7SEX+uEg47w89KEuq86OLZhUgIc90upbdwPhFR5
MPZFjkl0IRarJE06vRwplaKpCgISYRlu7ikrnv0BUNxgUR7YJGJKHDyk08W2Ejnk
6oB84D2YB87AfAcrfHc8wsNR0i8L9mBILUpmELfoHaqJBICWbu5YIoyZAoGBALj6
bIZaHGfHRRrh87q5LvhWGUnZ8GkTJlvJ7oZlTM3Zjw6x7miGvtApKXM5ofg+JA94
B5YJJQlhPMZbdJC0SACgHuZ1N4pXa99BoEhlp1Hq22Jnhb/uHgcqEFMeDQzZdVIK
6OXwLCVZQGc8murNhltB9RLYS1HgWU/X22fjhrqBAoGAV8rbzRN9QmApZ360FckW
ATnnnTQJNlNn+kLRZoxG8rm/5U8u/LJlt6wTkWsEq4V3LqT5ITDsyg889OqngSuJ
lsHaEF/T7Ik3PXScTYabZk7Ar1nC/mFk0R3/6qRCBfiZZJVcpgSu85qPSP9NxtIS
9Kfs6NP5EdnhyJtr8P5GDjo=
-----END PRIVATE KEY-----
access-log: # 访问日志的配置项
enable: true
demo: false # 开启演示模式
......@@ -223,3 +269,4 @@ iot:
# 插件配置
pf4j:
pluginsDir: ${user.home}/plugins # 插件目录
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment