谷粒商城项目笔记之高级篇(三)

1.9.22 提交订单的问题

1)、订单号显示、应付金额回显

image-20221225215059752

ps:之前代码忘记创建订单之后相应数据,这个方法应该补充以下两项。

image-20221225215120134

  • 出现问题:orderSn长度过长
    • 解决方案:数据库的表中的对应字段长度增大
    • image-20221225215144416
2)、提交订单消息回显

image-20221225220226298

  • confirm.html

image-20221225220241556

3)、为了确保锁库存失败后,订单和订单项也能回滚,需要抛出异常
  • 修改 NoStockException,将库存下的这个异常类直接放到公共服务下。并添加一个构造器。

image-20221225220311704

  • orderServiceImpl

image-20221225220328132


1.9.23 创建库存上锁、解锁 业务交换机&队列

1)、流程梳理

image-20221228184507854

image-20221228184928303

2)、解锁库存实现

①库存服务导入RabbitMQ的依赖

  <!--  RabbitMQ的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependen

②库存服务 RabbitMQ的配置

spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.virtual-host=/

库存服务 配置RabbitMQ的序列化机制

@Configuration
public class MyRabbitConfig {
   
    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
   
        return new Jackson2JsonMessageConverter();
    }
}

④ 库存服务 开启RabbitMQ

1671248966918

⑤ 按照下图创建交换机、队列、绑定关系

1671204724682

1671205296599

统一使用 topic交换机:因为交换机需要绑定多个队列,不同的路由键,且具有模糊匹配功能。

库存服务

package com.atguigu.gulimall.ware.config;

@Configuration
public class MyRabbitConfig {
   
    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
   
        return new Jackson2JsonMessageConverter();
    }

    /**
     出现问题: 并没创建交换机、队列、绑定关系

     出现问题的原因:只有当第一次连接上RabbitMQ时,发现没有这些东西才会创建

     解决方案:监听队列
     * @param message
     */
    @RabbitListener(queues = "stock.release.stock.queue")
    public void handle(Message message){
   

    }

    @Bean
    public Exchange stockEventExchange(){
   
        //String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
        return new TopicExchange("stock-event-exchange",true,false);
    }

    @Bean
    public Queue stockReleaseStockQueue(){
   

        //String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        return new Queue("stock.release.stock.queue",true,false,false,null);
    }

    /**
     * 延时队列
     * @return
     */
    @Bean
    public Queue stockDelayQueue() {
   
        Map<String, Object> arguments = new HashMap<>();

        arguments.put("x-dead-letter-exchange","stock-event-exchange");
        arguments.put("x-dead-letter-routing-key","stock.release");
        arguments.put("x-message-ttl",120000);

        return new Queue("stock.delay.queue",true,false,false,arguments);
    }

    @Bean
    public Binding stockReleaseBinding(){
   
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        // 			Map<String, Object> arguments
       return  new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);
    }

    @Bean
    public Binding stockLockedBinding(){
   
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        // 			Map<String, Object> arguments
        return  new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }

}
3)、测试及问题解决

进行启动测试-------出现问题: 并没创建交换机、队列、绑定关系

出现问题的原因:只有当第一次连接上RabbitMQ时,发现没有这些东西才会创建

解决方案:监听队列

1671249112991

注意:交换机、队列、绑定关系创建成功后,将上述代码注释

1.9.24 监听库存解锁

库存解锁的两种场景:

①下单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁

②下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁

1)、数据库新增字段

①更改数据库表 ---- 库存服务下的wms_ware_order_task_detail

  • 添加两个字段(仓库id和商品锁定状态),方便库存回滚

1671265700522

  • 实体类中需要修改的:

WareOrderTaskDetailEntity 加上 全参和无参构造器方便消息传播该实体类数据。

1671265739895

WareOrderTaskDetailDao.xml 中结果集映射修改

1671265832174

② 保存工作单详情方便回溯

  • WareSkuServiceImpl中的orderLockStock方法中增加下面的代码

1671265892152

1671266289442

③ Common服务中创建To,方便MQ发送消息

1671266016720

package com.atguigu.common.to.mq;

@Data
public class StockLockedTo {
   

