[TOC]
商品单位就是对商品的所涉及到的单位数据进行维护。
系统管理 -> 菜单管理 -> 新增 -> 主类目
基础数据
商品单位
package com.spzx.product.domain;
@Schema(description = "商品单位")
@Getter
@Setter
@TableName("product_unit")
public class ProductUnit extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "商品单位名称")
private String name;
}
package com.spzx.product.controller;
@Tag(name = "商品单位")
@RestController
@RequestMapping("/productUnit")
public class ProductUnitController extends BaseController {
@Autowired
private IProductUnitService productUnitService;
@Operation(summary = "获取分页列表")
@GetMapping("/list")
public TableDataInfo findPage(@Parameter(description = "商品单位名称") String name) {
startPage();
List<ProductUnit> list = productUnitService.selectProductUnitList(name);
return getDataTable(list);
}
}
List<ProductUnit> selectProductUnitList(String name);
@Override
public List<ProductUnit> selectProductUnitList(String name) {
LambdaQueryWrapper<ProductUnit> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.hasText(name), ProductUnit::getName, name);
return baseMapper.selectList(queryWrapper);
}
@Operation(summary = "获取商品单位详细信息")
@GetMapping("/{id}")
public AjaxResult getInfo(
@Parameter(description = "商品单位id")
@PathVariable("id") Long id) {
return success(productUnitService.getById(id));
}
@Operation(summary = "新增商品单位")
@PostMapping
public AjaxResult add(
@Parameter(description = "商品单位")
@RequestBody ProductUnit productUnit) {
productUnit.setCreateBy(SecurityUtils.getUsername());
return toAjax(productUnitService.save(productUnit));
}
@Operation(summary = "修改商品单位")
@PutMapping
public AjaxResult edit(
@Parameter(description = "商品单位")
@RequestBody ProductUnit productUnit) {
productUnit.setUpdateBy(SecurityUtils.getUsername());
return toAjax(productUnitService.updateById(productUnit));
}
@Operation(summary = "删除商品单位")
@DeleteMapping("/{ids}")
public AjaxResult remove(
@Parameter(description = "商品单位id列表")
@PathVariable List<Long> ids) {
return toAjax(productUnitService.removeBatchByIds(ids));
}
商品分类就是对商品的分类数据进行维护。分类表中的数据通过parent_id自关联。
系统管理 -> 菜单管理 -> 商品管理 -> 新增 -> 菜单
商品分类
下级分类采用懒加载
@TableField(exist = false):表示数据库中不存在这个列,只在实体类中定义
package com.spzx.product.domain;
@Schema(description = "商品分类")
@Getter
@Setter
@TableName("category")
public class Category extends BaseEntity
{
private static final long serialVersionUID = 1L;
@Schema(description = "分类名称")
private String name;
@Schema(description = "图标地址")
private String imageUrl;
@Schema(description = "上级分类id")
private Long parentId;
@Schema(description = "是否显示[0-不显示,1显示]")
private Integer status;
@Schema(description = "排序")
private Long orderNum;
@Schema(description = "是否有子节点")
@TableField(exist = false)
private Boolean hasChildren;
/*@Schema(description = "子节点列表")
@TableField(exist = false)
private List<Category> children;*/
}
package com.spzx.product.controller;
@Tag(name = "商品分类")
@RestController
@RequestMapping("/category")
public class CategoryController extends BaseController {
@Autowired
private ICategoryService categoryService;
@Operation(summary = "获取分类下拉树列表")
@GetMapping("/treeSelect/{id}")
public AjaxResult treeSelect(@Parameter(description = "分类id") @PathVariable Long id) {
return success(categoryService.treeSelect(id));
}
}
List<Category> treeSelect(Long parentId);
@Override
public List<Category> treeSelect(Long parentId) {
//根据parentId查找子节点
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Category::getParentId, parentId);
List<Category> categoryList = baseMapper.selectList(queryWrapper);
if(!CollectionUtils.isEmpty(categoryList)){
categoryList.forEach(category -> {
//查找当前节点是否有子节点
Long count = baseMapper.selectCount(
new LambdaQueryWrapper<Category>()
.eq(Category::getParentId, category.getId())
);
category.setHasChildren(count > 0);
});
}
return categoryList;
}
分类品牌管理就是将分类的数据和品牌的数据进行关联,分类数据和品牌数据之间的关系是多对多的关系,因此需要单独使用一张数据表来存储:category_brand
表。
系统管理 -> 菜单管理 -> 商品管理 -> 新增 -> 菜单
分类品牌
package com.spzx.product.domain;
@Schema(description = "分类品牌")
@Getter
@Setter
@TableName("category_brand")
public class CategoryBrand extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "品牌ID")
@NotNull(message = "品牌ID不能为空")
private Long brandId;
@Schema(description = "分类ID")
private Long categoryId;
@Schema(description = "分类名称")
@TableField(exist = false)
private String categoryName;
@Schema(description = "品牌名称")
@TableField(exist = false)
private String brandName;
@Schema(description = "品牌图标")
@TableField(exist = false)
private String logo;
}
package com.spzx.product.controller;
@Tag(name = "分类品牌")
@RestController
@RequestMapping("/categoryBrand")
public class CategoryBrandController extends BaseController {
@Autowired
private ICategoryBrandService categoryBrandService;
@Operation(summary = "查询分类品牌列表")
@GetMapping("/list")
public TableDataInfo list() {
startPage();
List<CategoryBrand> list = categoryBrandService.selectCategoryBrandList();
return getDataTable(list);
}
}
List<CategoryBrand> selectCategoryBrandList();
@Override
public List<CategoryBrand> selectCategoryBrandList() {
return baseMapper.selectCategoryBrandList();
}
List<CategoryBrand> selectCategoryBrandList();
<select id="selectCategoryBrandList" resultType="com.spzx.product.domain.CategoryBrand">
SELECT
cb.id,
cb.brand_id,
cb.category_id,
cb.create_time,
b.name as brandName,
b.logo,
c.name as categoryName
FROM category_brand cb
INNER JOIN category c ON cb.category_id = c.id
INNER JOIN brand b ON cb.brand_id = b.id
WHERE c.del_flag = '0' AND b.del_flag = '0' AND cb.del_flag = '0'
</select>
新增分类品牌时,页面中需要展示全部品牌列表:
@Operation(summary = "获取全部品牌")
@GetMapping("/getBrandAll")
public AjaxResult getBrandAll() {
return success(brandService.list());
}
@Operation(summary = "新增分类品牌")
@PostMapping
public AjaxResult add(
@Parameter(description = "分类品牌")
@RequestBody CategoryBrand categoryBrand) {
categoryBrand.setCreateBy(SecurityUtils.getUsername());
return toAjax(categoryBrandService.insertCategoryBrand(categoryBrand));
}
int insertCategoryBrand(CategoryBrand categoryBrand);
@Override
public int insertCategoryBrand(CategoryBrand categoryBrand) {
LambdaQueryWrapper<CategoryBrand> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper
.eq(CategoryBrand::getCategoryId, categoryBrand.getCategoryId())
.eq(CategoryBrand::getBrandId, categoryBrand.getBrandId());
long count = baseMapper.selectCount(queryWrapper);
if(count > 0) {
throw new ServiceException("该分类下已有该品牌");
}
return baseMapper.insert(categoryBrand);
}
@Operation(summary = "删除分类品牌")
@DeleteMapping("/{ids}")
public AjaxResult remove(
@Parameter(description = "分类品牌id列表", example = "[1,2]")
@PathVariable List<Long> ids) {
return toAjax(categoryBrandService.removeBatchByIds(ids));
}
商品规格指的是商品属性、型号、尺寸、颜色等具体描述商品特点和属性的标准化信息。
以T恤衫为例,它的规格可能包括以下几个方面:
1、颜色:白色、黑色
2、尺码:S、M、L、XL
以手机为例,它的规格可能包括以下几个方面:
1、屏幕尺寸:5.5寸 、6.7寸
2、分辨率:1920x1080、2960x1440、2532x1170
3、运行内存:6GB、8GB、12GB
4、存储容量:64GB、128GB、256GB
系统管理 -> 菜单管理 -> 商品管理 -> 新增 -> 菜单
商品规格
package com.spzx.product.domain;
@Schema(description = "商品规格")
@Getter
@Setter
@TableName("product_spec")
public class ProductSpec extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "分类ID")
private Long categoryId;
@Schema(description = "规格名称")
private String specName;
@Schema(description = "规格值")
private String specValue;
@Schema(description = "分类名称")
@TableField(exist = false)
private String categoryName;
}
package com.spzx.product.controller;
@Tag(name = "商品规格")
@RestController
@RequestMapping("/productSpec")
public class ProductSpecController extends BaseController {
@Autowired
private IProductSpecService productSpecService;
@Operation(summary = "查询商品规格列表")
@GetMapping("/list")
public TableDataInfo list() {
startPage();
List<ProductSpec> list = productSpecService.selectProductSpecList();
return getDataTable(list);
}
}
List<ProductSpec> selectProductSpecList();
@Override
public List<ProductSpec> selectProductSpecList() {
return baseMapper.selectProductSpecList();
}
List<ProductSpec> selectProductSpecList();
<select id="selectProductSpecList" resultType="com.spzx.product.domain.ProductSpec">
SELECT
ps.id,
ps.spec_name,
ps.spec_value,
ps.category_id,
c.name categoryName,
ps.create_time
FROM product_spec ps
INNER JOIN category c ON ps.category_id = c.id
WHERE ps.del_flag = '0' AND c.del_flag = '0'
</select>
@Operation(summary = "新增商品规格")
@PostMapping
public AjaxResult add(
@Parameter(description = "商品规格")
@RequestBody ProductSpec productSpec) {
productSpec.setCreateBy(SecurityUtils.getUsername());
return toAjax(productSpecService.save(productSpec));
}
@Operation(summary = "删除商品规格")
@DeleteMapping("/{ids}")
public AjaxResult remove(
@Parameter(description = "商品规格id列表", example = "[1,2]")
@PathVariable List<Long> ids) {
return toAjax(productSpecService.removeBatchByIds(ids));
}
商品管理就是对电商项目中所涉及到的商品数据进行维护。
数据库表:
商品SPU表 product
商品SKU表 product_sku
商品详情表 product_details
商品库存表 sku_stock
SPU = Standard Product Unit (标准化产品单元),泛指一类商品,这种商品具有相同的属性。
SKU = stock keeping unit(库存量单位),SKU即库存进出计量的单位。也就是说一款商品,可以根据SKU来确定具体的货物存量。
以手机为例,假设有一款名为 "XPhone" 的手机品牌,它推出了一款型号为 "X10" 的手机。这款手机一共有以下几种属性:
1、颜色:黑色、白色、金色
2、存储容量:64GB、128GB
3、内存大小:4GB、6GB
那么,“X10” 这款手机就是这个商品的 SPU,根据不同的属性组合,可以形成多个不同的 SKU。如下所示:
SPU | SKU | 颜色 | 存储容量 | 内存大小 |
---|---|---|---|---|
X10 | SKU1 | 黑色 | 64GB | 4GB |
X10 | SKU2 | 白色 | 64GB | 4GB |
X10 | SKU3 | 金色 | 64GB | 4GB |
X10 | SKU4 | 黑色 | 128GB | 4GB |
X10 | SKU5 | 白色 | 128GB | 4GB |
X10 | SKU6 | 金色 | 128GB | 4GB |
X10 | SKU7 | 黑色 | 64GB | 6GB |
X10 | SKU8 | 白色 | 64GB | 6GB |
X10 | SKU9 | 金色 | 64GB | 6GB |
X10 | SKU10 | 黑色 | 128GB | 6GB |
X10 | SKU11 | 白色 | 128GB | 6GB |
X10 | SKU12 | 金色 | 128GB | 6GB |
再以衣服为例,假设有一家服装网店推出了一款名为 "A衬衫" 的衣服。这款衣服一共有以下几种属性:
1、尺寸:S、M、L、XL
2、颜色:白色、黑色、灰色、蓝色
那么,“A衬衫” 这款衣服就是这个商品的 SPU,根据不同的属性组合,可以形成多个不同的 SKU。如下所示:
SPU | SKU | 尺寸 | 颜色 |
---|---|---|---|
A衬衫 | SKU1 | S | 白色 |
A衬衫 | SKU2 | M | 白色 |
A衬衫 | SKU3 | L | 白色 |
A衬衫 | SKU4 | XL | 白色 |
A衬衫 | SKU5 | S | 黑色 |
A衬衫 | SKU6 | M | 黑色 |
A衬衫 | SKU7 | L | 黑色 |
A衬衫 | SKU8 | XL | 黑色 |
A衬衫 | SKU9 | S | 灰色 |
A衬衫 | SKU10 | M | 灰色 |
A衬衫 | SKU11 | L | 灰色 |
A衬衫 | SKU12 | XL | 灰色 |
A衬衫 | SKU13 | S | 蓝色 |
A衬衫 | SKU14 | M | 蓝色 |
A衬衫 | SKU15 | L | 蓝色 |
A衬衫 | SKU16 | XL | 蓝色 |
系统管理 -> 菜单管理 -> 商品管理 -> 新增 -> 菜单
商品列表
package com.spzx.product.domain;
@Schema(description = "商品SPU")
@Getter
@Setter
@TableName("product")
public class Product extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "商品名称")
private String name;
@Schema(description = "品牌ID")
private Long brandId;
@Schema(description = "一级分类id")
private Long category1Id;
@Schema(description = "二级分类id")
private Long category2Id;
@Schema(description = "三级分类id")
private Long category3Id;
@Schema(description = "计量单位")
private String unitName;
@Schema(description = "轮播图")
private String sliderUrls;
@Schema(description = "商品规格json")
private String specValue;
@Schema(description = "状态:0-初始值,1-上架,-1-自主下架")
private Integer status;
@Schema(description = "品牌")
@TableField(exist = false)
private String brandName;
@Schema(description = "一级分类")
@TableField(exist = false)
private String category1Name;
@Schema(description = "二级分类")
@TableField(exist = false)
private String category2Name;
@Schema(description = "三级分类")
@TableField(exist = false)
private String category3Name;
}
package com.spzx.product.controller;
@Tag(name = "商品")
@RestController
@RequestMapping("/product")
public class ProductController extends BaseController {
@Autowired
private IProductService productService;
@Operation(summary = "查询商品列表")
@GetMapping("/list")
public TableDataInfo list() {
startPage();
List<Product> list = productService.selectProductList();
return getDataTable(list);
}
}
List<Product> selectProductList();
@Override
public List<Product> selectProductList() {
return baseMapper.selectProductList();
}
List<Product> selectProductList();
<select id="selectProductList" resultType="com.spzx.product.domain.Product">
select
p.id,
p.name,
p.unit_name,
p.slider_urls,
p.status,
b.name brandName ,
c1.name category1Name ,
c2.name category2Name ,
c3.name category3Name
from product p
LEFT JOIN brand b on b.id = p.brand_id and b.del_flag = 0
LEFT JOIN category c1 on c1.id = p.category1_id and c1.del_flag = '0'
LEFT JOIN category c2 on c2.id = p.category2_id and c2.del_flag = '0'
LEFT JOIN category c3 on c3.id = p.category2_id and c3.del_flag = '0'
WHERE p.del_flag = '0'
</select>
需求:当添加商品的表单对话框展示出来以后,此时就需要从数据库中查询出来所有的商品单位数据,并将查询到的商品单位数据在商品单元下拉框中
@Operation(summary = "获取全部单位")
@GetMapping("/getUnitAll")
public AjaxResult selectProductUnitAll() {
return success(productUnitService.list());
}
当用户选择了三级分类以后,此时需要将三级分类所对应的品牌数据查询出来在品牌下拉框中进行展示
@Operation(summary = "根据分类id获取品牌列表")
@GetMapping("/brandList/{categoryId}")
public AjaxResult selectBrandListByCategoryId(
@Parameter(description = "分类id")
@PathVariable Long categoryId) {
return success(categoryBrandService.selectBrandListByCategoryId(categoryId));
}
List<Brand> selectBrandListByCategoryId(Long categoryId);
@Override
public List<Brand> selectBrandListByCategoryId(Long categoryId) {
return baseMapper.selectBrandListByCategoryId(categoryId);
}
List<Brand> selectBrandListByCategoryId(Long categoryId);
<select id="selectBrandListByCategoryId" resultType="com.spzx.product.domain.Brand">
SELECT b.id,
b.name,
b.logo
FROM brand b
INNER JOIN category_brand cb ON b.id = cb.brand_id
WHERE cb.category_id = #{categoryId}
AND b.del_flag = '0'
AND cb.del_flag = '0'
</select>
当用户选择了三级分类以后,此时需要将三级分类对应的商品规格数据查询出来
@Operation(summary = "根据分类id获取商品规格列表")
@GetMapping("/productSpecList/{categoryId}")
public AjaxResult selectProductSpecListByCategoryId(
@Parameter(description = "分类id")
@PathVariable Long categoryId) {
return success(productSpecService.selectProductSpecListByCategoryId(categoryId));
}
List<ProductSpec> selectProductSpecListByCategoryId(Long categoryId);
@Override
public List<ProductSpec> selectProductSpecListByCategoryId(Long categoryId) {
LambdaQueryWrapper<ProductSpec> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ProductSpec::getCategoryId, categoryId);
return baseMapper.selectList(queryWrapper);
}
思路分析:
1、前端提交过来的数据,包含了SPU的基本数据,SKU的列表数据,商品详情数据,商品库存数据
2、保存数据的时候需要操作四张表:product、product_sku、product_detail、sku_stock
添加扩展属性
@Schema(description = "商品sku列表")
@TableField(exist = false)
private List<ProductSku> productSkuList;
@Schema(description = "详情图片列表")
@TableField(exist = false)
private List<String> detailsImageUrlList;
package com.spzx.product.domain;
@Schema(description = "商品sku")
@Getter
@Setter
@TableName("product_sku")
public class ProductSku extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "商品编号")
private String skuCode;
@Schema(description = "sku名称")
private String skuName;
@Schema(description = "商品ID")
private Long productId;
@Schema(description = "缩略图路径")
private String thumbImg;
@Schema(description = "售价")
private BigDecimal salePrice;
@Schema(description = "市场价")
private BigDecimal marketPrice;
@Schema(description = "成本价")
private BigDecimal costPrice;
@Schema(description = "sku规格信息json")
private String skuSpec;
@Schema(description = "重量")
private BigDecimal weight;
@Schema(description = "体积")
private BigDecimal volume;
@Schema(description = "线上状态:0-初始值,1-上架,-1-自主下架")
private Integer status;
// 扩展的属性
@Schema(description = "sku库存")
@TableField(exist = false)
private Integer stockNum;
}
package com.spzx.product.domain;
@Schema(description = "商品详情")
@Getter
@Setter
@TableName("product_details")
public class ProductDetails extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "商品id")
private Long productId;
@Schema(description = "详情图片地址")
private String imageUrls;
}
package com.spzx.product.domain;
@Schema(description = "商品sku库存")
@Getter
@Setter
@TableName("sku_stock")
public class SkuStock extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "skuID")
private Long skuId;
@Schema(description = "总库存数")
private Integer totalNum;
@Schema(description = "锁定库存数")
private Integer lockNum;
@Schema(description = "可用库存数")
private Integer availableNum;
@Schema(description = "销量")
private Integer saleNum;
@Schema(description = "线上状态:0-初始值,1-上架,-1-自主下架")
private Integer status;
}
@Operation(summary = "新增商品")
@PostMapping
public AjaxResult add(
@Parameter(description = "商品")
@RequestBody Product product) {
return toAjax(productService.insertProduct(product));
}
int insertProduct(Product product);
因为Spring
的默认的事务规则是遇到运行异常(RuntimeException)
和程序错误(Error)
才会回滚。如果想针对检查异常进行事务回滚,可以在@Transactional
注解里使用 rollbackFor
属性明确指定异常。
@Autowired
private ProductSkuMapper productSkuMapper;
@Autowired
private ProductDetailsMapper productDetailsMapper;
@Autowired
private SkuStockMapper skuStockMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public int insertProduct(Product product) {
baseMapper.insert(product);
List<ProductSku> productSkuList = product.getProductSkuList();
for (int i = 0, size = productSkuList.size(); i < size; i++) {
ProductSku productSku = productSkuList.get(i);
productSku.setSkuCode(product.getId() + "_" + i);
productSku.setProductId(product.getId());
String skuName = product.getName() + " " + productSku.getSkuSpec();
productSku.setSkuName(skuName);
productSku.setStatus(0);
productSkuMapper.insert(productSku);
//添加商品库存
SkuStock skuStock = new SkuStock();
skuStock.setSkuId(productSku.getId());
skuStock.setTotalNum(productSku.getStockNum());
skuStock.setLockNum(0);
skuStock.setAvailableNum(productSku.getStockNum());
skuStock.setSaleNum(0);
skuStockMapper.insert(skuStock);
}
ProductDetails productDetails = new ProductDetails();
productDetails.setProductId(product.getId());
productDetails.setImageUrls(String.join(",", product.getDetailsImageUrlList()));
productDetailsMapper.insert(productDetails);
return 1;
}
@Operation(summary = "更新上下架状态")
@GetMapping("/updateStatus/{id}/{status}")
public AjaxResult updateStatus(
@Parameter(description = "商品id") @PathVariable Long id,
@Parameter(description = "状态") @PathVariable Integer status) {
if(status != 1 && status != -1){
//throw new ServiceException("非法参数");
return AjaxResult.error("非法参数");
}
productService.updateStatus(id, status);
return success();
}
void updateStatus(Long id, Integer status);
@Transactional(rollbackFor = Exception.class)
@Override
public void updateStatus(Long id, Integer status) {
Product product = new Product();
product.setId(id);
product.setStatus(status);
baseMapper.updateById(product);
//更新sku的status
productSkuMapper.update(
null,
new LambdaUpdateWrapper<ProductSku>()
.eq(ProductSku::getProductId, id)
.set(ProductSku::getStatus, status));
}
@Operation(summary = "删除商品")
@DeleteMapping("/{ids}")
public AjaxResult remove(
@Parameter(description = "商品id列表")
@PathVariable List<Long> ids) {
return toAjax(productService.deleteProductByIds(ids));
}
int deleteProductByIds(List<Long> ids);
@Transactional(rollbackFor = Exception.class)
@Override
public int deleteProductByIds(List<Long> ids) {
// 删除商品
baseMapper.deleteBatchIds(ids);
//查询skuIds(先查询)
LambdaQueryWrapper<ProductSku> queryWrapper = new LambdaQueryWrapper<ProductSku>()
.in(ProductSku::getProductId, ids)
.select(ProductSku::getId);
List<ProductSku> productSkuList = productSkuMapper.selectList(queryWrapper);
List<Long> skuIds = productSkuList.stream().map(ProductSku::getId).collect(Collectors.toList());
// 删除商品sku(再删除)
productSkuMapper.delete(
new LambdaQueryWrapper<ProductSku>()
.in(ProductSku::getProductId, ids)
);
//删除商品详情
productDetailsMapper.delete(
new LambdaQueryWrapper<ProductDetails>()
.in(ProductDetails::getProductId, ids)
);
//删除商品库存
return skuStockMapper.delete(
new LambdaQueryWrapper<SkuStock>()
.in(SkuStock::getSkuId, skuIds)
);
}