3.5.java客户端
余庆先生提供了一个Java客户端,但是作为一个C程序员,写的java代码可想而知。而且已经很久不维护了。
这里推荐一个开源的FastDFS客户端,支持最新的SpringBoot2.0。
配置使用极为简单,支持连接池,支持自动生成缩略图,狂拽酷炫吊炸天啊,有木有。
依赖报错:
。这个问题是maven中央仓库没有fastdfs-client-java的jar包导致的,虽然可以在maven的库里查到依赖,但是无法下载到本地(脑阔疼)。 解决办法是:从gitHub上下载fastdfs的工具包,自己打个jar包出来。 1.下载fastdfs-client-java开发工具包
https://github.com/happyfish100/fastdfs-client-java
2.把fastdfs-client-java开发工具包打包到本地的Maven仓库,在Windows窗口输入如下命令(注意:本机必须安装Maven,并配置Maven环境变量,并且盘符要切换到fastdfs-client-java解压的目录下)
mvn clean install
jar包在仓库安装成功
冲突解决成功
FastDFS文件上传
进行Fdsfs测试的时候一定要把Linx的防火墙关闭
测试成功
地址访问成功:
文件下载:
缩略图显示成功:
修改文件上传逻辑:
String extend = StringUtils.substringAfterLast(file.getOriginalFilename(), ".");
StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), extend, null);
return "http://image.leyou.com/"+storePath.getFullPath();
完成测试:
--------------------------------------------------------------------------------------------------------
1.商品规格数据结构
乐优商城是一个全品类的电商网站,因此商品的种类繁多,每一件商品,其属性又有差别。为了更准确描述商品及细分差别,抽象出两个概念:SPU和SKU,了解一下:
1.1.SPU和SKU
SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集
SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品
以图为例来看:
-
本页的 华为Mate10 就是一个商品集(SPU)
-
因为颜色、内存等不同,而细分出不同的Mate10,如亮黑色128G版。(SKU)
可以看出:
-
SPU是一个抽象的商品集概念,为了方便后台的管理。
-
SKU才是具体要销售的商品,每一个SKU的价格、库存可能会不一样,用户购买的是SKU而不是SPU
1.2.数据库设计分析
1.2.1.思考并发现问题
弄清楚了SPU和SKU的概念区分,接下来我们一起思考一下该如何设计数据库表。
首先来看SPU,大家一起思考下SPU应该有哪些字段来描述?
id:主键
title:标题
description:描述
specification:规格
packaging_list:包装
after_service:售后服务
comment:评价
category_id:商品分类
brand_id:品牌
再看下SKU,大家觉得应该有什么字段?
id:主键
spu_id:关联的spu
price:价格
images:图片
stock:库存
颜色?
内存?
硬盘?
碰到难题了,不同的商品分类,可能属性是不一样的,比如手机有内存,衣服有尺码,我们是全品类的电商网站,这些不同的商品的不同属性,如何设计到一张表中?
其实颜色、内存、硬盘属性都是规格参数中的字段。所以,要解决这个问题,首先要能清楚规格参数。
1.2.2.分析规格参数
仔细查看每一种商品的规格你会发现:
虽然商品规格千变万化,但是同一类商品(如手机)的规格是统一的,有图为证:
华为的规格:
三星的规格:
1.2.3.SKU的特有属性
SPU中会有一些特殊属性,用来区分不同的SKU,我们称为SKU特有属性。如华为META10的颜色、内存属性。
不同种类的商品,一个手机,一个衣服,其SKU属性不相同。
同一种类的商品,比如都是衣服,SKU属性基本是一样的,都是颜色、尺码等。
这样说起来,似乎SKU的特有属性也是与分类相关的?事实上,仔细观察你会发现,SKU的特有属性是商品规格参数的一部分:
1.3.规格参数表
1.3.1.表结构
我们看下规格参数的格式:
1.3.2.规格组
规格参数分组表:tb_spec_group
CREATE TABLE `tb_spec_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',
`name` varchar(50) NOT NULL COMMENT '规格组的名称',
PRIMARY KEY (`id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';
规格组有3个字段:
-
id:主键
-
cid:商品分类id,一个分类下有多个模板
-
name:该规格组的名称。
通用属性
用一个布尔类型字段来标记是否为通用:
-
generic来标记是否为通用属性:
-
true:代表通用属性
-
false:代表sku特有属性
-
搜索过滤
与搜索相关的有两个字段:
-
searching:标记是否用作过滤
-
true:用于过滤搜索
-
false:不用于过滤
-
-
segments:某些数值类型的参数,在搜索时需要按区间划分,这里提前确定好划分区间
-
比如电池容量,0~2000mAh,2000mAh~3000mAh,3000mAh~4000mAh
-
数值类型
某些规格参数可能为数值类型,这样的数据才需要划分区间,我们有两个字段来描述:
-
numberic:是否为数值类型
-
true:数值类型
-
false:不是数值类型
-
-
unit:参数的单位
2.商品规格参数管理
2.1.页面布局
2.1.1.整体布局
打开规格参数页面,看到如下内容:
这里使用了v-layout
来完成页面布局,并且添加了row属性,代表接下来的内容是行布局(左右)。
可以看出页面分成2个部分:
-
<v-flex xs3>
:左侧,内部又分上下两部分:商品分类树及标题-
v-card-title
:标题部分,这里是提示信息,告诉用户要先选择分类,才能看到模板 -
v-tree
:这里用到的是我们之前讲过的树组件,展示商品分类树,
-
-
<v-flex xs9 class="px-1">
:右侧:内部是规格参数展示
2.2.3.后端代码
实体类
在leyou-item-interface
中添加实体类:后端代码其实还是SSM哪一套Controller,Servivce,Mapper
Controller:
@RestController
@RequestMapping("spec")
public class SpecificationController {
@Autowired
private SpecificationService specificationService;
/**
* 根据分类id查询分组
* @param cid
* @return
*/
@GetMapping("groups/{cid}")
public ResponseEntity<List<SpecGroup>> queryGroupsByCid(@PathVariable("cid")Long cid){
List<SpecGroup> groups = this.specificationService.queryGroupsByCid(cid);
if (CollectionUtils.isEmpty(groups)){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(groups);
}
}
Service:
@Service
public class SpecificationService {
@Autowired
private SpecGroupMapper groupMapper;
/**
* 根据分类id查询分组
* @param cid
* @return
*/
public List<SpecGroup> queryGroupsByCid(Long cid) {
SpecGroup specGroup = new SpecGroup();
specGroup.setCid(cid);
return this.groupMapper.select(specGroup);
}
}
Mapper:
public interface SpecGroupMapper extends Mapper<SpecGroup> {
}
public interface SpecParamMapper extends Mapper<SpecParam> {
}
我们访问:http://api.leyou.com/api/item/spec/groups/76
完成测试:
关于这个规格参数的增删改查和之前的基本类似
Controller:
@PostMapping(value = "param")
public ResponseEntity<Void> EditBrand(@RequestBody SpecParam specParam){
this.specificationService.addSpecParam(specParam);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PutMapping(value = "param")
public ResponseEntity<Void> EditSpecParam(@RequestBody SpecParam specParam){
this.specificationService.editSpecSpecParam(specParam);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@DeleteMapping(value = "param/{id}")
public ResponseEntity<Void> DeleteSpecParam(@PathVariable("id") Long id){
this.specificationService.deleteSpecParam(id);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
Service:
public void addSpecParam(SpecParam specParam) {
this.specParamMapper.insertSelective(specParam);
}
public void editSpecSpecParam(SpecParam specParam) {
this.specParamMapper.updateByPrimaryKeySelective(specParam);
}
public void deleteSpecParam(Long id) {
SpecParam record=new SpecParam();
record.setId(id);
this.specParamMapper.deleteByPrimaryKey(record);
}
3.SPU和SKU数据结构
规格确定以后,就可以添加商品了,先看下数据库表
3.1.SPU表
这张表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。
需要注意的是这两个字段:generic_spec和special_spec。
前面讲过规格参数与商品分类绑定,一个分类下的所有SPU具有类似的规格参数。SPU下的SKU可能会有不同的规格参数信息,因此我们计划是这样:
-
SPUDetail中保存通用的规格参数信息。
-
SKU中保存特有规格参数。
来看下我们的表如何存储这些信息。
3.1.1.generic_spec字段
首先是generic_spec
,其中保存通用规格参数信息的值,这里为了方便查询,使用了json格式:
3.2.SKU表
问题:为什么要将库存独立一张表?
因为库存字段写频率较高,而SKU的其它字段以读为主,因此我们将两张表分离,读写不会干扰。
特别需要注意的是sku表中的indexes
字段和own_spec
字段。sku中应该保存特有规格参数的值,就在这两个字段中。
4.商品查询
4.1.效果预览
接下来,我们实现商品管理的页面,先看下我们要实现的效果:
4.3.后台提供接口
页面已经准备好,接下来在后台提供分页查询SPU的功能。
Controller:
先分析:
-
请求方式:GET
-
请求路径:/spu/page
-
请求参数:
-
page:当前页
-
rows:每页大小
-
key:过滤条件
-
saleable:上架或下架
-
-
返回结果:商品SPU的分页信息。
-
要注意,页面展示的是商品分类和品牌名称,而数据库中保存的是id,怎么办?
我们可以新建一个类,继承SPU,并且拓展cname和bname属性,写到
leyou-item-interface
-
@Controller
public class SpuController {
@Autowired
private GoodService goodService;
@RequestMapping("spu/page")
public ResponseEntity<PageResult<SpuBo>> queryGoods(
@RequestParam(value = "key",required = false) String key,
@RequestParam(value = "saleable",required = false) String saleable,
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows
){
PageResult<SpuBo> pageResult= this.goodService.queryGoodsList(key,saleable,page,rows);
if (pageResult==null || CollectionUtils.isEmpty(pageResult.getItems())){
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(pageResult);
}
}
Service:
@Service
public class GoodService {
@Autowired
private BrandMapper brandMapper;
@Autowired
private CategoryService categoryService;
@Autowired
private SpuMapper spuMapper;
public PageResult<SpuBo> queryGoodsList(String key, String saleable, Integer page, Integer rows) {
//创建查询条件
Example example=new Example(Spu.class);
Example.Criteria criteria = example.createCriteria();
//检验搜索框是否为空
if (StringUtils.isNotBlank(key)){
criteria.andLike("title","%"+key+"%");
}
//检查上下架
if (saleable!=null){
criteria.andEqualTo("saleable",saleable);
}
//执行分页
PageHelper.startPage(page,rows);
//查询
List<Spu> spus = this.spuMapper.selectByExample(example);
//创建分页对象
PageInfo<Spu> pageInfo = new PageInfo<>(spus);
List<SpuBo> spuBoList=new ArrayList<>();
//将Spu转换为SpuBo,并设置Bname和Cname
spus.forEach(spu -> {
SpuBo spuBo = new SpuBo();
//赋值属性
BeanUtils.copyProperties(spu,spuBo);
spuBo.setBname(this.brandMapper.selectByPrimaryKey(spu.getBrandId()).getName());
List<String> names= this.categoryService.queryCategoryByNames(spu.getCid1(),spu.getCid2(),spu.getCid3());
spuBo.setCname(StringUtils.join(names,"/"));
spuBoList.add(spuBo);
});
return new PageResult<>(pageInfo.getTotal(),spuBoList);
}
}
Mapper:
public interface SpuMapper extends Mapper<Spu> {
}
完成测试: