支付服务
这里我们是使用的支付宝进行支付,所以需要调用支付宝的相关API,下面来了解一下怎样使用支付宝进行线上支付。
支付宝配置相关概念
支付宝开放平台传送门:支付宝开放平台
网站支付DEMO传送门:手机网站支付 DEMO | 网页&移动应用
RSA、加密加签、密钥等
对称加密
对称加密:发送方和接收方用的是同一把密钥,存在问题:当某一方将密钥泄漏之后,发送的消息可以被截取获悉并且随意进行通信。
非对称加密
非对称加密:发送方和接收方使用的不是同一把密钥,发送方使用密钥A对明文进行加密,接收方使用密钥B对密文进行解密,然后接收方将回复的明文用密钥C进行加密,发送方使用密钥D进行解密。采用非对称加密的好处是:即使有密钥被泄漏也不能自由的通信。
公钥与私钥
- 公钥和私钥是一个相对概念
- 密钥的公私性是相对于生成者而言的。发送方通过密钥A对明文进行加密,密钥A是只有发送方自己知道的,接收方想要解密密文,就需要拿到发送方公布出来的密钥B。
- 一对密钥生成后,保存在生产者手里的就是私钥(自己持有,不公开)
- 生成者发布出去让大家用的就是公钥
此时:
- 密钥A 和 密钥C 就是私钥,私钥用于加密,确保发送的消息不会被他人篡改
- 密钥B 和 密钥D 就是公钥,公钥用于解密,可以暴露出去。
加密 和 数字签名
签名:为了防止中途传输的数据被篡改和使用的方便,发送方采用私钥生成明文对应的签名,此过程被成为加签。接收方使用公钥去核验明文和签名是否对应,此过程被成为验签。
配置支付宝的沙箱环境:
这里我们需要获取的是支付宝的沙箱环境中的商户公钥与私钥以及支付宝的公钥
①采用系统默认生成的支付宝的公钥、商户(个人)私钥和公钥:
② 采用自定义密钥
使用支付宝的密钥生成工具,传送门:异步验签 | 开放平台
将支付宝密钥工具生成的应用公钥复制进加签内容配置中,会自动生成支付宝的公钥
沙箱账号:用于测试环境中的商品支付
内网穿透
内网穿透的原理: 内网穿透服务商是正常外网可以访问的ip地址,我们的电脑通过下载服务商软件客户端并与服务器建立起长连接,别人的电脑访问hello.hello.com会先找到hello.com即一级域名,然后由服务商将请求转发给我们电脑的二级域名。即内网穿透的服务商将他们的子域名租借给我们使用,在使用时只需要配置相关的本机地址和端口号并创建一个映射,就可以通过公网ip访问本机的服务了。
内网穿透功能可以允许我们使用外网的网址来访问主机;
正常的外网需要访问我们项目的流程是:
- 买服务器并且有公网固定 IP
- 买域名映射到服务器的 IP
- 域名需要进行备案和审核
使用场景
- 开发测试(微信、支付宝)
- 智慧互联
- 远程控制
- 私有云
环境准备-内网穿透常用软件和安装
续断:https://www.zhexi.tech/
第一步:登录
第二步:安装客户端下载链接
第三步:登录哲西云控制台,到“客户端”菜单,确认客户端已上线;
第四步:新建隧道
整合支付服务
整合支付前需要注意的问题: 保证所有项目的编码格式都是utf-8
第一步、导入依赖
<!--阿里支付模块-->
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>
第二步、抽取支付工具类并进行配置
成功调用该接口后,返回的数据就是支付页面的html,因此后续会使用@ResponseBody
1)、编写配置类
gulimall-order 服务的 com.atguigu.gulimall.order.config
路径下的 AlipayTemplate 配置类:
package com.atguigu.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.atguigu.gulimall.order.vo.PayVo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Data time:2022/4/15 14:52
* StudentID:2019112118
* Author:hgw
* Description: 支付宝沙箱测试配置类
*/
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
//在支付宝创建的应用的id
private String app_id = "个人appid";
// 商户私钥,您的PKCS8格式RSA2私钥
private String merchant_private_key = "个人的商户私钥";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
private String alipay_public_key = "支付宝公钥";
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url="http://**.natappfree.cc/payed/notify";
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功只好需要跳转的成功页即订单列表页
private String return_url="http://member.gulimall.com/memberOrder.html";
// 签名方式
private String sign_type = "RSA2";
// 字符编码格式
private String charset = "utf-8";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
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 +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);
return result;
}
}
2)、提交信息封装VO
package com.atguigu.gulimall.order.vo;
import lombok.Data;
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
3)、因为加上了@ConfigurationProperties(prefix = "alipay")
,是一个与配置文件绑定的配置类
我们可以在application.yaml 配置文件中编写相关的配置
# 支付宝相关的配置
alipay:
app-id: 2021000119667766
//...
第三步、修改前端页面
修改支付页的支付宝按钮发送使用支付宝付款请求
修改 gulimall-order 服务中的 pay.html 页面
<li>
<img src="/static/order/pay/img/zhifubao.png" style="weight:auto;height:30px;" alt="">
<a th:href="'http://order.gulimall.com/payOrder?orderSn='+${submitOrderResp.order.orderSn}">支付宝</a>
</li>
第四步、订单支付与同步通知
1)、编写Controller层接口调用gulimall-ordert 服务的 com.atguigu.gulimall.order.web
路径下的 PayWebController 类,映射/payOrder
produces属性:用于设置返回的数据类型
AlipayTemplate的pay()方法返回的就是一个用于浏览器响应的付款页面
package com.atguigu.gulimall.order.web;
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
/**
* 1、将支付页让浏览器展示
* 2、支付成功后,跳转到用户的订单列表项
* @param orderSn
* @return
* @throws AlipayApiException
*/
@ResponseBody
@GetMapping(value = "/payOrder",produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payVo = orderService.getOrderPay(orderSn);
// 返回的是一个页面。将此页面交给浏览器就行
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
}
2)、Service层实现类 OrderServiceImpl.java 编写获取当前订单的支付信息 方法
/**
* 获取当前订单的支付信息
* @param orderSn
* @return
*/
@Override
public PayVo getOrderPay(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity order = this.getOrderByOrderSn(orderSn);
//应付金额需要处理,支付宝只能支付保留两位小数的金额,采用ROUND_UP的进位模式
BigDecimal decimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(decimal.toString());
payVo.setOut_trade_no(order.getOrderSn());
List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
OrderItemEntity itemEntity = order_sn.get(0);
payVo.setSubject(itemEntity.getSkuName());
payVo.setBody(itemEntity.getSkuAttrsVals());
return payVo;
}
第五步、订单列表页渲染完成
环境准备
-
首先将资料中订单页静态资源部署到服务器中,让页面放到gulimall-member服务中。
-
配置网关
- id: gulimall_member_route uri: lb://gulimall-member predicates: - Host=member.gulimall.com
-
添加域名映射
# Gulimall Host Start 127.0.0.1 gulimall.com 127.0.0.1 search.gulimall.com 127.0.0.1 item.gulimall.com 127.0.0.1 auth.gulimall.com 127.0.0.1 cart.gulimall.com 127.0.0.1 order.gulimall.com 127.0.0.1 member.gulimall.com. "/etc/hosts"
-
整合SpringSession
-
导入依赖
<!-- 整合SpringSession完成Session共享问题--> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!--引入Redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
-
编写配置
spring: session: store-type: redis redis: host: 124.222.223.222
-
启动类加上注解
@EnableRedisHttpSession @EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign") @EnableDiscoveryClient @SpringBootApplication public class GulimallMemberApplication { public static void main(String[] args) { SpringApplication.run(GulimallMemberApplication.class, args); } }
-
-
配置拦截器(这里复制的同时一定要修改,放行:/member/member/**远程调用接口
-
用户登录拦截器
package com.atguigu.gulimall.member.interceptoe; import com.atguigu.common.constant.AuthServerConstant; import com.atguigu.common.vo.MemberRespVo; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Data time:2022/4/11 22:21 * StudentID:2019112118 * Author:hgw * Description: 用户登录拦截器 */ @Component public class LoginUserInterceptor implements HandlerInterceptor { public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>(); /** * 用户登录拦截器 * @param request * @param response * @param handler * @return * 用户登录:放行 * 用户未登录:跳转到登录页面 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // /order/order/status/222222222 String uri = request.getRequestURI(); boolean match = new AntPathMatcher().match("/member/**", uri); if (match){ return true; } MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER); if (attribute!=null){ loginUser.set(attribute); return true; } else { // 没登录就去登录 request.getSession().setAttribute("msg", "请先进行登录"); response.sendRedirect("http://auth.gulimall.cn/login.html"); return false; } } }
-
编写Web配置类,指定用户登录拦截器
package com.atguigu.gulimall.member.config; import com.atguigu.gulimall.member.interceptoe.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; /** * Data time:2022/4/15 17:03 * StudentID:2019112118 * Author:hgw * Description: Web配置 */ @Configuration public class MemberWebConfig implements WebMvcConfigurer { @Autowired LoginUserInterceptor loginUserInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**"); } }
-
接口编写
第一步、gulimall-order 服务中编写分页查询当前登录用户的所有订单接口方法
1)、在Controller层编写gulimall-order 服务中/src/main/java/com/atguigu/gulimall/order/controller
OrderController.java
package com.atguigu.gulimall.order.controller;
@RestController
@RequestMapping("order/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 分页查询当前登录用户的所有订单
*/
@PostMapping("/listWithItem")
public R listWithItem(@RequestBody Map<String, Object> params){
PageUtils page = orderService.queryPageWithItem(params);
return R.ok().put("page", page);
}
2)、Service层 OrderServiceImpl.java实现类方法编写:
gulimall-order 服务中 com/atguigu/gulimall/order/service/impl
OrderServiceImpl.java
@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>().eq("member_id", memberRespVo.getId()).orderByDesc("modify_time")
);
List<OrderEntity> order_sn = page.getRecords().stream().map(order -> {
List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));
order.setItemEntities(itemEntities);
return order;
}).collect(Collectors.toList());
page.setRecords(order_sn);
return new PageUtils(page);
}
3)、修改 OrderEntity.java 实体类,为其加上一个属性
gulimall-order 服务 com.atguigu.gulimall.order.entity
路径下的 OrderEntity类,添加以下属性:
@TableField(exist = false)
private List<OrderItemEntity> itemEntities;
第二步、在gulimall-member服务中调用 gulimall-order 服务接口
gulimall-member 服务的 com.atguigu.gulimall.member.feign
路径下的 OrderFeignService接口进行远程调用:
package com.atguigu.gulimall.member.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Map;
/**
* Data time:2022/4/15 20:46
* StudentID:2019112118
* Author:hgw
* Description:
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
@PostMapping("/order/order/listWithItem")
R listWithItem(@RequestBody Map<String, Object> params);
}
第三步、编写过滤器
因为这里用户服务中设计到了远程调用订单服务,且访问是以/memberOrder.html结尾的,这里是模拟访问的页面,那么访问页面时就会带上cookie,所以我们可以给请求拦截一下,让feign远程调用创建新request时带上老请求的cookie,这样到订单服务时就有session的cookie信息就不用登录了。
package com.atguigu.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.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* Data time:2022/4/12 11:20
* StudentID:2019112118
* Author:hgw
* Description:
*/
@Configuration
public class GulimallFeignConfig {
/**
* feign在远程调用之前会执行所有的RequestInterceptor拦截器
* @return
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
// 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes!=null){
HttpServletRequest request = attributes.getRequest();
// 2、同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
// 给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
}
}
};
}
}
第四步、Controller 层 MemberWebController 编写
package com.atguigu.gulimall.member.web;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.utils.R;
import com.atguigu.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 java.util.HashMap;
import java.util.Map;
/**
* Data time:2022/4/15 17:00
* StudentID:2019112118
* Author:hgw
* Description:
*/
@Controller
public class MemberWebController {
@Autowired
OrderFeignService orderFeignService;
//模拟的访问页面,所以可以拦截一下带上之前的cookie
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
Model model){
// 查处当前登录的用户的所有订单列表数据
Map<String,Object> page = new HashMap<>();
page.put("page",pageNum.toString());
R r = orderFeignService.listWithItem(page);
System.out.println(JSON.toJSONString(r));
model.addAttribute("orders",r);
return "orderList";
}
}
5.3、前端页面接收渲染
修改 orderList.html 页面的部分内容,渲染页面
<table class="table" th:each="order:${orders.page.list}">
<tr>
<td colspan="7" style="background:#F7F7F7">
<span style="color:#AAAAAA">2017-12-09 20:50:10</span>
<span><ruby style="color:#AAAAAA">订单号:</ruby> [[${order.orderSn}]]</span>
<span>谷粒商城<i class="table_i"></i></span>
<i class="table_i5 isShow"></i>
</td>
</tr>
<tr class="tr" th:each="item,itemStat:${order.itemEntities}">
<td colspan="3">
<img style="height: 60px; width: 60px;" th:src="${item.skuPic}" alt="" class="img">
<div>
<p style="width: 242px; height: auto; overflow: auto">
[[${item.skuName}]]
</p>
<div><i class="table_i4"></i>找搭配</div>
</div>
<div style="margin-left:15px;">x[[${item.skuQuantity}]]</div>
<div style="clear:both"></div>
</td>
<td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">[[${order.receiverName}]]<i><i class="table_i1"></i></i></td>
<td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}" style="padding-left:10px;color:#AAAAB1;">
<p style="margin-bottom:5px;">总额 ¥[[${order.payAmount}]]</p>
<hr style="width:90%;">
<p>在线支付</p>
</td>
<td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">
<ul>
<li style="color:#71B247;" th:if="${order.status==0}">待付款</li>
<li style="color:#71B247;" th:if="${order.status==1}">已付款</li>
<li style="color:#71B247;" th:if="${order.status==2}">已发货</li>
<li style="color:#71B247;" th:if="${order.status==3}">已完成</li>
<li style="color:#71B247;" th:if="${order.status==4}">已取消</li>
<li style="color:#71B247;" th:if="${order.status==5}">售后中</li>
<li style="color:#71B247;" th:if="${order.status==6}">售后完成</li>
<li style="margin:4px 0;" class="hide"><i class="table_i2"></i>跟踪<i class="table_i3"></i>
<div class="hi">
<div class="p-tit">
普通快递 运单号:390085324974
</div>
<div class="hideList">
<ul>
<li>
[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
的快件已签收,感谢您使用韵达快递)签收
</li>
<li>
[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
的快件已签收,感谢您使用韵达快递)签收
</li>
<li>
[北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描
</li>
<li>
[北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636
</li>
</ul>
</div>
</div>
</li>
<li class="tdLi">订单详情</li>
</ul>
</td>
<td>
<button>确认收货</button>
<p style="margin:4px 0; ">取消订单</p>
<p>催单</p>
</td>
</tr>
</table>
第六步、异步通知内网穿透环境搭建
- 订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态
- 由于同步跳转可能由于网络问题失败,所以使用异步通知
- 支付宝使用的是最大努力通知方案来实现分布式事务的最终一致性,保障数据一致性,隔一段时间会通知商户支付成功,直到返回
success
1)、建立内网穿透
2)内网穿透设置异步通知地址
-
将外网映射到本地的
order.gulimall.cn:80
-
由于回调的请求头不是
order.gulimall.cn
,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置将
/payed/notify
异步通知转发至订单服务
设置异步通知的地址:
3)、内网穿透联调
通过工具进行内网穿透,第三方并不是从浏览器发送过来请求,即使是从浏览中发送过来,那host也不对,故我们需要修改nginx的配置,来监听 /payed/notify
请求,设置默认的host
服务器的 mydata/nginx/conf/conf.d
目录下
hgw@HGWdeAir conf.d % vim gulimall.conf
1
server {
listen 80;
server_name gulimall.cn *.gulimall.cn mvaophzk6b.51xd.pub;#最后一个是内网穿透地址
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /static/ {
root /usr/share/nginx/html;
}
#匹配回调地址加上host地址,否则默认带的是内网穿透地址
location /payed/ {
proxy_set_header Host order.gulimall.cn;
proxy_pass http://gulimall;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
4)、编写登录拦截器方法,放行
/payed/notify
,因为该请求是支付宝回调确认请求,不需要登录
修改gulimall-order服务 com.atguigu.gulimall.order.interceptoe
路径的 LoginUserInterceptor 类,代码如下:
package com.atguigu.gulimall.order.interceptoe;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// /order/order/status/222222222
String uri = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match("/order/order/status/**", uri);
boolean match1 = matcher.match("/payed/notify", uri);
if (match || match1 ){
return true;
}
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
} else {
// 没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.cn/login.html");
return false;
}
}
}
第七步、验证签名,支付成功
- 验证签名
- 验签通过,即是支付宝发过来的数据,处理支付结果
- 保存交易流水 oms_payment_info
- 修改订单的状态信息 oms_order
- 返回"success"
- 验签失败,即不是支付宝发送过来的数据
- 返回非 “success”即可
- 验签通过,即是支付宝发过来的数据,处理支付结果
1、主体代码,Controller层接口编写
package com.atguigu.gulimall.order.listener;
import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.atguigu.gulimall.order.config.AlipayTemplate;
import com.atguigu.gulimall.order.service.OrderService;
import com.atguigu.gulimall.order.vo.PayAsyncVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* Data time:2022/4/15 21:54
* StudentID:2019112118
* Author:hgw
* Description: 接收支付宝的异步通知
*/
@RestController
public class OrderPayedListener {
@Autowired
OrderService orderService;
@Autowired
AlipayTemplate alipayTemplate;
@PostMapping("/payed/notify")
public String handleAliPayed(PayAsyncVo vo,HttpServletRequest request) throws AlipayApiException {
// 只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success,支付宝就再也不通知
// 验签
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.handlePayRequest(vo);
return result;
} else {
System.out.println("签名验证失败....");
return "error";
}
}
}
2、处理支付结果
配置SpringMVC日期转化格式
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
1、编写一个实体类Vo 用来映射支付宝异步通知回来的数据
gulimall-order 服务的 com.atguigu.gulimall.order.vo
路径下的 PayAsyncVo 类
package com.atguigu.gulimall.order.vo;
import lombok.Data;
import lombok.ToString;
@ToString
@Data
public class PayAsyncVo {
private String gmt_create;
private String charset;
private String gmt_payment;
private String 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
}
2)、设置 表oms_payment_info 的索引
因为一个订单对应一个流水号,所以我们给订单号和支付流水号加上两个唯一索引:
并修改 order_sn 属性的长度为 64位
3)、Service 层实现类 OrderServiceImpl.java 类编写 处理支付宝的支付结果 方法
gulimall-order 服务的 com/atguigu/gulimall/order/service/impl/OrderServiceImpl.java
路径下的 OrderServiceImpl.java
/**
* 处理支付宝的支付结果
* @param vo
* @return
*/
@Override
public String handlePayRequest(PayAsyncVo vo) {
// 1、保存交易流水 oms_payment_info
PaymentInfoEntity infoEntity = new PaymentInfoEntity();
infoEntity.setAlipayTradeNo(vo.getTrade_no());
infoEntity.setOrderSn(vo.getOut_trade_no());
infoEntity.setPaymentStatus(vo.getTrade_status());
infoEntity.setCallbackTime(vo.getNotify_time());
paymentInfoService.save(infoEntity);
// 2、修改订单的状态信息 oms_order
// 判断支付是否成功:支付宝返回 TRADE_SUCCESS、TRADE_FINISHED 都表示成功
if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")) {
// 支付成功状态,则修改订单的状态
String outTradeNo = vo.getOut_trade_no();
this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
}
return "success";
}
4)、修改订单的状态信息方法编写 oms_order
gulimall-order 服务的 com.atguigu.gulimall.order.dao
路径下的 OrderDao
package com.atguigu.gulimall.order.dao;
@Mapper
public interface OrderDao extends BaseMapper<OrderEntity> {
void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);
}
gulimall-order 服务的 gulimall-order/src/main/resources/mapper/order/OrderDao.xml
<update id="updateOrderStatus">
UPDATE oms_order SET `status`=#{code} WHERE order_sn=#{outTradeNo};
</update>
第八步、关单处理
- 由于买家的特殊原因,没能在订单过期前完成支付,等到订单状态过期了才支付,这时库存已进行解锁库存,但是会将订单状态改为已支付,此时就出现了错误
- 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
- 由于延时等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
- 订单解锁,手动调用收单
- 网络阻塞问题,订单支付成功的异步通知一直不到达
- 查询订单列表时,Ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态。
- 其他各种问题
- 每天晚上闲时下载支付宝对账单——进行对账
情况一:订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态已经改为已付款但是库存解锁了
解决方案:自动关单
alipay.close_pay.time_expire=1m
情况二: 由于网络延时原因,订单解锁完成,正要解锁库存时,支付成功的异步通知才到
即关单最后时间用户进行支付,此时因为网络慢,解锁订单和库存只执行完了解锁订单,还没有解锁库存,但是此时支付成功的异步通知发了过来,但是订单已经解锁取消掉了,但是库存减少了。
解决方案:订单解锁,手动关单
手动关单,参考支付Demo中的Close.jsp
感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。