商城业务记录

一.商品上架

上架的商品才可以在网站展示。
上架的商品需要可以被检索。

1.商品分析

分析:商品上架在 es 中是存 sku 还是 spu?
1)、检索的时候输入名字,是需要按照 sku 的 title 进行全文检索的
2)、检索使用商品规格,规格是 spu 的公共属性,每个 spu 是一样的
3)、按照分类 id 进去的都是直接列出 spu 的,还可以切换。
4)、我们如果将 sku 的全量信息保存到 es 中(包括 spu 属性)就太多量字段了。
5)、我们如果将 spu 以及他包含的 sku 信息保存到 es 中,也可以方便检索。但是 sku 属于
spu 的级联对象,在 es 中需要 nested 模型,这种性能差点。
6)、但是存储与检索我们必须性能折中。
7)、如果我们分拆存储,spu 和 attr 一个索引,sku 单独一个索引可能涉及的问题。
检索商品的名字,如“手机”,对应的 spu 有很多,我们要分析出这些 spu 的所有关联属性,
再做一次查询,就必须将所有 spu_id 都发出去。假设有 1 万个数据,数据传输一次就
10000*4=4MB;并发情况下假设 1000 检索请求,那就是 4GB 的数据,,传输阻塞时间会很
长,业务更加无法继续。
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数
据库范式。

1).添加映射

PUT product
{
  "mappings": {
    "properties": {
      "skuId": {
        "type": "long"
      },
      "spuId": {
        "type": "keyword"
      },
      "skuTitle": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice": {
        "type": "keyword"
      },
      "skuImg": {
        "type": "keyword",
        "index": false,
        "doc_values": false
      },
      "saleCount": {
        "type": "long"
      },
      "hasStock": {
        "type": "boolean"
      },
      "hotScore": {
        "type": "long"
      },
      "brandId": {
        "type": "long"
      },
      "catalogId": {
        "type": "long"
      },
      "brandName": {
        "type": "keyword",
        "index": false,
        "doc_values": false
      },
      "brandImg": {
        "type": "keyword",
        "index": false,
        "doc_values": false
      },
      "catalogName": {
        "type": "keyword",
        "index": false,
        "doc_values": false
      },
      "attrs": {
        "type": "nested",
        "properties": {
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword",
            "index": false,
            "doc_values": false
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

index
默认 true,如果为 false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能
当做检索条件。

doc_values:
默认 true,设置为 false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。
还可以通过设定 doc_values 为 true,index 为 false 来让字段不能被搜索但可以用于排序、聚
合以及脚本操作。

2.上架

上架是将后台的商品放在 es 中可以提供检索和查询功能
1)、hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要
更新一下 es
2)、库存补上以后,也需要重新更新一下 es
3)、hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新
热度值。
4)、下架就是从 es 中移除检索项,以及修改 mysql 状态
商品上架步骤:
1)、先在 es 中按照之前的 mapping 信息,建立 product 索引。
2)、点击上架,查询出所有 sku 的信息,保存到 es 中
3)、es 保存成功返回,更新数据库的上架状态信息。

3、数据一致性

1)、商品无库存的时候需要更新 es 的库存信息
2)、商品有库存也要更新 es 的信息

二、商品检索

1.检索业务分析

商品检索三个入口:
1)、选择分类进入商品检索

2)、输入检索关键字展示检索页

3)、选择筛选条件进入

  • 检索条件&排序条件
  • 全文检索:skuTitle
  •  排序:saleCount、hotScore、skuPrice
  •  过滤:hasStock、skuPrice 区间、brandId、catalogId、attrs
  •  聚合:attrs

完整的 url 参数
keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1
&catalogId=1&attrs=1_3G:4G:5G&attrs=2_骁龙 845&attrs=4_高清屏

2.构建语句检索

1.请求参数  

public class SearchParam {

    private  String keyword; //页面传递过来的全文匹配关键字

    private Long catalog3Id; //三级分类id

    /**
     * sort=saleCount_asc/desc
     * sort=skuPrice_Asc/desc
     * sort=hotScore_asc/desc
     */
    private String sort ; //排序条件

    /**
     * 好多的过滤条件
     * hasStock(是否有货) skuPrice区间 brandId catalog3Id  attrs
     * hasStock=0/1
     * skuPrice 1_500/_500/500_
     * brandId=1
     * attrs=2_5寸:6寸
     *
     */

    private Integer hasStock; //是否只显示有货 0无货 1有货

    private String skuPrice;//价格区间查询

    private List<Long> brandId;//按照品牌id进行查询,可以多选

    private List<String> attrs;//按照属性筛选

    private Integer pageNum=1;//页码


    private String _queryString;//原生的所有查询条件

2.构建参数

  private SearchRequest buildSearchRequest(SearchParam param) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();//构建dsl语句
        /**
         * 根据在kibana中的语法 (json数据里有)
         * 查询 :模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存)
         */
        //构建bool-Query 里面还有很多查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //1.1 must条件 模糊匹配
        if (!StringUtils.isEmpty(param.getKeyword())) {
            boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
        }
        //1.2 filter 按照三级分类id查询
        if (param.getCatalog3Id() != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
        }
        //1.2 filter 按照品牌id查询
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
            boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
        }
        //1.2 filter 按照所有指定的属性进行查询
        if (param.getAttrs() != null && param.getAttrs().size() > 0) {
            //attrs=1_5寸:8寸&attrs=2_16G:8G

            for (String attrStr : param.getAttrs()) {
                //attr=1_5寸:8寸
                BoolQueryBuilder nestedBoolQueryBuilder = QueryBuilders.boolQuery();
                String[] s = attrStr.split("_");
                String attrId = s[0];//检索的属性id
                String[] attrValues = s[1].split(":");//这个属性检索用的值
                nestedBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                nestedBoolQueryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
                //每一个都必须生成一个nested查询
                NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", nestedBoolQueryBuilder, ScoreMode.None);
                boolQueryBuilder.filter(nestedQueryBuilder);
            }

        }
        //1.2 filter 按照是否有库存进行查询
        if (param.getHasStock() != null) {

            boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
        }

        //1.2 filter 按照价格区间进行查询
        if (!StringUtils.isEmpty(param.getSkuPrice())) {
            //1_500 /_500/500_
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
            String[] s = param.getSkuPrice().split("_");
//            System.out.println("价格"+ Arrays.asList(s));
//            System.out.println(s[0]);
            if (s.length == 2) {
                //区间
                rangeQueryBuilder.gte(s[0]).lte(s[1]);
            } else if (s.length == 1) {
                if (param.getSkuPrice().startsWith("_")) {
                    rangeQueryBuilder.lte(s[0]);
                }
                if (param.getSkuPrice().endsWith("_")) {
                    rangeQueryBuilder.gte(s[0]);
                }
            }
            boolQueryBuilder.filter(rangeQueryBuilder);
        }

        //把以前所有的条件都拿来进行封装
        searchSourceBuilder.query(boolQueryBuilder);
        /**
         * 排序、分页、高亮、
         */
        //2.1排序
        if (!StringUtils.isEmpty(param.getSort())) {
            String sort = param.getSort();
            //sort=hotScore_asc/desc
            String[] s = sort.split("_");
            System.out.println(Arrays.asList(s));
            SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            searchSourceBuilder.sort(s[0], order);
        }
        //2.2分页 pageSize=5
        //pageNum :1 from :0 size:5 [0,1,2,3,4]
        //pageNum :2 from :1 size:5
        //from =(pageNum-1)*5
        searchSourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
        searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

        //2.3高亮
        if (!StringUtils.isEmpty(param.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            searchSourceBuilder.highlighter(highlightBuilder);

        }


        /**
         * 聚合分析
         */
        //1.品牌聚合
        TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg");
        brandAgg.field("brandId").size(50);
        //品牌聚合的子聚合
        brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(1));
        brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(1));
        searchSourceBuilder.aggregation(brandAgg);

        //2.分类聚合
        TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg");
        catalogAgg.field("catalogId").size(20);
        //分类聚合的子聚合
        catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
        searchSourceBuilder.aggregation(catalogAgg);

        //3属性聚合
        NestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attrs", "attrs");
        //聚合出当前所有的attrId
        TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
        //聚合分析出当前attr_id对应的名字
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(1));
        //聚合分析出当前attr_id对应的所有可能的属性值
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(50));
        attrsAgg.subAggregation(attrIdAgg);
        searchSourceBuilder.aggregation(attrsAgg);


        String s = searchSourceBuilder.toString();
        System.out.println("构建dsl" + s);
        SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
        return searchRequest;
    }

3.响应数据模型

public class SearchResult {

    //查询到的所有商品信息
    private List<SkuEsModel> products;

    /**
     * 分页信息
     */
    private Integer pageNum;//当前页码
    private Long   total;//总记录数
    private Integer totalPages;//总页码
    private List<Integer> pageNavs;
    private List<BrandVo> brands;//当前查到的结果,所有涉及到的品牌
    private List<AttrVo>attrs;//当前查到的结果,所有涉及到的属性
    private List<CatalogVo>catalogVos;//当前查到的结果,所有涉及到的分类


    //=====================以上是返回给页面的所有信息===============================

    /**
     * 面包屑导航数据
     */
    private List<NavVo> navs=new ArrayList<>();
    private List<Long> attrIds=new ArrayList<>();

    @Data
    public static  class NavVo{
        private String navName;
        private String navValue;
        private String link;
    }

    /**
     * 品牌vo
     */
    @Data
    public static class BrandVo{
        private Long brandId;
        private String brandName;
        private String brandImg;
    }

    /**
     * 属性vo
     */
    @Data
    public static class AttrVo{
        private Long attrId;
        private String attrName;
        private List<String> attrValue;
    }

    /**
     * 分类vo
     */
    @Data
    public static class CatalogVo{
        private Long catalogId;
        private String catalogName;

    }

4.响应结果封装

 private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
        SearchResult result = new SearchResult();
        //1.返回所有查询到的商品

        //获取命中的记录
        SearchHits hits = response.getHits();
        List<SkuEsModel> esModels = new ArrayList<>();
        if (hits.getHits() != null && hits.getHits().length > 0) {
            for (SearchHit hit : hits.getHits()) {
                String sourceAsString = hit.getSourceAsString();
                SkuEsModel skuEsModel = new SkuEsModel();
                //从es中检索得到的对象
                SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
                if (!StringUtils.isEmpty(param.getKeyword())) {
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    //高亮数据
                    String string = skuTitle.getFragments()[0].string();
                    esModel.setSkuTitle(string);
                }

                esModels.add(esModel);
            }
        }
        result.setProducts(esModels);


        //当前所有商品涉及到的所有属性
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        //获取得到属性的聚合
        ParsedNested attrsAgg = response.getAggregations().get("attrs");
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");
        List<? extends Terms.Bucket> attrIdAggBuckets = attrIdAgg.getBuckets();
        //每一个属性信息
        attrIdAggBuckets.forEach(attr -> {
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            //得到属性的id
            long attrId = attr.getKeyAsNumber().longValue();

            //得到属性的名字 子聚合
            ParsedStringTerms attrNameAgg = attr.getAggregations().get("attrNameAgg");
            //一个属性id对应一个属性名字
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();

            //得到属性的所有值
            ParsedStringTerms attrValueAgg = attr.getAggregations().get("attrValueAgg");
            //这个值不定向 不单一 所有属性的值
            List<String> attrValues = attrValueAgg.getBuckets().stream().map(attrValue -> {
                String attrValueKeyAsString = attrValue.getKeyAsString();
                return attrValueKeyAsString;
            }).collect(Collectors.toList());
            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            attrVo.setAttrValue(attrValues);
            result.getAttrIds().add(attrId);
            attrVos.add(attrVo);

        });
        result.setAttrs(attrVos);