    private Long id;//库存工作单的id

 
    private Long detailId;//库存工作单详情id    //接下来需要修改,这里只是暂时的


}

如果To仅仅保存这个两个数据的话,会存在一些问题,
当1号订单在1号仓库扣减1件商品成功,2号订单在2号仓库扣减2件商品成功,3号订单在3号仓库扣减3件商品失败时,库存工作单的数据将会回滚,
此时,数据库中将查不到1号和2号订单的库存工作单的数据,但是库存扣减是成功的,导致无法解锁库存

解决方案: 保存库存工作详情To

package com.atguigu.common.to.mq;

@Data
public class StockDetailTo {
   


    private Long id;
    /**
     * sku_id
     */
    private Long skuId;
    /**
     * sku_name
     */
    private String skuName;
    /**
     * 购买个数
     */
    private Integer skuNum;
    /**
     * 工作单id
     */
    private Long taskId;

    /**
     * 仓库id
     */
    private Long wareId;

    /**
     * 库存锁定状态
     */
    private Integer lockStatus;
}
  • 进行修改

1671266179218

④ WareSkuServiceImpl 中的orderLockStock ----- 向MQ发送库存锁定成功的消息

1671266316108

库存回滚解锁
1)库存锁定
在库存锁定时添加以下逻辑

  • 由于可能订单回滚的情况,所以为了能够得到库存锁定的信息,在锁定时需要记录库存工作单,其中包括订单信息和锁定库存时的信息(仓库id,商品id,锁了几件…)
  • 在锁定成功后,向延迟队列发消息,带上库存锁定的相关信息

逻辑:

  • 遍历订单项,遍历每个订单项的每个库存,直到锁到库存

  • 发消息后库存回滚也没关系,用id是查不到数据库的

  • 锁库存的sql

这里编写了发送消息队列的逻辑,下面写接收消息队列后还原库存的逻辑。

1.9.25 库存解锁逻辑&库存自动解锁完成&测试库存自动解锁

  • 解锁场景:

1.下单成功,库存锁定成功,接下来的业务如果调用失败导致订单回滚。之前锁定的库存就要自动解锁。

2.锁库存失败无需解锁

解决方案:通过查询订单的锁库存信息,如果有则仅仅说明库存锁定成功,还需判断是否有订单信息,如果有订单信息则判断订单状态,若订单状态已取消则解锁库存,反之:不能解锁库存,如果没有订单信息则需要解锁库存,如果没有锁库存信息则无需任何操作。

1.编写Vo,通过拷贝订单实体----> OrderEntity,用于接收订单信息

  • 库存服务中package com.atguigu.gulimall.ware.vo;

1671282013683

2. 远程服务编写,获取订单状态功能

订单服务下编写:OrderController

    @GetMapping("/status/{orderSn}")
    public R getOrderStatus(@PathVariable("orderSn") String orderSn){
   
       OrderEntity orderEntity =  orderService.getOrderByOrderSn(orderSn);
       return R.ok().setData(orderEntity);
    }

实现:

  • OrderServiceImpl
  @Override
    public OrderEntity getOrderByOrderSn(String orderSn) {
   
        OrderEntity order_sn = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));

        return order_sn;
    }

库存服务下编写:调用远程服务 OrderFeignService

@FeignClient("gulimall-order")
public interface OrderFeignService {
   

    @GetMapping("/order/order/status/{orderSn}")
    R getOrderStatus(@PathVariable("orderSn") String orderSn);
}

3.监听事件

接收消息

  • 延迟队列会将过期的消息路由至"stock.release.stock.queue",通过监听该队列实现库存的解锁
  • 为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队

库存服务 编写 StockReleaseListener

当@RabbitListener标注在类上,需要搭配@RabbitHandler一起使用

1671282189427

