美文网首页
业务流程分析

业务流程分析

作者: 影子猪_ | 来源:发表于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,订单服务增加历史任务表,并删除任务表数据。

相关文章

  • P2P理财端业务流程设计

    1、什么是流程分析? 「流程分析」主要分为「业务流程」和「页面流程」。「业务流程」(Transaction Flo...

  • 系统分析师之业务流程分析法

    业务流程分析法,主要方法有价值链分析法、客户关系分析法、供应链分析法、基于ERP的分析法和业务流程重组等。1、价值...

  • django项目--新闻详情页

    一、功能需求分析 1.功能 新闻详情 加载评论功能 添加评论功能 二、新闻详情页 1.业务流程分析 业务流程: 判...

  • PRD常用模板

    整理产品结构 分析核心业务流程 每个产品,都会有几个核心的业务,分析并梳理出几个核心业务流程,可以帮助产品经理了解...

  • 小编教您Springboot项目中异常拦截设计与处理

    项目运行过程中会出现各种各样的问题,常见的有以下几种情况: 业务流程分析疏漏,对业务流程的反向操作、边界分析设计不...

  • 系统分析-业务流程分析

    业务流程分析的目的是了解各个业务流程的过程,明确各个部门之间的业务关系和每个业务处理的意义。 业务分析的步骤:1、...

  • 周检视 | 2018年1月22日~1月28日

    一,工作 本周开始分析Snort源码业务流程。 1,完成Snort源码业务流程的Debug运行模式,程序运行可输出...

  • 业务流程分析

    一.搜索业务概要 用户有可能会根据分类搜索、品牌搜索,还有可能根据规格搜索,以及价格搜索和排序 操作。根据分类和品...

  • 顶级BPM软件

    业务流程管理软件主要用于为人们提供设计、构建、分析、修改和测试各种业务流程的平台。它有助于有效地模拟业务流程生命周...

  • TO B产品审批流程设计

    目录: 审批业务流程分析 将流程转换为系统语言 根据需求联想功能拓展 最基本的审批业务流程由不同角色用户、事件、状...

网友评论

      本文标题:业务流程分析

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