From 6baa74eb74d7ab96cecd8efffc46ed0f5488fbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 29 Aug 2025 16:44:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E6=B7=BB=E5=8A=A0=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1Native=E6=94=AF=E4=BB=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增微信支付配置属性类(WxPayProperties) - 新增微信支付常量类(WxPayConstants) - 实现微信Native支付接口(WxNativePayController)- 添加微信支付请求DTO(WxPayRequest) - 新增微信支付异常类(WxPayException)和异常处理器(WxPayExceptionHandler) - 修改订单服务实现类(ShopOrderServiceImpl),增加对微信支付的支持 --- .../shop/config/WxPayProperties.java | 108 ++++++ .../shop/constants/WxPayConstants.java | 200 ++++++++++++ .../controller/WxNativePayController.java | 266 +++++++++++++++ .../com/gxwebsoft/shop/dto/WxPayRequest.java | 117 +++++++ .../shop/exception/WxPayException.java | 196 +++++++++++ .../shop/exception/WxPayExceptionHandler.java | 140 ++++++++ .../shop/service/WxPayConfigService.java | 275 ++++++++++++++++ .../shop/service/WxPayNotifyService.java | 309 ++++++++++++++++++ .../service/impl/ShopOrderServiceImpl.java | 4 +- 9 files changed, 1614 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gxwebsoft/shop/config/WxPayProperties.java create mode 100644 src/main/java/com/gxwebsoft/shop/constants/WxPayConstants.java create mode 100644 src/main/java/com/gxwebsoft/shop/controller/WxNativePayController.java create mode 100644 src/main/java/com/gxwebsoft/shop/dto/WxPayRequest.java create mode 100644 src/main/java/com/gxwebsoft/shop/exception/WxPayException.java create mode 100644 src/main/java/com/gxwebsoft/shop/exception/WxPayExceptionHandler.java create mode 100644 src/main/java/com/gxwebsoft/shop/service/WxPayConfigService.java create mode 100644 src/main/java/com/gxwebsoft/shop/service/WxPayNotifyService.java diff --git a/src/main/java/com/gxwebsoft/shop/config/WxPayProperties.java b/src/main/java/com/gxwebsoft/shop/config/WxPayProperties.java new file mode 100644 index 0000000..6ee83ce --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/config/WxPayProperties.java @@ -0,0 +1,108 @@ +package com.gxwebsoft.shop.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 微信支付配置属性 + * 管理微信支付相关的配置项 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Data +@Component +@ConfigurationProperties(prefix = "wx.pay") +public class WxPayProperties { + + /** + * 服务器URL(用于构建回调地址) + */ + private String serverUrl = "https://server.gxwebsoft.com"; + + /** + * 回调路径模板 + */ + private String notifyUrlTemplate = "/api/system/wx-native-pay/notify/{tenantId}"; + + /** + * 订单超时时间(分钟) + */ + private int orderTimeoutMinutes = 30; + + /** + * 是否启用签名验证 + */ + private boolean enableSignatureVerification = true; + + /** + * 是否启用金额验证 + */ + private boolean enableAmountVerification = true; + + /** + * 最大重试次数 + */ + private int maxRetryCount = 3; + + /** + * 重试间隔(毫秒) + */ + private long retryIntervalMs = 1000; + + /** + * 是否启用详细日志 + */ + private boolean enableDetailedLogging = false; + + /** + * 测试环境配置 + */ + private TestConfig test = new TestConfig(); + + /** + * 测试环境配置 + */ + @Data + public static class TestConfig { + /** + * 测试商户号 + */ + private String merchantId = "1246610101"; + + /** + * 测试商户序列号 + */ + private String merchantSerialNumber = "2903B872D5CA36E525FAEC37AEDB22E54ECDE7B7"; + + /** + * 是否启用测试模式 + */ + private boolean enabled = false; + + /** + * 测试金额(分) + */ + private int testAmount = 1; + } + + /** + * 构建完整的回调URL + * + * @param tenantId 租户ID + * @return 完整的回调URL + */ + public String buildNotifyUrl(Integer tenantId) { + return serverUrl + notifyUrlTemplate.replace("{tenantId}", tenantId.toString()); + } + + /** + * 获取订单超时时间(秒) + * + * @return 超时时间(秒) + */ + public long getOrderTimeoutSeconds() { + return orderTimeoutMinutes * 60L; + } +} diff --git a/src/main/java/com/gxwebsoft/shop/constants/WxPayConstants.java b/src/main/java/com/gxwebsoft/shop/constants/WxPayConstants.java new file mode 100644 index 0000000..1618dd8 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/constants/WxPayConstants.java @@ -0,0 +1,200 @@ +package com.gxwebsoft.shop.constants; + +/** + * 微信支付常量类 + * 管理微信支付相关的常量配置 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +public class WxPayConstants { + + /** + * 微信支付类型 + */ + public static class PayType { + /** Native支付 */ + public static final String NATIVE = "NATIVE"; + /** JSAPI支付 */ + public static final String JSAPI = "JSAPI"; + /** H5支付 */ + public static final String MWEB = "MWEB"; + /** APP支付 */ + public static final String APP = "APP"; + } + + /** + * 支付状态 + */ + public static class PayStatus { + /** 支付成功 */ + public static final String SUCCESS = "SUCCESS"; + /** 转入退款 */ + public static final String REFUND = "REFUND"; + /** 未支付 */ + public static final String NOTPAY = "NOTPAY"; + /** 已关闭 */ + public static final String CLOSED = "CLOSED"; + /** 已撤销(付款码支付) */ + public static final String REVOKED = "REVOKED"; + /** 用户支付中(付款码支付) */ + public static final String USERPAYING = "USERPAYING"; + /** 支付失败(其他原因,如银行返回失败) */ + public static final String PAYERROR = "PAYERROR"; + } + + /** + * 回调通知相关 + */ + public static class Notify { + /** 成功响应 */ + public static final String SUCCESS_RESPONSE = "SUCCESS"; + /** 失败响应 */ + public static final String FAIL_RESPONSE = "FAIL"; + + /** 通知类型 - 支付成功 */ + public static final String EVENT_TYPE_PAYMENT = "TRANSACTION.SUCCESS"; + /** 通知类型 - 退款成功 */ + public static final String EVENT_TYPE_REFUND = "REFUND.SUCCESS"; + } + + /** + * 缓存键前缀 + */ + public static class CacheKey { + /** 支付配置缓存键前缀 */ + public static final String PAYMENT_CONFIG_PREFIX = "Payment:wxPay:"; + /** 微信小程序配置缓存键 */ + public static final String MP_WEIXIN_CONFIG = "mp-weixin"; + } + + /** + * 配置相关 + */ + public static class Config { + /** 货币类型 - 人民币 */ + public static final String CURRENCY_CNY = "CNY"; + /** 金额转换倍数(元转分) */ + public static final int AMOUNT_MULTIPLIER = 100; + + /** 开发环境标识 */ + public static final String PROFILE_DEV = "dev"; + /** 生产环境标识 */ + public static final String PROFILE_PROD = "prod"; + } + + /** + * 订单相关 + */ + public static class Order { + /** 订单超时时间(分钟) */ + public static final int TIMEOUT_MINUTES = 30; + /** 订单描述最大长度 */ + public static final int DESCRIPTION_MAX_LENGTH = 127; + } + + /** + * HTTP头部相关 + */ + public static class Header { + /** 微信支付签名 */ + public static final String WECHATPAY_SIGNATURE = "Wechatpay-Signature"; + /** 微信支付时间戳 */ + public static final String WECHATPAY_TIMESTAMP = "Wechatpay-Timestamp"; + /** 微信支付随机数 */ + public static final String WECHATPAY_NONCE = "Wechatpay-Nonce"; + /** 微信支付序列号 */ + public static final String WECHATPAY_SERIAL = "Wechatpay-Serial"; + /** 请求ID */ + public static final String REQUEST_ID = "Request-ID"; + } + + /** + * 错误信息 + */ + public static class ErrorMessage { + /** 配置未找到 */ + public static final String CONFIG_NOT_FOUND = "微信支付配置未找到"; + /** 证书文件不存在 */ + public static final String CERTIFICATE_NOT_FOUND = "微信支付证书文件不存在"; + /** 参数验证失败 */ + public static final String PARAM_VALIDATION_FAILED = "参数验证失败"; + /** 签名验证失败 */ + public static final String SIGNATURE_VERIFICATION_FAILED = "签名验证失败"; + /** 订单不存在 */ + public static final String ORDER_NOT_FOUND = "订单不存在"; + /** 订单状态异常 */ + public static final String ORDER_STATUS_INVALID = "订单状态异常"; + /** 金额不匹配 */ + public static final String AMOUNT_MISMATCH = "订单金额不匹配"; + /** 网络请求失败 */ + public static final String NETWORK_REQUEST_FAILED = "网络请求失败"; + /** 系统内部错误 */ + public static final String SYSTEM_INTERNAL_ERROR = "系统内部错误"; + } + + /** + * 日志相关 + */ + public static class LogMessage { + /** 支付请求开始 */ + public static final String PAY_REQUEST_START = "开始处理微信支付请求"; + /** 支付请求成功 */ + public static final String PAY_REQUEST_SUCCESS = "微信支付请求处理成功"; + /** 支付请求失败 */ + public static final String PAY_REQUEST_FAILED = "微信支付请求处理失败"; + + /** 回调处理开始 */ + public static final String CALLBACK_START = "开始处理微信支付回调"; + /** 回调处理成功 */ + public static final String CALLBACK_SUCCESS = "微信支付回调处理成功"; + /** 回调处理失败 */ + public static final String CALLBACK_FAILED = "微信支付回调处理失败"; + + /** 配置加载成功 */ + public static final String CONFIG_LOADED = "微信支付配置加载成功"; + /** 配置加载失败 */ + public static final String CONFIG_LOAD_FAILED = "微信支付配置加载失败"; + } + + /** + * 正则表达式 + */ + public static class Regex { + /** 商户号格式 */ + public static final String MERCHANT_ID = "^\\d{10}$"; + /** 订单号格式 */ + public static final String ORDER_NO = "^[a-zA-Z0-9_-]{1,32}$"; + /** 金额格式(分) */ + public static final String AMOUNT = "^[1-9]\\d*$"; + } + + /** + * 时间相关 + */ + public static class Time { + /** 签名有效期(秒) */ + public static final long SIGNATURE_VALID_SECONDS = 300; + /** 配置缓存有效期(秒) */ + public static final long CONFIG_CACHE_SECONDS = 3600; + } + + /** + * 文件相关 + */ + public static class File { + /** 证书文件扩展名 */ + public static final String CERT_EXTENSION = ".pem"; + /** 私钥文件名 */ + public static final String PRIVATE_KEY_FILE = "apiclient_key.pem"; + /** 商户证书文件名 */ + public static final String MERCHANT_CERT_FILE = "apiclient_cert.pem"; + /** 平台证书文件名 */ + public static final String PLATFORM_CERT_FILE = "wechatpay_cert.pem"; + } + + // 私有构造函数,防止实例化 + private WxPayConstants() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/controller/WxNativePayController.java b/src/main/java/com/gxwebsoft/shop/controller/WxNativePayController.java new file mode 100644 index 0000000..59d4126 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/controller/WxNativePayController.java @@ -0,0 +1,266 @@ +package com.gxwebsoft.shop.controller; + +import com.alibaba.fastjson.JSONObject; +import com.gxwebsoft.common.core.utils.CommonUtil; +import com.gxwebsoft.common.core.utils.RedisUtil; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.system.entity.Payment; +import com.gxwebsoft.common.system.service.SettingService; +import com.gxwebsoft.shop.constants.WxPayConstants; +import com.gxwebsoft.shop.entity.ShopOrder; +import com.gxwebsoft.shop.exception.WxPayException; +import com.gxwebsoft.shop.service.ShopOrderService; +import com.gxwebsoft.shop.config.WxPayProperties; +import com.gxwebsoft.shop.dto.WxPayRequest; +import com.gxwebsoft.shop.service.WxPayConfigService; +import com.gxwebsoft.shop.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 io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import java.math.BigDecimal; +import java.util.Map; + + +@Slf4j +@Validated +@Tag(name = "微信Native支付接口") +@RestController +@RequestMapping("/api/system/wx-native-pay") +public class WxNativePayController extends BaseController { + + @Resource + private RedisUtil redisUtil; + + @Resource + private SettingService settingService; + + @Resource + private ShopOrderService shopOrderService; + + @Resource + private WxPayConfigService wxPayConfigService; + + @Resource + private WxPayNotifyService wxPayNotifyService; + + @Resource + private WxPayProperties wxPayProperties; + + + @Operation(summary = "生成付款码") + @PostMapping("/codeUrl") + public ApiResult getCodeUrl(@Valid @RequestBody WxPayRequest payRequest) { + log.info("{}, 租户ID: {}, 金额: {}", + WxPayConstants.LogMessage.PAY_REQUEST_START, payRequest.getTenantId(), payRequest.getFormattedAmount()); + + try { + // 设置当前租户ID(从请求参数中获取) + setCurrentTenantId(payRequest.getTenantId()); + + // 获取支付配置 + Payment payment = getPaymentConfig(); + + // 获取微信小程序配置 + String appId = getWxAppId(payRequest.getTenantId()); + + // 准备订单数据 + ShopOrder order = buildOrderFromRequest(payRequest); + + // 创建支付请求 + PrepayRequest request = buildPrepayRequest(order, payment, appId); + + // 调用微信支付API + PrepayResponse response = callWxPayApi(request); + + log.info("{}, 租户ID: {}, 订单号: {}, 金额: {}", + WxPayConstants.LogMessage.PAY_REQUEST_SUCCESS, + payRequest.getTenantId(), order.getOrderNo(), payRequest.getFormattedAmount()); + + return success("生成付款码", response.getCodeUrl()); + + } catch (WxPayException e) { + log.error("{}, 租户ID: {}, 错误: {}", + WxPayConstants.LogMessage.PAY_REQUEST_FAILED, payRequest.getTenantId(), e.getMessage()); + if (wxPayProperties.isEnableDetailedLogging()) { + log.error("详细错误信息", e); + } + return fail(e.getMessage()); + } catch (Exception e) { + log.error("{}, 租户ID: {}, 系统错误: {}", + WxPayConstants.LogMessage.PAY_REQUEST_FAILED, payRequest.getTenantId(), e.getMessage(), e); + return fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR); + } + } + + + /** + * 设置当前租户ID(用于多租户上下文) + */ + private void setCurrentTenantId(Integer tenantId) { + // 这里可以设置到ThreadLocal或其他上下文中 + // 具体实现取决于你的多租户架构 + log.debug("设置当前租户ID: {}", tenantId); + } + + /** + * 从支付请求构建订单对象 + */ + private ShopOrder buildOrderFromRequest(WxPayRequest payRequest) { + ShopOrder order = new ShopOrder(); + + // 基本信息 + order.setTenantId(payRequest.getTenantId()); + order.setPayPrice(payRequest.getPayPrice()); + order.setMoney(payRequest.getPayPrice()); + order.setTotalPrice(payRequest.getPayPrice()); + order.setComments(payRequest.getEffectiveDescription()); + + // 生成订单号 + if (StringUtils.hasText(payRequest.getOrderNo())) { + order.setOrderNo(payRequest.getOrderNo()); + } else { + order.setOrderNo(CommonUtil.createOrderNo()); + } + + // 其他字段 + order.setUserId(payRequest.getUserId()); + order.setFormId(payRequest.getGoodsId()); + order.setTotalNum(payRequest.getQuantity()); + order.setType(payRequest.getOrderType()); + order.setDeliveryType(payRequest.getDeliveryType()); + order.setChannel(payRequest.getChannel()); + order.setBuyerRemarks(payRequest.getBuyerRemarks()); + order.setAddressId(payRequest.getAddressId()); + order.setSelfTakeMerchantId(payRequest.getSelfTakeMerchantId()); + + // 设置默认值 + order.setPayStatus(false); + order.setOrderStatus(0); + order.setPayType(102); // 微信Native支付 + + log.debug("从支付请求构建订单完成, 订单号: {}, 金额: {}", + order.getOrderNo(), order.getPayPrice()); + + return order; + } + + /** + * 获取支付配置 + */ + private Payment getPaymentConfig() throws WxPayException { + String cacheKey = WxPayConstants.CacheKey.PAYMENT_CONFIG_PREFIX + getTenantId(); + Payment payment = redisUtil.get(cacheKey, Payment.class); + + if (payment == null) { + throw WxPayException.configError(WxPayConstants.ErrorMessage.CONFIG_NOT_FOUND, getTenantId()); + } + + log.debug("获取支付配置成功, 租户ID: {}", getTenantId()); + return payment; + } + + /** + * 获取微信小程序AppId + */ + private String getWxAppId(Integer tenantId) throws WxPayException { + JSONObject setting = settingService.getBySettingKey(WxPayConstants.CacheKey.MP_WEIXIN_CONFIG, tenantId); + if (setting == null) { + throw WxPayException.configError("微信小程序配置未找到", tenantId); + } + + String appId = setting.getString("appId"); + if (!StringUtils.hasText(appId)) { + throw WxPayException.configError("微信小程序AppId未配置", tenantId); + } + + log.debug("获取微信AppId成功, 租户ID: {}", tenantId); + return appId; + } + + + + /** + * 构建预支付请求 + */ + private PrepayRequest buildPrepayRequest(ShopOrder order, Payment payment, String appId) throws WxPayException { + PrepayRequest request = new PrepayRequest(); + + // 设置金额(转换为分) + BigDecimal amountYuan = order.getMoney(); + int amountFen = amountYuan.multiply(new BigDecimal(WxPayConstants.Config.AMOUNT_MULTIPLIER)).intValue(); + + Amount amount = new Amount(); + amount.setTotal(amountFen); + amount.setCurrency(WxPayConstants.Config.CURRENCY_CNY); + + request.setAmount(amount); + request.setAppid(appId); + request.setMchid(payment.getMchId()); + request.setDescription(order.getComments()); + request.setOutTradeNo(order.getOrderNo()); + + // 构建回调URL + String notifyUrl = wxPayProperties.buildNotifyUrl(getTenantId()); + request.setNotifyUrl(notifyUrl); + + log.debug("预支付请求构建完成, 订单号: {}, 金额: {}分, 回调URL: {}", + order.getOrderNo(), amountFen, notifyUrl); + + return request; + } + + /** + * 调用微信支付API + */ + private PrepayResponse callWxPayApi(PrepayRequest request) throws WxPayException { + try { + // 获取微信支付配置 + Config wxPayConfig = wxPayConfigService.getWxPayConfig(getTenantId()); + + // 构建服务 + NativePayService service = new NativePayService.Builder().config(wxPayConfig).build(); + + // 调用预支付接口 + PrepayResponse response = service.prepay(request); + + if (response == null || !StringUtils.hasText(response.getCodeUrl())) { + throw WxPayException.networkError("微信支付API返回数据异常", null); + } + + log.debug("微信支付API调用成功, 订单号: {}", request.getOutTradeNo()); + return response; + + } catch (Exception e) { + if (e instanceof WxPayException) { + throw e; + } + throw WxPayException.networkError("调用微信支付API失败: " + e.getMessage(), e); + } + } + + @Operation(summary = "异步通知") + @PostMapping("/notify/{tenantId}") + public String wxNotify(@RequestHeader Map headers, + @RequestBody String body, + @PathVariable("tenantId") Integer tenantId) { + + log.info("收到微信支付回调通知, 租户ID: {}", tenantId); + + // 委托给专门的回调处理服务 + return wxPayNotifyService.handlePaymentNotify(headers, body, tenantId); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/dto/WxPayRequest.java b/src/main/java/com/gxwebsoft/shop/dto/WxPayRequest.java new file mode 100644 index 0000000..a125c85 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/dto/WxPayRequest.java @@ -0,0 +1,117 @@ +package com.gxwebsoft.shop.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.*; +import java.math.BigDecimal; + +/** + * 微信支付请求DTO + * 用于接收和验证微信支付请求参数 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Data +@Schema(name = "微信支付请求", description = "微信支付请求参数") +public class WxPayRequest { + + @Schema(description = "租户ID", required = true) + @NotNull(message = "租户ID不能为空") + @Positive(message = "租户ID必须为正数") + private Integer tenantId; + + @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 payPrice; + + @Schema(description = "订单描述", example = "商品订单") + @Size(max = 127, message = "订单描述不能超过127个字符") + private String description; + + @Schema(description = "订单号(可选,不提供则自动生成)") + @Size(max = 32, message = "订单号不能超过32个字符") + @Pattern(regexp = "^[a-zA-Z0-9_-]*$", message = "订单号只能包含字母、数字、下划线和横线") + private String orderNo; + + @Schema(description = "用户ID") + @Positive(message = "用户ID必须为正数") + private Integer userId; + + @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 = "订单类型不能为负数") + @Max(value = 10, message = "订单类型值超出范围") + private Integer orderType = 0; + + @Schema(description = "配送方式", example = "1") + @Min(value = 0, message = "配送方式不能为负数") + private Integer deliveryType; + + @Schema(description = "下单渠道", example = "0") + @Min(value = 0, message = "下单渠道不能为负数") + private Integer channel = 0; + + @Schema(description = "买家备注") + @Size(max = 500, message = "买家备注不能超过500个字符") + private String buyerRemarks; + + @Schema(description = "收货地址ID") + @Positive(message = "收货地址ID必须为正数") + private Integer addressId; + + @Schema(description = "自提店铺ID") + @Positive(message = "自提店铺ID必须为正数") + private Integer selfTakeMerchantId; + + /** + * 获取有效的订单描述 + * 如果没有提供描述,返回默认值 + */ + public String getEffectiveDescription() { + if (description == null || description.trim().isEmpty()) { + return "商品订单"; + } + return description.trim(); + } + + /** + * 验证必要参数是否完整 + */ + public boolean isValid() { + return tenantId != null && tenantId > 0 + && payPrice != null && payPrice.compareTo(BigDecimal.ZERO) > 0; + } + + /** + * 获取格式化的金额字符串 + */ + public String getFormattedAmount() { + if (payPrice == null) { + return "0.00"; + } + return String.format("%.2f", payPrice); + } + + /** + * 转换为分(微信支付API需要) + */ + public Integer getAmountInCents() { + if (payPrice == null) { + return 0; + } + return payPrice.multiply(new BigDecimal(100)).intValue(); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/exception/WxPayException.java b/src/main/java/com/gxwebsoft/shop/exception/WxPayException.java new file mode 100644 index 0000000..2733537 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/exception/WxPayException.java @@ -0,0 +1,196 @@ +package com.gxwebsoft.shop.exception; + +/** + * 微信支付异常类 + * 用于处理微信支付相关的业务异常 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +public class WxPayException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * 错误代码 + */ + private String errorCode; + + /** + * 租户ID(可选) + */ + private Integer tenantId; + + public WxPayException(String message) { + super(message); + } + + public WxPayException(String message, Throwable cause) { + super(message, cause); + } + + public WxPayException(String errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public WxPayException(String errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + public WxPayException(String errorCode, String message, Integer tenantId) { + super(message); + this.errorCode = errorCode; + this.tenantId = tenantId; + } + + public WxPayException(String errorCode, String message, Integer tenantId, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + this.tenantId = tenantId; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public Integer getTenantId() { + return tenantId; + } + + public void setTenantId(Integer tenantId) { + this.tenantId = tenantId; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("WxPayException{"); + if (errorCode != null) { + sb.append("errorCode='").append(errorCode).append("', "); + } + if (tenantId != null) { + sb.append("tenantId=").append(tenantId).append(", "); + } + sb.append("message='").append(getMessage()).append("'"); + sb.append("}"); + return sb.toString(); + } + + /** + * 微信支付错误代码常量 + */ + public static class ErrorCode { + /** 配置错误 */ + public static final String CONFIG_ERROR = "CONFIG_ERROR"; + + /** 证书错误 */ + public static final String CERTIFICATE_ERROR = "CERTIFICATE_ERROR"; + + /** 参数错误 */ + public static final String PARAM_ERROR = "PARAM_ERROR"; + + /** 网络错误 */ + public static final String NETWORK_ERROR = "NETWORK_ERROR"; + + /** 签名验证失败 */ + public static final String SIGNATURE_ERROR = "SIGNATURE_ERROR"; + + /** 订单状态错误 */ + public static final String ORDER_STATUS_ERROR = "ORDER_STATUS_ERROR"; + + /** 金额错误 */ + public static final String AMOUNT_ERROR = "AMOUNT_ERROR"; + + /** 租户配置错误 */ + public static final String TENANT_CONFIG_ERROR = "TENANT_CONFIG_ERROR"; + + /** 回调处理错误 */ + public static final String CALLBACK_ERROR = "CALLBACK_ERROR"; + + /** 系统内部错误 */ + public static final String SYSTEM_ERROR = "SYSTEM_ERROR"; + } + + /** + * 创建配置错误异常 + */ + public static WxPayException configError(String message) { + return new WxPayException(ErrorCode.CONFIG_ERROR, message); + } + + /** + * 创建配置错误异常(带租户ID) + */ + public static WxPayException configError(String message, Integer tenantId) { + return new WxPayException(ErrorCode.CONFIG_ERROR, message, tenantId); + } + + /** + * 创建证书错误异常 + */ + public static WxPayException certificateError(String message) { + return new WxPayException(ErrorCode.CERTIFICATE_ERROR, message); + } + + /** + * 创建参数错误异常 + */ + public static WxPayException paramError(String message) { + return new WxPayException(ErrorCode.PARAM_ERROR, message); + } + + /** + * 创建网络错误异常 + */ + public static WxPayException networkError(String message, Throwable cause) { + return new WxPayException(ErrorCode.NETWORK_ERROR, message, cause); + } + + /** + * 创建签名验证失败异常 + */ + public static WxPayException signatureError(String message) { + return new WxPayException(ErrorCode.SIGNATURE_ERROR, message); + } + + /** + * 创建订单状态错误异常 + */ + public static WxPayException orderStatusError(String message) { + return new WxPayException(ErrorCode.ORDER_STATUS_ERROR, message); + } + + /** + * 创建金额错误异常 + */ + public static WxPayException amountError(String message) { + return new WxPayException(ErrorCode.AMOUNT_ERROR, message); + } + + /** + * 创建租户配置错误异常 + */ + public static WxPayException tenantConfigError(String message, Integer tenantId) { + return new WxPayException(ErrorCode.TENANT_CONFIG_ERROR, message, tenantId); + } + + /** + * 创建回调处理错误异常 + */ + public static WxPayException callbackError(String message, Throwable cause) { + return new WxPayException(ErrorCode.CALLBACK_ERROR, message, cause); + } + + /** + * 创建系统内部错误异常 + */ + public static WxPayException systemError(String message, Throwable cause) { + return new WxPayException(ErrorCode.SYSTEM_ERROR, message, cause); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/exception/WxPayExceptionHandler.java b/src/main/java/com/gxwebsoft/shop/exception/WxPayExceptionHandler.java new file mode 100644 index 0000000..e241fe8 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/exception/WxPayExceptionHandler.java @@ -0,0 +1,140 @@ +package com.gxwebsoft.shop.exception; + +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.shop.constants.WxPayConstants; +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.shop.controller") +public class WxPayExceptionHandler { + + /** + * 处理微信支付业务异常 + */ + @ExceptionHandler(WxPayException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResult handleWxPayException(WxPayException e) { + log.warn("微信支付业务异常: {}", e.getMessage()); + + if (e.getTenantId() != null) { + log.warn("异常租户ID: {}", e.getTenantId()); + } + + if (e.getErrorCode() != null) { + log.warn("错误代码: {}", e.getErrorCode()); + } + + return ApiResult.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 ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + 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 ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + 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 ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + errorMessage); + } + + /** + * 处理非法参数异常 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResult handleIllegalArgumentException(IllegalArgumentException e) { + log.warn("非法参数异常: {}", e.getMessage()); + return ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + e.getMessage()); + } + + /** + * 处理空指针异常 + */ + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResult handleNullPointerException(NullPointerException e) { + log.error("空指针异常", e); + return ApiResult.fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR); + } + + /** + * 处理其他运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResult handleRuntimeException(RuntimeException e) { + log.error("运行时异常: {}", e.getMessage(), e); + return ApiResult.fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR); + } + + /** + * 处理其他异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResult handleException(Exception e) { + log.error("未知异常: {}", e.getMessage(), e); + return ApiResult.fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/service/WxPayConfigService.java b/src/main/java/com/gxwebsoft/shop/service/WxPayConfigService.java new file mode 100644 index 0000000..865d36c --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/service/WxPayConfigService.java @@ -0,0 +1,275 @@ +package com.gxwebsoft.shop.service; + +import com.gxwebsoft.common.core.config.CertificateProperties; +import com.gxwebsoft.common.core.config.ConfigProperties; +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.shop.exception.WxPayException; +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.File; + +/** + * 微信支付配置服务 + * 负责处理微信支付的配置获取和管理 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Slf4j +@Service +public class WxPayConfigService { + + @Value("${spring.profiles.active}") + private String activeProfile; + + @Resource + private RedisUtil redisUtil; + + @Resource + private ConfigProperties configProperties; + + @Resource + private CertificateService certificateService; + + @Resource + private CertificateProperties certificateProperties; + + /** + * 获取微信支付配置 + * + * @param tenantId 租户ID + * @return 微信支付配置 + * @throws WxPayException 配置获取失败时抛出 + */ + public Config getWxPayConfig(Integer tenantId) throws WxPayException { + if (tenantId == null) { + throw new WxPayException("租户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 WxPayException { + try { + // 获取支付配置信息 + Payment payment = getPaymentConfig(tenantId); + + // 获取证书路径 + String certificatePath = getCertificatePath(tenantId, payment); + + // 构建配置 + return createWxPayConfig(payment, certificatePath); + + } catch (Exception e) { + log.error("构建微信支付配置失败,租户ID: {}, 错误: {}", tenantId, e.getMessage(), e); + throw new WxPayException("微信支付配置构建失败: " + e.getMessage(), e); + } + } + + /** + * 获取支付配置信息 + */ + private Payment getPaymentConfig(Integer tenantId) throws WxPayException { + String cacheKey = "Payment:wxPay:" + tenantId; + Payment payment = redisUtil.get(cacheKey, Payment.class); + + if (payment == null && !"dev".equals(activeProfile)) { + throw new WxPayException("微信支付配置未找到,租户ID: " + tenantId); + } + + if (payment != null) { + log.debug("从缓存获取支付配置成功,租户ID: {}", tenantId); + } else { + log.debug("开发环境模式,将使用测试配置,租户ID: {}", tenantId); + } + + return payment; + } + + /** + * 获取证书文件路径 + */ + private String getCertificatePath(Integer tenantId, Payment payment) throws WxPayException { + if ("dev".equals(activeProfile)) { + return getDevCertificatePath(tenantId); + } else { + return getProdCertificatePath(payment); + } + } + + /** + * 获取开发环境证书路径 + */ + private String getDevCertificatePath(Integer tenantId) throws WxPayException { + try { + String relativePath = certificateService.getWechatPayCertPath( + certificateProperties.getWechatPay().getDev().getPrivateKeyFile() + ); + + String certificatePath; + if (certificateProperties.isClasspathMode()) { + // classpath模式 + try { + ClassPathResource resource = new ClassPathResource(relativePath); + if (resource.exists()) { + certificatePath = resource.getFile().getAbsolutePath(); + } else { + certificatePath = "classpath:" + relativePath; + } + } catch (Exception e) { + certificatePath = "classpath:" + relativePath; + } + } else { + // 文件系统模式 + certificatePath = new File(relativePath).getAbsolutePath(); + } + + // 验证证书文件是否存在 + if (!certificateService.certificateExists("wechat", + certificateProperties.getWechatPay().getDev().getPrivateKeyFile())) { + throw new WxPayException("微信支付私钥证书文件不存在"); + } + + log.debug("开发环境证书路径: {}", certificatePath); + return certificatePath; + + } catch (Exception e) { + throw new WxPayException("获取开发环境证书路径失败: " + e.getMessage(), e); + } + } + + /** + * 获取生产环境证书路径 + */ + private String getProdCertificatePath(Payment payment) throws WxPayException { + if (payment == null || payment.getApiclientKey() == null) { + throw new WxPayException("生产环境支付配置或证书路径为空"); + } + + String relativePath = payment.getApiclientKey(); + String certificatePath = configProperties.getUploadPath() + "file" + relativePath; + + // 验证证书文件是否存在 + File certFile = new File(certificatePath); + if (!certFile.exists()) { + throw new WxPayException("生产环境证书文件不存在: " + certificatePath); + } + + log.debug("生产环境证书路径: {}", certificatePath); + return certificatePath; + } + + /** + * 创建微信支付配置对象 + */ + private Config createWxPayConfig(Payment payment, String certificatePath) throws WxPayException { + try { + if ("dev".equals(activeProfile) && payment == null) { + // 开发环境测试配置 + return createDevTestConfig(certificatePath); + } else if (payment != null) { + // 正常配置 + return createNormalConfig(payment, certificatePath); + } else { + throw new WxPayException("无法创建微信支付配置:配置信息不完整"); + } + } catch (Exception e) { + throw new WxPayException("创建微信支付配置对象失败: " + e.getMessage(), e); + } + } + + /** + * 创建开发环境测试配置 + */ + private Config createDevTestConfig(String certificatePath) throws WxPayException { + String testMerchantId = "1246610101"; + String testMerchantSerialNumber = "2903B872D5CA36E525FAEC37AEDB22E54ECDE7B7"; + String testApiV3Key = certificateProperties.getWechatPay().getDev().getApiV3Key(); + + if (testApiV3Key == null || testApiV3Key.trim().isEmpty()) { + throw new WxPayException("开发环境APIv3密钥未配置"); + } + + 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 WxPayException { + if (payment.getMchId() == null || payment.getMerchantSerialNumber() == null || payment.getApiKey() == null) { + throw new WxPayException("支付配置信息不完整:商户号、序列号或APIv3密钥为空"); + } + + log.info("使用数据库支付配置"); + log.debug("商户号: {}", payment.getMchId()); + + return new RSAAutoCertificateConfig.Builder() + .merchantId(payment.getMchId()) + .privateKeyFromPath(certificatePath) + .merchantSerialNumber(payment.getMerchantSerialNumber()) + .apiV3Key(payment.getApiKey()) + .build(); + } + + /** + * 清除指定租户的配置缓存 + * + * @param tenantId 租户ID + */ + public void clearConfigCache(Integer tenantId) { + WxNativeUtil.addConfig(tenantId, null); + log.info("清除微信支付配置缓存,租户ID: {}", tenantId); + } + + /** + * 验证支付配置是否有效 + * + * @param tenantId 租户ID + * @return 配置是否有效 + */ + public boolean isConfigValid(Integer tenantId) { + try { + getWxPayConfig(tenantId); + return true; + } catch (Exception e) { + log.warn("微信支付配置验证失败,租户ID: {}, 错误: {}", tenantId, e.getMessage()); + return false; + } + } +} diff --git a/src/main/java/com/gxwebsoft/shop/service/WxPayNotifyService.java b/src/main/java/com/gxwebsoft/shop/service/WxPayNotifyService.java new file mode 100644 index 0000000..044fa8f --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/service/WxPayNotifyService.java @@ -0,0 +1,309 @@ +package com.gxwebsoft.shop.service; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.gxwebsoft.common.core.constants.OrderConstants; +import com.gxwebsoft.common.core.utils.RequestUtil; +import com.gxwebsoft.shop.constants.WxPayConstants; +import com.gxwebsoft.shop.entity.ShopOrder; +import com.gxwebsoft.shop.exception.WxPayException; +import com.wechat.pay.java.core.Config; +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.util.Map; + +/** + * 微信支付回调通知处理服务 + * 负责处理微信支付的异步通知回调 + * + * @author 科技小王子 + * @since 2025-01-26 + */ +@Slf4j +@Service +public class WxPayNotifyService { + + @Resource + private WxPayConfigService wxPayConfigService; + + @Resource + private ShopOrderService shopOrderService; + + @Resource + private RequestUtil requestUtil; + + /** + * 处理微信支付回调通知 + * + * @param headers 请求头 + * @param body 请求体 + * @param tenantId 租户ID + * @return 处理结果响应 + */ + public String handlePaymentNotify(Map headers, String body, Integer tenantId) { + log.info("{}, 租户ID: {}", WxPayConstants.LogMessage.CALLBACK_START, tenantId); + + try { + // 参数验证 + validateNotifyParams(headers, body, tenantId); + + // 获取微信支付配置 + Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId); + + // 解析并验证回调数据 + Transaction transaction = parseAndVerifyNotification(headers, body, wxPayConfig); + + // 处理支付结果 + processPaymentResult(transaction, tenantId); + + log.info("{}, 租户ID: {}, 订单号: {}", + WxPayConstants.LogMessage.CALLBACK_SUCCESS, tenantId, transaction.getOutTradeNo()); + + return WxPayConstants.Notify.SUCCESS_RESPONSE; + + } catch (WxPayException e) { + log.error("{}, 租户ID: {}, 错误: {}", + WxPayConstants.LogMessage.CALLBACK_FAILED, tenantId, e.getMessage(), e); + return WxPayConstants.Notify.FAIL_RESPONSE; + } catch (Exception e) { + log.error("{}, 租户ID: {}, 系统错误: {}", + WxPayConstants.LogMessage.CALLBACK_FAILED, tenantId, e.getMessage(), e); + return WxPayConstants.Notify.FAIL_RESPONSE; + } + } + + /** + * 验证回调通知参数 + */ + private void validateNotifyParams(Map headers, String body, Integer tenantId) + throws WxPayException { + + if (tenantId == null) { + throw WxPayException.paramError("租户ID不能为空"); + } + + if (headers == null || headers.isEmpty()) { + throw WxPayException.paramError("请求头不能为空"); + } + + if (!StringUtils.hasText(body)) { + throw WxPayException.paramError("请求体不能为空"); + } + + // 验证必要的微信支付头部 + String signature = headers.get(WxPayConstants.Header.WECHATPAY_SIGNATURE); + String timestamp = headers.get(WxPayConstants.Header.WECHATPAY_TIMESTAMP); + String nonce = headers.get(WxPayConstants.Header.WECHATPAY_NONCE); + String serial = headers.get(WxPayConstants.Header.WECHATPAY_SERIAL); + + if (!StringUtils.hasText(signature)) { + throw WxPayException.paramError("微信支付签名不能为空"); + } + + if (!StringUtils.hasText(timestamp)) { + throw WxPayException.paramError("微信支付时间戳不能为空"); + } + + if (!StringUtils.hasText(nonce)) { + throw WxPayException.paramError("微信支付随机数不能为空"); + } + + if (!StringUtils.hasText(serial)) { + throw WxPayException.paramError("微信支付序列号不能为空"); + } + + log.debug("回调通知参数验证通过, 租户ID: {}", tenantId); + } + + /** + * 解析并验证回调通知 + */ + private Transaction parseAndVerifyNotification(Map headers, String body, Config wxPayConfig) + throws WxPayException { + + try { + // 构建请求参数 + RequestParam requestParam = new RequestParam.Builder() + .serialNumber(headers.get(WxPayConstants.Header.WECHATPAY_SERIAL)) + .nonce(headers.get(WxPayConstants.Header.WECHATPAY_NONCE)) + .signature(headers.get(WxPayConstants.Header.WECHATPAY_SIGNATURE)) + .timestamp(headers.get(WxPayConstants.Header.WECHATPAY_TIMESTAMP)) + .body(body) + .build(); + + // 创建通知解析器 + NotificationParser parser = new NotificationParser(wxPayConfig); + + // 解析并验证通知 + Transaction transaction = parser.parse(requestParam, Transaction.class); + + if (transaction == null) { + throw WxPayException.callbackError("解析回调通知失败:transaction为空", null); + } + + log.debug("回调通知解析成功, 订单号: {}, 交易状态: {}", + transaction.getOutTradeNo(), transaction.getTradeState()); + + return transaction; + + } catch (Exception e) { + if (e instanceof WxPayException) { + throw e; + } + throw WxPayException.signatureError("签名验证失败: " + e.getMessage()); + } + } + + /** + * 处理支付结果 + */ + private void processPaymentResult(Transaction transaction, Integer tenantId) throws WxPayException { + String outTradeNo = transaction.getOutTradeNo(); + String tradeState = transaction.getTradeState(); + + if (!StringUtils.hasText(outTradeNo)) { + throw WxPayException.paramError("商户订单号不能为空"); + } + + // 查询订单 + ShopOrder order = shopOrderService.getByOutTradeNo(outTradeNo); + if (order == null) { + throw WxPayException.orderStatusError("订单不存在: " + outTradeNo); + } + + // 验证租户ID + if (!tenantId.equals(order.getTenantId())) { + throw WxPayException.tenantConfigError("订单租户ID不匹配", tenantId); + } + + // 验证订单状态 - 使用Boolean类型的payStatus字段 + if (Boolean.TRUE.equals(order.getPayStatus())) { + log.info("订单已支付,跳过处理, 订单号: {}", outTradeNo); + return; + } + + // 根据交易状态处理 + switch (tradeState) { + case WxPayConstants.PayStatus.SUCCESS: + handlePaymentSuccess(order, transaction); + break; + case WxPayConstants.PayStatus.REFUND: + handlePaymentRefund(order, transaction); + break; + case WxPayConstants.PayStatus.CLOSED: + case WxPayConstants.PayStatus.REVOKED: + case WxPayConstants.PayStatus.PAYERROR: + handlePaymentFailed(order, transaction); + break; + default: + log.warn("未处理的交易状态: {}, 订单号: {}", tradeState, outTradeNo); + break; + } + } + + /** + * 处理支付成功 + */ + private void handlePaymentSuccess(ShopOrder order, Transaction transaction) throws WxPayException { + try { + // 验证金额 + validateAmount(order, transaction); + + // 更新订单状态 + order.setPayStatus(true); // 使用Boolean类型 + order.setTransactionId(transaction.getTransactionId()); + order.setPayTime(transaction.getSuccessTime()); + + // 使用专门的更新方法,会触发支付成功后的业务逻辑 + shopOrderService.updateByOutTradeNo(order); + + // 推送支付结果通知 + pushPaymentNotification(order, transaction); + + log.info("支付成功处理完成, 订单号: {}, 微信交易号: {}", + order.getOrderNo(), transaction.getTransactionId()); + + } catch (Exception e) { + throw WxPayException.callbackError("处理支付成功回调失败: " + e.getMessage(), e); + } + } + + /** + * 处理支付退款 + */ + private void handlePaymentRefund(ShopOrder order, Transaction transaction) throws WxPayException { + try { + log.info("处理支付退款, 订单号: {}, 微信交易号: {}", + order.getOrderNo(), transaction.getTransactionId()); + + // 这里可以添加退款相关的业务逻辑 + // 例如:更新订单状态、处理库存、发送通知等 + + } catch (Exception e) { + throw WxPayException.callbackError("处理支付退款回调失败: " + e.getMessage(), e); + } + } + + /** + * 处理支付失败 + */ + private void handlePaymentFailed(ShopOrder order, Transaction transaction) throws WxPayException { + try { + log.info("处理支付失败, 订单号: {}, 交易状态: {}", + order.getOrderNo(), transaction.getTradeState()); + + // 这里可以添加支付失败相关的业务逻辑 + // 例如:释放库存、发送通知等 + + } catch (Exception e) { + throw WxPayException.callbackError("处理支付失败回调失败: " + e.getMessage(), e); + } + } + + /** + * 验证支付金额 + */ + private void validateAmount(ShopOrder order, Transaction transaction) throws WxPayException { + if (transaction.getAmount() == null || transaction.getAmount().getTotal() == null) { + throw WxPayException.amountError("回调通知中金额信息为空"); + } + + // 将订单金额转换为分 + BigDecimal orderAmount = order.getMoney(); + if (orderAmount == null) { + throw WxPayException.amountError("订单金额为空"); + } + + int orderAmountFen = orderAmount.multiply(new BigDecimal(WxPayConstants.Config.AMOUNT_MULTIPLIER)).intValue(); + int callbackAmountFen = transaction.getAmount().getTotal(); + + if (orderAmountFen != callbackAmountFen) { + throw WxPayException.amountError( + String.format("订单金额不匹配,订单金额: %d分, 回调金额: %d分", + orderAmountFen, callbackAmountFen)); + } + + log.debug("金额验证通过, 订单号: {}, 金额: {}分", order.getOrderNo(), orderAmountFen); + } + + /** + * 推送支付结果通知 + */ + private void pushPaymentNotification(ShopOrder order, Transaction transaction) { + try { + // 使用现有的推送工具 + requestUtil.pushWxPayNotify(transaction, null); + log.debug("支付结果通知推送成功, 订单号: {}", order.getOrderNo()); + } catch (Exception e) { + log.warn("支付结果通知推送失败, 订单号: {}, 错误: {}", order.getOrderNo(), e.getMessage()); + // 推送失败不影响主流程,只记录日志 + } + } +} diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java index 3a6082e..3fc0e43 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java @@ -144,7 +144,9 @@ public class ShopOrderServiceImpl extends ServiceImpl