登录功能
浏览器登录请求 -> 后端执行登录操作访问数据库 -> 用户存在则生成令牌并返回给浏览器 -> 接下来的浏览器请求都会带着这个令牌被拦截器拦截,由拦截器解析请求头中的token标识的令牌。
令牌
在拦截器通过在请求头获得令牌jwtProperties.getAdminTokenName()是我们在jwtProperties中通过注入的方式赋值,在yml配置文件中配置了他的值。
String token = request.getHeader(jwtProperties.getAdminTokenName());
密码加密
MD5的加密方式是不可逆的(只能明文->密文)。除了MD5的加密方式,还有HS256
所以MD5比对方式是对比密文。因为是对比数据库中的密码所以要将数据库中的用户密码改成密文。
md5DigestAsHex(转换), DigestUtils(对比)
"进行d5加密,然后再进行比对"
password = DigestUtils.md5DigestAsHex(password.getBytes()),
if(!password.equals(employee.getPassword())){
"密码错误"
throw new PasswordErrorException(MessageConstant.PASSWORD ERROR);
}
导入接口文档
可以用Yapi(接口管理平台),也可以用Apifox导入YApi文件
进入到Apifox中后创建新项目-> 项目里点更多功能-> 导入-> 选Yapi
Swagger
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。类似postman。用Apifox的话就不用一下的配置了。
1.Knife4i(封装了Swagger)是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
如果用的Apifox的话就不用配置这个了。
<dependency>
<groupld>com.github.xiaoymin</goupld>
<artifactld>knife4j-spring-boot-starter</artifactld>
<version>3.0.2</version>
</dependency>
2.还需要完成Knifej的配置,在WebMvcConfigurationg中声名第三方的bean。
@Bean
public Docket docket(){
log.info =("准备生成接口文档...")
Apilnfo apilnfo = new ApilnfoBuilder()
.title(“苍穹外卖项目接口文档”)
.version(“2.0”)
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER 2)
.apilnfo(apilnfo)
.select();
//指定生成接口需要扫描的包
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
扫描的包是controller,它会根据controller包中的方法来生成对应的接口文档。
3.设置静态资源映射,(否则接口文档页面无法访问)。
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射…");
registry.addResourceHandler("/doc.htm!").addResourceLocations("classpath:/META-INF/resources/.")
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/ebjars/");
}/WebMvcConfigurationg
当我们请求Hander中的路径是就会把资源(生成的接口文档)映射到Locations中的路径中去。
运行完成后就可以通过localhost:8080/doc.html链接显示接口文档了(此接口文档由Knifej动态生成)。
常用注解
@Api 用在类上,例如Controller,表示对类的说明
@ApiModel 用在属性上,描述属性信息
@ApiModelProperty 用在属性上,
@ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用
员工操作
新增员工
(1)设计
1.接口设计
设计请求方式,请求参数,返回数据等
管理端发出的请求,统一使用/admin作为前缀
用户端发出的请求,统一使用/user作为前缀
2.数据库设计
如根据需求设计字段的属性,约束等.
补充:DTO
在创建新用户的时候,我们一般用对应的实体类来接受数据,但因为创建用户的话有些属性可以先不填,此时我们可以用DTO来精确的封装,当然想用实体类的话也可以。比如在前端创建用户是用输入名字和密码,而实体类中的属性有id,名字和密码。这时候我们可以创建一个只有名字和密码的DTO。但是在传给持久层的时候建议使用实体类(在service实现类方法中进行类型转换,然后补全数据),说明:DTO跟实体类字段个数不同的话可以防止数据库字段暴露。
在service中可以使用,对象属性拷贝 能自动为两个类的对象中名字相同的属性赋值。1付给2
BeanUtils.copyProperties(employeeDTO, employee);
整体构造:Controller -> service(补充数据) -> mapper
(2)获取登录员的ID
在下发令牌的时候我们就将登录员的ID放到了令牌里,在发放令牌后,接下来浏览器的请求头中都会携带这个token,然后在拦截器中解析token,此时我们就可以获取登陆员的ID,那么我们在拦截器中获得了ID,怎么将他传到service中呢?
我们可以用ThreadLocal,他并不是一个Thread(一个请求就是一个线程),而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间(那么我们就可以在线程的生命周期内,在所有地方去共享这个空间),具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不
能访问。
在黑马提供的初始代码中我们在BaseContext中设置好了ThreadLocal,我们将拦截器获取的ID存到里面。
ThreadLocal常用方法:
· public void set(T value) 设置当前线程的线程局部变量的值
· public T get() 返回当前线程所对应的线程局部变量的值
· public void remove() 移除当前线程的线程局部变量
service层代码
设计代码展开
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//这是对象属性拷贝 能自动为名字想相同的属性赋值。
BeanUtils.copyProperties(employeeDTO, employee);
employee.setStatus(StatusConstant.ENABLE);
//设置默认密码123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//ToDo 后续完善
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);
}
分页查询
1.接口设计
通过Query形式,就是用地址+?的形式传参;
请求参数为page(页码)、pageSize(每页记录数),返回数据记录数和员工信息
2.代码开发
设计DTO(name(非必须),page,pageSize),设计PageResult类,将PageResult放到Result中返回就行。记得在Controller方法中的返回值类型写这个Result
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total;//总记录数
private List records;//当前页数据集合
}
3.业务层
在service中使用pagehelper(需导入插件)可以简化分页查询的SQL语句。根据pagehelper的使用规则我们需要用page来接收查询的结果。底层用到了ThreadLocal,将page和pageSize存到了空间中,在我们编写Sql语句后,他会动态的加上limit语句,并从空间中取用page和pageSize。并且它会自动执行一个SQL语句:select count(0) fron 数据表; 来统计数据量。
controller层
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
Log.info("员工分页查询,参数为:{}",employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result. success(pageResult);
}
service层
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO){
// select * from employee limit 0,10
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
//这里用到了有参构造。
return new PageResult(total, records);
}
因为name可有可无所以用动态SQL,在service -> yml配置文件中用mapper-locations来扫描Mapper.xml文件。
employeeMapperxml
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
优化
我们会发现我们返回的员工信息中的时间格式,前端识别不出来导致前端页面不能正常显示。
时间格式处理
方式一:采用注解(只能处理一个类的属性)
@JsonFormat (pattern = "yyyy-MM-dd HH:mm: ss")
private LocalDateTime updateTime;
方式二:统一处理
protected void extendMessageConverters(List<HttpMessageConverter <? >> converters){
log.info("开始扩展消息转换器.");
//创建一个消息转化器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,可以将Java对象序列化为json数据
converter.setobjectMapper(new JacksonObjectMapper());
//将我们自己的转换器放入spring MVC框架的容器中
converters.add(0,converter);
}对象转换器黑马提供的初始框架中就有commen -> json -> JacksonObjectMapper
对象转化器可以将java对象转换成json,也可以将解析json成java对象(反序列化)
converters中存放的是工程中所有的消息转换器。
其中add中的零是说优先执行converter消息转换器。
启用禁用员工账号
1.设计接口
我们通过路径参数传入状态,发送方式:post路径:/admin/employee/status/{status}
通过Query发送员工id数据
2.代码设计
动态SQL的一般写法,没新的东西。启用禁用通过status的值来判断。
编辑员工
1.接口设计
编辑员工设计两个接口。1.根据id查员工信息(Get,路径参数)。2.编辑员工信息(PUT,json形式)。
2.代码开发
在service层,其他一般的写法。
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO,employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}
因为在前面我们已经通过ThreadLocal设置好了Id拿来直接用就行了。
分类(菜品)管理
设计修改、查询、分页查询、新增,查看分类等操作;在数据库中category表中储存着数据
设计代码展开
1.DishMapper和SetmealMapper中实现查询菜单和setmeal(套餐)分类下的菜品数量;@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
2.在categoryMapper中编写insert,pagequery,delete,update等方法。
3.在service层中实现对应的方法。
展开
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
//新增分类
public void save(CategoryDTO categoryDTO) {
Category category = new Category();
//属性拷贝
BeanUtils.copyProperties(categoryDTO, category);
//分类状态默认为禁用状态0
category.setStatus(StatusConstant.DISABLE);
//设置创建时间、修改时间、创建人、修改人
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(BaseContext.getCurrentId());
category.setUpdateUser(BaseContext.getCurrentId());
categoryMapper.insert(category);
}
//分页查询
public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
//下一条sql进行分页,自动加入limit关键字分页
Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
public void deleteById(Long id) {
//查询当前分类是否关联了菜品,如果关联了就抛出业务异常
Integer count = dishMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
}
//查询当前分类是否关联了套餐,如果关联了就抛出业务异常
count = setmealMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
}
//删除分类数据
categoryMapper.deleteById(id);
}
public void update(CategoryDTO categoryDTO) {
Category category = new Category();
BeanUtils.copyProperties(categoryDTO,category);
//设置修改时间、修改人
category.setUpdateTime(LocalDateTime.now());
category.setUpdateUser(BaseContext.getCurrentId());
categoryMapper.update(category);
}
public void startOrStop(Integer status, Long id) {
Category category = Category.builder()
.id(id)
.status(status)
.updateTime(LocalDateTime.now())
.updateUser(BaseContext.getCurrentId())
.build();
categoryMapper.update(category);
}
//根据类型查询分类
public List<Category> list(Integer type) {
return categoryMapper.list(type);
}
}
公共字段自动填充
如创建/修改时间,创建/修改者ID等都要我们在service实现类中手动填充,我们要做的是让他们自动填充。
具体方法:
● 自定义注解AutoFill(主要是放在mapper上),用于标识需要进行公共字段自动填充的方法
● 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
● 在Mapper的方法上加入AutoFill 注解
技术点:枚举、注解、AOP、反射
自定义注解在server -> annotation -> Autofill
设计代码展开
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//指定数据库的操作类型。
OperationType value();
}
设计切面类
设计代码展开
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
//切入点
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
//拦截mapper中的所有方法。但是新增,还有删除的化,就不需要太南充字段,
//所以使用annotation中指定只在加上了AutoFill注解的方法进行太填充
public void autoFillPointCut(){}
// 使用前置通知
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填冲");
//获得方法签名的对象,getMethod是MethodSignature特有的方法,所以要转型。
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获得在方法上的注解对象,目的是看配置的数据库操作类型
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
//获得方法中的参数
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
//在我们定义的方法中实体类对象一般是放在方法参数的第一位
Object entity = args[0];
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//用ser方法进行映射赋值,在双引号中设置方法名的时候用可常量代替。
if(operationType == OperationType.INSERT){
try {
Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
try {
Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后在Mapper方法上加上注解如:更新@AutoFill(value = OperationType.UPDATE),OperationType.UPDATE是我们定义的常量。common -> enumeration;
public enum OperationType {
UPDATE,
INSERT
}
配置好后就可以将service实现类中把对应的普补充数据的语法删掉就行了。
文件上传
(1) 设计
业务规则:
· 菜品名称必须是唯一的。
· 菜品必须属于某个分类下,不能单独存在。
· 新增菜品时可以根据情况选择菜品的口味。
· 每个菜品必须对应一张图片。
在上面我们完成了分类查询的功能,现在我们设计文件上传和新增菜品
引入概念:逻辑外键:我们在程序中把该字段看成外键,但是并不把他的约束设置成外键。
上传文件接口设计:post 在请求头中Content-Type配置成multipart/form-data Body是file类型的
(2) 代码开发
设计上传文件的Controller,我们这里命名成CommonController
1.配置属性,我们可以直接在application.yml中填写数据,也可以用引用application-dev.yml的属性,以下是引用的方法。前提是在yml文件中的spring标签中的profils中和dev进行绑定,之就说明我们可以绑定其他的配置文件。(application.yml是主配置文件),这样的作用是环境隔离。
配置文件展开 >
# dev.yml 记得改回自己的密钥
sky:
alioss:
endpoint:
access-key-id:
access-key-secret:
bucket-name:
# yml
sky:
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
设计代码展开
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
//文件上传
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
设计代码展开
@Configuration
@Slf4j
public class OssConfiguration{
@Bean
@ConditionalOnMissingBean//这个注解保证这个bean是单例。
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
4.CommonController
先注入AliOssUtil
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
Log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error(“文件上传失败:{}”,e);
}
return Result.error("文件上传失败。");
}
如果不能正常显示图片可以试着将bucket(Alioss中)权限设置为公共读。
菜品操作
创建DishController,service和mapper,因为我们一个菜品还对应者多个口味,在数据库中我们有菜品表和口味表,在菜品的实体类中我们还有一个存储口味的集合list<口味类>,涉及到多张表所以要用到事务(在service中用注解开启事务)。
新增
准备Dish,DishDTO(其中具有Flavor集合),DishFlavor实体类,DishServiceImpl,Dish Mapper和DishFlavorMapper,及对应xml配置文件
DishController
@GetMapping("/page")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
DishServiceImpl
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavoerMapper dishFlavoerMapper;
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.insert(dish);
//因为id是自动分配的所以在插入前我们不知道id是多少,通过在Mapper.XML的配置就可以返回id。
//从而获取id的值。赋值给dish_id
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors!=null&&flavors.size()>0){
//遍历将外键赋值
flavors.forEach(dishFlavor->{
dishFlavor.setDishId(dishId);
});
dishFlavoerMapper.insertBatch(flavors);
}
}
}
对于DishFlavor因为传入的是集合所以采用批量处理的操作,foreach
DishFlavorMapper
@Mapper
public interface DishFlavoerMapper {
//批量加如口味
void insertBatch(List<DishFlavor> flavors);
}
DishFlavorMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavoerMapper">
<insert id="insertBatch">
insert into dish_flavor(dish_id, name, value) values
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
collection指定集合名称,item为集合元素命名,separator因为集合中的元素是以逗号进行分割的,所以指定逗号分割。
DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish(name, category_id, price, image, description, create_time, update_time, create_user, update_user,status)
values
(#{name},#{categoryId},#{price},#{image},#{description},#{categoryId},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
</insert>
</mapper>
useGeneratedKeys是否返回主键。KeyProperty主键映射指定成实体类中主键的命名
DishController
@RestController
@RequestMapping("/admin/dish")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品{}",dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
分页查询
接口:get方式,参数类型:Query,返回数据:id,name,price,image,description,status,updateTime,catoryName。因为在dish类中并没有分类名称,只有分类ID,所以我们需要创建一个VO用来返回。整体的话和员工分页查询一样,只不过多了一个VO
VO展开
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//更新时间
private LocalDateTime updateTime;
//分类名称
private String categoryName;
//菜品关联的口味
private List<DishFlavor> flavors = new ArrayList<>();
//private Integer copies;
}
service层
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
动态SQL采用了多表联查,因为在dish表和category表中的名字字段都是name,所以在映射的时候可能导致DishVO的catgoryname属性赋值不上,所以需要在查询SQL中为category的name字段起别名。
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*,c.name as categoryName from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>
删除菜品
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
接口:delete,query传参,参数名名称ids,在地址中id之间用逗号隔开(如:ids = 1,2,3)。删除的话数据库设计需要涉及三张表dish,dish_flavor,setmeal_dish(套餐关联表)
对于地址中的ids他其实是字符串类型的,在controller中可以用String来接收,再用逗号分割后处理,当然用集合接收是最简单的
controller
public Result delete(@RequestParam List<Long> ids){
log.info("菜品删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
service
展开
public void deleteBatch(List<Long> ids) {
//判断菜品是否是起售中的菜品
for(Long id:ids){
Dish dish = dishMapper.getById(id);
if(dish.getStatus() == StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//是否配套餐关联了
List<Long> setmealids = setmealDishMapper.getSetmealIdByDishIds(ids);
if(setmealids!=null&&setmealids.size()>0){
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除dish表中的数据
for(Long id:ids){
dishMapper.deleteById(id);
//删除dish_flavor数据
dishFlavoerMapper.deleteByDishId(id);
}
}
for循环可以用批量查询(SQL -> in)来代替,先批量查询,然后拿到结果集,使用java8的stream中的anyMatch来做,替换的换写法和下面的SetmealDish.xml一样
书写DishMapper,DishFlavoerMapper,SetmealDishMapper
SetmealDish.xml
<select id="getSetmealIdByDishIds" resultType="java.lang.Long">
select * from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>
修改菜品
在修改之前需要进行查询操作,在修改页面将信息回显。在查询菜品的时候需要将口味相关的数据也查询出来。所以要用DishVO来返回
接口设计:查询GET Path参数,修改:PUT,json数据
1.用两步在Dishservice实现类中查询数据,controller和mapper一般写法
public DishVO getByIdWithFlavor(@PathVariable Long id) {
Dish dish = dishMapper.getById(id);
List<DishFlavor> dishFlavors = dishFlavoerMapper.getByDishId(id);
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish,dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
2.修改提交的数据和新增的接收一样用DishDTO,返回的话Result中date是空
展开
1.controller一般写法(别忘了写查询的Mapping方法)@PutMapping
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
2.service的话稍微难一点,因为口味在修改时可以把原来的删除重新添加新的。我们采用先删除再重新插入新数据。所以我们分三步1.修改菜品基本信息。2.删除口味。3.重新插入口味(采用上面的批量插入:saveWithFlavor方法中)。
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//修改菜品基本信息
dishMapper.update(dish);
//删除原本口味
dishFlavoerMapper.deleteByDishId(dish.getId());
//重新插入新口味
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors!=null&&flavors.size()>0){
flavors.forEach(dishFlavor->{
dishFlavor.setDishId(dishDTO.getId());
});
dishFlavoerMapper.insertBatch(flavors);
}
}
插入的时候需要给Flavor中的dish_id赋值。这个功能用到Mapper都是以前写的。
菜品起售禁售
采用post接口,id用Query传,status用path传
1.dishController
@PostMapping("/{status}")
public Result sale(@PathVariable Integer status,Long dishId){
dishService.updateSale(status,dishId);
return Result.success();
}
dishServiceimpl
public void updateSale(Integer status,Long dishId) {
Dish dish = Dish.builder()
.status(status)
.id(dishId)
.build();
dishMapper.update(dish);
}
DishMapper.xml代码展开
<update id="update">
update dish
<set>
<if test="name != null">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>
套餐操作
新增套餐
根据需求我们需要设计两个接口1.根据分类Id查询菜品{Get;Query}。2.新增套餐(和新增菜品差不多){Post;json},Query数据接收的时候不用注解,Path参数接收需要注解,因为在套餐中包含菜品,我们需要准备好SetmealDish实体类,还有SetmealDishMapper及xml,方便我们在setmealService中调用setmeal_dish数据库。
根据Id查询菜品,在套餐添加菜品时前端会发送此请求
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}
public List<Dish> list(Long categoryId) {
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}
<select id="list" resultType="Dish" parameterType="Dish">
select * from dish
<where>
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
order by create_time desc
</select>
套餐新增
public class SetmealController {
@Autowired
private SetmealService setmealService;
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
//向套餐表插入数据
setmealMapper.insert(setmeal);
//获取生成的套餐id
Long setmealId = setmeal.getId();
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
正常写SetmealMapper和xml记得返回主键
SetmealDishMapper.xml
<insert id="insertBatch" parameterType="list">
insert into setmeal_dish
(setmeal_id,dish_id,name,price,copies)
values
<foreach collection="setmealDishes" item="sd" separator=",">
(#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
</foreach>
</insert>
修改,删除,分页查询可以参考菜品操作
分页查询setmealMapper.xml代码展开
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select
s.*,c.name categoryName
from
setmeal s
left join
category c
on
s.category_id = c.id
<where>
<if test="name != null">
and s.name like concat('%',#{name},'%')
</if>
<if test="status != null">
and s.status = #{status}
</if>
<if test="categoryId != null">
and s.category_id = #{categoryId}
</if>
</where>
order by s.create_time desc
</select>
起售和停售
SetmealController
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}
SetmealService
void startOrStop(Integer status, Long id);
SetmealServiceImpl
public void startOrStop(Integer status, Long id) {
//起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
if(status == StatusConstant.ENABLE){
//select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
List<Dish> dishList = dishMapper.getBySetmealId(id);
if(dishList != null && dishList.size() > 0){
dishList.forEach(dish -> {
if(StatusConstant.DISABLE == dish.getStatus()){
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
}
});
}
}
Setmeal setmeal = Setmeal.builder()
.id(id)
.status(status)
.build();
setmealMapper.update(setmeal);
}
DishMapper
@Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
List<Dish> getBySetmealId(Long setmealId);
到这里可以看一下:《Redis快速入门》
店铺营业状态设置
需求分析
接口设计:需要查询状态(GET)的接口和修改状态(PUT)的接口。这里我们需要设计两个查询的接口一个用户端和一个管理端。在前面管理端用/admin做前缀,用户端用/user做前缀。
1.设置营业状态。
2.查询
管理端和用户端就一个前缀不同。
现在我们想把这个status存到哪里呢,我们创建一个表来存储的话,一张表就这一个数据显然太浪费了。我们可以将这个status用Redis的字符串来存储。
代码实现
代码也是非常的简单,就写一个ShopController就行了。
@Autowired
private RedisTemplate redisTemplate;
@PutMapping("/{status}")
public Result setStatus(@PathVariable Integer status){
log.info("设置店铺营业状态为{}",status == 1 ? "营业中" : "打烊中");
redisTemplate.opsForValue().set("SHOP_STATUS",status);
return Result.success();
}
@GetMapping("/status")
public Result<Integer> getstatus(){
Integer status = (Integer)redisTemplate.opsForValue().get("SHOP_STATUS");
log.info("获取到店铺的营业状态:{}",status == 1 ? "营业中" : "打烊中");
return null;
}
需要注意一下我们用什么数据传给Redis,取用的时候需要强转回原本的数据类型
对于用户端我们在controller中创建一个新的包,需要注意的是在admin和user中的Conller不能是同名,这些Cotroller都会放到spring容器中,如果重名的话,创建的bean也会重名就会报错,我们可以在Controller类中指定Bean的名称来避免这个问题(RestController指定名称。)
