feat: 接入支付宝沙箱电脑网站支付功能
1. 新增支付宝支付相关接口、实体类与配置类 2. 后端新增支付宝服务实现与支付回调接口 3. 前端替换模拟支付为跳转支付宝支付流程 4. 优化订单支付提示与支付完成跳转逻辑 5. 删除本地Maven配置文件
This commit is contained in:
parent
f3fac48ad6
commit
004745fd65
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<localRepository>C:\Users\OrBit-0297\.m2\repository</localRepository>
|
||||
</settings>
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
<s3.version>2.44.4</s3.version>
|
||||
<minio.version>9.0.0</minio.version>
|
||||
<aliyun-oss.version>3.18.5</aliyun-oss.version>
|
||||
<alipay.version>4.40.918.ALL</alipay.version>
|
||||
<tencent-cos.version>5.6.269</tencent-cos.version>
|
||||
<weixin-java.version>4.8.3.B</weixin-java.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
|
|
@ -142,6 +143,13 @@
|
|||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>${alipay.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.snack.server.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 支付宝沙箱配置
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "alipay")
|
||||
public class AlipayProperties {
|
||||
|
||||
/** 支付宝开放平台应用 ID */
|
||||
private String appId;
|
||||
|
||||
/** 应用私钥(PKCS8) */
|
||||
private String appPrivateKey;
|
||||
|
||||
/** 支付宝公钥 */
|
||||
private String alipayPublicKey;
|
||||
|
||||
/** 异步通知地址(可只配域名,代码自动拼接 /api/payment/alipay/notify) */
|
||||
private String notifyUrl;
|
||||
|
||||
/** 支付完成同步返回地址(完整 URL) */
|
||||
private String returnUrl;
|
||||
|
||||
/** 网关地址(沙箱:https://openapi.alipaydev.com/gateway.do) */
|
||||
private String gateway = "https://openapi.alipaydev.com/gateway.do";
|
||||
|
||||
/** 签名算法 */
|
||||
private String signType = "RSA2";
|
||||
|
||||
/** 字符集 */
|
||||
private String charset = "utf-8";
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.snack.server.common.Result;
|
|||
import com.snack.server.module.order.dto.req.OrderPageReq;
|
||||
import com.snack.server.module.order.dto.req.OrderSubmitReq;
|
||||
import com.snack.server.module.order.entity.Order;
|
||||
import com.snack.server.module.order.service.AlipayService;
|
||||
import com.snack.server.module.order.service.OrderService;
|
||||
import com.snack.server.module.order.vo.OrderDetailVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
|
@ -27,6 +28,7 @@ import java.util.Map;
|
|||
public class OrderController {
|
||||
|
||||
private final OrderService orderService;
|
||||
private final AlipayService alipayService;
|
||||
|
||||
@Operation(summary = "提交订单")
|
||||
@SaCheckLogin
|
||||
|
|
@ -58,6 +60,13 @@ public class OrderController {
|
|||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取支付宝电脑网站支付链接")
|
||||
@SaCheckLogin
|
||||
@GetMapping("/{id}/pay-url")
|
||||
public Result<Map<String, String>> payUrl(@Parameter(description = "订单 ID") @PathVariable Long id) {
|
||||
return Result.ok(Map.of("payUrl", alipayService.createPagePayUrl(id)));
|
||||
}
|
||||
|
||||
@Operation(summary = "取消订单")
|
||||
@SaCheckLogin
|
||||
@PostMapping("/{id}/cancel")
|
||||
|
|
|
|||
|
|
@ -1,77 +1,59 @@
|
|||
package com.snack.server.module.order.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.snack.server.common.Result;
|
||||
import com.snack.server.module.order.service.OrderService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import com.snack.server.module.order.service.AlipayService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付宝 / 微信支付回调(沙箱版)
|
||||
* 支付宝回调(电脑网站支付)
|
||||
*
|
||||
* 真实生产:
|
||||
* 1. 支付宝 POST 异步通知到 /api/payment/alipay/notify
|
||||
* 2. 校验 sign 签名
|
||||
* 3. 解析 out_trade_no(订单号)、trade_no(支付宝交易号)
|
||||
* 4. 调用 OrderService.handlePaySuccess()
|
||||
* 5. 返回 "success" / "fail" 给支付宝
|
||||
*
|
||||
* 沙箱测试(手动调用):
|
||||
* POST /api/payment/alipay/notify
|
||||
* ?orderId=1&payChannel=alipay&payTradeNo=20260601xxxx
|
||||
* 1. 支付成功后支付宝 POST 异步通知 /api/payment/alipay/notify
|
||||
* 2. 服务端验签后更新订单状态,返回 "success" 给支付宝
|
||||
* 3. 用户支付完成同步跳转 /api/payment/alipay/return,再重定向到前端
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "支付回调(沙箱测试)")
|
||||
@RestController
|
||||
@RequestMapping("/api/payment")
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentNotifyController {
|
||||
|
||||
private final OrderService orderService;
|
||||
private final AlipayService alipayService;
|
||||
|
||||
/**
|
||||
* 支付宝支付成功异步通知
|
||||
* 支付宝支付成功异步通知(必须返回纯文本 success / fail)
|
||||
*/
|
||||
@Operation(summary = "支付宝支付成功回调(沙箱测试用)")
|
||||
@PostMapping("/alipay/notify")
|
||||
public Result<String> alipayNotify(
|
||||
@RequestParam Long orderId,
|
||||
@RequestParam(required = false, defaultValue = "alipay") String payChannel,
|
||||
@RequestParam(required = false) String payTradeNo) {
|
||||
if (orderId == null) {
|
||||
return Result.fail(400, "orderId 不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(payTradeNo)) {
|
||||
payTradeNo = "ALIPAY-MOCK-" + System.currentTimeMillis();
|
||||
}
|
||||
log.info("收到支付宝支付回调 orderId={} payTradeNo={}", orderId, payTradeNo);
|
||||
boolean ok = orderService.handlePaySuccess(orderId, payChannel, payTradeNo);
|
||||
// 支付宝要求返回纯文本 "success" / "fail",用 body 输出
|
||||
return ok ? Result.ok("success") : Result.fail("fail");
|
||||
public String alipayNotify(@RequestParam Map<String, String> params) {
|
||||
log.info("收到支付宝支付异步通知 out_trade_no={} trade_status={}",
|
||||
params.get("out_trade_no"), params.get("trade_status"));
|
||||
return alipayService.handleNotify(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝退款成功异步通知
|
||||
* 支付宝退款异步通知(必须返回纯文本 success / fail)
|
||||
*/
|
||||
@Operation(summary = "支付宝退款成功回调(沙箱测试用)")
|
||||
@PostMapping("/alipay/refund/notify")
|
||||
public Result<String> alipayRefundNotify(
|
||||
@RequestParam Long orderId,
|
||||
@RequestParam(required = false) String refundTradeNo) {
|
||||
if (orderId == null) {
|
||||
return Result.fail(400, "orderId 不能为空");
|
||||
public String alipayRefundNotify(@RequestParam Map<String, String> params) {
|
||||
log.info("收到支付宝退款异步通知 out_trade_no={}", params.get("out_trade_no"));
|
||||
return alipayService.handleRefundNotify(params);
|
||||
}
|
||||
if (StrUtil.isBlank(refundTradeNo)) {
|
||||
refundTradeNo = "RF-MOCK-" + System.currentTimeMillis();
|
||||
}
|
||||
log.info("收到支付宝退款回调 orderId={} refundTradeNo={}", orderId, refundTradeNo);
|
||||
boolean ok = orderService.handleRefundSuccess(orderId, refundTradeNo);
|
||||
return ok ? Result.ok("success") : Result.fail("fail");
|
||||
|
||||
/**
|
||||
* 支付宝电脑网站支付同步返回(用户支付后从支付宝跳回)
|
||||
*/
|
||||
@GetMapping("/alipay/return")
|
||||
public void alipayReturn(@RequestParam Map<String, String> params,
|
||||
HttpServletResponse response) throws IOException {
|
||||
String redirectUrl = alipayService.handleReturn(params);
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.snack.server.module.order.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付宝支付服务(电脑网站支付,沙箱)
|
||||
*/
|
||||
public interface AlipayService {
|
||||
|
||||
/**
|
||||
* 生成电脑网站支付跳转链接
|
||||
*
|
||||
* @param orderId 订单 ID
|
||||
* @return 支付宝支付页面 URL
|
||||
*/
|
||||
String createPagePayUrl(Long orderId);
|
||||
|
||||
/**
|
||||
* 处理支付宝支付异步通知
|
||||
*
|
||||
* @param params 支付宝回调参数
|
||||
* @return 支付宝要求返回的 "success" / "fail"
|
||||
*/
|
||||
String handleNotify(Map<String, String> params);
|
||||
|
||||
/**
|
||||
* 处理支付宝支付同步返回(验签后跳转前端)
|
||||
*
|
||||
* @param params 支付宝返回参数
|
||||
* @return 前端跳转地址
|
||||
*/
|
||||
String handleReturn(Map<String, String> params);
|
||||
|
||||
/**
|
||||
* 处理支付宝退款异步通知
|
||||
*
|
||||
* @param params 支付宝回调参数
|
||||
* @return 支付宝要求返回的 "success" / "fail"
|
||||
*/
|
||||
String handleRefundNotify(Map<String, String> params);
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package com.snack.server.module.order.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayClient;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayTradePagePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePagePayResponse;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.snack.server.common.ResultCode;
|
||||
import com.snack.server.config.AlipayProperties;
|
||||
import com.snack.server.exception.BusinessException;
|
||||
import com.snack.server.module.order.entity.Order;
|
||||
import com.snack.server.module.order.enums.OrderStatusEnum;
|
||||
import com.snack.server.module.order.enums.PayChannelEnum;
|
||||
import com.snack.server.module.order.mapper.OrderMapper;
|
||||
import com.snack.server.module.order.service.AlipayService;
|
||||
import com.snack.server.module.order.service.OrderService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 支付宝支付业务实现(电脑网站支付,沙箱)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlipayServiceImpl implements AlipayService {
|
||||
|
||||
private static final String NOTIFY_PATH = "/api/payment/alipay/notify";
|
||||
|
||||
private final AlipayProperties alipayProperties;
|
||||
private final OrderMapper orderMapper;
|
||||
private final OrderService orderService;
|
||||
|
||||
@Override
|
||||
public String createPagePayUrl(Long orderId) {
|
||||
Order order = orderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
throw new BusinessException(ResultCode.ORDER_NOT_EXIST);
|
||||
}
|
||||
if (!Objects.equals(order.getUserId(), StpUtil.getLoginIdAsLong())) {
|
||||
throw new BusinessException(1000, "订单不存在或无权访问");
|
||||
}
|
||||
if (!Objects.equals(order.getStatus(), OrderStatusEnum.PENDING_PAY.getCode())) {
|
||||
throw new BusinessException(ResultCode.ORDER_STATUS_ERROR, "当前订单状态不可支付");
|
||||
}
|
||||
|
||||
AlipayClient alipayClient = buildClient();
|
||||
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
||||
request.setNotifyUrl(fullNotifyUrl());
|
||||
request.setReturnUrl(alipayProperties.getReturnUrl());
|
||||
|
||||
Map<String, Object> bizContent = new LinkedHashMap<>();
|
||||
bizContent.put("out_trade_no", order.getOrderNo());
|
||||
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
|
||||
bizContent.put("total_amount", order.getPayAmount().toPlainString());
|
||||
bizContent.put("subject", "零食商城-订单" + order.getOrderNo());
|
||||
bizContent.put("timeout_express", "30m");
|
||||
request.setBizContent(JSONUtil.toJsonStr(bizContent));
|
||||
|
||||
try {
|
||||
AlipayTradePagePayResponse response = alipayClient.pageExecute(request, "GET");
|
||||
String payUrl = response.getBody();
|
||||
if (StrUtil.isBlank(payUrl)) {
|
||||
throw new BusinessException(1000, "支付宝下单失败,未生成支付链接");
|
||||
}
|
||||
log.info("生成支付宝支付链接 orderNo={}", order.getOrderNo());
|
||||
return payUrl;
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("生成支付宝支付链接失败 orderNo={}", order.getOrderNo(), e);
|
||||
throw new BusinessException(1000, "支付宝下单失败:" + e.getErrMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String handleNotify(Map<String, String> params) {
|
||||
if (!rsaCheck(params)) {
|
||||
log.warn("支付宝异步通知签名校验失败");
|
||||
return "fail";
|
||||
}
|
||||
String tradeStatus = params.get("trade_status");
|
||||
if (!"TRADE_SUCCESS".equals(tradeStatus) && !"TRADE_FINISHED".equals(tradeStatus)) {
|
||||
log.info("支付宝异步通知忽略状态 trade_status={}", tradeStatus);
|
||||
return "success";
|
||||
}
|
||||
|
||||
String orderNo = params.get("out_trade_no");
|
||||
String tradeNo = params.get("trade_no");
|
||||
if (StrUtil.isBlank(orderNo) || StrUtil.isBlank(tradeNo)) {
|
||||
return "fail";
|
||||
}
|
||||
Order order = findOrderByNo(orderNo);
|
||||
if (order == null) {
|
||||
log.warn("支付宝异步通知订单不存在 orderNo={}", orderNo);
|
||||
return "fail";
|
||||
}
|
||||
|
||||
String totalAmount = params.get("total_amount");
|
||||
if (StrUtil.isNotBlank(totalAmount)
|
||||
&& new BigDecimal(totalAmount).compareTo(order.getPayAmount()) != 0) {
|
||||
log.warn("支付宝异步通知金额不一致 orderNo={} notify={} order={}",
|
||||
orderNo, totalAmount, order.getPayAmount());
|
||||
return "fail";
|
||||
}
|
||||
// 幂等处理:已支付订单直接返回 success,避免支付宝重复通知
|
||||
if (Objects.equals(order.getStatus(), OrderStatusEnum.PENDING_SHIP.getCode())) {
|
||||
return "success";
|
||||
}
|
||||
boolean ok = orderService.handlePaySuccess(
|
||||
order.getId(), PayChannelEnum.ALIPAY.getCode(), tradeNo);
|
||||
return ok ? "success" : "fail";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String handleReturn(Map<String, String> params) {
|
||||
if (!rsaCheck(params)) {
|
||||
throw new BusinessException(1000, "支付宝返回参数校验失败");
|
||||
}
|
||||
String orderNo = params.get("out_trade_no");
|
||||
String base = StrUtil.removeSuffix(alipayProperties.getReturnUrl(), "/");
|
||||
return StrUtil.isBlank(orderNo) ? base : base + "?pay=success&orderNo=" + orderNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String handleRefundNotify(Map<String, String> params) {
|
||||
if (!rsaCheck(params)) {
|
||||
log.warn("支付宝退款异步通知签名校验失败");
|
||||
return "fail";
|
||||
}
|
||||
String orderNo = params.get("out_trade_no");
|
||||
if (StrUtil.isBlank(orderNo)) {
|
||||
return "fail";
|
||||
}
|
||||
Order order = findOrderByNo(orderNo);
|
||||
if (order == null) {
|
||||
return "fail";
|
||||
}
|
||||
if (Objects.equals(order.getStatus(), OrderStatusEnum.REFUNDED.getCode())) {
|
||||
return "success";
|
||||
}
|
||||
String tradeNo = params.get("trade_no");
|
||||
boolean ok = orderService.handleRefundSuccess(
|
||||
order.getId(), StrUtil.blankToDefault(tradeNo, "RF-" + order.getOrderNo()));
|
||||
return ok ? "success" : "fail";
|
||||
}
|
||||
|
||||
private AlipayClient buildClient() {
|
||||
return new DefaultAlipayClient(
|
||||
alipayProperties.getGateway(),
|
||||
alipayProperties.getAppId(),
|
||||
alipayProperties.getAppPrivateKey(),
|
||||
"json",
|
||||
alipayProperties.getCharset(),
|
||||
alipayProperties.getAlipayPublicKey(),
|
||||
alipayProperties.getSignType());
|
||||
}
|
||||
|
||||
private String fullNotifyUrl() {
|
||||
String base = alipayProperties.getNotifyUrl();
|
||||
if (StrUtil.isBlank(base)) {
|
||||
throw new BusinessException(1000, "支付宝回调地址未配置");
|
||||
}
|
||||
return StrUtil.removeSuffix(base, "/") + NOTIFY_PATH;
|
||||
}
|
||||
|
||||
private boolean rsaCheck(Map<String, String> params) {
|
||||
try {
|
||||
return AlipaySignature.rsaCheckV1(
|
||||
params,
|
||||
alipayProperties.getAlipayPublicKey(),
|
||||
alipayProperties.getCharset(),
|
||||
alipayProperties.getSignType());
|
||||
} catch (AlipayApiException e) {
|
||||
log.warn("支付宝签名校验异常:{}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Order findOrderByNo(String orderNo) {
|
||||
return orderMapper.selectOne(
|
||||
new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));
|
||||
}
|
||||
}
|
||||
|
|
@ -104,3 +104,16 @@ snack:
|
|||
seckill:
|
||||
# 抢购用户限购记录在 Redis 中的过期时间(秒)—— 用于活动结束后清理
|
||||
record-ttl-seconds: 86400
|
||||
|
||||
alipay:
|
||||
appId: 9021000129648617
|
||||
# 沙箱网关
|
||||
gateway: https://openapi.alipaydev.com/gateway.do
|
||||
# 应用私钥
|
||||
appPrivateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFR2RFzPaCeO0o8ajYOnvSLgekKJ1RKiqUPx2Yw5olXvHIBzEn3Amkyy0SRIQDjoAXdvAsKkOFXLMbEHEAEkCOJLfm5/RQP9hY7jaCrLBV4rH/uJsFkHInPJmUXwOZk7CYdPS5vivW82LqXb8qFpYOETr/6dB074X2smk1r8hISm01TPDsGYr/z5ry8t+JtMTKkPioO32OvaG0ZeksPnHGdgZFoFboaVkOM/jbFSnURlhtVfw6RxnQWMSL7zrtDreLJjJ6CyZI4dEaUcJFyb77QVb+jbDmBhO9/1ul1n65jjRD1YgRyhZg8B9+nvCj7nM60R82PbD0ZzA16R/mW9xfAgMBAAECggEAWx2PHYH99OLfPoLm5f+VAs1JsDMl/b/I7LF9/PmSr2H7r9RKisthFODWT1Kf2nithqjjYrKefvECtCRS3KKReFVTLu3A9HOYe/KB8LbLts8+QQrFZruBTcf6cwGzSvJgpPmXslY4L044SGDGEy1dXt/sbRhTtD6QlBFw8F/g1CniuT+dwGZ5NjEa4HnlB7V0AYXL1hjAn4eKEkR8bzCnHY4g0PM582bqoJv7M6/I93uGAK9BPG6LhsIA54xvYaFcgkiqPFM6GYr1nCGGtK6W1eKh/oCtpIskR94SaUqTxQ4C/TGK+EXNDfgT9pcb28/5pUwiaUDNGsG2sQ0nVxIpkQKBgQD+XLkXQZemoz+YQJ4DtYeHSz88TReYzQKLia5Bt6eqkRuE8bDJ6oG6/dGniv1rh7OWCWvE1BTaOvjQPjw78si0nkJ9MHagZbnHyQE7cGpvXMZrTULsiacC6iH9fGA5+g7pW6/+wM9wED3dvjv4IztykQzxOG4DAEEDidVVErRxOQKBgQDGjJNZ7IH/LuGDzclUwemzeJ9jtJcWSjmY4zixU/wN2iVUavtotC6PBanLJVtsVdJWtSn2E34E+ptJWPHeoIzRAPUo+BrtC53W74CgOJwvBz9oAhbctUUaSp0L0zzmnFmHjcYVbL87pIr2wNNmoOPOk1bCTqvtODMef9iNe45yVwKBgAs6cp05MwWTHUKOT6tgPwxU4QS7bng7TIp2WG/kOI2J/EoBFUnEhVeIztFzjD5L7jyIS+6TYhxEECm5JZWL4RpABjVah1ILS4krMVe7Xadu0/92mKayaOHzDe8Pp3vHsxLQDlPTlRSgUurP8/u/KmejSYv4brrJLxWF5xnrSnXZAoGBAJEfYvyRUBtTCuap3YSkD8tsWSQpdV2Hdz32pxOGDW+aiTqAz64iNP245/hiH8a5m/pghIEmki/VdPdRmchdlU+W4ZrGbffhS8c0W9HATvhY1dGR5WAA5rdm3g8soRD2KsJXr/cs+0H+7Mua+WnEI350Vy/DDQtgzox2abRQITgVAoGBAPV/CRcWAPN1XTXFJMN8GwqndTkQ+Wgc77rE90+4s4kcu7GCaKWYapVDGbpUeAIckoE5uqK5CRgvoiT/ZhlYhSUej4vRrLTr5mD6y9qxW0AN+UiYvkBtRsxgmKMsY7hKIzyf4wEQj5TvN+9UG7ASvQFXVeQAgIhQ3yZ/nHvcrY7y
|
||||
# 支付宝公钥
|
||||
alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl54qsMxRkuisHTif0XyMG/ct3+Dx44gC5yj5k729AAJxXd5Y7uFit+FeN805W7ltm0sXB/gFNKsxe0ZJv5N+CO3/GfFtxDZK3FGxgkzH+6isXOI2pl2bOv5jWh/LcyOckA0SAq/p0AUjy0m8/FNJmq321q9SCz8UYSq188iYgawPdUtbuhwUlX2pJTWW25c5qSyNBeiXViODqkeQ9PhBYaEivRa/iY536ypRpEJ8QKcWcfj0INhh7gvDdbCSveRakCAxTkklMtahmkSuUlzVRhAkzhShX+1U2l7tUScimqEzjYJ2FdvhYYRQxjOt054hPug67ptNxPutGhPOI4L8FwIDAQAB
|
||||
# 回调地址
|
||||
notifyUrl: http://g2et3x.natappfree.cc
|
||||
# 返回地址
|
||||
returnUrl: http://127.0.0.1:5174/user
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,11 @@
|
|||
import { request } from '@/utils/request'
|
||||
import type { PageResult } from '@/types/common'
|
||||
import type { OrderDetail, OrderSubmitReq, OrderSubmitResult } from '@/types/order'
|
||||
import type {
|
||||
OrderDetail,
|
||||
OrderPayUrlResult,
|
||||
OrderSubmitReq,
|
||||
OrderSubmitResult
|
||||
} from '@/types/order'
|
||||
|
||||
/**
|
||||
* 提交订单(需登录)
|
||||
|
|
@ -39,7 +44,18 @@ export function getOrderDetailApi(id: number) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 支付订单(测试用模拟支付)
|
||||
* 获取支付宝电脑网站支付链接(需登录)
|
||||
* GET /api/orders/{id}/pay-url
|
||||
*/
|
||||
export function getOrderPayUrlApi(id: number) {
|
||||
return request<OrderPayUrlResult>({
|
||||
url: `/api/orders/${id}/pay-url`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付订单(旧模拟支付接口,预留兼容)
|
||||
* POST /api/orders/{id}/pay(需登录)
|
||||
*/
|
||||
export function payOrderApi(id: number) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export interface OrderSubmitResult {
|
|||
orderNo: string
|
||||
}
|
||||
|
||||
/** 获取支付宝支付链接返回 */
|
||||
export interface OrderPayUrlResult {
|
||||
payUrl: string
|
||||
}
|
||||
|
||||
/** 订单(对应后端 OrderDetailVO) */
|
||||
export interface OrderDetail {
|
||||
id: number
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* 确认订单页
|
||||
*
|
||||
* 结算购物车中已选中的商品:选择收货地址、优惠券、填写备注,提交订单后支持模拟支付。
|
||||
* 结算购物车中已选中的商品:选择收货地址、优惠券、填写备注,提交订单后跳转支付宝支付。
|
||||
*/
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
|
|
@ -20,7 +20,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
|
|||
import { getCartApi } from '@/api/cart'
|
||||
import { getAddressListApi, createAddressApi } from '@/api/address'
|
||||
import { getMyCouponsApi } from '@/api/coupon'
|
||||
import { submitOrderApi, payOrderApi } from '@/api/order'
|
||||
import { submitOrderApi, getOrderPayUrlApi } from '@/api/order'
|
||||
import type { CartItem } from '@/types/cart'
|
||||
import type { Address, AddressSaveReq } from '@/types/address'
|
||||
import type { UserCoupon } from '@/types/coupon'
|
||||
|
|
@ -177,9 +177,12 @@ async function handlePay() {
|
|||
if (!submitted.value) return
|
||||
paying.value = true
|
||||
try {
|
||||
await payOrderApi(submitted.value.orderId)
|
||||
ElMessage.success('支付成功')
|
||||
router.push('/user')
|
||||
const data = await getOrderPayUrlApi(submitted.value.orderId)
|
||||
if (data?.payUrl) {
|
||||
window.location.href = data.payUrl
|
||||
return
|
||||
}
|
||||
ElMessage.warning('支付宝支付链接生成失败')
|
||||
} catch {
|
||||
/* 拦截器已提示 */
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* - 我的优惠券(按状态筛选)
|
||||
*/
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
User as UserIcon,
|
||||
|
|
@ -30,7 +30,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
|
|||
import { useUserStore } from '@/stores/modules/user'
|
||||
import {
|
||||
getOrderPageApi,
|
||||
payOrderApi,
|
||||
getOrderPayUrlApi,
|
||||
cancelOrderApi,
|
||||
receiveOrderApi
|
||||
} from '@/api/order'
|
||||
|
|
@ -50,6 +50,7 @@ import type { FavoriteItem } from '@/types/favorite'
|
|||
import type { UserCoupon } from '@/types/coupon'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// ==================== 侧边栏 ====================
|
||||
|
|
@ -183,8 +184,8 @@ function onOrderPageChange(page: number) {
|
|||
|
||||
async function handlePayOrder(order: OrderDetail) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定支付该订单吗?', '提示', {
|
||||
confirmButtonText: '支付',
|
||||
await ElMessageBox.confirm('将跳转到支付宝沙箱完成支付,是否继续?', '提示', {
|
||||
confirmButtonText: '去支付',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
|
|
@ -193,9 +194,12 @@ async function handlePayOrder(order: OrderDetail) {
|
|||
}
|
||||
orderActingId.value = order.id
|
||||
try {
|
||||
await payOrderApi(order.id)
|
||||
ElMessage.success('支付成功')
|
||||
loadOrders()
|
||||
const data = await getOrderPayUrlApi(order.id)
|
||||
if (data?.payUrl) {
|
||||
window.location.href = data.payUrl
|
||||
return
|
||||
}
|
||||
ElMessage.warning('支付宝支付链接生成失败')
|
||||
} catch {
|
||||
/* 拦截器已提示 */
|
||||
} finally {
|
||||
|
|
@ -445,6 +449,12 @@ function onCouponTabChange(name: any) {
|
|||
onMounted(() => {
|
||||
loadAddresses()
|
||||
loadCoupons()
|
||||
// 支付宝支付完成后跳回个人中心,直接展示订单列表
|
||||
if (route.query.pay === 'success') {
|
||||
activeMenu.value = 'orders'
|
||||
loadOrders()
|
||||
ElMessage.success('支付成功')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue