订单服务-----功能实现逻辑1

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 订单服务-----功能实现逻辑

订单服务的实现流程(确认订单->提交订单->支付)

1、整合SpringSession

使用SpringSession的目的是来解决分布式session不同步不共享的问题,其实就是为了让登录信息在订单微服务里共享

注意:由于这里使用springsession的用的类型是redis,所以这springsession和redis都要一起加入依赖和配置

(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>
</dependency>

(2)在application.properties配置文件里配置springsession

#配置springsession
spring.session.store-type=redis
server.servlet.session.timeout=30m
#配置redis的ip地址
spring.redis.host=192.168.241.128

(3)在config配置中加入springSession配置类

package com.saodai.saodaimall.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* springSession配置类(所有要使用session的服务的session配置要一致)
*/
@Configuration
public class GulimallSessionConfig {
    /**
    * 配置session(主要是为了放大session作用域)
    * @return
    */
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        //放大作用域
        cookieSerializer.setDomainName("saodaimall.com");
        cookieSerializer.setCookieName("SAODAISESSION");
        return cookieSerializer;
    }
    /**
    * 配置Session放到redis存储的格式为json(其实就是json序列化)
    * @return
    */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

(4)在启动类上添加@EnableRedisHttpSession注解

package com.saodai.saodaimall.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
 * 订单服务启动类
 */
@EnableFeignClients
@EnableRedisHttpSession
@EnableDiscoveryClient
@SpringBootApplication
public class SaodaimallOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(SaodaimallOrderApplication.class, args);
    }
}


2、增加登录拦截器

(1)点击去结算后会去订单详情确认页面,这个时候需要用户登录才可以去结算

package com.saodai.saodaimall.order.interceptor;
import com.saodai.common.vo.MemberResponseVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static com.saodai.common.constant.AuthServerConstant.LOGIN_USER;
/**
 * 登录拦截器
 */
