1.公共字段自动填充
问题
有些字段,如 : create_time,create_user , update_time , update_user是公共的,每次赋值都要重新编写代码,会造成代码冗余 ;
序号 | 字段名 | 含义 | 数据类型 |
1 | create_time | 创建时间 | datetime |
2 | create_user | 创建人id | bigint |
3 | update_time | 修改时间 | datetime |
4 | update_user | 修改人id | bigint |
实现:
自定义注解: AutoFill,用于表示需要进行公共自读那自动填充的方法
自定义切面类,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值。
1 创建枚举类
在common下定义。需要加上注解的数据库操作类型
2.自定义注解AutoFill
3.自定义拦截器
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面用来填充,创建人,创建时间.....等字段男人
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
//使用该注解进行指定拦截
@Pointcut("execution(* com.sky.service.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {
}
//前置通知,在通知中为公共字段赋值
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行字段字段填充");
//获取到当前被拦截方法上的数据库操作类型
MethodSignature signature = (MethodSignature)joinPoint.getSignature();//方法签名对象
AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);
OperationType value = annotation.value();//获得数据库操作类型
//获取当前被拦截方法的参数
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0){
return;
}
//实体类为第一位的时候,把实体类中的数据赋值到entity
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型,进行不同的字段赋值
if (value == OperationType.INSERT){
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
}catch (Exception e){
e.printStackTrace();
}
} else if (value == OperationType.UPDATE){
//为两个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4.在mapper对应发方法上加上注解
2.新增菜品
需求分析与设计
产品原型
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当 前菜品所属的菜品分类。并且需要上传菜品
新增菜品的原型图:
当填写完表单信息,点击"保存按钮后",会提交表单的数据到后台,后台需要接受数据,把数据保存到数据库中
业务规则:
菜品名称唯一
菜品必须属于某哥分类下,不能单独存在
新增菜品时可以根据情况选择菜品的口味
每一个菜品必须对应一张图片
2.1.2接口设计
根据上述原型图共包含3个接口
接口设计:
根据类型查询分类
文件上传
新增菜品
分析接口:
2.文件上传
3.新增菜品
2.1.3 表设计
通过原型图进行分析:
新增菜品: 就是把添加页面用户所输入的菜品信息添加到dish表中,如果添加了口味,还需要向dish_flavor表中插入数据,所以新增菜品的时候,涉及到两个表
表名 | 说明 |
dish | 菜品表 |
dish_flavor | 菜品口味表 |
1.菜品表
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 菜品名称 | 唯一 |
category_id | bigint | 分类id | 逻辑外键 |
price | decimal(10,2) | 菜品价格 | |
image | varchar(255) | 图片路径 | |
description | varchar(255) | 菜品描述 | |
status | int | 售卖状态 | 1起售 0停售 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
2.菜品口味表
字段名 | 数据类型 | 说明 | 备注 |
id | bigint | 主键 | 自增 |
dish_id | bigint | 菜品id | 逻辑外键 |
name | varchar(32) | 口味名称 | |
value | varchar(255) | 口味值 |
2.2 代码开发
2.2.1 文件上传实现
新增菜品时,需要上传菜品对应的文件,包括后续其他功能也会使用到文件上传,方便省事的情况下,直接写一个接口(百度搜索会有更优质的代码)
本项目选用阿里云的oss服务进行文件存储
实现步骤:
定义OSS相关配置
在sky-server模块
application-dev.yml
application.yml
注意:
在sky-commom下的properties中的AliOssProperties配置类中,把yml中配置的四个属性,封装了成了一个JAVA对象
yml中和对象中对于属性的书写不一样,如 : yml中的bucket-name,在AliOssProperties中就是bucketName,采取的是驼峰命名法,这里在spring框架中能够自动进行转换 ;在yml中习惯使用横线来分割单词;
2.读取OSS配置
在sky-common模块中,已经定义
上传文件到阿里云 OSS工具类
package com.sky.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
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();
}
}
然后,我详细解释一下这段代码中的关键点:
这段代码是一个用于上传文件到阿里云 OSS工具类。
public String upload(byte[] bytes, String objectName)
是用于文件上传的方法,接收一个字节数组和文件名作为参数。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
创建一个OSSClient实例,用于与阿里云 OSS服务进行通信。ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
调用OSSClient的putObject方法将文件上传到指定的存储桶中。ossClient.shutdown()
用于关闭OSSClient实例。StringBuilder stringBuilder = new StringBuilder("https://");
创建一个字符串构建器,并初始化为"https://"。stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);
将存储桶名称、OSS服务端点、以及文件名拼接成一个完整的访问路径。log.info("文件上传到:{}", stringBuilder.toString());
使用日志记录器打印文件上传的完整访问路径。return stringBuilder.toString();
返回文件的访问路径。- 这段代码的作用是通过阿里云的OSS服务将文件上传到指定的存储桶,并返回文件的访问路径
3.生成OSS工具类对象
在sky-server模块
4.定义文件上传接口
在sky-server模块中定义接口
package com.sky.controller.admin;
import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
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(MessageConstant.UPLOAD_FAILED);
}
}
2.2.2新增菜品实现
1.设计DTO类
在sky-pojo模块中
2.Controller层
进入到sky-server模块中
3.Service层次接口
4.Service实现类
注意:
@Transactional表示该方法的原子性,要么全部成功,要么全部失败
5.Mapper层
DishMapper中添加
DishMapper.xml
注意:
keyColumn: keyColumn指的是数据库表中自增主键的字段名
keyProperty:keyProperty指定在Java 实体类中对应的主键
useGeneratedKeys: 在mybatis的XML映射器中设置useGeneratedKeys=true,是为了在执行insert 、update操作后能获取操作成功后的插入对象的自增主键id
6.DishFlavorMapper
DishFlavorMapper实现类
2.3 功能测试
进入到菜品管理--->新建菜品
由于没有实现菜品查询功能,所以保存后,暂且在表中查看添加的数据。
3.菜品分类查询
1.需求分析
业务规则
根据页码展示菜品信息
每页展示10条数据
分页查询时可以根据需要输入菜品名称,菜品分类 ,菜品状态进行查询
接口设计
Path : /admin/dish/page
Method : GET
请求参数 :
page : 必须 : 页码
pageSize : 必须 : 每页记录数
name : 非必须 : 菜品名称
categoryId : 非必须 : 分类id
status : 非必须 : 菜品售卖状态
返回数据:
2.代码开发
根据菜品业务查询接口定义对应的Dto
2.设计VO
根据菜品分页查询接口定义设计对应的VO
在VO中扩展了categoryName属性
package com.sky.vo;
import com.sky.entity.DishFlavor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@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;
}
用于和前端交互
3.controller
4.Service层
5.impl实现类
注意:
这里使用分页插件PageHelper,原理是ThreadLocal
6.Mapper
7.Mapper.xml
注意:这里使用分页查询,用动态sql拼接可能为空的属性
4.删除菜品
1.需求设计与分析
产品原型
删除菜品:
业务规则
可以一次删除一个菜品,也可以批量删除菜品
起售中的菜品不能删除
被套餐关联的菜品不能删除
删除菜品后,关联的口味数据夜删除
接口设计
批量删除:可以只设计一个接口
Path : /admin/dish
Method : Delete
请求参数 : query
-
ids : 必须 : 菜品id,之间用,分割
返回数据 :
-
code : 必须
-
data : 非必须
-
msg : 非必须
2.代码开发
controller
service
impl
Mapper
Mapper.xml
优化:
-
在删除菜品和相关口味时,遍历的方式会多次访问数据库,这样造成性能的消耗,所以可以采用批量删除的操作来优化
-
// 根据菜品id集合批量删除菜品数据 // sql : delete from dish where id in (?,?,?) dishMapper.deleteByIds(ids);
// 根据菜品id集合批量删除口味数据 // sql : delete from dish_flavor where dish_id in (?,?,?) dishFlavorMapper.deleteByDishIds(ids);
DishMapper :
-
/** * 根据菜品id集合批量删除菜品数据 */ void deleteByIds(List<Long> ids);
DishFlavorMapper
/** * 根据菜品id集合批量删除关联的口味数据 * @param dishIds */ void deleteByDishIds(List<Long> dishIds);
DishMapper.xml :
-
<delete id="deleteByIds"> delete from dish where id in <foreach collection="ids" open="(" close=")" separator="," item="id"> #{id} </foreach> </delete>
DishFlavorMapper.xml :
-
<delete id="deleteByDishIds"> delete from dish_flavor where dish_id in <foreach collection="dishIds" open="(" close=")" separator="," item="dishId"> #{dishId} </foreach> </delete>
5.修改菜品
1.需求设计与分析
接口设计 :
-
根据id查询菜品(用于回显数据)
-
根据类型查询分类(已实现)
-
文件上传(已实现)
-
修改菜品
2.代码开发
1.controller层
根据id查询菜品的接口定义在DishController中
2.Service层接口
在DishService接口中声明getByIdWithFlavor方法:(根据id查询菜品对应的口味数据)
3.Service实现类
在DishServiceImpl中实现getByIdWithFlavor方法:
4.Mapper层
在DishFlavorMapper中声明getByDishId方法,并配置SQL:
5.2.1修改菜品实现类
1.Contriller层
根据修改菜品的接口定义在DishController中创建方法:
2.Service层实现类和Service层
在DishServiceImpl中实现updateWithFlavor方法:
4.Mapper层
在DishMapper中,声明update方法:
并在DishMapper.xml文件中编写SQL: