项目分析过程,已经学习思路记录

作为电商产品经理,你不得不知----电商后台系统-商品中心 - 知乎 (zhihu.com)

raw.githubusercontent.com/elastic/elasticsearch/7.4/docs/src/test/resources/accounts.json

document.querySelector ('bwp-video').playbackRate =0.8

应该区分产品的类型spu,和产品的类型的实例sku,gitee.com/agoni_no/gulimall

后台管理的平台属性

    商品系统商品分类维护0

开始:

    商品种类管理0:是对商品唯一种类归类,通过三级分类的形式去组织查找种类产品信息,该产品类型描述产品的功能特性,是一个抽象层,就产品的spu,一个该类型下的实例产品称位sku,sku具有商品的规格基本属性即分组属性,以及产品的销售属性,即具体区别的分组对应的详细属性,这里的商品种类分级通过字段来实现,level分为父子孙三级,任意一种都可以改变三者关系,在前端的展示,就是根据所有的种类的集合对象通过strem流进行分组,所有level=1的为一级显示为,为二的判断父类的的对象加入该一级分组的实体的chlidren子集,完成所有搜集并显示前端。

2024年3月4日重新过一遍知识(电商项目值得一直回看),后台系统大概模块如图:

系统管理模块、商品系统模块(子功能模块 分类维护、商品管理、平台属性(属性分组、规格参数 、销售属性 )、商品维护(子模块spu管理、发布商品、商品管理))、优惠营销模块、库存系统模块

品牌管理 1

   品牌id,商品名称,商品log,介绍,显示状态,检索首字母,排序,操作等视图

这是品牌实体的cur操作,包括品牌的关联分类是在已经存在的分类基础上添加的。比如小米这个品牌它对应分类是手机,当修改的售后,同样需要品牌分类的三级菜单回显修改传入.

需要分页处理添加配置类,加上@EnableTransactionManagement开启事务。

 @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面,true为回到首页,false为继续请��
        paginationInterceptor.setOverflow(true);
        // 设置最大page数
        paginationInterceptor.setLimit(1000);
        return paginationInterceptor;
    }

品牌管理里面,默认进入管理页面为查询一页展示数据,通过搜索框可以查询其他数据,可以对展示数据的修改,删除操作。

品牌的添加较为麻烦使用到了阿里云的文件上传系统将图片文件保存到远程服务器,使用非对称加密,通过发放的公钥兑换自己的公钥加上已经有的私钥,前端通过请求文件上传,获取到签名字段,直接请求阿里云完成图片上传功能,验证签名解决跨域后完成上传。实现细节如下几种

文件上传策略1前端js直接拿到云存储的账号密码在js前端携带密码账号向云储存的桶容器存储数据流

2浏览器前本地选择的图片流交给后端发送,缺点是上传请求多需要过网关在过服务消耗性能

3浏览器请求后端,向云服务器发送请求拿到上传的验证信息,将验证信息给前端,前端js直接将文件流和策略传给云存储容器,由服务器自己验证是否上传成功,其前端提交的行为action就是桶的外域名地址,提交表单时oss的桶的外域地址和表单所在的提交域名不同,所以存在跨域无法发送数据到外域容器系统,解决oss容器的跨域通过请求问题的权限,会携带后端的请求的头到存储服务,然后验证上传

其实后端使用aliyun的子账户生成的accesskeyid和accesskeysercet在后端构建一个上传客户端对象,直接使用该对象调用方法参数是图片流数据地址 ,直接就可以上传到服务器,担是这里是前端选择的图片,交给前端直接上传更快捷。

这是原生的sdk在java后端对oos的操作aliyun同样提供了对oos的spring-Cloud-starter-aliyun-oss版本对oos操作支持它封装原生的sdk来操作oss的云储存容器数据,该starter中就导入了原生aliyun-sdk-oss。

单文件上传action的地址是oss桶的外域地址,:data=”dataobj”是封装的品牌对象,提交的是post请求,携带的数据就是js向后端请求的验证签名,在此基础上的key属性加上像上传的目录+uuid(防止重复)+文件名加上要穿的属性文件列表发给oss服务系统解析,验证,上传成功,成功后会将oss的图片地址,回绑到dataobj的logo属性上并且是oss上的图片地址,选择图片时自动提交会带流数据,文件提交空间只需要传给服务的验证签名,和携带文件流生成的新文件名和所在目录,而文件的流是对象自带的数据不需要额外关心和处理自然就没必要有处理的方式。细节如图 上传组件action提交oss的地址,携带请求头数据header有js向后端请求到的签名,key是文件的目录结构,最下面表单中封装file属性对应了就是文件的二进制数据流,oss保存的数据完成。

关联分类功能  (清楚品牌列表下的功能,向中间表商品分类和品牌关联表中加入id,和对应的name属性,好处避免大数据关联查询,在添加中间表品牌和商品分类中会用到各自的mapper对象完成实体结合关联类CategoryBrandRelationEntity的分段数据查询,查到各自的name属性维护到中间表中去代码如下)

        每一个品牌都有所属的分类,比如华为、小米、vivo为手机分类下的品牌,一个品牌可以关联多个分类,比如小米关联了手机,电视 、手表等分类,一个品牌对应多个分类,一个分类可以对多个品牌,整体是一个多对多的关系。所以使用中间关联表category_brand_relation来维护多对多关系。

当点击该品牌关联分类时会请求分类产品品牌关联表加上brandId查出品牌下的所有分类,即获取品牌的关联分类(文档地址15、获取品牌关联的分类 - 谷粒商城)该分类就是分类品牌关联表的实体对象,它封装了详细信息,返回前端显示。品牌下的所有关联分类查询完成。

     @Override
    public List<CategoryBrandRelationEntity> getBrandCateRelation(Long brandId) {
      //baseMapper相当于代理类,mybassPlus通过自定义的服务实现类,把生成的代理对象封装到
      //ServiceImp中去,第一个参数就是类型,定义了该类型的变量baseMapper,这是生成的代理对象
        List<CategoryBrandRelationEntity> relationEntityList = baseMapper.selectList(new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
        return relationEntityList;
    }

新增关联关系时会发送品牌id和分类id到关联表中,比如华为-》手机分类;华为-》电脑分类;小米-》手机;小米-》电脑,那么这些品牌所对应的商品分类就可以衍生出多种产品如手机分类:小米-》手机-》(小米5、小米7、小米11),小米11为spu,青春版对应sku是具体实例,定义了销售属性如颜色,尺寸。除此之外中间表还要保存brand_name、catelog_name添加两个冗余字段,避免做关联查询,多表关联影响性能。

  /**
     * 保存 品牌和分类的id和名称
     *
     * @param categoryBrandRelation
     */
    @Transactional
    @Override
    public void saveIdAndName(CategoryBrandRelationEntity categoryBrandRelation) {
        Long brandId = categoryBrandRelation.getBrandId();
        Long catelogId = categoryBrandRelation.getCatelogId();
        BrandEntity brandEntity = brandDao.selectById(brandId);
        CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
        categoryBrandRelation.setBrandName(brandEntity.getName());
        categoryBrandRelation.setCatelogName(categoryEntity.getName());
        baseMapper.insert(categoryBrandRelation);
    }

完成后在品牌列表中点击关联如选择华为品牌关联到(手机/手机通讯/手机;家用电器/大家电)分类中就生成两个表中已经存在的记录建立关系。生成记录到关联表中,其中对商品分类的修改和品牌的修改,都需要同步到中间表防止无效的数据存在。

品牌管理之关联分类:商品分类和品牌是多对对关系,需要一个额外的中间表进行关联,里面关联了费雷的id品牌的id,一个品牌可以对应多个产品分类,比如小米品牌有分类的小米汽车,小米说及,一个分类也同样可以对应多个品牌,比如手机分类可以对应小米平拍,华为品牌,荣耀品牌等等。品牌与分类的绑定是通过关联关系绑定,初始添加时没有直接关系,如果有的话较为麻烦,因为需要一次性确定所有关系,所以通过品牌和分类的选择绑定功能更符合生活动态化需求。

所谓品牌和分类的绑定是就是对品牌和分类的中间表添加一个完整的对应关系,通过手动绑定的分类id和品牌id分别去查出两个对象,然后分装分类和品牌表的中间表的实体对象,中间表设置额外的显示字段如分类名称和品牌名称,好处可以直接查中间表进行显示各自的名称(前端在绑定关联的时候只能选择选择控件的分类id和点击绑定关联的品牌id所以需要到后端更具id查出他们的名称,然后插入关联表中)

如图存储了冗余的字段是分类名和品牌名,担是如果品牌修改后或分类名称修改后就需要同步到中间表中。

考虑关系中间表是减少级联查询影响的性能问题,中间表带来了额外的维护细节。

    属性分组管理 2

    规格参数管理 3

    销售属性管理 4

开始属性分组管理 2

属性分组的查询 实现思路 点击分类的三级菜单项,只有属性三级菜单的level的菜单才会触发请求查询,如果没有选择分类菜单项通过关键字key模糊查询,key的查询对象可以是分组属性的id分组属性的name以及查询的分页参数没有分类id就查找所有符合条件的分类。

理清属性组和属性和产品属性值的查询规则,以及这些关联的分类id和skuid之间建立的联系

属性分组-商品规格属性-销售属性 三级分类 进行关联(只有三级分类的菜单项点击才会查到三级分类的属性组对应的属性)

商品类别以三级分类的形式组织和搜索,特定分类下的商品,携带着产品分类类别id查找属性分组表中对应唯一类别商品的多个id属性,从属性分组表获取id后,去属性分组和属性关联表中查找具体的属性,这时一个分组id找出多个属性,在用关联的属性id,去查属性的具体信息。(通过中间表的形式较少联合查询),一个商品是一个spuid,一个spuid对应一组分组属性,一种分组属性对应多个属性类别和属性值(如屏幕组->(属性大小:12、清晰度:20))->cpu组->(cpu型号:七零、cpu核心数:7等),先去查商品的分类,进行三级展示,通过分类找出该产品的sku的规格基础属性即分组属性,更具分组找到该组下的所有属性字段和属性值,有了属性id再去sku-arry关联表中获取具体属性的sku产品的属性值。

在属性组管理对象中,依赖商品分类查询即商品分类的三级菜单,当点击商品即spu时会传如catgoryId去到属性关联表中的属性分组表,属性分组表中有属性id,再通过属性id查出每组属性的多个商品属性字段

用户没有通过三级分类,去点击,而是通过搜索框输入,就会传一个key的属性字段,此时是自定义查询,用于多条件模糊查询,比如属性组id或属性组名称满足都能查出结果,通过构建查询条件。QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<>();来构建多条件查询

        wrapper.and((obj) -> {
                obj.eq("attr_group_id", key).or().like("attr_group_name", key);
            });

        if (!StringUtils.isEmpty(key)) {
            wrapper.and((obj) -> {
                obj.eq("attr_group_id", key).or().like("attr_group_name", key);
            });
        }
        if (catelogId == 0) {
            IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
                    wrapper);
            return new PageUtils(page);
        } else {
          //如果是三级分类去查,传入了id就按id查找,否则如上查出所有商品以及对应的属性组
            wrapper.eq("catelog_id", catelogId);
            IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
                    wrapper);
            return new PageUtils(page);
        }

构造查询wrapper要注意在where后的=用eq方法在加and时并列上(多条件满足)就使用and方法加函数式接口条件判断,obj是表中要匹配的每个字段。

让后通过当前Service的实现类this.page的功能去查询出结果集,并把结果其和查询的条件封装成QueryUtil返回给前端,前端查询/product/attrgroup/listt/225?page=1&key=aaa;就能查到特点商品的分组信息,包括sku的销售属性,sku的基本规格属性。

