package com.xuekairui.payment.service; import cn.hutool.core.util.IdUtil; import com.alipay.api.AlipayClient; import com.alipay.api.domain.AlipayTradePrecreateModel; import com.alipay.api.request.AlipayTradePrecreateRequest; import com.alipay.api.response.AlipayTradePrecreateResponse; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; import com.github.binarywang.wxpay.service.WxPayService; import com.xuekairui.common.BusinessException; import com.xuekairui.common.ErrorCode; import com.xuekairui.payment.config.PaymentProperties; import com.xuekairui.payment.dto.CreateOrderRequest; import com.xuekairui.payment.dto.OrderResponse; import com.xuekairui.payment.dto.PaymentResultResponse; import com.xuekairui.payment.entity.PaymentOrder; import com.xuekairui.payment.entity.PaymentPlan; import com.xuekairui.payment.enums.PaymentChannel; import com.xuekairui.payment.enums.PaymentStatus; import com.xuekairui.payment.mapper.PaymentOrderMapper; import com.xuekairui.user.entity.UserMembership; import com.xuekairui.user.enums.MembershipLevel; import com.xuekairui.user.service.MembershipService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; /** * 支付订单服务 */ @Slf4j @Service @RequiredArgsConstructor public class PaymentOrderService { private final PaymentOrderMapper paymentOrderMapper; private final PaymentPlanService paymentPlanService; private final QRCodeService qrCodeService; private final MembershipService membershipService; private final PaymentProperties paymentProperties; private final ObjectProvider alipayClientProvider; private final ObjectProvider wxPayServiceProvider; /** * 创建支付订单 */ @Transactional public OrderResponse createOrder(Long userId, CreateOrderRequest request) { // 校验渠道 PaymentChannel channel = PaymentChannel.fromCode(request.getChannel()); if (channel == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_SUPPORT, "支持的渠道:WECHAT, ALIPAY"); } // 获取方案 PaymentPlan plan = paymentPlanService.getEnabledPlanById(request.getPlanId()); // 生成订单号 String orderNo = "OP" + IdUtil.getSnowflake(1, 1).nextIdStr(); // 过期时间 LocalDateTime expireTime = LocalDateTime.now() .plusMinutes(paymentProperties.getOrderExpireMinutes()); // 生成支付二维码/链接 String qrContent; String payUrl = null; if (paymentProperties.isMock()) { qrContent = "MOCK_PAY:" + orderNo; } else { // 生产模式:调用对应渠道预下单接口获取支付链接 PrepayResult prepayResult = createPrepay(channel, orderNo, plan.getPrice(), plan.getPlanName(), plan.getDescription()); qrContent = prepayResult.getQrContent(); payUrl = prepayResult.getPayUrl(); } // 仅在有二维码内容时生成二维码图片(微信Native/支付宝precreate均返回qrContent,由后端统一生成base64) String qrCodeBase64 = (qrContent != null && !qrContent.isEmpty()) ? qrCodeService.generateBase64(qrContent) : null; // 保存订单 PaymentOrder order = PaymentOrder.builder() .orderNo(orderNo) .userId(userId) .planId(plan.getId()) .amount(plan.getPrice()) .channel(request.getChannel()) .status(PaymentStatus.PENDING.name()) .qrCodeUrl(qrContent) .expireTime(expireTime) .build(); paymentOrderMapper.insert(order); log.info("创建支付订单: {} 用户={} 方案={} 金额={}元 渠道={}", orderNo, userId, plan.getPlanName(), plan.getPrice(), channel.getDisplayName()); return OrderResponse.builder() .orderNo(orderNo) .planName(plan.getPlanName()) .amount(order.getAmount()) .channel(order.getChannel()) .status(order.getStatus()) .qrCodeBase64(qrCodeBase64) .payUrl(payUrl) .expireTime(order.getExpireTime()) .expireHint("请在" + paymentProperties.getOrderExpireMinutes() + "分钟内完成付款,超时订单将自动过期") .createTime(order.getCreateTime()) .build(); } /** * 查询单个订单(自动检查过期) */ public OrderResponse queryOrder(String orderNo) { PaymentOrder order = paymentOrderMapper.selectByOrderNo(orderNo); if (order == null) { throw new BusinessException(ErrorCode.PAY_ORDER_NOT_FOUND); } checkExpired(order); return toResponse(order); } /** * 查询用户的所有订单 */ public List listUserOrders(Long userId) { List orders = paymentOrderMapper.selectByUserId(userId); orders.forEach(this::checkExpired); return orders.stream().map(this::toResponse).collect(Collectors.toList()); } /** * 手动确认支付(开发/测试用) *

* mock模式:模拟支付,自动生成交易号 * 真实支付模式:手动确认支付(用于测试回调流程,不实际扣款) *

*/ @Transactional public PaymentResultResponse confirmPay(String orderNo) { PaymentOrder order = paymentOrderMapper.selectByOrderNo(orderNo); if (order == null) { throw new BusinessException(ErrorCode.PAY_ORDER_NOT_FOUND); } if (PaymentStatus.PAID.name().equals(order.getStatus())) { throw new BusinessException(ErrorCode.PAY_FAILED, "该订单已支付"); } if (PaymentStatus.EXPIRED.name().equals(order.getStatus())) { throw new BusinessException(ErrorCode.PAY_ORDER_EXPIRED); } String prefix = paymentProperties.isMock() ? "MOCK_" : "MANUAL_"; String tradeNo = prefix + IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase(); return processPaidCallback(orderNo, tradeNo); } /** * 支付回调处理(微信/支付宝回调统一入口) * @param orderNo 订单号 * @param tradeNo 第三方交易号 * @return 支付结果 */ @Transactional public PaymentResultResponse processPaidCallback(String orderNo, String tradeNo) { PaymentOrder order = paymentOrderMapper.selectByOrderNo(orderNo); if (order == null) { throw new BusinessException(ErrorCode.PAY_ORDER_NOT_FOUND); } // 幂等:已支付直接返回 if (PaymentStatus.PAID.name().equals(order.getStatus())) { PaymentPlan plan = paymentPlanService.getPlanById(order.getPlanId()); return buildResultResponse(order, plan); } if (PaymentStatus.EXPIRED.name().equals(order.getStatus())) { throw new BusinessException(ErrorCode.PAY_ORDER_EXPIRED); } return handlePaymentSuccess(order, tradeNo); } /** * 支付成功处理 */ private PaymentResultResponse handlePaymentSuccess(PaymentOrder order, String tradeNo) { PaymentPlan plan = paymentPlanService.getPlanById(order.getPlanId()); MembershipLevel level = MembershipLevel.fromCode(plan.getMembershipLevel()); // 更新订单状态 LocalDateTime now = LocalDateTime.now(); order.setStatus(PaymentStatus.PAID.name()); order.setPaidTime(now); order.setTradeNo(tradeNo); paymentOrderMapper.updateById(order); // 激活会员(叠加模式:保留低级别剩余时长) UserMembership membership = membershipService.activateFromPayment( order.getUserId(), level, plan.getDurationDays(), order.getOrderNo()); // 关联会员记录 order.setMembershipRecordId(membership.getId()); paymentOrderMapper.updateById(order); log.info("支付成功: {} 用户={} 方案={} 金额={}元 会员={} 交易号={}", order.getOrderNo(), order.getUserId(), plan.getPlanName(), order.getAmount(), level.getCode(), tradeNo); return buildResultResponse(order, plan); } private PaymentResultResponse buildResultResponse(PaymentOrder order, PaymentPlan plan) { return PaymentResultResponse.builder() .orderNo(order.getOrderNo()) .status(PaymentStatus.PAID.name()) .amount(order.getAmount()) .planName(plan.getPlanName()) .membershipLevel(plan.getMembershipLevel()) .paidTime(order.getPaidTime()) .membershipStartDate(null) .membershipEndDate(null) .build(); } /** * 检查并自动过期订单 */ private void checkExpired(PaymentOrder order) { if (PaymentStatus.PENDING.name().equals(order.getStatus()) && LocalDateTime.now().isAfter(order.getExpireTime())) { order.setStatus(PaymentStatus.EXPIRED.name()); paymentOrderMapper.updateById(order); log.info("订单 {} 已自动过期", order.getOrderNo()); } } /** * 定时清理过期订单(每分钟) */ @Scheduled(fixedDelay = 60000) @Transactional public void cleanupExpiredOrders() { List expiredOrders = paymentOrderMapper .selectExpiredPending(LocalDateTime.now()); for (PaymentOrder order : expiredOrders) { order.setStatus(PaymentStatus.EXPIRED.name()); paymentOrderMapper.updateById(order); } if (!expiredOrders.isEmpty()) { log.info("定时清理过期订单: {} 笔", expiredOrders.size()); } } /** * 生产模式预下单结果 */ @lombok.Data @lombok.Builder @lombok.NoArgsConstructor @lombok.AllArgsConstructor private static class PrepayResult { /** 二维码内容(用于本地生成二维码图片) */ private String qrContent; /** 支付跳转URL(前端可直接跳转,H5支付场景) */ private String payUrl; } /** * 调用第三方支付预下单接口 * *

上线时替换为真实SDK调用: *

    *
  • 微信:使用 weixin-java-pay 的 WxPayService.createOrder() 获取 code_url
  • *
  • 支付宝:使用 alipay-sdk-java 的 AlipayClient.pageExecute() 获取支付跳转链接
  • *
* * @param channel 支付渠道 * @param orderNo 订单号 * @param amount 金额 * @param subject 商品标题 * @param body 商品详细描述 * @return 预下单结果 */ private PrepayResult createPrepay(PaymentChannel channel, String orderNo, java.math.BigDecimal amount, String subject, String body) { return switch (channel) { case WECHAT -> createWechatPrepay(orderNo, amount, subject); case ALIPAY -> createAlipayPrepay(orderNo, amount, subject, body); }; } /** * 微信Native支付预下单(API v3) * *

使用 WxJava SDK 调用微信支付 v3 Native下单接口, * 返回 codeUrl(二维码链接),前端可直接生成二维码展示。 * 用户扫码后在微信App中完成付款,微信异步回调 notifyUrl。 */ private PrepayResult createWechatPrepay(String orderNo, java.math.BigDecimal amount, String subject) { PaymentProperties.Wechat wc = paymentProperties.getWechat(); if (wc.getAppId() == null || wc.getMchId() == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "微信支付未配置,请检查 payment.wechat 参数"); } WxPayService wxPayService = wxPayServiceProvider.getIfAvailable(); if (wxPayService == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "WxPayService未初始化,请确认 payment.mock=false 且已配置微信支付参数"); } try { WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); request.setOutTradeNo(orderNo); request.setDescription(subject); request.setNotifyUrl(wc.getNotifyUrl()); // 金额单位为分 WxPayUnifiedOrderV3Request.Amount amountObj = new WxPayUnifiedOrderV3Request.Amount(); amountObj.setTotal(amount.movePointRight(2).intValue()); amountObj.setCurrency("CNY"); request.setAmount(amountObj); // 过期时间(RFC3339格式) String timeExpire = java.time.format.DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX") .format(java.time.ZonedDateTime.now() .plusMinutes(paymentProperties.getOrderExpireMinutes())); request.setTimeExpire(timeExpire); // WxJava 4.7.0: NATIVE 模式下 createOrderV3 直接返回 code_url 字符串 String codeUrl = wxPayService.createOrderV3(TradeTypeEnum.NATIVE, request); log.info("微信预下单成功: orderNo={}, amount={}, codeUrl={}", orderNo, amount, codeUrl); return PrepayResult.builder() .qrContent(codeUrl) .payUrl(codeUrl) .build(); } catch (BusinessException e) { throw e; } catch (Exception e) { log.error("微信预下单异常: orderNo={}", orderNo, e); throw new BusinessException(ErrorCode.PAY_FAILED, "微信下单异常: " + e.getMessage()); } } /** * 支付宝当面付预下单(alipay.trade.precreate) * *

调用支付宝「统一收单线下交易预创建」接口,生成支付码串(qr_code), * 后端将其转换为二维码 base64 图片,前端在自定义弹窗中展示。 * 用户用支付宝 App 扫码完成付款,支付宝异步回调 notifyUrl 通知支付结果。 * * @see 统一收单线下交易预创建 */ private PrepayResult createAlipayPrepay(String orderNo, java.math.BigDecimal amount, String subject, String body) { PaymentProperties.Alipay al = paymentProperties.getAlipay(); if (al.getAppId() == null || al.getAppPrivateKey() == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "支付宝未配置,请检查 payment.alipay 参数"); } AlipayClient alipayClient = alipayClientProvider.getIfAvailable(); if (alipayClient == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "AlipayClient未初始化,请确认 payment.mock=false 且已配置支付宝参数"); } AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); request.setNotifyUrl(al.getNotifyUrl()); AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); model.setOutTradeNo(orderNo); model.setTotalAmount(amount.toPlainString()); model.setSubject(subject); if (body != null && !body.isBlank()) { model.setBody(body); } model.setProductCode("QR_CODE_OFFLINE"); model.setTimeoutExpress(paymentProperties.getOrderExpireMinutes() + "m"); request.setBizModel(model); try { // 公钥模式用 execute(),证书模式用 certificateExecute() boolean publicKeyMode = al.getAlipayPublicKey() != null && !al.getAlipayPublicKey().isBlank(); AlipayTradePrecreateResponse response = publicKeyMode ? alipayClient.execute(request) : alipayClient.certificateExecute(request); if (!response.isSuccess()) { log.error("支付宝预下单失败: orderNo={}, code={}, msg={}, subCode={}, subMsg={}", orderNo, response.getCode(), response.getMsg(), response.getSubCode(), response.getSubMsg()); throw new BusinessException(ErrorCode.PAY_FAILED, "支付宝下单失败: " + response.getSubMsg()); } String qrCode = response.getQrCode(); log.info("支付宝预下单成功: orderNo={}, amount={}, qrCode={}", orderNo, amount, qrCode); // 返回 qr_code 支付码串,后端生成base64二维码,前端在自定义弹窗中展示 return PrepayResult.builder() .qrContent(qrCode) .payUrl(qrCode) .build(); } catch (BusinessException e) { throw e; } catch (Exception e) { log.error("支付宝预下单异常: orderNo={}", orderNo, e); throw new BusinessException(ErrorCode.PAY_FAILED, "支付宝下单异常: " + e.getMessage()); } } private OrderResponse toResponse(PaymentOrder order) { String hint = PaymentStatus.PENDING.name().equals(order.getStatus()) ? "请在" + paymentProperties.getOrderExpireMinutes() + "分钟内完成付款,超时订单将自动过期" : null; return OrderResponse.builder() .orderNo(order.getOrderNo()) .planName(null) // 由调用方补充 .amount(order.getAmount()) .channel(order.getChannel()) .status(order.getStatus()) .qrCodeBase64(null) .payUrl(null) // 查询场景不再返回,仅创建时提供 .expireTime(order.getExpireTime()) .expireHint(hint) .createTime(order.getCreateTime()) .build(); } }