1.集成支付宝沙箱支付功能

2.订单下单
3.回调通知(未成功)
4.回查订单支付状态
This commit is contained in:
海言
2025-10-22 17:41:38 +08:00
parent b6eb4d5341
commit 7c33311efb
16 changed files with 414 additions and 13 deletions

View File

@@ -0,0 +1,35 @@
package cn.xf.basedemo.config.pay;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* AliPayConfig
*
* @author 海言
* @date 2025/10/22
* @time 13:47
* @Description 支付宝支付配置类
*/
@Configuration
@EnableConfigurationProperties(AliPayConfigProperties.class)
public class AliPayConfig {
@Bean(name = "alipayClient")
@ConditionalOnProperty(value = "pay.ali.enabled", havingValue = "true", matchIfMissing = false)
public AlipayClient alipayClient(AliPayConfigProperties properties){
return new DefaultAlipayClient(
properties.getGatewayUrl(),
properties.getAppId(),
properties.getMerchantPrivateKey(),
properties.getFormat(),
properties.getCharset(),
properties.getAlipayPublicKey(),
properties.getSign_type());
}
}

View File

@@ -0,0 +1,52 @@
package cn.xf.basedemo.config.pay;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* AliPayConfig
*
* @author 海言
* @date 2025/10/22
* @time 13:35
* @Description 支付宝支付配置常量
*/
@Data
@ConfigurationProperties(prefix = "pay.ali")
public class AliPayConfigProperties {
/**
* 应用ID,您的APPID收款账号既是您的APPID对应支付宝账号
*/
private String appId;
/**
* 商户PID
*/
private String pId;
/**
* 商家私钥
*/
private String merchantPrivateKey;
/**
* 支付宝公钥
*/
private String alipayPublicKey;
/**
* 支付宝沙箱网关
*/
private String gatewayUrl;
// 签名方式
private String sign_type = "RSA2";
// 字符编码格式
private String charset = "GBK";
// 传输格式
private String format = "json";
}

View File

@@ -0,0 +1,36 @@
package cn.xf.basedemo.controller.business;
import cn.xf.basedemo.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
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.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* PayCallbackController
*
* @author 海言
* @date 2025/10/22
* @time 16:16
* @Description 支付回调控制器
*/
@RestController
@RequestMapping("/pay/callback")
public class PayCallbackController {
@Resource
private OrderService userService;
//回调
@Operation(summary = "支付宝支付回调", description = "支付宝支付回调")
@PostMapping("/ali/notice")
public String aliCallback(HttpServletRequest request) {
userService.aliCallback(request);
return "success";
}
}

View File

@@ -0,0 +1,45 @@
package cn.xf.basedemo.controller.business;
import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.model.req.PayOrderFrom;
import cn.xf.basedemo.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* PayOrderController
*
* @author 海言
* @date 2025/10/22
* @time 14:07
* @Description 支付下单控制器
*/
@RestController
@RequestMapping("/pay")
public class PayOrderController {
@Resource
private OrderService orderService;
@Operation(summary = "支付宝支付下单", description = "支付宝支付下单")
@PostMapping("/ali/createOrder")
public RetObj aliCreateOrder(@RequestBody PayOrderFrom from) {
return orderService.aliCreateOrder(from);
}
//掉单查询支付宝支付订单状态
@Operation(summary = "掉单查询支付宝支付订单状态", description = "掉单查询支付宝支付订单状态")
@GetMapping("/ali/queryOrderStatus")
public String queryAlipayOrderStatus(String orderNo) {
return orderService.queryAlipayOrderStatus(orderNo);
}
}

View File

@@ -3,7 +3,7 @@ package cn.xf.basedemo.controller.business;
import cn.xf.basedemo.common.model.LoginUser;
import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.interceptor.SessionContext;
import cn.xf.basedemo.model.res.LoginInfoRes;
import cn.xf.basedemo.model.req.LoginInfoReq;
import cn.xf.basedemo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -27,7 +27,7 @@ public class UserController {
@Operation(summary = "用户登录", description = "用户登录")
@PostMapping("/login")
public RetObj login(@RequestBody LoginInfoRes res) {
public RetObj login(@RequestBody LoginInfoReq res) {
return userService.login(res);
}

View File

@@ -25,7 +25,7 @@ public class InterceptorConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor()) //登录逻辑拦截类
.addPathPatterns("/**") //需要拦截的请求(设置的全部拦截)
.excludePathPatterns("/user/login", "/web/**"); //忽略的请求
.excludePathPatterns("/user/login", "/web/**", "/pay/callback/**"); //忽略的请求
}

