谷粒商城P173~P192【检索服务】

1.controller


@Controller
public class SearchController {
    @Autowired
    MallSearchService mallSearchService;

    @GetMapping("/list.html")
    public String listPage(SearchParam param, Model model){
        SearchResult result = mallSearchService.search(param);
        model.addAttribute("result",result);
        return "list";
    }
}

2. 商城检索条件分析。然后把前端传来的所有可能的查询条件的数据封装在一个vo中,利用springmvc自动把前端传来的数据封装为一个对象进行接收,相当的方便

在这里插入图片描述
还有页码


/**
 * 封装页面所有可能传递过来的查询条件
 */
@Data
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=0/1是否有货
     * skuPrice 1_500/_500/500_价格区间
     * brandId=1 品牌Id
     * catalog3Id
     * attrs=8寸
     */
    private Integer haStock;//是否只显示有货
    private String skuPrice;//价格区间
    private List<Long> brandId;
    private List<String> attrs;//按照属性进行筛选
    private Integer pageNum;//页码
}

3.商城检索返回的数据分析,把查询到的数据封装在一个vo中,返回给前端

在这里插入图片描述

@Data
public class SearchResult {
    //查询到的所有商品的信息
    private List<SkuEsModel> products;

    //分页信息
    private Integer pageNum;//当前页码
    private Long total;//总记录数
    private Integer totalPages;//总页码

    //当前查询结果所涉及到的所有品牌
    private List<BrandVo> brands;
    //当前查询结果所涉及到的所有分类
    private List<CatalogVo> catalogs;
    //当前查询结果所涉及到的所有属性
    private List<AttrVo> attrs;

    @Data
    public static class BrandVo{
        private Long brandId;
        private String brandName;
        private String brandImg;
    }
    @Data
    public static class CatalogVo{
        private Long catalogId;
        private String catalogName;
    }
    @Data
    public static class AttrVo{
        private Long attrId;
        private String attrName;
        private List<String> attrValues;
    }
}

4.service

接口

public interface MallSearchService {
    /**
     * 根据页面传来的检索条件去es中检索出所有商品并过滤聚合出相应的数据
     * @param param
     * @return
     */
    SearchResult search(SearchParam param);
}

实现类

先在kibanna中用es的DSL测试	

在这里插入图片描述

  1. search.gmall.com/list.html?keyword=华为
    全文匹配用must里的match
    在这里插入图片描述

  2. search.gmall.com/list.html?keyword=华为&catalogId=225
    分类、属性、价格这些不需要参与评分的写在filter里。(也可以在must里match后面接着term,但是既然不需要评分,就可以写在filter中)
    在这里插入图片描述

  3. search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9
    brandId是个数组,一个属性、多个值,用terms
    在这里插入图片描述

  4. search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色
    attrs是nested的(嵌入式的),查询的时候要用嵌入式的查询语句
    在这里插入图片描述
    如果再查16号属性,还得再写一个nested哦

  5. search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true
    有无库存继续term
    在这里插入图片描述

  6. search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true&skuPrice=_6000
    按价格区间检索用range
    在这里插入图片描述

  7. search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true&skuPrice=_6000&sort=skuPrice_desc
    排序是与查询并列的,在query后面写sort
    在这里插入图片描述

  8. search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true&skuPrice=_6000&sort=skuPrice_desc&pageNum=4
    分页用from和size,"from":x,"size":y表示从x开始查y个
    在这里插入图片描述
    9.高亮全文查询关键词
    在这里插入图片描述

10.聚合分析(从所有查询结果中提取出相关的属性等信息)
在这里插入图片描述
使用agg
在这里插入图片描述
我们想聚合分析得到属性名字和分类名字应该怎么做呢?可以通过子聚合直接得到,子聚合会用到父聚合的结果再次进行聚合
在这里插入图片描述
要想对brandName进行聚合要求它的doc_valuestrue,所以需要对索引进行修改,修改方式:1.新建一个索引 2. 数据迁移
在这里插入图片描述
新建索引:

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

数据迁移

POST _reindex
{
  "source": {
    "index": "product"
  },
  "dest": {
    "index": "gmall_product"
  }
}

记得把java中商品上架当时写的那个索引名改过来
在这里插入图片描述
在这里插入图片描述

  1. 聚合属性 ,与品牌、分类不同,由于属性是嵌入式的,所以聚合也得用嵌入式的
    在这里插入图片描述

  2. 最终所有的查询语句:


GET gmall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "2"
            ]
          }
        },
        {
          "term": {
            "hasStock": "false"
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 1000,
              "lte": 7000
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "6"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 5,
  "highlight": {
    "fields": {
      "skuTitle": {}
    },
    "pre_tags": "<b style='color:red'>",
    "post_tags": "</b>"
  },
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": {
        "brandNameAgg": {
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
        "brandImgAgg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
      }
    },
    "catalogAgg": {
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalogNameAgg": {
          "terms": {
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attrs": {
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attrIdAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

5. 在service中使用RestHighLevelClient,根据前端传来的条件动态构造DSL语句进行es检索

  @Service
public class MallSearchServiceImpl implements MallSearchService {
    @Autowired
    RestHighLevelClient restHighLevelClient;
    @Autowired
    ProductFeignService productFeignService;

    @Override
    public SearchResult search(SearchParam param) {

        SearchResult searchResult = null;
        // 通过请求参数构建检索请求
        SearchRequest request = bulidSearchRequest(param);
        //使用restHighLevelClient执行检索请求
        try {
            SearchResponse searchResponse = restHighLevelClient.search(request, GmallElasticSearchConfig.COMMON_OPTIONS);
            // 将es响应数据封装成返回给前端的数据
            searchResult = bulidSearchResult(param,searchResponse);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return searchResult;

    }

    /**
     * 构建检索请求SearchRequest
     *
     * @param param
     * @return
     */
    private SearchRequest bulidSearchRequest(SearchParam param) {

        // 用于构建DSL语句
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //1. 构建query
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        //1.1  query-bool-must
        if (!StringUtils.isEmpty(param.getKeyword())) {
            boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
        }

        //1.2 query-bool-filter
        //1.2.1 query-bool-filter-term    catalog
        if (param.getCatalog3Id() != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
        }
        //1.2.2 query-bool-filter-terms     brand
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
            boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
        }
        //1.2.3 query-bool-filter-term     hasStock
        if (param.getHasStock() != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
        }
        //1.2.4 query-bool-filter-range     priceRange
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
        if (!StringUtils.isEmpty(param.getSkuPrice())) {
            String[] prices = param.getSkuPrice().split("_");
            if (prices.length == 1) {
                if (param.getSkuPrice().startsWith("_")) {
                    rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
                } else {
                    rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
                }
            } else if (prices.length == 2) {
                //_6000会截取成["","6000"]
                if (!prices[0].isEmpty()) {
                    rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
                }
                rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
            }
            boolQueryBuilder.filter(rangeQueryBuilder);
        }
        //1.2.5 query-bool-filter-nested     attrs
        //attrs=1_5寸:8寸&attrs=2_16G:8G
        if (param.getAttrs() != null && param.getAttrs().size() > 0) {
            for (String attrStr : param.getAttrs()) {
                BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
                String[] s = attrStr.split("_");
                String attrId = s[0];//检索属性的id
                String[] attrValues = s[1].split(":");
                nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
                //每一个都得生成一个nested查询
                NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
                boolQueryBuilder.filter(nestedQueryBuilder);
            }
        }

        //bool放入searchSourceBuilder
        searchSourceBuilder.query(boolQueryBuilder);

        //2. sort  sort=saleCount_desc/asc
        if (!StringUtils.isEmpty(param.getSort())) {
            String[] sortSplit = param.getSort().split("_");
            searchSourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
        }

        //3. 分页 // 是检测结果分页
        searchSourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
        searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

        //4. 高亮highlight(有模糊匹配才需要高亮)
        if (!StringUtils.isEmpty(param.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            searchSourceBuilder.highlighter(highlightBuilder);
        }

        //5. 聚合
        //5.1 按照brand聚合
        TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg").field("brandId").size(20);
        brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(1));
        brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(1));//字段存错了
        searchSourceBuilder.aggregation(brandAgg);

        //5.2 按照catalog聚合
        TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg").field("catalogId").size(20);
        catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
        searchSourceBuilder.aggregation(catalogAgg);

        //5.3 按照attrs聚合
        NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attrs", "attrs");
        //按照attrId聚合     //按照attrId聚合之后再按照attrName和attrValue聚合
        TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(1));
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(50));
        nestedAggregationBuilder.subAggregation(attrIdAgg);
        searchSourceBuilder.aggregation(nestedAggregationBuilder);

//            log.debug("构建的DSL语句 {}",searchSourceBuilder.toString());

        SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
        System.out.println(request);
        return request;


    }

 

6.将从es中检索到的数据进行封装,封装为发送给前端的数据

在这里插入图片描述

/**
     * 把es响应数据封装为要返回给前端的数据
     *
     * @param  searchResponse
     * @return SearchResult
     */
    private SearchResult bulidSearchResult(SearchParam searchParam,SearchResponse searchResponse) {

        SearchResult result = new SearchResult();

        SearchHits hits = searchResponse.getHits();
        //1. 封装查询到的所有商品信息
        if (hits.getHits() != null && hits.getHits().length > 0) {
            List<SkuEsModel> skuEsModels = new ArrayList<>();
            for (SearchHit hit : hits) {
                String sourceAsString = hit.getSourceAsString();
                SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
                //如果按关键字检索了,拿到高亮的内容
                if (!StringUtils.isEmpty(searchParam.getKeyword())) {
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String highLight = skuTitle.getFragments()[0].string();
                    skuEsModel.setSkuTitle(highLight);
                }
                skuEsModels.add(skuEsModel);
            }
            result.setProducts(skuEsModels);
        }

        //2. 封装分页信息
        //2.1 当前页码
        result.setPageNum(searchParam.getPageNum());
        //2.2 总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        //2.3 总页码
        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);

        //3. 查询结果涉及到的品牌
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        Aggregations aggregations = searchResponse.getAggregations();
        //ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
        ParsedLongTerms brandAgg = aggregations.get("brandAgg");
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
            //3.1 得到品牌id
            Long brandId = bucket.getKeyAsNumber().longValue();

            Aggregations subBrandAggs = bucket.getAggregations();
            //3.2 得到品牌图片
            ParsedStringTerms brandImgAgg = subBrandAggs.get("brandImgAgg");
            String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
            //3.3 得到品牌名字
            Terms brandNameAgg = subBrandAggs.get("brandNameAgg");
            String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            brandVo.setBrandId(brandId);
            brandVo.setBrandImg(brandImg);
            brandVo.setBrandName(brandName);
            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);

        //4. 查询涉及到的所有分类
        List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
        for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
            //4.1 获取分类id
            Long catalogId = bucket.getKeyAsNumber().longValue();
            Aggregations subcatalogAggs = bucket.getAggregations();
            //4.2 获取分类名
            ParsedStringTerms catalogNameAgg = subcatalogAggs.get("catalogNameAgg");
            String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo(catalogId,catalogName);
            catalogVos.add(catalogVo);
        }
        result.setCatalogs(catalogVos);

        //5 查询涉及到的所有属性
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        //ParsedNested用于接收内置属性的聚合
        ParsedNested parsedNested = aggregations.get("attrs");
        ParsedLongTerms attrIdAgg = parsedNested.getAggregations().get("attrIdAgg");
        for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
            //5.1 查询属性id
            Long attrId = bucket.getKeyAsNumber().longValue();

            Aggregations subAttrAgg = bucket.getAggregations();
            //5.2 查询属性名
            ParsedStringTerms attrNameAgg = subAttrAgg.get("attrNameAgg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            //5.3 查询属性值
            ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg");
            List<String> attrValues = new ArrayList<>();
            for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
                String attrValue = attrValueAggBucket.getKeyAsString();
                attrValues.add(attrValue);
                List<SearchResult.NavVo> navVos = new ArrayList<>();
            }
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues);
            attrVos.add(attrVo);
        }
        result.setAttrs(attrVos);

        // 6. 构建面包屑导航
        List<String> attrs = searchParam.getAttrs();
        if (attrs != null && attrs.size() > 0) {
            List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
                String[] split = attr.split("_");
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                //6.1 设置属性值
                navVo.setNavValue(split[1]);
                //6.2 查询并设置属性名
                try {
                    R r = productFeignService.attrInfo(Long.parseLong(split[0]));
                    if (r.getCode() == 0) {
                        AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
                        });
                        navVo.setNavName(attrResponseVo.getAttrName());
                    }
                } catch (Exception e) {
//                    log.error("远程调用商品服务查询属性失败", e);
                }
                //6.3 设置面包屑跳转链接
                String queryString = searchParam.get_queryString();
                String replace = queryString.replace("&attrs=" + attr, "").replace("attrs=" + attr + "&", "").replace("attrs=" + attr, "");
                navVo.setLink("http://search.gulimall.com/search.html" + (replace.isEmpty() ? "" : "?" + replace));
                return navVo;
            }).collect(Collectors.toList());
            result.setNavs(navVos);
        }
        System.out.println(result);
        return result;


    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