@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
   

    @Autowired
    WareSkuService wareSkuService;

    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
   
        System.out.println("收到解锁库存的消息...");
        try {
   
            wareSkuService.unlockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
   
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

WareSkuServiceImpl

库存解锁

  • 如果工作单详情不为空,说明该库存锁定成功

    • 查询最新的订单状态,
    • 如果订单不存在,说明订单提交出现异常回滚,
    • 如果订单存在(但订单处于已取消的状态),我们要对已锁定的库存进行解锁
  • 如果工作单详情为空,说明库存未锁定,自然无需解锁

  • 为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁


    @Autowired
    WareSkuDao wareSkuDao;

    @Autowired
    WareOrderTaskService orderTaskService;

    @Autowired
    WareOrderTaskDetailService orderTaskDetailService;


    @Autowired
    RabbitTemplate rabbitTemplate;

    @Autowired
    OrderFeignService orderFeignService;


/**
     * 1、库存自动解锁。
     * 下订单成功,库存锁定成功,接下来的业务如果调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
     * 2、订单失败。
     * 锁库存失败。
     * <p>
     * 只要解锁库存的消息失败。一定要告诉服务器解锁失败。
     *
     * @param to
     */
    @Override
    public void unlockStock(StockLockedTo to) {
   
        StockDetailTo detail = to.getDetail();
        Long detailId = detail.getId();
        //解锁
        //1、查询数据库关于这个订单的锁定库存信息。
        //有:证明库存锁定成功了。
        //    解锁:查询订单情况。
        //          1、没有这个订单。必须解锁
        //          2、有这个订单。不是解锁库存。
        //                订单状态:已取消:解锁库存
        //                         没取消:不能解锁
        //没有:库存锁定失败了,库存回滚了。这种情况无需解锁
        WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
        if (byId != null) {
   
            //解锁
            Long id = to.getId();
            WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
            String orderSn = taskEntity.getOrderSn();//根据订单号查询订单的状态
            R r = orderFeignService.getOrderStatus(orderSn);
            if (r.getCode() == 0) {
   
                //订单数据返回成功
                OrderVo data = r.getData(new TypeReference<OrderVo>() {
   
                });
                if (data == null || data.getStatus() == 4) {
   
                    //订单不存在
                    //订单已经被取消了。才能解锁库存
                    // detailId
                    if (byId.getLockStatus() == 1){
   
                        //当前库存工作单详情,状态 是1 :已锁定但是未解锁才可以解锁
                        unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
                    }
                }
            } else {
   
                //消息拒绝以后重新放到队里里面,让别人继续消费解锁。
                throw new RuntimeException("远程服务失败");
            }
        } else {
   
            //无需解锁
        }
    }

    /**
     * 解锁方法
     */
    private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
   
        //库存解锁
        wareSkuDao.unlockStock(skuId, wareId, num);
        //更新库存工作单的状态
        WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
        entity.setId(taskDetailId);
        entity.setLockStatus(2);//变为已解锁
        orderTaskDetailService.updateById(entity);

    }

WareSkuDao.xml

SQL编写:

UPDATE `wms_ware_sku` SET stock_locked = stock_locked -1
WHERE sku_id = 1 AND ware_id = 2
    <update id="unlockStock">
        UPDATE `wms_ware_sku` SET stock_locked = stock_locked -#{num}
        WHERE sku_id = #{skuId} AND ware_id = #{wareId}
    </update>

4. 远程服务调用可能会出现失败,需要设置手动ACK,确保其它服务能消费此消息

库存服务下

#手动ACK设置
spring.rabbitmq.listener.simple.acknowledge-mode=manual

5.出现问题: 远程调用订单服务时被拦截器拦截

解决方案:请求路径适配放行

订单服务下的 拦截器。

1671282561293

1.9.26 定时关单完成

image-20221228220124369

1.定时关单代码编写

①订单创建成功,给MQ发送关单消息

订单服务下的 OrderServiceImpl 的 submitOrder提交订单方法

1671288070746

② 监听事件,进行关单

订单服务下

package com.atguigu.gulimall.order.listener;