View File

@@ -32,7 +32,7 @@ public class TokenInterceptor implements HandlerInterceptor {
//不拦截的请求列表
private static final List<String> EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login","/swagger-ui.html","/v3/api-docs","/swagger-ui/index.html");
private static final List<String> EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login", "/swagger-ui.html", "/v3/api-docs", "/swagger-ui/index.html");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
@@ -49,7 +49,7 @@ public class TokenInterceptor implements HandlerInterceptor {
token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
throw new LoginException("请先登录");
}else {
} else {
//验证token
if (!token.startsWith("Bearer ")) {
throw new LoginException(ResponseCode.USER_INPUT_ERROR);

View File

@@ -1,4 +1,4 @@
package cn.xf.basedemo.model.res;
package cn.xf.basedemo.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -11,7 +11,7 @@ import lombok.Data;
* @create: 2022-07-04 11:46
**/
@Data
public class LoginInfoRes {
public class LoginInfoReq {
/**
* 登录密文

View File

@@ -0,0 +1,31 @@
package cn.xf.basedemo.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* PayOrderFrom
*
* @author 海言
* @date 2025/10/22
* @time 14:09
* @Description
*/
@Data
public class PayOrderFrom {
@Schema(name = "商品id")
private Long productId;
@Schema(name = "商品名称")
private String productName;
@Schema(name = "商品价格")
private BigDecimal price;
@Schema(name = "商品数量")
private Integer num;
}

View File

@@ -0,0 +1,21 @@
package cn.xf.basedemo.model.res;
import lombok.Data;
/**
* PayOrderRes
*
* @author 海言
* @date 2025/10/22
* @time 14:12
* @Description
*/
@Data
public class PayOrderRes {
private String orderNo;
private String from;
}

View File

@@ -0,0 +1,15 @@
package cn.xf.basedemo.service;
import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.model.req.PayOrderFrom;
import javax.servlet.http.HttpServletRequest;
public interface OrderService {
RetObj aliCreateOrder(PayOrderFrom from);
String aliCallback(HttpServletRequest request);
String queryAlipayOrderStatus(String orderNo);
}

View File

@@ -1,7 +1,7 @@
package cn.xf.basedemo.service;
import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.model.res.LoginInfoRes;
import cn.xf.basedemo.model.req.LoginInfoReq;
/**
* @program: xf-boot-base
@@ -12,7 +12,7 @@ import cn.xf.basedemo.model.res.LoginInfoRes;
**/
public interface UserService {
RetObj login(LoginInfoRes res);
RetObj login(LoginInfoReq res);
RetObj syncEs(Long userId);

View File

@@ -0,0 +1,153 @@
package cn.xf.basedemo.service.impl;
import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.config.pay.AliPayConfigProperties;
import cn.xf.basedemo.model.req.PayOrderFrom;
import cn.xf.basedemo.model.res.PayOrderRes;
import cn.xf.basedemo.service.OrderService;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayClient;
import com.alipay.api.domain.AlipayTradeQueryModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* OrderServiceImpl
*
* @author 海言
* @date 2025/10/22
* @time 14:08
* @Description
*/
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private AlipayClient alipayClient;
@Resource
private AliPayConfigProperties aliPayConfigProperties;
@Override
public RetObj<PayOrderRes> aliCreateOrder(PayOrderFrom from) {
try {
//校验用户数据
//校验商品库存
//校验余额账户
//...
//创建订单
//获取订单编码
String orderNo = "O" + System.currentTimeMillis();
// PC/H5方式支付
AlipayTradePagePayRequest request = getAlipayTradePagePayRequest(from, orderNo);
AlipayTradePagePayResponse payResponse = alipayClient.pageExecute(request);
//创建支付单数据
//...
String body = payResponse.getBody();
PayOrderRes res = new PayOrderRes();
res.setOrderNo(orderNo);
res.setFrom(body);
return RetObj.success(res);
} catch (Exception e) {
log.error("创建订单失败", e);
}
return null;
}
@Override
public String aliCallback(HttpServletRequest request) {
try {
log.info("支付回调,消息接收 {}", request.getParameter("trade_status"));
if (request.getParameter("trade_status").equals("TRADE_SUCCESS")) {
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
params.put(name, request.getParameter(name));
}
String tradeNo = params.get("out_trade_no");
String gmtPayment = params.get("gmt_payment");
String alipayTradeNo = params.get("trade_no");
String sign = params.get("sign");
String content = AlipaySignature.getSignCheckContentV1(params);
boolean checkSignature = AlipaySignature.rsa256CheckContent(content, sign, aliPayConfigProperties.getAlipayPublicKey(), "UTF-8"); // 验证签名
// 支付宝验签
if (checkSignature) {
// 验签通过
log.info("支付回调,交易名称: {}", params.get("subject"));
log.info("支付回调,交易状态: {}", params.get("trade_status"));
log.info("支付回调,支付宝交易凭证号: {}", params.get("trade_no"));
log.info("支付回调,商户订单号: {}", params.get("out_trade_no"));
log.info("支付回调,交易金额: {}", params.get("total_amount"));
log.info("支付回调买家在支付宝唯一id: {}", params.get("buyer_id"));
log.info("支付回调,买家付款时间: {}", params.get("gmt_payment"));
log.info("支付回调,买家付款金额: {}", params.get("buyer_pay_amount"));
log.info("支付回调,支付回调,更新订单 {}", tradeNo);
// 更新订单未已支付
// orderService.changeOrderPaySuccess(tradeNo);
}
}
return "success";
} catch (Exception e) {
log.error("支付回调,处理失败", e);
return "false";
}
}
/**
* 回查订单支付状态
*
* @param orderNo
* @return
*/
@Override
public String queryAlipayOrderStatus(String orderNo) {
//实践方案:创建定时任务,查询订单数据中超过某个自定义时间未支付的订单进行回查
try {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel bizModel = new AlipayTradeQueryModel();
bizModel.setOutTradeNo(orderNo);
request.setBizModel(bizModel);
AlipayTradeQueryResponse alipayTradeQueryResponse = alipayClient.execute(request);
String code = alipayTradeQueryResponse.getCode();
// 判断状态码
if ("10000".equals(code)) {
//更新订单状态
//...
return "success";
}
}catch (Exception e){
log.error("回查支付订单失败", e);
}
return "not";
}
@NotNull
private AlipayTradePagePayRequest getAlipayTradePagePayRequest(PayOrderFrom from, String orderNo) {
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl("http://zfd8b38d.natappfree.cc/pay/callback/ali/notice");
request.setReturnUrl("www.baidu.com");
//构建请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
bizContent.put("total_amount", from.getPrice().multiply(new BigDecimal(from.getNum())));
bizContent.put("subject", from.getProductName());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toJSONString());
return request;
}
}

View File

@@ -11,7 +11,7 @@ import cn.xf.basedemo.common.utils.StringUtil;
import cn.xf.basedemo.config.GlobalConfig;
import cn.xf.basedemo.mappers.UserMapper;
import cn.xf.basedemo.model.domain.User;
import cn.xf.basedemo.model.res.LoginInfoRes;
import cn.xf.basedemo.model.req.LoginInfoReq;
import cn.xf.basedemo.mq.RocketMqMsgProducer;
import cn.xf.basedemo.service.UserService;
import com.alibaba.fastjson.JSONObject;
@@ -24,7 +24,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -56,7 +55,7 @@ public class UserServiceImpl implements UserService {
private RocketMqMsgProducer rocketMqMsgProducer;
@Override
public RetObj login(LoginInfoRes res) {
public RetObj login(LoginInfoReq res) {
if (Objects.isNull(res) || StringUtils.isEmpty(res.getEncryptedData())) {
return null;