一、增删改(员工、菜品、套餐,以员工信息为例)
1、增加
(1)接口设计
请求参数:使用Post请求,JSON数据格式。使用具有员工信息的DTO对象接收。 @RequestBody EmployeeDTO employeeDTO
返回值:返回状态码即可。
DTO设计示例
@Data
public class EmployeeDTO implements Serializable {
//编号
private Long id;
//用户名
private String username;
//姓名
private String name;
//电话号码
private String phone;
//性别
private String sex;
//员工id
private String idNumber;
}
补充:@Data注解的作用是自动生成以下方法:①所有字段的 get 和 set 方法②toString() 方法,用于将对象转换成字符串形式,便于日志输出和调试。③hashCode() 方法,用于计算对象的哈希值。④equals() 方法,用于判断两个对象是否相等。⑤会生成一个无参构造方法
(2)数据库设计
(3)代码实现:
Controller:接收请求与数据返回,调用业务层的方法。
Service:实现业务功能。①将DTO对象转换为实体类对象②进行属性拷贝
③进行相关属性设置、默认值设置④进行异常处理⑤调用mapper方法
⑥返回值的处理
Mapper:与数据库进行交互。Sql语句的编写Insert
(4)Sql语句
普通插入代码示例(注意数据表中的命名方式和属性小驼峰的命名)
<insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
insert into orders (number,status,user_id,address_book_id,order_time,checkout_time,
pay_method,pay_status,amount,remark,phone,address,consignee,
estimated_delivery_time,delivery_status,pack_amount,
tableware_number,tableware_status)
values
(#{number},#{status},#{userId},#{addressBookId},#{orderTime},
#{checkoutTime},#{payMethod},#{payStatus}, #{amount},#{remark},
#{phone},#{address},#{consignee},#{estimatedDeliveryTime},
#{deliveryStatus}, #{packAmount},#{tablewareNumber},
#{tablewareStatus})
</insert>
批量插入代码示例
<insert id="insertBatch">
insert into order_detail (name,image,order_id,dish_id,setmeal_id,
dish_flavor,number,amount)
values
<foreach collection="orderDetailList" item="od" separator=",">
(#{od.name},#{od.image},#{od.orderId},#{od.dishId},
#{od.setmealId},#{od.dishFlavor},#{od.number},#{od.amount})
</foreach>
</insert>
2、删除
(1) 接口设计
请求参数:使用Delete请求。通过地址栏完成传参
@RequestParam List<Long> ids
返回值:返回状态码即可。
(2)代码实现:
Controller:接收请求与数据返回,调用业务层的方法。
Service:实现业务功能。①查询当前数据是否存在②判断当前数据是否具备可删除条件
③不具备删除条件可抛出删除异常信息④调用mapper方法删除
Mapper:与数据库进行交互。Sql语句的编写Delete
(3)Sql语句
批量删除
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</delete>
3、修改
根据id查询当前数据用于回显
(1) 接口设计
请求参数:使用GET请求。路径参数
@PathVariable Long id
返回值:返回状态码、Data(VO对象)
(2)代码实现
Controller:接收请求与数据返回,调用业务层的方法。
Service:实现业务功能。①查询当前数据,调用mapper方法②判断返回数据是否为空
③为空抛出异常④不为空封装返回结果
Mapper:与数据库进行交互。Sql语句的编写Select
编辑修改数据
(1) 接口设计
请求参数:使用PUT请求。JSON格式,封装为DTO对象
返回值:返回状态码即可
(2)代码实现
Controller:接收请求与数据返回,调用业务层的方法。
Service:实现业务功能。①将DTO对象转换为实体类②调用mapper方法进行更新数据
Mapper:与数据库进行交互。Sql语句的编写Update(可写为动态更新提升代码复用率)
(3)Sql语句
<update id="update">
update setmeal
<set>
<if test="categoryId!=null"> category_id=#{categoryId},</if>
<if test="name!=null"> name=#{name},</if>
<if test="price!=null"> price=#{price},</if>
<if test="image!=null"> image=#{image},</if>
<if test="description!=null"> description=#{description},</if>
<if test="status!=null"> status=#{status},</if>
<if test="updateTime!=null"> update_time=#{updateTime},</if>
<if test="updateUser!=null"> update_user=#{updateUser},</if>
</set>
where id =#{id}
</update>
二、分页查询
(1) 接口设计
请求参数:使用GET请求,使用Query查询参数(地址栏?)用DTO对象接收
EmployeePageQueryDTO employeePageQueryDTO(第几页、每页个数、其它参数)
返回值:返回状态码、data(total数据总数,records集合;封装成PageResult)
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
补充:@AllArgsConstructor包含所有参数的构造器
@NoArgsConstructor无参构造器(直接翻译)
(2)代码实现:
Controller:接收请求与数据返回,调用业务层的方法。返回Result<PageResult>
Service:基于PageHelper的分页查询
/**
* 分页查询方法
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询(自动进行分页)
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
//手动进行name的拼接工作
Page<Employee> page=employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total,records);
}
Mapper:与数据库进行交互。Sql语句的编写,完成分页查询Select
(3)Sql语句编写
上面代码中员工分页查询代码示例 employeeMapper.pageQuery()
<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>
较复杂的订单查询代码示例:
<select id="page" resultType="com.sky.entity.Orders"
parameterType="com.sky.dto.OrdersPageQueryDTO">
select * from orders
<where>
<if test="number != null and number!=''">
and number like concat('%',#{number},'%')
</if>
<if test="phone != null and phone!=''">
and phone like concat('%',#{phone},'%')
</if>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="beginTime != null">
and order_time >#{beginTime}
</if>
<if test="endTime != null">
and order_time < #{endTime}
</if>
</where>
order by order_time desc
</select>
(4)PageHelper插件
基于MyBatis的拦截器实现,进行动态sql拼接
PageHelper和pageQuery的关系,怎么进行连接:PageHelper底层实现了ThreadLocal,存储相关数据,在pageQuery的Sql语句中再取出来,拼进Sql语句中并返回。
三、自动填充字段(自定义注解、AOP设计思想)
1、应用需求
在员工、菜品、套餐增加和更新时,都需要编辑创建时间、创建人、修改时间、修改人信息
2、自定义注解AutoFill
用于表示需要进行公共字段自动填充的方法
/**
* 自定义注解,用于标识某个方法需要进行功能字段的自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据操作类型,UPDATE INSERT 内部有一个属性
OperationType value();
}
补充:@Target里面的标记,用于标识该注解在什么位置使用。常见的自定义注解有四种:注解用在类上面、注解用在方法上面、注解用在属性上面、注解用在参数上面,也可以写多个,比如:@Target({ElementType.METHOD,ElementType.FIELD})
@Retention注解标识注解的生命周期:RetentionPolicy.SOURCE 源文件 RetentionPolicy.CLASS类文件(默认)RetentionPolicy.RUNTIME 运行时也存在
3、自定义切面类AutoFillAspect
统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) &&@annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段补充
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");
//获取当前被拦截方法上的数据库操作类型 INSERT/UPDTE
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;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime localTime = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据类型通过反射进行赋值
if(value==OperationType.INSERT){
try {
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.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,localTime);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,localTime);
setUpdateUser.invoke(entity,currentId);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException 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,localTime);
setUpdateUser.invoke(entity,currentId);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
代码解读:
4、在Mapper方法上加上对应的注解
/**
* 根据主键动态修改属性
* @param employee
*/
@AutoFill(value=OperationType.UPDATE)
void update(Employee employee);
5、AOP核心概念补充
①连接点(JointPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
②切入点(Pointcut):匹配连接点的式子。在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
③通知(Advice):在切入点执行的操作,也就是共性功能
④通知类:定义通知的类
⑤切面(Aspect):描述通知与切入点的对应关系
四、TnreadLocal获取当前用户id
1、用户登录流程及问题分析
在用户认证后发送请求携带的Token中,可以解析出来用户id
问题:在拦截器中没有直接调用Service方法。如何将解析出来的id传递至Service的save方法。
2、ThreadLocal
ThreadLocal是线程的一个局部变量。为每个线程提供单独一份存储空间,具有线程隔离的效果。只有在线程内才能取到对应的值,在线程外是无法访问的。
由于客户端每发的一次请求都是一次线程,可以通过Thread.currentThread().getId()来验证。因此可以通过ThreadLocal存值取值的方式实现不同类不同方法之间的变量的动态获取。
3、ThreadLocal常用方法
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量
补充:项目中使用了工具类进行封装该方法。
五、基于阿里云OSS的文件上传与获取
实现步骤:
- 配置alioss的属性
- 开始上传阿里云上传的工具类对象(在配置类中创建)
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
补充:ConditionalOnMissingBean是保证容器中只有一个阿里云工具类对象。
-
构造新的文件名称(UUID+原始文件后缀),通过aliOssUtil调用upload方法上传文件保存至云盘
-
返回路径
@RestController
@RequestMapping("/admin/common")
@Api(tags ="通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
//使用配置类里面创建对象的方式 创建了一个Bean
/**
* 文件上传
* @param file
* @return
* 此处MultipartFile file的file必须和接口上传的参数保持一致
*/
@ApiOperation("文件上传")
@PostMapping("/upload")
public Result<String> upload(MultipartFile file ){
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀
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);
}
}
@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();
}
}