feat(payment): 初始化支付模块核心代码

- 添加支付常量类PaymentConstants,定义支付状态、微信、支付宝、银联等相关常量
- 创建微信支付类型常量类WechatPayType,支持JSAPI、NATIVE、H5、APP支付方式
- 新增支付控制器PaymentController,提供创建支付、查询状态、退款等统一接口
- 实现支付回调控制器PaymentNotifyController,处理微信、支付宝、银联异步通知
- 添加支付请求数据传输对象PaymentRequest,支持多种支付方式参数校验
- 定义支付响应、状态更新请求等相关DTO类- 集成Swagger注解,完善接口文档说明- 添加参数校验和异常处理机制,确保支付流程安全可靠
This commit is contained in:
2025-11-03 12:31:47 +08:00
parent 894b4bf7ce
commit 5749fab9e8
25 changed files with 4952 additions and 9 deletions

View File

@@ -0,0 +1,153 @@
package com.gxwebsoft.payment.strategy;
import com.gxwebsoft.payment.dto.PaymentRequest;
import com.gxwebsoft.payment.dto.PaymentResponse;
import com.gxwebsoft.payment.enums.PaymentType;
import com.gxwebsoft.payment.exception.PaymentException;
import java.util.Map;
/**
* 支付策略接口
* 定义所有支付方式的统一接口
*
* @author 科技小王子
* @since 2025-01-26
*/
public interface PaymentStrategy {
/**
* 获取支持的支付类型
*
* @return 支付类型
*/
PaymentType getSupportedPaymentType();
/**
* 验证支付请求参数
*
* @param request 支付请求
* @throws PaymentException 参数验证失败时抛出
*/
void validateRequest(PaymentRequest request) throws PaymentException;
/**
* 创建支付订单
*
* @param request 支付请求
* @return 支付响应
* @throws PaymentException 支付创建失败时抛出
*/
PaymentResponse createPayment(PaymentRequest request) throws PaymentException;
/**
* 查询支付状态
*
* @param orderNo 订单号
* @param tenantId 租户ID
* @return 支付响应
* @throws PaymentException 查询失败时抛出
*/
PaymentResponse queryPayment(String orderNo, Integer tenantId) throws PaymentException;
/**
* 处理支付回调通知
*
* @param headers 请求头
* @param body 请求体
* @param tenantId 租户ID
* @return 处理结果,返回给第三方的响应内容
* @throws PaymentException 处理失败时抛出
*/
String handleNotify(Map<String, String> headers, String body, Integer tenantId) throws PaymentException;
/**
* 申请退款
*
* @param orderNo 订单号
* @param refundNo 退款单号
* @param totalAmount 订单总金额
* @param refundAmount 退款金额
* @param reason 退款原因
* @param tenantId 租户ID
* @return 退款响应
* @throws PaymentException 退款申请失败时抛出
*/
PaymentResponse refund(String orderNo, String refundNo,
java.math.BigDecimal totalAmount, java.math.BigDecimal refundAmount,
String reason, Integer tenantId) throws PaymentException;
/**
* 查询退款状态
*
* @param refundNo 退款单号
* @param tenantId 租户ID
* @return 退款查询响应
* @throws PaymentException 查询失败时抛出
*/
PaymentResponse queryRefund(String refundNo, Integer tenantId) throws PaymentException;
/**
* 关闭订单
*
* @param orderNo 订单号
* @param tenantId 租户ID
* @return 关闭结果
* @throws PaymentException 关闭失败时抛出
*/
boolean closeOrder(String orderNo, Integer tenantId) throws PaymentException;
/**
* 是否支持退款
*
* @return true表示支持退款
*/
default boolean supportRefund() {
return false;
}
/**
* 是否支持查询
*
* @return true表示支持查询
*/
default boolean supportQuery() {
return false;
}
/**
* 是否支持关闭订单
*
* @return true表示支持关闭订单
*/
default boolean supportClose() {
return false;
}
/**
* 是否需要异步通知
*
* @return true表示需要异步通知
*/
default boolean needNotify() {
return false;
}
/**
* 获取策略名称
*
* @return 策略名称
*/
default String getStrategyName() {
return getSupportedPaymentType().getName();
}
/**
* 获取策略描述
*
* @return 策略描述
*/
default String getStrategyDescription() {
return getSupportedPaymentType().getName() + "支付策略";
}
}

View File

