1. 新增SPU
1.1. 分析
为了保证数据表的查询效率,SPU数据中的“详情”(通过富文本编辑器输入的内容)被设计在pms_spu_detail
表中,而其它的一般数据在pms_spu
表中,当“新增SPU”时,本质上需要同时对这2张表进行“插入数据”的操作!
需要执行的SQL语句大致是:
insert into pms_spu (字段列表) values (值列表);
insert into pms_spu_detail (字段列表) values (值列表);
注意:在pms_spu
表中,主键(id
)并不是自动编号的(要考虑分库分表时,相关的数据表的主键都不允许使用自动编号)!
另外,在插入数据之前,相关数据需要进行检查,包括:检查品牌、类别、相册的id是否存在、是否有效等,这些功能此前已经完成!需要注意:关于品牌、类别、相册的名称,前端是可以提交这些数据的,但是,服务器端不应该直接使用这些数据写入到数据表,只使用前端提交的品牌id、类别id、相册id,至于品牌名称、类别名称、相册名称,应该是在检查时一并查出来,并使用查询到的数据写入到数据表中!
1.2. 关于Mapper层
关于插入Spu数据
在根包下创建pojo.entity.Spu
实体类。
在根包下创建mapper.SpuMapper
接口,添加抽象方法:
@Repository
public interface SpuMapper {
/**
* 插入SPU数据
*
* @param spu SPU数据
* @return 受影响的行数
*/
int insert(Spu spu);
}
在src/main/resources/mapper
下粘贴得到SpuMapper.xml
文件,配置SQL语句:
<mapper namespace="cn.tedu.csmall.product.mapper.SpuMapper">
<!-- 由于pms_spu表的id不是自动编号的,在插入数据时,需要显式指定此字段的值 -->
<!-- 所以,不需要配置useGeneratedKeys和keyProperty -->
<!-- int insert(Spu spu); -->
<insert id="insert">
INSERT INTO pms_spu (
id, name, type_number, title, description,
list_price, stock, stock_threshold, unit, brand_id,
brand_name, category_id, category_name, attribute_template_id, album_id,
pictures, keywords, tags, sort, is_deleted,
is_published, is_new_arrival, is_recommend, is_checked, gmt_check
) VALUES (
#{id}, #{name}, #{typeNumber}, #{title}, #{description},
#{listPrice}, #{stock}, #{stockThreshold}, #{unit}, #{brandId},
#{brandName}, #{categoryId}, #{categoryName}, #{attributeTemplateId}, #{albumId},
#{pictures}, #{keywords}, #{tags}, #{sort}, #{isDeleted},
#{isPublished}, #{isNewArrival}, #{isRecommend}, #{isChecked}, #{gmtCheck}
)
</insert>
</mapper>
在src/test/java
的根包下创建SpuMapperTests
测试类,测试以上抽象方法:
@Slf4j
@SpringBootTest
public class SpuMapperTests {
@Autowired
SpuMapper mapper;
@Test
public void testInsert() {
Spu spu = new Spu();
spu.setId(11000L); // 重要,必须
spu.setName("小米13");
log.debug("插入数据之前,参数={}", spu);
int rows = mapper.insert(spu);
log.debug("rows = {}", rows);
log.debug("插入数据之后,参数={}", spu);
}
}
关于插入SpuDetail数据
在根包下创建pojo.entity.SpuDetail
实体类。
在根包下创建mapper.SpuDetailMapper
接口,添加抽象方法:
@Repository
public interface SpuDetailMapper {
/**
* 插入SPU详情数据
*
* @param spuDetail SPU详情数据
* @return 受影响的行数
*/
int insert(SpuDetail spuDetail);
}
在src/main/resources/mapper
下粘贴得到SpuDetailMapper.xml
文件,配置SQL语句:
<mapper namespace="cn.tedu.csmall.product.mapper.SpuDetailMapper">
<!-- int insert(SpuDetail spuDetail); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO pms_spu_detail (
spu_id,detail
) VALUES (
#{spuId},#{detail}
)
</insert>
</mapper>
在src/test/java
的根包下创建SpuDetailMapperTests
测试类,测试以上抽象方法:
@Slf4j
@SpringBootTest
public class SpuDetailMapperTests {
@Autowired
SpuDetailMapper mapper;
@Test
public void testInsert() {
SpuDetail spuDetail = new SpuDetail();
spuDetail.setSpuId(10000L);
spuDetail.setDetail("这是1号Spu的详情");
log.debug("插入数据之前,参数={}", spuDetail);
int rows = mapper.insert(spuDetail);
log.debug("rows = {}", rows);
log.debug("插入数据之后,参数={}", spuDetail);
}
}
1.3. 关于Service层
为了保证Spu的id的唯一性,且基于“不会高频率新增Spu”,可以使用时间加随机数字作为id值。
考虑到后续可能调整生成id的策略,则将生成id的代码写在专门的工具类中,不写在业务层。
在根包下创建util.IdUtils
类,定义生成id的静态方法:
package cn.tedu.csmall.product.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
/**
* Id工具类
*
* @author java@tedu.cn
* @version 0.0.1
*/
public final class IdUtils {
private IdUtils() {}
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private static Random random = new Random();
// 临时策略:使用“年月日时分秒毫秒”加2位随机数作为id
public static Long getId() {
LocalDateTime now = LocalDateTime.now();
String dateTimeString = dateTimeFormatter.format(now);
int randomNumber = random.nextInt(89) + 10;
Long id = Long.valueOf(dateTimeString + randomNumber);
return id;
}
}
在根包下创建pojo.dto.SpuAddNewDTO
类(注意:相对于实体类,需要删除不由客户端提交的数据,并且,需要补充detail
属性,表示“Spu详情”):
package cn.tedu.csmall.product.pojo.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* SPU(Standard Product Unit)
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Data
public class SpuAddNewDTO implements Serializable {
/**
* SPU名称
*/
private String name;
/**
* SPU编号
*/
private String typeNumber;
/**
* 标题
*/
private String title;
/**
* 简介
*/
private String description;
/**
* 价格(显示在列表中)
*/
private BigDecimal listPrice;
/**
* 当前库存(冗余)
*/
private Integer stock;
/**
* 库存预警阈值(冗余)
*/
private Integer stockThreshold;
/**
* 计件单位
*/
private String unit;
/**
* 品牌id
*/
private Long brandId;
/**
* 类别id
*/
private Long categoryId;
/**
* 属性模板id
*/
// private Long attributeTemplateId;
/**
* 相册id
*/
private Long albumId;
/**
* 组图URLs,使⽤JSON格式表示
*/
// private String pictures;
/**
* 关键词列表,各关键词使⽤英⽂的逗号分隔
*/
private String keywords;
/**
* 标签列表,各标签使⽤英⽂的逗号分隔,原则上最多3个
*/
private String tags;
/**
* ⾃定义排序序号
*/
private Integer sort;
/**
* Spu详情
*/
private String detail;
}
在根包下创建ISpuService
接口,并在接口中添加“新增SPU”的抽象方法:
@Transactional
public interface ISpuService {
void addNew(SpuAddNewDTO spuAddNewDTO);
}
在根包下创建SpuServiceImpl
类,是组件类,实现以上接口:
@Slf4j
@Service
public class SpuServiceImpl implements ISpuService {
@Autowired
private SpuMapper spuMapper;
@Autowired
private SpuDetailMapper spuDetailMapper;
@Autowired
private BrandMapper brandMapper;
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private AlbumMapper albumMapper;
@Override
public void addNew(SpuAddNewDTO spuAddNewDTO) {
// 从参数spuAddNewDTO中取出brandId
// 调用brandMapper的getDetailsById()方法查询品牌
// 判断查询结果是否为null
// 是:抛出异常:选择的品牌不存在
// 判断查询到的品牌的enable是否为0
// 是:抛出异常
// 从参数spuAddNewDTO中取出categoryId
// 调用categoryMapper的getDetailsById()方法查询类别
// 判断查询结果是否为null
// 是:抛出异常:选择的类别不存在
// 判断查询到的类别的enable是否为0
// 是:抛出异常
// 判断查询到的类别的isParent是否为1
// 是:抛出异常
// 从参数spuAddNewDTO中取出albumId
// 调用albumMapper的getDetailsById()方法查询相册
// 判断查询结果是否为null
// 是:抛出异常:选择的相册不存在
// 创建Spu对象
// 将参数spuAddNewDTO的属性值复制到Spu对象中
// 补全Spu对象的属性值:id >>> 自行决定
// 补全Spu对象的属性值:brandName >>> 前序查询品牌的结果中取出
// 补全Spu对象的属性值:categoryName >>> 前序查询类别的结果中取出
// 补全Spu对象的属性值:sales / commentCount / positiveCommentCount >>> 0
// 补全Spu对象的属性值:isDelete / isPublished >>> 0
// 补全Spu对象的属性值:isNewArrival / isRecommend >>> 自行决定
// 补全Spu对象的属性值:isChecked >>> 0
// 补全Spu对象的属性值:checkUser / gmtCheck >>> null
// 调用spuMapper的int insert(Spu spu)方法插入Spu数据,并获取返回值
// 判断返回值是否不为1
// 是:抛出异常
// 创建SpuDetail对象
// 补全SpuDetail对象的属性值:spuId >>> 同以上Spu对象的id
// 补全SpuDetail对象的属性值:detail >>> 来自spuAddNewDTO参数
// 调用spuDetailMapper的int insert(SpuDetail spuDetail)方法插入SpuDetail数据,并获取返回值
// 判断返回值是否不为1
// 是:抛出异常
}
}
具体实现为:
package cn.tedu.csmall.product.service.impl;
import cn.tedu.csmall.product.ex.ServiceCode;
import cn.tedu.csmall.product.ex.ServiceException;
import cn.tedu.csmall.product.mapper.*;
import cn.tedu.csmall.product.pojo.dto.SpuAddNewDTO;
import cn.tedu.csmall.product.pojo.entity.Spu;
import cn.tedu.csmall.product.pojo.entity.SpuDetail;
import cn.tedu.csmall.product.pojo.vo.AlbumStandardVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import cn.tedu.csmall.product.pojo.vo.CategoryStandardVO;
import cn.tedu.csmall.product.service.ISpuService;
import cn.tedu.csmall.product.util.IdUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 处理Spu业务的实现类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Service
@Slf4j
public class SpuServiceImpl implements ISpuService {
@Autowired
private SpuMapper spuMapper;
@Autowired
private SpuDetailMapper spuDetailMapper;
@Autowired
private BrandMapper brandMapper;
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private AlbumMapper albumMapper;
@Override
public void addNew(SpuAddNewDTO spuAddNewDTO) {
// 从参数spuAddNewDTO中取出brandId
Long brandId = spuAddNewDTO.getBrandId();
// 调用brandMapper的getDetailsById()方法查询品牌
BrandStandardVO brand = brandMapper.getStandardById(brandId);
// 判断查询结果是否为null
if (brand == null) {
// 是:抛出异常:选择的品牌不存在
String message = "新增Spu失败,尝试绑定的品牌数据不存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
}
// 判断查询到的品牌的enable是否为0
if (brand.getEnable() == 0) {
// 是:抛出异常
String message = "新增Spu失败,尝试绑定的品牌已经被禁用!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_CONFLICT, message);
}
// 从参数spuAddNewDTO中取出categoryId
Long categoryId = spuAddNewDTO.getCategoryId();
// 调用categoryMapper的getDetailsById()方法查询类别
CategoryStandardVO category = categoryMapper.getStandardById(categoryId);
// 判断查询结果是否为null
if (category == null) {
// 是:抛出异常:选择的类别不存在
String message = "新增Spu失败,尝试绑定的类别数据不存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
}
// 判断查询到的类别的enable是否为0
if (category.getEnable() == 0) {
// 是:抛出异常
String message = "新增Spu失败,尝试绑定的类别已经被禁用!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_CONFLICT, message);
}
// 判断查询到的类别的isParent是否为1
if (category.getIsParent() == 1) {
// 是:抛出异常
String message = "新增Spu失败,尝试绑定的类别包含子级类别,不允许使用此类别!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_CONFLICT, message);
}
// 从参数spuAddNewDTO中取出albumId
Long albumId = spuAddNewDTO.getAlbumId();
// 调用albumMapper的getDetailsById()方法查询相册
AlbumStandardVO album = albumMapper.getStandardById(albumId);
// 判断查询结果是否为null
if (album == null) {
// 是:抛出异常:选择的相册不存在
String message = "新增Spu失败,尝试绑定的相册数据不存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
}
// 获取id(由别处生成)
Long id = IdUtils.getId();
// 创建Spu对象
Spu spu = new Spu();
// 将参数spuAddNewDTO的属性值复制到Spu对象中
BeanUtils.copyProperties(spuAddNewDTO, spu);
// 补全Spu对象的属性值:id >>> 自行决定
spu.setId(id);
// 补全Spu对象的属性值:brandName >>> 前序查询品牌的结果中取出
spu.setBrandName(brand.getName());
// 补全Spu对象的属性值:categoryName >>> 前序查询类别的结果中取出
spu.setCategoryName(category.getName());
// 补全Spu对象的属性值:sales / commentCount / positiveCommentCount >>> 0
spu.setSales(0);
spu.setCommentCount(0);
spu.setPositiveCommentCount(0);
// 补全Spu对象的属性值:isDelete / isPublished >>> 0
spu.setIsDeleted(0);
spu.setIsPublished(0);
// 补全Spu对象的属性值:isNewArrival / isRecommend >>> 自行决定
spu.setIsNewArrival(0);
spu.setIsRecommend(0);
// 补全Spu对象的属性值:isChecked >>> 0
spu.setIsChecked(0);
// 补全Spu对象的属性值:checkUser / gmtCheck >>> null
// 调用spuMapper的int insert(Spu spu)方法插入Spu数据,并获取返回值
int rows = spuMapper.insert(spu);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出异常
String message = "新增Spu失败!服务器忙,请稍后再次尝试![错误代码:1]";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_INSERT, message);
}
// 创建SpuDetail对象
SpuDetail spuDetail = new SpuDetail();
// 补全SpuDetail对象的属性值:spuId >>> 同以上Spu对象的id
spuDetail.setSpuId(id);
// 补全SpuDetail对象的属性值:detail >>> 来自spuAddNewDTO参数
spuDetail.setDetail(spuAddNewDTO.getDetail());
// 调用spuDetailMapper的int insert(SpuDetail spuDetail)方法插入SpuDetail数据,并获取返回值
rows = spuDetailMapper.insert(spuDetail);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出异常
String message = "新增Spu失败!服务器忙,请稍后再次尝试![错误代码:2]";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_INSERT, message);
}
}
}
在src/test/java
的根包下创建service.SpuServiceTests
测试类,编写并执行测试:
@Slf4j
@SpringBootTest
public class SpuServiceTests {
@Autowired
ISpuService service;
@Test
void testAddNew() {
try {
SpuAddNewDTO spuAddNewDTO = new SpuAddNewDTO();
spuAddNewDTO.setBrandId(2L);
spuAddNewDTO.setCategoryId(3L);
spuAddNewDTO.setAlbumId(2L);
spuAddNewDTO.setName("测试Spu-001");
service.addNew(spuAddNewDTO);
log.debug("新增Spu成功!");
} catch (ServiceException e) {
log.debug("serviceCode : " + e.getServiceCode());
log.debug("message : " + e.getMessage());
}
}
}
1.4. 关于Controller层
在根包下创建SpuController
控制器类,并处理请求:
package cn.tedu.csmall.product.controller;
/**
* 处理Spu相关请求的控制器
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@RestController
@RequestMapping("/spu")
@Api(tags = "08. SPU管理模块")
public class SpuController {
@Autowired
private ISpuService spuService;
public SpuController() {
log.info("创建控制器:SpuController");
}
// 添加SPU
// http://localhost:9080/spu/add-new
@ApiOperation("新增SPU")
@ApiOperationSupport(order = 100)
@PostMapping("/add-new")
public JsonResult<Void> addNew(@Validated SpuAddNewDTO spuAddNewDTO) {
log.debug("开始处理【新增SPU】的请求:{}", spuAddNewDTO);
spuService.addNew(spuAddNewDTO);
return JsonResult.ok();
}
}
关于Redis
Redis是一款使用K-V结构的、基于内存实现数据存取的NoSQL非关系型数据库。
使用Redis的主要目的是“缓存数据”,以提高查询数据的效率,对数据库也有一定的保护作用。
需要注意:缓存的数据可能存在“不一致”的问题,因为,如果修改了数据库(例如MySQL)中的数据,而缓存(例如Redis)中的数据没有一并更新,则缓存中的数据是不准确的!但是,并不是所有的场景都要求数据非常准确!
所以,使用Redis的前提条件:
- 对数据的准确性要求不高
- 例如:新浪微博的热搜排名、某个热门视频的播放量
- 数据的修改频率不高
- 例如:电商平台中的类别、电商平台中的品牌
反之,某些情况下是不应该使用Redis的,例如:在秒杀商品时,使用Redis记录一些频繁修改的数据!
在操作系统的终端下,通过redis-cli
命令即可登录Redis控制台:
redis-cli
在Redis控制台中,使用set
和get
命令就可以存取基本类型的数据:
set name liucangsong
get name
在Redis中,有5种典型数据类型:字符串、Hash、Set、zSet、List,在结合编程时,通常,只需要使用“字符串”即可,在程序中,非字符串类型的数据(例如对象、集合等)都会通过工具转换成JSON格式的字符串再存入到Redis中,后续,从Redis中取出的也是字符串,再通过工具转换成原本的类型(例如对象、集合等)即可。
作业
实现以下功能(含前端页面与后端服务)
- 显示SPU列表
- 删除SPU
- 业务规则:数据必须存在
- 注意:需删除
pms_spu
和pms_spu_detail
这2张表中的相关数据
- 逻辑删除SPU
- 业务规则:数据必须存在
- 注意:本质上是执行
UPDATE
操作,将is_delete
改为1
- 恢复SPU
- 业务规则:数据必须存在
- 注意:本质上是执行
UPDATE
操作,将is_delete
改为0
- 根据id查询SPU详情
- 特别说明:只需要完成后端,不需要实现前端页面