欢迎访问加群:1107019965,学习更多的知识
订单搞定之后就是支付了,首先搭建支付工程。
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.atguigugroupId>
<artifactId>gmall-1010artifactId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>com.atguigugroupId>
<artifactId>gmall-paymentartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gmall-paymentname>
<description>谷粒商城支付系统description>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>gmall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>gmall-oms-interfaceartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-sdk-javaartifactId>
<version>4.10.0.ALLversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbit-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
bootstrap.yml:
spring:
application:
name: payment-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
application.yml:
server:
port: 18092
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8179
zipkin:
base-url: http://localhost:9411/
sender:
type: web
discovery-client-enabled: false
sleuth:
sampler:
probability: 1
redis:
host: 172.16.116.100
rabbitmq:
host: 172.16.116.100
virtual-host: /fengge
username: fengge
password: fengge
listener:
simple:
acknowledge-mode: manual
prefetch: 1
thymeleaf:
cache: false
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://172.16.116.100:3306/guli_payment
username: root
password: root
feign:
sentinel:
enabled: true
mybatis-plus:
global-config:
db-config:
id-type: auto
启动类:
@SpringBootApplication
@EnableFeignClients
@MapperScan("com.atguigu.gmall.payment.mapper")
public class GmallPaymentApplication {
public static void main(String[] args) {
SpringApplication.run(GmallPaymentApplication.class, args);
}
}
网关配置:
nginx配置:加入payment.gmall.com
server {
listen 80;
server_name api.gmall.com search.gmall.com www.gmall.com item.gmall.com sso.gmall.com cart.gmall.com order.gmall.com payment.gmall.com;
proxy_set_header Host $host;
location / {
proxy_pass http://192.168.221.1:8888;
}
}
重新加载nginx配置:nginx -s reload
在hosts中添加payment.gmall.com:
支付流程如下:
下单成功后,请求路径:http://payment.gmall.com/pay.html?orderToken=202006041844036401268493714385424386
已知条件是订单编号,而支付可能需要订单金额等一些订单信息。所以订单工程应该提供一个根据订单编号查询订单的数据接口。
在gmall-oms工程中的OrderController中添加根据订单编号查询订单的接口方法:
@GetMapping("token/{orderSn}")
public ResponseVo<OrderEntity> queryOrderByOrderSn(@PathVariable("orderSn")String orderSn){
OrderEntity orderEntity = this.orderService.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
return ResponseVo.ok(orderEntity);
}
在gmall-oms-interface工程中的GmallOmsApi添加接口方法:
@GetMapping("oms/order/token/{orderSn}")
public ResponseVo<OrderEntity> queryOrderByOrderSn(@PathVariable("orderSn")String orderSn);
在gmall-payment工程中实现页面跳转。
PaymentController:
@Controller
public class PaymentController {
@Autowired
private PaymentService paymentService;
@GetMapping("pay.html")
public String toPay(@RequestParam("orderToken") String orderToken, Model model){
OrderEntity orderEntity = this.paymentService.queryOrderByOrderToken(orderToken);
model.addAttribute("orderEntity", orderEntity);
return "pay";
}
}
PaymentService:
@Service
public class PaymentService {
@Autowired
private GmallOmsClient omsClient;
public OrderEntity queryOrderByOrderToken(String orderToken) {
ResponseVo<OrderEntity> orderEntityResponseVo = this.omsClient.queryOrderByOrderSn(orderToken);
return orderEntityResponseVo.getData();
}
}
GmallOmsClient:
@FeignClient("oms-service")
public interface GmallOmsClient extends GmallOmsApi {
}
这里支付已支付宝为例,支付宝的支付流程如下:
调用顺序如下:
注意:
支付异步通知需要独立ip使阿里支付成功后可以回调我们的接口,所以前提条件就是内网穿透。
哲西云:https://cloud.zhexi.tech
使用内网穿透后,外网无法通过payment.gmall.com访问支付系统了。只能通过内网穿透提供的地址访问,那么我们的网关也就无法通过域名转发请求,只能通过路径转发,于是在网关中配置路径路由:
将支付数据保存到数据库,以便跟支付宝进行对账。
创建guli_payment数据库,导入一下sql:
CREATE TABLE `payment_info` (
`id` bigint(20) NOT NULL COMMENT '商户订单号',
`out_trade_no` varchar(64) DEFAULT NULL,
`payment_type` tinyint(4) DEFAULT NULL COMMENT '支付类型(微信与支付宝)',
`trade_no` varchar(64) DEFAULT NULL COMMENT '支付宝交易凭证号',
`total_amount` decimal(18,4) DEFAULT NULL COMMENT '订单金额。订单中获取',
`subject` varchar(100) DEFAULT NULL COMMENT '交易内容。利用商品名称拼接。',
`payment_status` tinyint(4) DEFAULT NULL COMMENT '支付状态,默认值0-未支付,1-已支付。',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`callback_time` datetime DEFAULT NULL COMMENT '回调时间,初始为空,支付宝异步回调时记录',
`callback_content` text COMMENT '回调信息,初始为空,支付宝异步回调时记录',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='支付对账表';
对应的实体类如下:
@Data
@TableName("payment_info")
public class PaymentInfoEntity {
@Id
private Long id;
private String outTradeNo;
private Integer paymentType;
private String tradeNo;
private BigDecimal totalAmount;
private String subject;
private Integer paymentStatus;
private Date createTime;
private Date callbackTime;
private String callbackContent;
}
mapper接口:
public interface PaymentInfoMapper extends BaseMapper<PaymentInfoEntity> {
}
继续改造gmall-order工程
在pom.xml中,引入阿里支付的依赖:
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-sdk-javaartifactId>
<version>4.10.0.ALLversion>
dependency>
在application.yml中添加阿里支付的配置:
alipay:
app_id: 2021001163617452
gatewayUrl: https://openapi.alipay.com/gateway.do
merchant_private_key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQ
alipay_public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkWs
notify_url: http://9glldacce2.52http.net/pay/success
return_url: http://9glldacce2.52http.net/pay/ok
app_id、私钥、公钥参照资料中的《支付宝秘钥.txt》
把课前资料中封装的阿里支付工具类及PayVo对象copy到工程中:
修改提交订单的gmall-order中OrderController方法,如下:
@GetMapping("alipay.html")
@ResponseBody
public String alipay(@RequestParam("orderToken") String orderToken){
try {
// 校验订单状态
OrderEntity orderEntity = this.paymentService.queryOrderByOrderToken(orderToken);
if (orderEntity.getStatus() != 0){
throw new OrderException("此订单无法支付,可能已经过期!");
}
// 调用支付宝接口获取支付表单
PayVo payVo = new PayVo();
payVo.setOut_trade_no(orderEntity.getOrderSn());
// payVo.setTotal_amount(orderEntity.getPayAmount().toString());
payVo.setTotal_amount("0.01");
payVo.setSubject("谷粒商城支付平台");
// 把支付信息保存到数据库
Long payId = this.paymentService.save(orderEntity, 1);
payVo.setPassback_params(payId.toString());
String form = alipayTemplate.pay(payVo);
// 跳转到支付页
return form;
} catch (AlipayApiException e) {
e.printStackTrace();
throw new OrderException("支付出错,请刷新后重试!");
}
}
PaymentService:
@Service
public class PaymentService {
@Autowired
private GmallOmsClient omsClient;
@Autowired
private PaymentInfoMapper paymentInfoMapper;
public OrderEntity queryOrderByOrderToken(String orderToken) {
ResponseVo<OrderEntity> orderEntityResponseVo = this.omsClient.queryOrderByOrderSn(orderToken);
return orderEntityResponseVo.getData();
}
public Long save(OrderEntity orderEntity, Integer payType){
// 查看支付记录,是否已存在。
PaymentInfoEntity paymentInfoEntity = this.paymentInfoMapper.selectOne(new QueryWrapper<PaymentInfoEntity>().eq("out_trade_no", orderEntity.getOrderSn()));
// 如果存在,直接结束
if (paymentInfoEntity != null) {
return paymentInfoEntity.getId();
}
// 否则,新增支付记录
paymentInfoEntity = new PaymentInfoEntity();
paymentInfoEntity.setOutTradeNo(orderEntity.getOrderSn());
paymentInfoEntity.setPaymentType(payType);
paymentInfoEntity.setSubject("谷粒商城支付平台");
// paymentInfoEntity.setTotalAmount(orderEntity.getPayAmount());
paymentInfoEntity.setTotalAmount(new BigDecimal(0.01));
paymentInfoEntity.setPaymentStatus(0);
paymentInfoEntity.setCreateTime(new Date());
this.paymentInfoMapper.insert(paymentInfoEntity);
return paymentInfoEntity.getId();
}
}
测试效果:
由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
接收到回调要做的事情:
请求方式:Post请求
请求路径:/pay/success
请求参数:PayAsyncVo
返回值:success/failure
给PaymentController新增支付成功后的回调方法:
@PostMapping("pay/success")
@ResponseBody
public String paySuccess(PayAsyncVo payAsyncVo){
// 1.验签
Boolean flag = this.alipayTemplate.verifySignature(payAsyncVo);
if (!flag) {
//TODO:验签失败则记录异常日志
return "failure"; // 支付失败
}
// 2.验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
String payId = payAsyncVo.getPassback_params();
if (StringUtils.isBlank(payId)){
return "failure";
}
PaymentInfoEntity paymentInfoEntity = this.paymentService.queryPayMentById(Long.valueOf(payId));
if (paymentInfoEntity == null
|| !StringUtils.equals(payAsyncVo.getApp_id(), this.alipayTemplate.getApp_id())
|| !StringUtils.equals(payAsyncVo.getOut_trade_no(), paymentInfoEntity.getOutTradeNo())
|| paymentInfoEntity.getTotalAmount().compareTo(new BigDecimal(payAsyncVo.getBuyer_pay_amount())) != 0){
return "failure";
}
// 3.校验支付状态。根据 trade_status 进行后续业务处理 TRADE_SUCCESS
if (!StringUtils.equals("TRADE_SUCCESS", payAsyncVo.getTrade_status())) {
return "failure";
}
// 4.正常的支付成功,记录支付记录方便对账
paymentService.paySuccess(payAsyncVo);
// 5.发送消息更新订单状态,并减库存
this.rabbitTemplate.convertAndSend("order-exchange", "order.pay", payAsyncVo.getOut_trade_no());
// 6.给支付宝成功回执
return "success";
}
给PaymentService添加方法:
/**
* 根据id查询支付信息
* @param id
* @return
*/
public PaymentInfoEntity queryPayMentById(Long id) {
return this.paymentInfoMapper.selectById(id);
}
/**
* 更新支付状态
* @param payAsyncVo
*/
public void paySuccess(PayAsyncVo payAsyncVo){
PaymentInfoEntity paymentInfoEntity = new PaymentInfoEntity();
paymentInfoEntity.setCallbackTime(new Date());
paymentInfoEntity.setPaymentStatus(1);
paymentInfoEntity.setCallbackContent(JSON.toJSONString(payAsyncVo));
this.paymentInfoMapper.update(paymentInfoEntity, new UpdateWrapper<PaymentInfoEntity>().eq("out_trade_no", payAsyncVo.getOut_trade_no()));
}
给gmall-oms中的OrderListener添加修改订单状态为支付成功(待发货)的消息监听方法:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "ORDER-PAY-QUEUE", durable = "true"),
exchange = @Exchange(value = "ORDER-EXCHANGE", ignoreDeclarationExceptions = "true"),
key = {"order.pay"}
))
public void successOrder(String orderToken, Channel channel, Message message) throws IOException {
if (this.orderMapper.successOrder(orderToken) == 1){
// 如果订单支付成功,真正的减库存
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "stock.minus", orderToken);
// 给用户添加积分信息
OrderEntity orderEntity = this.orderService.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderToken));
UserBoundVO userBoundVO = new UserBoundVO();
userBoundVO.setUserId(orderEntity.getUserId());
userBoundVO.setIntegration(orderEntity.getIntegration());
userBoundVO.setGrowth(orderEntity.getGrowth());
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "bound.plus", userBoundVO);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
给gmall-oms工程的OrderMapper接口及实现类添加successOrder方法:
int successOrder(String orderToken);
<update id="successOrder">
update oms_order set `status`=1 where order_sn=#{orderToken} and `status`=0
update>
给gmall-oms-interface工程添加UserBoundVO
内容:
@Data
public class UserBoundVO {
private Long userId;
private Integer integration;
private Integer growth;
}
给gmall-wms的StockListener添加减库存的监听器方法:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "STOCK-MINUS-QUEUE", durable = "true"),
exchange = @Exchange(value = "ORDER-EXCHANGE", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),
key = {"stock.minus"}
))
public void minusStock(String orderToken, Channel channel, Message message) throws IOException {
try {
// 获取redis中该订单的锁定库存信息
String json = this.redisTemplate.opsForValue().get(KEY_PREFIX + orderToken);
if (StringUtils.isNotBlank(json)){
// 反序列化获取库存的锁定信息
List<SkuLockVo> skuLockVos = JSON.parseArray(json, SkuLockVo.class);
// 遍历并解锁库存信息
skuLockVos.forEach(skuLockVo -> {
this.wareSkuMapper.minus(skuLockVo.getWareSkuId(), skuLockVo.getCount());
});
// 删除redis中库存锁定信息
this.redisTemplate.delete(KEY_PREFIX + orderToken);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
给gmall-wms的WareSkuMapper添加方法:
void minus(@Param("id") Long wareSkuId, @Param("count") Integer count);
给gmall-wms的WareSkuMapper.xml添加映射
<update id="minus">
update wms_ware_sku set stock_locked = stock_locked - #{count}, stock = stock - #{count}, sales = sales + #{count} where id = #{id}
update>
gmall-ums中加积分的监听器略。。。。。。
用户扫描支付成功后,我们可以通过同步回调,跳转到商户的支付成功页。
请求方式:GET
请求路径:/pay/ok
请求参数:参照异步请求(比异步请求略少)
返回视图名称
@GetMapping("pay/ok")
public String payOk(PayAsyncVo payAsyncVo){
// 查询订单数据展示在支付成功页面
// String orderToken = payAsyncVo.getOut_trade_no();
// TODO:查询并通过model响应给页面
return "paysuccess";
}
秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存 (+ 页面静态化)。
限流方式:
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 分布式并发工具类,快速的腾出服务器的资源来处理其他请求;
* @param skuId
* @return
*/
@GetMapping("/miaosha/{skuId}")
public ResponseVo<Object> kill(@PathVariable("skuId") Long skuId){
Long userId = LoginInterceptor.getUserInfo().getUserId();
if(userId!=null){
// 查询库存
String stock = this.redisTemplate.opsForValue().get("sec:stock:" + skuId);
if (StringUtils.isEmpty(stock)){
return ResponseVo.fail("秒杀结束!");
}
// 通过信号量,获取秒杀库存
RSemaphore semaphore = this.redissonClient.getSemaphore("sec:semaphore:" + skuId);
semaphore.trySetPermits(Integer.valueOf(stock));
//0.1s
boolean b = semaphore.tryAcquire();
if(b){
//创建订单
String orderSn = IdWorker.getTimeId();
SkuLockVO lockVO = new SkuLockVO();
lockVO.setOrderToken(orderSn);
lockVO.setCount(1);
lockVO.setSkuId(skuId);
//准备闭锁信息
RCountDownLatch latch = this.redissonClient.getCountDownLatch("sec:countdown:" + orderSn);
latch.trySetCount(1);
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "sec.kill", lockVO);
return ResponseVo.ok("秒杀成功,订单号:" + orderSn);
}else {
return ResponseVo.fail("秒杀失败,欢迎再次秒杀!");
}
}
return ResponseVo.fail("请登录后再试!");
}
@GetMapping("/miaosha/pay")
public String payKillOrder(String orderSn) throws InterruptedException {
RCountDownLatch latch = this.redissonClient.getCountDownLatch("sec:countdown:" + orderSn);
latch.await();
// 查询订单信息
return "";
}
d();
SkuLockVO lockVO = new SkuLockVO();
lockVO.setOrderToken(orderSn);
lockVO.setCount(1);
lockVO.setSkuId(skuId);
//准备闭锁信息
RCountDownLatch latch = this.redissonClient.getCountDownLatch("sec:countdown:" + orderSn);
latch.trySetCount(1);
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "sec.kill", lockVO);
return ResponseVo.ok("秒杀成功,订单号:" + orderSn);
}else {
return ResponseVo.fail("秒杀失败,欢迎再次秒杀!");
}
}
return ResponseVo.fail("请登录后再试!");
}
@GetMapping("/miaosha/pay")
public String payKillOrder(String orderSn) throws InterruptedException {
RCountDownLatch latch = this.redissonClient.getCountDownLatch("sec:countdown:" + orderSn);
latch.await();
// 查询订单信息
return "";
}