@@ -0,0 +1,401 @@
package com.gxwebsoft.payment.strategy;
import cn.hutool.core.util.IdUtil;
import com.gxwebsoft.common.core.utils.CommonUtil;
import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.payment.constants.PaymentConstants;
import com.gxwebsoft.payment.dto.PaymentRequest;
import com.gxwebsoft.payment.dto.PaymentResponse;
import com.gxwebsoft.payment.enums.PaymentStatus;
import com.gxwebsoft.payment.enums.PaymentType;
import com.gxwebsoft.payment.exception.PaymentException;
import com.gxwebsoft.payment.service.WxPayConfigService;
import com.gxwebsoft.payment.service.WxPayNotifyService;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import com.wechat.pay.java.service.payments.nativepay.model.QueryOrderByOutTradeNoRequest;
import com.wechat.pay.java.service.payments.model.Transaction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Map;
/**
* 微信Native支付策略实现
* 处理微信Native扫码支付
*
* @author 科技小王子
* @since 2025-01-26
*/
@Slf4j
@Component
public class WechatNativeStrategy implements PaymentStrategy {
@Resource
private WxPayConfigService wxPayConfigService;
@Resource
private WxPayNotifyService wxPayNotifyService;
@Override
public PaymentType getSupportedPaymentType() {
return PaymentType.WECHAT_NATIVE;
}
@Override
public void validateRequest(PaymentRequest request) throws PaymentException {
if (request == null) {
throw PaymentException.paramError("支付请求不能为空");
}
if (request.getTenantId() == null) {
throw PaymentException.paramError("租户ID不能为空");
}
if (request.getUserId() == null) {
throw PaymentException.paramError("用户ID不能为空");
}
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw PaymentException.amountError("支付金额必须大于0");
}
if (!StringUtils.hasText(request.getSubject())) {
throw PaymentException.paramError("订单标题不能为空");
}
// 验证金额范围
if (request.getAmount().compareTo(new BigDecimal("0.01")) < 0) {
throw PaymentException.amountError("支付金额不能小于0.01元");
}
if (request.getAmount().compareTo(new BigDecimal("999999.99")) > 0) {
throw PaymentException.amountError("支付金额不能超过999999.99元");
}
// 验证订单标题长度
if (request.getSubject().length() > PaymentConstants.Config.DESCRIPTION_MAX_LENGTH) {
throw PaymentException.paramError("订单标题长度不能超过" + PaymentConstants.Config.DESCRIPTION_MAX_LENGTH + "个字符");
}
log.debug("微信Native支付请求参数验证通过, 租户ID: {}, 金额: {}",
request.getTenantId(), request.getFormattedAmount());
}
@Override
public PaymentResponse createPayment(PaymentRequest request) throws PaymentException {
log.info("{}, 支付类型: {}, 租户ID: {}, 金额: {}",
PaymentConstants.LogMessage.PAYMENT_START, getSupportedPaymentType(),
request.getTenantId(), request.getFormattedAmount());
try {
// 验证请求参数
validateRequest(request);
// 生成订单号
String orderNo = generateOrderNo(request);
log.info("生成的订单号: {}", orderNo);
// 获取Native支付的Payment配置包含appId等信息
Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(request.getTenantId());
// 获取微信支付配置
Config wxPayConfig = wxPayConfigService.getWxPayConfig(request.getTenantId());
// 构建预支付请求
PrepayRequest prepayRequest = buildPrepayRequest(request, orderNo, paymentConfig);
// 调用微信支付API
PrepayResponse prepayResponse = callWechatPayApi(prepayRequest, wxPayConfig);
// 构建响应
PaymentResponse response = PaymentResponse.wechatNative(
orderNo, prepayResponse.getCodeUrl(), request.getAmount(), request.getTenantId());
response.setUserId(request.getUserId());
// 确保orderNo被正确设置
response.setOrderNo(orderNo);
// 调试日志检查响应对象的orderNo
log.info("构建的响应对象 - orderNo: {}, codeUrl: {}, success: {}",
response.getOrderNo(), response.getCodeUrl(), response.getSuccess());
log.info("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 金额: {}",
PaymentConstants.LogMessage.PAYMENT_SUCCESS, getSupportedPaymentType(),
request.getTenantId(), orderNo, request.getFormattedAmount());
return response;
} catch (PaymentException e) {
log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}",
PaymentConstants.LogMessage.PAYMENT_FAILED, getSupportedPaymentType(),
request.getTenantId(), e.getMessage());
throw e;
} catch (Exception e) {
log.error("{}, 支付类型: {}, 租户ID: {}, 系统错误: {}",
PaymentConstants.LogMessage.PAYMENT_FAILED, getSupportedPaymentType(),
request.getTenantId(), e.getMessage(), e);
throw PaymentException.systemError("微信Native支付创建失败: " + e.getMessage(), e);
}
}
@Override
public PaymentResponse queryPayment(String orderNo, Integer tenantId) throws PaymentException {
log.info("开始查询微信Native支付状态, 订单号: {}, 租户ID: {}", orderNo, tenantId);
try {
// 参数验证
if (!StringUtils.hasText(orderNo)) {
throw PaymentException.paramError("订单号不能为空");
}
if (tenantId == null) {
throw PaymentException.paramError("租户ID不能为空");
}
// 获取支付配置(包含商户号等信息)
Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(tenantId);
// 获取微信支付配置
Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId);
// 调用微信支付查询API
return queryWechatPaymentStatus(orderNo, tenantId, paymentConfig, wxPayConfig);
} catch (Exception e) {
if (e instanceof PaymentException) {
throw e;
}
log.error("查询微信Native支付状态失败, 订单号: {}, 租户ID: {}, 错误: {}",
orderNo, tenantId, e.getMessage(), e);
throw PaymentException.systemError("查询微信支付状态失败: " + e.getMessage(), e);
}
}
@Override
public String handleNotify(Map<String, String> headers, String body, Integer tenantId) throws PaymentException {
log.info("{}, 支付类型: {}, 租户ID: {}",
PaymentConstants.LogMessage.NOTIFY_START, getSupportedPaymentType(), tenantId);
try {
// 委托给专门的回调处理服务
return wxPayNotifyService.handlePaymentNotify(headers, body, tenantId);
} catch (Exception e) {
log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}",
PaymentConstants.LogMessage.NOTIFY_FAILED, getSupportedPaymentType(),
tenantId, e.getMessage(), e);
throw PaymentException.systemError("微信支付回调处理失败: " + e.getMessage(), e);
}
}
@Override
public PaymentResponse refund(String orderNo, String refundNo, BigDecimal totalAmount,
BigDecimal refundAmount, String reason, Integer tenantId) throws PaymentException {
// TODO: 实现微信支付退款逻辑
throw PaymentException.unsupportedPayment("暂不支持微信支付退款", PaymentType.WECHAT_NATIVE);
}
@Override
public PaymentResponse queryRefund(String refundNo, Integer tenantId) throws PaymentException {
// TODO: 实现微信退款查询逻辑
throw PaymentException.unsupportedPayment("暂不支持微信退款查询", PaymentType.WECHAT_NATIVE);
}
@Override
public boolean closeOrder(String orderNo, Integer tenantId) throws PaymentException {
// TODO: 实现微信订单关闭逻辑
throw PaymentException.unsupportedPayment("暂不支持微信订单关闭", PaymentType.WECHAT_NATIVE);
}
@Override
public boolean supportRefund() {
return true;
}
@Override
public boolean supportQuery() {
return true;
}
@Override
public boolean supportClose() {
return true;
}
@Override
public boolean needNotify() {
return true;
}
/**
* 生成订单号(使用雪花算法保证全局唯一)
*/
private String generateOrderNo(PaymentRequest request) {
if (StringUtils.hasText(request.getOrderNo())) {
return request.getOrderNo();
}
return Long.toString(IdUtil.getSnowflakeNextId());
}
/**
* 构建微信预支付请求
*/
private PrepayRequest buildPrepayRequest(PaymentRequest request, String orderNo, Payment paymentConfig) {
PrepayRequest prepayRequest = new PrepayRequest();
// 设置应用ID和商户号关键修复
prepayRequest.setAppid(paymentConfig.getAppId());
prepayRequest.setMchid(paymentConfig.getMchId());
// 设置金额
Amount amount = new Amount();
amount.setTotal(request.getAmountInCents());
amount.setCurrency(PaymentConstants.Wechat.CURRENCY);
prepayRequest.setAmount(amount);
// 设置基本信息
prepayRequest.setOutTradeNo(orderNo);
prepayRequest.setDescription(request.getEffectiveDescription());
log.info("创建微信支付订单 - 订单号: {}, 商户号: {}, 金额: {}分",
orderNo, paymentConfig.getMchId(), request.getAmountInCents());
// 设置回调URL必填字段
String notifyUrl = null;
if (StringUtils.hasText(request.getNotifyUrl())) {
// 优先使用请求中的回调URL
notifyUrl = request.getNotifyUrl();
} else if (StringUtils.hasText(paymentConfig.getNotifyUrl())) {
// 使用配置中的回调URL
notifyUrl = paymentConfig.getNotifyUrl();
} else {
// 如果都没有,抛出异常
throw new RuntimeException("回调通知地址不能为空请在支付请求中设置notifyUrl或在支付配置中设置notifyUrl");
}
prepayRequest.setNotifyUrl(notifyUrl);
log.debug("构建微信预支付请求完成, 订单号: {}, 金额: {}分, AppID: {}, 商户号: {}, 回调URL: {}",
orderNo, request.getAmountInCents(), paymentConfig.getAppId(), paymentConfig.getMchId(), notifyUrl);
return prepayRequest;
}
/**
* 查询微信支付状态
*/
private PaymentResponse queryWechatPaymentStatus(String orderNo, Integer tenantId, Payment paymentConfig, Config wxPayConfig) throws PaymentException {
try {
log.info("开始查询微信支付状态 - 订单号: {}, 商户号: {}, 租户ID: {}",
orderNo, paymentConfig.getMchId(), tenantId);
// 构建查询请求
QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest();
queryRequest.setOutTradeNo(orderNo);
queryRequest.setMchid(paymentConfig.getMchId());
// 构建服务
NativePayService service = new NativePayService.Builder().config(wxPayConfig).build();
// 调用查询接口
Transaction transaction = service.queryOrderByOutTradeNo(queryRequest);
if (transaction == null) {
throw PaymentException.systemError("微信支付查询返回空结果", null);
}
// 转换支付状态
PaymentStatus paymentStatus = convertWechatPaymentStatus(transaction.getTradeState());
// 构建响应
PaymentResponse response = new PaymentResponse();
response.setSuccess(true);
response.setOrderNo(orderNo);
response.setPaymentStatus(paymentStatus);
response.setTenantId(tenantId);
response.setPaymentType(PaymentType.WECHAT_NATIVE);
if (transaction.getAmount() != null) {
// 微信返回的金额是分,需要转换为元
BigDecimal amount = new BigDecimal(transaction.getAmount().getTotal()).divide(new BigDecimal("100"));
response.setAmount(amount);
}
if (transaction.getTransactionId() != null) {
response.setTransactionId(transaction.getTransactionId());
}
log.info("微信Native支付状态查询成功, 订单号: {}, 状态: {}, 微信交易号: {}",
orderNo, paymentStatus, transaction.getTransactionId());
return response;
} catch (Exception e) {
if (e instanceof PaymentException) {
throw e;
}
log.error("查询微信支付状态失败, 订单号: {}, 错误: {}", orderNo, e.getMessage(), e);
throw PaymentException.networkError("查询微信支付状态失败: " + e.getMessage(), PaymentType.WECHAT_NATIVE, e);
}
}
/**
* 转换微信支付状态
*/
private PaymentStatus convertWechatPaymentStatus(Transaction.TradeStateEnum tradeState) {
if (tradeState == null) {
return PaymentStatus.PENDING;
}
switch (tradeState) {
case SUCCESS:
return PaymentStatus.SUCCESS;
case REFUND:
return PaymentStatus.REFUNDED;
case NOTPAY:
return PaymentStatus.PENDING;
case CLOSED:
return PaymentStatus.CANCELLED;
case REVOKED:
return PaymentStatus.CANCELLED;
case USERPAYING:
return PaymentStatus.PROCESSING;
case PAYERROR:
return PaymentStatus.FAILED;
default:
return PaymentStatus.PENDING;
}
}
/**
* 调用微信支付API
*/
private PrepayResponse callWechatPayApi(PrepayRequest request, Config wxPayConfig) throws PaymentException {
try {
// 构建服务
NativePayService service = new NativePayService.Builder().config(wxPayConfig).build();
// 调用预支付接口
PrepayResponse response = service.prepay(request);
if (response == null || !StringUtils.hasText(response.getCodeUrl())) {
throw PaymentException.networkError("微信支付API返回数据异常", PaymentType.WECHAT_NATIVE, null);
}
log.debug("微信支付API调用成功, 订单号: {}", request.getOutTradeNo());
return response;
} catch (Exception e) {
if (e instanceof PaymentException) {
throw e;
}
throw PaymentException.networkError("调用微信支付API失败: " + e.getMessage(), PaymentType.WECHAT_NATIVE, e);
}
}
}