苍穹外卖技术点
完善登录功能
1 员工表中的密码是明文存储,安全性太低
解决办法
- 对前端提交的明文密码进行MD5加密后再与数据库中的密码进行对比。
- 利用SPring框架自带的工具类DigestUtils
password= DigestUtils.md5DigestAsHex(password.getBytes());
2 新增员工时,若录入的用户名已存在,抛出异常没有处理。
分析原因和解决方法
- 创建员工表时,规定用户名是唯一的。
- 若录入的用户名已存在,则会抛异常
- 通过全局异常处理器来统一捕获此问题引发的SQL异常:SQLIntegrityConstraintViolationException
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
//Duplicate entry 'lisi' for key 'employee.idx_username'
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username=split[2];
String msg=username+ MessageConstant.ALREADY_EXIST;
return Result.error(msg);
}else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
3 如何通过某种方式动态获取当前登录用户的id

解决方案
- 使用ThreadLocal存储登陆用户的id
- 封装ThreadLocal为一个工具类,利用get和set方法存值和取值
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
4 后端给前端响应的数据格式存在问题(日期格式)

注意:常用方式二
5 删除表数据时,先判断要删除数据是否关联了其他表数据(逻辑外键关系导致)。
原因分析及解决方案
- 对于分类表,它关联了套餐表和菜品表。关系为:一个菜品分类可能包含多个菜品,一个套餐分类包含多个套餐。具有一对多关系,分类表是1方,而菜品表和套餐表是多的一方。在多的一方存有分类表的外键,以此建立逻辑外键关系。
- 所以在删除某条分类数据时,要先判断该条分类所关联的菜品和套餐是否存在。
- 若存在,则抛出业务异常(当下分类下有菜品/套餐),若不存在,则执行删除操作
通用注意事项
1 在修改表数据时,需要更新update_Time,update_User字段。
业务表中的公共字段
出现的问题: 代码冗余,不便于后期维护
1 create_Time,create_User,update_Time,update_Time,
解决办法
- 自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法。
/**
* 自定义注解,用于标识某个方法需要进行功能字段填充处理
*/
//表示该注解只能加在方法上
@Target(ElementType.METHOD)
//指定注解的保留时间
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//指定一个属性,用于指定当前数据库操作类型: UPDATE INSERT
//可以通过枚举方式指定
OperationType value();
}
- 自定义切面类AutoFillAspect,同意拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点:对哪些类的哪些方法进行拦截
*/
//切入点表达式(对加有自定义AutoFill注解的com.sky.mapper下所有类的所有方法进行拦截)
@Pointcut("execution(* com.sky.mapper.*.*(..))&& @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {
}
/**
* 前置通知,在通知中进行公共字段填充
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行进行公共字段填充。。。");
//获取当前被拦截方法上数据库操作类型
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();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if (operationType == 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 (operationType == 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();
}
}
}
}
- 在mapper方法上加上AutoFill注解
/**
* 修改分类
* @param category
*/
@AutoFill(value = OperationType.UPDATE)
void update(Category category);
技术点:枚举、注解、AOP、反射
枚举:
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
事务注解
时机:对于一个业务逻辑,需要涉及多张表的操作,则需在当前业务逻辑方法上加上事务注解,保证原子性。
准备:在启动类上声明@EnableTransactionManagement ,以开启注解方式的事务管理
例子:添加菜品数据时,既要操作菜品表,也要操作口味表。(向菜品表插入1条数据,向口味表插入多条数据)
删除菜品
业务规则
1 可以一次删除一个菜品,也可以批量删除菜品
2 起售中的菜品不能删除
3 被套餐关联的菜品不能删除
4 删除菜品后,关联的口味数据也需要删除掉
注意
- 在后端controller层接受前端发送的请求参数ids时,可用如下注解@RequestParam进行参数解析,并将前端字符串ids以逗号分隔封装成list集合
/**
*批量删除菜品
*/
@DeleteMapping
@ApiOperation("批量删除菜品")
public Result delete(@RequestParam List<Long> ids){
log.info("批量删除菜品:{}",ids);
dishService.delete(ids);
return Result.success();
}
- 加事务注解
菜品起售、禁售状态更改
注意:
如果是停售操作,还需要将包含当前菜品的套餐也停售
遇到的问题分析总结:在书写多表连接的SQL语句时,表示每个字段时不要漏掉别名.
重点:
若对多表添加了别名,则对每个字段的表示,需要添上相应的表名别名,否则会因多个表拥有相同字段而查询失败。只有在每个字段上标注表名别名,才能具体指代是哪张表的哪个字段。
示例
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select s.*,c.name as categoryName from setmeal s left outer 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>
Redis缓存注意点
- 当处理管理端发送的增删改请求时,如果只对数据库进行相应的增删改操作,则会导致数据库和缓存数据不一致的问题。
解决办法: 当处理管理端发送的增删改请求时,要同时删除缓存中的数据。

浙公网安备 33010602011771号