@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 {
        /**
         *直接放行的路径(也就是下面的两个路径不需要登录用于库存解锁)
         */
        String uri = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        //根据订单号查询订单实体类
        boolean match = antPathMatcher.match("/order/order/status/**", uri);
        //支付宝支付成功后的异步回调
        boolean match1 = antPathMatcher.match("/payed/notify", uri);
        if (match || match1) {
            return true;
        }
        //获取登录的用户信息
        MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
        if (attribute != null) {
            //把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);
            return true;
        } else {
            //未登录,返回登录页面
             request.getSession().setAttribute("msg", "请先进行登录");
             response.sendRedirect("http://auth.saodaimall.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)一定要记得在SpringMVC的配置文件里注册拦截器,不然拦截器不会生效

package com.saodai.saodaimall.order.config;
import com.saodai.saodaimall.order.interceptor.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * springmvc配置类
 **/
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginUserInterceptor loginUserInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}


3、根据订单业务需求抽取模型

(1)订单确认页类OrderConfirmVo(也就是订单确认页需要用的数据)

package com.saodai.saodaimall.order.vo;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
 * 订单确认页类(订单确认页需要用的数据)
 **/
public class OrderConfirmVo {
    @Getter @Setter
    /** 会员收获地址列表 **/
    List<MemberAddressVo> memberAddressVos;
    @Getter @Setter
    /** 所有选中的购物项 **/
    List<OrderItemVo> items;
    /** 发票记录 **/
    @Getter @Setter
    /** 优惠券(会员积分) **/
    private Integer integration;
    /** 防止重复提交的令牌 **/
    @Getter @Setter
    private String orderToken;
    @Getter @Setter
    Map<Long,Boolean> stocks;
    public Integer getCount() {
        Integer count = 0;
        if (items != null && items.size() > 0) {
            for (OrderItemVo item : items) {
                count += item.getCount();
            }
        }
        return count;
    }
    /** 订单总额 **/
    //BigDecimal total;
    //计算订单总额
    public BigDecimal getTotal() {
        BigDecimal totalNum = BigDecimal.ZERO;
        if (items != null && items.size() > 0) {
            for (OrderItemVo item : items) {
                //计算当前商品的总价格
                BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
                //再计算全部商品的总价格
                totalNum = totalNum.add(itemPrice);
            }
        }
        return totalNum;
    }
    /** 应付价格 **/
    //BigDecimal payPrice;
    public BigDecimal getPayPrice() {
        return getTotal();
    }
}

(2)订单项类OrderItemVo

package com.saodai.saodaimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
 * 订单项类(其实就是购物项)
 **/
@Data
public class OrderItemVo {
    private Long skuId;
    //
    private Boolean check;
    private String title;
    private String image;
    /**
     * 商品套餐属性
     */
    private List<String> skuAttrValues;
    private BigDecimal price;
    private Integer count;
    private BigDecimal totalPrice;
    private Boolean hasStock;
    /** 商品重量 **/
    private BigDecimal weight = new BigDecimal("0.085");
}

(3)用户收货信息类MemberAddressVo

package com.saodai.saodaimall.order.vo;
import lombok.Data;
/**
 * 用户订单的地址
 **/
@Data
public class MemberAddressVo {
    /**
     * 地址id
     */
    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;
}


4、点击去结算按钮(确认订单)

(1)订单服务的web的OrderWebController控制器来处理/toTrade请求

@Autowired
    private OrderService orderService;
    /**
     * 去结算确认页
     * @param model
     * @param request
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @GetMapping(value = "/toTrade")
    public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        model.addAttribute("confirmOrderData",confirmVo);
        //展示订单确认的数据
        return "confirm";
    }

(2)处理/toTrade请求的confirmOrder方法具体实现

流程:


1、远程调用会员服务来查询所有的收获地址列表


2、远程调用购物车服务来查询购物车所有选中的购物项


3、远程调用库存服务来批量查询所有商品的库存是否有货


4、查询用户积分


5、生成一个防重令牌并放到redis中(防止表单重复提交),这格令牌是会过期的,过期时间为30分钟(注意是结算的时候服务器生成一个防重令牌,然后把这个令牌隐藏在订单页面,点击去结算只是生成这个令牌并存到redis中和订单页面上,下面的点击提交订单按钮才会做校验令牌,保证原子性,防刷这些操作)


格式为key:order:token:+用户id,value:防重令牌

  /**
     * 去结算确认页时封装订单确认页返回需要用的数据
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        //构建OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //获取当前用户登录的信息(直接获取)
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
        //获取当前线程请求头信息(用于解决Feign异步调用丢失上下文的问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        /**开启第一个异步任务来远程查询所有的收获地址列表**/
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
            //每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题)
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        }, threadPoolExecutor);
        /**开启第二个异步任务来远程查询购物车所有选中的购物项**/
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
            //每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题)
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, threadPoolExecutor).thenRunAsync(() -> {
            /** 开启第三个异步任务来远程批量查询所有商品的库存是否有货**/
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream().map((itemVo -> itemVo.getSkuId())).collect(Collectors.toList());
            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
           //SkuStockVo就是下面的SkuHasStockVo类
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
            if (skuStockVos != null && skuStockVos.size() > 0) {
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },threadPoolExecutor);
        //3、查询用户积分
        Integer integration = memberResponseVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4、价格数据由OrderConfirmVo的getTotal方法自动计算
        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        //防重令牌一个放到redis里  USER_ORDER_TOKEN_PREFIX = "order:token"
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        //防重令牌一个放到前台页面(不过隐藏了)
        confirmVo.setOrderToken(token);
        //阻塞异步线程,只有两个异步都完成了才可以进行下一步
        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();
        return confirmVo;
    }
package com.saodai.common.vo;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
 *会员信息
 **/
@ToString
@Data
public class MemberResponseVo implements Serializable {
    private static final long serialVersionUID = 5573669251256409786L;
    private Long id;
    /**
     * 会员等级id
     */
    private Long levelId;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 昵称
     */
    private String nickname;
    /**
     * 手机号码
     */
    private String mobile;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 头像
     */
    private String header;
    /**
     * 性别
     */
    private Integer gender;
    /**
     * 生日
     */
    private Date birth;
    /**
     * 所在城市
     */
    private String city;
    /**
     * 职业
     */
    private String job;
    /**
     * 个性签名
     */
    private String sign;
    /**
     * 用户来源
     */
    private Integer sourceType;
    /**
     * 积分
     */
    private Integer integration;
    /**
     * 成长值
     */
    private Integer growth;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 注册时间
     */
    private Date createTime;
    /**
     * 社交登录用户的ID
     */
    private String socialId;
    /**
     * 社交登录用户的名称
     */
    private String socialName;
    /**
     * 社交登录用户的自我介绍
     */
    private String socialBio;
}

1>远程调用会员服务来查询所有的收获地址列表

  @Autowired
    private MemberReceiveAddressService memberReceiveAddressService;
    /**
     * 根据会员id查询会员的所有地址(用于获取订单时远程的查询地址)
     * @param memberId
     * @return
     */
    @GetMapping(value = "/{memberId}/address")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
        List<MemberReceiveAddressEntity> addressList = memberReceiveAddressService.getAddress(memberId);
        return addressList;
    }
//根据会员id查询会员的所有地址(用于获取订单时远程的查询地址)
    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
         //MemberReceiveAddressEntity对象就是上面的用户收货信息类MemberAddressVo
        List<MemberReceiveAddressEntity> addressList = this.baseMapper.selectList
                (new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
        return addressList;
    }

2>远程调用购物车服务来查询购物车所有选中的购物项

package com.saodai.saodaimall.order.feign;
import com.saodai.saodaimall.order.vo.OrderItemVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
/**
 * 远程调用购物车服务
 **/
@FeignClient("saodaimall-cart")
public interface CartFeignService {
    /**
     * 查询当前用户购物车选中的商品项
     * @return
     */
    @GetMapping(value = "/currentUserCartItems")
    List<OrderItemVo> getCurrentCartItems();
}
    @Resource
    private CartService cartService;
    /**
     * 获取当前用户的购物车商品项(订单生成需要查询用户购物车中选择的购物项)
     * @return
     */
    @GetMapping(value = "/currentUserCartItems")
    @ResponseBody
    public List<CartItemVo> getCurrentCartItems() {
        List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
        return cartItemVoList;
    }

查询购物车所有选中的购物项分流程:


1、获取当前用户登录的信息来判断登录没


2、组装Redis中的HashMap结构的Hash值,用来绑定Hash操作(这里的购物车和购物项的数据都是存到Reids缓存里的,格式为Hash值:saodaimall:cart:10290038,key:39,value:CartItemVo对象的String类型,其中10290038是用户id,39是skuId)


3、调用getCartItems方法来把所有的value值取出来并都封装成CartItemVo对象(注意每一个Hash值对应一个用户的购物车,所以其中的key和value合在一起表示每个购物项)


