SpringBoot+Vue+Mysql苍穹外卖
一.项目介绍
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
@Bean
public 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 后期改为当前用户的id
employee.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);
@ExceptionHandler
public 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);
@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);
}
<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">
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>
修改套餐
@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表,执行delete
setmealDishMapper.deleteBySetmealId(setmealId);
List<SetmealDish> setmealDishes = setmealdto.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
setmealDishMapper.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);