| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- 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.dto.SwitchChannelRequest;
- import com.xuekairui.payment.entity.PaymentOrder;
- import com.xuekairui.payment.entity.PaymentOrderExtraPayment;
- import com.xuekairui.payment.entity.PaymentPlan;
- import com.xuekairui.payment.enums.OrderType;
- import com.xuekairui.payment.enums.PaymentChannel;
- import com.xuekairui.payment.enums.PaymentStatus;
- import com.xuekairui.payment.mapper.PaymentOrderExtraPaymentMapper;
- 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.beans.factory.annotation.Qualifier;
- 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 PaymentOrderExtraPaymentMapper extraPaymentMapper;
- private final PaymentPlanService paymentPlanService;
- private final QRCodeService qrCodeService;
- private final MembershipService membershipService;
- private final PaymentProperties paymentProperties;
- @Qualifier("alipayClient")
- private final ObjectProvider<AlipayClient> alipayClientProvider;
- private final ObjectProvider<WxPayService> wxPayServiceProvider;
- private final PaymentAgreementService agreementService;
- private final PaymentRefundService refundService;
- /**
- * 创建支付订单
- */
- @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());
- // 生成支付二维码/链接
- QrCodeResult qrCodeResult = generateQrCode(channel, orderNo, plan.getPrice(), plan.getPlanName(), plan.getDescription());
- String qrContent = qrCodeResult.getQrContent();
- String payUrl = qrCodeResult.getPayUrl();
- String qrCodeBase64 = qrCodeResult.getQrCodeBase64();
- // 保存订单
- 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();
- }
- /**
- * 切换支付渠道并重新生成二维码
- * <p>
- * 用户创建订单后,可在订单详情页切换为其他支付渠道,后端根据新渠道重新调用预下单接口生成二维码。
- */
- @Transactional
- public OrderResponse switchChannel(Long userId, String orderNo, SwitchChannelRequest request) {
- PaymentChannel newChannel = PaymentChannel.fromCode(request.getChannel());
- if (newChannel == null) {
- throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_SUPPORT,
- "支持的渠道:WECHAT, ALIPAY");
- }
- PaymentOrder order = paymentOrderMapper.selectByOrderNo(orderNo);
- if (order == null) {
- throw new BusinessException(ErrorCode.PAY_ORDER_NOT_FOUND);
- }
- if (!order.getUserId().equals(userId)) {
- throw new BusinessException(ErrorCode.FORBIDDEN, "无权操作该订单");
- }
- checkExpired(order);
- if (!PaymentStatus.PENDING.name().equals(order.getStatus())) {
- throw new BusinessException(ErrorCode.PAY_FAILED, "订单已支付或已过期,无法切换渠道");
- }
- PaymentPlan plan = paymentPlanService.getPlanById(order.getPlanId());
- // 不关闭原渠道订单:15分钟内允许用户在多个渠道间重复切换,原渠道二维码仍可被扫描支付
- // 以第一个支付成功渠道为准,后续支付记录到 t_payment_order_extra_payment
- QrCodeResult qrCodeResult = generateQrCode(newChannel, orderNo, order.getAmount(),
- plan.getPlanName(), plan.getDescription());
- // 更新订单渠道和二维码
- order.setChannel(request.getChannel());
- order.setQrCodeUrl(qrCodeResult.getQrContent());
- paymentOrderMapper.updateById(order);
- log.info("切换支付渠道: orderNo={} 新渠道={}", orderNo, newChannel.getDisplayName());
- return OrderResponse.builder()
- .orderNo(order.getOrderNo())
- .planName(plan.getPlanName())
- .amount(order.getAmount())
- .channel(order.getChannel())
- .status(order.getStatus())
- .qrCodeBase64(qrCodeResult.getQrCodeBase64())
- .payUrl(qrCodeResult.getPayUrl())
- .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<OrderResponse> listUserOrders(Long userId) {
- List<PaymentOrder> orders = paymentOrderMapper.selectByUserId(userId);
- orders.forEach(this::checkExpired);
- return orders.stream().map(this::toResponse).collect(Collectors.toList());
- }
- /**
- * 手动确认支付(开发/测试用)
- * <p>
- * mock模式:模拟支付,自动生成交易号
- * 真实支付模式:手动确认支付(用于测试回调流程,不实际扣款)
- * </p>
- */
- @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, order.getChannel());
- }
- /**
- * 支付回调处理(微信/支付宝回调统一入口)
- *
- * @param orderNo 订单号
- * @param tradeNo 第三方交易号
- * @param paidChannel 实际支付渠道(WECHAT/ALIPAY)
- * @return 支付结果
- */
- @Transactional
- public PaymentResultResponse processPaidCallback(String orderNo, String tradeNo, String paidChannel) {
- PaymentOrder order = paymentOrderMapper.selectByOrderNo(orderNo);
- if (order == null) {
- throw new BusinessException(ErrorCode.PAY_ORDER_NOT_FOUND);
- }
- // 已支付:说明是切换渠道后另一个渠道也回调了,记录重复支付但不重复激活会员
- if (PaymentStatus.PAID.name().equals(order.getStatus())) {
- return handleDuplicatePayment(order, tradeNo, paidChannel);
- }
- if (PaymentStatus.EXPIRED.name().equals(order.getStatus())) {
- throw new BusinessException(ErrorCode.PAY_ORDER_EXPIRED);
- }
- return handlePaymentSuccess(order, tradeNo, paidChannel);
- }
- /**
- * 支付成功处理(第一次支付)
- */
- private PaymentResultResponse handlePaymentSuccess(PaymentOrder order, String tradeNo, String paidChannel) {
- 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.setPaidChannel(paidChannel);
- order.setTradeNo(tradeNo);
- paymentOrderMapper.updateById(order);
- // 激活会员(叠加模式:保留低级别剩余时长)
- UserMembership membership = membershipService.activateFromPayment(
- order.getUserId(), level, plan.getDurationDays(), order.getOrderNo());
- // 关联会员记录
- order.setMembershipRecordId(membership.getId());
- paymentOrderMapper.updateById(order);
- // 自动续费扣款订单:推进协议下次扣款时间
- if (OrderType.AUTO_DEDUCT.getCode().equals(order.getOrderType())) {
- try {
- agreementService.onDeductSuccess(order);
- } catch (Exception e) {
- log.warn("推进协议扣款时间失败(不影响会员激活): orderNo={}, error={}", order.getOrderNo(), e.getMessage());
- }
- }
- log.info("支付成功: {} 用户={} 方案={} 金额={}元 会员={} 交易号={} 类型={}",
- order.getOrderNo(), order.getUserId(),
- plan.getPlanName(), order.getAmount(), level.getCode(), tradeNo,
- order.getOrderType());
- 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();
- }
- /**
- * 处理重复支付(切换渠道后多个渠道都支付成功)
- * <p>
- * 以第一次支付为准,后续支付记录到 {@code t_payment_order_extra_payment} 表,
- * 不重复激活会员,并自动发起原路退款。
- */
- private PaymentResultResponse handleDuplicatePayment(PaymentOrder order, String tradeNo, String paidChannel) {
- PaymentPlan plan = paymentPlanService.getPlanById(order.getPlanId());
- // 幂等:同一交易号不重复记录
- Long existCount = extraPaymentMapper.selectCount(
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PaymentOrderExtraPayment>()
- .eq(PaymentOrderExtraPayment::getOrderNo, order.getOrderNo())
- .eq(PaymentOrderExtraPayment::getTradeNo, tradeNo));
- if (existCount == null || existCount == 0) {
- PaymentOrderExtraPayment extra = PaymentOrderExtraPayment.builder()
- .orderNo(order.getOrderNo())
- .channel(paidChannel)
- .tradeNo(tradeNo)
- .amount(order.getAmount())
- .paidTime(LocalDateTime.now())
- .build();
- extraPaymentMapper.insert(extra);
- log.warn("订单重复支付已记录: orderNo={}, paidChannel={}, tradeNo={}, 主交易渠道={}",
- order.getOrderNo(), paidChannel, tradeNo, order.getPaidChannel());
- // 自动发起原路退款
- doRefund(extra);
- }
- return buildResultResponse(order, plan);
- }
- /**
- * 对重复支付记录发起原路退款,并更新退款状态
- */
- private void doRefund(PaymentOrderExtraPayment extra) {
- try {
- String refundTradeNo = refundService.refund(
- extra.getChannel(), extra.getOrderNo(), extra.getTradeNo(), extra.getAmount());
- extra.setRefundStatus("SUCCESS");
- extra.setRefundTradeNo(refundTradeNo);
- extra.setRefundTime(LocalDateTime.now());
- extraPaymentMapper.updateById(extra);
- log.info("重复支付已自动退款: orderNo={}, channel={}, tradeNo={}, refundTradeNo={}",
- extra.getOrderNo(), extra.getChannel(), extra.getTradeNo(), refundTradeNo);
- } catch (Exception e) {
- extra.setRefundStatus("FAILED");
- extra.setRefundFailReason(e.getMessage());
- extraPaymentMapper.updateById(extra);
- log.error("重复支付自动退款失败: orderNo={}, channel={}, tradeNo={}, error={}",
- extra.getOrderNo(), extra.getChannel(), extra.getTradeNo(), e.getMessage(), e);
- }
- }
- /**
- * 检查并自动过期订单
- */
- 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<PaymentOrder> 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;
- }
- /**
- * 二维码生成结果(含 base64 图片)
- */
- @lombok.Data
- @lombok.Builder
- @lombok.NoArgsConstructor
- @lombok.AllArgsConstructor
- private static class QrCodeResult {
- private String qrContent;
- private String payUrl;
- private String qrCodeBase64;
- }
- /**
- * 统一生成支付二维码
- */
- private QrCodeResult generateQrCode(PaymentChannel channel, String orderNo,
- java.math.BigDecimal amount, String subject, String body) {
- String qrContent;
- String payUrl = null;
- if (paymentProperties.isMock()) {
- qrContent = "MOCK_PAY:" + orderNo;
- } else {
- PrepayResult prepayResult = createPrepay(channel, orderNo, amount, subject, body);
- qrContent = prepayResult.getQrContent();
- payUrl = prepayResult.getPayUrl();
- }
- String qrCodeBase64 = (qrContent != null && !qrContent.isEmpty())
- ? qrCodeService.generateBase64(qrContent) : null;
- return QrCodeResult.builder()
- .qrContent(qrContent)
- .payUrl(payUrl)
- .qrCodeBase64(qrCodeBase64)
- .build();
- }
- /**
- * 调用第三方支付预下单接口
- *
- * <p>上线时替换为真实SDK调用:
- * <ul>
- * <li>微信:使用 weixin-java-pay 的 WxPayService.createOrder() 获取 code_url</li>
- * <li>支付宝:使用 alipay-sdk-java 的 AlipayClient.pageExecute() 获取支付跳转链接</li>
- * </ul>
- *
- * @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, body);
- case ALIPAY -> createAlipayPrepay(orderNo, amount, subject, body);
- };
- }
- /**
- * 构建微信支付 description,将商品标题与详细描述合并。
- * 微信 Native 下单的 description 字段上限为 127 字节(UTF-8),超出时自动截断。
- */
- private String buildWechatDescription(String subject, String body) {
- if (body == null || body.isBlank()) {
- return truncateUtf8(subject, 127);
- }
- String combined = subject.trim() + " - " + body.trim();
- return truncateUtf8(combined, 127);
- }
- /**
- * 按 UTF-8 字节长度截断字符串,避免截断到多字节字符中间。
- */
- private String truncateUtf8(String value, int maxBytes) {
- if (value == null) {
- return "";
- }
- byte[] bytes = value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
- if (bytes.length <= maxBytes) {
- return value;
- }
- int cut = maxBytes;
- // 避免截断到多字节字符中间:向前回退直到是合法 UTF-8 起始字节
- while (cut > 0 && (bytes[cut] & 0xC0) == 0x80) {
- cut--;
- }
- return new String(bytes, 0, cut, java.nio.charset.StandardCharsets.UTF_8);
- }
- /**
- * 微信Native支付预下单(API v3)
- *
- * <p>使用 WxJava SDK 调用微信支付 v3 Native下单接口,
- * 返回 codeUrl(二维码链接),前端可直接生成二维码展示。
- * 用户扫码后在微信App中完成付款,微信异步回调 notifyUrl。
- */
- private PrepayResult createWechatPrepay(String orderNo, java.math.BigDecimal amount, String subject, String body) {
- 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(buildWechatDescription(subject, body));
- 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)
- *
- * <p>调用支付宝「统一收单线下交易预创建」接口,生成支付码串(qr_code),
- * 后端将其转换为二维码 base64 图片,前端在自定义弹窗中展示。
- * 用户用支付宝 App 扫码完成付款,支付宝异步回调 notifyUrl 通知支付结果。
- *
- * @see <a href="https://opendocs.alipay.com/open/02ekfh">统一收单线下交易预创建</a>
- */
- 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) {
- PaymentPlan plan = paymentPlanService.getPlanById(order.getPlanId());
- String hint = PaymentStatus.PENDING.name().equals(order.getStatus())
- ? "请在" + paymentProperties.getOrderExpireMinutes() + "分钟内完成付款,超时订单将自动过期" : null;
- // 待支付订单补充二维码与支付链接,支持重新查询后继续支付
- String qrCodeBase64 = null;
- String payUrl = null;
- if (PaymentStatus.PENDING.name().equals(order.getStatus())
- && order.getQrCodeUrl() != null && !order.getQrCodeUrl().isBlank()) {
- qrCodeBase64 = qrCodeService.generateBase64(order.getQrCodeUrl());
- payUrl = order.getQrCodeUrl();
- }
- return OrderResponse.builder()
- .orderNo(order.getOrderNo())
- .planName(plan != null ? plan.getPlanName() : null)
- .amount(order.getAmount())
- .channel(order.getChannel())
- .status(order.getStatus())
- .qrCodeBase64(qrCodeBase64)
- .payUrl(payUrl)
- .expireTime(order.getExpireTime())
- .expireHint(hint)
- .createTime(order.getCreateTime())
- .build();
- }
- }
|