谷粒商城篇章10 -- P262-P291/P295-P310 -- 订单服务(支付)【分布式高级篇七】

目录

1 页面环境搭建

1.1 静态资源上传到nginx

1.2 SwitchHosts增加配置

1.3 网关配置

1.4 订单模块基础配置

1.4.1 引入 thymeleaf 依赖

1.4.2 application.yml配置

1.4.3 bootstrap.properties配置

1.4.4 开启nacos注册发现和远程调用

1.5 修改各个页面的静态资源路径

1.6 测试

1.6.1 订单确认页

1.6.2 订单详情页

1.6.3 订单列表页

1.6.4 订单支付页

2 整合SpringSession

2.1 引入依赖

2.2 开启SpringSession

2.3 session数据存储方式配置

2.4 修改订单相关页面同步用户登录信息

2.5 整合效果

3 自定义线程池

3.1 线程池属性配置类

3.2 yml中线程池相关配置

3.3 自定义线程池

4 订单基本概念

4.1 订单中心

4.1.1 订单构成

4.1.1.1 用户信息

4.1.1.2 订单基础信息

4.1.1.3 商品信息

4.1.1.4 优惠信息

4.1.1.5 支付信息

4.1.1.6 物流信息

4.1.2 订单状态

4.2 订单流程

4.2.1 订单创建与支付

4.2.2 逆向流程

5 订单实现

5.1 订单登录拦截

5.2 订单确认页

5.2.1 模型抽取

5.2.1.1 订单确认页Vo

5.2.1.2 订单页用户收货地址Vo

 5.2.1.3 订单页购物项Vo

5.2.2 订单确认页流程

5.2.3 功能实现

5.2.3.1 controller层

5.2.3.2 service层

5.2.3.3 远程调用接口

5.2.2.3.1 远程查询会员所有的收货地址

5.2.2.3.2 远程查询购物车中所有选中的购物项 

5.2.2.3.3 远程查询商品库存信息

5.2.4 Feign远程调用丢失请求头问题

5.2.5 Feign异步情况丢失上下文问题

5.2.6 创建防重令牌

5.2.7 模拟运费效果

5.2.7.1 后端实现

5.2.7.2 前端实现

5.2.8 提交订单

5.2.8.1 封装下单接口入参出参vo

5.2.8.1.1 订单提交接口入参vo

5.2.8.1.2 订单提交接口出参vo

5.2.8.2 原子验证防重令牌

5.2.8.3 创建订单、订单项等信息

5.2.8.4 验价

5.2.8.5 保存订单

5.2.8.6 锁定库存

5.2.8.7 提交订单完整代码

5.2.8.8 提交订单的问题

5.2.8.9 Seata分布式事务

5.2.8.9.1 Windows下安装Seata

5.2.8.9.2 服务端整合Seata(AT模式)

5.2.8.9.3 启用分布式事务

5.2.8.9.4 测试

5.2.8.10 最终一致性库存解锁逻辑:【可靠消息+最终一致性】【库存解锁】

5.2.8.10.1 库存服务整合RabbitMQ,创建交换机、队列、绑定

5.2.8.10.2 监听库存解锁

5.2.8.11 最终一致性库存解锁逻辑:【可靠消息+最终一致性】【定时关单】

5.2.8.11.1 订单服务整合RabbitMQ,创建交换机、队列、绑定

5.2.8.11.2 监听订单定时关单

5.2.8.11.3 订单创建成功,机器卡顿,消息延迟导致库存无法解锁(关单逻辑升级) 

5.2.8.12 消息丢失、积压、重复等解决方案(如何保证消息可靠性?)

5.2.8.12.1 消息丢失

5.2.8.12.2 消息重复

5.2.8.12.3 消息积压

5.3  订单支付页

5.3.1 加密分类

5.3.1.1 对称加密

5.3.1.2 非对称加密

5.3.2 支付宝支付

5.3.2.1 支付宝加密原理

5.3.2.1.1 什么是公钥、 私钥、 加密、 签名和验签

5.3.2.2 支付宝-电脑网站支付Demo测试

5.3.2.2.1 使用支付宝沙箱环境进行测试

5.3.2.2.2 系统默认密钥

5.3.2.2.3 修改Demo中配置AlipayConfig 

5.3.2.2.4 启动Demo测试

5.3.2.3 支付宝支付流程

5.3.3 内网穿透

5.3.3.1 简介

5.3.3.2 使用场景

5.3.3.3 内网穿透的几个常用软件

5.3.3.4 natapp内网穿透

5.3.4 整合支付

5.3.4.1 导入依赖

5.3.4.2 yml配置 

5.3.4.3 支付Vo

5.3.4.4 阿里云支付模板

5.3.4.5 订单支付宝支付接口

5.3.4.6 前端页面修改pay.html

5.3.4.7 支付测试

5.3.5 订单列表页渲染(member服务)

5.3.5.1 静态资源上传到nginx

5.3.5.2 会员服务整合thymeleaf

5.3.5.3 网关配置

5.3.5.4 SwitchHosts添加配置

5.3.5.5 整合SpringSession(登录后才可以查看订单信息)

5.3.5.6 配置拦截器

5.3.5.7 订单支付成功回调页面接口

5.3.5.8 设置支付宝支付成功回调url

5.3.5.9 订单列表页渲染 

5.3.6 接收支付宝异步通知

5.3.6.1 支付宝异步通知信息vo

5.3.6.2 接收支付宝异步通知接口

5.3.6.3 登录拦截器放行异步通知接口

5.3.6.4 设置支付宝异步通知路径

5.3.6.5 设置接收异步通知信息相关日期格式

5.3.6.6 解决订单号长度报错

5.3.7 异步通知内网穿透环境搭建 

5.3.7.1 修改内网穿透隧道配置  

5.3.7.2 修改nginx配置

5.3.8 支付测试

5.3.9 收单

6 接口幂等性 

6.1 什么是幂等性

6.2 哪些情况需要防止

6.3 什么情况下需要幂等

6.4 幂等解决方案

6.4.1 token机制

6.4.2 各种锁机制

6.4.2.1 数据库悲观锁

6.4.2.2 数据库乐观锁

6.4.2.3 业务层分布式锁

6.4.3 各种唯一约束

6.4.3.1 数据库唯一约束

6.4.3.2 redis set 防重

6.4.4 防重表

6.4.5 全局请求唯一id

7 本地事务与分布式事务

7.1 本地事务

7.1.1 事务的基本性质

7.1.2 事务的隔离级别

7.1.3 事务的七种传播行为

7.1.4 SpringBoot 事务关键点

7.1.4.1 事务的自动配置

7.1.4.2 事务的坑 

7.2 分布式事务

7.2.1 为什么有分布式事务

7.2.2 CAP定理与BASE理论

7.2.2.1 CAP定理

7.2.2.2 面临问题

7.2.2.3 BASE理论

7.2.2.4 强一致性、弱一致性、最终一致性

7.2.3 分布式事务的几种方案

7.2.3.1 2PC模式

7.2.3.2 柔性事务-TCC事务补偿型方案(手动补偿)

7.2.3.3 柔性事务-最大努力通知型方案

7.2.3.4 柔性事务-可靠消息+最终一致性方案(异步确保型)


1 页面环境搭建

1.1 静态资源上传到nginx

等待付款 -》detail

订单页    -》list

结算页    -》confirm

收银页    -》pay

1.2 SwitchHosts增加配置

添加订单服务的域名与ip映射:xxx.xxx.11.10 order.gulimall.com 

1.3 网关配置

gulimall-gateway/src/main/resources/application.yml

- id: gulimall_order_route
  uri: lb://gulimall-order
  predicates:
    # 由以下的主机域名访问转发到订单服务
    - Host=order.gulimall.com

1.4 订单模块基础配置

1.4.1 引入 thymeleaf 依赖

gulimall-order/pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

1.4.2 application.yml配置

开启nacos注册发现、关闭thymeleaf缓存。

gulimall-order/src/main/resources/application.yml

spring:
  cloud:
    nacos:
      discovery:
        server-addr: xxx.xxx.xxx.10:8848
  thymeleaf:
    cache: false

1.4.3 bootstrap.properties配置

服务名、开启nacos配置中心(可以不开启)。

gulimall-order/src/main/resources/bootstrap.properties

spring.application.name=gulimall-order
spring.cloud.nacos.config.server-addr=xxx.xxx.xxx.10:8848
spring.cloud.nacos.config.namespace=de91d4bf-xxxx-xxxx-b095-d8ac87337d8a

1.4.4 开启nacos注册发现和远程调用

主类开启注册发现、远程调用。

@EnableFeignClients // 远程调用
@EnableDiscoveryClient // 注册发现

1.5 修改各个页面的静态资源路径

1. confirm.html

src="  =>  src="/static/order/confirm/

href="  =>  href="/static/order/confirm/

2. detail.html

src="  =>  src="/static/order/detail/

href="  =>  href="/static/order/detail/

3. list.html

src="  =>  src="/static/order/list/

href="  =>  href="/static/order/list/

4. pay.html

src="  =>  src="/static/order/pay/

href="  =>  href="/static/order/pay/

1.6 测试

测试代码

gulimall-order/src/main/java/com/wen/gulimall/order/web/HelloController.java

@Controller
public class HelloController {

    /**
     * 测试订单相关页面访问
     * @param page
     * @return
     */
    @GetMapping("/{page}.html")
    public String listPage(@PathVariable String page){

        return page;
    }
}

1.6.1 订单确认页

注意:如果页面展示不全,删除确认页面注释代码中的 /*。

http://order.gulimall.com/confirm.html

1.6.2 订单详情页

 http://order.gulimall.com/detail.html

1.6.3 订单列表页

http://order.gulimall.com/list.html

1.6.4 订单支付页

http://order.gulimall.com/pay.html

2 整合SpringSession

注意: SpringSession的配置类GulimallSessionConfig.java在公共模块(gulimall-common)已经配置,这里购物车模块直接引入公共模块即可。

公共模块(gulimall-common)可以参考我之前的博客:谷粒商城篇章7 ---- P211-P235 ---- 认证服务【分布式高级篇四】_谷粒商城p235-优快云博客

2.1 引入依赖

<!--	整合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>

2.2 开启SpringSession

gulimall-order/src/main/java/com/wen/gulimall/order/GulimallOrderApplication.java

主类上添加以下注解:

@EnableRedisHttpSession

2.3 session数据存储方式配置

gulimall-order/src/main/resources/application.yml

spring:
  redis:
    host: 172.1.11.10
  session:
    store-type: redis

2.4 修改订单相关页面同步用户登录信息

1.订单详情页detail.html的134-140行,修改如下:

<li style="width: 100px">
    <a href="" th:if="${session.loginUser}!=null">欢迎:[[${session.loginUser==null?"":session.loginUser.nickname}]]</a>
    <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser}==null">欢迎,请登录</a>
</li>
<li th:if="${session.loginUser}==null">
    <a href="http://auth.gulimall.com/reg.html" class="li_2">免费注册</a>
</li>

2.订单确认页confirm.html的78行,修改如下:
<li>[[${session.loginUser==null?"":session.loginUser.nickname}]]<img src="/static/order/confirm/img/03.png" style="margin-bottom: 0px;margin-left3: 3px;" /><img src="/static/order/confirm/img/06.png" /></li>

3.订单支付页pay.html的14行,修改如下:
<li><span>[[${session.loginUser==null?"":session.loginUser.nickname}]]</span><span>退出</span></li>

2.5 整合效果

登录成功后用户信息共享,如下:

3 自定义线程池

3.1 线程池属性配置类

gulimall-order/src/main/java/com/wen/gulimall/order/config/ThreadPoolConfigProperties.java

@Data
@Component
@ConfigurationProperties(prefix = "gulimall.thread")
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

3.2 yml中线程池相关配置

gulimall:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10

3.3 自定义线程池

gulimall-order/src/main/java/com/wen/gulimall/order/config/MyThreadConfig.java

@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties poolProperties){
        return new ThreadPoolExecutor(poolProperties.getCoreSize(),
                poolProperties.getMaxSize(),
                poolProperties.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

    }
}

4 订单基本概念

4.1 订单中心

电商系统涉及到 3 流, 分别时信息流, 资金流, 物流, 而订单系统作为中枢将三者有机的集
合起来。
订单模块是电商系统的枢纽, 在订单这个环节上需求获取多个模块的数据和信息, 同时对这
些信息进行加工处理后流向下个环节, 这一系列就构成了订单的信息流通。

4.1.1 订单构成

4.1.1.1 用户信息

        用户信息包括用户账号、 用户等级、 用户的收货地址、 收货人、 收货人电话等组成, 用户账户需要绑定手机号码, 但是用户绑定的手机号码不一定是收货信息上的电话。 用户可以添加多个收货信息, 用户等级信息可以用来和促销系统进行匹配, 获取商品折扣, 同时用户等级还可以获取积分的奖励等。

4.1.1.2 订单基础信息

        订单基础信息是订单流转的核心, 其包括订单类型、 父/子订单、 订单编号、 订单状态、 订单流转的时间等。

(1) 订单类型包括实体商品订单和虚拟订单商品等, 这个根据商城商品和服务类型进行区分。

(2) 同时订单都需要做父子订单处理, 之前在初创公司一直只有一个订单, 没有做父子订单处理后期需要进行拆单的时候就比较麻烦, 尤其是多商户商场, 和不同仓库商品的时候,父子订单就是为后期做拆单准备的。

(3) 订单编号不多说了, 需要强调的一点是父子订单都需要有订单编号, 需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。

(4) 订单状态记录订单每次流转过程, 后面会对订单状态进行单独的说明。

(5) 订单流转时间需要记录下单时间, 支付时间, 发货时间, 结束时间/关闭时间等等

4.1.1.3 商品信息

        商品信息从商品库中获取商品的 SKU 信息、 图片、 名称、 属性规格、 商品单价、 商户信息等, 从用户下单行为记录的用户下单数量, 商品合计价格等。

4.1.1.4 优惠信息

        优惠信息记录用户参与的优惠活动, 包括优惠促销活动, 比如满减、 满赠、 秒杀等, 用户使用的优惠券信息, 优惠券满足条件的优惠券需要默认展示出来, 具体方式已在之前的优惠券篇章做过详细介绍, 另外还虚拟币抵扣信息等进行记录。

为什么把优惠信息单独拿出来而不放在支付信息里面呢?

因为优惠信息只是记录用户使用的条目, 而支付信息需要加入数据进行计算, 所以做为区分。

4.1.1.5 支付信息

(1) 支付流水单号, 这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号, 财务通过订单号和流水单号与支付通道进行对账使用。

(2) 支付方式用户使用的支付方式, 比如微信支付、 支付宝支付、 钱包支付、 快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。

(3) 商品总金额, 每个商品加总后的金额; 运费, 物流产生的费用; 优惠总金额, 包括促销活动的优惠金额, 优惠券优惠金额, 虚拟积分或者虚拟币抵扣的金额, 会员折扣的金额等之和; 实付金额, 用户实际需要付款的金额。

        用户实付金额=商品总金额+运费-优惠总金额

4.1.1.6 物流信息

        物流信息包括配送方式, 物流公司, 物流单号, 物流状态, 物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。

4.1.2 订单状态

1. 待付款
        用户提交订单后, 订单进行预下单, 目前主流电商网站都会唤起支付, 便于用户快速完成支
付, 需要注意的是待付款状态下可以对库存进行锁定, 锁定库存需要配置支付超时时间, 超时后将自动取消订单, 订单变更关闭状态。
2. 已付款/待发货
        用户完成订单支付, 订单系统需要记录支付时间, 支付流水单号便于对账, 订单下放到 WMS系统, 仓库进行调拨, 配货, 分拣, 出库等操作。
3. 待收货/已发货
        仓储将商品出库后, 订单进入物流环节, 订单系统需要同步物流信息, 便于用户实时知悉物
品物流状态
4. 已完成
        用户确认收货后, 订单交易完成。 后续支付侧进行结算, 如果订单存在问题进入售后状态
5. 已取消
        付款之前取消订单。 包括超时未付款或用户商户取消订单都会产生这种订单状态。
6. 售后中
        用户在付款后申请退款, 或商家发货后用户申请退换货。
        售后也同样存在各种状态, 当发起售后申请后生成售后订单, 售后订单状态为待审核, 等待
商家审核, 商家审核通过后订单状态变更为待退货, 等待用户将商品寄回, 商家收货后订单状态更新为待退款状态, 退款到用户原账户后订单状态更新为售后成功。

4.2 订单流程

        订单流程是指从订单产生到完成整个流转的过程, 从而行程了一套标准流程规则。 而不同的产品类型或业务类型在系统中的流程会千差万别, 比如上面提到的线上实物订单和虚拟订单的流程, 线上实物订单与 O2O 订单等, 所以需要根据不同的类型进行构建订单流程。

        不管类型如何订单都包括正向流程和逆向流程, 对应的场景就是购买商品和退换货流程, 正向流程就是一个正常的网购步骤: 订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后, 订单是如何在多系统之间交互流转的, 可概括如下图

4.2.1 订单创建与支付

  1. 订单创建前需要预览订单, 选择收货信息等    
  2. 订单创建需要锁定库存, 库存有才可创建, 否则不能创建
  3. 订单创建后超时未支付需要解锁库存
  4. 支付成功后, 需要进行拆单, 根据商品打包方式, 所在仓库, 物流等进行拆单
  5. 支付的每笔流水都需要记录, 以待查账
  6. 订单创建, 支付成功等状态都需要给 MQ 发送消息, 方便其他系统感知订阅

4.2.2 逆向流程

  1. 修改订单, 用户没有提交订单, 可以对订单一些信息进行修改, 比如配送信息,优惠信息,及其他一些订单可修改范围的内容, 此时只需对数据进行变更即可。
  2. 订单取消, 用户主动取消订单和用户超时未支付, 两种情况下订单都会取消订单, 而超时情况是系统自动关闭订单, 所以在订单支付的响应机制上面要做支付的限时处理, 尤其是在前面说的下单减库存的情形下面, 可以保证快速的释放库存。另外需要需要处理的是促销优惠中使用的优惠券, 权益等视平台规则, 进行相应补回给用户。
  3. 退款, 在待发货订单状态下取消订单时, 分为缺货退款和用户申请退款。 如果是全部退款则订单更新为关闭状态, 若只是做部分退款则订单仍需进行进行, 同时生成一条退款的售后订单, 走退款流程。 退款金额需原路返回用户的账户。
  4. 发货后的退款, 发生在仓储货物配送, 在配送过程中商品遗失, 用户拒收, 用户收货后对商品不满意, 这样情况下用户发起退款的售后诉求后, 需要商户进行退款的审核, 双方达成一致后, 系统更新退款状态, 对订单进行退款操作, 金额原路返回用户的账户, 同时关闭原订单数据。 仅退款情况下暂不考虑仓库系统变化。 如果发生双方协调不一致情况下, 可以申请平台客服介入。 在退款订单商户不处理的情况下, 系统需要做限期判断, 比如 5 天商户不处理, 退款单自动变更同意退款 。

5 订单实现

5.1 订单登录拦截

        订单系统必然会涉及到用户相关信息,所以进入订单系统的所有请求必须是已登录的状态下。这里编写用户登录拦截器,对未登录情况下的订单请求进行拦截。

自定义拦截器

gulimall-order/src/main/java/com/wen/gulimall/order/interceptor/LoginUserInterceptor.java

/**
 * @author W
 * @createDate 2024/02/27
 * @description: 登录拦截器
 * 从session中(redis中)获取了登录信息,封装到ThreadLocal
 * 自定义拦截器需要添加到webmvc中,否则不起作用
 */
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    // 同一个线程共享数据
    private static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        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.com/login.html");
            return false;
        }
    }
}

将拦截器添加到webMvc中

gulimall-order/src/main/java/com/wen/gulimall/order/config/OrderWebConfiguration.java

@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
    @Resource
    private LoginUserInterceptor interceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 订单服务的所有请求都要走登录拦截
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

5.2 订单确认页

5.2.1 模型抽取

5.2.1.1 订单确认页Vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderConfirmVo.java

public class OrderConfirmVo {
    // 收货地址
    @Setter @Getter
    private List<MemberAddressVo> address;
    // 所有选中的购物项
    @Setter @Getter
    private List<OrderItemVo> items;
    // 发票信息
    // 优惠券信息...,这里使用积分
    @Setter @Getter
    private Integer integration;

    // 订单的防重令牌
    @Setter @Getter
    private String orderToken;

    @Setter @Getter
    private Map<Long, Boolean> stocks;

    // 商品总数量
    public Integer getCount(){
        Integer i = 0;
        if(items!=null){
            for (OrderItemVo item : items) {
                i+=item.getCount();
            }
        }
        return i;
    }
    // 订单总额
    //private BigDecimal total;

    public BigDecimal getTotal() {
        BigDecimal sum = new BigDecimal("0");
        if(items!=null){
            for (OrderItemVo item : items) {
                BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
                sum = sum.add(multiply);
            }
        }
        return sum;
    }

    // 应付价格
    //private BigDecimal payPrice;

    public BigDecimal getPayPrice() {
        return getTotal();
    }
}
5.2.1.2 订单页用户收货地址Vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/MemberAddressVo.java

@Data
public class MemberAddressVo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 收货人姓名
     */
    private String name;
    /**
     * 电话
     */
    private String phone;
    /**
     * 邮政编码
     */
    private String postCode;
    /**
     * 省份/直辖市
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 区
     */
    private String region;
    /**
     * 详细地址(街道)
     */
    private String detailAddress;
    /**
     * 省市区代码
     */
    private String areacode;
    /**
     * 是否默认
     */
    private Integer defaultStatus;
}
 5.2.1.3 订单页购物项Vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderItemVo.java

/**
 * @author W
 * @createDate 2024/02/28
 * @description: 订单确认页的购物项
 * 不需要选中标志
 */
@Data
public class OrderItemVo {
    private Long skuId; // 商品编号
    private String title; // 标题
    private String image;// 图片
    private List<String> skuAttr;// 商品销售属性集合
    private BigDecimal price; // 单价
    private Integer count; // 数量
    private BigDecimal totalPrice;// 总价
    private BigDecimal weight = new BigDecimal("0.085");// 商品重量
}

5.2.2 订单确认页流程

1. 判断是否登录,使用登录拦截器LoginUserInterceptor;

2. 远程获取用户所有的地址列表【gulimall_ums库=》ums_member_receive_address表】,字段default_status值1-表示默认地址,0-非默认地址,默认地址高亮;

3. 远程查询购物车中所有的购物项,【gulimall-cart调用商品服务,获取购物项的最新价格】;

4. 订单总额,根据购物项的价格和数量计算,然后求和;

5. 应付价格【暂时跟订单总额一样】,实际总额要减去优惠价格等;

6. 远程查询库存服务;

7. 根据选中的地址,通过ajax请求调用/ware/wareinfo/fare?addrId=addrIdValue接口获取运费信息和地址信息;

8. 防重令牌(幂等性:提交一次和多次的效果是一样的)防止订单重复提交,数据库只保存一条订单信息。

5.2.3 功能实现

5.2.3.1 controller层

gulimall-order/src/main/java/com/wen/gulimall/order/web/OrderWebController.java

@Controller
public class OrderWebController {
    @Resource
    private OrderService orderService;

    @GetMapping("/toTrade")
    public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = orderService.confirmOrder();
        model.addAttribute("orderConfirmData",orderConfirmVo);
        return "confirm";
    }

}
5.2.3.2 service层

 gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java

public interface OrderService extends IService<OrderEntity> {

    ...

    /**
     * 订单确认页返回需要的数据
     * @return
     */
    OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException;
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

业务流程,如下:

(1)获取登录用户信息;

(2)远程查询登录用户所有的收货地址列表;

(3)远程查询购物车中所有的购物项列表;

(4)远程查询商品库存信息;

(5)查询用户积分;

(6)价格等信息自动计算;

(7)防重令牌(幂等性),防止订单重复提交。

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ThreadPoolExecutor executor;
   
    ...

   @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        // 获取之前的请求
        // RequestContextHolder是同一个线程共享请求数据
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            // 每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 1. 远程查询会员所有的收货地址
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            orderConfirmVo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 2. 远程查询购物车中所有选中的购物项
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
            orderConfirmVo.setItems(currentUserCartItems);
            // feign在远程调用之前需要构造新的请求,调用很多拦截器
            // RequestInterceptor interceptor : RequestInterceptors
        }, executor).thenRunAsync(()->{
            // 远程查询库存
            List<OrderItemVo> items = orderConfirmVo.getItems();
            List<Long> collect = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
            // 远程调用库存服务
            R skusHasStock = wmsFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = skusHasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if(data!=null) {
                // 以map的形式显示每个商品是否有库存
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                orderConfirmVo.setStocks(map);
            }
        },executor);

        // 3. 查询用户积分
        Integer integration = memberRespVo.getIntegration();
        orderConfirmVo.setIntegration(integration);

        // 4. 其他数据自动计算

        // TODO 5. 放重令牌(幂等性)防止订单重复提交
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        // 防重令牌设置30分钟的过期时间,存放在redis
        stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
        orderConfirmVo.setOrderToken(token);
        CompletableFuture.anyOf(getAddressFuture, cartFuture).get();

        return orderConfirmVo;
    }
}
5.2.3.3 远程调用接口
5.2.2.3.1 远程查询会员所有的收货地址

1. 模拟数据

2. controller层

gulimall-member/src/main/java/com/wen/gulimall/member/controller/MemberReceiveAddressController.java

@RestController
@RequestMapping("member/memberreceiveaddress")
public class MemberReceiveAddressController {
    @Autowired
    private MemberReceiveAddressService memberReceiveAddressService;

    @GetMapping("/{memberId}/addresses")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable Long memberId){
        return memberReceiveAddressService.getAddress(memberId);
    }
    
    ...
}

3. service层

gulimall-member/src/main/java/com/wen/gulimall/member/service/MemberReceiveAddressService.java

public interface MemberReceiveAddressService extends IService<MemberReceiveAddressEntity> {

    ...

    List<MemberReceiveAddressEntity> getAddress(Long memberId);
}

gulimall-member/src/main/java/com/wen/gulimall/member/service/impl/MemberReceiveAddressServiceImpl.java 

@Service("memberReceiveAddressService")
public class MemberReceiveAddressServiceImpl extends ServiceImpl<MemberReceiveAddressDao, MemberReceiveAddressEntity> implements MemberReceiveAddressService {

    ...

    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
        return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id",memberId));
    }

}

4. Feign接口

gulimall-order/src/main/java/com/wen/gulimall/order/feign/MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @GetMapping("/member/memberreceiveaddress/{memberId}/addresses")
    List<MemberAddressVo> getAddress(@PathVariable Long memberId);
}
5.2.2.3.2 远程查询购物车中所有选中的购物项 

1. controller层

gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java

@Controller
public class CartController {
    @Resource
    private CartService cartService;

    @ResponseBody
    @GetMapping("/currentUserCartItems")
    public List<CartItemVo> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }

    ...
}

2. service层

gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java

public interface CartService {

    ...

    List<CartItemVo> getUserCartItems();
}

gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java

@Service
public class CartServiceImpl implements CartService {
    
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;

    ...

    @Override
    public List<CartItemVo> getUserCartItems() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if(userInfoTo.getUserId()==null){
            return null;
        }else {
            String cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
            List<CartItemVo> cartItems = getCartItems(cartKey);
            // 获取所有被选中的购物项
            List<CartItemVo> collect = cartItems.stream()
                    .filter(item -> item.getCheck())
                    .map(item ->{
                        BigDecimal price = productFeignService.getPrice(item.getSkuId());
                        // TODO 更新为最新价格
                        item.setPrice(price);
                        return item;
                    })
                    .collect(Collectors.toList());
            return collect;
        }
    }
}

3. Feign接口

 gulimall-order/src/main/java/com/wen/gulimall/order/feign/CartFeignService.java

@FeignClient("gulimall-cart")
public interface CartFeignService {
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}

4. 远程调用商品服务查询商品的最新价格

gulimall-product/src/main/java/com/wen/gulimall/product/app/SkuInfoController.java

@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
    @Autowired
    private SkuInfoService skuInfoService;

    /**
     * 订单确认页查询商品此时的价格
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}/price")
    public BigDecimal getPrice(@PathVariable Long skuId){
        SkuInfoEntity byId = skuInfoService.getById(skuId);
        return byId.getPrice();
    }

    ...
}

gulimall-cart/src/main/java/com/wen/gulimall/cart/feign/ProductFeignService.java 

@FeignClient("gulimall-product")
public interface ProductFeignService {

    ...

    @GetMapping("/product/skuinfo/{skuId}/price")
    BigDecimal getPrice(@PathVariable Long skuId);
}
5.2.2.3.3 远程查询商品库存信息

1. 库存vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/SkuStockVo.java

@Data
public class SkuStockVo {
    private Long skuId;
    private Boolean hasStock;
}

2. controller层

gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareSkuController.java

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;

    @PostMapping("/hasstock")
    public R getSkusHasStock(@RequestBody List<Long> skuIds){
        // sku_id, stock
        List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
        R ok = R.ok();
        ok.setData(vos);
        return ok;
    }
    
    ...
}

3. service层

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareSkuService.java

public interface WareSkuService extends IService<WareSkuEntity> {

   
    ...

    List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds);
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java 

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
    
    ...

    @Override
    public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
        List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
            SkuHasStockVo skuHasStockVo = new SkuHasStockVo();

            // 查询是否有库存 = (库存数-锁定库存)> 0
            Long count = this.baseMapper.getSkuStock(skuId);
            skuHasStockVo.setSkuId(skuId);
            skuHasStockVo.setHasStock(count==null?false:count > 0);
            return skuHasStockVo;
        }).collect(Collectors.toList());
        return collect;
    }

}

4. Feign接口

gulimall-order/src/main/java/com/wen/gulimall/order/feign/WmsFeignService.java

@FeignClient("gulimall-ware")
public interface WmsFeignService {

    @PostMapping("/ware/waresku/hasstock")
    R getSkusHasStock(@RequestBody List<Long> skuIds);
}

5.2.4 Feign远程调用丢失请求头问题

问题产生原因:通过feign进行远程调用时,会创建一个新的RequestTemplate,没有请求头,Cookie信息没有了,导致在远程调用cart服务时,购物车拦截器无法从session中获取登录信息,无法获取userId。

解决方案:将老请求头的信息同步给新请求头。

原理:feign进行远程调用时会创建新的请求,然后调用很多拦截器(debug了解原理),我们可以自定义拦截器设置请求头。

gulimall-order/src/main/java/com/wen/gulimall/order/config/GuliFeignConfig.java

/**
 * @author W
 * @createDate 2024/02/29
 * @description: 解决 Feign远程调用请求头丢失问题
 * 远程调用会创建一个新的请求,新的请求没有请求头
 * 使用RequestInterceptor拦截器为新构建的请求添加请求头
 */
@Configuration
public class GuliFeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                // 1.RequestContextHolder拿到刚在进来的请求
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                assert requestAttributes != null;
                HttpServletRequest request = requestAttributes.getRequest(); // 老请求
                if(request!=null) {
                    // 同步请求头数据,Cookie
                    String cookie = request.getHeader("Cookie");
                    // 给构建的新请求同步老请求的Cookie
                    template.header("Cookie", cookie);
                }
            }
        };
    }
}

5.2.5 Feign异步情况丢失上下文问题

产生问题:RequestInterceptor拦截器报空指针异常,获取当前请求上下文的RequestContextHolder类本质上是一个ThreadLocal,同一个线程共享请求数据,异步线程无法共享之前的请求数据。

解决方案:获取主线程的请求数据设置到子线程的请求上下文RequestContextHolder中,如下。

5.2.6 创建防重令牌

        使用令牌机制实现下订单的幂等性,在订单确认页到来之前为订单确认页生成一个令牌,提交订单时带上这个令牌。令牌存在两个地方,服务器一个,页面一个。

        订单幂等性做了两种:防重令牌和数据库订单编号唯一性。

        订单防重复提交=》订单的幂等性,提交一次和多次的效果是一样的。(接口幂等性详解见)

令牌前缀常量

gulimall-order/src/main/java/com/wen/gulimall/order/constant/OrderConstant.java

public class OrderConstant {
    /** 订单确认也防重复提交令牌前缀 */
    public static final String USER_ORDER_TOKEN_PREFIX = "order:token:";
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

// TODO 5. 放重令牌(幂等性)防止订单重复提交
String token = UUID.randomUUID().toString().replaceAll("-", "");
// 防重令牌设置30分钟的过期时间,存放在redis
stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
orderConfirmVo.setOrderToken(token);

 订单表(oms_order),订单编号添加唯一索引。

5.2.7 模拟运费效果

5.2.7.1 后端实现

运费vo

gulimall-ware/src/main/java/com/wen/gulimall/ware/vo/FareVo.java

@Data
public class FareVo {
    private MemberAddressVo address;
    private BigDecimal fare;
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareInfoController.java

@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
    @Autowired
    private WareInfoService wareInfoService;

    @GetMapping("/fare")
    public R getFare(@RequestParam("addrId") Long addrId){
        FareVo fare = wareInfoService.getFare(addrId);
        return R.ok().setData(fare);
    }

    ...
}

 gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareInfoService.java

public interface WareInfoService extends IService<WareInfoEntity> {

   ...

    /**
     * 根据用户的收货地址计算运费
     * @param addrId
     * @return
     */
    FareVo getFare(Long addrId);
}

 gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareInfoServiceImpl.java

@Service("wareInfoService")
public class WareInfoServiceImpl extends ServiceImpl<WareInfoDao, WareInfoEntity> implements WareInfoService {

    @Resource
    private MemberFeignService memberFeignService;

    ...

    @Override
    public FareVo getFare(Long addrId) {
        FareVo fareVo = new FareVo();
        // 获取地址的详细信息
        R r = memberFeignService.attrInfo(addrId);
        MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
        });
        if(data!=null){
            // 调用第三方运费计算系统
            // 这里截取用户手机号码最后一位作为我们的运费
            String phone = data.getPhone();
            String fare = phone.substring(phone.length() - 1);
            BigDecimal bigDecimal = new BigDecimal(fare);
            fareVo.setFare(bigDecimal);
            fareVo.setAddress(data);

            return fareVo;
        }
        return null;
    }

}

 远程获取地址详细信息

地址vo

gulimall-ware/src/main/java/com/wen/gulimall/ware/vo/MemberAddressVo.java 

@Data
public class MemberAddressV
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值