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>
|
<s3.version>2.44.4</s3.version>
|
||||||
<minio.version>9.0.0</minio.version>
|
<minio.version>9.0.0</minio.version>
|
||||||
<aliyun-oss.version>3.18.5</aliyun-oss.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>
|
<tencent-cos.version>5.6.269</tencent-cos.version>
|
||||||
<weixin-java.version>4.8.3.B</weixin-java.version>
|
<weixin-java.version>4.8.3.B</weixin-java.version>
|
||||||
<mapstruct.version>1.6.3</mapstruct.version>
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
|
|
@ -142,6 +143,13 @@
|
||||||
<artifactId>mapstruct</artifactId>
|
<artifactId>mapstruct</artifactId>
|
||||||
<version>${mapstruct.version}</version>
|
<version>${mapstruct.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alipay.sdk</groupId>
|
||||||
|
<artifactId>alipay-sdk-java</artifactId>
|
||||||
|
<version>${alipay.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<dependencyManagement>
|
<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.OrderPageReq;
|
||||||
import com.snack.server.module.order.dto.req.OrderSubmitReq;
|
import com.snack.server.module.order.dto.req.OrderSubmitReq;
|
||||||
import com.snack.server.module.order.entity.Order;
|
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.service.OrderService;
|
||||||
import com.snack.server.module.order.vo.OrderDetailVO;
|
import com.snack.server.module.order.vo.OrderDetailVO;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
@ -27,6 +28,7 @@ import java.util.Map;
|
||||||
public class OrderController {
|
public class OrderController {
|
||||||
|
|
||||||
private final OrderService orderService;
|
private final OrderService orderService;
|
||||||
|
private final AlipayService alipayService;
|
||||||
|
|
||||||
@Operation(summary = "提交订单")
|
@Operation(summary = "提交订单")
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
|
|
@ -58,6 +60,13 @@ public class OrderController {
|
||||||
return Result.ok();
|
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 = "取消订单")
|
@Operation(summary = "取消订单")
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@PostMapping("/{id}/cancel")
|
@PostMapping("/{id}/cancel")
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,59 @@
|
||||||
package com.snack.server.module.order.controller;
|
package com.snack.server.module.order.controller;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import com.snack.server.module.order.service.AlipayService;
|
||||||
import com.snack.server.common.Result;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import com.snack.server.module.order.service.OrderService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付宝 / 微信支付回调(沙箱版)
|
* 支付宝回调(电脑网站支付)
|
||||||
*
|
*
|
||||||
* 真实生产:
|
* 1. 支付成功后支付宝 POST 异步通知 /api/payment/alipay/notify
|
||||||
* 1. 支付宝 POST 异步通知到 /api/payment/alipay/notify
|
* 2. 服务端验签后更新订单状态,返回 "success" 给支付宝
|
||||||
* 2. 校验 sign 签名
|
* 3. 用户支付完成同步跳转 /api/payment/alipay/return,再重定向到前端
|
||||||
* 3. 解析 out_trade_no(订单号)、trade_no(支付宝交易号)
|
|
||||||
* 4. 调用 OrderService.handlePaySuccess()
|
|
||||||
* 5. 返回 "success" / "fail" 给支付宝
|
|
||||||
*
|
|
||||||
* 沙箱测试(手动调用):
|
|
||||||
* POST /api/payment/alipay/notify
|
|
||||||
* ?orderId=1&payChannel=alipay&payTradeNo=20260601xxxx
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Tag(name = "支付回调(沙箱测试)")
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/payment")
|
@RequestMapping("/api/payment")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PaymentNotifyController {
|
public class PaymentNotifyController {
|
||||||
|
|
||||||
private final OrderService orderService;
|
private final AlipayService alipayService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付宝支付成功异步通知
|
* 支付宝支付成功异步通知(必须返回纯文本 success / fail)
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "支付宝支付成功回调(沙箱测试用)")
|
|
||||||
@PostMapping("/alipay/notify")
|
@PostMapping("/alipay/notify")
|
||||||
public Result<String> alipayNotify(
|
public String alipayNotify(@RequestParam Map<String, String> params) {
|
||||||
@RequestParam Long orderId,
|
log.info("收到支付宝支付异步通知 out_trade_no={} trade_status={}",
|
||||||
@RequestParam(required = false, defaultValue = "alipay") String payChannel,
|
params.get("out_trade_no"), params.get("trade_status"));
|
||||||
@RequestParam(required = false) String payTradeNo) {
|
return alipayService.handleNotify(params);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付宝退款成功异步通知
|
* 支付宝退款异步通知(必须返回纯文本 success / fail)
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "支付宝退款成功回调(沙箱测试用)")
|
|
||||||
@PostMapping("/alipay/refund/notify")
|
@PostMapping("/alipay/refund/notify")
|
||||||
public Result<String> alipayRefundNotify(
|
public String alipayRefundNotify(@RequestParam Map<String, String> params) {
|
||||||
@RequestParam Long orderId,
|
log.info("收到支付宝退款异步通知 out_trade_no={}", params.get("out_trade_no"));
|
||||||
@RequestParam(required = false) String refundTradeNo) {
|
return alipayService.handleRefundNotify(params);
|
||||||
if (orderId == null) {
|
}
|
||||||
return Result.fail(400, "orderId 不能为空");
|
|
||||||
}
|
/**
|
||||||
if (StrUtil.isBlank(refundTradeNo)) {
|
* 支付宝电脑网站支付同步返回(用户支付后从支付宝跳回)
|
||||||
refundTradeNo = "RF-MOCK-" + System.currentTimeMillis();
|
*/
|
||||||
}
|
@GetMapping("/alipay/return")
|
||||||
log.info("收到支付宝退款回调 orderId={} refundTradeNo={}", orderId, refundTradeNo);
|
public void alipayReturn(@RequestParam Map<String, String> params,
|
||||||
boolean ok = orderService.handleRefundSuccess(orderId, refundTradeNo);
|
HttpServletResponse response) throws IOException {
|
||||||
return ok ? Result.ok("success") : Result.fail("fail");
|
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:
|
seckill:
|
||||||
# 抢购用户限购记录在 Redis 中的过期时间(秒)—— 用于活动结束后清理
|
# 抢购用户限购记录在 Redis 中的过期时间(秒)—— 用于活动结束后清理
|
||||||
record-ttl-seconds: 86400
|
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 { request } from '@/utils/request'
|
||||||
import type { PageResult } from '@/types/common'
|
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(需登录)
|
* POST /api/orders/{id}/pay(需登录)
|
||||||
*/
|
*/
|
||||||
export function payOrderApi(id: number) {
|
export function payOrderApi(id: number) {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ export interface OrderSubmitResult {
|
||||||
orderNo: string
|
orderNo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取支付宝支付链接返回 */
|
||||||
|
export interface OrderPayUrlResult {
|
||||||
|
payUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
/** 订单(对应后端 OrderDetailVO) */
|
/** 订单(对应后端 OrderDetailVO) */
|
||||||
export interface OrderDetail {
|
export interface OrderDetail {
|
||||||
id: number
|
id: number
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
/**
|
/**
|
||||||
* 确认订单页
|
* 确认订单页
|
||||||
*
|
*
|
||||||
* 结算购物车中已选中的商品:选择收货地址、优惠券、填写备注,提交订单后支持模拟支付。
|
* 结算购物车中已选中的商品:选择收货地址、优惠券、填写备注,提交订单后跳转支付宝支付。
|
||||||
*/
|
*/
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,7 +20,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
|
||||||
import { getCartApi } from '@/api/cart'
|
import { getCartApi } from '@/api/cart'
|
||||||
import { getAddressListApi, createAddressApi } from '@/api/address'
|
import { getAddressListApi, createAddressApi } from '@/api/address'
|
||||||
import { getMyCouponsApi } from '@/api/coupon'
|
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 { CartItem } from '@/types/cart'
|
||||||
import type { Address, AddressSaveReq } from '@/types/address'
|
import type { Address, AddressSaveReq } from '@/types/address'
|
||||||
import type { UserCoupon } from '@/types/coupon'
|
import type { UserCoupon } from '@/types/coupon'
|
||||||
|
|
@ -177,9 +177,12 @@ async function handlePay() {
|
||||||
if (!submitted.value) return
|
if (!submitted.value) return
|
||||||
paying.value = true
|
paying.value = true
|
||||||
try {
|
try {
|
||||||
await payOrderApi(submitted.value.orderId)
|
const data = await getOrderPayUrlApi(submitted.value.orderId)
|
||||||
ElMessage.success('支付成功')
|
if (data?.payUrl) {
|
||||||
router.push('/user')
|
window.location.href = data.payUrl
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.warning('支付宝支付链接生成失败')
|
||||||
} catch {
|
} catch {
|
||||||
/* 拦截器已提示 */
|
/* 拦截器已提示 */
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
* - 我的优惠券(按状态筛选)
|
* - 我的优惠券(按状态筛选)
|
||||||
*/
|
*/
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
User as UserIcon,
|
User as UserIcon,
|
||||||
|
|
@ -30,7 +30,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import {
|
import {
|
||||||
getOrderPageApi,
|
getOrderPageApi,
|
||||||
payOrderApi,
|
getOrderPayUrlApi,
|
||||||
cancelOrderApi,
|
cancelOrderApi,
|
||||||
receiveOrderApi
|
receiveOrderApi
|
||||||
} from '@/api/order'
|
} from '@/api/order'
|
||||||
|
|
@ -50,6 +50,7 @@ import type { FavoriteItem } from '@/types/favorite'
|
||||||
import type { UserCoupon } from '@/types/coupon'
|
import type { UserCoupon } from '@/types/coupon'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// ==================== 侧边栏 ====================
|
// ==================== 侧边栏 ====================
|
||||||
|
|
@ -183,8 +184,8 @@ function onOrderPageChange(page: number) {
|
||||||
|
|
||||||
async function handlePayOrder(order: OrderDetail) {
|
async function handlePayOrder(order: OrderDetail) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定支付该订单吗?', '提示', {
|
await ElMessageBox.confirm('将跳转到支付宝沙箱完成支付,是否继续?', '提示', {
|
||||||
confirmButtonText: '支付',
|
confirmButtonText: '去支付',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'info'
|
type: 'info'
|
||||||
})
|
})
|
||||||
|
|
@ -193,9 +194,12 @@ async function handlePayOrder(order: OrderDetail) {
|
||||||
}
|
}
|
||||||
orderActingId.value = order.id
|
orderActingId.value = order.id
|
||||||
try {
|
try {
|
||||||
await payOrderApi(order.id)
|
const data = await getOrderPayUrlApi(order.id)
|
||||||
ElMessage.success('支付成功')
|
if (data?.payUrl) {
|
||||||
loadOrders()
|
window.location.href = data.payUrl
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.warning('支付宝支付链接生成失败')
|
||||||
} catch {
|
} catch {
|
||||||
/* 拦截器已提示 */
|
/* 拦截器已提示 */
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -445,6 +449,12 @@ function onCouponTabChange(name: any) {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAddresses()
|
loadAddresses()
|
||||||
loadCoupons()
|
loadCoupons()
|
||||||
|
// 支付宝支付完成后跳回个人中心,直接展示订单列表
|
||||||
|
if (route.query.pay === 'success') {
|
||||||
|
activeMenu.value = 'orders'
|
||||||
|
loadOrders()
|
||||||
|
ElMessage.success('支付成功')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue