diff --git a/.gitignore b/.gitignore index 9097399..0af552c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ yarn-error.log* .vscode/ *.swp *.swo +/java/ diff --git a/java/payment/constants/PaymentConstants.java b/java/payment/constants/PaymentConstants.java new file mode 100644 index 0000000..80f0354 --- /dev/null +++ b/java/payment/constants/PaymentConstants.java @@ -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"); + } +} diff --git a/java/payment/constants/WechatPayType.java b/java/payment/constants/WechatPayType.java new file mode 100644 index 0000000..68b2d84 --- /dev/null +++ b/java/payment/constants/WechatPayType.java @@ -0,0 +1,81 @@ +package com.gxwebsoft.payment.constants; + +/** + * 微信支付类型常量 + * 定义微信支付的具体实现方式 + * + * @author 科技小王子 + * @since 2025-08-30 + */ +public class WechatPayType { + + /** + * JSAPI支付 - 小程序/公众号内支付 + * 需要用户的openid + */ + public static final String JSAPI = "JSAPI"; + + /** + * Native支付 - 扫码支付 + * 生成二维码供用户扫描支付 + */ + public static final String NATIVE = "NATIVE"; + + /** + * H5支付 - 手机网页支付 + * 在手机浏览器中调起微信支付 + */ + public static final String H5 = "H5"; + + /** + * APP支付 - 移动应用支付 + * 在APP中调起微信支付 + */ + public static final String APP = "APP"; + + /** + * 根据openid自动选择微信支付类型 + * + * @param openid 用户openid + * @return JSAPI 或 NATIVE + */ + public static String getAutoType(String openid) { + return (openid != null && !openid.trim().isEmpty()) ? JSAPI : NATIVE; + } + + /** + * 检查是否为有效的微信支付类型 + * + * @param payType 支付类型 + * @return true表示有效 + */ + public static boolean isValidType(String payType) { + return JSAPI.equals(payType) || NATIVE.equals(payType) || + H5.equals(payType) || APP.equals(payType); + } + + /** + * 获取支付类型描述 + * + * @param payType 支付类型 + * @return 描述文本 + */ + public static String getDescription(String payType) { + if (payType == null) { + return "未知支付类型"; + } + + switch (payType) { + case JSAPI: + return "小程序/公众号支付"; + case NATIVE: + return "扫码支付"; + case H5: + return "手机网页支付"; + case APP: + return "移动应用支付"; + default: + return "未知支付类型: " + payType; + } + } +} diff --git a/java/payment/controller/PaymentController.java b/java/payment/controller/PaymentController.java new file mode 100644 index 0000000..0d51551 --- /dev/null +++ b/java/payment/controller/PaymentController.java @@ -0,0 +1,360 @@ +package com.gxwebsoft.payment.controller; + +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.payment.constants.PaymentConstants; +import com.gxwebsoft.payment.dto.PaymentRequest; +import com.gxwebsoft.payment.dto.PaymentResponse; +import com.gxwebsoft.payment.dto.PaymentStatusUpdateRequest; +import com.gxwebsoft.payment.dto.PaymentWithOrderRequest; +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("unifiedPaymentController") +@RequestMapping("/api/payment") +public class PaymentController extends BaseController { + + @Resource(name = "unifiedPaymentServiceImpl") + private PaymentService paymentService; + + @Operation(summary = "创建支付订单", description = "支持微信、支付宝、银联等多种支付方式") + @PostMapping("/create") + public ApiResult createPayment(@Valid @RequestBody PaymentRequest request) { + log.info("收到支付请求: {}", request); + final User loginUser = getLoginUser(); + + if(loginUser == null){ + return fail("请先登录"); + } + + request.setUserId(loginUser.getUserId()); + if(request.getTenantId() == null){ + request.setTenantId(loginUser.getTenantId()); + } + try { + PaymentResponse response = paymentService.createPayment(request); + return this.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("/create-with-order") + public ApiResult createPaymentWithOrder(@Valid @RequestBody PaymentWithOrderRequest request) { + log.info("收到支付与订单创建请求: {}", request); + final User loginUser = getLoginUser(); + + if(loginUser == null){ + return fail("请先登录"); + } + + // 设置用户信息 + if(request.getTenantId() == null){ + request.setTenantId(loginUser.getTenantId()); + } + + try { + PaymentResponse response = paymentService.createPaymentWithOrder(request, loginUser); + return this.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); + + // 参数验证 + if (orderNo == null || orderNo.trim().isEmpty()) { + return fail("订单号不能为空"); + } + if (paymentType == null) { + return fail("支付类型不能为空"); + } + if (tenantId == null || tenantId <= 0) { + return fail("租户ID不能为空且必须为正数"); + } + + try { + PaymentResponse response = paymentService.queryPayment(orderNo, paymentType, tenantId); + return this.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.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.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 paymentTypes = paymentService.getSupportedPaymentTypes(); + return this.>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 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> strategiesInfo = paymentService.getAllPaymentStrategyInfo(); + return this.>>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 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.>success("检查支持情况成功", support); + } catch (Exception e) { + log.error("检查支持情况失败: {}", e.getMessage(), e); + return fail(PaymentConstants.ErrorMessage.SYSTEM_ERROR); + } + } + + @Operation(summary = "手动更新支付状态", description = "用于手动同步支付状态,通常用于异常情况处理") + @PutMapping("/update-status") + public ApiResult updatePaymentStatus(@Valid @RequestBody PaymentStatusUpdateRequest request) { + log.info("收到支付状态更新请求: {}", request); + + try { + // 查询并更新支付状态 + PaymentResponse response = paymentService.queryPayment( + request.getOrderNo(), + PaymentType.WECHAT_NATIVE, + request.getTenantId() + ); + + return this.success("支付状态更新成功", response); + } catch (Exception e) { + log.error("更新支付状态失败: {}", e.getMessage(), e); + return fail("更新支付状态失败: " + e.getMessage()); + } + } + + @Operation(summary = "检查支付配置", description = "检查指定租户的支付配置是否完整") + @GetMapping("/config/check") + public ApiResult checkPaymentConfig( + @Parameter(description = "租户ID", required = true) + @RequestParam @NotNull(message = "租户ID不能为空") @Positive(message = "租户ID必须为正数") Integer tenantId) { + + log.info("检查支付配置,租户ID: {}", tenantId); + + try { + Map configStatus = paymentService.checkPaymentConfig(tenantId); + return this.>success("配置检查完成", configStatus); + } catch (Exception e) { + log.error("检查支付配置失败: {}", e.getMessage(), e); + return fail("检查支付配置失败: " + e.getMessage()); + } + } + + @Operation(summary = "查询用户最近的支付订单", description = "当orderNo缺失时,查询用户最近创建的支付订单") + @GetMapping("/query-recent") + public ApiResult queryRecentPayment( + @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("查询用户最近支付订单: paymentType={}, tenantId={}", paymentType, tenantId); + + final User loginUser = getLoginUser(); + if(loginUser == null){ + return fail("请先登录"); + } + + try { + // 这里需要实现查询用户最近订单的逻辑 + // 可以通过用户ID和租户ID查询最近创建的订单 + return fail("此功能需要实现查询用户最近订单的业务逻辑"); + + } catch (Exception e) { + log.error("查询用户最近支付订单失败: {}", e.getMessage(), e); + return fail("查询失败: " + e.getMessage()); + } + } +} diff --git a/java/payment/controller/PaymentNotifyController.java b/java/payment/controller/PaymentNotifyController.java new file mode 100644 index 0000000..c181514 --- /dev/null +++ b/java/payment/controller/PaymentNotifyController.java @@ -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 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 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 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 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 extractHeaders(HttpServletRequest request) { + Map headers = new HashMap<>(); + + Enumeration 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"; + } + } +} diff --git a/java/payment/dto/PaymentRequest.java b/java/payment/dto/PaymentRequest.java new file mode 100644 index 0000000..241b549 --- /dev/null +++ b/java/payment/dto/PaymentRequest.java @@ -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") + @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 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); + } +} diff --git a/java/payment/dto/PaymentResponse.java b/java/payment/dto/PaymentResponse.java new file mode 100644 index 0000000..dea992f --- /dev/null +++ b/java/payment/dto/PaymentResponse.java @@ -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 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); + } +} diff --git a/java/payment/dto/PaymentStatusUpdateRequest.java b/java/payment/dto/PaymentStatusUpdateRequest.java new file mode 100644 index 0000000..5cee3bc --- /dev/null +++ b/java/payment/dto/PaymentStatusUpdateRequest.java @@ -0,0 +1,41 @@ +package com.gxwebsoft.payment.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; + +/** + * 支付状态更新请求DTO + * 用于手动更新支付状态的请求参数 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Data +@Schema(name = "支付状态更新请求", description = "用于手动更新支付状态") +public class PaymentStatusUpdateRequest { + + @Schema(description = "订单号", required = true, example = "ORDER_1756544921075") + @NotBlank(message = "订单号不能为空") + private String orderNo; + + @Schema(description = "租户ID", required = true, example = "10398") + @NotNull(message = "租户ID不能为空") + @Positive(message = "租户ID必须为正数") + private Integer tenantId; + + @Schema(description = "第三方交易号", example = "4200001234567890123") + private String transactionId; + + @Schema(description = "支付时间", example = "2025-01-26T10:30:00") + private String payTime; + + @Override + public String toString() { + return String.format("PaymentStatusUpdateRequest{orderNo='%s', tenantId=%d, transactionId='%s', payTime='%s'}", + orderNo, tenantId, transactionId, payTime); + } +} diff --git a/java/payment/dto/PaymentWithOrderRequest.java b/java/payment/dto/PaymentWithOrderRequest.java new file mode 100644 index 0000000..06ee407 --- /dev/null +++ b/java/payment/dto/PaymentWithOrderRequest.java @@ -0,0 +1,158 @@ +package com.gxwebsoft.payment.dto; + +import com.gxwebsoft.payment.enums.PaymentType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.*; +import java.math.BigDecimal; +import java.util.List; + +/** + * 支付与订单创建请求DTO + * 用于统一支付模块中的订单创建和支付 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Data +@Schema(name = "PaymentWithOrderRequest", description = "支付与订单创建请求") +public class PaymentWithOrderRequest { + + // ========== 支付相关字段 ========== + + @Schema(description = "支付类型", required = true) + @NotNull(message = "支付类型不能为空") + private PaymentType paymentType; + + @Schema(description = "支付金额", required = true) + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0.01", message = "支付金额必须大于0") + @Digits(integer = 10, fraction = 2, message = "支付金额格式不正确") + private BigDecimal amount; + + @Schema(description = "订单标题", required = true) + @NotBlank(message = "订单标题不能为空") + @Size(max = 60, message = "订单标题长度不能超过60个字符") + private String subject; + + @Schema(description = "订单描述") + @Size(max = 500, message = "订单描述长度不能超过500个字符") + private String description; + + @Schema(description = "租户ID", required = true) + @NotNull(message = "租户ID不能为空") + @Positive(message = "租户ID必须为正数") + private Integer tenantId; + + // ========== 订单相关字段 ========== + + @Schema(description = "订单信息", required = true) + @Valid + @NotNull(message = "订单信息不能为空") + private OrderInfo orderInfo; + + /** + * 订单信息 + */ + @Data + @Schema(name = "OrderInfo", description = "订单信息") + public static class OrderInfo { + + @Schema(description = "订单类型,0商城订单 1预定订单/外卖 2会员卡") + @NotNull(message = "订单类型不能为空") + @Min(value = 0, message = "订单类型值无效") + @Max(value = 2, message = "订单类型值无效") + private Integer type; + + @Schema(description = "收货人姓名") + @Size(max = 50, message = "收货人姓名长度不能超过50个字符") + private String realName; + + @Schema(description = "收货地址") + @Size(max = 200, message = "收货地址长度不能超过200个字符") + private String address; + + @Schema(description = "关联收货地址ID") + private Integer addressId; + + @Schema(description = "快递/自提,0快递 1自提") + private Integer deliveryType; + + @Schema(description = "下单渠道,0小程序预定 1俱乐部训练场 3活动订场") + private Integer channel; + + @Schema(description = "商户ID") + private Long merchantId; + + @Schema(description = "商户名称") + private String merchantName; + + @Schema(description = "使用的优惠券ID") + private Integer couponId; + + @Schema(description = "备注") + @Size(max = 500, message = "备注长度不能超过500字符") + private String comments; + + @Schema(description = "订单商品列表", required = true) + @Valid + @NotEmpty(message = "订单商品列表不能为空") + private List goodsItems; + } + + /** + * 订单商品项 + */ + @Data + @Schema(name = "OrderGoodsItem", description = "订单商品项") + public static class OrderGoodsItem { + + @Schema(description = "商品ID", required = true) + @NotNull(message = "商品ID不能为空") + @Positive(message = "商品ID必须为正数") + private Integer goodsId; + + @Schema(description = "商品SKU ID") + private Integer skuId; + + @Schema(description = "商品数量", required = true) + @NotNull(message = "商品数量不能为空") + @Min(value = 1, message = "商品数量必须大于0") + private Integer quantity; + + @Schema(description = "规格信息,如:颜色:红色|尺寸:L") + private String specInfo; + } + + /** + * 获取格式化的金额字符串 + */ + public String getFormattedAmount() { + if (amount == null) { + return "0.00"; + } + return String.format("%.2f", amount); + } + + /** + * 验证订单商品总金额是否与支付金额一致 + */ + public boolean isAmountConsistent() { + if (amount == null || orderInfo == null || orderInfo.getGoodsItems() == null) { + return false; + } + + // 这里可以添加商品金额计算逻辑 + // 实际实现时需要查询数据库获取商品价格 + return true; + } + + @Override + public String toString() { + return String.format("PaymentWithOrderRequest{paymentType=%s, amount=%s, subject='%s', tenantId=%d, goodsCount=%d}", + paymentType, getFormattedAmount(), subject, tenantId, + orderInfo != null && orderInfo.getGoodsItems() != null ? orderInfo.getGoodsItems().size() : 0); + } +} diff --git a/java/payment/enums/PaymentChannel.java b/java/payment/enums/PaymentChannel.java new file mode 100644 index 0000000..f7396a0 --- /dev/null +++ b/java/payment/enums/PaymentChannel.java @@ -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); + } +} diff --git a/java/payment/enums/PaymentStatus.java b/java/payment/enums/PaymentStatus.java new file mode 100644 index 0000000..809bd9a --- /dev/null +++ b/java/payment/enums/PaymentStatus.java @@ -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); + } +} diff --git a/java/payment/enums/PaymentType.java b/java/payment/enums/PaymentType.java new file mode 100644 index 0000000..946b7ba --- /dev/null +++ b/java/payment/enums/PaymentType.java @@ -0,0 +1,224 @@ +package com.gxwebsoft.payment.enums; + +/** + * 支付类型枚举 + * 定义系统支持的所有支付方式 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +public enum PaymentType { + + /** 余额支付 */ + BALANCE(0, "余额支付", "balance"), + + /** 微信支付(包含JSAPI和Native) */ + WECHAT(1, "微信支付", "wechat"), + + /** 支付宝支付 */ + ALIPAY(2, "支付宝支付", "alipay"), + + /** 银联支付 */ + UNION_PAY(3, "银联支付", "union_pay"), + + /** 现金支付 */ + CASH(4, "现金支付", "cash"), + + /** POS机支付 */ + POS(5, "POS机支付", "pos"), + + /** 免费 */ + FREE(6, "免费", "free"), + + /** 积分支付 */ + POINTS(7, "积分支付", "points"), + + // ========== 已废弃的支付方式(保留用于数据兼容) ========== + + /** @deprecated 微信Native支付 - 已合并到WECHAT */ + @Deprecated + WECHAT_NATIVE(102, "微信Native支付", "wechat_native"), + + /** @deprecated 会员卡支付 - 建议使用余额支付 */ + @Deprecated + MEMBER_CARD_OLD(8, "会员卡支付", "member_card"), + + /** @deprecated VIP月卡 - 建议使用余额支付 */ + @Deprecated + VIP_MONTHLY(9, "VIP月卡", "vip_monthly"), + + /** @deprecated VIP年卡 - 建议使用余额支付 */ + @Deprecated + VIP_YEARLY(10, "VIP年卡", "vip_yearly"), + + /** @deprecated VIP次卡 - 建议使用余额支付 */ + @Deprecated + VIP_COUNT(11, "VIP次卡", "vip_count"), + + /** @deprecated 免费(旧编号) - 已迁移到新编号6 */ + @Deprecated + FREE_OLD(12, "免费", "free"), + + /** @deprecated VIP充值卡 - 建议使用余额支付 */ + @Deprecated + VIP_RECHARGE(13, "VIP充值卡", "vip_recharge"), + + /** @deprecated IC充值卡 - 建议使用余额支付 */ + @Deprecated + IC_RECHARGE(14, "IC充值卡", "ic_recharge"), + + /** @deprecated 积分支付(旧编号) - 已迁移到新编号7 */ + @Deprecated + POINTS_OLD(15, "积分支付", "points"), + + /** @deprecated VIP季卡 - 建议使用余额支付 */ + @Deprecated + VIP_QUARTERLY(16, "VIP季卡", "vip_quarterly"), + + /** @deprecated IC月卡 - 建议使用余额支付 */ + @Deprecated + IC_MONTHLY(17, "IC月卡", "ic_monthly"), + + /** @deprecated IC年卡 - 建议使用余额支付 */ + @Deprecated + IC_YEARLY(18, "IC年卡", "ic_yearly"), + + /** @deprecated IC次卡 - 建议使用余额支付 */ + @Deprecated + IC_COUNT(19, "IC次卡", "ic_count"), + + /** @deprecated IC季卡 - 建议使用余额支付 */ + @Deprecated + IC_QUARTERLY(20, "IC季卡", "ic_quarterly"), + + /** @deprecated 代付 - 建议通过业务逻辑实现 */ + @Deprecated + PROXY_PAY(21, "代付", "proxy_pay"), + + /** @deprecated 支付宝(旧编号) - 已迁移到新编号2 */ + @Deprecated + ALIPAY_OLD(22, "支付宝支付", "alipay"), + + /** @deprecated 银联支付(旧编号) - 已迁移到新编号3 */ + @Deprecated + UNION_PAY_OLD(23, "银联支付", "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; + } + + /** + * 获取微信支付的具体类型 + * @param openid 用户openid + * @return JSAPI 或 NATIVE + */ + public String getWechatPayType(String openid) { + if (!isWechatPay()) { + return null; + } + + // 有openid使用JSAPI,无openid使用Native + return (openid != null && !openid.trim().isEmpty()) ? "JSAPI" : "NATIVE"; + } + + /** + * 是否为第三方支付 + */ + public boolean isThirdPartyPay() { + return isWechatPay() || this == ALIPAY || this == UNION_PAY; + } + + /** + * 是否需要在线支付 + */ + public boolean isOnlinePay() { + return isThirdPartyPay(); + } + + /** + * 是否为卡类支付(已废弃的支付方式) + * @deprecated 卡类支付已废弃,建议使用余额支付 + */ + @Deprecated + public boolean isCardPay() { + return this == MEMBER_CARD_OLD || + 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; + } + + /** + * 是否为推荐使用的核心支付方式 + */ + public boolean isCorePaymentType() { + return this == BALANCE || this == WECHAT || this == ALIPAY || this == UNION_PAY || + this == CASH || this == POS || this == FREE || this == POINTS; + } + + /** + * 是否为已废弃的支付方式 + */ + public boolean isDeprecated() { + return !isCorePaymentType(); + } + + @Override + public String toString() { + return String.format("PaymentType{code=%d, name='%s', channel='%s'}", code, name, channel); + } +} diff --git a/java/payment/exception/PaymentException.java b/java/payment/exception/PaymentException.java new file mode 100644 index 0000000..35d2ac3 --- /dev/null +++ b/java/payment/exception/PaymentException.java @@ -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); + } +} diff --git a/java/payment/exception/PaymentExceptionHandler.java b/java/payment/exception/PaymentExceptionHandler.java new file mode 100644 index 0000000..04c6cda --- /dev/null +++ b/java/payment/exception/PaymentExceptionHandler.java @@ -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 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 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> 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); + } +} diff --git a/java/payment/service/PaymentService.java b/java/payment/service/PaymentService.java new file mode 100644 index 0000000..0f90fd5 --- /dev/null +++ b/java/payment/service/PaymentService.java @@ -0,0 +1,182 @@ +package com.gxwebsoft.payment.service; + +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.payment.dto.PaymentRequest; +import com.gxwebsoft.payment.dto.PaymentResponse; +import com.gxwebsoft.payment.dto.PaymentWithOrderRequest; +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 request 支付与订单创建请求 + * @param loginUser 当前登录用户 + * @return 支付响应 + * @throws PaymentException 创建失败时抛出 + */ + PaymentResponse createPaymentWithOrder(PaymentWithOrderRequest request, User loginUser) 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 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 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 getPaymentStrategyInfo(PaymentType paymentType); + + /** + * 获取所有支付策略信息 + * + * @return 所有策略信息列表 + */ + List> getAllPaymentStrategyInfo(); + + /** + * 检查支付配置 + * + * @param tenantId 租户ID + * @return 配置检查结果 + */ + Map checkPaymentConfig(Integer tenantId); +} diff --git a/java/payment/service/WxPayConfigService.java b/java/payment/service/WxPayConfigService.java new file mode 100644 index 0000000..f072786 --- /dev/null +++ b/java/payment/service/WxPayConfigService.java @@ -0,0 +1,338 @@ +package com.gxwebsoft.payment.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +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.common.system.param.PaymentParam; +import com.gxwebsoft.common.system.service.PaymentService; +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; +import java.util.concurrent.TimeUnit; + +/** + * 微信支付配置服务 + * 负责管理微信支付的配置信息和证书 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Slf4j +@Service +public class WxPayConfigService { + + @Resource + private RedisUtil redisUtil; + + @Resource + private CertificateService certificateService; + + @Resource + private CertificateProperties certificateProperties; + + @Resource + private PaymentService paymentService; + + @Value("${spring.profiles.active:dev}") + private String activeProfile; + + /** + * 获取支付配置信息(Payment对象)- 公开方法 + * + * @param tenantId 租户ID + * @return 支付配置信息 + * @throws PaymentException 配置获取失败时抛出 + */ + public Payment getPaymentConfigForStrategy(Integer tenantId) throws PaymentException { + if (tenantId == null) { + throw PaymentException.paramError("租户ID不能为空"); + } + return getPaymentConfig(tenantId); + } + + /** + * 获取微信支付配置 + * + * @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); + System.out.println("payment = " + payment); + if (payment != null) { + log.debug("从缓存获取支付配置成功,租户ID: {}", tenantId); +// return payment; + } + + // 缓存中没有,尝试从数据库查询 + try { + final PaymentParam paymentParam = new PaymentParam(); + paymentParam.setType(102); + paymentParam.setTenantId(tenantId); + + log.debug("查询数据库支付配置,参数: type=102, tenantId={}", tenantId); + payment = paymentService.getByType(paymentParam); + log.debug("数据库查询结果: {}", payment != null ? "找到配置" : "未找到配置"); + + if (payment != null) { + log.info("从数据库获取支付配置成功,租户ID: {},将缓存配置", tenantId); + // 将查询到的配置缓存起来,缓存1天 + redisUtil.set(cacheKey, payment, 1L, TimeUnit.DAYS); + return payment; + } else { + log.warn("数据库中未找到支付配置,租户ID: {}, type: 102", tenantId); + } + } catch (Exception e) { + log.error("从数据库查询支付配置失败,租户ID: {},错误: {}", tenantId, e.getMessage(), e); + // 抛出更详细的异常信息 + throw PaymentException.systemError("查询支付配置失败,租户ID: " + tenantId + ",错误: " + e.getMessage(), e); + } + + // 数据库也没有配置 + if (!"dev".equals(activeProfile)) { + throw PaymentException.systemError("微信支付配置未找到,租户ID: " + tenantId + ",请检查数据库配置", null); + } + + log.debug("开发环境模式,将使用测试配置,租户ID: {}", tenantId); + // 开发环境返回测试Payment配置 + return createDevTestPayment(tenantId); + } + + /** + * 获取证书文件路径 + */ + 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 { + // 根据租户ID构建证书路径 + String certPath = "dev/wechat/" + tenantId + "/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); + } + } + + /** + * 创建开发环境测试Payment配置 + */ + private Payment createDevTestPayment(Integer tenantId) { + Payment testPayment = new Payment(); + testPayment.setTenantId(tenantId); + testPayment.setType(102); // Native支付 + testPayment.setAppId("wxa67c676fc445590e"); // 开发环境测试AppID + testPayment.setMchId("1246610101"); // 开发环境测试商户号 + testPayment.setMerchantSerialNumber("48749613B40AA8F1D768583FC352358E13EB5AF0"); + testPayment.setApiKey(certificateProperties.getWechatPay().getDev().getApiV3Key()); + testPayment.setNotifyUrl("http://frps-10550.s209.websoft.top/api/payment/notify"); + testPayment.setName("微信Native支付-开发环境"); + testPayment.setStatus(true); // 启用 + + log.info("创建开发环境测试Payment配置,租户ID: {}, AppID: {}, 商户号: {}", + tenantId, testPayment.getAppId(), testPayment.getMchId()); + + return testPayment; + } + + /** + * 创建开发环境测试配置 + */ + private Config createDevTestConfig(String certificatePath) throws PaymentException { + String testMerchantId = "1246610101"; + String testMerchantSerialNumber = "48749613B40AA8F1D768583FC352358E13EB5AF0"; + 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 { + // 验证配置完整性 + validatePaymentConfig(payment); + + log.info("使用数据库支付配置"); + log.debug("商户号: {}", payment.getMchId()); + + return new RSAAutoCertificateConfig.Builder() + .merchantId(payment.getMchId()) + .privateKeyFromPath(certificatePath) + .merchantSerialNumber(payment.getMerchantSerialNumber()) + .apiV3Key(payment.getApiKey()) + .build(); + } + + /** + * 验证支付配置完整性 + */ + private void validatePaymentConfig(Payment payment) throws PaymentException { + if (payment == null) { + throw PaymentException.systemError("支付配置为空", null); + } + + if (payment.getMchId() == null || payment.getMchId().trim().isEmpty()) { + throw PaymentException.systemError("商户号(mchId)未配置", null); + } + + if (payment.getMerchantSerialNumber() == null || payment.getMerchantSerialNumber().trim().isEmpty()) { + throw PaymentException.systemError("商户证书序列号(merchantSerialNumber)未配置", null); + } + + if (payment.getApiKey() == null || payment.getApiKey().trim().isEmpty()) { + throw PaymentException.systemError("APIv3密钥(apiKey)未配置", null); + } + + if (payment.getApiclientKey() == null || payment.getApiclientKey().trim().isEmpty()) { + throw PaymentException.systemError("证书文件名(apiclientKey)未配置", null); + } + + log.debug("支付配置验证通过,租户ID: {}, 商户号: {}", payment.getTenantId(), payment.getMchId()); + } + + /** + * 清除指定租户的配置缓存 + * + * @param tenantId 租户ID + */ + public void clearConfigCache(Integer tenantId) { + WxNativeUtil.addConfig(tenantId, null); + log.info("清除微信支付配置缓存,租户ID: {}", tenantId); + } +} diff --git a/java/payment/service/WxPayNotifyService.java b/java/payment/service/WxPayNotifyService.java new file mode 100644 index 0000000..9378dcd --- /dev/null +++ b/java/payment/service/WxPayNotifyService.java @@ -0,0 +1,366 @@ +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 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 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 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 { + log.info("开始推送支付成功通知, 订单号: {}, 交易号: {}, 用户ID: {}", + order.getOrderNo(), transaction.getTransactionId(), order.getUserId()); + + // 1. 记录支付成功日志 + logPaymentSuccess(order, transaction); + + // 2. 发送支付成功通知(可扩展) + sendPaymentSuccessNotification(order, transaction); + + // 3. 触发其他业务逻辑(可扩展) + triggerPostPaymentActions(order, transaction); + + log.info("支付结果通知推送完成, 订单号: {}, 交易号: {}", + order.getOrderNo(), transaction.getTransactionId()); + } catch (Exception e) { + log.warn("支付结果通知推送失败, 订单号: {}, 错误: {}", order.getOrderNo(), e.getMessage()); + // 推送失败不影响主流程,只记录日志 + } + } + + /** + * 记录支付成功日志 + */ + private void logPaymentSuccess(ShopOrder order, Transaction transaction) { + try { + log.info("=== 支付成功详细信息 ==="); + log.info("订单号: {}", order.getOrderNo()); + log.info("微信交易号: {}", transaction.getTransactionId()); + log.info("支付金额: {}元", order.getPayPrice()); + log.info("支付时间: {}", transaction.getSuccessTime()); + log.info("用户ID: {}", order.getUserId()); + log.info("租户ID: {}", order.getTenantId()); + log.info("订单标题: {}", order.getTitle()); + log.info("========================"); + } catch (Exception e) { + log.warn("记录支付成功日志失败: {}", e.getMessage()); + } + } + + /** + * 发送支付成功通知 + */ + private void sendPaymentSuccessNotification(ShopOrder order, Transaction transaction) { + try { + // TODO: 实现具体的通知逻辑 + // 1. 发送邮件通知 + // 2. 发送短信通知 + // 3. 站内消息通知 + // 4. 微信模板消息通知 + + log.debug("支付成功通知发送完成, 订单号: {}", order.getOrderNo()); + } catch (Exception e) { + log.warn("发送支付成功通知失败, 订单号: {}, 错误: {}", order.getOrderNo(), e.getMessage()); + } + } + + /** + * 触发支付成功后的其他业务逻辑 + */ + private void triggerPostPaymentActions(ShopOrder order, Transaction transaction) { + try { + // TODO: 根据业务需求实现 + // 1. 开通网站服务 + // 2. 激活会员权益 + // 3. 发放积分奖励 + // 4. 触发营销活动 + + log.debug("支付后业务逻辑触发完成, 订单号: {}", order.getOrderNo()); + } catch (Exception e) { + log.warn("触发支付后业务逻辑失败, 订单号: {}, 错误: {}", order.getOrderNo(), e.getMessage()); + } + } +} diff --git a/java/payment/service/impl/PaymentServiceImpl.java b/java/payment/service/impl/PaymentServiceImpl.java new file mode 100644 index 0000000..cfc4b27 --- /dev/null +++ b/java/payment/service/impl/PaymentServiceImpl.java @@ -0,0 +1,668 @@ +package com.gxwebsoft.payment.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.gxwebsoft.common.core.utils.CommonUtil; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.payment.constants.PaymentConstants; +import com.gxwebsoft.payment.dto.PaymentRequest; +import com.gxwebsoft.payment.dto.PaymentResponse; +import com.gxwebsoft.payment.dto.PaymentWithOrderRequest; +import com.gxwebsoft.payment.enums.PaymentType; +import com.gxwebsoft.payment.exception.PaymentException; +import com.gxwebsoft.payment.service.PaymentService; +import com.gxwebsoft.payment.service.WxPayConfigService; +import com.gxwebsoft.payment.strategy.PaymentStrategy; +import com.gxwebsoft.shop.dto.OrderCreateRequest; +import com.gxwebsoft.shop.service.OrderBusinessService; +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("unifiedPaymentServiceImpl") +public class PaymentServiceImpl implements PaymentService { + + /** + * 支付策略映射表 + */ + private final Map strategyMap = new ConcurrentHashMap<>(); + + /** + * 注入所有支付策略实现 + */ + @Resource + private List paymentStrategies; + + /** + * 订单业务服务 + */ + @Resource + private OrderBusinessService orderBusinessService; + + /** + * 微信支付配置服务 + */ + @Resource + private WxPayConfigService wxPayConfigService; + + /** + * 初始化策略映射 + */ + @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 createPaymentWithOrder(PaymentWithOrderRequest request, User loginUser) throws PaymentException { + log.info("开始创建支付订单(包含订单信息), 支付类型: {}, 租户ID: {}, 用户ID: {}, 金额: {}", + request.getPaymentType(), request.getTenantId(), loginUser.getUserId(), request.getFormattedAmount()); + + try { + // 1. 参数验证 + validatePaymentWithOrderRequest(request, loginUser); + + // 2. 转换为订单创建请求 + OrderCreateRequest orderRequest = convertToOrderCreateRequest(request, loginUser); + + // 3. 创建订单(包含商品验证、库存扣减等完整业务逻辑) + Map wxOrderInfo = orderBusinessService.createOrder(orderRequest, loginUser); + + // 4. 构建支付响应(复用现有的微信支付返回格式) + PaymentResponse response = buildPaymentResponseFromWxOrder(wxOrderInfo, request, orderRequest.getOrderNo()); + + log.info("支付订单创建成功(包含订单信息), 支付类型: {}, 租户ID: {}, 订单号: {}, 金额: {}", + request.getPaymentType(), request.getTenantId(), response.getOrderNo(), request.getFormattedAmount()); + + return response; + + } catch (PaymentException e) { + log.error("创建支付订单失败(包含订单信息), 支付类型: {}, 租户ID: {}, 错误: {}", + request.getPaymentType(), request.getTenantId(), e.getMessage()); + throw e; + } catch (Exception e) { + log.error("创建支付订单系统错误(包含订单信息), 支付类型: {}, 租户ID: {}, 系统错误: {}", + 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 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 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 getPaymentStrategyInfo(PaymentType paymentType) { + PaymentStrategy strategy = strategyMap.get(paymentType); + if (strategy == null) { + return null; + } + + Map 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> getAllPaymentStrategyInfo() { + return strategyMap.keySet().stream() + .map(this::getPaymentStrategyInfo) + .collect(Collectors.toList()); + } + + @Override + public Map checkPaymentConfig(Integer tenantId) { + Map result = new HashMap<>(); + result.put("tenantId", tenantId); + + try { + // 检查微信支付配置 + wxPayConfigService.getPaymentConfigForStrategy(tenantId); + result.put("wechatConfigExists", true); + result.put("wechatConfigError", null); + } catch (Exception e) { + result.put("wechatConfigExists", false); + result.put("wechatConfigError", e.getMessage()); + } + + try { + // 检查微信支付Config构建 + wxPayConfigService.getWxPayConfig(tenantId); + result.put("wechatConfigValid", true); + result.put("wechatConfigValidError", null); + } catch (Exception e) { + result.put("wechatConfigValid", false); + result.put("wechatConfigValidError", e.getMessage()); + } + + return result; + } + + /** + * 获取支付策略 + */ + 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 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"); + } + } + + /** + * 验证支付与订单创建请求参数 + */ + private void validatePaymentWithOrderRequest(PaymentWithOrderRequest request, User loginUser) throws PaymentException { + if (request == null) { + throw PaymentException.paramError("请求参数不能为空"); + } + if (loginUser == null) { + throw PaymentException.paramError("用户未登录"); + } + if (request.getPaymentType() == null) { + throw PaymentException.paramError("支付类型不能为空"); + } + if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) { + throw PaymentException.amountError("支付金额必须大于0"); + } + if (!StringUtils.hasText(request.getSubject())) { + throw PaymentException.paramError("订单标题不能为空"); + } + if (request.getTenantId() == null || request.getTenantId() <= 0) { + throw PaymentException.paramError("租户ID不能为空且必须大于0"); + } + if (request.getOrderInfo() == null) { + throw PaymentException.paramError("订单信息不能为空"); + } + if (request.getOrderInfo().getGoodsItems() == null || request.getOrderInfo().getGoodsItems().isEmpty()) { + throw PaymentException.paramError("订单商品列表不能为空"); + } + } + + /** + * 转换为订单创建请求 + */ + private OrderCreateRequest convertToOrderCreateRequest(PaymentWithOrderRequest request, User loginUser) { + OrderCreateRequest orderRequest = new OrderCreateRequest(); + + // 生成订单号(使用雪花算法保证全局唯一) + String orderNo = Long.toString(IdUtil.getSnowflakeNextId()); + orderRequest.setOrderNo(orderNo); + log.info("为订单创建请求生成订单号(雪花算法): {}", orderNo); + + // 设置基本信息 + orderRequest.setType(request.getOrderInfo().getType()); + orderRequest.setTitle(request.getSubject()); + orderRequest.setComments(request.getOrderInfo().getComments()); + orderRequest.setTenantId(request.getTenantId()); + + // 设置收货信息 + orderRequest.setRealName(request.getOrderInfo().getRealName()); + orderRequest.setAddress(request.getOrderInfo().getAddress()); + orderRequest.setAddressId(request.getOrderInfo().getAddressId()); + orderRequest.setDeliveryType(request.getOrderInfo().getDeliveryType()); + + // 设置商户信息 + orderRequest.setMerchantId(request.getOrderInfo().getMerchantId()); + orderRequest.setMerchantName(request.getOrderInfo().getMerchantName()); + + // 设置支付信息 + orderRequest.setPayType(request.getPaymentType().getCode()); + orderRequest.setTotalPrice(request.getAmount()); + orderRequest.setPayPrice(request.getAmount()); + + // 设置优惠券 + orderRequest.setCouponId(request.getOrderInfo().getCouponId()); + + // 转换商品列表 + List goodsItems = request.getOrderInfo().getGoodsItems().stream() + .map(this::convertToOrderGoodsItem) + .collect(java.util.stream.Collectors.toList()); + orderRequest.setGoodsItems(goodsItems); + + return orderRequest; + } + + /** + * 转换商品项 + */ + private OrderCreateRequest.OrderGoodsItem convertToOrderGoodsItem(PaymentWithOrderRequest.OrderGoodsItem item) { + OrderCreateRequest.OrderGoodsItem orderItem = new OrderCreateRequest.OrderGoodsItem(); + orderItem.setGoodsId(item.getGoodsId()); + orderItem.setSkuId(item.getSkuId()); + orderItem.setQuantity(item.getQuantity()); + orderItem.setSpecInfo(item.getSpecInfo()); + return orderItem; + } + + /** + * 从微信订单信息构建支付响应 + */ + private PaymentResponse buildPaymentResponseFromWxOrder(Map wxOrderInfo, + PaymentWithOrderRequest request, + String orderNo) { + PaymentResponse response = PaymentResponse.wechatNative( + orderNo, + wxOrderInfo.get("codeUrl"), + request.getAmount(), + request.getTenantId() + ); + + // 设置额外信息 + response.setSuccess(true); + // 确保orderNo被正确设置 + response.setOrderNo(orderNo); + + // 调试日志 + log.info("构建支付响应成功, 订单号: {}, 二维码URL: {}, 响应中的orderNo: {}", + orderNo, wxOrderInfo.get("codeUrl"), response.getOrderNo()); + + return response; + } +} diff --git a/java/payment/strategy/PaymentStrategy.java b/java/payment/strategy/PaymentStrategy.java new file mode 100644 index 0000000..560c365 --- /dev/null +++ b/java/payment/strategy/PaymentStrategy.java @@ -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 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() + "支付策略"; + } +} diff --git a/java/payment/strategy/WechatNativeStrategy.java b/java/payment/strategy/WechatNativeStrategy.java new file mode 100644 index 0000000..6c43eaa --- /dev/null +++ b/java/payment/strategy/WechatNativeStrategy.java @@ -0,0 +1,401 @@ +package com.gxwebsoft.payment.strategy; + +import cn.hutool.core.util.IdUtil; +import com.gxwebsoft.common.core.utils.CommonUtil; +import com.gxwebsoft.common.system.entity.Payment; +import com.gxwebsoft.payment.constants.PaymentConstants; +import com.gxwebsoft.payment.dto.PaymentRequest; +import com.gxwebsoft.payment.dto.PaymentResponse; +import com.gxwebsoft.payment.enums.PaymentStatus; +import com.gxwebsoft.payment.enums.PaymentType; +import com.gxwebsoft.payment.exception.PaymentException; +import com.gxwebsoft.payment.service.WxPayConfigService; +import com.gxwebsoft.payment.service.WxPayNotifyService; +import com.wechat.pay.java.core.Config; +import com.wechat.pay.java.service.payments.nativepay.NativePayService; +import com.wechat.pay.java.service.payments.nativepay.model.Amount; +import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest; +import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse; +import com.wechat.pay.java.service.payments.nativepay.model.QueryOrderByOutTradeNoRequest; +import com.wechat.pay.java.service.payments.model.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.Map; + +/** + * 微信Native支付策略实现 + * 处理微信Native扫码支付 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Slf4j +@Component +public class WechatNativeStrategy implements PaymentStrategy { + + @Resource + private WxPayConfigService wxPayConfigService; + + @Resource + private WxPayNotifyService wxPayNotifyService; + + @Override + public PaymentType getSupportedPaymentType() { + return PaymentType.WECHAT_NATIVE; + } + + @Override + public void validateRequest(PaymentRequest request) throws PaymentException { + if (request == null) { + throw PaymentException.paramError("支付请求不能为空"); + } + + if (request.getTenantId() == null) { + throw PaymentException.paramError("租户ID不能为空"); + } + + if (request.getUserId() == null) { + throw PaymentException.paramError("用户ID不能为空"); + } + + if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) { + throw PaymentException.amountError("支付金额必须大于0"); + } + + if (!StringUtils.hasText(request.getSubject())) { + throw PaymentException.paramError("订单标题不能为空"); + } + + // 验证金额范围 + if (request.getAmount().compareTo(new BigDecimal("0.01")) < 0) { + throw PaymentException.amountError("支付金额不能小于0.01元"); + } + + if (request.getAmount().compareTo(new BigDecimal("999999.99")) > 0) { + throw PaymentException.amountError("支付金额不能超过999999.99元"); + } + + // 验证订单标题长度 + if (request.getSubject().length() > PaymentConstants.Config.DESCRIPTION_MAX_LENGTH) { + throw PaymentException.paramError("订单标题长度不能超过" + PaymentConstants.Config.DESCRIPTION_MAX_LENGTH + "个字符"); + } + + log.debug("微信Native支付请求参数验证通过, 租户ID: {}, 金额: {}", + request.getTenantId(), request.getFormattedAmount()); + } + + @Override + public PaymentResponse createPayment(PaymentRequest request) throws PaymentException { + log.info("{}, 支付类型: {}, 租户ID: {}, 金额: {}", + PaymentConstants.LogMessage.PAYMENT_START, getSupportedPaymentType(), + request.getTenantId(), request.getFormattedAmount()); + + try { + // 验证请求参数 + validateRequest(request); + + // 生成订单号 + String orderNo = generateOrderNo(request); + log.info("生成的订单号: {}", orderNo); + + // 获取Native支付的Payment配置(包含appId等信息) + Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(request.getTenantId()); + + // 获取微信支付配置 + Config wxPayConfig = wxPayConfigService.getWxPayConfig(request.getTenantId()); + + // 构建预支付请求 + PrepayRequest prepayRequest = buildPrepayRequest(request, orderNo, paymentConfig); + + // 调用微信支付API + PrepayResponse prepayResponse = callWechatPayApi(prepayRequest, wxPayConfig); + + // 构建响应 + PaymentResponse response = PaymentResponse.wechatNative( + orderNo, prepayResponse.getCodeUrl(), request.getAmount(), request.getTenantId()); + response.setUserId(request.getUserId()); + + // 确保orderNo被正确设置 + response.setOrderNo(orderNo); + + // 调试日志:检查响应对象的orderNo + log.info("构建的响应对象 - orderNo: {}, codeUrl: {}, success: {}", + response.getOrderNo(), response.getCodeUrl(), response.getSuccess()); + + log.info("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 金额: {}", + PaymentConstants.LogMessage.PAYMENT_SUCCESS, getSupportedPaymentType(), + request.getTenantId(), orderNo, request.getFormattedAmount()); + + return response; + + } catch (PaymentException e) { + log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}", + PaymentConstants.LogMessage.PAYMENT_FAILED, getSupportedPaymentType(), + request.getTenantId(), e.getMessage()); + throw e; + } catch (Exception e) { + log.error("{}, 支付类型: {}, 租户ID: {}, 系统错误: {}", + PaymentConstants.LogMessage.PAYMENT_FAILED, getSupportedPaymentType(), + request.getTenantId(), e.getMessage(), e); + throw PaymentException.systemError("微信Native支付创建失败: " + e.getMessage(), e); + } + } + + @Override + public PaymentResponse queryPayment(String orderNo, Integer tenantId) throws PaymentException { + log.info("开始查询微信Native支付状态, 订单号: {}, 租户ID: {}", orderNo, tenantId); + + try { + // 参数验证 + if (!StringUtils.hasText(orderNo)) { + throw PaymentException.paramError("订单号不能为空"); + } + if (tenantId == null) { + throw PaymentException.paramError("租户ID不能为空"); + } + + // 获取支付配置(包含商户号等信息) + Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(tenantId); + + // 获取微信支付配置 + Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId); + + // 调用微信支付查询API + return queryWechatPaymentStatus(orderNo, tenantId, paymentConfig, wxPayConfig); + + } catch (Exception e) { + if (e instanceof PaymentException) { + throw e; + } + log.error("查询微信Native支付状态失败, 订单号: {}, 租户ID: {}, 错误: {}", + orderNo, tenantId, e.getMessage(), e); + throw PaymentException.systemError("查询微信支付状态失败: " + e.getMessage(), e); + } + } + + @Override + public String handleNotify(Map headers, String body, Integer tenantId) throws PaymentException { + log.info("{}, 支付类型: {}, 租户ID: {}", + PaymentConstants.LogMessage.NOTIFY_START, getSupportedPaymentType(), tenantId); + + try { + // 委托给专门的回调处理服务 + return wxPayNotifyService.handlePaymentNotify(headers, body, tenantId); + } catch (Exception e) { + log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}", + PaymentConstants.LogMessage.NOTIFY_FAILED, getSupportedPaymentType(), + tenantId, e.getMessage(), e); + throw PaymentException.systemError("微信支付回调处理失败: " + e.getMessage(), e); + } + } + + @Override + public PaymentResponse refund(String orderNo, String refundNo, BigDecimal totalAmount, + BigDecimal refundAmount, String reason, Integer tenantId) throws PaymentException { + // TODO: 实现微信支付退款逻辑 + throw PaymentException.unsupportedPayment("暂不支持微信支付退款", PaymentType.WECHAT_NATIVE); + } + + @Override + public PaymentResponse queryRefund(String refundNo, Integer tenantId) throws PaymentException { + // TODO: 实现微信退款查询逻辑 + throw PaymentException.unsupportedPayment("暂不支持微信退款查询", PaymentType.WECHAT_NATIVE); + } + + @Override + public boolean closeOrder(String orderNo, Integer tenantId) throws PaymentException { + // TODO: 实现微信订单关闭逻辑 + throw PaymentException.unsupportedPayment("暂不支持微信订单关闭", PaymentType.WECHAT_NATIVE); + } + + @Override + public boolean supportRefund() { + return true; + } + + @Override + public boolean supportQuery() { + return true; + } + + @Override + public boolean supportClose() { + return true; + } + + @Override + public boolean needNotify() { + return true; + } + + /** + * 生成订单号(使用雪花算法保证全局唯一) + */ + private String generateOrderNo(PaymentRequest request) { + if (StringUtils.hasText(request.getOrderNo())) { + return request.getOrderNo(); + } + return Long.toString(IdUtil.getSnowflakeNextId()); + } + + + + /** + * 构建微信预支付请求 + */ + private PrepayRequest buildPrepayRequest(PaymentRequest request, String orderNo, Payment paymentConfig) { + PrepayRequest prepayRequest = new PrepayRequest(); + + // 设置应用ID和商户号(关键修复) + prepayRequest.setAppid(paymentConfig.getAppId()); + prepayRequest.setMchid(paymentConfig.getMchId()); + + // 设置金额 + Amount amount = new Amount(); + amount.setTotal(request.getAmountInCents()); + amount.setCurrency(PaymentConstants.Wechat.CURRENCY); + prepayRequest.setAmount(amount); + + // 设置基本信息 + prepayRequest.setOutTradeNo(orderNo); + prepayRequest.setDescription(request.getEffectiveDescription()); + + log.info("创建微信支付订单 - 订单号: {}, 商户号: {}, 金额: {}分", + orderNo, paymentConfig.getMchId(), request.getAmountInCents()); + + // 设置回调URL(必填字段) + String notifyUrl = null; + if (StringUtils.hasText(request.getNotifyUrl())) { + // 优先使用请求中的回调URL + notifyUrl = request.getNotifyUrl(); + } else if (StringUtils.hasText(paymentConfig.getNotifyUrl())) { + // 使用配置中的回调URL + notifyUrl = paymentConfig.getNotifyUrl(); + } else { + // 如果都没有,抛出异常 + throw new RuntimeException("回调通知地址不能为空,请在支付请求中设置notifyUrl或在支付配置中设置notifyUrl"); + } + prepayRequest.setNotifyUrl(notifyUrl); + + log.debug("构建微信预支付请求完成, 订单号: {}, 金额: {}分, AppID: {}, 商户号: {}, 回调URL: {}", + orderNo, request.getAmountInCents(), paymentConfig.getAppId(), paymentConfig.getMchId(), notifyUrl); + + return prepayRequest; + } + + /** + * 查询微信支付状态 + */ + private PaymentResponse queryWechatPaymentStatus(String orderNo, Integer tenantId, Payment paymentConfig, Config wxPayConfig) throws PaymentException { + try { + log.info("开始查询微信支付状态 - 订单号: {}, 商户号: {}, 租户ID: {}", + orderNo, paymentConfig.getMchId(), tenantId); + + // 构建查询请求 + QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest(); + queryRequest.setOutTradeNo(orderNo); + queryRequest.setMchid(paymentConfig.getMchId()); + + // 构建服务 + NativePayService service = new NativePayService.Builder().config(wxPayConfig).build(); + + // 调用查询接口 + Transaction transaction = service.queryOrderByOutTradeNo(queryRequest); + + if (transaction == null) { + throw PaymentException.systemError("微信支付查询返回空结果", null); + } + + // 转换支付状态 + PaymentStatus paymentStatus = convertWechatPaymentStatus(transaction.getTradeState()); + + // 构建响应 + PaymentResponse response = new PaymentResponse(); + response.setSuccess(true); + response.setOrderNo(orderNo); + response.setPaymentStatus(paymentStatus); + response.setTenantId(tenantId); + response.setPaymentType(PaymentType.WECHAT_NATIVE); + + if (transaction.getAmount() != null) { + // 微信返回的金额是分,需要转换为元 + BigDecimal amount = new BigDecimal(transaction.getAmount().getTotal()).divide(new BigDecimal("100")); + response.setAmount(amount); + } + + if (transaction.getTransactionId() != null) { + response.setTransactionId(transaction.getTransactionId()); + } + + log.info("微信Native支付状态查询成功, 订单号: {}, 状态: {}, 微信交易号: {}", + orderNo, paymentStatus, transaction.getTransactionId()); + + return response; + + } catch (Exception e) { + if (e instanceof PaymentException) { + throw e; + } + log.error("查询微信支付状态失败, 订单号: {}, 错误: {}", orderNo, e.getMessage(), e); + throw PaymentException.networkError("查询微信支付状态失败: " + e.getMessage(), PaymentType.WECHAT_NATIVE, e); + } + } + + /** + * 转换微信支付状态 + */ + private PaymentStatus convertWechatPaymentStatus(Transaction.TradeStateEnum tradeState) { + if (tradeState == null) { + return PaymentStatus.PENDING; + } + + switch (tradeState) { + case SUCCESS: + return PaymentStatus.SUCCESS; + case REFUND: + return PaymentStatus.REFUNDED; + case NOTPAY: + return PaymentStatus.PENDING; + case CLOSED: + return PaymentStatus.CANCELLED; + case REVOKED: + return PaymentStatus.CANCELLED; + case USERPAYING: + return PaymentStatus.PROCESSING; + case PAYERROR: + return PaymentStatus.FAILED; + default: + return PaymentStatus.PENDING; + } + } + + /** + * 调用微信支付API + */ + private PrepayResponse callWechatPayApi(PrepayRequest request, Config wxPayConfig) throws PaymentException { + try { + // 构建服务 + NativePayService service = new NativePayService.Builder().config(wxPayConfig).build(); + + // 调用预支付接口 + PrepayResponse response = service.prepay(request); + + if (response == null || !StringUtils.hasText(response.getCodeUrl())) { + throw PaymentException.networkError("微信支付API返回数据异常", PaymentType.WECHAT_NATIVE, null); + } + + log.debug("微信支付API调用成功, 订单号: {}", request.getOutTradeNo()); + return response; + + } catch (Exception e) { + if (e instanceof PaymentException) { + throw e; + } + throw PaymentException.networkError("调用微信支付API失败: " + e.getMessage(), PaymentType.WECHAT_NATIVE, e); + } + } +} diff --git a/java/payment/utils/PaymentTypeCompatibilityUtil.java b/java/payment/utils/PaymentTypeCompatibilityUtil.java new file mode 100644 index 0000000..682bfef --- /dev/null +++ b/java/payment/utils/PaymentTypeCompatibilityUtil.java @@ -0,0 +1,165 @@ +package com.gxwebsoft.payment.utils; + +import com.gxwebsoft.payment.enums.PaymentType; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * 支付方式兼容性处理工具类 + * 处理废弃支付方式到核心支付方式的映射转换 + * + * @author 科技小王子 + * @since 2025-08-30 + */ +@Slf4j +public class PaymentTypeCompatibilityUtil { + + /** + * 废弃支付方式到核心支付方式的映射表 + */ + private static final Map DEPRECATED_TO_CORE_MAPPING = new HashMap<>(); + + static { + // 旧编号到新编号的映射 + DEPRECATED_TO_CORE_MAPPING.put(3, 2); // 支付宝(旧3) -> 支付宝(新2) + DEPRECATED_TO_CORE_MAPPING.put(12, 6); // 免费(旧12) -> 免费(新6) + DEPRECATED_TO_CORE_MAPPING.put(15, 7); // 积分支付(旧15) -> 积分支付(新7) + DEPRECATED_TO_CORE_MAPPING.put(19, 3); // 银联支付(旧19) -> 银联支付(新3) + + // 会员卡类支付 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(2, 0); // 会员卡支付 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(6, 0); // VIP月卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(7, 0); // VIP年卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(8, 0); // VIP次卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(9, 0); // IC月卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(10, 0); // IC年卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(11, 0); // IC次卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(13, 0); // VIP充值卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(14, 0); // IC充值卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(16, 0); // VIP季卡 -> 余额支付 + DEPRECATED_TO_CORE_MAPPING.put(17, 0); // IC季卡 -> 余额支付 + + // 微信Native -> 微信支付 + DEPRECATED_TO_CORE_MAPPING.put(102, 1); // 微信Native -> 微信支付 + + // 代付 -> 微信支付(默认) + DEPRECATED_TO_CORE_MAPPING.put(18, 1); // 代付 -> 微信支付 + } + + /** + * 将废弃的支付方式转换为核心支付方式 + * + * @param originalPayType 原始支付方式代码 + * @return 转换后的核心支付方式代码 + */ + public static Integer convertToCore(Integer originalPayType) { + if (originalPayType == null) { + return null; + } + + // 检查是否为废弃的支付方式 + if (DEPRECATED_TO_CORE_MAPPING.containsKey(originalPayType)) { + Integer corePayType = DEPRECATED_TO_CORE_MAPPING.get(originalPayType); + log.warn("检测到废弃的支付方式: {} -> {},建议升级到核心支付方式", + originalPayType, corePayType); + return corePayType; + } + + // 如果是核心支付方式,直接返回 + return originalPayType; + } + + /** + * 检查支付方式是否已废弃 + * + * @param payType 支付方式代码 + * @return true表示已废弃 + */ + public static boolean isDeprecated(Integer payType) { + return payType != null && DEPRECATED_TO_CORE_MAPPING.containsKey(payType); + } + + /** + * 获取支付方式的迁移说明 + * + * @param payType 支付方式代码 + * @return 迁移说明文本 + */ + public static String getMigrationMessage(Integer payType) { + if (payType == null || !isDeprecated(payType)) { + return null; + } + + PaymentType originalType = PaymentType.getByCode(payType); + PaymentType coreType = PaymentType.getByCode(convertToCore(payType)); + + if (originalType != null && coreType != null) { + return String.format("支付方式 %s(%d) 已废弃,建议使用 %s(%d)", + originalType.getName(), payType, + coreType.getName(), coreType.getCode()); + } + + return "该支付方式已废弃,请使用核心支付方式"; + } + + /** + * 获取所有核心支付方式代码 + * + * @return 核心支付方式代码数组 + */ + public static Integer[] getCorePaymentTypeCodes() { + return new Integer[]{0, 1, 2, 3, 4, 5, 6, 7}; + } + + /** + * 检查是否为核心支付方式 + * + * @param payType 支付方式代码 + * @return true表示是核心支付方式 + */ + public static boolean isCorePaymentType(Integer payType) { + if (payType == null) { + return false; + } + + for (Integer coreType : getCorePaymentTypeCodes()) { + if (coreType.equals(payType)) { + return true; + } + } + return false; + } + + /** + * 生成支付方式迁移报告 + * + * @return 迁移报告文本 + */ + public static String generateMigrationReport() { + StringBuilder report = new StringBuilder(); + report.append("=== 支付方式迁移报告 ===\n"); + report.append("核心支付方式(8种):\n"); + + for (Integer coreType : getCorePaymentTypeCodes()) { + PaymentType type = PaymentType.getByCode(coreType); + if (type != null) { + report.append(String.format(" %d - %s\n", coreType, type.getName())); + } + } + + report.append("\n废弃支付方式映射:\n"); + for (Map.Entry entry : DEPRECATED_TO_CORE_MAPPING.entrySet()) { + PaymentType originalType = PaymentType.getByCode(entry.getKey()); + PaymentType coreType = PaymentType.getByCode(entry.getValue()); + if (originalType != null && coreType != null) { + report.append(String.format(" %d(%s) -> %d(%s)\n", + entry.getKey(), originalType.getName(), + entry.getValue(), coreType.getName())); + } + } + + return report.toString(); + } +} diff --git a/src/api/clinic/clinicPrescription/index.ts b/src/api/clinic/clinicPrescription/index.ts index 03dc77a..301ae2a 100644 --- a/src/api/clinic/clinicPrescription/index.ts +++ b/src/api/clinic/clinicPrescription/index.ts @@ -106,3 +106,40 @@ export async function getClinicPrescription(id: number) { } return Promise.reject(new Error(res.message)); } + +/** + * 微信支付返回数据 + */ +export interface WxPayResult { + prepayId: string; + orderNo: string; + timeStamp: string; + nonceStr: string; + package: string; + signType: string; + paySign: string; +} + +/** + * 处方订单创建请求 + */ +export interface PrescriptionOrderRequest { + // 处方ID + prescriptionId: number; + // 支付方式 0余额支付, 1微信支付,3支付宝 + payType: number; +} + +/** + * 创建处方订单并支付 + */ +export async function createPrescriptionOrder(data: PrescriptionOrderRequest) { + const res = await request.post>( + '/clinic/clinic-prescription/order', + data + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/payment/index.ts b/src/api/payment/index.ts new file mode 100644 index 0000000..74b9b74 --- /dev/null +++ b/src/api/payment/index.ts @@ -0,0 +1,13 @@ +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; + +/** + * 统一支付 + */ +export async function pay(data: any) { + const res = await request.post>('/payment/create',data); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/payment/model/index.ts b/src/api/payment/model/index.ts new file mode 100644 index 0000000..a3edbc1 --- /dev/null +++ b/src/api/payment/model/index.ts @@ -0,0 +1,29 @@ + +/** + * 首页布局样式 + */ +export interface Layout { + // 内容区域的宽度 + width?: string; + // 文字颜色 + color?: string; + // 高亮颜色 + hover?: string; + // 背景颜色 + backgroundColor?: string; + headerStyle?: any; + siteNameStyle?: any; + showBanner?: boolean; + // 背景图片 + banner?: string; +} + +/** + * 修改密码参数 + */ +export interface UpdatePasswordParam { + // 新密码 + password: string; + // 原始密码 + oldPassword: string; +} diff --git a/src/clinic/clinicPatientUser/prescription.tsx b/src/clinic/clinicPatientUser/prescription.tsx index dcb3276..34c981e 100644 --- a/src/clinic/clinicPatientUser/prescription.tsx +++ b/src/clinic/clinicPatientUser/prescription.tsx @@ -4,11 +4,11 @@ import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag} from '@nutui import {View, Text} from '@tarojs/components' import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model"; import { - pageClinicPrescription + pageClinicPrescription, + WxPayResult } from "@/api/clinic/clinicPrescription"; import {copyText} from "@/utils/common"; -import {PaymentHandler} from "@/utils/payment"; -import {OrderCreateRequest} from "@/api/shop/shopOrder/model"; +import {pay} from "@/api/payment"; const ClinicPrescriptionList = () => { const [list, setList] = useState([]) @@ -35,18 +35,137 @@ const ClinicPrescriptionList = () => { } + /** + * 处理微信支付 + */ + const handleWechatPay = async (result: WxPayResult): Promise => { + console.log('处理微信支付:', result); + + if (!result) { + throw new Error('微信支付参数错误'); + } + + // 验证微信支付必要参数 + if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) { + throw new Error('微信支付参数不完整'); + } + + try { + await Taro.requestPayment({ + timeStamp: result.timeStamp, + nonceStr: result.nonceStr, + package: result.package, + signType: result.signType as any, + paySign: result.paySign, + }); + + console.log('微信支付成功'); + } catch (payError: any) { + console.error('微信支付失败:', payError); + + // 处理微信支付特定错误 + if (payError.errMsg) { + if (payError.errMsg.includes('cancel')) { + throw new Error('用户取消支付'); + } else if (payError.errMsg.includes('fail')) { + throw new Error('微信支付失败,请重试'); + } + } + + throw new Error('微信支付失败'); + } + }; + /** * 统一支付入口 */ const onPay = async (item: ClinicPrescription) => { - const orderData = { - ...item + if (!item.id) { + Taro.showToast({ + title: '处方信息缺失', + icon: 'error' + }); + return; } - console.log(orderData,'统一支付入口统一支付入口统一支付入口') + + Taro.showLoading({title: '支付中...'}); + try { - // 执行支付 - await PaymentHandler.pay(orderData, 1); + // 调用统一支付接口 + const result = await pay( + // addressId: 10951, + // orderId: 0, + // deliveryType: 0, + // paymentType: 'JSAPI', + // amount: 1, + // subject: '开处方' + { + "paymentType": "WECHAT", + "amount": 100.00, + "subject": "网站建设服务订单", + "description": "网站建设服务", + "orderInfo": { + "type": 0, + "realName": "无", + "address": "无", + "addressId": 0, + "deliveryType": 0, + "channel": 0, + "merchantId": null, + "merchantName": null, + "couponId": null, + "comments": "网站建设服务订单", + "goodsItems": [ + { + "goodsId": 10004, + "skuId": null, + "quantity": 1, + "specInfo": null + } + ] + } + } + ); + console.log(result, 'resultresultresultresultresultresult') + + console.log('订单创建结果:', result); + + if (!result) { + throw new Error('创建订单失败'); + } + + if (!result.orderNo) { + throw new Error('订单号获取失败'); + } + + // 调用微信支付 + await handleWechatPay(result); + + // 支付成功 + console.log('支付成功,订单号:', result.orderNo); + + Taro.showToast({ + title: '支付成功', + icon: 'success' + }); + + // 延迟刷新列表 + setTimeout(() => { + reload(); + }, 2000); + } catch (error: any) { + console.error('支付失败:', error); + + // 获取错误信息 + const errorMessage = error.message || '支付失败,请重试'; + + Taro.showToast({ + title: errorMessage, + icon: 'error' + }); + } finally { + Taro.hideLoading(); } }; @@ -123,7 +242,7 @@ const ClinicPrescriptionList = () => {