前言
本系列博客基于B站谷粒商城,只作为本人学习总结使用。这里我会比较注重业务逻辑的编写和相关配置的流程。有问题可以评论或者联系我互相交流。原视频地址谷粒商城雷丰阳版。本人git仓库地址Draknessssw的谷粒商城
两种加密方式
相关概念
私钥,公钥
私钥适合加密数据,公钥适合明文传输数据。
加密和数字签名
验证签名
设置沙箱环境
在支付宝开放平台助手上生成密钥
公钥复制到沙箱环境生成支付宝的公钥
至此,支付宝支付配置了支付宝公钥和自己的私钥
package com.xxxx.gulimall.order.config;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.xxxx.gulimall.order.vo.PayVo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
public String app_id;
// 商户私钥,您的PKCS8格式RSA2私钥
public String merchant_private_key;
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
public String alipay_public_key;
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
public String notify_url;
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
public String return_url;
// 签名方式
private String sign_type;
// 字符编码格式
private String charset;
//订单超时时间
private String timeout = "1m";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
public String gatewayUrl;
public String pay(PayVo vo) throws AlipayApiException {
//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
//1、根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);
//2、创建一个支付请求 //设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = vo.getOut_trade_no();
//付款金额,必填
String total_amount = vo.getTotal_amount();
//订单名称,必填
String subject = vo.getSubject();
//商品描述,可空
String body = vo.getBody();
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\""+timeout+"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);
return result;
}
}
内网穿透
设置支付宝异步回调页面
沙箱环境中也要进行配置
最后是properties文件中的配置
公钥和私钥按照自己的来吧
alipay.app_id=2021000121642435
alipay.merchant_private_key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCLjwJ0VNa8I5N8jVhY6Y3+rSrJLlyLcIxbQKY/nQMkDm2D7pbarVbQ5+kTP6uI50BBKMVAV6SenjNbdRNSC18Ynbqns/vWp4NjvG1c4R6zC5tkwqA3HkHtaGX9awl15qaK8W5vLO9qczuFA+s2Syk5KkEs4IW8cF93Mkb4cxAoU6j/3XONzHShWOrGyF1vbQyRpvADL0U+EUTvAhvkPfhbl/FdvB9ng6c3PnLm6D8Gsq6zDBiCnsx6esY7hX7iUpfI3BVBG39BfIjQWz/GBRWADvCKeJnbr+gxPzIF+P+8h2mcrVZHQ5wTwru5DzO9mxacd/K6bbvj0L69SxKRLpg/AgMBAAECggEAf0T94gT0h0KSX8WuyfbD7XsSR8Gl1+vds+IzOlP/50PZD7XnkKj/QSgc39byobjyWfnKWLEUiSqQf1k3M1z3bkV5UTLL+aduJOhTe545FuMA5VSwM1O+n7jTOdYBHKopOxCNu4lpFNTddKJsQGkuwNkN8tUWYRDBnrwVCMI1++9g+IsEcBEEph8Uj4LIrJnH7+d1+azmy+Vl+qqd94ZraK972rysz2w0+sa8IXu7vmpLMymQayF58P01rFG+OsBb/YMK7kOwRTNjveTFcY7R7TXPz8gsW1PjtnssUw0EUgOGUpqd5S+LOnKUc6aWuZ8NK9r7QlqRkR1RUbR46fyR0QKBgQDivRlK8MEGmk3r+iltBnnXK/zkPsNeaCcODdDiB4/hL7MBdOenGBQuaZo+HDLNiXNvFkO5KIJ/2ljNKQlkZWqZFv6oVVM6Y2AgL6/C3KQj1YDRj/sDplg/5DK+kfw4qZt6sASVBkXetaj8N9KhUTuMJ6tDTisDsFSrzzFReLnZzQKBgQCdkbETiN0dawswfhEOT38Mv9CN0pziYtQUFM3q6QpKVfzI3rF32xg+W1P5ixWKL8CMSYCKyEvkPN1rRNe0jwW6FZPRcAZDpbexq7XQTrascmJXL8aZuz+FPFb2CPUULcMK9Ev1MePxh9AYjh3VN6MlbLzSzD4/fz7CiQ6HeUD+OwKBgQCeAFIww8Zu+HYWW/QkMmATTmbjEs2H6yJUC9Kkv8pGjLu75yBKc2AU26gNYg8Q5ZiYL7avv4f42koJZXBTEs0Os1RwL01ZIcjphPGA48pJ4kzrO98asv9KPpYR8J8HSUG8ZA49XuqvgH2qjKftnDLXvwj8VOtqnaTTOQXQFUXFfQKBgASnmSOKl32W++2iy74wewBVakPGRPwrDzjIpIyb9cHcaGtGqNdxkXXGHOTyRuCeKIH8ad+vqw5C/gd9MSIUV4b3vDYjqQu2iYamG+jbamoNtvn8X1GLRoUZEziRayv9bhWUwemsX59y86LGD/uMeTVR0QIpJm2ZxLDae6Nk4ZahAoGAVYgsCuXaTlvGqWBH98RcvyQMT76w1FU/y9B8XSplpSGpHp1bGGZEnHzr3cwxhwNPhixKAEgA9jZrwEcEKaQELLNF5eXKnCwSrBevyh3SFy+KRa6KIox7ymOoElKccjPXdYEE7YO/3isgKC2Wdsfdy82luqo1XLGo1e3j1FVpn/I=
alipay.alipay_public_key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtbbxUtAxCfZCIy47WM3N/oVxa4X5RzwUb94AcPtkHJsNYsvrpZ5moAceTZiMKlxg1+SRU1Hex5DJ7+tEB5d/Tz+5TKjY4YENgmdyIc3jv0KysFd8e2/8+yH4HNu062OHMr92rc7q/HqK2jwwJ5+4KWAOEbBxLf69pZZwbTA/aSXonR7i32+JzYzoJ1jgVL0YnMl3IgPAqo7dZrl3lho7XtFxe5BF+WyhiKJKwgYlGvYcyYmkDMwR7Mzj5bsrC1ZAEEVkjtt7rpRR30BIIAS8EkKCl7dY1VIc+Eh0q8Ax/W9ur1GkXY1ti7mEQK+AphaVTg8LE546yEKSJKKNYVxLhQIDAQAB
alipay.notify_url=http://dongrongrong.free.svipss.top/payed/notify
#alipay.notify_url=http://member.gulimall.com/payed/notify
alipay.return_url=http://member.gulimall.com/memberOrder.html
alipay.sign_type=RSA2
alipay.charset=utf-8
alipay.gatewayUrl=https://openapi.alipaydev.com/gateway.do
订单支付
引入依赖
整合支付宝的sdk
<!-- 支付宝sdk -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.10.111.ALL</version>
</dependency>
准备支付的Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
/**
* @author LinLinD
* @Create 2022-08-03-18:07
*/
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
获取订单的支付信息
需要注意的是,这里返回的是alipatTemplate处理过的html页面,而不是json数据。
/**
* 用户下单:支付宝支付
* 1、让支付页让浏览器展示
* 2、支付成功以后,跳转到用户的订单列表页
* @param orderSn
* @return
* @throws AlipayApiException
*/
@ResponseBody
@GetMapping(value = "/aliPayOrder",produces = "text/html")
public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payVo = orderService.getOrderPay(orderSn);
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
实现类
/**
* 获取当前订单的支付信息
* @param orderSn
* @return
*/
@Override
public PayVo getOrderPay(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity orderInfo = this.getOrderByOrderSn(orderSn);
//保留两位小数点,向上取值
BigDecimal payAmount = orderInfo.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(payAmount.toString());
payVo.setOut_trade_no(orderInfo.getOrderSn());
//查询订单项的数据
List<OrderItemEntity> orderItemInfo = orderItemService.list(
new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
OrderItemEntity orderItemEntity = orderItemInfo.get(0);
payVo.setBody(orderItemEntity.getSkuAttrsVals());
payVo.setSubject(orderItemEntity.getSkuName());
return payVo;
}
支付成功返回订单列表页面
配置订单列表页面为同步回调地址
alipay.return_url=http://member.gulimall.com/memberOrder.html
回到会员服务
配置拦截器,确保用户在登录状态,而且是member下的页面
package com.xxxx.gulimall.member.interceptor;
import com.xxxx.common.vo.MemberResponseVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
import static com.xxxx.common.constant.AuthServerConstant.LOGIN_USER;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/member/**", uri);
if (match) {
return true;
}
HttpSession session = request.getSession();
//获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
// session.setAttribute("msg", "请先进行登录");
// response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
同样配置远程请求的cookie同步配置,session配置和添加拦截器配置
package com.xxxx.gulimall.member.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
package com.xxxx.gulimall.member.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
package com.xxxx.gulimall.member.config;
import com.xxxx.gulimall.member.interceptor.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MemberWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
去往订单页面
package com.xxxx.gulimall.member.web;
import com.alibaba.fastjson.JSON;
import com.xxxx.common.utils.R;
import com.xxxx.gulimall.member.feign.OrderFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Controller
public class MemberWebController {
@Autowired
private OrderFeignService orderFeignService;
@GetMapping(value = "/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",required = false,defaultValue = "0") Integer pageNum,
Model model, HttpServletRequest request) {
//获取到支付宝给我们转来的所有请求数据
//request,验证签名
//查出当前登录用户的所有订单列表数据
Map<String,Object> page = new HashMap<>();
page.put("page",pageNum.toString());
//远程查询订单服务订单数据
R orderInfo = orderFeignService.listWithItem(page);
System.out.println(JSON.toJSONString(orderInfo));
model.addAttribute("orders",orderInfo);
return "orderList";
}
}
远程接口
package com.xxxx.gulimall.member.feign;
import com.xxxx.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;
@FeignClient("gulimall-order")
public interface OrderFeignService {
/**
* 分页查询当前登录用户的所有订单信息
* @param params
* @return
*/
@PostMapping("/order/order/listWithItem")
R listWithItem(@RequestBody Map<String, Object> params);
}
/**
* 分页查询当前登录用户的所有订单信息
* @param params
* @return
*/
@PostMapping("/listWithItem")
//@RequiresPermissions("order:order:list")
public R listWithItem(@RequestBody Map<String, Object> params){
PageUtils page = orderService.queryPageWithItem(params);
return R.ok().put("page", page);
}
实现类
/**
* 查询当前用户所有订单数据
* @param params
* @return
*/
@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>()
.eq("member_id",memberResponseVo.getId()).orderByDesc("create_time")
);
//遍历所有订单集合
List<OrderEntity> orderEntityList = page.getRecords().stream().map(order -> {
//根据订单号查询订单项里的数据
List<OrderItemEntity> orderItemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
.eq("order_sn", order.getOrderSn()));
order.setOrderItemEntityList(orderItemEntities);
return order;
}).collect(Collectors.toList());
page.setRecords(orderEntityList);
return new PageUtils(page);
}
异步通知页面构建
写一个监听器来监听支付宝的异步通知
需要验证签名和处理订单结果
package com.xxxx.gulimall.order.listener;
import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.xxxx.gulimall.order.config.AlipayTemplate;
import com.xxxx.gulimall.order.service.OrderService;
import com.xxxx.gulimall.order.vo.PayAsyncVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
@RestController
public class OrderPayedListener {
@Autowired
private OrderService orderService;
@Autowired
private AlipayTemplate alipayTemplate;
@PostMapping(value = "/payed/notify")
public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
// 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
// 获取支付宝POST过来反馈信息
//TODO 需要验签
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
if (signVerified) {
System.out.println("签名验证成功...");
//去修改订单状态
String result = orderService.handlePayResult(asyncVo);
return result;
} else {
System.out.println("签名验证失败...");
return "error";
}
}
}
异步通知Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
@ToString
@Data
public class PayAsyncVo {
private String gmt_create;
private String charset;
private String gmt_payment;
private Date notify_time;
private String subject;
private String sign;
private String buyer_id;//支付者的id
private String body;//订单的信息
private String invoice_amount;//支付金额
private String version;
private String notify_id;//通知id
private String fund_bill_list;
private String notify_type;//通知类型; trade_status_sync
private String out_trade_no;//订单号
private String total_amount;//支付的总额
private String trade_status;//交易状态 TRADE_SUCCESS
private String trade_no;//流水号
private String auth_app_id;//
private String receipt_amount;//商家收到的款
private String point_amount;//
private String app_id;//应用id
private String buyer_pay_amount;//最终支付的金额
private String sign_type;//签名类型
private String seller_id;//商家的id
}
此时访问nginx服务器会有丢失请求头的问题
去nginx的配置文件配置这个异步通知页面的请求头信息,同时配置内网穿透的网址
在登录的拦截器里放行
处理支付结果
需要在支付信息表保存数据
/**
* 处理支付宝的支付结果
* @param asyncVo
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public String handlePayResult(PayAsyncVo asyncVo) {
//保存交易流水信息
PaymentInfoEntity paymentInfo = new PaymentInfoEntity();
paymentInfo.setOrderSn(asyncVo.getOut_trade_no());
paymentInfo.setAlipayTradeNo(asyncVo.getTrade_no());
paymentInfo.setTotalAmount(new BigDecimal(asyncVo.getBuyer_pay_amount()));
paymentInfo.setSubject(asyncVo.getBody());
paymentInfo.setPaymentStatus(asyncVo.getTrade_status());
paymentInfo.setCreateTime(new Date());
paymentInfo.setCallbackTime(asyncVo.getNotify_time());
//添加到数据库中
this.paymentInfoService.save(paymentInfo);
//修改订单状态
//获取当前状态
String tradeStatus = asyncVo.getTrade_status();
if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) {
//支付成功状态
String orderSn = asyncVo.getOut_trade_no(); //获取订单号
this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY);
}
return "success";
}
更新支付状态的方法
/**
* 修改订单状态
* @param orderSn
* @param code
*/
private void updateOrderStatus(String orderSn, Integer code,Integer payType) {
this.baseMapper.updateOrderStatus(orderSn,code,payType);
}
sql
<update id="updateOrderStatus">
UPDATE oms_order
SET `status` = #{code},modify_time = NOW(),pay_type = #{payType},payment_time = NOW()
WHERE order_sn = #{orderSn}
</update>