@Service
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {
   

    @Autowired
    OrderService orderService;

    /**
     * 定时关单
     * @param entity
     * @param channel
     * @param message
     * @throws IOException
     */
    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
   
        System.out.println("收到过期的订单消息:准备关闭订单" + entity.getOrderSn());
        try {
   
            orderService.closeOrder(entity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
   
            //失败了重新放到队列中,让其他消费者能够消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

OrderServiceImpl

/**
     * 关单操作
     * @param entity
     */
    @Override
    public void closeOrder(OrderEntity entity) {
   

        //查询当前这个订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
   
            //关单
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);
        }

    }

1671288293945

订单释放和库存解锁逻辑: 当订单创建成功,1分钟之后,向MQ发送关单消息,2分钟后,向MQ发送解锁库存消息,关单操作完成之后,过了1分钟解锁库存操作。

存在问题:由于机器卡顿、消息延迟等导致关单消息延迟发送,解锁库存消息正常发送和监听,导致解锁库存消息被消费,当执行完关单操作后便无法再执行解锁库存操作,导致卡顿的订单永远无法解锁库存。

解决方案:采取主动补偿的策略。当关单操作正常完成之后,主动去发送解锁库存消息给MQ,监听解锁库存消息进行解锁。
1671288628987

③ 按上图创建绑定关系

订单服务MyMQConfig

1671288668918

④ common服务中,创建OrderTo(拷贝order实体)

1671288720098

⑤ 向MQ发送解锁库存消息

  • OrderServiceImpl

1671288876594

 package com.atguigu.gulimall.order.service.impl;


/**
     * 关单操作
     * @param entity
     */
    @Override
    public void closeOrder(OrderEntity entity) {
   

        //查询当前这个订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
   
            //未付款状态,进行关单
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity,orderTo);
            //发给MQ一个消息:解锁库存
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
        }

    }

⑥ 解锁库存操作

库存服务下

