Browse Source
- 实现 PaymentController,提供创建支付订单、查询支付状态等接口 - 添加 PaymentNotifyController,处理支付回调通知 - 创建 PaymentRequest DTO,用于统一支付请求参数pan
16 changed files with 3610 additions and 0 deletions
@ -0,0 +1,244 @@ |
|||
package com.gxwebsoft.payment.constants; |
|||
|
|||
/** |
|||
* 支付模块常量类 |
|||
* 统一管理支付相关的常量配置 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
public class PaymentConstants { |
|||
|
|||
/** |
|||
* 支付状态常量 |
|||
*/ |
|||
public static class Status { |
|||
/** 待支付 */ |
|||
public static final String PENDING = "PENDING"; |
|||
/** 支付成功 */ |
|||
public static final String SUCCESS = "SUCCESS"; |
|||
/** 支付失败 */ |
|||
public static final String FAILED = "FAILED"; |
|||
/** 支付取消 */ |
|||
public static final String CANCELLED = "CANCELLED"; |
|||
/** 支付超时 */ |
|||
public static final String TIMEOUT = "TIMEOUT"; |
|||
/** 退款成功 */ |
|||
public static final String REFUNDED = "REFUNDED"; |
|||
} |
|||
|
|||
/** |
|||
* 微信支付相关常量 |
|||
*/ |
|||
public static class Wechat { |
|||
/** 货币类型 */ |
|||
public static final String CURRENCY = "CNY"; |
|||
/** 金额转换倍数(元转分) */ |
|||
public static final int AMOUNT_MULTIPLIER = 100; |
|||
|
|||
/** 支付状态 */ |
|||
public static final String PAY_SUCCESS = "SUCCESS"; |
|||
public static final String PAY_REFUND = "REFUND"; |
|||
public static final String PAY_NOTPAY = "NOTPAY"; |
|||
public static final String PAY_CLOSED = "CLOSED"; |
|||
public static final String PAY_REVOKED = "REVOKED"; |
|||
public static final String PAY_USERPAYING = "USERPAYING"; |
|||
public static final String PAY_PAYERROR = "PAYERROR"; |
|||
|
|||
/** 回调响应 */ |
|||
public static final String NOTIFY_SUCCESS = "SUCCESS"; |
|||
public static final String NOTIFY_FAIL = "FAIL"; |
|||
|
|||
/** 通知类型 */ |
|||
public static final String EVENT_PAYMENT = "TRANSACTION.SUCCESS"; |
|||
public static final String EVENT_REFUND = "REFUND.SUCCESS"; |
|||
|
|||
/** HTTP头部 */ |
|||
public static final String HEADER_SIGNATURE = "Wechatpay-Signature"; |
|||
public static final String HEADER_TIMESTAMP = "Wechatpay-Timestamp"; |
|||
public static final String HEADER_NONCE = "Wechatpay-Nonce"; |
|||
public static final String HEADER_SERIAL = "Wechatpay-Serial"; |
|||
public static final String HEADER_REQUEST_ID = "Request-ID"; |
|||
} |
|||
|
|||
/** |
|||
* 支付宝相关常量 |
|||
*/ |
|||
public static class Alipay { |
|||
/** 货币类型 */ |
|||
public static final String CURRENCY = "CNY"; |
|||
|
|||
/** 支付状态 */ |
|||
public static final String PAY_SUCCESS = "TRADE_SUCCESS"; |
|||
public static final String PAY_FINISHED = "TRADE_FINISHED"; |
|||
public static final String PAY_CLOSED = "TRADE_CLOSED"; |
|||
|
|||
/** 回调响应 */ |
|||
public static final String NOTIFY_SUCCESS = "success"; |
|||
public static final String NOTIFY_FAIL = "failure"; |
|||
|
|||
/** 产品码 */ |
|||
public static final String PRODUCT_CODE_WEB = "FAST_INSTANT_TRADE_PAY"; |
|||
public static final String PRODUCT_CODE_WAP = "QUICK_WAP_WAY"; |
|||
public static final String PRODUCT_CODE_APP = "QUICK_MSECURITY_PAY"; |
|||
} |
|||
|
|||
/** |
|||
* 银联支付相关常量 |
|||
*/ |
|||
public static class UnionPay { |
|||
/** 货币类型 */ |
|||
public static final String CURRENCY = "156"; // 人民币代码
|
|||
|
|||
/** 支付状态 */ |
|||
public static final String PAY_SUCCESS = "00"; |
|||
public static final String PAY_FAILED = "01"; |
|||
|
|||
/** 交易类型 */ |
|||
public static final String TXN_TYPE_CONSUME = "01"; // 消费
|
|||
public static final String TXN_TYPE_REFUND = "04"; // 退货
|
|||
} |
|||
|
|||
/** |
|||
* 缓存键常量 |
|||
*/ |
|||
public static class CacheKey { |
|||
/** 支付配置缓存前缀 */ |
|||
public static final String PAYMENT_CONFIG = "payment:config:"; |
|||
/** 支付订单缓存前缀 */ |
|||
public static final String PAYMENT_ORDER = "payment:order:"; |
|||
/** 支付锁前缀 */ |
|||
public static final String PAYMENT_LOCK = "payment:lock:"; |
|||
/** 回调处理锁前缀 */ |
|||
public static final String NOTIFY_LOCK = "payment:notify:lock:"; |
|||
} |
|||
|
|||
/** |
|||
* 配置相关常量 |
|||
*/ |
|||
public static class Config { |
|||
/** 订单超时时间(分钟) */ |
|||
public static final int ORDER_TIMEOUT_MINUTES = 30; |
|||
/** 订单描述最大长度 */ |
|||
public static final int DESCRIPTION_MAX_LENGTH = 127; |
|||
/** 最大重试次数 */ |
|||
public static final int MAX_RETRY_COUNT = 3; |
|||
/** 重试间隔(毫秒) */ |
|||
public static final long RETRY_INTERVAL_MS = 1000; |
|||
/** 签名有效期(秒) */ |
|||
public static final long SIGNATURE_VALID_SECONDS = 300; |
|||
} |
|||
|
|||
/** |
|||
* 错误信息常量 |
|||
*/ |
|||
public static class ErrorMessage { |
|||
/** 参数错误 */ |
|||
public static final String PARAM_ERROR = "参数错误"; |
|||
/** 配置未找到 */ |
|||
public static final String CONFIG_NOT_FOUND = "支付配置未找到"; |
|||
/** 支付方式不支持 */ |
|||
public static final String PAYMENT_TYPE_NOT_SUPPORTED = "支付方式不支持"; |
|||
/** 金额错误 */ |
|||
public static final String AMOUNT_ERROR = "金额错误"; |
|||
/** 订单不存在 */ |
|||
public static final String ORDER_NOT_FOUND = "订单不存在"; |
|||
/** 订单状态错误 */ |
|||
public static final String ORDER_STATUS_ERROR = "订单状态错误"; |
|||
/** 签名验证失败 */ |
|||
public static final String SIGNATURE_ERROR = "签名验证失败"; |
|||
/** 网络请求失败 */ |
|||
public static final String NETWORK_ERROR = "网络请求失败"; |
|||
/** 系统内部错误 */ |
|||
public static final String SYSTEM_ERROR = "系统内部错误"; |
|||
/** 余额不足 */ |
|||
public static final String INSUFFICIENT_BALANCE = "余额不足"; |
|||
/** 支付超时 */ |
|||
public static final String PAYMENT_TIMEOUT = "支付超时"; |
|||
/** 重复支付 */ |
|||
public static final String DUPLICATE_PAYMENT = "重复支付"; |
|||
} |
|||
|
|||
/** |
|||
* 日志消息常量 |
|||
*/ |
|||
public static class LogMessage { |
|||
/** 支付请求开始 */ |
|||
public static final String PAYMENT_START = "开始处理支付请求"; |
|||
/** 支付请求成功 */ |
|||
public static final String PAYMENT_SUCCESS = "支付请求处理成功"; |
|||
/** 支付请求失败 */ |
|||
public static final String PAYMENT_FAILED = "支付请求处理失败"; |
|||
|
|||
/** 回调处理开始 */ |
|||
public static final String NOTIFY_START = "开始处理支付回调"; |
|||
/** 回调处理成功 */ |
|||
public static final String NOTIFY_SUCCESS = "支付回调处理成功"; |
|||
/** 回调处理失败 */ |
|||
public static final String NOTIFY_FAILED = "支付回调处理失败"; |
|||
|
|||
/** 退款请求开始 */ |
|||
public static final String REFUND_START = "开始处理退款请求"; |
|||
/** 退款请求成功 */ |
|||
public static final String REFUND_SUCCESS = "退款请求处理成功"; |
|||
/** 退款请求失败 */ |
|||
public static final String REFUND_FAILED = "退款请求处理失败"; |
|||
} |
|||
|
|||
/** |
|||
* 正则表达式常量 |
|||
*/ |
|||
public static class Regex { |
|||
/** 订单号格式 */ |
|||
public static final String ORDER_NO = "^[a-zA-Z0-9_-]{1,32}$"; |
|||
/** 金额格式(分) */ |
|||
public static final String AMOUNT = "^[1-9]\\d*$"; |
|||
/** 手机号格式 */ |
|||
public static final String MOBILE = "^1[3-9]\\d{9}$"; |
|||
/** 邮箱格式 */ |
|||
public static final String EMAIL = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; |
|||
} |
|||
|
|||
/** |
|||
* 时间相关常量 |
|||
*/ |
|||
public static class Time { |
|||
/** 配置缓存有效期(秒) */ |
|||
public static final long CONFIG_CACHE_SECONDS = 3600; |
|||
/** 订单缓存有效期(秒) */ |
|||
public static final long ORDER_CACHE_SECONDS = 1800; |
|||
/** 支付锁有效期(秒) */ |
|||
public static final long PAYMENT_LOCK_SECONDS = 60; |
|||
/** 回调锁有效期(秒) */ |
|||
public static final long NOTIFY_LOCK_SECONDS = 30; |
|||
} |
|||
|
|||
/** |
|||
* 文件相关常量 |
|||
*/ |
|||
public static class File { |
|||
/** 证书文件扩展名 */ |
|||
public static final String CERT_EXTENSION = ".pem"; |
|||
/** 私钥文件后缀 */ |
|||
public static final String PRIVATE_KEY_SUFFIX = "_key.pem"; |
|||
/** 公钥文件后缀 */ |
|||
public static final String PUBLIC_KEY_SUFFIX = "_cert.pem"; |
|||
} |
|||
|
|||
/** |
|||
* 环境相关常量 |
|||
*/ |
|||
public static class Environment { |
|||
/** 开发环境 */ |
|||
public static final String DEV = "dev"; |
|||
/** 测试环境 */ |
|||
public static final String TEST = "test"; |
|||
/** 生产环境 */ |
|||
public static final String PROD = "prod"; |
|||
} |
|||
|
|||
// 私有构造函数,防止实例化
|
|||
private PaymentConstants() { |
|||
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); |
|||
} |
|||
} |
@ -0,0 +1,246 @@ |
|||
package com.gxwebsoft.payment.controller; |
|||
|
|||
import com.gxwebsoft.common.core.web.ApiResult; |
|||
import com.gxwebsoft.common.core.web.BaseController; |
|||
import com.gxwebsoft.payment.constants.PaymentConstants; |
|||
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 com.gxwebsoft.payment.service.PaymentService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.validation.Valid; |
|||
import javax.validation.constraints.NotBlank; |
|||
import javax.validation.constraints.NotNull; |
|||
import javax.validation.constraints.Positive; |
|||
import java.math.BigDecimal; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 统一支付控制器 |
|||
* 提供所有支付方式的统一入口 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@Validated |
|||
@Tag(name = "统一支付接口", description = "支持所有支付方式的统一支付接口") |
|||
@RestController |
|||
@RequestMapping("/api/payment") |
|||
public class PaymentController extends BaseController { |
|||
|
|||
@Resource |
|||
private PaymentService paymentService; |
|||
|
|||
@Operation(summary = "创建支付订单", description = "支持微信、支付宝、银联等多种支付方式") |
|||
@PostMapping("/create") |
|||
public ApiResult<?> createPayment(@Valid @RequestBody PaymentRequest request) { |
|||
log.info("收到支付请求: {}", request); |
|||
|
|||
try { |
|||
PaymentResponse response = paymentService.createPayment(request); |
|||
return this.<PaymentResponse>success("支付订单创建成功", response); |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("支付订单创建失败: {}", e.getMessage()); |
|||
return fail(e.getMessage()); |
|||
} catch (Exception e) { |
|||
log.error("支付订单创建系统错误: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "查询支付状态", description = "查询指定订单的支付状态") |
|||
@GetMapping("/query") |
|||
public ApiResult<?> queryPayment( |
|||
@Parameter(description = "订单号", required = true) |
|||
@RequestParam @NotBlank(message = "订单号不能为空") String orderNo, |
|||
|
|||
@Parameter(description = "支付类型", required = true) |
|||
@RequestParam @NotNull(message = "支付类型不能为空") PaymentType paymentType, |
|||
|
|||
@Parameter(description = "租户ID", required = true) |
|||
@RequestParam @NotNull(message = "租户ID不能为空") @Positive(message = "租户ID必须为正数") Integer tenantId) { |
|||
|
|||
log.info("查询支付状态: orderNo={}, paymentType={}, tenantId={}", orderNo, paymentType, tenantId); |
|||
|
|||
try { |
|||
PaymentResponse response = paymentService.queryPayment(orderNo, paymentType, tenantId); |
|||
return this.<PaymentResponse>success("支付状态查询成功", response); |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("支付状态查询失败: {}", e.getMessage()); |
|||
return fail(e.getMessage()); |
|||
} catch (Exception e) { |
|||
log.error("支付状态查询系统错误: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "申请退款", description = "申请订单退款") |
|||
@PostMapping("/refund") |
|||
public ApiResult<?> refund( |
|||
@Parameter(description = "订单号", required = true) |
|||
@RequestParam @NotBlank(message = "订单号不能为空") String orderNo, |
|||
|
|||
@Parameter(description = "退款单号", required = true) |
|||
@RequestParam @NotBlank(message = "退款单号不能为空") String refundNo, |
|||
|
|||
@Parameter(description = "支付类型", required = true) |
|||
@RequestParam @NotNull(message = "支付类型不能为空") PaymentType paymentType, |
|||
|
|||
@Parameter(description = "订单总金额", required = true) |
|||
@RequestParam @NotNull(message = "订单总金额不能为空") @Positive(message = "订单总金额必须大于0") BigDecimal totalAmount, |
|||
|
|||
@Parameter(description = "退款金额", required = true) |
|||
@RequestParam @NotNull(message = "退款金额不能为空") @Positive(message = "退款金额必须大于0") BigDecimal refundAmount, |
|||
|
|||
@Parameter(description = "退款原因") |
|||
@RequestParam(required = false) String reason, |
|||
|
|||
@Parameter(description = "租户ID", required = true) |
|||
@RequestParam @NotNull(message = "租户ID不能为空") @Positive(message = "租户ID必须为正数") Integer tenantId) { |
|||
|
|||
log.info("申请退款: orderNo={}, refundNo={}, paymentType={}, totalAmount={}, refundAmount={}, tenantId={}", |
|||
orderNo, refundNo, paymentType, totalAmount, refundAmount, tenantId); |
|||
|
|||
try { |
|||
PaymentResponse response = paymentService.refund(orderNo, refundNo, paymentType, |
|||
totalAmount, refundAmount, reason, tenantId); |
|||
return this.<PaymentResponse>success("退款申请成功", response); |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("退款申请失败: {}", e.getMessage()); |
|||
return fail(e.getMessage()); |
|||
} catch (Exception e) { |
|||
log.error("退款申请系统错误: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "查询退款状态", description = "查询指定退款单的状态") |
|||
@GetMapping("/refund/query") |
|||
public ApiResult<?> queryRefund( |
|||
@Parameter(description = "退款单号", required = true) |
|||
@RequestParam @NotBlank(message = "退款单号不能为空") String refundNo, |
|||
|
|||
@Parameter(description = "支付类型", required = true) |
|||
@RequestParam @NotNull(message = "支付类型不能为空") PaymentType paymentType, |
|||
|
|||
@Parameter(description = "租户ID", required = true) |
|||
@RequestParam @NotNull(message = "租户ID不能为空") @Positive(message = "租户ID必须为正数") Integer tenantId) { |
|||
|
|||
log.info("查询退款状态: refundNo={}, paymentType={}, tenantId={}", refundNo, paymentType, tenantId); |
|||
|
|||
try { |
|||
PaymentResponse response = paymentService.queryRefund(refundNo, paymentType, tenantId); |
|||
return this.<PaymentResponse>success("退款状态查询成功", response); |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("退款状态查询失败: {}", e.getMessage()); |
|||
return fail(e.getMessage()); |
|||
} catch (Exception e) { |
|||
log.error("退款状态查询系统错误: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "关闭订单", description = "关闭未支付的订单") |
|||
@PostMapping("/close") |
|||
public ApiResult<?> closeOrder( |
|||
@Parameter(description = "订单号", required = true) |
|||
@RequestParam @NotBlank(message = "订单号不能为空") String orderNo, |
|||
|
|||
@Parameter(description = "支付类型", required = true) |
|||
@RequestParam @NotNull(message = "支付类型不能为空") PaymentType paymentType, |
|||
|
|||
@Parameter(description = "租户ID", required = true) |
|||
@RequestParam @NotNull(message = "租户ID不能为空") @Positive(message = "租户ID必须为正数") Integer tenantId) { |
|||
|
|||
log.info("关闭订单: orderNo={}, paymentType={}, tenantId={}", orderNo, paymentType, tenantId); |
|||
|
|||
try { |
|||
boolean result = paymentService.closeOrder(orderNo, paymentType, tenantId); |
|||
return success(result ? "订单关闭成功" : "订单关闭失败", result); |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("订单关闭失败: {}", e.getMessage()); |
|||
return fail(e.getMessage()); |
|||
} catch (Exception e) { |
|||
log.error("订单关闭系统错误: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "获取支持的支付类型", description = "获取系统支持的所有支付类型列表") |
|||
@GetMapping("/types") |
|||
public ApiResult<?> getSupportedPaymentTypes() { |
|||
try { |
|||
List<PaymentType> paymentTypes = paymentService.getSupportedPaymentTypes(); |
|||
return this.<List<PaymentType>>success("获取支付类型成功", paymentTypes); |
|||
} catch (Exception e) { |
|||
log.error("获取支付类型失败: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "获取支付策略信息", description = "获取指定支付类型的策略信息") |
|||
@GetMapping("/strategy/{paymentType}") |
|||
public ApiResult<?> getPaymentStrategyInfo( |
|||
@Parameter(description = "支付类型", required = true) |
|||
@PathVariable @NotNull(message = "支付类型不能为空") PaymentType paymentType) { |
|||
|
|||
try { |
|||
Map<String, Object> strategyInfo = paymentService.getPaymentStrategyInfo(paymentType); |
|||
if (strategyInfo == null) { |
|||
return fail("不支持的支付类型: " + paymentType); |
|||
} |
|||
return success("获取策略信息成功", strategyInfo); |
|||
} catch (Exception e) { |
|||
log.error("获取策略信息失败: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "获取所有支付策略信息", description = "获取系统所有支付策略的详细信息") |
|||
@GetMapping("/strategies") |
|||
public ApiResult<?> getAllPaymentStrategyInfo() { |
|||
try { |
|||
List<Map<String, Object>> strategiesInfo = paymentService.getAllPaymentStrategyInfo(); |
|||
return this.<List<Map<String, Object>>>success("获取所有策略信息成功", strategiesInfo); |
|||
} catch (Exception e) { |
|||
log.error("获取所有策略信息失败: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "检查支付类型支持情况", description = "检查指定支付类型的功能支持情况") |
|||
@GetMapping("/support/{paymentType}") |
|||
public ApiResult<?> checkPaymentTypeSupport( |
|||
@Parameter(description = "支付类型", required = true) |
|||
@PathVariable @NotNull(message = "支付类型不能为空") PaymentType paymentType) { |
|||
|
|||
try { |
|||
Map<String, Boolean> support = Map.of( |
|||
"supported", paymentService.isPaymentTypeSupported(paymentType), |
|||
"refundSupported", paymentService.isRefundSupported(paymentType), |
|||
"querySupported", paymentService.isQuerySupported(paymentType), |
|||
"closeSupported", paymentService.isCloseSupported(paymentType), |
|||
"notifyNeeded", paymentService.isNotifyNeeded(paymentType) |
|||
); |
|||
return this.<Map<String, Boolean>>success("检查支持情况成功", support); |
|||
} catch (Exception e) { |
|||
log.error("检查支持情况失败: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,188 @@ |
|||
package com.gxwebsoft.payment.controller; |
|||
|
|||
import com.gxwebsoft.payment.constants.PaymentConstants; |
|||
import com.gxwebsoft.payment.enums.PaymentType; |
|||
import com.gxwebsoft.payment.exception.PaymentException; |
|||
import com.gxwebsoft.payment.service.PaymentService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.util.Enumeration; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 统一支付回调控制器 |
|||
* 处理所有支付方式的异步通知回调 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@Tag(name = "统一支付回调接口", description = "处理所有支付方式的异步通知回调") |
|||
@RestController |
|||
@RequestMapping("/api/payment/notify") |
|||
public class PaymentNotifyController { |
|||
|
|||
@Resource |
|||
private PaymentService paymentService; |
|||
|
|||
@Operation(summary = "微信支付回调通知", description = "处理微信支付的异步通知") |
|||
@PostMapping("/wechat/{tenantId}") |
|||
public String wechatNotify( |
|||
@Parameter(description = "租户ID", required = true) |
|||
@PathVariable("tenantId") Integer tenantId, |
|||
@RequestBody String body, |
|||
HttpServletRequest request) { |
|||
|
|||
log.info("收到微信支付回调通知, 租户ID: {}", tenantId); |
|||
|
|||
try { |
|||
// 提取请求头
|
|||
Map<String, String> headers = extractHeaders(request); |
|||
|
|||
// 处理回调
|
|||
String result = paymentService.handlePaymentNotify(PaymentType.WECHAT_NATIVE, headers, body, tenantId); |
|||
|
|||
log.info("微信支付回调处理完成, 租户ID: {}, 结果: {}", tenantId, result); |
|||
return result; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("微信支付回调处理失败, 租户ID: {}, 错误: {}", tenantId, e.getMessage()); |
|||
return PaymentConstants.Wechat.NOTIFY_FAIL; |
|||
} catch (Exception e) { |
|||
log.error("微信支付回调系统错误, 租户ID: {}, 错误: {}", tenantId, e.getMessage(), e); |
|||
return PaymentConstants.Wechat.NOTIFY_FAIL; |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "支付宝支付回调通知", description = "处理支付宝支付的异步通知") |
|||
@PostMapping("/alipay/{tenantId}") |
|||
public String alipayNotify( |
|||
@Parameter(description = "租户ID", required = true) |
|||
@PathVariable("tenantId") Integer tenantId, |
|||
@RequestBody String body, |
|||
HttpServletRequest request) { |
|||
|
|||
log.info("收到支付宝支付回调通知, 租户ID: {}", tenantId); |
|||
|
|||
try { |
|||
// 提取请求头
|
|||
Map<String, String> headers = extractHeaders(request); |
|||
|
|||
// 处理回调
|
|||
String result = paymentService.handlePaymentNotify(PaymentType.ALIPAY, headers, body, tenantId); |
|||
|
|||
log.info("支付宝支付回调处理完成, 租户ID: {}, 结果: {}", tenantId, result); |
|||
return result; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("支付宝支付回调处理失败, 租户ID: {}, 错误: {}", tenantId, e.getMessage()); |
|||
return PaymentConstants.Alipay.NOTIFY_FAIL; |
|||
} catch (Exception e) { |
|||
log.error("支付宝支付回调系统错误, 租户ID: {}, 错误: {}", tenantId, e.getMessage(), e); |
|||
return PaymentConstants.Alipay.NOTIFY_FAIL; |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "银联支付回调通知", description = "处理银联支付的异步通知") |
|||
@PostMapping("/unionpay/{tenantId}") |
|||
public String unionPayNotify( |
|||
@Parameter(description = "租户ID", required = true) |
|||
@PathVariable("tenantId") Integer tenantId, |
|||
@RequestBody String body, |
|||
HttpServletRequest request) { |
|||
|
|||
log.info("收到银联支付回调通知, 租户ID: {}", tenantId); |
|||
|
|||
try { |
|||
// 提取请求头
|
|||
Map<String, String> headers = extractHeaders(request); |
|||
|
|||
// 处理回调
|
|||
String result = paymentService.handlePaymentNotify(PaymentType.UNION_PAY, headers, body, tenantId); |
|||
|
|||
log.info("银联支付回调处理完成, 租户ID: {}, 结果: {}", tenantId, result); |
|||
return result; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("银联支付回调处理失败, 租户ID: {}, 错误: {}", tenantId, e.getMessage()); |
|||
return "failure"; |
|||
} catch (Exception e) { |
|||
log.error("银联支付回调系统错误, 租户ID: {}, 错误: {}", tenantId, e.getMessage(), e); |
|||
return "failure"; |
|||
} |
|||
} |
|||
|
|||
@Operation(summary = "通用支付回调通知", description = "处理指定支付类型的异步通知") |
|||
@PostMapping("/{paymentType}/{tenantId}") |
|||
public String genericNotify( |
|||
@Parameter(description = "支付类型", required = true) |
|||
@PathVariable("paymentType") PaymentType paymentType, |
|||
@Parameter(description = "租户ID", required = true) |
|||
@PathVariable("tenantId") Integer tenantId, |
|||
@RequestBody String body, |
|||
HttpServletRequest request) { |
|||
|
|||
log.info("收到{}支付回调通知, 租户ID: {}", paymentType.getName(), tenantId); |
|||
|
|||
try { |
|||
// 提取请求头
|
|||
Map<String, String> headers = extractHeaders(request); |
|||
|
|||
// 处理回调
|
|||
String result = paymentService.handlePaymentNotify(paymentType, headers, body, tenantId); |
|||
|
|||
log.info("{}支付回调处理完成, 租户ID: {}, 结果: {}", paymentType.getName(), tenantId, result); |
|||
return result; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("{}支付回调处理失败, 租户ID: {}, 错误: {}", paymentType.getName(), tenantId, e.getMessage()); |
|||
return getFailureResponse(paymentType); |
|||
} catch (Exception e) { |
|||
log.error("{}支付回调系统错误, 租户ID: {}, 错误: {}", paymentType.getName(), tenantId, e.getMessage(), e); |
|||
return getFailureResponse(paymentType); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 提取HTTP请求头 |
|||
*/ |
|||
private Map<String, String> extractHeaders(HttpServletRequest request) { |
|||
Map<String, String> headers = new HashMap<>(); |
|||
|
|||
Enumeration<String> headerNames = request.getHeaderNames(); |
|||
while (headerNames.hasMoreElements()) { |
|||
String headerName = headerNames.nextElement(); |
|||
String headerValue = request.getHeader(headerName); |
|||
headers.put(headerName, headerValue); |
|||
} |
|||
|
|||
// 记录关键头部信息(不记录敏感信息)
|
|||
log.debug("提取请求头完成, 头部数量: {}", headers.size()); |
|||
|
|||
return headers; |
|||
} |
|||
|
|||
/** |
|||
* 根据支付类型获取失败响应 |
|||
*/ |
|||
private String getFailureResponse(PaymentType paymentType) { |
|||
switch (paymentType) { |
|||
case WECHAT: |
|||
case WECHAT_NATIVE: |
|||
return PaymentConstants.Wechat.NOTIFY_FAIL; |
|||
case ALIPAY: |
|||
return PaymentConstants.Alipay.NOTIFY_FAIL; |
|||
case UNION_PAY: |
|||
return "failure"; |
|||
default: |
|||
return "fail"; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,207 @@ |
|||
package com.gxwebsoft.payment.dto; |
|||
|
|||
import com.gxwebsoft.payment.enums.PaymentChannel; |
|||
import com.gxwebsoft.payment.enums.PaymentType; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.constraints.*; |
|||
import java.math.BigDecimal; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 统一支付请求DTO |
|||
* 支持所有支付方式的统一请求格式 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Data |
|||
@Schema(name = "统一支付请求", description = "支持所有支付方式的统一支付请求参数") |
|||
public class PaymentRequest { |
|||
|
|||
@Schema(description = "租户ID", required = true) |
|||
@NotNull(message = "租户ID不能为空") |
|||
@Positive(message = "租户ID必须为正数") |
|||
private Integer tenantId; |
|||
|
|||
@Schema(description = "用户ID", required = true) |
|||
@NotNull(message = "用户ID不能为空") |
|||
@Positive(message = "用户ID必须为正数") |
|||
private Integer userId; |
|||
|
|||
@Schema(description = "支付类型", required = true, example = "WECHAT_NATIVE") |
|||
@NotNull(message = "支付类型不能为空") |
|||
private PaymentType paymentType; |
|||
|
|||
@Schema(description = "支付渠道", example = "wechat_native") |
|||
private PaymentChannel paymentChannel; |
|||
|
|||
@Schema(description = "支付金额", required = true, example = "0.01") |
|||
@NotNull(message = "支付金额不能为空") |
|||
@DecimalMin(value = "0.01", message = "支付金额必须大于0.01元") |
|||
@DecimalMax(value = "999999.99", message = "支付金额不能超过999999.99元") |
|||
@Digits(integer = 6, fraction = 2, message = "支付金额格式不正确,最多6位整数2位小数") |
|||
private BigDecimal amount; |
|||
|
|||
@Schema(description = "订单号(可选,不提供则自动生成)") |
|||
@Size(max = 32, message = "订单号不能超过32个字符") |
|||
@Pattern(regexp = "^[a-zA-Z0-9_-]*$", message = "订单号只能包含字母、数字、下划线和横线") |
|||
private String orderNo; |
|||
|
|||
@Schema(description = "订单标题", required = true) |
|||
@NotBlank(message = "订单标题不能为空") |
|||
@Size(max = 127, message = "订单标题不能超过127个字符") |
|||
private String subject; |
|||
|
|||
@Schema(description = "订单描述") |
|||
@Size(max = 500, message = "订单描述不能超过500个字符") |
|||
private String description; |
|||
|
|||
@Schema(description = "商品ID") |
|||
@Positive(message = "商品ID必须为正数") |
|||
private Integer goodsId; |
|||
|
|||
@Schema(description = "购买数量", example = "1") |
|||
@Min(value = 1, message = "购买数量必须大于0") |
|||
@Max(value = 9999, message = "购买数量不能超过9999") |
|||
private Integer quantity = 1; |
|||
|
|||
@Schema(description = "订单类型", example = "0") |
|||
@Min(value = 0, message = "订单类型不能为负数") |
|||
private Integer orderType = 0; |
|||
|
|||
@Schema(description = "客户端IP地址") |
|||
private String clientIp; |
|||
|
|||
@Schema(description = "用户代理") |
|||
private String userAgent; |
|||
|
|||
@Schema(description = "回调通知URL") |
|||
private String notifyUrl; |
|||
|
|||
@Schema(description = "支付成功跳转URL") |
|||
private String returnUrl; |
|||
|
|||
@Schema(description = "支付取消跳转URL") |
|||
private String cancelUrl; |
|||
|
|||
@Schema(description = "订单超时时间(分钟)", example = "30") |
|||
@Min(value = 1, message = "订单超时时间必须大于0分钟") |
|||
@Max(value = 1440, message = "订单超时时间不能超过1440分钟(24小时)") |
|||
private Integer timeoutMinutes = 30; |
|||
|
|||
@Schema(description = "买家备注") |
|||
@Size(max = 500, message = "买家备注不能超过500个字符") |
|||
private String buyerRemarks; |
|||
|
|||
@Schema(description = "商户备注") |
|||
@Size(max = 500, message = "商户备注不能超过500个字符") |
|||
private String merchantRemarks; |
|||
|
|||
@Schema(description = "收货地址ID") |
|||
@Positive(message = "收货地址ID必须为正数") |
|||
private Integer addressId; |
|||
|
|||
@Schema(description = "扩展参数") |
|||
private Map<String, Object> extraParams; |
|||
|
|||
// 微信支付特有参数
|
|||
@Schema(description = "微信OpenID(JSAPI支付必填)") |
|||
private String openId; |
|||
|
|||
@Schema(description = "微信UnionID") |
|||
private String unionId; |
|||
|
|||
// 支付宝特有参数
|
|||
@Schema(description = "支付宝用户ID") |
|||
private String alipayUserId; |
|||
|
|||
@Schema(description = "花呗分期数") |
|||
private Integer hbFqNum; |
|||
|
|||
// 银联支付特有参数
|
|||
@Schema(description = "银行卡号") |
|||
private String cardNo; |
|||
|
|||
@Schema(description = "银行代码") |
|||
private String bankCode; |
|||
|
|||
/** |
|||
* 获取有效的支付渠道 |
|||
*/ |
|||
public PaymentChannel getEffectivePaymentChannel() { |
|||
if (paymentChannel != null) { |
|||
return paymentChannel; |
|||
} |
|||
return PaymentChannel.getDefaultByPaymentType(paymentType); |
|||
} |
|||
|
|||
/** |
|||
* 获取有效的订单描述 |
|||
*/ |
|||
public String getEffectiveDescription() { |
|||
if (description != null && !description.trim().isEmpty()) { |
|||
return description.trim(); |
|||
} |
|||
return subject; |
|||
} |
|||
|
|||
/** |
|||
* 获取格式化的金额字符串 |
|||
*/ |
|||
public String getFormattedAmount() { |
|||
if (amount == null) { |
|||
return "0.00"; |
|||
} |
|||
return String.format("%.2f", amount); |
|||
} |
|||
|
|||
/** |
|||
* 转换为分(微信支付API需要) |
|||
*/ |
|||
public Integer getAmountInCents() { |
|||
if (amount == null) { |
|||
return 0; |
|||
} |
|||
return amount.multiply(new BigDecimal(100)).intValue(); |
|||
} |
|||
|
|||
/** |
|||
* 验证必要参数是否完整 |
|||
*/ |
|||
public boolean isValid() { |
|||
return tenantId != null && tenantId > 0 |
|||
&& userId != null && userId > 0 |
|||
&& paymentType != null |
|||
&& amount != null && amount.compareTo(BigDecimal.ZERO) > 0 |
|||
&& subject != null && !subject.trim().isEmpty(); |
|||
} |
|||
|
|||
/** |
|||
* 验证微信JSAPI支付参数 |
|||
*/ |
|||
public boolean isValidForWechatJsapi() { |
|||
return isValid() && paymentType.isWechatPay() && openId != null && !openId.trim().isEmpty(); |
|||
} |
|||
|
|||
/** |
|||
* 验证支付宝支付参数 |
|||
*/ |
|||
public boolean isValidForAlipay() { |
|||
return isValid() && paymentType == PaymentType.ALIPAY; |
|||
} |
|||
|
|||
/** |
|||
* 获取订单超时时间(秒) |
|||
*/ |
|||
public long getTimeoutSeconds() { |
|||
return timeoutMinutes * 60L; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return String.format("PaymentRequest{tenantId=%d, userId=%d, paymentType=%s, amount=%s, orderNo='%s', subject='%s'}", |
|||
tenantId, userId, paymentType, getFormattedAmount(), orderNo, subject); |
|||
} |
|||
} |
@ -0,0 +1,294 @@ |
|||
package com.gxwebsoft.payment.dto; |
|||
|
|||
import com.gxwebsoft.payment.enums.PaymentChannel; |
|||
import com.gxwebsoft.payment.enums.PaymentStatus; |
|||
import com.gxwebsoft.payment.enums.PaymentType; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.time.LocalDateTime; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 统一支付响应DTO |
|||
* 支持所有支付方式的统一响应格式 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Data |
|||
@Schema(name = "统一支付响应", description = "支持所有支付方式的统一支付响应") |
|||
public class PaymentResponse { |
|||
|
|||
@Schema(description = "是否成功") |
|||
private Boolean success; |
|||
|
|||
@Schema(description = "错误代码") |
|||
private String errorCode; |
|||
|
|||
@Schema(description = "错误信息") |
|||
private String errorMessage; |
|||
|
|||
@Schema(description = "订单号") |
|||
private String orderNo; |
|||
|
|||
@Schema(description = "第三方交易号") |
|||
private String transactionId; |
|||
|
|||
@Schema(description = "支付类型") |
|||
private PaymentType paymentType; |
|||
|
|||
@Schema(description = "支付渠道") |
|||
private PaymentChannel paymentChannel; |
|||
|
|||
@Schema(description = "支付状态") |
|||
private PaymentStatus paymentStatus; |
|||
|
|||
@Schema(description = "支付金额") |
|||
private BigDecimal amount; |
|||
|
|||
@Schema(description = "实际支付金额") |
|||
private BigDecimal paidAmount; |
|||
|
|||
@Schema(description = "货币类型") |
|||
private String currency; |
|||
|
|||
@Schema(description = "租户ID") |
|||
private Integer tenantId; |
|||
|
|||
@Schema(description = "用户ID") |
|||
private Integer userId; |
|||
|
|||
@Schema(description = "创建时间") |
|||
private LocalDateTime createTime; |
|||
|
|||
@Schema(description = "支付时间") |
|||
private LocalDateTime payTime; |
|||
|
|||
@Schema(description = "过期时间") |
|||
private LocalDateTime expireTime; |
|||
|
|||
// 微信支付特有字段
|
|||
@Schema(description = "微信支付二维码URL(Native支付)") |
|||
private String codeUrl; |
|||
|
|||
@Schema(description = "微信支付参数(JSAPI支付)") |
|||
private WechatPayParams wechatPayParams; |
|||
|
|||
@Schema(description = "微信H5支付URL") |
|||
private String h5Url; |
|||
|
|||
// 支付宝特有字段
|
|||
@Schema(description = "支付宝支付表单(网页支付)") |
|||
private String alipayForm; |
|||
|
|||
@Schema(description = "支付宝支付URL(手机网站支付)") |
|||
private String alipayUrl; |
|||
|
|||
@Schema(description = "支付宝支付参数(APP支付)") |
|||
private String alipayParams; |
|||
|
|||
// 银联支付特有字段
|
|||
@Schema(description = "银联支付表单") |
|||
private String unionPayForm; |
|||
|
|||
@Schema(description = "银联支付URL") |
|||
private String unionPayUrl; |
|||
|
|||
@Schema(description = "扩展参数") |
|||
private Map<String, Object> extraParams; |
|||
|
|||
/** |
|||
* 微信支付参数 |
|||
*/ |
|||
@Data |
|||
@Schema(name = "微信支付参数", description = "微信JSAPI支付所需参数") |
|||
public static class WechatPayParams { |
|||
@Schema(description = "应用ID") |
|||
private String appId; |
|||
|
|||
@Schema(description = "时间戳") |
|||
private String timeStamp; |
|||
|
|||
@Schema(description = "随机字符串") |
|||
private String nonceStr; |
|||
|
|||
@Schema(description = "订单详情扩展字符串") |
|||
private String packageValue; |
|||
|
|||
@Schema(description = "签名方式") |
|||
private String signType; |
|||
|
|||
@Schema(description = "签名") |
|||
private String paySign; |
|||
} |
|||
|
|||
/** |
|||
* 创建成功响应 |
|||
*/ |
|||
public static PaymentResponse success(String orderNo, PaymentType paymentType) { |
|||
PaymentResponse response = new PaymentResponse(); |
|||
response.setSuccess(true); |
|||
response.setOrderNo(orderNo); |
|||
response.setPaymentType(paymentType); |
|||
response.setPaymentStatus(PaymentStatus.PENDING); |
|||
response.setCreateTime(LocalDateTime.now()); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建失败响应 |
|||
*/ |
|||
public static PaymentResponse failure(String errorCode, String errorMessage) { |
|||
PaymentResponse response = new PaymentResponse(); |
|||
response.setSuccess(false); |
|||
response.setErrorCode(errorCode); |
|||
response.setErrorMessage(errorMessage); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建微信Native支付响应 |
|||
*/ |
|||
public static PaymentResponse wechatNative(String orderNo, String codeUrl, BigDecimal amount, Integer tenantId) { |
|||
PaymentResponse response = success(orderNo, PaymentType.WECHAT_NATIVE); |
|||
response.setCodeUrl(codeUrl); |
|||
response.setPaymentChannel(PaymentChannel.WECHAT_NATIVE); |
|||
response.setAmount(amount); |
|||
response.setTenantId(tenantId); |
|||
response.setCurrency("CNY"); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建微信JSAPI支付响应 |
|||
*/ |
|||
public static PaymentResponse wechatJsapi(String orderNo, WechatPayParams payParams, BigDecimal amount, Integer tenantId) { |
|||
PaymentResponse response = success(orderNo, PaymentType.WECHAT); |
|||
response.setWechatPayParams(payParams); |
|||
response.setPaymentChannel(PaymentChannel.WECHAT_JSAPI); |
|||
response.setAmount(amount); |
|||
response.setTenantId(tenantId); |
|||
response.setCurrency("CNY"); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建微信H5支付响应 |
|||
*/ |
|||
public static PaymentResponse wechatH5(String orderNo, String h5Url, BigDecimal amount, Integer tenantId) { |
|||
PaymentResponse response = success(orderNo, PaymentType.WECHAT); |
|||
response.setH5Url(h5Url); |
|||
response.setPaymentChannel(PaymentChannel.WECHAT_H5); |
|||
response.setAmount(amount); |
|||
response.setTenantId(tenantId); |
|||
response.setCurrency("CNY"); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建支付宝网页支付响应 |
|||
*/ |
|||
public static PaymentResponse alipayWeb(String orderNo, String alipayForm, BigDecimal amount, Integer tenantId) { |
|||
PaymentResponse response = success(orderNo, PaymentType.ALIPAY); |
|||
response.setAlipayForm(alipayForm); |
|||
response.setPaymentChannel(PaymentChannel.ALIPAY_WEB); |
|||
response.setAmount(amount); |
|||
response.setTenantId(tenantId); |
|||
response.setCurrency("CNY"); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建支付宝手机网站支付响应 |
|||
*/ |
|||
public static PaymentResponse alipayWap(String orderNo, String alipayUrl, BigDecimal amount, Integer tenantId) { |
|||
PaymentResponse response = success(orderNo, PaymentType.ALIPAY); |
|||
response.setAlipayUrl(alipayUrl); |
|||
response.setPaymentChannel(PaymentChannel.ALIPAY_WAP); |
|||
response.setAmount(amount); |
|||
response.setTenantId(tenantId); |
|||
response.setCurrency("CNY"); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建支付宝APP支付响应 |
|||
*/ |
|||
public static PaymentResponse alipayApp(String orderNo, String alipayParams, BigDecimal amount, Integer tenantId) { |
|||
PaymentResponse response = success(orderNo, PaymentType.ALIPAY); |
|||
response.setAlipayParams(alipayParams); |
|||
response.setPaymentChannel(PaymentChannel.ALIPAY_APP); |
|||
response.setAmount(amount); |
|||
response.setTenantId(tenantId); |
|||
response.setCurrency("CNY"); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 创建余额支付响应 |
|||
*/ |
|||
public static PaymentResponse balance(String orderNo, BigDecimal amount, Integer tenantId, Integer userId) { |
|||
PaymentResponse response = success(orderNo, PaymentType.BALANCE); |
|||
response.setPaymentChannel(PaymentChannel.BALANCE); |
|||
response.setPaymentStatus(PaymentStatus.SUCCESS); |
|||
response.setAmount(amount); |
|||
response.setPaidAmount(amount); |
|||
response.setTenantId(tenantId); |
|||
response.setUserId(userId); |
|||
response.setCurrency("CNY"); |
|||
response.setPayTime(LocalDateTime.now()); |
|||
return response; |
|||
} |
|||
|
|||
/** |
|||
* 判断是否为成功响应 |
|||
*/ |
|||
public boolean isSuccess() { |
|||
return Boolean.TRUE.equals(success); |
|||
} |
|||
|
|||
/** |
|||
* 判断是否需要用户进一步操作 |
|||
*/ |
|||
public boolean needUserAction() { |
|||
return isSuccess() && paymentStatus == PaymentStatus.PENDING; |
|||
} |
|||
|
|||
/** |
|||
* 获取支付结果描述 |
|||
*/ |
|||
public String getResultDescription() { |
|||
if (!isSuccess()) { |
|||
return errorMessage != null ? errorMessage : "支付失败"; |
|||
} |
|||
|
|||
if (paymentStatus == null) { |
|||
return "支付状态未知"; |
|||
} |
|||
|
|||
switch (paymentStatus) { |
|||
case SUCCESS: |
|||
return "支付成功"; |
|||
case PENDING: |
|||
return "等待支付"; |
|||
case PROCESSING: |
|||
return "支付处理中"; |
|||
case FAILED: |
|||
return "支付失败"; |
|||
case CANCELLED: |
|||
return "支付已取消"; |
|||
case TIMEOUT: |
|||
return "支付超时"; |
|||
default: |
|||
return paymentStatus.getName(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return String.format("PaymentResponse{success=%s, orderNo='%s', paymentType=%s, paymentStatus=%s, amount=%s}", |
|||
success, orderNo, paymentType, paymentStatus, amount); |
|||
} |
|||
} |
@ -0,0 +1,159 @@ |
|||
package com.gxwebsoft.payment.enums; |
|||
|
|||
/** |
|||
* 支付渠道枚举 |
|||
* 定义具体的支付渠道类型 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
public enum PaymentChannel { |
|||
|
|||
/** 微信JSAPI支付 */ |
|||
WECHAT_JSAPI("wechat_jsapi", "微信JSAPI支付", PaymentType.WECHAT), |
|||
|
|||
/** 微信Native支付 */ |
|||
WECHAT_NATIVE("wechat_native", "微信Native支付", PaymentType.WECHAT_NATIVE), |
|||
|
|||
/** 微信H5支付 */ |
|||
WECHAT_H5("wechat_h5", "微信H5支付", PaymentType.WECHAT), |
|||
|
|||
/** 微信APP支付 */ |
|||
WECHAT_APP("wechat_app", "微信APP支付", PaymentType.WECHAT), |
|||
|
|||
/** 微信小程序支付 */ |
|||
WECHAT_MINI("wechat_mini", "微信小程序支付", PaymentType.WECHAT), |
|||
|
|||
/** 支付宝网页支付 */ |
|||
ALIPAY_WEB("alipay_web", "支付宝网页支付", PaymentType.ALIPAY), |
|||
|
|||
/** 支付宝手机网站支付 */ |
|||
ALIPAY_WAP("alipay_wap", "支付宝手机网站支付", PaymentType.ALIPAY), |
|||
|
|||
/** 支付宝APP支付 */ |
|||
ALIPAY_APP("alipay_app", "支付宝APP支付", PaymentType.ALIPAY), |
|||
|
|||
/** 支付宝小程序支付 */ |
|||
ALIPAY_MINI("alipay_mini", "支付宝小程序支付", PaymentType.ALIPAY), |
|||
|
|||
/** 银联网关支付 */ |
|||
UNION_WEB("union_web", "银联网关支付", PaymentType.UNION_PAY), |
|||
|
|||
/** 银联手机支付 */ |
|||
UNION_WAP("union_wap", "银联手机支付", PaymentType.UNION_PAY), |
|||
|
|||
/** 余额支付 */ |
|||
BALANCE("balance", "余额支付", PaymentType.BALANCE), |
|||
|
|||
/** 现金支付 */ |
|||
CASH("cash", "现金支付", PaymentType.CASH), |
|||
|
|||
/** POS机支付 */ |
|||
POS("pos", "POS机支付", PaymentType.POS); |
|||
|
|||
private final String code; |
|||
private final String name; |
|||
private final PaymentType paymentType; |
|||
|
|||
PaymentChannel(String code, String name, PaymentType paymentType) { |
|||
this.code = code; |
|||
this.name = name; |
|||
this.paymentType = paymentType; |
|||
} |
|||
|
|||
public String getCode() { |
|||
return code; |
|||
} |
|||
|
|||
public String getName() { |
|||
return name; |
|||
} |
|||
|
|||
public PaymentType getPaymentType() { |
|||
return paymentType; |
|||
} |
|||
|
|||
/** |
|||
* 根据代码获取支付渠道 |
|||
*/ |
|||
public static PaymentChannel getByCode(String code) { |
|||
if (code == null) { |
|||
return null; |
|||
} |
|||
for (PaymentChannel channel : values()) { |
|||
if (channel.code.equals(code)) { |
|||
return channel; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 根据支付类型获取默认渠道 |
|||
*/ |
|||
public static PaymentChannel getDefaultByPaymentType(PaymentType paymentType) { |
|||
if (paymentType == null) { |
|||
return null; |
|||
} |
|||
|
|||
switch (paymentType) { |
|||
case WECHAT: |
|||
return WECHAT_JSAPI; |
|||
case WECHAT_NATIVE: |
|||
return WECHAT_NATIVE; |
|||
case ALIPAY: |
|||
return ALIPAY_WEB; |
|||
case UNION_PAY: |
|||
return UNION_WEB; |
|||
case BALANCE: |
|||
return BALANCE; |
|||
case CASH: |
|||
return CASH; |
|||
case POS: |
|||
return POS; |
|||
default: |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 是否为微信支付渠道 |
|||
*/ |
|||
public boolean isWechatChannel() { |
|||
return paymentType.isWechatPay(); |
|||
} |
|||
|
|||
/** |
|||
* 是否为支付宝支付渠道 |
|||
*/ |
|||
public boolean isAlipayChannel() { |
|||
return paymentType == PaymentType.ALIPAY; |
|||
} |
|||
|
|||
/** |
|||
* 是否为银联支付渠道 |
|||
*/ |
|||
public boolean isUnionPayChannel() { |
|||
return paymentType == PaymentType.UNION_PAY; |
|||
} |
|||
|
|||
/** |
|||
* 是否为第三方支付渠道 |
|||
*/ |
|||
public boolean isThirdPartyChannel() { |
|||
return paymentType.isThirdPartyPay(); |
|||
} |
|||
|
|||
/** |
|||
* 是否支持退款 |
|||
*/ |
|||
public boolean supportRefund() { |
|||
return isThirdPartyChannel(); |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return String.format("PaymentChannel{code='%s', name='%s', paymentType=%s}", |
|||
code, name, paymentType); |
|||
} |
|||
} |
@ -0,0 +1,141 @@ |
|||
package com.gxwebsoft.payment.enums; |
|||
|
|||
/** |
|||
* 支付状态枚举 |
|||
* 定义支付过程中的各种状态 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
public enum PaymentStatus { |
|||
|
|||
/** 待支付 */ |
|||
PENDING(0, "待支付", "PENDING"), |
|||
|
|||
/** 支付中 */ |
|||
PROCESSING(1, "支付中", "PROCESSING"), |
|||
|
|||
/** 支付成功 */ |
|||
SUCCESS(2, "支付成功", "SUCCESS"), |
|||
|
|||
/** 支付失败 */ |
|||
FAILED(3, "支付失败", "FAILED"), |
|||
|
|||
/** 支付取消 */ |
|||
CANCELLED(4, "支付取消", "CANCELLED"), |
|||
|
|||
/** 支付超时 */ |
|||
TIMEOUT(5, "支付超时", "TIMEOUT"), |
|||
|
|||
/** 退款中 */ |
|||
REFUNDING(6, "退款中", "REFUNDING"), |
|||
|
|||
/** 退款成功 */ |
|||
REFUNDED(7, "退款成功", "REFUNDED"), |
|||
|
|||
/** 退款失败 */ |
|||
REFUND_FAILED(8, "退款失败", "REFUND_FAILED"), |
|||
|
|||
/** 部分退款 */ |
|||
PARTIAL_REFUNDED(9, "部分退款", "PARTIAL_REFUNDED"); |
|||
|
|||
private final Integer code; |
|||
private final String name; |
|||
private final String status; |
|||
|
|||
PaymentStatus(Integer code, String name, String status) { |
|||
this.code = code; |
|||
this.name = name; |
|||
this.status = status; |
|||
} |
|||
|
|||
public Integer getCode() { |
|||
return code; |
|||
} |
|||
|
|||
public String getName() { |
|||
return name; |
|||
} |
|||
|
|||
public String getStatus() { |
|||
return status; |
|||
} |
|||
|
|||
/** |
|||
* 根据代码获取支付状态 |
|||
*/ |
|||
public static PaymentStatus getByCode(Integer code) { |
|||
if (code == null) { |
|||
return null; |
|||
} |
|||
for (PaymentStatus status : values()) { |
|||
if (status.code.equals(code)) { |
|||
return status; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 根据状态字符串获取支付状态 |
|||
*/ |
|||
public static PaymentStatus getByStatus(String status) { |
|||
if (status == null) { |
|||
return null; |
|||
} |
|||
for (PaymentStatus paymentStatus : values()) { |
|||
if (paymentStatus.status.equals(status)) { |
|||
return paymentStatus; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 是否为最终状态(不会再变化) |
|||
*/ |
|||
public boolean isFinalStatus() { |
|||
return this == SUCCESS || this == FAILED || this == CANCELLED || |
|||
this == TIMEOUT || this == REFUNDED || this == REFUND_FAILED; |
|||
} |
|||
|
|||
/** |
|||
* 是否为成功状态 |
|||
*/ |
|||
public boolean isSuccessStatus() { |
|||
return this == SUCCESS; |
|||
} |
|||
|
|||
/** |
|||
* 是否为失败状态 |
|||
*/ |
|||
public boolean isFailedStatus() { |
|||
return this == FAILED || this == CANCELLED || this == TIMEOUT || this == REFUND_FAILED; |
|||
} |
|||
|
|||
/** |
|||
* 是否为退款相关状态 |
|||
*/ |
|||
public boolean isRefundStatus() { |
|||
return this == REFUNDING || this == REFUNDED || this == REFUND_FAILED || this == PARTIAL_REFUNDED; |
|||
} |
|||
|
|||
/** |
|||
* 是否可以退款 |
|||
*/ |
|||
public boolean canRefund() { |
|||
return this == SUCCESS || this == PARTIAL_REFUNDED; |
|||
} |
|||
|
|||
/** |
|||
* 是否可以取消 |
|||
*/ |
|||
public boolean canCancel() { |
|||
return this == PENDING || this == PROCESSING; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return String.format("PaymentStatus{code=%d, name='%s', status='%s'}", code, name, status); |
|||
} |
|||
} |
@ -0,0 +1,162 @@ |
|||
package com.gxwebsoft.payment.enums; |
|||
|
|||
/** |
|||
* 支付类型枚举 |
|||
* 定义系统支持的所有支付方式 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
public enum PaymentType { |
|||
|
|||
/** 余额支付 */ |
|||
BALANCE(0, "余额支付", "balance"), |
|||
|
|||
/** 微信支付 */ |
|||
WECHAT(1, "微信支付", "wechat"), |
|||
|
|||
/** 微信Native支付 */ |
|||
WECHAT_NATIVE(102, "微信Native支付", "wechat_native"), |
|||
|
|||
/** 会员卡支付 */ |
|||
MEMBER_CARD(2, "会员卡支付", "member_card"), |
|||
|
|||
/** 支付宝支付 */ |
|||
ALIPAY(3, "支付宝支付", "alipay"), |
|||
|
|||
/** 现金支付 */ |
|||
CASH(4, "现金支付", "cash"), |
|||
|
|||
/** POS机支付 */ |
|||
POS(5, "POS机支付", "pos"), |
|||
|
|||
/** VIP月卡 */ |
|||
VIP_MONTHLY(6, "VIP月卡", "vip_monthly"), |
|||
|
|||
/** VIP年卡 */ |
|||
VIP_YEARLY(7, "VIP年卡", "vip_yearly"), |
|||
|
|||
/** VIP次卡 */ |
|||
VIP_COUNT(8, "VIP次卡", "vip_count"), |
|||
|
|||
/** IC月卡 */ |
|||
IC_MONTHLY(9, "IC月卡", "ic_monthly"), |
|||
|
|||
/** IC年卡 */ |
|||
IC_YEARLY(10, "IC年卡", "ic_yearly"), |
|||
|
|||
/** IC次卡 */ |
|||
IC_COUNT(11, "IC次卡", "ic_count"), |
|||
|
|||
/** 免费 */ |
|||
FREE(12, "免费", "free"), |
|||
|
|||
/** VIP充值卡 */ |
|||
VIP_RECHARGE(13, "VIP充值卡", "vip_recharge"), |
|||
|
|||
/** IC充值卡 */ |
|||
IC_RECHARGE(14, "IC充值卡", "ic_recharge"), |
|||
|
|||
/** 积分支付 */ |
|||
POINTS(15, "积分支付", "points"), |
|||
|
|||
/** VIP季卡 */ |
|||
VIP_QUARTERLY(16, "VIP季卡", "vip_quarterly"), |
|||
|
|||
/** IC季卡 */ |
|||
IC_QUARTERLY(17, "IC季卡", "ic_quarterly"), |
|||
|
|||
/** 代付 */ |
|||
PROXY_PAY(18, "代付", "proxy_pay"), |
|||
|
|||
/** 银联支付 */ |
|||
UNION_PAY(19, "银联支付", "union_pay"); |
|||
|
|||
private final Integer code; |
|||
private final String name; |
|||
private final String channel; |
|||
|
|||
PaymentType(Integer code, String name, String channel) { |
|||
this.code = code; |
|||
this.name = name; |
|||
this.channel = channel; |
|||
} |
|||
|
|||
public Integer getCode() { |
|||
return code; |
|||
} |
|||
|
|||
public String getName() { |
|||
return name; |
|||
} |
|||
|
|||
public String getChannel() { |
|||
return channel; |
|||
} |
|||
|
|||
/** |
|||
* 根据代码获取支付类型 |
|||
*/ |
|||
public static PaymentType getByCode(Integer code) { |
|||
if (code == null) { |
|||
return null; |
|||
} |
|||
for (PaymentType type : values()) { |
|||
if (type.code.equals(code)) { |
|||
return type; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 根据渠道获取支付类型 |
|||
*/ |
|||
public static PaymentType getByChannel(String channel) { |
|||
if (channel == null) { |
|||
return null; |
|||
} |
|||
for (PaymentType type : values()) { |
|||
if (type.channel.equals(channel)) { |
|||
return type; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 是否为微信支付类型 |
|||
*/ |
|||
public boolean isWechatPay() { |
|||
return this == WECHAT || this == WECHAT_NATIVE; |
|||
} |
|||
|
|||
/** |
|||
* 是否为第三方支付 |
|||
*/ |
|||
public boolean isThirdPartyPay() { |
|||
return isWechatPay() || this == ALIPAY || this == UNION_PAY; |
|||
} |
|||
|
|||
/** |
|||
* 是否需要在线支付 |
|||
*/ |
|||
public boolean isOnlinePay() { |
|||
return isThirdPartyPay(); |
|||
} |
|||
|
|||
/** |
|||
* 是否为卡类支付 |
|||
*/ |
|||
public boolean isCardPay() { |
|||
return this == MEMBER_CARD || |
|||
this == VIP_MONTHLY || this == VIP_YEARLY || this == VIP_COUNT || this == VIP_QUARTERLY || |
|||
this == IC_MONTHLY || this == IC_YEARLY || this == IC_COUNT || this == IC_QUARTERLY || |
|||
this == VIP_RECHARGE || this == IC_RECHARGE; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return String.format("PaymentType{code=%d, name='%s', channel='%s'}", code, name, channel); |
|||
} |
|||
} |
@ -0,0 +1,221 @@ |
|||
package com.gxwebsoft.payment.exception; |
|||
|
|||
import com.gxwebsoft.payment.enums.PaymentType; |
|||
|
|||
/** |
|||
* 支付异常基类 |
|||
* 统一处理支付相关的业务异常 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
public class PaymentException extends Exception { |
|||
|
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 错误代码 |
|||
*/ |
|||
private String errorCode; |
|||
|
|||
/** |
|||
* 支付类型 |
|||
*/ |
|||
private PaymentType paymentType; |
|||
|
|||
/** |
|||
* 租户ID |
|||
*/ |
|||
private Integer tenantId; |
|||
|
|||
/** |
|||
* 订单号 |
|||
*/ |
|||
private String orderNo; |
|||
|
|||
public PaymentException(String message) { |
|||
super(message); |
|||
} |
|||
|
|||
public PaymentException(String message, Throwable cause) { |
|||
super(message, cause); |
|||
} |
|||
|
|||
public PaymentException(String errorCode, String message) { |
|||
super(message); |
|||
this.errorCode = errorCode; |
|||
} |
|||
|
|||
public PaymentException(String errorCode, String message, Throwable cause) { |
|||
super(message, cause); |
|||
this.errorCode = errorCode; |
|||
} |
|||
|
|||
public PaymentException(String errorCode, String message, PaymentType paymentType) { |
|||
super(message); |
|||
this.errorCode = errorCode; |
|||
this.paymentType = paymentType; |
|||
} |
|||
|
|||
public PaymentException(String errorCode, String message, PaymentType paymentType, Integer tenantId) { |
|||
super(message); |
|||
this.errorCode = errorCode; |
|||
this.paymentType = paymentType; |
|||
this.tenantId = tenantId; |
|||
} |
|||
|
|||
public PaymentException(String errorCode, String message, PaymentType paymentType, Integer tenantId, String orderNo) { |
|||
super(message); |
|||
this.errorCode = errorCode; |
|||
this.paymentType = paymentType; |
|||
this.tenantId = tenantId; |
|||
this.orderNo = orderNo; |
|||
} |
|||
|
|||
// Getters and Setters
|
|||
public String getErrorCode() { |
|||
return errorCode; |
|||
} |
|||
|
|||
public void setErrorCode(String errorCode) { |
|||
this.errorCode = errorCode; |
|||
} |
|||
|
|||
public PaymentType getPaymentType() { |
|||
return paymentType; |
|||
} |
|||
|
|||
public void setPaymentType(PaymentType paymentType) { |
|||
this.paymentType = paymentType; |
|||
} |
|||
|
|||
public Integer getTenantId() { |
|||
return tenantId; |
|||
} |
|||
|
|||
public void setTenantId(Integer tenantId) { |
|||
this.tenantId = tenantId; |
|||
} |
|||
|
|||
public String getOrderNo() { |
|||
return orderNo; |
|||
} |
|||
|
|||
public void setOrderNo(String orderNo) { |
|||
this.orderNo = orderNo; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
StringBuilder sb = new StringBuilder(); |
|||
sb.append("PaymentException{"); |
|||
if (errorCode != null) { |
|||
sb.append("errorCode='").append(errorCode).append("', "); |
|||
} |
|||
if (paymentType != null) { |
|||
sb.append("paymentType=").append(paymentType).append(", "); |
|||
} |
|||
if (tenantId != null) { |
|||
sb.append("tenantId=").append(tenantId).append(", "); |
|||
} |
|||
if (orderNo != null) { |
|||
sb.append("orderNo='").append(orderNo).append("', "); |
|||
} |
|||
sb.append("message='").append(getMessage()).append("'"); |
|||
sb.append("}"); |
|||
return sb.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 支付错误代码常量 |
|||
*/ |
|||
public static class ErrorCode { |
|||
/** 参数错误 */ |
|||
public static final String PARAM_ERROR = "PARAM_ERROR"; |
|||
/** 配置错误 */ |
|||
public static final String CONFIG_ERROR = "CONFIG_ERROR"; |
|||
/** 证书错误 */ |
|||
public static final String CERTIFICATE_ERROR = "CERTIFICATE_ERROR"; |
|||
/** 网络错误 */ |
|||
public static final String NETWORK_ERROR = "NETWORK_ERROR"; |
|||
/** 签名错误 */ |
|||
public static final String SIGNATURE_ERROR = "SIGNATURE_ERROR"; |
|||
/** 金额错误 */ |
|||
public static final String AMOUNT_ERROR = "AMOUNT_ERROR"; |
|||
/** 订单错误 */ |
|||
public static final String ORDER_ERROR = "ORDER_ERROR"; |
|||
/** 状态错误 */ |
|||
public static final String STATUS_ERROR = "STATUS_ERROR"; |
|||
/** 余额不足 */ |
|||
public static final String INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"; |
|||
/** 支付超时 */ |
|||
public static final String TIMEOUT_ERROR = "TIMEOUT_ERROR"; |
|||
/** 重复支付 */ |
|||
public static final String DUPLICATE_PAYMENT = "DUPLICATE_PAYMENT"; |
|||
/** 不支持的支付方式 */ |
|||
public static final String UNSUPPORTED_PAYMENT = "UNSUPPORTED_PAYMENT"; |
|||
/** 系统错误 */ |
|||
public static final String SYSTEM_ERROR = "SYSTEM_ERROR"; |
|||
} |
|||
|
|||
// 静态工厂方法
|
|||
public static PaymentException paramError(String message) { |
|||
return new PaymentException(ErrorCode.PARAM_ERROR, message); |
|||
} |
|||
|
|||
public static PaymentException configError(String message, PaymentType paymentType, Integer tenantId) { |
|||
return new PaymentException(ErrorCode.CONFIG_ERROR, message, paymentType, tenantId); |
|||
} |
|||
|
|||
public static PaymentException certificateError(String message, PaymentType paymentType) { |
|||
return new PaymentException(ErrorCode.CERTIFICATE_ERROR, message, paymentType); |
|||
} |
|||
|
|||
public static PaymentException networkError(String message, PaymentType paymentType, Throwable cause) { |
|||
return new PaymentException(ErrorCode.NETWORK_ERROR, message, cause); |
|||
} |
|||
|
|||
public static PaymentException signatureError(String message, PaymentType paymentType) { |
|||
return new PaymentException(ErrorCode.SIGNATURE_ERROR, message, paymentType); |
|||
} |
|||
|
|||
public static PaymentException amountError(String message) { |
|||
return new PaymentException(ErrorCode.AMOUNT_ERROR, message); |
|||
} |
|||
|
|||
public static PaymentException orderError(String message, String orderNo) { |
|||
PaymentException exception = new PaymentException(ErrorCode.ORDER_ERROR, message); |
|||
exception.setOrderNo(orderNo); |
|||
return exception; |
|||
} |
|||
|
|||
public static PaymentException statusError(String message, String orderNo) { |
|||
PaymentException exception = new PaymentException(ErrorCode.STATUS_ERROR, message); |
|||
exception.setOrderNo(orderNo); |
|||
return exception; |
|||
} |
|||
|
|||
public static PaymentException insufficientBalance(String message, Integer tenantId) { |
|||
return new PaymentException(ErrorCode.INSUFFICIENT_BALANCE, message, PaymentType.BALANCE, tenantId); |
|||
} |
|||
|
|||
public static PaymentException timeoutError(String message, PaymentType paymentType, String orderNo) { |
|||
PaymentException exception = new PaymentException(ErrorCode.TIMEOUT_ERROR, message, paymentType); |
|||
exception.setOrderNo(orderNo); |
|||
return exception; |
|||
} |
|||
|
|||
public static PaymentException duplicatePayment(String message, String orderNo) { |
|||
PaymentException exception = new PaymentException(ErrorCode.DUPLICATE_PAYMENT, message); |
|||
exception.setOrderNo(orderNo); |
|||
return exception; |
|||
} |
|||
|
|||
public static PaymentException unsupportedPayment(String message, PaymentType paymentType) { |
|||
return new PaymentException(ErrorCode.UNSUPPORTED_PAYMENT, message, paymentType); |
|||
} |
|||
|
|||
public static PaymentException systemError(String message, Throwable cause) { |
|||
return new PaymentException(ErrorCode.SYSTEM_ERROR, message, cause); |
|||
} |
|||
} |
@ -0,0 +1,153 @@ |
|||
package com.gxwebsoft.payment.exception; |
|||
|
|||
import com.gxwebsoft.common.core.web.ApiResult; |
|||
import com.gxwebsoft.common.core.web.BaseController; |
|||
import com.gxwebsoft.payment.constants.PaymentConstants; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.validation.BindException; |
|||
import org.springframework.validation.FieldError; |
|||
import org.springframework.web.bind.MethodArgumentNotValidException; |
|||
import org.springframework.web.bind.annotation.ExceptionHandler; |
|||
import org.springframework.web.bind.annotation.ResponseStatus; |
|||
import org.springframework.web.bind.annotation.RestControllerAdvice; |
|||
|
|||
import javax.validation.ConstraintViolation; |
|||
import javax.validation.ConstraintViolationException; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 统一支付异常处理器 |
|||
* 处理所有支付相关的异常和参数验证异常 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@RestControllerAdvice(basePackages = {"com.gxwebsoft.payment.controller", "com.gxwebsoft.shop.controller"}) |
|||
public class PaymentExceptionHandler extends BaseController { |
|||
|
|||
/** |
|||
* 处理支付业务异常 |
|||
*/ |
|||
@ExceptionHandler(PaymentException.class) |
|||
@ResponseStatus(HttpStatus.BAD_REQUEST) |
|||
public ApiResult<?> handlePaymentException(PaymentException e) { |
|||
log.warn("支付业务异常: {}", e.getMessage()); |
|||
|
|||
// 记录详细的异常信息
|
|||
if (e.getTenantId() != null) { |
|||
log.warn("异常租户ID: {}", e.getTenantId()); |
|||
} |
|||
|
|||
if (e.getPaymentType() != null) { |
|||
log.warn("异常支付类型: {}", e.getPaymentType()); |
|||
} |
|||
|
|||
if (e.getOrderNo() != null) { |
|||
log.warn("异常订单号: {}", e.getOrderNo()); |
|||
} |
|||
|
|||
if (e.getErrorCode() != null) { |
|||
log.warn("错误代码: {}", e.getErrorCode()); |
|||
} |
|||
|
|||
return fail(e.getMessage()); |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* 处理参数验证异常(@Valid注解) |
|||
*/ |
|||
@ExceptionHandler(MethodArgumentNotValidException.class) |
|||
@ResponseStatus(HttpStatus.BAD_REQUEST) |
|||
public ApiResult<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { |
|||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors(); |
|||
|
|||
String errorMessage = fieldErrors.stream() |
|||
.map(error -> error.getField() + ": " + error.getDefaultMessage()) |
|||
.collect(Collectors.joining("; ")); |
|||
|
|||
log.warn("参数验证失败: {}", errorMessage); |
|||
|
|||
return fail(PaymentConstants.ErrorMessage.PARAM_ERROR + ": " + errorMessage); |
|||
} |
|||
|
|||
/** |
|||
* 处理绑定异常 |
|||
*/ |
|||
@ExceptionHandler(BindException.class) |
|||
@ResponseStatus(HttpStatus.BAD_REQUEST) |
|||
public ApiResult<?> handleBindException(BindException e) { |
|||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors(); |
|||
|
|||
String errorMessage = fieldErrors.stream() |
|||
.map(error -> error.getField() + ": " + error.getDefaultMessage()) |
|||
.collect(Collectors.joining("; ")); |
|||
|
|||
log.warn("数据绑定失败: {}", errorMessage); |
|||
|
|||
return fail(PaymentConstants.ErrorMessage.PARAM_ERROR + ": " + errorMessage); |
|||
} |
|||
|
|||
/** |
|||
* 处理约束违反异常(@Validated注解) |
|||
*/ |
|||
@ExceptionHandler(ConstraintViolationException.class) |
|||
@ResponseStatus(HttpStatus.BAD_REQUEST) |
|||
public ApiResult<?> handleConstraintViolationException(ConstraintViolationException e) { |
|||
Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); |
|||
|
|||
String errorMessage = violations.stream() |
|||
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) |
|||
.collect(Collectors.joining("; ")); |
|||
|
|||
log.warn("约束验证失败: {}", errorMessage); |
|||
|
|||
return fail(PaymentConstants.ErrorMessage.PARAM_ERROR + ": " + errorMessage); |
|||
} |
|||
|
|||
/** |
|||
* 处理非法参数异常 |
|||
*/ |
|||
@ExceptionHandler(IllegalArgumentException.class) |
|||
@ResponseStatus(HttpStatus.BAD_REQUEST) |
|||
public ApiResult<?> handleIllegalArgumentException(IllegalArgumentException e) { |
|||
log.warn("非法参数异常: {}", e.getMessage()); |
|||
return fail(PaymentConstants.ErrorMessage.PARAM_ERROR + ": " + e.getMessage()); |
|||
} |
|||
|
|||
/** |
|||
* 处理空指针异常 |
|||
*/ |
|||
@ExceptionHandler(NullPointerException.class) |
|||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) |
|||
public ApiResult<?> handleNullPointerException(NullPointerException e) { |
|||
log.error("空指针异常", e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
|
|||
/** |
|||
* 处理其他运行时异常 |
|||
*/ |
|||
@ExceptionHandler(RuntimeException.class) |
|||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) |
|||
public ApiResult<?> handleRuntimeException(RuntimeException e) { |
|||
log.error("运行时异常: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
|
|||
/** |
|||
* 处理其他异常 |
|||
*/ |
|||
@ExceptionHandler(Exception.class) |
|||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) |
|||
public ApiResult<?> handleException(Exception e) { |
|||
log.error("未知异常: {}", e.getMessage(), e); |
|||
return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); |
|||
} |
|||
} |
@ -0,0 +1,161 @@ |
|||
package com.gxwebsoft.payment.service; |
|||
|
|||
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.math.BigDecimal; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 统一支付服务接口 |
|||
* 提供所有支付方式的统一入口 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
public interface PaymentService { |
|||
|
|||
/** |
|||
* 创建支付订单 |
|||
* |
|||
* @param request 支付请求 |
|||
* @return 支付响应 |
|||
* @throws PaymentException 支付创建失败时抛出 |
|||
*/ |
|||
PaymentResponse createPayment(PaymentRequest request) throws PaymentException; |
|||
|
|||
/** |
|||
* 查询支付状态 |
|||
* |
|||
* @param orderNo 订单号 |
|||
* @param paymentType 支付类型 |
|||
* @param tenantId 租户ID |
|||
* @return 支付响应 |
|||
* @throws PaymentException 查询失败时抛出 |
|||
*/ |
|||
PaymentResponse queryPayment(String orderNo, PaymentType paymentType, Integer tenantId) throws PaymentException; |
|||
|
|||
/** |
|||
* 处理支付回调通知 |
|||
* |
|||
* @param paymentType 支付类型 |
|||
* @param headers 请求头 |
|||
* @param body 请求体 |
|||
* @param tenantId 租户ID |
|||
* @return 处理结果,返回给第三方的响应内容 |
|||
* @throws PaymentException 处理失败时抛出 |
|||
*/ |
|||
String handlePaymentNotify(PaymentType paymentType, Map<String, String> headers, String body, Integer tenantId) throws PaymentException; |
|||
|
|||
/** |
|||
* 申请退款 |
|||
* |
|||
* @param orderNo 订单号 |
|||
* @param refundNo 退款单号 |
|||
* @param paymentType 支付类型 |
|||
* @param totalAmount 订单总金额 |
|||
* @param refundAmount 退款金额 |
|||
* @param reason 退款原因 |
|||
* @param tenantId 租户ID |
|||
* @return 退款响应 |
|||
* @throws PaymentException 退款申请失败时抛出 |
|||
*/ |
|||
PaymentResponse refund(String orderNo, String refundNo, PaymentType paymentType, |
|||
BigDecimal totalAmount, BigDecimal refundAmount, |
|||
String reason, Integer tenantId) throws PaymentException; |
|||
|
|||
/** |
|||
* 查询退款状态 |
|||
* |
|||
* @param refundNo 退款单号 |
|||
* @param paymentType 支付类型 |
|||
* @param tenantId 租户ID |
|||
* @return 退款查询响应 |
|||
* @throws PaymentException 查询失败时抛出 |
|||
*/ |
|||
PaymentResponse queryRefund(String refundNo, PaymentType paymentType, Integer tenantId) throws PaymentException; |
|||
|
|||
/** |
|||
* 关闭订单 |
|||
* |
|||
* @param orderNo 订单号 |
|||
* @param paymentType 支付类型 |
|||
* @param tenantId 租户ID |
|||
* @return 关闭结果 |
|||
* @throws PaymentException 关闭失败时抛出 |
|||
*/ |
|||
boolean closeOrder(String orderNo, PaymentType paymentType, Integer tenantId) throws PaymentException; |
|||
|
|||
/** |
|||
* 获取支持的支付类型列表 |
|||
* |
|||
* @return 支付类型列表 |
|||
*/ |
|||
List<PaymentType> getSupportedPaymentTypes(); |
|||
|
|||
/** |
|||
* 检查支付类型是否支持 |
|||
* |
|||
* @param paymentType 支付类型 |
|||
* @return true表示支持 |
|||
*/ |
|||
boolean isPaymentTypeSupported(PaymentType paymentType); |
|||
|
|||
/** |
|||
* 检查支付类型是否支持退款 |
|||
* |
|||
* @param paymentType 支付类型 |
|||
* @return true表示支持退款 |
|||
*/ |
|||
boolean isRefundSupported(PaymentType paymentType); |
|||
|
|||
/** |
|||
* 检查支付类型是否支持查询 |
|||
* |
|||
* @param paymentType 支付类型 |
|||
* @return true表示支持查询 |
|||
*/ |
|||
boolean isQuerySupported(PaymentType paymentType); |
|||
|
|||
/** |
|||
* 检查支付类型是否支持关闭订单 |
|||
* |
|||
* @param paymentType 支付类型 |
|||
* @return true表示支持关闭订单 |
|||
*/ |
|||
boolean isCloseSupported(PaymentType paymentType); |
|||
|
|||
/** |
|||
* 检查支付类型是否需要异步通知 |
|||
* |
|||
* @param paymentType 支付类型 |
|||
* @return true表示需要异步通知 |
|||
*/ |
|||
boolean isNotifyNeeded(PaymentType paymentType); |
|||
|
|||
/** |
|||
* 验证支付请求参数 |
|||
* |
|||
* @param request 支付请求 |
|||
* @throws PaymentException 参数验证失败时抛出 |
|||
*/ |
|||
void validatePaymentRequest(PaymentRequest request) throws PaymentException; |
|||
|
|||
/** |
|||
* 获取支付策略信息 |
|||
* |
|||
* @param paymentType 支付类型 |
|||
* @return 策略信息Map,包含策略名称、描述等 |
|||
*/ |
|||
Map<String, Object> getPaymentStrategyInfo(PaymentType paymentType); |
|||
|
|||
/** |
|||
* 获取所有支付策略信息 |
|||
* |
|||
* @return 所有策略信息列表 |
|||
*/ |
|||
List<Map<String, Object>> getAllPaymentStrategyInfo(); |
|||
} |
@ -0,0 +1,242 @@ |
|||
package com.gxwebsoft.payment.service; |
|||
|
|||
import com.gxwebsoft.common.core.config.CertificateProperties; |
|||
import com.gxwebsoft.common.core.service.CertificateService; |
|||
import com.gxwebsoft.common.core.utils.RedisUtil; |
|||
import com.gxwebsoft.common.core.utils.WxNativeUtil; |
|||
import com.gxwebsoft.common.system.entity.Payment; |
|||
import com.gxwebsoft.payment.exception.PaymentException; |
|||
import com.wechat.pay.java.core.Config; |
|||
import com.wechat.pay.java.core.RSAAutoCertificateConfig; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.core.io.ClassPathResource; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.io.IOException; |
|||
|
|||
/** |
|||
* 微信支付配置服务 |
|||
* 负责管理微信支付的配置信息和证书 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@Service |
|||
public class WxPayConfigService { |
|||
|
|||
@Resource |
|||
private RedisUtil redisUtil; |
|||
|
|||
@Resource |
|||
private CertificateService certificateService; |
|||
|
|||
@Resource |
|||
private CertificateProperties certificateProperties; |
|||
|
|||
@Value("${spring.profiles.active:dev}") |
|||
private String activeProfile; |
|||
|
|||
/** |
|||
* 获取微信支付配置 |
|||
* |
|||
* @param tenantId 租户ID |
|||
* @return 微信支付配置 |
|||
* @throws PaymentException 配置获取失败时抛出 |
|||
*/ |
|||
public Config getWxPayConfig(Integer tenantId) throws PaymentException { |
|||
if (tenantId == null) { |
|||
throw PaymentException.paramError("租户ID不能为空"); |
|||
} |
|||
|
|||
// 先从缓存获取已构建的配置
|
|||
Config cachedConfig = WxNativeUtil.getConfig(tenantId); |
|||
if (cachedConfig != null) { |
|||
log.debug("从缓存获取微信支付配置成功,租户ID: {}", tenantId); |
|||
return cachedConfig; |
|||
} |
|||
|
|||
// 构建新的配置
|
|||
Config newConfig = buildWxPayConfig(tenantId); |
|||
|
|||
// 缓存配置
|
|||
WxNativeUtil.addConfig(tenantId, newConfig); |
|||
log.info("微信支付配置创建并缓存成功,租户ID: {}", tenantId); |
|||
|
|||
return newConfig; |
|||
} |
|||
|
|||
/** |
|||
* 构建微信支付配置 |
|||
*/ |
|||
private Config buildWxPayConfig(Integer tenantId) throws PaymentException { |
|||
try { |
|||
// 获取支付配置信息
|
|||
Payment payment = getPaymentConfig(tenantId); |
|||
|
|||
// 获取证书文件路径
|
|||
String certificatePath = getCertificatePath(tenantId, payment); |
|||
|
|||
// 创建微信支付配置对象
|
|||
return createWxPayConfig(payment, certificatePath); |
|||
|
|||
} catch (Exception e) { |
|||
if (e instanceof PaymentException) { |
|||
throw e; |
|||
} |
|||
throw PaymentException.systemError("构建微信支付配置失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取支付配置信息 |
|||
*/ |
|||
private Payment getPaymentConfig(Integer tenantId) throws PaymentException { |
|||
String cacheKey = "Payment:wxPay:" + tenantId; |
|||
Payment payment = redisUtil.get(cacheKey, Payment.class); |
|||
|
|||
if (payment == null && !"dev".equals(activeProfile)) { |
|||
throw PaymentException.systemError("微信支付配置未找到,租户ID: " + tenantId, null); |
|||
} |
|||
|
|||
if (payment != null) { |
|||
log.debug("从缓存获取支付配置成功,租户ID: {}", tenantId); |
|||
} else { |
|||
log.debug("开发环境模式,将使用测试配置,租户ID: {}", tenantId); |
|||
} |
|||
|
|||
return payment; |
|||
} |
|||
|
|||
/** |
|||
* 获取证书文件路径 |
|||
*/ |
|||
private String getCertificatePath(Integer tenantId, Payment payment) throws PaymentException { |
|||
if ("dev".equals(activeProfile)) { |
|||
return getDevCertificatePath(tenantId); |
|||
} else { |
|||
return getProdCertificatePath(payment); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取开发环境证书路径 |
|||
*/ |
|||
private String getDevCertificatePath(Integer tenantId) throws PaymentException { |
|||
try { |
|||
String certPath = "cert/wechat-pay/apiclient_key.pem"; |
|||
ClassPathResource resource = new ClassPathResource(certPath); |
|||
|
|||
if (!resource.exists()) { |
|||
throw PaymentException.systemError("开发环境微信支付证书文件不存在: " + certPath, null); |
|||
} |
|||
|
|||
String absolutePath = resource.getFile().getAbsolutePath(); |
|||
log.debug("开发环境证书路径: {}", absolutePath); |
|||
return absolutePath; |
|||
|
|||
} catch (IOException e) { |
|||
throw PaymentException.systemError("获取开发环境证书路径失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取生产环境证书路径 |
|||
*/ |
|||
private String getProdCertificatePath(Payment payment) throws PaymentException { |
|||
if (payment == null || payment.getApiclientKey() == null || payment.getApiclientKey().trim().isEmpty()) { |
|||
throw PaymentException.systemError("生产环境支付配置或证书密钥文件为空", null); |
|||
} |
|||
|
|||
try { |
|||
// 使用微信支付证书路径
|
|||
String certificatePath = certificateService.getWechatPayCertPath(payment.getApiclientKey()); |
|||
if (certificatePath == null) { |
|||
throw PaymentException.systemError("证书文件路径获取失败,证书文件: " + payment.getApiclientKey(), null); |
|||
} |
|||
|
|||
log.debug("生产环境证书路径: {}", certificatePath); |
|||
return certificatePath; |
|||
|
|||
} catch (Exception e) { |
|||
throw PaymentException.systemError("获取生产环境证书路径失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 创建微信支付配置对象 |
|||
*/ |
|||
private Config createWxPayConfig(Payment payment, String certificatePath) throws PaymentException { |
|||
try { |
|||
if ("dev".equals(activeProfile) && payment == null) { |
|||
// 开发环境测试配置
|
|||
return createDevTestConfig(certificatePath); |
|||
} else if (payment != null) { |
|||
// 正常配置
|
|||
return createNormalConfig(payment, certificatePath); |
|||
} else { |
|||
throw PaymentException.systemError("无法创建微信支付配置:配置信息不完整", null); |
|||
} |
|||
} catch (Exception e) { |
|||
if (e instanceof PaymentException) { |
|||
throw e; |
|||
} |
|||
throw PaymentException.systemError("创建微信支付配置对象失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 创建开发环境测试配置 |
|||
*/ |
|||
private Config createDevTestConfig(String certificatePath) throws PaymentException { |
|||
String testMerchantId = "1246610101"; |
|||
String testMerchantSerialNumber = "2903B872D5CA36E525FAEC37AEDB22E54ECDE7B7"; |
|||
String testApiV3Key = certificateProperties.getWechatPay().getDev().getApiV3Key(); |
|||
|
|||
if (testApiV3Key == null || testApiV3Key.trim().isEmpty()) { |
|||
throw PaymentException.systemError("开发环境APIv3密钥未配置", null); |
|||
} |
|||
|
|||
log.info("使用开发环境测试配置"); |
|||
log.debug("测试商户号: {}", testMerchantId); |
|||
log.debug("测试序列号: {}", testMerchantSerialNumber); |
|||
|
|||
return new RSAAutoCertificateConfig.Builder() |
|||
.merchantId(testMerchantId) |
|||
.privateKeyFromPath(certificatePath) |
|||
.merchantSerialNumber(testMerchantSerialNumber) |
|||
.apiV3Key(testApiV3Key) |
|||
.build(); |
|||
} |
|||
|
|||
/** |
|||
* 创建正常配置 |
|||
*/ |
|||
private Config createNormalConfig(Payment payment, String certificatePath) throws PaymentException { |
|||
if (payment.getMchId() == null || payment.getMerchantSerialNumber() == null || payment.getApiKey() == null) { |
|||
throw PaymentException.systemError("支付配置信息不完整:商户号、序列号或APIv3密钥为空", null); |
|||
} |
|||
|
|||
log.info("使用数据库支付配置"); |
|||
log.debug("商户号: {}", payment.getMchId()); |
|||
|
|||
return new RSAAutoCertificateConfig.Builder() |
|||
.merchantId(payment.getMchId()) |
|||
.privateKeyFromPath(certificatePath) |
|||
.merchantSerialNumber(payment.getMerchantSerialNumber()) |
|||
.apiV3Key(payment.getApiKey()) |
|||
.build(); |
|||
} |
|||
|
|||
/** |
|||
* 清除指定租户的配置缓存 |
|||
* |
|||
* @param tenantId 租户ID |
|||
*/ |
|||
public void clearConfigCache(Integer tenantId) { |
|||
WxNativeUtil.addConfig(tenantId, null); |
|||
log.info("清除微信支付配置缓存,租户ID: {}", tenantId); |
|||
} |
|||
} |
@ -0,0 +1,308 @@ |
|||
package com.gxwebsoft.payment.service; |
|||
|
|||
|
|||
import com.gxwebsoft.payment.constants.PaymentConstants; |
|||
import com.gxwebsoft.payment.exception.PaymentException; |
|||
import com.gxwebsoft.shop.entity.ShopOrder; |
|||
import com.gxwebsoft.shop.service.ShopOrderService; |
|||
import com.wechat.pay.java.core.Config; |
|||
import com.wechat.pay.java.core.notification.NotificationConfig; |
|||
import com.wechat.pay.java.core.notification.NotificationParser; |
|||
import com.wechat.pay.java.core.notification.RequestParam; |
|||
import com.wechat.pay.java.service.payments.model.Transaction; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.math.BigDecimal; |
|||
import java.time.LocalDateTime; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 微信支付回调通知处理服务 |
|||
* 负责处理微信支付的异步通知回调 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@Service |
|||
public class WxPayNotifyService { |
|||
|
|||
@Resource |
|||
private WxPayConfigService wxPayConfigService; |
|||
|
|||
@Resource |
|||
private ShopOrderService shopOrderService; |
|||
|
|||
/** |
|||
* 处理微信支付回调通知 |
|||
* |
|||
* @param headers 请求头 |
|||
* @param body 请求体 |
|||
* @param tenantId 租户ID |
|||
* @return 处理结果响应 |
|||
*/ |
|||
public String handlePaymentNotify(Map<String, String> headers, String body, Integer tenantId) { |
|||
log.info("{}, 租户ID: {}", PaymentConstants.LogMessage.NOTIFY_START, tenantId); |
|||
|
|||
try { |
|||
// 参数验证
|
|||
validateNotifyParams(headers, body, tenantId); |
|||
|
|||
// 获取微信支付配置
|
|||
Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId); |
|||
|
|||
// 解析并验证回调数据
|
|||
Transaction transaction = parseAndVerifyNotification(headers, body, wxPayConfig); |
|||
|
|||
// 处理支付结果
|
|||
processPaymentResult(transaction, tenantId); |
|||
|
|||
log.info("{}, 租户ID: {}, 订单号: {}", |
|||
PaymentConstants.LogMessage.NOTIFY_SUCCESS, tenantId, transaction.getOutTradeNo()); |
|||
|
|||
return "SUCCESS"; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("{}, 租户ID: {}, 错误: {}", |
|||
PaymentConstants.LogMessage.NOTIFY_FAILED, tenantId, e.getMessage(), e); |
|||
return "FAIL"; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证回调通知参数 |
|||
*/ |
|||
private void validateNotifyParams(Map<String, String> headers, String body, Integer tenantId) throws PaymentException { |
|||
if (tenantId == null) { |
|||
throw PaymentException.paramError("租户ID不能为空"); |
|||
} |
|||
|
|||
if (headers == null || headers.isEmpty()) { |
|||
throw PaymentException.paramError("请求头不能为空"); |
|||
} |
|||
|
|||
if (!StringUtils.hasText(body)) { |
|||
throw PaymentException.paramError("请求体不能为空"); |
|||
} |
|||
|
|||
// 验证必要的微信支付头部信息
|
|||
String signature = headers.get("Wechatpay-Signature"); |
|||
String timestamp = headers.get("Wechatpay-Timestamp"); |
|||
String nonce = headers.get("Wechatpay-Nonce"); |
|||
String serial = headers.get("Wechatpay-Serial"); |
|||
|
|||
if (!StringUtils.hasText(signature)) { |
|||
throw PaymentException.paramError("微信支付签名不能为空"); |
|||
} |
|||
|
|||
if (!StringUtils.hasText(timestamp)) { |
|||
throw PaymentException.paramError("微信支付时间戳不能为空"); |
|||
} |
|||
|
|||
if (!StringUtils.hasText(nonce)) { |
|||
throw PaymentException.paramError("微信支付随机数不能为空"); |
|||
} |
|||
|
|||
if (!StringUtils.hasText(serial)) { |
|||
throw PaymentException.paramError("微信支付序列号不能为空"); |
|||
} |
|||
|
|||
log.debug("回调通知参数验证通过, 租户ID: {}", tenantId); |
|||
} |
|||
|
|||
/** |
|||
* 解析并验证回调通知 |
|||
*/ |
|||
private Transaction parseAndVerifyNotification(Map<String, String> headers, String body, Config wxPayConfig) throws PaymentException { |
|||
if (wxPayConfig == null) { |
|||
throw PaymentException.systemError("微信支付配置为空", null); |
|||
} |
|||
|
|||
try { |
|||
// 构建请求参数
|
|||
RequestParam requestParam = new RequestParam.Builder() |
|||
.serialNumber(headers.get("Wechatpay-Serial")) |
|||
.nonce(headers.get("Wechatpay-Nonce")) |
|||
.signature(headers.get("Wechatpay-Signature")) |
|||
.timestamp(headers.get("Wechatpay-Timestamp")) |
|||
.body(body) |
|||
.build(); |
|||
|
|||
// 创建通知解析器
|
|||
NotificationParser parser = new NotificationParser((NotificationConfig) wxPayConfig); |
|||
|
|||
// 解析并验证通知
|
|||
Transaction transaction = parser.parse(requestParam, Transaction.class); |
|||
|
|||
if (transaction == null) { |
|||
throw PaymentException.systemError("解析回调通知失败:transaction为空", null); |
|||
} |
|||
|
|||
log.debug("回调通知解析成功, 订单号: {}, 交易状态: {}", |
|||
transaction.getOutTradeNo(), transaction.getTradeState()); |
|||
|
|||
return transaction; |
|||
|
|||
} catch (Exception e) { |
|||
if (e instanceof PaymentException) { |
|||
throw e; |
|||
} |
|||
throw PaymentException.systemError("解析回调通知失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理支付结果 |
|||
*/ |
|||
private void processPaymentResult(Transaction transaction, Integer tenantId) throws PaymentException { |
|||
String outTradeNo = transaction.getOutTradeNo(); |
|||
String tradeState = String.valueOf(transaction.getTradeState()); |
|||
|
|||
if (!StringUtils.hasText(outTradeNo)) { |
|||
throw PaymentException.paramError("商户订单号不能为空"); |
|||
} |
|||
|
|||
// 查询订单
|
|||
ShopOrder order = shopOrderService.getByOutTradeNo(outTradeNo); |
|||
if (order == null) { |
|||
throw PaymentException.systemError("订单不存在: " + outTradeNo, null); |
|||
} |
|||
|
|||
// 验证租户ID
|
|||
if (!tenantId.equals(order.getTenantId())) { |
|||
throw PaymentException.paramError("订单租户ID不匹配"); |
|||
} |
|||
|
|||
// 验证订单状态 - 使用Boolean类型的payStatus字段
|
|||
if (Boolean.TRUE.equals(order.getPayStatus())) { |
|||
log.info("订单已支付,跳过处理, 订单号: {}", outTradeNo); |
|||
return; |
|||
} |
|||
|
|||
// 根据交易状态处理
|
|||
switch (tradeState) { |
|||
case "SUCCESS": |
|||
handlePaymentSuccess(order, transaction); |
|||
break; |
|||
case "REFUND": |
|||
handlePaymentRefund(order, transaction); |
|||
break; |
|||
case "CLOSED": |
|||
case "REVOKED": |
|||
case "PAYERROR": |
|||
handlePaymentFailed(order, transaction); |
|||
break; |
|||
default: |
|||
log.warn("未处理的交易状态: {}, 订单号: {}", tradeState, outTradeNo); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理支付成功 |
|||
*/ |
|||
private void handlePaymentSuccess(ShopOrder order, Transaction transaction) throws PaymentException { |
|||
try { |
|||
// 验证金额
|
|||
validateAmount(order, transaction); |
|||
|
|||
// 更新订单状态
|
|||
order.setPayStatus(true); // 使用Boolean类型
|
|||
order.setTransactionId(transaction.getTransactionId()); |
|||
order.setPayTime(LocalDateTime.parse(transaction.getSuccessTime())); |
|||
|
|||
// 使用专门的更新方法,会触发支付成功后的业务逻辑
|
|||
shopOrderService.updateByOutTradeNo(order); |
|||
|
|||
// 推送支付结果通知
|
|||
pushPaymentNotification(order, transaction); |
|||
|
|||
log.info("支付成功处理完成, 订单号: {}, 微信交易号: {}", |
|||
order.getOrderNo(), transaction.getTransactionId()); |
|||
|
|||
} catch (Exception e) { |
|||
throw PaymentException.systemError("处理支付成功回调失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理支付退款 |
|||
*/ |
|||
private void handlePaymentRefund(ShopOrder order, Transaction transaction) throws PaymentException { |
|||
try { |
|||
log.info("处理支付退款, 订单号: {}, 微信交易号: {}", |
|||
order.getOrderNo(), transaction.getTransactionId()); |
|||
|
|||
// 这里可以添加退款相关的业务逻辑
|
|||
// 例如:更新订单状态、处理库存、发送通知等
|
|||
|
|||
} catch (Exception e) { |
|||
throw PaymentException.systemError("处理支付退款回调失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理支付失败 |
|||
*/ |
|||
private void handlePaymentFailed(ShopOrder order, Transaction transaction) throws PaymentException { |
|||
try { |
|||
log.info("处理支付失败, 订单号: {}, 交易状态: {}", |
|||
order.getOrderNo(), transaction.getTradeState()); |
|||
|
|||
// 这里可以添加支付失败相关的业务逻辑
|
|||
// 例如:释放库存、发送通知等
|
|||
|
|||
} catch (Exception e) { |
|||
throw PaymentException.systemError("处理支付失败回调失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证支付金额 |
|||
*/ |
|||
private void validateAmount(ShopOrder order, Transaction transaction) throws PaymentException { |
|||
if (transaction.getAmount() == null || transaction.getAmount().getTotal() == null) { |
|||
throw PaymentException.amountError("回调通知中金额信息为空"); |
|||
} |
|||
|
|||
// 将订单金额转换为分
|
|||
BigDecimal orderAmount = order.getMoney(); |
|||
if (orderAmount == null) { |
|||
throw PaymentException.amountError("订单金额为空"); |
|||
} |
|||
|
|||
int orderAmountFen = orderAmount.multiply(new BigDecimal(100)).intValue(); |
|||
int callbackAmountFen = transaction.getAmount().getTotal(); |
|||
|
|||
if (orderAmountFen != callbackAmountFen) { |
|||
throw PaymentException.amountError( |
|||
String.format("订单金额不匹配,订单金额: %d分, 回调金额: %d分", |
|||
orderAmountFen, callbackAmountFen)); |
|||
} |
|||
|
|||
log.debug("金额验证通过, 订单号: {}, 金额: {}分", order.getOrderNo(), orderAmountFen); |
|||
} |
|||
|
|||
/** |
|||
* 推送支付结果通知 |
|||
*/ |
|||
private void pushPaymentNotification(ShopOrder order, Transaction transaction) { |
|||
try { |
|||
// TODO: 实现支付结果通知推送逻辑
|
|||
// 可以在这里添加具体的通知推送实现,比如:
|
|||
// - 发送邮件通知
|
|||
// - 发送短信通知
|
|||
// - 推送到消息队列
|
|||
// - 调用第三方通知接口等
|
|||
|
|||
log.debug("支付结果通知推送成功, 订单号: {}, 交易号: {}", |
|||
order.getOrderNo(), transaction.getTransactionId()); |
|||
} catch (Exception e) { |
|||
log.warn("支付结果通知推送失败, 订单号: {}, 错误: {}", order.getOrderNo(), e.getMessage()); |
|||
// 推送失败不影响主流程,只记录日志
|
|||
} |
|||
} |
|||
} |
@ -0,0 +1,476 @@ |
|||
package com.gxwebsoft.payment.service.impl; |
|||
|
|||
import com.gxwebsoft.payment.constants.PaymentConstants; |
|||
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 com.gxwebsoft.payment.service.PaymentService; |
|||
import com.gxwebsoft.payment.strategy.PaymentStrategy; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import javax.annotation.Resource; |
|||
import java.math.BigDecimal; |
|||
import java.util.*; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 统一支付服务实现 |
|||
* 基于策略模式实现多种支付方式的统一管理 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@Service |
|||
public class PaymentServiceImpl implements PaymentService { |
|||
|
|||
/** |
|||
* 支付策略映射表 |
|||
*/ |
|||
private final Map<PaymentType, PaymentStrategy> strategyMap = new ConcurrentHashMap<>(); |
|||
|
|||
/** |
|||
* 注入所有支付策略实现 |
|||
*/ |
|||
@Resource |
|||
private List<PaymentStrategy> paymentStrategies; |
|||
|
|||
/** |
|||
* 初始化策略映射 |
|||
*/ |
|||
@PostConstruct |
|||
public void initStrategies() { |
|||
if (paymentStrategies != null && !paymentStrategies.isEmpty()) { |
|||
for (PaymentStrategy strategy : paymentStrategies) { |
|||
try { |
|||
PaymentType paymentType = strategy.getSupportedPaymentType(); |
|||
strategyMap.put(paymentType, strategy); |
|||
log.info("注册支付策略: {} -> {}", paymentType.getName(), strategy.getClass().getSimpleName()); |
|||
} catch (Exception e) { |
|||
log.warn("注册支付策略失败: {}, 错误: {}", strategy.getClass().getSimpleName(), e.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
log.info("支付策略初始化完成,共注册 {} 种支付方式", strategyMap.size()); |
|||
|
|||
if (strategyMap.isEmpty()) { |
|||
log.warn("⚠️ 没有可用的支付策略,支付功能将不可用"); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public PaymentResponse createPayment(PaymentRequest request) throws PaymentException { |
|||
log.info("{}, 支付类型: {}, 租户ID: {}, 用户ID: {}, 金额: {}", |
|||
PaymentConstants.LogMessage.PAYMENT_START, request.getPaymentType(), |
|||
request.getTenantId(), request.getUserId(), request.getFormattedAmount()); |
|||
|
|||
try { |
|||
// 基础参数验证
|
|||
validatePaymentRequest(request); |
|||
|
|||
// 获取支付策略
|
|||
PaymentStrategy strategy = getPaymentStrategy(request.getPaymentType()); |
|||
|
|||
// 执行支付
|
|||
PaymentResponse response = strategy.createPayment(request); |
|||
|
|||
log.info("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 金额: {}", |
|||
PaymentConstants.LogMessage.PAYMENT_SUCCESS, request.getPaymentType(), |
|||
request.getTenantId(), response.getOrderNo(), request.getFormattedAmount()); |
|||
|
|||
return response; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}", |
|||
PaymentConstants.LogMessage.PAYMENT_FAILED, request.getPaymentType(), |
|||
request.getTenantId(), e.getMessage()); |
|||
throw e; |
|||
} catch (Exception e) { |
|||
log.error("{}, 支付类型: {}, 租户ID: {}, 系统错误: {}", |
|||
PaymentConstants.LogMessage.PAYMENT_FAILED, request.getPaymentType(), |
|||
request.getTenantId(), e.getMessage(), e); |
|||
throw PaymentException.systemError("支付创建失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public PaymentResponse queryPayment(String orderNo, PaymentType paymentType, Integer tenantId) throws PaymentException { |
|||
log.info("开始查询支付状态, 支付类型: {}, 租户ID: {}, 订单号: {}", |
|||
paymentType, tenantId, orderNo); |
|||
|
|||
try { |
|||
// 参数验证
|
|||
validateQueryParams(orderNo, paymentType, tenantId); |
|||
|
|||
// 获取支付策略
|
|||
PaymentStrategy strategy = getPaymentStrategy(paymentType); |
|||
|
|||
// 检查是否支持查询
|
|||
if (!strategy.supportQuery()) { |
|||
throw PaymentException.unsupportedPayment("该支付方式不支持查询", paymentType); |
|||
} |
|||
|
|||
// 执行查询
|
|||
PaymentResponse response = strategy.queryPayment(orderNo, tenantId); |
|||
|
|||
log.info("支付状态查询成功, 支付类型: {}, 租户ID: {}, 订单号: {}, 状态: {}", |
|||
paymentType, tenantId, orderNo, response.getPaymentStatus()); |
|||
|
|||
return response; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("支付状态查询失败, 支付类型: {}, 租户ID: {}, 订单号: {}, 错误: {}", |
|||
paymentType, tenantId, orderNo, e.getMessage()); |
|||
throw e; |
|||
} catch (Exception e) { |
|||
log.error("支付状态查询系统错误, 支付类型: {}, 租户ID: {}, 订单号: {}, 错误: {}", |
|||
paymentType, tenantId, orderNo, e.getMessage(), e); |
|||
throw PaymentException.systemError("支付查询失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public String handlePaymentNotify(PaymentType paymentType, Map<String, String> headers, String body, Integer tenantId) throws PaymentException { |
|||
log.info("{}, 支付类型: {}, 租户ID: {}", |
|||
PaymentConstants.LogMessage.NOTIFY_START, paymentType, tenantId); |
|||
|
|||
try { |
|||
// 参数验证
|
|||
validateNotifyParams(paymentType, headers, body, tenantId); |
|||
|
|||
// 获取支付策略
|
|||
PaymentStrategy strategy = getPaymentStrategy(paymentType); |
|||
|
|||
// 检查是否需要异步通知
|
|||
if (!strategy.needNotify()) { |
|||
log.warn("该支付方式不需要异步通知, 支付类型: {}", paymentType); |
|||
return PaymentConstants.Wechat.NOTIFY_SUCCESS; |
|||
} |
|||
|
|||
// 处理回调
|
|||
String result = strategy.handleNotify(headers, body, tenantId); |
|||
|
|||
log.info("{}, 支付类型: {}, 租户ID: {}", |
|||
PaymentConstants.LogMessage.NOTIFY_SUCCESS, paymentType, tenantId); |
|||
|
|||
return result; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}", |
|||
PaymentConstants.LogMessage.NOTIFY_FAILED, paymentType, tenantId, e.getMessage()); |
|||
throw e; |
|||
} catch (Exception e) { |
|||
log.error("{}, 支付类型: {}, 租户ID: {}, 系统错误: {}", |
|||
PaymentConstants.LogMessage.NOTIFY_FAILED, paymentType, tenantId, e.getMessage(), e); |
|||
throw PaymentException.systemError("支付回调处理失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public PaymentResponse refund(String orderNo, String refundNo, PaymentType paymentType, |
|||
BigDecimal totalAmount, BigDecimal refundAmount, |
|||
String reason, Integer tenantId) throws PaymentException { |
|||
log.info("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 退款单号: {}, 退款金额: {}", |
|||
PaymentConstants.LogMessage.REFUND_START, paymentType, tenantId, orderNo, refundNo, refundAmount); |
|||
|
|||
try { |
|||
// 参数验证
|
|||
validateRefundParams(orderNo, refundNo, paymentType, totalAmount, refundAmount, tenantId); |
|||
|
|||
// 获取支付策略
|
|||
PaymentStrategy strategy = getPaymentStrategy(paymentType); |
|||
|
|||
// 检查是否支持退款
|
|||
if (!strategy.supportRefund()) { |
|||
throw PaymentException.unsupportedPayment("该支付方式不支持退款", paymentType); |
|||
} |
|||
|
|||
// 执行退款
|
|||
PaymentResponse response = strategy.refund(orderNo, refundNo, totalAmount, refundAmount, reason, tenantId); |
|||
|
|||
log.info("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 退款单号: {}, 退款金额: {}", |
|||
PaymentConstants.LogMessage.REFUND_SUCCESS, paymentType, tenantId, orderNo, refundNo, refundAmount); |
|||
|
|||
return response; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 退款单号: {}, 错误: {}", |
|||
PaymentConstants.LogMessage.REFUND_FAILED, paymentType, tenantId, orderNo, refundNo, e.getMessage()); |
|||
throw e; |
|||
} catch (Exception e) { |
|||
log.error("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 退款单号: {}, 系统错误: {}", |
|||
PaymentConstants.LogMessage.REFUND_FAILED, paymentType, tenantId, orderNo, refundNo, e.getMessage(), e); |
|||
throw PaymentException.systemError("退款申请失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public PaymentResponse queryRefund(String refundNo, PaymentType paymentType, Integer tenantId) throws PaymentException { |
|||
log.info("开始查询退款状态, 支付类型: {}, 租户ID: {}, 退款单号: {}", |
|||
paymentType, tenantId, refundNo); |
|||
|
|||
try { |
|||
// 参数验证
|
|||
validateRefundQueryParams(refundNo, paymentType, tenantId); |
|||
|
|||
// 获取支付策略
|
|||
PaymentStrategy strategy = getPaymentStrategy(paymentType); |
|||
|
|||
// 检查是否支持退款查询
|
|||
if (!strategy.supportRefund()) { |
|||
throw PaymentException.unsupportedPayment("该支付方式不支持退款查询", paymentType); |
|||
} |
|||
|
|||
// 执行查询
|
|||
PaymentResponse response = strategy.queryRefund(refundNo, tenantId); |
|||
|
|||
log.info("退款状态查询成功, 支付类型: {}, 租户ID: {}, 退款单号: {}, 状态: {}", |
|||
paymentType, tenantId, refundNo, response.getPaymentStatus()); |
|||
|
|||
return response; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("退款状态查询失败, 支付类型: {}, 租户ID: {}, 退款单号: {}, 错误: {}", |
|||
paymentType, tenantId, refundNo, e.getMessage()); |
|||
throw e; |
|||
} catch (Exception e) { |
|||
log.error("退款状态查询系统错误, 支付类型: {}, 租户ID: {}, 退款单号: {}, 错误: {}", |
|||
paymentType, tenantId, refundNo, e.getMessage(), e); |
|||
throw PaymentException.systemError("退款查询失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean closeOrder(String orderNo, PaymentType paymentType, Integer tenantId) throws PaymentException { |
|||
log.info("开始关闭订单, 支付类型: {}, 租户ID: {}, 订单号: {}", |
|||
paymentType, tenantId, orderNo); |
|||
|
|||
try { |
|||
// 参数验证
|
|||
validateCloseParams(orderNo, paymentType, tenantId); |
|||
|
|||
// 获取支付策略
|
|||
PaymentStrategy strategy = getPaymentStrategy(paymentType); |
|||
|
|||
// 检查是否支持关闭订单
|
|||
if (!strategy.supportClose()) { |
|||
throw PaymentException.unsupportedPayment("该支付方式不支持关闭订单", paymentType); |
|||
} |
|||
|
|||
// 执行关闭
|
|||
boolean result = strategy.closeOrder(orderNo, tenantId); |
|||
|
|||
log.info("订单关闭{}, 支付类型: {}, 租户ID: {}, 订单号: {}", |
|||
result ? "成功" : "失败", paymentType, tenantId, orderNo); |
|||
|
|||
return result; |
|||
|
|||
} catch (PaymentException e) { |
|||
log.error("订单关闭失败, 支付类型: {}, 租户ID: {}, 订单号: {}, 错误: {}", |
|||
paymentType, tenantId, orderNo, e.getMessage()); |
|||
throw e; |
|||
} catch (Exception e) { |
|||
log.error("订单关闭系统错误, 支付类型: {}, 租户ID: {}, 订单号: {}, 错误: {}", |
|||
paymentType, tenantId, orderNo, e.getMessage(), e); |
|||
throw PaymentException.systemError("订单关闭失败: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public List<PaymentType> getSupportedPaymentTypes() { |
|||
return new ArrayList<>(strategyMap.keySet()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isPaymentTypeSupported(PaymentType paymentType) { |
|||
return strategyMap.containsKey(paymentType); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isRefundSupported(PaymentType paymentType) { |
|||
PaymentStrategy strategy = strategyMap.get(paymentType); |
|||
return strategy != null && strategy.supportRefund(); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isQuerySupported(PaymentType paymentType) { |
|||
PaymentStrategy strategy = strategyMap.get(paymentType); |
|||
return strategy != null && strategy.supportQuery(); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isCloseSupported(PaymentType paymentType) { |
|||
PaymentStrategy strategy = strategyMap.get(paymentType); |
|||
return strategy != null && strategy.supportClose(); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isNotifyNeeded(PaymentType paymentType) { |
|||
PaymentStrategy strategy = strategyMap.get(paymentType); |
|||
return strategy != null && strategy.needNotify(); |
|||
} |
|||
|
|||
@Override |
|||
public void validatePaymentRequest(PaymentRequest request) throws PaymentException { |
|||
if (request == null) { |
|||
throw PaymentException.paramError("支付请求不能为空"); |
|||
} |
|||
|
|||
if (request.getPaymentType() == null) { |
|||
throw PaymentException.paramError("支付类型不能为空"); |
|||
} |
|||
|
|||
if (request.getTenantId() == null || request.getTenantId() <= 0) { |
|||
throw PaymentException.paramError("租户ID不能为空且必须大于0"); |
|||
} |
|||
|
|||
if (request.getUserId() == null || request.getUserId() <= 0) { |
|||
throw PaymentException.paramError("用户ID不能为空且必须大于0"); |
|||
} |
|||
|
|||
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) { |
|||
throw PaymentException.amountError("支付金额必须大于0"); |
|||
} |
|||
|
|||
if (!StringUtils.hasText(request.getSubject())) { |
|||
throw PaymentException.paramError("订单标题不能为空"); |
|||
} |
|||
|
|||
// 检查支付类型是否支持
|
|||
if (!isPaymentTypeSupported(request.getPaymentType())) { |
|||
throw PaymentException.unsupportedPayment("不支持的支付类型: " + request.getPaymentType(), request.getPaymentType()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Object> getPaymentStrategyInfo(PaymentType paymentType) { |
|||
PaymentStrategy strategy = strategyMap.get(paymentType); |
|||
if (strategy == null) { |
|||
return null; |
|||
} |
|||
|
|||
Map<String, Object> info = new HashMap<>(); |
|||
info.put("paymentType", paymentType); |
|||
info.put("strategyName", strategy.getStrategyName()); |
|||
info.put("strategyDescription", strategy.getStrategyDescription()); |
|||
info.put("supportRefund", strategy.supportRefund()); |
|||
info.put("supportQuery", strategy.supportQuery()); |
|||
info.put("supportClose", strategy.supportClose()); |
|||
info.put("needNotify", strategy.needNotify()); |
|||
return info; |
|||
} |
|||
|
|||
@Override |
|||
public List<Map<String, Object>> getAllPaymentStrategyInfo() { |
|||
return strategyMap.keySet().stream() |
|||
.map(this::getPaymentStrategyInfo) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
/** |
|||
* 获取支付策略 |
|||
*/ |
|||
private PaymentStrategy getPaymentStrategy(PaymentType paymentType) throws PaymentException { |
|||
PaymentStrategy strategy = strategyMap.get(paymentType); |
|||
if (strategy == null) { |
|||
throw PaymentException.unsupportedPayment("不支持的支付类型: " + paymentType, paymentType); |
|||
} |
|||
return strategy; |
|||
} |
|||
|
|||
/** |
|||
* 验证查询参数 |
|||
*/ |
|||
private void validateQueryParams(String orderNo, PaymentType paymentType, Integer tenantId) throws PaymentException { |
|||
if (!StringUtils.hasText(orderNo)) { |
|||
throw PaymentException.paramError("订单号不能为空"); |
|||
} |
|||
if (paymentType == null) { |
|||
throw PaymentException.paramError("支付类型不能为空"); |
|||
} |
|||
if (tenantId == null || tenantId <= 0) { |
|||
throw PaymentException.paramError("租户ID不能为空且必须大于0"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证回调参数 |
|||
*/ |
|||
private void validateNotifyParams(PaymentType paymentType, Map<String, String> headers, String body, Integer tenantId) throws PaymentException { |
|||
if (paymentType == null) { |
|||
throw PaymentException.paramError("支付类型不能为空"); |
|||
} |
|||
if (headers == null || headers.isEmpty()) { |
|||
throw PaymentException.paramError("请求头不能为空"); |
|||
} |
|||
if (!StringUtils.hasText(body)) { |
|||
throw PaymentException.paramError("请求体不能为空"); |
|||
} |
|||
if (tenantId == null || tenantId <= 0) { |
|||
throw PaymentException.paramError("租户ID不能为空且必须大于0"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证退款参数 |
|||
*/ |
|||
private void validateRefundParams(String orderNo, String refundNo, PaymentType paymentType, |
|||
BigDecimal totalAmount, BigDecimal refundAmount, Integer tenantId) throws PaymentException { |
|||
if (!StringUtils.hasText(orderNo)) { |
|||
throw PaymentException.paramError("订单号不能为空"); |
|||
} |
|||
if (!StringUtils.hasText(refundNo)) { |
|||
throw PaymentException.paramError("退款单号不能为空"); |
|||
} |
|||
if (paymentType == null) { |
|||
throw PaymentException.paramError("支付类型不能为空"); |
|||
} |
|||
if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) { |
|||
throw PaymentException.amountError("订单总金额必须大于0"); |
|||
} |
|||
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) { |
|||
throw PaymentException.amountError("退款金额必须大于0"); |
|||
} |
|||
if (refundAmount.compareTo(totalAmount) > 0) { |
|||
throw PaymentException.amountError("退款金额不能大于订单总金额"); |
|||
} |
|||
if (tenantId == null || tenantId <= 0) { |
|||
throw PaymentException.paramError("租户ID不能为空且必须大于0"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证退款查询参数 |
|||
*/ |
|||
private void validateRefundQueryParams(String refundNo, PaymentType paymentType, Integer tenantId) throws PaymentException { |
|||
if (!StringUtils.hasText(refundNo)) { |
|||
throw PaymentException.paramError("退款单号不能为空"); |
|||
} |
|||
if (paymentType == null) { |
|||
throw PaymentException.paramError("支付类型不能为空"); |
|||
} |
|||
if (tenantId == null || tenantId <= 0) { |
|||
throw PaymentException.paramError("租户ID不能为空且必须大于0"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证关闭订单参数 |
|||
*/ |
|||
private void validateCloseParams(String orderNo, PaymentType paymentType, Integer tenantId) throws PaymentException { |
|||
if (!StringUtils.hasText(orderNo)) { |
|||
throw PaymentException.paramError("订单号不能为空"); |
|||
} |
|||
if (paymentType == null) { |
|||
throw PaymentException.paramError("支付类型不能为空"); |
|||
} |
|||
if (tenantId == null || tenantId <= 0) { |
|||
throw PaymentException.paramError("租户ID不能为空且必须大于0"); |
|||
} |
|||
} |
|||
} |
@ -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() + "支付策略"; |
|||
} |
|||
} |
@ -0,0 +1,255 @@ |
|||
package com.gxwebsoft.payment.strategy; |
|||
|
|||
import com.gxwebsoft.common.core.utils.CommonUtil; |
|||
import com.gxwebsoft.payment.constants.PaymentConstants; |
|||
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 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 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); |
|||
|
|||
// 获取微信支付配置
|
|||
Config wxPayConfig = wxPayConfigService.getWxPayConfig(request.getTenantId()); |
|||
|
|||
// 构建预支付请求
|
|||
PrepayRequest prepayRequest = buildPrepayRequest(request, orderNo); |
|||
|
|||
// 调用微信支付API
|
|||
PrepayResponse prepayResponse = callWechatPayApi(prepayRequest, wxPayConfig); |
|||
|
|||
// 构建响应
|
|||
PaymentResponse response = PaymentResponse.wechatNative( |
|||
orderNo, prepayResponse.getCodeUrl(), request.getAmount(), request.getTenantId()); |
|||
response.setUserId(request.getUserId()); |
|||
|
|||
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 { |
|||
// TODO: 实现微信支付查询逻辑
|
|||
throw PaymentException.unsupportedPayment("暂不支持微信支付查询", PaymentType.WECHAT_NATIVE); |
|||
} |
|||
|
|||
@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 CommonUtil.createOrderNo(); |
|||
} |
|||
|
|||
/** |
|||
* 构建微信预支付请求 |
|||
*/ |
|||
private PrepayRequest buildPrepayRequest(PaymentRequest request, String orderNo) { |
|||
PrepayRequest prepayRequest = new PrepayRequest(); |
|||
|
|||
// 设置金额
|
|||
Amount amount = new Amount(); |
|||
amount.setTotal(request.getAmountInCents()); |
|||
amount.setCurrency(PaymentConstants.Wechat.CURRENCY); |
|||
prepayRequest.setAmount(amount); |
|||
|
|||
// 设置基本信息
|
|||
prepayRequest.setOutTradeNo(orderNo); |
|||
prepayRequest.setDescription(request.getEffectiveDescription()); |
|||
|
|||
// 设置回调URL
|
|||
if (StringUtils.hasText(request.getNotifyUrl())) { |
|||
prepayRequest.setNotifyUrl(request.getNotifyUrl()); |
|||
} |
|||
|
|||
log.debug("构建微信预支付请求完成, 订单号: {}, 金额: {}分", |
|||
orderNo, request.getAmountInCents()); |
|||
|
|||
return prepayRequest; |
|||
} |
|||
|
|||
/** |
|||
* 调用微信支付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); |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue