【大型电商项目开发】订单功能实现(拦截器、feign丢失请求头、接口幂等性)-55

news2024/12/25 12:45:37

一:订单概念

1.1 订单中心

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

1.2 订单构成

在这里插入图片描述

1.2.1 用户信息

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

1.2.2 订单基础信息

订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。
(1)订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。
(2)同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
(3)订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
(4)订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
(5)订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等

1.2.3 商品信息

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

1.2.4 优惠信息

优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。
为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。

1.2.5 支付信息

(1)支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
(2)支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。
(3)商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等之和;实付金额,用户实际需要付款的金额。用户实付金额=商品总金额+运费-优惠总金额

1.2.6 物流信息

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

1.3 订单状态

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

1.4 订单流程

订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与 O2O 订单等,所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图
在这里插入图片描述

1.4.1 订单创建与支付

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

1.4.2 逆向流程

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

二:订单登录拦截

因为订单登录的controller必须确保处于登录状态,所以需要添加拦截器进行校验

2.1 新建LoginUserInterceptor拦截器

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取登录的用户信息
        MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
        if (attribute != null) {
            //把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);
            return true;
        } else {
            //未登录,返回登录页面
            session.setAttribute("msg", "请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

2.2 新建OrderWebConfig配置类,用于处理拦截器

@Configuration
public class OrderWebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginUserInterceptor loginUserInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}

三:查询订单信息

3.1 创建实体类

3.1.1 建立订单确认页信息实体类OrderConfirmVo

/**
 * 订单确认页信息实体类
 * @author
 */
public class OrderConfirmVo {
    /**
     * 会员收货地址列表
     */
    @Getter @Setter
    private List<MemberAddressVo> address;

    /**
     * 所有选中的购物项
     */
    @Getter @Setter
    private List<OrderItemVo> items;

    /**
     * 优惠券信息
     */
    @Getter @Setter
    private Integer integration;

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

    /**
     *订单总额
     */
    public BigDecimal getTotal() {
        BigDecimal sumTotal = new BigDecimal("0");
        if(!CollectionUtils.isEmpty(items)){
            for (OrderItemVo item : items) {
                //价格*数量=总价
                BigDecimal multiply = item.getPrice().multiply(BigDecimal.valueOf(item.getCount()));
                sumTotal = sumTotal.add(multiply);
            }

        }
        return sumTotal;
    }

    /**
     * 应付价格
     */
    public BigDecimal getPayPrice() {
        return getTotal();
    }

3.1.2 建立会员收货地址实体类

/**
 * 会员收货地址
 */
@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;
}

3.1.3 新建购物项实体类OrderItemVo

/**
 * 购物项
 */
@Data
public class OrderItemVo {
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 商品标题
     */
    private String title;
    /**
     * 商品图片
     */
    private String image;
    /**
     * 商品属性
     */
    private List<String> skuAttr;
    /**
     * 商品价格
     */
    private BigDecimal price;
    /**
     * 商品总数
     */
    private Integer count;
    /**
     * 商品小计
     */
    private BigDecimal totalPrice;
}

3.2 编写service方法

3.2.1 编写OrderServiceImpl的方法

@Override
    public OrderConfirmVo confirmOrder() {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //1.远程查询所有的收获列表
        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
        confirmVo.setAddress(address);
        //2.远程查询购物车所选中的购物项
        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(items);
        //3.查询优惠券信息、用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4.其他数据自动计算
        //5.订单的防重令牌
        return confirmVo;
    }

3.2.2 编写getAddress方法

feign:远程调用

@FeignClient("gulimail-member")
public interface MemberFeignService {

    /**
     * 获取会员地址信息
     * @param memberId
     * @return
     */
    @GetMapping("/member/memberreceiveaddress/{memberId}/addresses")
    List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
        return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id",memberId));
    }

3.2.3 编写getCurrentUserCartItems方法

@FeignClient("gulimail-cart")
public interface CartFeignService {
    /**
     * 获取当前登录用户的购物项
     * @return
     */
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}
@Override
    public List<CartItem> getUserCartItems() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        List<CartItem> cartItems = null;
        //判断是否登录
        if (userInfoTo != null) {
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            cartItems = getCartItems(cartKey);
            //获取所有被选中的购物项
            if (!CollectionUtils.isEmpty(cartItems)) {
                return cartItems.stream()
                        .filter(CartItem::getCheck)
                        .map(item -> {
                            BigDecimal price = productFeignService.getPrice(item.getSkuId());
                            //更新为最新价格
                            item.setPrice(price);
                            return item;
                        }).collect(Collectors.toList());
            }
        }
        return null;
    }

四:feign远程调用丢失请求头问题

在这里插入图片描述
处理方案:在新建request的时候,添加feign远程调用的拦截器

4.1 新建feign的配置类,用来处理远程调用时cookie丢失问题

@Configuration
public class GuliFeignConfig {
    /**
     * 给容器中添加拦截器
     * @return
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate template) {
                //1.使用RequestContextHolder获取刚进来的请求
                ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
                //老请求
                assert attributes != null;
                HttpServletRequest request = attributes.getRequest();
                //同步请求数据cookie
                String cookie = request.getHeader("Cookie");
                //给新请求同步老请求的cookie
                template.header("Cookie",cookie);
            }
        };
    }
}

注:feign远程调用时,请求头必须从老请求中获取过来

五:使用多线程异步获取订单确认信息

5.1 配置多线程

5.1.1 添加多线程配置类

@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(
                pool.getCoreSize(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

5.1.2 配置核心线程数

@ConfigurationProperties(prefix = "gulimail.thread")
@Component
@Data
public class ThreadPoolConfigProperties {

    private Integer coreSize;

    private Integer maxSize;

    private Integer keepAliveTime;


}

5.1.3 配置文件中配置核心参数

gulimail.thread.core-size=20
gulimail.thread.max-size=200
gulimail.thread.keep-alive-time=10

5.2 使用异步进行远程调用

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //1.远程查询所有的收获列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //2.远程查询购物车所选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor);
        //feign在远程调用之前会构造请求,构造请求会调用很多拦截器
        //3.查询优惠券信息、用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4.其他数据自动计算
        //5.订单的防重令牌
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

5.3 使用异步远程调用丢失上下文的问题

在这里插入图片描述
问题:使用异步调用address和cart的过程中调用了另外一个线程,导致threadLocal不能共享数据了

解决方案

使用RequestContextHolder在每个线程中将信息重新赋值

 @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1.远程查询所有的收获列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2.远程查询购物车所选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            //3.查询库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            if(!CollectionUtils.isEmpty(items)){
                List<Long> skuIds = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
                R skusHasStock = wmsFeignService.getSkusHasStock(skuIds);
                List<SkuStockVo> data = skusHasStock.getData(new TypeReference<List<SkuStockVo>>() {
                });
                if(!CollectionUtils.isEmpty(data)){
                    Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                    confirmVo.setStocks(map);
                }
            }
        }, executor);
        //feign在远程调用之前会构造请求,构造请求会调用很多拦截器
        //3.查询优惠券信息、用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4.其他数据自动计算
        //5.订单的防重令牌
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

六:接口幂等性

6.1 什么是幂等性

  接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。

6.2 哪些情况需要防止

  • 用户多次点击按钮
  • 用户页面回退再次提交
  • 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
  • 其他业务情况

6.3 什么情况下需要幂等

以 SQL 为例,有些操作是天然幂等的。

  • SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
  • UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
  • delete from user where userid=1,多次操作,结果一样,具备幂等性
  • insert into user(userid,name) values(1,‘a’) 如 userid 为唯一主键,即重复操作上面的业务,只
    会插入一条用户数据,具备幂等性。
  • UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
  • insert into user(userid,name) values(1,‘a’) 如 userid 不是主键,可以重复,那上面业务多次操
    作,数据都会新增多条,不具备幂等性。

6.4 幂等解决方案

6.4.1 token 机制

  • 1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
  • 2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
  • 3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业
    务。
  • 4、如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
    危险性:
  • 1、先删除 token 还是后删除 token;
    (1) 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致,请求还是不能执行。
    (2) 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别人继续重试,导致业务被执行两边
    (3) 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。
  • 2、Token 获取、比较和删除必须是原子性
    (1) redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行
    (2) 可以在 redis 使用 lua 脚本完成这个操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

6.4.2 各种锁机制

  • 1.数据库悲观锁
    select * from xxxx where id = 1 for update;
    悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
    另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会
    非常麻烦。
  • 2.数据库乐观锁
    这种方法适合在更新的场景中,
    update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
    根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候
    带上此 version 号。我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务
    version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订
    单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变
    为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
    乐观锁主要使用于处理读多写少的问题
  • 3.业务层分布式锁
    如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数
    据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断
    这个数据是否被处理过。

6.4.3 各种唯一约束

  • 1、数据库唯一约束
    插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
  • 2、redis set 防重
    很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set,
    每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。
  • 3、防重表
    使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。之前说的 redis 防重也算
  • 4、全局请求唯一 id
    调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。可以使用 nginx 设置每一个请求的唯一 id;
    proxy_set_header X-Request-Id $request_id;

6.5 幂等性项目实战

在这里插入图片描述

6.5.1 查询订单信息时添加token令牌,分别返回给客户端和保存在服务端

@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1.远程查询所有的收获列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2.远程查询购物车所选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            //3.查询库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            if(!CollectionUtils.isEmpty(items)){
                List<Long> skuIds = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
                R skusHasStock = wmsFeignService.getSkusHasStock(skuIds);
                List<SkuStockVo> data = skusHasStock.getData(new TypeReference<List<SkuStockVo>>() {
                });
                if(!CollectionUtils.isEmpty(data)){
                    Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                    confirmVo.setStocks(map);
                }
            }
        }, executor);
        //feign在远程调用之前会构造请求,构造请求会调用很多拦截器
        //3.查询优惠券信息、用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4.其他数据自动计算
        //5.订单的防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        //返回给客户端
        confirmVo.setOrderToken(token);
        //保存在服务端
        stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

6.5.2 提交订单携带token

 @Override
    @Transactional(rollbackFor = Exception.class)
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        confirmVoThreadLocal.set(vo);
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //下单功能——下单后去创建订单,校验令牌,验价格,锁库存
        //1.验证令牌-保证对比和删除是原则性的
        //lua脚本返回的是0失败和1成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script,Long.class);
        //原则删除和校验
        Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if(result == 0L){
            //令牌验证失败
            response.setCode(1);
            return response;
        } else {
            //令牌验证成功
        }
    }

七:验证价格、保存订单、库存锁定

7.1 验证价格

//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {

}

7.2 保存订单

/**
     * 保存订单所有数据
     * @param orderCreateTo
     */
    private void saveOrder(OrderCreateTo orderCreateTo) {
        //获取订单信息
        OrderEntity order = orderCreateTo.getOrder();
        order.setModifyTime(new Date());
        order.setCreateTime(new Date());
        //保存订单
        this.baseMapper.insert(order);
        //获取订单项信息
        List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();
        //批量保存订单项数据
        orderItemService.saveBatch(orderItems);
    }

7.3 库存锁定

if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
                //金额对比
                //3、保存订单
                saveOrder(order);
                //4、库存锁定,只要有异常,回滚订单数据
                //订单号、所有订单项信息(skuId,skuNum,skuName)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());
                //获取出要锁定的商品数据信息
                List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(orderItemVos);
                //调用远程锁定库存的方法
                //出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
                //为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
                R r = wmsFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    //锁定成功
                    response.setOrder(order.getOrder());
                    // int i = 10/0;
                    //订单创建成功,发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
                    //删除购物车里的数据
                    stringRedisTemplate.delete(CartConstant.CART_PREFIX + memberRespVo.getId());
                    return response;
                } else {
                    //锁定失败
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                }
            } else {
                response.setCode(2);
                return response;
            }
/**
     * 为某个订单锁定库存
     * @param vo
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean orderLockStock(WareSkuLockVo vo) {
        /**
         * 保存库存工作单详情信息
         * 追溯
         */
        WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
        wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
        wareOrderTaskEntity.setCreateTime(new Date());
        wareOrderTaskService.save(wareOrderTaskEntity);
        //1、按照下单的收货地址,找到一个就近仓库,锁定库存
        //2、找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStock> collect = locks.stream().map((item) -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪个仓库有库存
            List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIdList);
            return stock;
        }).collect(Collectors.toList());
        //2、锁定库存
        for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();
            if (CollectionUtils.isEmpty(wareIds)) {
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            //1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
            //2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
            for (Long wareId : wareIds) {
                //锁定成功就返回1,失败就返回0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1) {
                    skuStocked = true;
                    WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
                            .skuId(skuId)
                            .skuName("")
                            .skuNum(hasStock.getNum())
                            .taskId(wareOrderTaskEntity.getId())
                            .wareId(wareId)
                            .lockStatus(1)
                            .build();
                    wareOrderTaskDetailService.save(taskDetailEntity);
                    //告诉MQ库存锁定成功
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(wareOrderTaskEntity.getId());
                    StockDetailTo detailTo = new StockDetailTo();
                    BeanUtils.copyProperties(taskDetailEntity,detailTo);
                    lockedTo.setDetailTo(detailTo);
                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                    break;
                } else {
                    //当前仓库锁失败,重试下一个仓库
                }
            }
            if (skuStocked == false) {
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }
        //3、肯定全部都是锁定成功的
        return true;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/126728.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多线程问题(三)

目录 一、线程安全的单例模式 1、饿汉模式 2、懒汉模式 二、阻塞队列 三、定时器 1、标准库中定时器的使用用法 2、模拟实现定时器 a、首先需要创建出一个专门的类来表示schedule中的任务&#xff08;TimerTask&#xff09; b、使用合适的数据结构组织任务 c、…

Servlet基础教程 (保姆级教学)

Servlet基础教程一、Servlet 是什么二、第一个 Servlet 程序2.1 创建项目2.2 引入依赖2.3 创建目录2.4 编写代码2.5 打包程序2.6 部署程序2.7 验证程序三、更方便的部署方式3.1 安装 Smart Tomcat 插件3.2 配置 Smart Tomcat 插件四、常见的访问出错4.1 出现 4044.2 出现 4054.…

【jrebel and xrebel问题记录】激活时出现LS client not configued

教程目录问题描述所使用的环境和版本解决过程手动下载jrebel结束语问题描述 笔者在重装另一台电脑的时候又遇到了这个安装jrebel and xrebel进行激活的问题 但是我在网上找了很多的办法&#xff08;其实都是相同的办法&#xff0c;只是在尝试别人不同的用于激活的服务器&#…

【Java编程进阶】方法初识

推荐学习专栏&#xff1a;Java 编程进阶之路【从入门到精通】 文章目录1. Java 方法初识2. 方法的创建与使用3. 方法的分类3.1 无参无返回值3.2 无参带返回值3.3 有参无返回值3.4 有参带返回值4. 递归方法5. 总结1. Java 方法初识 方法是组合在一起来执行操作语句的集合&#…

k8s收集日志

k8s收集日志 一.收集控制台日志 采用fluentdeskibana来做 所需要的文件可以在这里找 https://github.com/kubernetes/kubernetes/tree/v1.23.0/cluster/addons/fluentd-elasticsearch1.创建目录并下载所需文件 cd /root/k8s/yaml/efk [rootworker1 efk]# ll total 44 -rw-…

绝缘子红外图像检测项目(TF2)

目录 1. 项目背景 2. 图像数据集介绍 labelimg的安装流程&#xff1a; 1. 打开Anaconda Prompt&#xff08;Anaconda3&#xff09; 2. 创建一个新环境来安装labelimg 3. 激活新创建的环境labelimg 4.输入 5.输入labelimg 即可运行 3. 模型介绍 4. 模型性能测试 1. 项目…

Linux学习笔记——Linux实用操作(二)

04、Linux实用操作 4.6、IP地址、主机名 4.6.1、IP地址、主机名 学习目标&#xff1a; 掌握什么是IP地址掌握什么是主机名掌握什么是域名解析 4.6.1.1、IP地址 1、每一台联网的电脑都会有一个地址&#xff0c;用于和其它计算机进行通讯。 IP地址主要有2个版本&#xff0…

2023上半年软考高级-信息系统项目管理师【名师授课班】

信息系统项目管理师是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目之一&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资…

2022年圣诞节 | matlab实现炫酷的圣诞树

*2022年圣诞节到来啦&#xff0c;很高兴这次我们又能一起度过~ 这里的部分代码已经在网上出现过&#xff0c;做了部分优化。是matlab版本。 一、内容介绍 这段代码是一个生成3D圣诞树的Matlab函数。运行该函数时&#xff0c;它使用圆柱函数创建圣诞树的 3D 表面&#xff0c;对…

【一】微服务技术栈导学

微服务技术栈导学什么是微服务&#xff1f;微服务技术栈注册中心配置中心服务网关分布式缓存分布式搜索消息队列分布式日志服务&系统监控和链路追踪自动化部署微服务技术栈包含知识点学习路线知识内容来自于黑马程序员视频教学和百度百科。博主仅作笔记整理便于回顾学习。如…

Android设计模式详解之适配器模式

前言 适配器模式在Android开发中使用率很高&#xff0c;如ListView、RecyclerView&#xff1b; 定义&#xff1a;适配器模式把一个类的接口变换成客户端所期待的另一个接口&#xff0c;从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作&#xff1b; 使用场景&…

2023年加密行业会更难吗?欧科云链研究院“七大趋势预测”

回望2022&#xff0c;加密行业遭遇了种种不可控因素而导致的艰难险阻&#xff0c;也在变革与发展中孕育着生机与活力。 这一年&#xff0c;我们亲眼目睹了Luna暴雷&#xff0c;三箭资本、FTX这些曾经被认为“大而不倒”的机构接连倒下&#xff0c;市场信心严重受挫&#xff1b;…

登陆港股市场,阳光保险的 “价值锚点”

不确定性环境里&#xff0c;信心比黄金还重要。 最近&#xff0c;利好信号频频出现在保险行业&#xff0c;资本信心不断加固。上个月月底&#xff0c;个人养老金制度启动实施&#xff0c;市场迅速传来喝彩声。这不仅将加快推动养老保险作为第三支柱的壮大&#xff0c;而且还为…

ARM体系架构中的存储系统

在计算机系统当中&#xff0c;数据的存储是以字节为单位的&#xff0c;每个地址单元当中都可以存放一个字节的数据&#xff0c;每个字节为8bit。在C语言中编译器为char型的数据分配了一个字节的存储空间&#xff0c;为long型的数据分配了4个字节的存储空间&#xff0c;为int型的…

【NI Multisim 14.0编辑环境——工具栏】

目录 序言 一、工具栏 &#x1f34a;1.“标准”工具栏 &#x1f34a; 2.视图工具栏 &#x1f34a;3.“主”工具栏 &#x1f34a;4.“元器件”工具栏 &#x1f34a;5.“Simulation”&#xff08;仿真&#xff09;工具栏 &#x1f34a;6.“Place probe”&#xff08;放置探针…

ARM64内存虚拟化分析(2)常用结构体

内存虚拟化相关的几个重要结构体如下图所示&#xff1a; 这里介绍几个结构体以及相互之间有关系。 &#xff08;1&#xff09;AddressSpace结构体 它用于表示一个虚拟机或虚拟CPU能够访问的所有物理地址。其中&#xff1a; root&#xff1a;指向根MR Current_map&#xff1…

营销在中国

&#xff08;1&#xff09;4P、4C、4R、4I作为一个企业&#xff0c;不外乎就是两个是&#xff1a;产-销。你生产-客户购买&#xff0c;这个交易能做成&#xff0c;不外乎在于交换的价值&#xff0c;以及交易的价格-成本。一、4P4P&#xff0c;是美国密歇根大学教授杰罗姆麦卡锡…

向量的点乘与X乘以及意义

一、向量的点乘 向量的点乘&#xff08;dot&#xff09;是一个标量积&#xff0c;也叫向量的内积、数量积。 点乘公式&#xff1a; 有向量a b a(a1,a2,a3,...,an) b(b1,b2,b3,...,bn); 那么向量a(dot)ba1b1a2b2a3b3....anbn 从上面我们能可以看出&#xff0c;点乘得到的结…

2022年个人融资方法和工具研究报告

第一章 理论和概况 1.1 融资概念 融资&#xff0c;英文为Financing&#xff0c;指为支付超过现金或转账的购货款而采取的货币交易手段&#xff0c;或者为取得特定资产而筹集资金所采取的货币手段。融资通常指货币资金的持有者和需求者之间&#xff0c;直接或间接地进行资金融…

Appium基础 — 模拟手势点击坐标

1、模拟手势点击坐标 在定位元素的时候&#xff0c;你使出了十八班武艺还是定位不到&#xff0c;怎么办呢&#xff1f;&#xff08;面试经常会问&#xff09; 那就拿出绝招&#xff1a;点击元素所在位置的坐标。&#xff08;坐标定位&#xff09; 详细说明&#xff1a; 如下…