1671288764555

    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
   
        System.out.println("订单关闭准备解锁库存...");
        try {
   
            wareSkuService.unlockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
   
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

1671288950151

//防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期。查订单状态新建状态,什么都不做就走了。
    //导致卡顿的订单,永远不能解锁库存。
    @Transactional
    @Override
    public void unlockStock(OrderTo orderTo) {
   

        String orderSn = orderTo.getOrderSn();
        //查一下最新库存的状态,防止重复解锁库存
        WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = task.getOrderId();
        //按照工作单找到所有 没有解锁的库存,进行解锁
        List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(
                new QueryWrapper<WareOrderTaskDetailEntity>()
                        .eq("task_id", id)
                        .eq("lock_status", 1));

        // Long skuId, Long wareId, Integer num, Long taskDetailId

        for (WareOrderTaskDetailEntity entity : entities) {
   
            unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
        }
    }

WareOrderTaskServiceImpl

  @Override
    public WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {
   

        WareOrderTaskEntity one = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
        return one;

    }

1.9.27 消息丢失、积压、重复等解决方案

  • 如何保证消息可靠性-消息丢失

1、消息丢失

  • 消息发送出去,由于网络问题没有抵达服务器

  • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式

  • 做好日志记录,每个消息状态是否都被服务器收到都应该记录

  • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发

  • 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。

  • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。

  • 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机

  • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

情况一: 消息发送出去但是由于网络原因未到达服务器,解决方案:采用try-catch将发送失败的消息持久化到数据库中,采用定期扫描重发的方式。

drop table if exists mq_message;
CREATE TABLE `mq_message` (
	`message_id` CHAR(32) NOT NULL,
	`content` TEXT,#json
	`to_exchange` VARCHAR(255) DEFAULT NULL,
	`routing_key` VARCHAR(255) DEFAULT NULL,
	`class_type` VARCHAR(255) DEFAULT NULL,
	`message_status` INT(1) DEFAULT '0' COMMENT '0-新建  1-已发送  2-错误抵达  3-已抵达',
	`create_time` DATETIME DEFAULT NULL,
	`update_time` DATETIME DEFAULT NULL,
	PRIMARY KEY (`message_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4

情况二:消息抵达服务器的队列中才算完成消息的持久化,解决方案----->publish + consumer的两端的ack机制

1671291076371

情况三: 防止自动ack带来的缺陷,采用手动ack,解决方案上面都有这里不再细说

  • 如何保证消息可靠性-消息重复

2、消息重复

  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
    • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
    • 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
    • rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的

消息被成功消费,ack时宕机,消息由unack变成ready,Broker又重新发送。解决方案:将消费者的业务消费接口应该设计为幂等性的,比如扣库存有工作单的状态标志。

  • 如何保证消息可靠性-消息积压

3、消息积压

  • 消费者宕机积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大
    • 上线更多的消费者,进行正常消费
    • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

消息积压即消费者的消费能力不够, 上线更多的消费者进行正常的消费。


1.9.28 支付服务

1)、支付宝沙箱代码
① 文档和资料

支付宝开放平台传送门:支付宝开放平台

网站支付DEMO传送门:手机网站支付 DEMO | 网页&移动应用

沙箱应用传送门:沙箱应用

密钥工具下载传送门:密钥工具

沙箱环境文档:沙箱环境文档

  • 下载手机网站支付DEMO,使用eclipse导入

1671354960765

1671354989025

2)、RSA、加密加签、密钥等相关概念

1671355152168

对称加密:发送方和接收方用的是同一把密钥,存在问题:当某一方将密钥泄漏之后,发送的消息可以被截取获悉并且随意进行通信。

1671355199521

非对称加密:发送方和接收方使用的不是同一把密钥,发送方使用密钥A对明文进行加密,接收方使用密钥B对密文进行解密,然后接收方将回复的明文用密钥C进行加密,发送方使用密钥D进行解密。采用非对称加密的好处是:即使有密钥被泄漏也不能自由的通信

密钥的公私性是相对于生成者而言的。发送方通过密钥A对明文进行加密,密钥A是只有发送方自己知道的,接收方想要解密密文,就需要拿到发送方公布出来的密钥B。

公钥:生成者发布的密钥可供大家使用的

私钥:生成者自己持有的密钥

1671355422855

签名:为了防止中途传输的数据被篡改和使用的方便,发送方采用私钥生成明文对应的签名,此过程被成为加签。接收方使用公钥去核验明文和签名是否对应,此过程被成为验签。

配置支付宝的沙箱环境:

沙箱环境配置查看传送门:登录 - 支付宝

1671355503281

接口加签方式共有两种:

①采用系统默认生成的支付宝的公钥、应用的私钥和公钥:

1671355561472

② 采用自定义密钥

1671355636513

传送门:密钥工具下载

1671355723785

将支付宝密钥工具生成的应用公钥复制进加签内容配置中,会自动生成支付宝的公钥

  • 沙箱账号:用于测试环境中的商品支付

沙箱账号

1671355890188

  • 使用eclipse测试:

注意如果项目报红:因为老师给的 沙箱测试Demo 默认使用的 是 tomact7.0 ,所以这里需要将 tomact7.0移除掉,使用我们自己本机安装的 tomcat。

1671356066793

选择 tomcat 7.0移除 ,然后导入自己的 就行。这里我已经移除好了。

1671356110313

参考:当eclipse导入新项目后出现红叉解决办法


  • 老师课件:

一、支付宝支付
1、进入“蚂蚁金服开放平台”
https://open.alipay.com/platform/home.htm

2、下载支付宝官方demo,进行配置和测试

文档地址
https://open.alipay.com/platform/home.htm 支付宝&蚂蚁金服开发者平台

1671356335327

https://docs.open.alipay.com/catalog 开发者文档
https://docs.open.alipay.com/270/106291/ 全部文档=>电脑网站支付文档;下载demo

1671356349583

3、配置使用沙箱进行测试
1、使用RSA 工具生成签名
2、下载沙箱版钱包
3、运行官方demo 进行测试

4、什么是公钥、私钥、加密、签名和验签?
1、公钥私钥
公钥和私钥是一个相对概念
它们的公私性是相对于生成者来说的。
一对密钥生成后,保存在生成者手里的就是私钥,
生成者发布出去大家用的就是公钥

2、加密和数字签名

  • 加密是指:

    • 我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解密的技术。
    • 公钥和私钥都可以用来加密,也都可以用来解密。
    • 但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。
    • 加密的目的是:
      • 为了确保数据传输过程中的不可读性,就是不想让别人看到。
  • 签名:

    • 给我们将要发送的数据,做上一个唯一签名(类似于指纹)
    • 用来互相验证接收方和发送方的身份;
    • 在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以
      用来达到数据的明文传输。
  • 验签

    • 支付宝为了验证请求的数据是否商户本人发的,
    • 商户为了验证响应的数据是否支付宝发的
3)、 内网穿透

1671369972076

如果别人直接访问我们自己电脑的本地项目,是不能访问的。

1671370039988

内网穿透的原理: 内网穿透服务商是正常外网可以访问的ip地址,我们的电脑通过下载服务商软件客户端并与服务器建立起长连接,然后服务商就会给我们的电脑分配一个域名,别人的电脑访问hello.hello.com会先找到hello.com即一级域名,然后由服务商将请求转发给我们电脑的二级域名;从而实现了别人可以通过IP地址访问我们本地的项目。

下面是老师课件:

二、内网穿透

1、简介
内网穿透功能可以允许我们使用外网的网址来访问主机;
正常的外网需要访问我们项目的流程是:

  • 1、买服务器并且有公网固定IP
  • 2、买域名映射到服务器的IP
  • 3、域名需要进行备案和审核

2、使用场景

  • 1、开发测试(微信、支付宝)
  • 2、智慧互联
  • 3、远程控制
  • 4、私有云

3、内网穿透的几个常用软件
1、natapp:https://natapp.cn/ 优惠码:022B93FD(9 折)[仅限第一次使用]
2、续断:www.zhexi.tech 优惠码:SBQMEA(95 折)[仅限第一次使用]
3、花生壳:https://www.oray.com/

老师课件中使用的 是 续断进行测试的,这里我们使用免费的内网穿透工具进行测试。

内网穿免费工具下载地址:cpolar - 安全的内网穿透工具

使用教程: Win系统如何下载安装使用cpolar内网穿透工具?_Cpolar Lisa的博客-CSDN博客

超好用的内网穿透工具【永久免费不限制流量】

上面两个 教程都可以参考,个人推荐 第二个教程。

已经成功建立连接。

1671370917715

【需要注意的是,对于免费版本的cpolar随机URL地址是会在24小时之后变化的,如果需要进一步使用,可以将站点配置成二级子域名,或自定义域名(使用自己的域名)长期使用。】

  • 配置修改:

修改url前缀:

1671370868841

  • 测试:访问成功。

1671370825139

4)、 整合支付

注意,需要保证所有项目的编码格式都是 utf-8

1671432147193

1.导入支付宝支付SDK的依赖

阿里支付依赖传送门

订单服务

  <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
        <!--导入支付宝的SDK-->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.35.7.ALL</version>
        </dependency>

2. 编写AlipayTemplate工具类和PayVo

直接复制老师给的课件。

1671376504843

1671376519219

更改一些,这里我使用的是 绑定配置文件的方式来声明一些变量的:因为老师使用了一个@ConfigurationProperties(prefix = “alipay”)。

  • AlipayTemplate
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
   

    //在支付宝创建的应用的id
    @Value("${alipay.app_id}")//这里使用的是绑定配置文件的方式
    private   String app_id;

    // 商户私钥,您的PKCS8格式RSA2私钥
    @Value("${alipay.merchant_private_key}")
    private  String merchant_private_key;

    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    @Value("${alipay.alipay_public_key}")
    private  String alipay_public_key;

    // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
    @Value("${alipay.notify_url}")
    private  String notify_url;

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    //同步通知,支付成功,一般跳转到成功页
    @Value("${alipay.return_url}")
    private  String return_url;

    // 签名方式
    private  String sign_type = "RSA2";

    // 字符编码格式
    private  String charset = "utf-8";

    // 支付宝网关; https://openapi.alipaydev.com/gateway.do
    @Value("${alipay.gatewayUrl}")
    private  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 +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        String result = alipayClient.pageExecute(alipayRequest).getBody();

        //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
        System.out.println("支付宝的响应:"+result);

        return result;

    }
}

application.properties

1671376692140PayVo

//封装数据

@Data
public class PayVo {
   
    private String out_trade_no; // 商户订单号 必填
    private String subject; // 订单名称 必填
    private String total_amount;  // 付款金额 必填
    private String body; // 商品描述 可空
}

3.访问支付接口

1671376749323

4. 编写支付接口

produces属性:用于设置返回的数据类型

AlipayTemplate的pay()方法返回的就是一个用于浏览器响应的付款页面

  • 订单服务下PayWebController
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 = new PayVo();
        // payVo.setBody();//订单的备注
        // payVo.setOut_trade_no();//订单号
        // payVo.setSubject()
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值