        //当前商品所涉及的品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        //获取到的品牌聚合
        ParsedLongTerms brandAgg = response.getAggregations().get("brandAgg");
        List<? extends Terms.Bucket> brandBuckets = brandAgg.getBuckets();
        brandBuckets.forEach(bucket -> {
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            //得到品牌id
            long brandId = bucket.getKeyAsNumber().longValue();

            //得到品牌名字 子聚合
            ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brandNameAgg");
            String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();

            //得到品牌图片 子聚合
            ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brandImgAgg");
            String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
            brandVo.setBrandId(brandId);
            brandVo.setBrandName(brandName);
            brandVo.setBrandImg(brandImg);
            brandVos.add(brandVo);

        });
        result.setBrands(brandVos);

        //当前商品所涉及的所有分类信息
        ParsedLongTerms catalogAgg = response.getAggregations().get("catalogAgg");
        List<? extends Terms.Bucket> catalogBuckets = catalogAgg.getBuckets();
        List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        catalogBuckets.forEach(bucket -> {
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            //得到分类id
            String keyAsString = bucket.getKeyAsString();
            catalogVo.setCatalogId(Long.parseLong(keyAsString));

            //得到分类名字 得到子聚合
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");
            //根据分类id查到的分类名字
            String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalogName);
            catalogVos.add(catalogVo);
        });
        result.setCatalogVos(catalogVos);

        //=======以上从聚合信息中获取===
        //分页信息-页码
        result.setPageNum(param.getPageNum());

        //分页信息-总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);

        //分页信息-总页码  计算得到 11条总记录数/2 =5...1
        Integer totalPages = (int) total % EsConstant.PRODUCT_PAGESIZE == 0 ? (int) total / EsConstant.PRODUCT_PAGESIZE : (int) (total / EsConstant.PRODUCT_PAGESIZE + 1);
        result.setTotalPages(totalPages);
        List<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= totalPages; i++) {
            pageNavs.add(i);
        }
        result.setPageNavs(pageNavs);


        //构建面包屑导航功能

        //遍历检索属性
        if (param.getAttrs() != null && param.getAttrs().size() > 0) {


            List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {

                //分析每个attrs传过来的查询参数值
                //attrs=2_5存:6寸
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                String[] s = attr.split("_");
                navVo.setNavValue(s[1]);
                R r = productFeignService.getAttrInfo(Long.parseLong(s[0]));
                result.getAttrIds().add(Long.parseLong(s[0]));
                if (r.getCode() == 0) {
                    AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
                    });
                    String attrName = data.getAttrName();
                    navVo.setNavName(attrName);
                } else {
                    navVo.setNavName(s[0]);
                }

                //取消了这个面包屑以后 我们要跳转到那个地方 将请求地址的url里面置空
                //拿到所有的查询条件 去掉当前
                //attrs= 10_海思
                String replace = replaceQueryString(param, attr, "attrs");
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);
                return navVo;
            }).collect(Collectors.toList());


            result.setNavs(collect);
        }
        //品牌 分类
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
            List<SearchResult.NavVo> navs = result.getNavs();
            SearchResult.NavVo navVo = new SearchResult.NavVo();
            navVo.setNavName("品牌");
            //todo远程查询所有品牌
            R r = productFeignService.brandsInfo(param.getBrandId());
            if (r.getCode() == 0) {
                List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
                });
                System.out.println(brand.toString());
                StringBuffer buffer = new StringBuffer();
                String replace = "";
                for (BrandVo brandVo : brand) {
                    buffer.append(brandVo.getName() + ";");
                    replace = replaceQueryString(param, brandVo.getBrandId() + "", "brandId");
                }
                navVo.setNavValue(buffer.toString());
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);
            }


            navs.add(navVo);
        }
        //todo 分类不需要导航取消

        return result;
    }
private String replaceQueryString(SearchParam param, String value, String key) {
        String encode = null;
        try {
            encode = URLEncoder.encode(value, "UTF-8");
            encode = encode.replace("+", "%20"); //浏览器对空格的编码和Java不一样,差异化处理
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return param.get_queryString().replace("&" + key + "=" + encode, "");
    }

三、购物车

1.购物车需求

1)、需求描述

用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】

  • - 放入数据库
  • - mongodb
  • - 放入 redis(采用)

登录以后,会将临时购物车的数据全部合并用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】

  • - 放入 localstorage(客户端存储,后台不存)
  • - cookie
  • - WebSQL
  • - 放入 redis(采用)

浏览器即用户可以使用购物车一起结算下单

  • - 给购物车添加商品
  • - 用户可以查询自己的购物车
  • - 用户可以在购物车中修改购买商品的数量。
  • - 用户可以在购物车中删除商品。
  • - 选中不选中商品
  • - 在购物车中展示商品优惠信息
  • - 提示购物车商品价格变化使关闭,下次进入,临时购物车数据都在过来,并清空临时购物车;

2).数据结构
因此每一个购物项信息,都是一个对象,基本字段包括:

{
skuId: 2131241,
check: true,
title: "Apple iphone.....",
defaultImage: "...",
price: 4999,
count: 1,
totalPrice: 4999,
skuSaleVO: {...}
}

另外,购物车中不止一条数据,因此最终会是对象的数组。即:

[
{...},{...},{...}
]

Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map<String, List<String>>
首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,Value 是
用户的所有购物车信息。这样看来基本的`k-v`结构就可以了。
 但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,
为了方便后期处理,我们的购物车也应该是`k-v`结构,key 是商品 id,value 才是这个商品的
购物车信息。
综上所述,我们的购物车结构是一个双层 Map:Map<String,Map<String,String>>
第一层 Map,Key 是用户 id
第二层 Map,Key 是购物车中商品 id,值是购物项数据

3)、流程

user-key 是随机生成的 id,不管有没有登录都会有这个 cookie 信息。
两个功能:新增商品到购物车、查询购物车。
新增商品:判断是否登录
- 是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key。
- 否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key。
查询购物车列表:判断是否登录
- 否:直接根据 user-key 查询 redis 中数据并展示
- 是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
- 有:需要提交到后台添加到 redis,合并数据,而后查询。
- 否:直接去后台查询 redis,而后返回。

2.临时购物车

2、临时购物车
/**
*
获取到我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey = "";
if (userInfoTo.getUserId() != null) {
//zm:cart:1
cartKey = CART_PREFIX + userInfoTo.getUserId();
} else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations =
redisTemplate.boundHashOps(cartKey);
return operations;
}

3.登录购物车

public CartVo getCart() throws ExecutionException, InterruptedException {
        CartVo cartVo = new CartVo();
        //获取用户信息
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if (userInfoTo.getUserId() != null) {
            //登录状态的key
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            //通过登录用户的key获取redis中的数据
//            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);

            //如果临时购物车里的数据还没有合并
            //临时购物车的key
            String s = CART_PREFIX + userInfoTo.getUserKey();
            //临时购物车的数据 从redis中获取
            List<CartItemVo> tempCartItems = getCartItemList(s);
            if (tempCartItems != null && tempCartItems.size() > 0) {
                //临时购物车有数据,需要合并
                for (CartItemVo cartItem : tempCartItems) {
                    //遍历集合 添加到购物车里合并
                    //调用方法 存在即数量叠加 不存在则新增
                    addToCart(cartItem.getSkuId(), cartItem.getCount());
                }
                //合并完成之后删除临时购物车
                deleteCart(s);
            }


            //获取登录后的购物车的数据 包含合并的临时购物车数据
            List<CartItemVo> loginCartItem = getCartItemList(cartKey);
            cartVo.setItems(loginCartItem);

        } else {
            //没登录
            //获取临时用户的key
            String cartKey = CART_PREFIX + userInfoTo.getUserKey();
            //通过临时用户的key获取redis中的数据
            List<CartItemVo> cartList = getCartItemList(cartKey);
            cartVo.setItems(cartList);

        }
        return cartVo;
    }

四、订单业务

1.环境搭建

可以发现订单结算页,包含以下信息:
1. 收货人信息:有更多地址,即有多个收货地址,其中有一个默认收货地址
2. 支付方式:货到付款、在线支付,不需要后台提供
3. 送货清单:配送方式(不做)及商品列表(根据购物车选中的 skuId 到数据库中查询)
4. 发票:不做
5. 优惠:查询用户领取的优惠券(不做)及可用积分(京豆)

1)、订单确认页vo实体类

public class OrderConfirmVo {

    //收货地址 ums_member_receive_address
    //用户地址信息
    @Getter
    @Setter
    private List<MemberReceiveAddVo> memberReceiveAddVos;

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

    //发票记录

    //优惠券信息...
    //积分
    @Getter
    @Setter
    private Integer integration;


    //防止用户重复提交(令牌)
    @Getter
    @Setter
    private String orderToken;

    //库存信息
    @Getter
    @Setter
    Map<Long, Boolean> stocks;

    //订单总额
//    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();
    }


    //总件数计算
    public Integer getCount() {
        Integer i = 0;
        if (items != null) {
            for (OrderItemVo item : items) {
                i += item.getCount();
            }
        }
        return i;
    }

2)OrderItemVO

public class OrderItemVo {

    private Long skuId;
    //    private Boolean check = true;//是否被选中
    private String title;//标题
    private String image;//图片
    private List<String> skuAttr;//销售属性组合描述
    private BigDecimal price;//商品单价
    private Integer count;//商品数量
    private BigDecimal totalPrice;//总价,需要计算

    private boolean hasStock;//是否有货

    private BigDecimal weight;//重量
}

2.创建订单

当用户点击提交订单按钮,应该收集页面数据提交到后台并生成订单数据

1)、数据模型

public class OrderSubmitVo {

    private Long addrId;//收货地址的id

    private Integer payType;//支付方式

    //无需提交购买的商品 去购物车在获取一遍
    //优惠 发票

    private String orderToken;//防重令牌
    private BigDecimal payPrice;//应付金额  验证价格

    //用户相关信息都在session里

    private String note;//订单备注
}

2)、响应数据模型

public class SubmitOrderResponseVo {

    private OrderEntity order;//订单实体类
    private Integer code;//状态码【0-成功】

}

五、支付业务

1.支付宝

还需要配置加密、加签。

整合支付宝

对应的文档中心

2.内网穿透

使用花生壳

 3.整合支付

1)、依赖下载

   <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.33.55.ALL</version>
        </dependency>

2)、模板类

@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {

    //在支付宝创建的应用的id
    private String app_id = "";

    // 商户私钥,您的PKCS8格式RSA2私钥
    private String merchant_private_key="";
    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    private String alipay_public_key = "";
    // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
    private String notify_url ="https://6026y553n3.zicp.fun/payed/notify";

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    //同步通知,支付成功,一般跳转到成功页
    private String return_url="";

    // 签名方式
    private String sign_type = "RSA2";

    // 字符编码格式
    private String charset = "utf-8";

    // 支付宝网关; https://openapi.alipaydev.com/gateway.do
    private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";

    private String timeOut="1m";

    public String pay(PayVo vo) throws AlipayApiException {

        //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
        //1、根据支付宝的配置生成一个支付客户端
        AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
                app_id, merchant_private_key, "json",
                charset, alipay_public_key, sign_type);

        //2、创建一个支付请求 //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(return_url);
        alipayRequest.setNotifyUrl(notify_url);

        //商户订单号,商户网站订单系统中唯一订单号,必填
        String out_trade_no = vo.getOut_trade_no();
        //付款金额,必填
        String total_amount = vo.getTotal_amount();
        //订单名称,必填
        String subject = vo.getSubject();
        //商品描述,可空
        String body = vo.getBody();

        alipayRequest.setBizContent("{\"out_trade_no\":\"" + out_trade_no + "\","
                + "\"total_amount\":\"" + total_amount + "\","
                + "\"subject\":\"" + subject + "\","
                + "\"body\":\"" + body + "\","
                + "\"timeout_express\":\"" + timeOut + "\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        String result = alipayClient.pageExecute(alipayRequest).getBody();

        //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
        System.out.println("支付宝的响应:" + result);

        return result;

    }
}

3)、实体类

@Data
public class PayVo {

    private String out_trade_no;// 商户订单号 必填
    private String subject;// 订单名称 必填
    private String total_amount; // 付款金额 必填
    private String body; // 商品描述 可空
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值