一.项目介绍
1.项目内容
苍穹外卖是一款为大学学子设计的校园外卖服务软件,旨在提供便捷的食堂外卖送至宿舍的服务。该软件包含系统管理后台和用户端(微信小程序)两部分,支持在线浏览菜品、添加购物车、下单等功能,并由学生兼职提供跑腿送餐服务。
2.技术栈
SpringBoot+Vue+Mybatis+Mysql+Redis+Nginx
3.Nginx
网页-->nginx-->服务器
nginx反向代理优势:
1.提高访问速度(nginx可以做缓存)
2.进行负载均衡(将大量请求均匀分发请求)
3.保证后端服务的安全
# 反向代理,处理管理端发送的请求location /api/ {proxy_pass http://localhost:8080/admin/;#proxy_pass http://webservers/admin/;}# 反向代理,处理用户端发送的请求location /user/ {proxy_pass http://webservers/user/;}
4.Swagger
@Beanpublic Docket docket() {log.info("准备生成接口文档");ApiInfo apiInfo = new ApiInfoBuilder().title("苍穹外卖项目接口文档").version("2.0").description("苍穹外卖项目接口文档").build();Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo).select().apis(RequestHandlerSelectors.basePackage("com.sky.controller")).paths(PathSelectors.any()).build();return docket;}
常用注解
二.具体实现
一.登录功能
用户注册,输入密码-->对密码进行md5加密进行存储-->用户进行登录,密文解码进行比对
@PostMapping("/login")public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {log.info("员工登录:{}", employeeLoginDTO);Employee employee = employeeService.login(employeeLoginDTO);//登录成功后,生成jwt令牌Map<String, Object> claims = new HashMap<>();claims.put(JwtClaimsConstant.EMP_ID, employee.getId());String token = JwtUtil.createJWT(jwtProperties.getAdminSecretKey(),jwtProperties.getAdminTtl(),claims);EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder().id(employee.getId()).userName(employee.getUsername()).name(employee.getName()).token(token).build();return Result.success(employeeLoginVO);}public Employee login(EmployeeLoginDTO employeeLoginDTO) {String username = employeeLoginDTO.getUsername();String password = employeeLoginDTO.getPassword();//1、根据用户名查询数据库中的数据Employee employee = employeeMapper.getByUsername(username);//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)if (employee == null) {//账号不存在throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);}//密码比对,先进行md5加密再进行密码比较password = DigestUtils.md5DigestAsHex(password.getBytes());if (!password.equals(employee.getPassword())) {//密码错误throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);}if (employee.getStatus() == StatusConstant.DISABLE) {//账号被锁定throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);}//3、返回实体对象return employee;}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断当前拦截到的是Controller的方法还是其他资源if (!(handler instanceof HandlerMethod)) {//当前拦截到的不是动态方法,直接放行return true;}//1、从请求头中获取令牌String token = request.getHeader(jwtProperties.getAdminTokenName());//2、校验令牌try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());log.info("当前员工id:", empId);BaseContext.setCurrentId(empId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
二.公共字段自动填充
自定义注解AutoFill,用于标识需要公共字段自定义填充的方法
自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
在Mapper上加入AutoFill注解public enum OperationType {UPDATE,INSERT
}@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)")public void AutoFillCut() {}/*** 前置通知,为公共字段进行赋值*/@Before("AutoFillCut()")public void AutoFill(JoinPoint joinPoint) throws Exception {log.info("AutoFill start");//获取当前数据库操作的类型MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();AutoFill autoFill = methodSignature.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) {Method setCrateTime= entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setCrateUser=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);setCrateTime.invoke(entity,now);setCrateUser.invoke(entity,currentId);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} else if (operationType == OperationType.UPDATE) {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);}}
}
三.员工管理
新增员工
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO) {log.info("新增员工{}",employeeDTO);employeeService.save(employeeDTO);return Result.success();
}void save(EmployeeDTO employeeDTO);
public void save(EmployeeDTO employeeDTO) {Employee employee = new Employee();//对象属性拷贝BeanUtils.copyProperties(employeeDTO,employee);employee.setStatus(StatusConstant.ENABLE);employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());//设置当前记录人的id//TODO 后期改为当前用户的idemployee.setCreateUser(10L);employee.setUpdateUser(10L);employeeMapper.insert(employee);}@Insert("INSERT INTO employee(name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user, status) " +"VALUES (#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})")void insert(Employee employee);@ExceptionHandlerpublic Result exceptionHandler(SQLIntegrityConstraintViolationException ex){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);}}ThreadLocal
它为每个线程提供了一个独立的变量副本,使得每个线程可以独立地访问和修改自己的变量副本
一个请求一个线程
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();}
}
拦截器
BaseContext.setCurrentId(empId);
//新增员工时设置当前记录人的id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
查询员工
PageHelper 是一个基于 MyBatis 的分页插件,用于简化分页查询的实现。
它通过 MyBatis 的拦截器机制,自动在 SQL 查询中添加分页逻辑.@GetMapping("/page")@ApiOperation("员工分页查询")public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {log.info("查询员工{}",employeePageQueryDTO);PageResult pageResult = employeeService.page(employeePageQueryDTO);return Result.success(pageResult);}PageResult page(EmployeePageQueryDTO employeePageQueryDTO);
@Override
public PageResult page(EmployeePageQueryDTO employeePageQueryDTO) {//开始分页查询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);}Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<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>
编辑员工
@PutMapping@ApiOperation("修改员工信息")public Result update(@RequestBody EmployeeDTO employeedao) {employeeService.update(employeedao);return Result.success();}void update(EmployeeDTO employee);
@Override
public void update(EmployeeDTO employeedao) {Employee employee = new Employee();BeanUtils.copyProperties(employeedao,employee);employee.setUpdateTime(LocalDateTime.now());employee.setUpdateUser(BaseContext.getCurrentId());employeeMapper.update(employee);
}<update id="update" parameterType="Employee">update employee<set><if test="name!=null">name = #{name},</if><if test="username!=null">username = #{username},</if><if test="password!=null">password = #{password},</if><if test="phone!=null">phone = #{phone},</if><if test="sex!=null">sex = #{sex},</if><if test="idNumber!=null">id_number = #{idNumber},</if><if test="updateTime!=null">update_time = #{updateTime},</if><if test="updateUser!=null">update_user = #{updateUser},</if><if test="status!=null">status = #{status}, </if></set>where id = #{id}</update>
四.菜品管理
新增菜品
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishdao) {log.info("新增菜品{}",dishdao);dishService.saveWithFlavor(dishdao);return Result.success();
}public void saveWithFlavor(DishDTO dishdao);
@PostMapping
@Override
@Transactional
public void saveWithFlavor(DishDTO dishdao) {//向菜品表插入1条数据Dish dish = new Dish();BeanUtils.copyProperties(dishdao, dish);dishMapper.insert(dish);//向口味表插入n条数据//获取insert语句的主键值long dish_id = dish.getId();List<DishFlavor> flavors=dishdao.getFlavors();if (flavors!=null&&flavors.size()>0){flavors.forEach(dishFlavor -> dishFlavor.setDishId(dish_id));dishFloarMapper.insertBatch(flavors);}
}@AutoFill(OperationType.INSERT)
void insert(Dish dish);
<insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into dish(name,category_id,price,image,description,status,create_time,update_time,create_user,update_user)values(#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>void insertBatch(List<DishFlavor> flavors);
<insert id="insertBatch">insert into dish_flavor(dish_id, name, value)values<foreach collection="flavors" item="df" separator=",">(#{df.dishId}, #{df.name}, JSON_ARRAY(#{df.value}))</foreach>
</insert>
查询菜品
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> GetDish(DishPageQueryDTO dto){log.info("菜品查询");PageResult list =dishService.getDish(dto);return Result.success(list);
}PageResult getDish(DishPageQueryDTO dto);
public PageResult getDish(DishPageQueryDTO dto) {PageHelper.startPage(dto.getPage(),dto.getPageSize());Page<DishVO> page = dishMapper.pageQuery(dto);return new PageResult(page.getTotal(),page.getResult());
}Page<DishVO> pageQuery(DishPageQueryDTO dto);
<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 = #{category_id}</if><if test="status!=null">and d.status = #{status}</if></where>order by d.create_time desc
</select>
删除菜品
@DeleteMapping()
@ApiOperation("删除菜品")public Result delete(@RequestParam List<Long> ids) {log.info("删除菜品{}",ids);dishService.delete(ids);return Result.success();
}@Transactional
@Override
public void delete(List<Long> ids) {//起售中的菜品不能删除for (Long id : ids) {Dish dish = dishMapper.geibyid(id);if (dish.getStatus() == StatusConstant.ENABLE){throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);}}//被套餐关联的菜品不能删除List<Long> SetmealIds = setmealDishMapper.getSetmealDishIdsBydishlId(ids);if (SetmealIds!=null&&SetmealIds.size()>0){throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);}/*可以删除一个菜品,也可以删除多个菜品for (Long id : ids) {dishMapper.delete(id);//删除菜品后,关联的口味也需要删除dishFloarMapper.delete(id);}*///优化,根据菜品id集合批量删除dishMapper.deletes(ids);dishFloarMapper.deletes(ids);
}void deletes(List<Long> ids);
<delete id="deletes">delete from dish where id in<foreach collection="ids" open="(" close=")" separator="," item="id">#{id}</foreach></delete>
修改菜品
@PutMapping@ApiOperation("修改菜品")public Result update(@RequestBody DishDTO dishdao) {dishService.update(dishdao);return Result.success();
}void update(DishDTO dishdao);
public void update(DishDTO dishdao) {//修改菜品表Dish dish = new Dish();BeanUtils.copyProperties(dishdao, dish);dishMapper.updatedish(dish);//修改口味表,先删除所有口味,在插入传过来的口味dishFloarMapper.delete(dishdao .getId());List<DishFlavor> flavors=dishdao.getFlavors();if (flavors!=null&&flavors.size()>0){flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dish.getId());});flavors.forEach(dishFlavor -> dishFlavor.setValue(dishFlavor.getValue().toString()));dishFloarMapper.insertBatch(flavors);}
}
五.套餐管理
新增套餐
@PostMapping@ApiOperation("新增套餐")public Result save(@RequestBody SetmealDTO setmealDTO) {setmealService.saveWithDish(setmealDTO);return Result.success();
}void saveWithDish(SetmealDTO setmealDTO);
@Transactionalpublic void saveWithDish(SetmealDTO setmealDTO) {Setmeal setmeal = new Setmeal();BeanUtils.copyProperties(setmealDTO, setmeal);//向套餐表插入数据setmealMapper.insert(setmeal);//获取生成的套餐idLong setmealId = setmeal.getId();List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();setmealDishes.forEach(setmealDish -> {setmealDish.setSetmealId(setmealId);});//保存套餐和菜品的关联关系setmealDishMapper.insertBatch(setmealDishes);
}<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">insert into setmeal(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},#{createUser}, #{updateUser})
</insert>
<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>
查询套餐
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);return Result.success(pageResult);
}PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {int pageNum = setmealPageQueryDTO.getPage();int pageSize = setmealPageQueryDTO.getPageSize();PageHelper.startPage(pageNum, pageSize);Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);return new PageResult(page.getTotal(), page.getResult());
}Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">selects.*,c.name categoryNamefromsetmeal sleft joincategory cons.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>
修改套餐
@PutMapping@ApiOperation("修改套餐")public Result updateSetmeal(@RequestBody SetmealDTO setmealdto){log.info("修改套餐{}",setmealdto);setmealService.updateSetmeal(setmealdto);return Result.success();
}void updateSetmeal(SetmealDTO setmealdto);
public void updateSetmeal(SetmealDTO setmealdto) {Setmeal setmeal = new Setmeal();BeanUtils.copyProperties(setmealdto, setmeal);//修改套餐信息setmealMapper.updateSetmeal(setmeal);//修改对应的套餐菜品Long setmealId = setmeal.getId();//删除套餐和菜品的关联关系,操作setmeal_dish表,执行deletesetmealDishMapper.deleteBySetmealId(setmealId);List<SetmealDish> setmealDishes = setmealdto.getSetmealDishes();setmealDishes.forEach(setmealDish -> {setmealDish.setSetmealId(setmealId);});//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insertsetmealDishMapper.insertBatch(setmealDishes);}<update id="updateSetmeal">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="description!=null">description = #{description}, </if><if test="image!=null">image = #{image},</if><if test="status!=null">status = #{status},</if></set>where id = #{id}
</update>
删除套餐
@DeleteMapping@ApiOperation("批量删除套餐")public Result delete(@RequestParam List<Long> ids){setmealService.deleteBatch(ids);return Result.success();
}void deleteBatch(List<Long> ids);
public void deleteBatch(List<Long> ids) {//起售中的套餐无法删除for (Long id : ids) {Setmeal setmeal = setmealMapper.getsetmealbyid(id);if(setmeal.getStatus()== StatusConstant.ENABLE){throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);}}for (Long id : ids) {//删除套餐表中的数据setmealMapper.deleteById(id);//删除套餐菜品关系表中的数据setmealDishMapper.deleteBySetmealId(id);}
}@Delete("delete from setmeal where id = #{id}")
void deleteById(Long id);