目前没有数据,就在arrGroup_id中添加catelog_id中添加225三级菜单对应商品种类id,并对应属性组名字为主体,添加多个属性组(主体,基本信息)商品种类id都是255,完成一对多查询,当三级菜单点击后会查出结果,并把数据展示到右侧的表格当中,如果直接搜索是通过key字段在查属性组表中模糊查询,id字段和属性分组名字查到都可,没有categoryId就查出上面这些,包括不同商品的分组属性信息

分组属性的添加

        通过级联选择器选择来维护添加属性分组,属于当前产品种类的哪一个sku,在该产品实例下定义添加分组,一组分组可能对应多个组内属性,如先择手机分类(分类和商品关系是一对多,对属性来书是一对一的分组,分组对多多),也就是同一个手机种类下有相同的属性分组,不同手机有相同的分组,分组和产品id的约束可能,组内可能有不同的小的属性,他们的属性对应着销售属性。

添加分组属性:

 查出商品的三级分类,在三级分类的基础上,把商品回显,添加属性组时添加该该属性属于的catelogId(该分类id是需要实时查询回显),属性组名,和属性组生成iD,对于组内的属性,是实例共有或特有的。

这个时候属性组内该种类的商品就有了一个分组属性,比如手机对应的categorId=255就多了一条属性记录,在添加分组属性时也会出现商品分类的三级菜单,修改同样如此。

修改回显问题,当修改该属性分组时,无法回显属性组归属的三级分类路径和,会得到当前属性组的分类id,它的分类名以及父分类名字需要到后端实时查询,只能得到产品种类id255,需要去categoryService查出所对应的商品信息对象CategoryEntity,判断该对象是否有父类对象,递归形式获取,把id加入集合,有该集合关系后就在前端展示完成后它会请求到所有分类的信息,把把要回显的的属性的分类以及它的父父分类返回一个数组的形式,在已经实时查出的父分类的数据集中指定要显示的当前属性组的分类id的名称和父亲分类的名称,他们的所有的路径名称。分类id的商品以及它的父分类名称/商品子分类名称/商品孙子名称进行回显。(有效的回显需要先查询所有的分类)该回显组件是一个数组,当属性组添加时会收集父类商品id/子类商品id/孙子商品id,我们要的是孙子商品id,但修改回显时需要这些id所对应的路径,即产品种类名称,当回显后又回到了默认的熟悉的商品三级分类,且还是手机选项上,我们可以重新选择一个分类,将屏幕属性改为三级分类改为移动数据,那么属性分组中对应的商品种类CategoryId由255变为231,然后属性页面,重新展示数据集,默认查所有属性组记录。如图

问题后端返回了属性组的所属分类id,以及它的父亲分类的id,只拿到这些id就能回显出数据的原因是前端的数据绑定功能,比如el树需要的是整个集合,以分类的id进行绑定以及它的lebel的绑定到分类的名称上,当前控件的选定状态可以用返回的分类id来指定,就完成分类id的指定三级菜单的选定状态果。

属性组的直接关联是分类,每个分类下有多个属性组的项,属性组的项只能指定到所属的三级分类的id,回显的时候需要二次查询,而且需要所有的分类的实时集合才能绑定到树上,在指定选中状态所需要绑定的id就是修改要回显的父子id树,就能让el树有选中状态。

分类id可以查到属性组,属性组没有依赖到属性,而是属性表中去关联分类id,分类是中间层,分类去查属性组,分类去查属性,属性组如何查出所有属性,就又用了中间表pms_attr_attrgroup_relation,有了档期男分类id就有了属性组,有了属性组去属性组属性中间表查属性组下的属性id,属性id得到后最终去属性表中查出属性的完整记录。

p76:

完成规格属性的添加(要确定规格属性的分类,有了分类id就能回显属性组添加该分类下的所属属性组的规格属性,规格属性添加必须依赖于属性组,属性保存后还有保存属性和属性组关联关系pms_attr_attrgroup_relation也就是中间表属性id和属性组的id对应还有属性在属性组内的排序,先插入属性,mybatisplus会为对象自增的id插入前对该对象的id属性做数据库自增键的加1操作,插入后原来的属性对象就有了属性id然后再更行中间表写入属性id和属性组id)

        在属性规格页面中添加属性, 属性名入网型号,属性参数(可选类型规格参数,销售参数),在所属分类中又出现商品分类的三级菜单,选择手机,在所属分组中出现该分类对应的属性分组表中的字段,这里是数组形式给出它的分类下的属性组的所有选项,这里选择基本属性,添加,添加成功,发现属性记录表中有数据,属性和属性分组表的中间表却没有值,也就是AttrEntity,没有关联上AttrAttrgroupRelationEntity对象,中间表也就没有数据,即便前端传的有,解决是在AttrEntity加字段attrGtoup并加上@TableField(exist=false)表示实体表不存在的字段。这里推荐使用VO对象做封装避免对数据库表对应的实体做任何的修改。VO视图对象用于封装前端的数据,TO用于微服务中传输的对象,BO是多个mapper查询结果汇总返回前端。修改后,重写属性添加逻辑如下:

  @Transactional
    @Override
    public void saveVo(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
        if (attrEntity != null) {
          //spring提供的将对象相同字段的值进行赋值
            BeanUtils.copyProperties(attr, attrEntity);
        }
        baseMapper.insert(attrEntity);
        // 保存分组关系
        if (attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId() != null) {
            AttrAttrgroupRelationEntity groupEntity = new AttrAttrgroupRelationEntity();
            groupEntity.setAttrGroupId(attr.getAttrGroupId());
            groupEntity.setAttrId(attrEntity.getAttrId());
            attrAttrgroupRelationDao.insert(groupEntity);
        }
    }

重新添加规格属性页面中添加属性上市年份,选择分类为手机,属性组为屏幕。最终在属性和属性分组的中间表attr_attrGroup_relation中生成一条数据,注意点这个是中间表不要和属性分组表attr_group弄混,想查看多个表中的关系可以到数据库中查看。

attr表

当要展示商品的属性时先展示属性分组,再展示分组下的多个属性比如分组1为屏幕,该组下有上市年份属性,对应值1999. 继续完成规格参数页面的删改分页查功能能,这些比较简单,拿分页查询为例,当查询带有分类条件,查特点商品下的属性就动态拼接上,没有时不查询,然后获取查询的key,在当前属性表中多字段模糊查询,注意查询条件后面的and拼接当作为一个()时表明后面还有条件,此时QueryWapper就用and的接口编程里面传当前wapper对象继续条件判断相当于where  t1 and (t2 and t3) and t4,加括号就是做整体条件判断。分页查询实现如下:

 @Override
    public AttrResVo getAttrInfo(Long attrId) {
        AttrEntity attrEntity = baseMapper.selectById(attrId);
        AttrResVo attrResVo = new AttrResVo();
        BeanUtils.copyProperties(attrEntity, attrResVo);
        AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>()
                .eq("attr_id", attrEntity.getAttrId()));
        // 将attrGroupId 和 GroupName 返回
        if (relationEntity != null) {
            attrResVo.setAttrGroupId(relationEntity.getAttrGroupId());
            AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
            if (attrGroupEntity != null) {
                attrResVo.setGroupName(attrGroupEntity.getAttrGroupName());
            }
        }
        // 将catelogPath和 catlogName 返回
        Long[] categoryPath = categoryService.findCategoryPath(attrEntity.getCatelogId());
        attrResVo.setCatelogPath(categoryPath);
        CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
        if (categoryEntity != null) {
            attrResVo.setCatelogName(categoryEntity.getName());
        }
        return attrResVo;
    }

 查询属性时携带了其他表的字段如商品分类表的分类名称,分组表的分组名,再封装一个VO继承属性Attr_Entity并添加上catelogName;groupName;字段,在查询出基础表的结果后,再去根据属性所属的分类字段查出分类名,和当前属性id在属性和属性分组表的中间表中属性id所对应的属性组id,有了 属性组id就能查出给属性属于哪个分组名称。

p77上面完成了规格属性的添加(主要是指定它的分类id,根据根据回显的所有分类树选择分类id,然后关联查出分类id的所有属性分组,选择属性分组,保存规格属性记录,其中存在着中间表用于记录所选分组下的id和它的所有的规格属性对应,即保存数据时分多部)规格属性添加完成后完成规格属性的查询功能,也就是attr表,可以根据key进行id条件查或者根据规格属性的name模糊查但前端展示的内容远不仅仅是属性内容,需要根据查出的属性下的所在分类id去查出分类的名字、以及该属性在属性和属性组中间表中对应的属性组id,在根据id去属性组中查询出属性组的name并将所有查出的数据分装vo当中给前端展示。

n属性修改功能:

       两个重要的点,所属分类是一个三级选择器,当点击修改时,要求数据回显,定义一个查询属性的接口,里面扩展封装返回的VO对象包含分类的数组型路径以及分类下的分组属性的id对应的name进行回显,当点击修改时触发查询回显数据,查出属性的基础字段,在根据管理属性查询其他字段封装成VO返回前端,问题点,完整的路径是数组id的形式如何显示三级分类的?其实这是一个局部请求左侧有一个三级菜单,前端有js数组对象,要什么结果就从js结果集中获取,回显时只是选择器选择了这条属性对应的唯一内容,主要强调的默认展示。没有携带过多内容信息,在分类选择器上完全可以用js的数据,然后属性分组也是js保存好的,动态回显,回显后完成后台修改,修改获取前端VO拷贝基础表对象,完成修改,其他属性交给中关联表去查,比如拿属性id去查中间表attr_attrGroup_relation,获取属性组id到属性组表更新前端对象的相关属性,问题当前没有属性分组id的话,也就是拿属性id去属性属性分组关联表中查出0条,当修改时没有数据可以修改,这里当查到结果大于0表示修改,查出结果等于0表示添加,商品分类不需要处理,没有关联关系,只是所属关系,修改了就没有所属关系,其他无字段处理。

p78 完成规格参数的属性添加后(因为在规格属性录入时需要选择所属分类所以规格参数页面也有分类树)接着完成规格属性的修改,根据属性的添加想一想修改时需要哪些内容回显,点击属性时只会传id,要更具id去查实时数据,查出属性所属的分组,查属性表分类id的分类名称然后就是属性的内容,和插入一样封装成vo回显到修改表单中去,vo中有属性所属分类的id所在的树路径,在回显分类时以树的形式选择,只需要给出三级菜单的绑定数据的key值即分类的id就会自动选择三级树的状态,这些绑定的分类树并不是在修改的时候发起查询分类的树的数据,而是已经存在的数据绑定,现在只需要给组件指定选中状态的那一组绑定的key值就会以三级菜单的方式回显,这仅仅是完成查询该条属性的回显信息,接下是正真完成修改,将前端所展示的所有参数和值重新封装成vo然后分步去修改,包括中间表,内容大概如下,修改属性和属性组的关联表的属性组id分类id的关联不用处理。

p79:完成销售属性管理功能(平台属性的最后一个功能如图)

  前面完成的是规格属性,这里完成销售属性,属性都存放在attr表中用字段attr_type来区分,0销售属性,1基本属性 2即是销售属性非(基本属性=规格属性)2可以放弃使用。

完成销售属性页面数据查询,销售属性和规格属性是同一张表,用type字段区分,请求路径上用动态的路径变量去区分,这两个查询共用一个方法,注意点销售属性是不存在分组的,也就是在查询中,避免走属性规格一样的查中间表条件。销售属性也是基本属性区分与规格属性,在前端请求时会传入type参数用于说明是规格属性请求还是基本销售属性请求,在查询的时候都要带上type,这个type是枚举类型分0和1,在前端传type时做判断,基本销售属性显示时没有所属分组。有分类会拿属性表中的分类id再去查分类表获取分类名字。除了关键字查询基本属性和规格属性相同,直接点击三级分类的三级菜单就可以查出属性表中所有分类id的属性,当前查基本属性就加上type,Math.ceil向上取整,

基本属性添加:对于销售属性的添加和规格属性查不同,会选择属性的分类,属性的type为默认销售属性,也不会有属性组的选择(认知当点击添加时主页面无刷新,仅仅弹出一个隐藏的组件并设置了显示的层级关系,所以添加的窗口还在同一个vue实例则页面请求到的数据在添加框中依然可用),分类菜单数据是主页面已经查询到的,在弹出的组件直接绑定三级选择器,现在引用了type查询,前面的规格属性查询都要加上查询type判断。

  @Override
    public PageUtils queryByCid(Map<String, Object> params, Long catelogId, String attrType) {
        QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>();
        String key = (String) params.get("key");
        if (!StringUtils.isEmpty(key)) {
            wrapper.and((obj) -> {
                obj.eq("attr_id", key).or().like("attr_name", key);
            });
        }
        PageUtils pageUtils = null;
        IPage<AttrEntity> page = null;
        if (catelogId != 0) {
            wrapper.eq("catelog_id", catelogId);
        }
        wrapper.eq("attr_type", "base".equalsIgnoreCase(attrType) ? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() : ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
        page = this.page(new Query<AttrEntity>().getPage(params),
                wrapper);
        pageUtils = new PageUtils(page);
        List<AttrEntity> records = page.getRecords();
        // 获取当前页所有结果集需要将groupName 和 catelogName 一同返回
        List<AttrResVo> attrResVoList = records.stream().map((attrEntity) -> {
            AttrResVo attrResVo = new AttrResVo();
            BeanUtils.copyProperties(attrEntity, attrResVo);
            //可能用到可能用不到,所以放在这里
            AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao
                    .selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>()
                            .eq("attr_id", attrEntity.getAttrId()));
            if ("base".equalsIgnoreCase(attrType)) {
              //带属性组名称的规格属性查询,销售属性不带就不查询
                if (relationEntity != null && relationEntity.getAttrGroupId() != null) {
                    AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
                    attrResVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }
            }
            CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
            if (categoryEntity != null) {
                attrResVo.setCatelogName(categoryEntity.getName());
            }
            return attrResVo;
        }).collect(Collectors.toList());
        pageUtils.setList(attrResVoList);
        return pageUtils;
    }

wrapper.and相当于where 对当前条件加()让后在拼接一个and ,(内部条件)and形式,查询、更新有不同的wrapper,上面的查询唯一标识是类路径上的参数判别,如果是base表明是规格参数(基本属性)属性,三元判断否则就是销售属性完成条件查询,在保存属性时销售属性不用属性分组,不用关联中间表,获取spu规格,spu是产品的描述对象,并不是实例对象,实例对象对应sku,规格参数外,也有属性参数。(目前属性和属性组有中间表,商品分类和品牌有中间表)

在修改时只修改base类型的分组属性的中间表,销售属性不修改,查询的时候也是base有关联关系表要查带上属性id查中间表的属性分组id,然后在属性分组表中年查出name字段,分类可以直接查询,无中间表。添加属性时可以添加多个值,选择类型为销售属性时不会向中间表中建立双表关联,添加一个,在attr_attrgroup_relation表中无attrId,无attr_attrgruopId,不用维护关系,添加删除都要判断,修改是可以修改所属的属性类型(这是难点和复杂点)。

80:属性组下的分组属性项,重新关联该属性下的规格属性(前面已成规格属性添加修改)

大概思路:点击修改拿到属性组id,查属性属性组,拿到该组下的所有规格属性,回显到关联组件,并在组件内属性项显示后面加解除绑定,则在中间表删除一条对应的绑定记录,除此之外还有添加关联,批量删除关联,这是一个选择框,选择项是规格属性并且没有在中间表中绑定的规格属性项。

。p81:

查询当前属性组可以用来绑定的其他属性,并且要求属性同是一个分类级别下没有被其他分组绑定的属性,解决思想,根据分类id在当前属性组表查出分类的所有属性主属性,包括当前的属性组,会返回一个attrgroupEntity集合,通过stream搜集他们的id,在attr_attrgroup_alation中查出所有组中的属性id,最终获取当该分类下所有已经绑定了的属性,查出未绑定的属性,就直接查属性表条件在同一个分类并且不在上面id中的属性,完成查询返回前端的选择器上可以选择没有绑定的属性,选择后会在非未绑定选择器的下方属性已经绑定的属性列表。点击关联时会显示已经关联的属性,并且有移除的功能,它是简单的中间表维护的关系,直接删除中间表,移除后新增关联关系会看到刚才移除的属性。

p83 商品的发布流程(难点可以是核心点)

流程 基本属性-》规格属性-》销售属性-》sku属性-》保存成功

发布商品

基本属性-商品的描述、选择分类、选择品牌、商品重量、设置积分、成长值、商品介绍图片、商品图集。

商品发布是前端统一发送一个vo,后端做出拆分,分步骤完成修改逻辑,原来的属性只是值,现在显示值在前端给它一个关联的值属性到sku的属性上,这个属性就需要引用原有字段值关联表来联系。

销售属性:颜色(可以选择也可以创建自定义颜色)内存4g、8g、16g、版本8+256g等

sku:当选择了黑色和银河色并且有8+255、8+128就会形成组合式的sku,比如黑色+8+255/128,

银河色+8+255/128生成四条sku信息,在该面板会额外添加副标题、价格展开设置更多,会有折扣信息满多少件打多少折,设置满多少元减多少元。选择图集可以选择图片默认展示信息,保存商品就会在商品表中添加四条记录。前面录入spu信息,后面录入了sku信息最终是一个大json串,发送到后端保存。商品的图集是针对sku的,是产品实例具体的图片信息。

s

基本信息需要跨服务查询,

获取分类关联的所有品牌

当选择商品分类后就要查询商品的分类id所关联的品牌,在下面的选择器上可以共选择。

涉及点:

        商品分类表category、品牌表、商品分类和商品品牌表,现在我们要拿到商品分类id去查中间表,获取中间实体,在通过stream进行映射返回出关联的所有品牌id集合,再用品牌接口批量查数据库,代码实现如下:

 /**
     * 根据catelogId获取品牌信息
     *
     * @param catId
     * @return
     */
    @Override
    public List<BrandEntity> getBrandByCatlogId(Long catId) {
        List<CategoryBrandRelationEntity> brandRelationEntityList = baseMapper.selectList(new QueryWrapper<CategoryBrandRelationEntity>()
                .eq("catelog_id", catId));
        List<Long> brandIds = brandRelationEntityList.stream().map(item -> {
            return item.getBrandId();
        }).collect(Collectors.toList());
        List<BrandEntity> brandEntityList = brandDao.selectBatchIds(brandIds);
        return brandEntityList;
    }

它仅仅是录入基本信息时实时数据回显到选择器上,选择你添加商品的品牌,下一步到规格参数

规格参数的录入:进入下一步时,就根据分类去查询分类下的属性所有分组,分组下关联的所有属性。每个属性组对应一个视图,如图在属性组中查到了主体、基本信息、主芯片、分别对应页面去填写该组内属性字段的值,这些属性根据商品是否需要,可以选择性的录入。

在录取基础属性的时候,同样是查询的方式去先查询基础属性的值,然后放入选择器中共用户选择,下一步进入销售属性,会产生多个sku实例,主要是前面选择的做了笛卡尔积,4个颜色,两个版本,就形成8个sku,在这里可以修改标题信息,方便后面做搜索,修改副标题等,最右侧可以查看之前选择的信息,比如会员优惠具体信息,银牌会员金牌会员有优惠,普通会员无优惠,还可以上传每一个sku的图片集,包括正面图片,背面图片等信息。

前端会生成大量的json数据我们解析这些数据,定义一个对象来接收这些信息,把json转为java的VO对象,当出入到后端后,对相应的数据进行处理,这里注意VO的属性的数据类型对于小数的一定使用BigDecimal。防止属性拷贝的时候数据类型不一致导致数据精度丢失。

保存产品的录入信息,调用SpuInfoController下的save方法,使用post请求让spring

mvc自动封装@RequestBody SpuSaveVo对象,保存分两个步骤,一个是保存spu基本信息,一个保存spu的图片集,数以当前有两个表spu_images,spu_info表,使用spu_info_desc关联表的方式,在查询时将数据组合一起,该表只有spu_id,decript两个字段,我们先保存spu的基本信息,让后保存spu的描述信息,通过描述信息定位spu的图片集信息,并保存他们。1保存spu的基本信息,在spu_info2保存spu的图片描述在spu_info_desc,3保存spu的图片集在表spu_image4保存spu的规格参数(去操作另外product_attr_value表它的spu_id,属性id,属性名,属性值信息)5:保存spu的积分信息sms_spu_bounds;6保存当前sku对应的所有sku信息,有很多;6.1sku的基本信息:sku_info;6.2;sku的图片地址信息,默认图片信息sku_images表中6.3sku的销售属性信息sku_sale_attr_value;6.4sku的优惠信息,满减等信息,跨库gulimall_sms-》sms_sku_ladder(),sms_sku_full_reduction满多少减多少表,sms_member_price会员表,整个保存完就成功

这个地方对整体的流程都清除了可以不看视屏,直接去看代码,代码看懂跳过这部分,难点就是设计表之间的关系,sku,spu到底解决什么问题。要理解流程就要弄清他们实体直接的关系,商品模块,现在要弄清15个实体都是如何依赖的,再谈具体的小细节。

,在这之上有如保存sku信息,就要对8个具体产品实例的sku信息进行保存,先保存sku的基础表信息,从前端VO集合收集,然后批量插入数据,然后再处理关联表所属的图片资源,知道自己的sku的id集合去向pms_spu_info关联表中加入图片数据,批量保存后完成资源定位,

接下来处理sku的销售属性,也就是前端会提交过来多个属性对象,这些属性对象就有销售属性,销售属性不仅要自身保存外,还要保存在sku_sale_attr_value(表明属性对应的sku,或sku对应具体的属性)

属性要和sku关联采用中间表pms_sku_sale_attr_value的形式,当有属性的时候插入,也要根据属性id,属性名,在中间表插入,并同时插入sku,这样就确定关系。sku和attr是同时存在才具有完整性,由于sku要保存销售属性,产品实例会有多个销售属性,这时通过中间表pms_sku_sale_attr_value,只需要加skuid,和attr_id,就能得到一个skuid,对多个销售属性,也就形成属性组的概念,而属性组又有自己所属,

spu不仅仅是存储单元,而且是优惠活动的处理单元。

商品发布处理商品(巨大的抽象体,不以商品对象为存储)之积分数据保存,需要跨积分服务,这里使用@FeignClient("服务名"),接口方法上加请求注解

要远程保存成长积分和购物积分,就要从VO中解析获取Bound,是前端json解析的对象信息载体,电商中商品是宏观的抽象,是一个大熔炉,spu是产品单元是具体类型的实体,sku是类型实体的一个唯一表现,

关于优惠卷的实体有核心字段像一个把锁来控制优惠系统是否生效

SpuBoundsEntity 下的Integer work,控制产品单元下优惠生效情况[1111(四个状态位,从右到左);0 - 无优惠,成长积分是否赠送;1 - 无优惠,购物积分是否赠送;2 - 有优惠,成长积分是否赠送;3 - 有优惠,购物积分是否赠送【状态位0:不赠送,1:赠送】]

对应成长积分,和购买后获得积分,这些是随商品创建而录入,商品的积分是产品的类型唯一单元,它的积分是在改类型的实体上生效,所以积分必须保存spu产品标准类型id的信息通过TO传输优惠服务系统。

保存sku的优惠信息(区别spu和sku等具有的优惠特性,sku有成长积分,购买积分,sku有优惠打折信息,sku是具体的实体,spu是类型描述体)sku信息保存,就要在优惠服务,保存它的优惠信息,将当前的sku分装成TO传输到优惠系统完成SkuFullReductionEntity 的MemberPriceService和SkuLadderDao服务的优惠信息保存,(两种情况满减打折,和打折),具体步骤如下

1保存满减打折,会员价,或阶梯型打折,获取折扣信息对像服务,创建要保存的实体,将接收的数据属性注入,对需要计算打几折的字段进行计算,我们只保存sku的打折信息的保存,拿到sku的优惠信息后SkuFullReductionEntity,调用它的服务,进行保存,统一打折处理交给SkuFullReductionEntity对象完成,sku的打折信息由SkuFullReductionServiceImpll对象保存,分为 ;2 sms_sku_full_reduction;3 保存会员价格 sms_member_price,具体的实现:

1,sms_sku_ladder为阶梯价格表, 1保存满减价格 sms_sku_ladder

2,sms_sku_full_reduction商品满减信息表, 保存满减信息 ,保存sku时必须保存

3,会员也要维护会员所具有的优惠信息, 保存会员价格 sms_member_price表,对应实体MemberPriceEntity(关键字段,sku具体的商品,会员等级,会员对应价格,是否优惠可以叠加)以及用到的MemberPrice工具收集数据,

@Transactional
    @Override
    public void saveSkuReduction(SkuReductionTo skuReductionTo) {
        // 保存满减价格 sms_sku_ladder
        SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
        skuLadderEntity.setSkuId(skuReductionTo.getSkuId());
        skuLadderEntity.setFullCount(skuReductionTo.getFullCount());
        skuLadderEntity.setDiscount(skuReductionTo.getDiscount());
        skuLadderEntity.setAddOther(skuReductionTo.getCountStatus());
        skuLadderDao.insert(skuLadderEntity);
        // 保存满减信息 sms_sku_full_reduction
        SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();
        skuFullReductionEntity.setAddOther(skuReductionTo.getCountStatus());
        skuFullReductionEntity.setFullPrice(skuReductionTo.getFullPrice());
        skuFullReductionEntity.setReducePrice(skuReductionTo.getReducePrice());
        skuFullReductionEntity.setSkuId(skuReductionTo.getSkuId());
        baseMapper.insert(skuFullReductionEntity);
        // 保存会员价格 sms_member_price
        List<MemberPrice> memberPrice = skuReductionTo.getMemberPrice();
        List<MemberPriceEntity> memberPriceEntityList = memberPrice.stream().map(item -> {
            MemberPriceEntity memberPriceEntity = new MemberPriceEntity();
            memberPriceEntity.setAddOther(skuReductionTo.getCountStatus());
            memberPriceEntity.setMemberLevelId(item.getId());
            memberPriceEntity.setMemberLevelName(item.getName());
            memberPriceEntity.setMemberPrice(item.getPrice());
            memberPriceEntity.setSkuId(skuReductionTo.getSkuId());
            return memberPriceEntity;
        }).filter(item -> {
            return (item.getMemberPrice().compareTo(new BigDecimal("0")) == 1);
        }).collect(Collectors.toList());
        memberPriceService.saveBatch(memberPriceEntityList);
    }

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

就完成持久化,另外一种是满减少打折,还是一句话只存sku的信息,不用考虑优化的使用情况,只需要持久sku,spu等所有的信息才算商品的发布成功,里面有优化的细节,包括前端对全量url的提交,当为空时不提交,在远程保存时还有判断具体是否有打折无打折不提交,会员无优惠不提交,无满减不提交保存,商品发布任务已经完成,具体细节看图片

基础属性设置:

规则属性:

销售属性设置:

sku信息设置:标题副标题在选择sku时详情的标题副标题。

sku的在>头有更多设置(前面的规格属性,销售属性是关于spu,前面商品名称是spu的名称),如下图:

商品维护模块:发布商品;SPU管理;商品管理

SPU管理:前面完成了商品的发布,现在完成spu的管理?spu是什么?

spu查询,spu是产品具体的实体类型,是标准化单元,它是商品实例的类型实例,是实例的非具体化表现,查询sku,根据多个条件并的条件查询,只提供多条件查询,比如分类、品牌名、新建状态,关键子key查。where(id=?or spu_id=?)and publier_status=? and beand_id=? and category_id=?,spu管理目前只完成查询,与sku做同步剩余功能待完成,

接下来完成Sku管理(商品查询):在价格查询的时候注意商品价格默认为的时候,做条件判断只查价格大于0。

。支持商品价格范围搜素,更多上传图片、参与秒杀满减设置、折扣设置、会员设置、库存设置、优惠劵等信息,这是对sku产品的具有的活动功能的设置。

完成库存功能:库存系统-》仓库维护、库存工作单、商品库存、采购单维护(采购需求、采购单)

gulimall-ware首先清除几个表,或者实体,

1 4 4商品库存表:wms_ware_sku(sku_id,wareId仓库id,stock库存数,skuName,stockLocked锁定库存)

2 库存工作单:wms_ware_order_task(orderId,order_sn, consignee收获人,consigneeTel收货人电话,deliveryAddress配送地址,orderComment订单备注,paymentWay付款方式(1在线付款,货到付款),taskStatus任务状态,orderBody订单描述,trackingNo物流单号,wareId仓储id,taskComment工作单备注)

3 库存工作单详情:wms_ware_order_task_detail(sku_id,sku_name,skuNum购买个数,taskId工作单id)

4 仓储信息wms_ware_info(name仓库名称,address仓库地址,areacode区域编码)同时要设置网关,对于网关的用法有待提高.

5采购信息表:wms_purchase(assigneeId采购人id,assigneeName采购人名,priority优先级,status状态,wareId仓库id,amount总金额)

6采购详情表:wms_purchase_detail(purchaseId采购单id,skuNum采购数量,skuPrice采购金额,wareId仓库id,status状态[0新建,1已分配,2正在采购,3已完成,4采购失败])

p96:查询商品库存:是结合已经存在的仓库和skuID去查询,库存管理对采购功能并不是直接在改页面下完成,而是分子处理方式,人工,加其他系统预警等来完成库存的添加 ;如当采购人员采购商品完成,在进库时扫描入库,库存是不可操作的,只有商品入库时才会有改修改操作,库存是由采购单来填充,采购单维护分采购需求,采购单,由采购需求决定采购单,采购需求由人工创建或库存报警创建,采购需求可以对多个采购需求生成采购单,先有采购需求再有采购订单,。

库存服务,项目逆向工程就具备仓储服务的采购单维护功能的基本查找,采购单分为了,采购需求、采购单进行了流程细化,采购需求自身数据库表逆向工程有采购需求的创建搜索删除功能,需要在搜索时添加模糊搜索修改,接下来就是采购单的处理,采购单是由采购需求的多个单所合并而成,

在创基采购单后,可以给采购单分配人员,合并多个需求采购单需要查询采购单进行合并选择器的回显,指定合并到哪一个采购单,这些采购单要求是没有被领取,其状态status必须是0或者1,新建或刚分配人,查询很简单QueryWapper后跟ep条件,如果在合并需求采购单时没有选择采购单就会新建采购单,并完成合并,需求采购单会被解析关联到采购单,插入到采购单的采购单详情的id完成关联关系,PurchaseEntity和PurchaseDetailEntity 关联,将采购的PurchaseDetailEntity采购需求单绑定采购单上分配给菜购人员,领取采购单(可以使用mq完成通知功能),领取采购单后要在采购详情表中更新采购项状态,表示当前需求采购单已经合并成采购单并且已经分配并由采购员接收,这时采购详情中的采购项就是正在采购状态(区分采购单的状态)(需求采购单之上指明它所属的采购单并不会在需求采后单页面中有变化,只是它的归属处理字段有了值),采购员领取所属于的采购单,采购单中归属的采购项的状态也会发生改变

当采购人完成采购单后,会继续更改采购需求中的采购项的状态,如果采后人确认都采购完成则采购单完成,否则采购单是未完成状态,并且失败的采购想会添加失败原因,(采购项是否失败是根据前端采购员将采购项状态以VO形式传参封装成实体用来更新状态,包括失败的信息)以及该采购单的采购成功率比,1更新采购详情中的采购项的状态,如果采购成功并同时入库 2如果采购项全部完成则跟新采购单状态完成  3,所有更新完成将成功的采购项入库操作(wms_ware_sku)(sku最小入库单元也是库存单元) 加库存的具体操作对应数据库如下:

首先找到具体sku产品id,以及所属的库存id,如果没有就是添加操作创建新的库存sku单位。

现在模拟采购员领取采购单,采购单采购项改变状态,库存系统只有一个sku为1号铲平,采购单里面有1,4产品,当前仓库系统没有4号,现在采购员回应采购系统采购情况,向接口发送数据

此时采购单4已经完成状态,参构单项也是已完成,1号库存累加1号新填数量 4号sku库存没有,就创建4号库存单元,并且指定库存数量,在库存数量展示时还有库存的name需要展示,所以还要去查skuId对应的产品名,当前是仓库系统,需要Figen远程服务调用获取,发送的方式有两种1直接给服务发送请求2给网关发送请求,在获取name时发生远程调用失败我们try掉不发生事务回滚。

仓库管理,采购功能完成。

回看商品系统,商品维护,spu管理商品的信息,它有一个规格,点击它出现规格维护如图:

用来修改库存单元商品的规格属性(spu属性组)sku的基础属性,我们要求它能回显出来,所以要添加查询接口,它有product_attr_value为商品(sku)的基本属性保存了spu_id,attr_id,attr_name,arr_value。包括快速展示属性quick_show等信息,查询规格属性并回显

查询商品属性规格是根据spu去查找它是定义sku的类型实例的,是以spu向外暴漏,是sku的定位器。

这是spu管理,是产品的sku的直接所数的标准产品单元,比如小米七,小米六,它不是具体的sku而是产品的一个标准类型,它有名称、描述、商品分类、品牌、重量、上架状态、等产品类型上的特点,完成回显后,可以修改,如下图,这里要批量更新spu的属性

采购单和采购需求分析:

         采购需求需要合并到采购单上,采购单创建时为新建状态可以合并,采购单有分配人员,修改、删除功能,分配采购人员给采购单,修改采购单的人员信息。创建采购需求,也是存在多个状态,直接创建采购需求,采购需求添加时候选择仓库地址,采购数量采购的sku名称等信息,合并采购需求,当不选择已经存在的采购单的时候,会默认创建采购单,并根据采购单的id改变采购需求的状态。点击合并采购需求,会查找已经存在的采购单,选择要合并到的采购单id,点击合并,则当前所又的采购需求的采购人,采购需求状态都改变。

购物车去支付跳转到订单确认详情页

购物车列表,选中的购物车处理购物车项转为订单项并跳转到订单确认详情页,该页面需要向拿到当前登陆的用户 id向会员服务请求获取地址信息,向购物车获取选中的购物车项的订单项,向库存系统查询订单的skuId是否存在库存,从库存系统中获取订单地址的运会费,要展示的页面数据如下:运费、运送至、应付总价=订单商品价格*商品数量+地址选项产生的费用。

执行逻辑是当订单确认详情页加载的时候,主动获取页面已经选中的地址标签,向库存系统发起ajax请求将获得到的地址绑定到订单详情页的接收人地址上,以及运费,并计算出总的价格,当促发地址选择的时候也能执行该逻辑,调用库存服务getFare返回一个FareVo对象封装一个地址MemberAddressVo和BigDecimal 费用,其中地址需要在库存服务中发起远程请求会员服务memberFeignService.info(addrId);返回给订单服务的确认页中的ajax的请求中并动态渲染数据。

订单提交的幂等性问题:一次提交与多次提交的最终只做一件事情,防止重复提交,1按钮重复点击2页面提交成功回退再提交 3微服务相互调用,由于网络问题,导致请求失败 ,feign触发重试机制 。

订单的确认页面所需要的数据需要请求多个微服务如会员服务的会员地址、库存服务是否有库存,商品服务的价格,在订单服务中收集成一个Vo显示在订单确认页面,页面获取数据并渲染用户的所有地址,并通过js获取默认选定的地址通过获取地址id加上ajax动态地请求地址详细信息以及它对应的费用,使用一个FreeVo统一封装,计算出动态选定的地址总费用,当选择不同的地址时触发点击事件,会发送ajax请求重新获取地址以及费用在订单的确认页计算并显示出来。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值