谷粒商城P139~140的nginx配置中,我们可以通过反向代理和动静分离来实现。首先,在本机的docker里安装nginx,并将域名和nginx服务器的IP地址对应起来,可以通过修改本机的host文件实现这一点。具体的配置文件包括nginx.conf和gmall.conf。配置完成后,可以访问首页,但是访问静态资源会报404错误。为了解决这个问题,可以按照文档中的指引进行修改。 要创建一个新的nginx容器并进行相关的配置,可以执行以下命令: ``` docker run -p 80:80 --name nginx -v /mydata/nginx/html:/usr/share/nginx/html -v /mydata/nginx/logs:/var/log/nginx -v /mydata/nginx/conf:/etc/nginx -d nginx:1.10 ``` 这样,nginx的html文件夹下的所有资源都可以直接访问。同时,我们还可以建立域名访问。 对于以/static为前缀的请求,我们可以使用以下配置将其转发至html文件夹: ``` location /static { root /usr/share/nginx/html; } ``` 而对于其他请求,我们可以使用反向代理将其转发至gulimall服务器,并设置相应的请求头。例如: ``` location / { proxy_pass http://gulimall; proxy_set_header Host $host; } ``` 最后,可以根据需要进行堆内存的设置。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [谷粒商城P139~140 nginx配置反向代理、动静分离](https://blog.csdn.net/huhuhutony/article/details/120379667)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [谷粒商城-08-p139-p172](https://blog.csdn.net/weixin_45041878/article/details/124761544)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值