美文网首页
业务流程分析

业务流程分析

作者: 影子猪_ | 来源:发表于2020-07-24 10:18 被阅读0次

    一.搜索业务概要

    用户有可能会根据分类搜索、品牌搜索,还有可能根据规格搜索,以及价格搜索和排序 操作。根据分类和品牌搜索的时候,可以直接根据指定域搜索,而规格搜索的域数据是 不确定的,价格是一个区间搜索,所以我们可以分为三段实现,先实现分类、品牌搜 素,再实现规格搜索,然后实现价格区间搜索。


    搜索依据.png

    1.根据关键字查询

    需求分析

    根据关键字模糊查询

    实现思路

    (1)Service层添加一个搜索方法,搜索参数用Map集合接收。
    (2)通过ElasticSearchTemplate.queryForPage(SearchQuery query, Class<T> clazz, SearchResultMapper mapper)方法实现查询,返回一个结果操作类
    (3)将需要返回给前端页面的数据信息封装到一个自定义Map
    (4)Controller层添加一个接口方法,用于搜索

    具体实现

    该流程主要代码实现在queryForPage方法的编写

    public Map search(Map<String, String> searchMap) {
    
    
    
            if (searchMap != null) {
                Map<String,Object> resultMap = new HashMap<>();
                //构建布尔查询
                BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
                //布尔查询中需要用到模糊查询 match(字段类型,具体值即传入参数)
                if(StringUtils.isNotEmpty(searchMap.get("keyword"))){
                    boolQuery.must(QueryBuilders.matchQuery("name", searchMap.get("keyword")).operator(Operator.AND));
    
                }
    
                //原生搜索实现类(第一个参数)
                NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
                nativeSearchQueryBuilder.withQuery(boolQuery);
    
    
                /**
                 * 第一个参数:条件构建对象
                 * 第二个参数:查询操作实体类
                 * 第三个参数:查询结果操作对象
                 */
                AggregatedPage<SkuInfo> resultInfo = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
                    @Override
                    public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
                        //拿到命中的所有数据
                        SearchHits hits = searchResponse.getHits();
                        long total = hits.getTotalHits();
                        Aggregations aggregations = searchResponse.getAggregations();
                        List<T> list = new ArrayList<>();
                        if(hits != null){
                            for (SearchHit hit : hits) {
                                //将单条数据封装成一个SkuInfo对象
                                SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
                                list.add((T) skuInfo);
                            }
                        }
                        // new出这个接口的实现类, 关注该返回对象需要哪些参数
                        return new AggregatedPageImpl<T>(list,pageable,total,aggregations);
                    }
                });
    
                //需要的数据包括:查询结果对象,总记录数,总页数
                resultMap.put("list",resultInfo.getContent());
                resultMap.put("totals",resultInfo.getTotalElements());
                resultMap.put("totalPages",resultInfo.getTotalPages());
                return resultMap;
            }
           return null;
        }
    

    controller层的代码如下:

        /**
         * 根据关键字全文检索
         * @param searchMap
         * @return
         */
        @GetMapping
        public Result search(@RequestParam Map<String,String> searchMap){
            this.handlerSearchMap(searchMap);
            Map map = searchService.search(searchMap);
            return new Result(true, StatusCode.OK,"查询成功",map);
        }
    
        //对搜索入参带有特殊符号进行处理
        private void handlerSearchMap(Map<String, String> searchMap) {
            if(null != searchMap){
                Set<Map.Entry<String, String>> entries = searchMap.entrySet();
                for (Map.Entry<String, String> entry : entries) {
                    if(entry.getKey().startsWith("spec_")){
                        searchMap.put(entry.getKey(),entry.getValue().replace("+","%2B"));
                    }
                }
            }
        }
    

    二.页面静态化流程

    1商品详情页实现页面静态化

    需求分析

    采用静态页面生成的方式生成商品详情页面,并部署到高性能的web服务器中进行访问

    实现思路

    (1)新建一个页面的微服务,使用canal监听数据库中tb_spu表,当监测到商品上架后,将该商品的spuId发送到中间件rabbitmq。
    (2)新建一个页面静态化队列,将商品上架交换机与该队列绑定。
    (3)静态页微服务设置监听器, 监听静态页生成队列, 根据商品id获取商品详细数据并使用thymeleaf的模板技术生成静态页面。

    具体实现

    1.通过Feign微服务调用拿到模板页面所需要的数据信息,准备一个getItemData方法

        @Autowired
        private SkuFeign skuFeign;
    
        @Autowired
        private SpuFeign spuFeign;
    
        @Autowired
        private CategoryFeign categoryFeign;
    
        private Map<String, Object> getItemData(String spuId) {
            Map<String,Object> resultMap = new HashMap<>();
            //spu数据
            Spu spu = spuFeign.findSpuById(spuId).getData();
            resultMap.put("spu",spu);
            //图片信息
            if(spu != null){
                if(StringUtils.isNotEmpty(spu.getImages())){
                    resultMap.put("imageList",spu.getImages().split(","));
                }
            }
            //sku数据
            List<Sku> skuList = skuFeign.findListBySpuId(spuId);
            resultMap.put("skuList",skuList);
    
            //商品分类信息
            Category category1 =  categoryFeign.findById(spu.getCategory1Id()).getData();
            resultMap.put("category1",category1);
            Category category2 =  categoryFeign.findById(spu.getCategory2Id()).getData();
            resultMap.put("category2",category2);
            Category category3 =  categoryFeign.findById(spu.getCategory3Id()).getData();
            resultMap.put("category3",category3);
    
            //规格信息
            resultMap.put("specificationList", JSON.parseObject(spu.getSpecItems(),Map.class));
    
            return resultMap;
        }
    

    2.html页面生成的方法

        @Value("${pagepath}")
        private String pagepath;
    
        @Autowired
        private TemplateEngine templateEngine;
    
        @Override
        public void generateHtml(String spuId)  {
    
            //1.拿数据
            Context context = new Context();
            Map<String, Object> itemData = this.getItemData(spuId);
            context.setVariables(itemData);
            //2.获取详情页面存储位置
            File dir = new File(pagepath);
            //3.文件存在
            if(!dir.exists()){
                dir.mkdirs();
            }
            //4.定义输出流,完成文件生成
            File file = new File(dir + "/" + spuId + ".html");
            Writer out= null;
            try{
                out = new PrintWriter(file);
                //生成静态化页面
                // 模板名称 context 输出流
                templateEngine.process("item",context,out);
            }catch(Exception e){
                e.printStackTrace();
            }finally {
                try {
                    //5.关闭流
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            
        }
    

    3.消息消费端实现方法调用

    @Component
    public class PageListener {
    
        @Autowired
        private PageService pageService;
    
        @RabbitListener(queues = RabbitmqConfig.PAGE_CREATE_QUEUE)
        public void receiceMessage(String spuId){
            pageService.generateHtml(spuId);
        }
    
    }
    
    

    三.认证开发+SpringSecurity+Oauth2+网关

    1.认证分析

    需求分析

    实现单点登录和第三方账号登录。
    1.单点登录:
    用户访问的项目中,有多个微服务需要识别用户身份信息,每次都要登录的话就太麻烦了。让用户在一个系统中登录成功后,在其他任意受信任的系统都可以访问,这个功能叫做单点登录。
    2.第三方账号登录
    主要是基于用户在第三方平台已有的账号完成当前应用的登录功能,常用的有微信和QQ等。

    解决方案

    1.单点登录:通常将认证系统独立出来,将用户身份信息存储在单独的存储介质,考虑性能要求,存储在redis中,整个过程如图:

    单点登录流程图.png

    用户请求首先经过网关,将请求转发到具体的微服务上比如订单服务,它会首先找到用户认证系统进行用户认证,需要获取用户信息并存储到redis中。
    用户认证框架有Apache Shiro ,CAS ,SpringSecurity 等。

    2.第三方登录技术方案
    最主要需要解决的是认证协议通用标准化问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。而OAUTH协议为用户资源授权提供了一个安全开放又简易的标准,具体流程如下图:


    微信认证流程图.png

    用户访问畅购登录页面,选择第三方登录比如微信登陆,畅购网站会向微信端请求认证。
    微信端接着向用户返回一个授权页面,用户授权通过后,微信端会返回一个授权码给畅购网站,然后畅购网站向微信端申请一个令牌,微信端返回一个令牌给畅购网站,接着拿着令牌去获取用户信息,在用户信息端会检验令牌合法性,合法则会响应给畅购网站用户信息。

    本项目采用的方案

    使用 Spring security + Oauth2+ JWT完成用户认证及用户授权。


    认证流程.png

    1.用户在登录页面发送登录请求,经过网关自动放行和登录有关的资源。
    2.在认证系统中会暴露一个接口接收用户的登录信息,通过restTemplate发起请求“/oauth/token”,请求springsecurity认证接口,验证登陆成功后,会返回一个令牌信息,将段令牌jti存入cookie中,同时jti和jwt令牌以KV形式存入redis。
    3.用户再次访问时,网关首先会检测请求头cookie中是否有jti令牌,再根据jti从redis中查找jwt令牌,将jwt令牌携带到request头信息中,当访问到用户资源服务时,springsecurity框架通过公钥会自动校验jwt,判断用户有无访问资格。

    具体实现流程

    1.用户从登录页面发送登录请求,在认证系统user-oauth服务中会暴露一个接口接收用户的登录信息。

        @PostMapping("/login")
        @ResponseBody
        public Result login(String username, String password, HttpServletResponse response){
            if(StringUtils.isEmpty(username)){throw new RuntimeException("用户名为空");}
            if(StringUtils.isEmpty(password)){throw new RuntimeException("密码为空");}
            AuthToken authToken = authService.login(username, password, clientId, clientSecret);
            //将jti存储到cookie
            this.saveJti2Cookie(authToken.getJti(),response);
            return new Result(true, StatusCode.OK,"登陆成功",authToken.getJti());
        }
    

    2.在service层中通过restTemplate发起请求“/oauth/token”,请求springsecurity认证接口UserDetailsServiceImpl,在loadUserByUsername方法中,用过Feign远程调用user服务中查询User信息进行校对,登陆通过(还可以指定当前用户的角色权限,在资源服务对应的接口方法上使用@PreAuthorize("hasAnyAuthority("xxxx")"),对权限进行控制),则会颁发一个令牌信息并将jti和jwt作为键值对存入redis中,jti存入cookie中。令牌包括6部分数据:
    access_token:访问令牌,携带此令牌访问资源
    token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,Oauth2采 用 Bearer Token。
    refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
    expires_in:过期时间,单位为秒。
    scope:范围,与定义的客户端范围一致。
    jti:当前token的唯一标识。

    public AuthToken login(String username, String password, String clientId, String clientSecret) {
    
            //申请令牌
            ServiceInstance serviceInstance = loadBalancerClient.choose("USER-AUTH");
            URI uri = serviceInstance.getUri();
            String url = uri +"/oauth/token";
    
    
            MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
            body.add("grant_type","password");//密码模式
            body.add("username",username);
            body.add("password",password);
            MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
            headers.add("Authorization",this.getHttpBasic("changgou","changgou"));
    
            restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
                @Override
                public void handleError(ClientHttpResponse response) throws IOException {
                    if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                        super.handleError(response);
                    }
                }
            });
    
            HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(body,headers);
            ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
            //得到令牌信息
            Map map = responseEntity.getBody();
    
            if(map ==null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null){
                throw new RuntimeException("申请令牌失败");
            }
            //封装结果数据
            AuthToken authToken = new AuthToken();
            authToken.setAccessToken((String) map.get("access_token"));
            authToken.setJti((String) map.get("jti"));
            authToken.setRefreshToken((String) map.get("refresh_token"));
    
            //jti jwt存放在redis
            stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);
    
            return authToken;
        }
    

    3.用户下一次访问服务页面时,网关层会检验请求参数cookie中是否含有jti,如果有再去redis中根绝jti拿到Jwt,将令牌信息携带到当前请求的请求头中,去访问用户资源服务。在资源服务中springsecurity框架通过公钥会自动校验jwt,判断用户有无访问资格。

    四.购物车功能

    1.添加购物车

    需求分析

    用户在商品详细页点击加入购物车,提交商品Sku编号和购买数量,添加到购物车。购物车展示页面如下:


    购物车页面展示.png

    表结构分析

    用户登录后将商品添加到购物车,我们需要在订单数据库中添加一张购物车详情表,存储商品详细信息,分类信息,以及数量等等。

    实现思路

    1.添加购物车后,将用户名username,商品skuId,以及购物车详情表中对应的信息封装的OrderItem存入redis中,通过hash结构(key field value)储存数据,大Key和username相关,小Key和skuId有关,value存储购物车详情,一个hash代表一个用户购物车。
    2.用定时任务持久化到mysql,维护一个跟日期相关的set,记录当前日期哪些用户操作了购物车,定时任务可以先去访问set,拿到对应的username。

    具体实现

        @GetMapping("/add")
        public Result addCart(@RequestParam("skuId") String skuId, @RequestParam("num")Integer num){
            String username = tokenDecode.getUserInfo().get("username");
            cartService.addCart(skuId,num,username);
            return new Result(true, StatusCode.OK,"添加购物车成功");
        }
    

    service层:

    @Override
        public void addCart(String skuId, Integer num, String username) {
    
            //查询redis有无商品信息
            OrderItem orderItem = (OrderItem) redisTemplate.boundHashOps(CART+username).get(skuId);
    
            if (orderItem != null) {
                //有 修改商品价格和数量
                orderItem.setNum(orderItem.getNum() + num);
                if(orderItem.getNum()<=0){
                    redisTemplate.boundHashOps(CART+username).delete(skuId);
                    return;
                }
                orderItem.setMoney(orderItem.getNum() * orderItem.getPrice());
                orderItem.setPayMoney(orderItem.getNum() * orderItem.getPrice());
            } else {
                //无 把商品相关的数据添加到redis中
                Sku sku = skuFeign.findById(skuId).getData();
                Spu spu = spuFeign.findSpuById(sku.getSpuId()).getData();
                orderItem = this.sku2OrderItem(sku, spu, num);
            }
    
            redisTemplate.boundHashOps(CART+username).put(skuId,orderItem);
        }
    
    
        private OrderItem sku2OrderItem(Sku sku, Spu spu, Integer num) {
            OrderItem orderItem = new OrderItem();
            orderItem.setSpuId(sku.getSpuId());
            orderItem.setSkuId(sku.getId());
            orderItem.setName(sku.getName());
            orderItem.setPrice(sku.getPrice());
            orderItem.setNum(num);
            orderItem.setMoney(num * orderItem.getPrice()); //单价*数量
            orderItem.setPayMoney(num * orderItem.getPrice()); //实付金额
            orderItem.setImage(sku.getImage());
            orderItem.setWeight(sku.getWeight() * num); //重量=单个重量*数量
    
            //分类ID设置
            orderItem.setCategoryId1(spu.getCategory1Id());
            orderItem.setCategoryId2(spu.getCategory2Id());
            orderItem.setCategoryId3(spu.getCategory3Id());
    
            return orderItem;
        }
    

    2.查询购物车列表

    实现思路

    根据username查询hash,获取orderItemList,总计,金额......

    具体实现

    controller层

        @GetMapping("/list")
        public Map list(){
    
            String username = tokenDecode.getUserInfo().get("username");
            Map map = cartService.list(username);
            return map;
        }
    

    service层

        public Map list(String username) {
            Map map = new HashMap<>();
            //redis中hash结构     Key field value  这里拿到value
            List<OrderItem> orderItemList = redisTemplate.boundHashOps(CART + username).values();
            Integer totalNum = 0 ;
            Integer totalMoney = 0 ;
            for (OrderItem orderItem : orderItemList) {
                totalNum += orderItem.getNum();
                totalMoney += orderItem.getMoney();
            }
            map.put("orderItemList",orderItemList);
            map.put("totalNum",totalNum);
            map.put("totalMoney",totalMoney);
    
            return map;
        }
    

    五.订单功能

    1.分析

    用户申请提交订单后,访问订单微服务。首先将本次订单信息用一个订单表维护,存入数据库。还要通过Feign远程调用商品微服务,完成sku商品的库存扣减操作,记录销量增加,调用user微服务实现user信息中的积分增加功能。

    具体实现

    1.点击提交订单,页面js出发异步请求,访问我们的订单渲染服务中增加新订单的接口。


    提交订单异步请求.png

    从web渲染页面服务Feign调用order微服务


    远程调用order微服务.png

    2.在订单服务中先解析请求头中的令牌信息,拿到当前请求发起的用户名信息,调用service层的具体业务逻辑。

    订单服务controller层.png

    3.根据用户名去查询购物车列表数据(查redis),封装本次订单信息存入订单表,同时将订单商品的详情信息OrderItem也维护到数据库中。至此可以继续完成扣减库存和添加积分的操作了,同时因为我们已经把redis中的数据存入了数据库,删除redis中的内容。

        /**
         * 增加
         * @param order
         */
        @Override
        public void add(Order order){
    
            //1.获取购物车的数据
            Map cartMap = cartService.list(order.getUsername());
            Integer totalMoney = (Integer) cartMap.get("totalMoney");
            Integer totalNum = (Integer) cartMap.get("totalNum");
            List<OrderItem> orderItemList = (List<OrderItem>) cartMap.get("orderItemList");
    
            //填充订单数据到tb_order表
            order.setTotalNum(totalNum);
            order.setTotalMoney(totalMoney);
            order.setPayMoney(totalMoney);
            order.setCreateTime(new Date());
            order.setUpdateTime(new Date());
            order.setBuyerRate("0");//商品评价
            order.setSourceType("1"); //来源,1:WEB
            order.setOrderStatus("0"); //0:未完成,1:已完成,2:已退货
            order.setPayStatus("0"); //0:未支付,1:已支付,2:支付失败
            order.setConsignStatus("0"); //0:未发货,1:已发货,2:已收货
            String orderId = idWorker.nextId() + "";
            order.setId(orderId);
            //插入order表
            orderMapper.insertSelective(order);
    
            //订单明细
            for (OrderItem orderItem : orderItemList) {
                orderItem.setId(idWorker.nextId()+"");
                orderItem.setIsReturn("0");
                orderItem.setOrderId(order.getId());
                orderItemMapper.insertSelective(orderItem);
            }
    
            //扣减库存
            skuFeign.decrCount(order.getUsername());
    
    
            //添加积分
            userFeign.addUserPoints(order.getPayMoney());
    
            //已经把购物车信息维护到数据库了,可以删除redis中的购物车数据信息了
            redisTemplate.delete("cart_"+order.getUsername());
        }
    
    

    五.分布式事务实现

    1.Senta(Fescar原理)

    以畅购项目为例,用户发送下单请求到订单微服务,需要远程调用商品服务实现扣减库存,因为两个数据库位于不同的服务中,所以需要用分布式事务控制。在下单方法上添加一个@GlobalTransactional注解,在分支事务上添加@Transactional注解。
    事务管理器TM会向事务协调器TC注册一个唯一的全局事务Id,通过Feign向下传递给各个资源管理器RM,参与本次事务操作的所有RM就会向TC注册本次分布式事务分支(本地事务),TC将他们归并到本地全局事务的管理范围内,接着分支事务会提交事务,记录undolog日志并返回给TM事务提交结果,由TM向TC发送本次全局事务的提交或者回滚的决议。若发起全局提交,TC释放全局事务锁,异步通知各个RM移除uodolog日志。若发起全局回滚,TC同步通知各个RM基于undolog日志做数据库的反向变动后,移除undolog日志,并释放全局锁。

    2.消息队列

    在订单服务中:生成本次订单,向任务表中添加数据(username,orderId,point积分),设置定时任务(每隔七秒)扫描任务表刷新数据,发送任务数据给MQ

    用户服务中:判断任务在redis中是否存在,若不存在,继续判断在DB中是否存在该任务,不存在,则执行业务逻辑。

    @Transactional
    将任务存入redis,设置过期时间;
    修改用户积分记录;
    增加积分日志表记录;
    删除redis中的任务信息;

    通知订单服务,通过MQ,订单服务增加历史任务表,并删除任务表数据。

    相关文章

      网友评论

          本文标题:业务流程分析

          本文链接:https://www.haomeiwen.com/subject/sekklktx.html