4、远程调用商品服务来查询每个商品的最新价格(由于redis缓存中的数据可能不是最新的数据)


   /**
     * 获取当前用户的购物车商品项(订单生成需要查询用户购物车中选择的购物项)
     * @return
     */
    @Override
    public List<CartItemVo> getUserCartItems() {
        List<CartItemVo> cartItemVoList = new ArrayList<>();
        //获取当前用户登录的信息
        UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
        //如果用户未登录直接返回null
        // userInfoTo.getUserId()== null是因为拦截器判断用户登录了就设置其userId
        // 没有登录就设置userKey,所以不能像以前那样直接就用userInfoTo==null来判断
        if (userInfoTo.getUserId() == null) {
            return null;
        } else {
            //获取购物车项  String CART_PREFIX = "saodaimall:cart:"是Hash值
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            //把Hash值传给getCartItems来获取购物车的所有购物项
            List<CartItemVo> cartItems = getCartItems(cartKey);
            if (cartItems == null) {
                throw new CartExceptionHandler();
            }
            //由于redis缓存中的数据可能不是最新的数据,所以要远程在查一次价格
            cartItemVoList = cartItems.stream()
                    .filter(items -> items.getCheck())
                    .map(item -> {
                        //更新为最新的价格(远程调用商品服务来查询数据库)
                        BigDecimal price = productFeignService.getPrice(item.getSkuId());
                        item.setPrice(price);
                        return item;
                    })
                    .collect(Collectors.toList());
        }
        return cartItemVoList;
    }
    /**
     * 获取购物车里面的数据
     * @param cartKey redis中的外围map的key值
     * @return
     */
    private List<CartItemVo> getCartItems(String cartKey) {
        //获取购物车里面的所有商品
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        //注意这里从Reids中取出来的是Object类型
        List<Object> values = operations.values();
        if (values != null && values.size() > 0) {
            List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
                //这里要转为String类型是因为reids中取出来的是Obhect类型
                String str = (String) obj;
                //在把String转为CartItemVo对象
                CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
                return cartItem;
            }).collect(Collectors.toList());
            return cartItemVoStream;
        }
        return null;
    }
    @Autowired
    private SkuInfoService skuInfoService;
    /**
     * 根据skuId查询当前商品的价格(订单生成需要查询用户购物车中选择的购物项时的最新价格)
     * @param skuId
     * @return
     */
    @GetMapping(value = "/{skuId}/price")
    public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
        //获取当前商品的信息(skuInfoService.getById()这个方法是代码构造器自动生成的代码里有的)
        SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
        //获取商品的价格
        BigDecimal price = skuInfo.getPrice();
        return price;
    }


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
4月前
|
存储 前端开发 Java
支付系统----微信支付17-----定义统一结果,返回时间格式不一样,怎样解决
支付系统----微信支付17-----定义统一结果,返回时间格式不一样,怎样解决
要会创建接口------支付系统19-------支付宝支付-----统一收单下单并支付页面接口----接口说明,接口文档中应该对如何调用接口进行一个详细的说明
要会创建接口------支付系统19-------支付宝支付-----统一收单下单并支付页面接口----接口说明,接口文档中应该对如何调用接口进行一个详细的说明
支付系统----微信支付16----创建案例项目-引入Swagger
支付系统----微信支付16----创建案例项目-引入Swagger
支付系统22------统一收单下单并支付页面接口-----调用支付宝接口,创建订单后期出现异常,能够打印出来的方法
支付系统22------统一收单下单并支付页面接口-----调用支付宝接口,创建订单后期出现异常,能够打印出来的方法
|
4月前
|
前端开发 API
支付系统27-------梳理一下支付按钮,前端的代码
支付系统27-------梳理一下支付按钮,前端的代码
|
4月前
|
关系型数据库 MySQL 数据库
生成订单的过程------支付系统21------支付宝支付----统一收单下单并支付页面接口----创建订单,下订单,我们要在我们数据库的订单表中,设置订单,订单表常用数据库设置格式
生成订单的过程------支付系统21------支付宝支付----统一收单下单并支付页面接口----创建订单,下订单,我们要在我们数据库的订单表中,设置订单,订单表常用数据库设置格式
|
4月前
|
前端开发 Java
支付系统20-----支付宝支付-----统一收单下单并支付页面接口----定义controller,跨域注解,统一收单下单并支付页面接口的创建,打印日志的注解
支付系统20-----支付宝支付-----统一收单下单并支付页面接口----定义controller,跨域注解,统一收单下单并支付页面接口的创建,打印日志的注解
|
消息中间件 前端开发 数据库
订单服务-----功能实现逻辑3
订单服务-----功能实现逻辑3
82 0
|
JavaScript Java 数据库
订单服务-----功能实现逻辑2
订单服务-----功能实现逻辑2
201 0
|
JSON 数据安全/隐私保护 数据格式
支付服务-----功能实现逻辑
支付服务-----功能实现逻辑
504 0