软件开发整体介绍
前端搭建
在非中文目录中双击nginx.exe然后浏览器访问localhost即可
后端搭建
基础准备
导入初始文件
使用git进行版本控制
创建本地仓库和远程仓库,提交Git
连接数据库
连接数据库把资料中的文件放入运行即可
前后端联调测试
Nginx反向代理
前端发送的请求,是如何请求到后端服务器的?
nginx反向代理,就是将前端发送的动态请求由nginx转发到后端服务器.
使用Nginx的好处:
- 提高访问速度
- 进行负载均衡
- 保证后端服务的安全
员工管理模块
新增员工
编写新增员工功能,需要注意密码进行默认加密,通过调用常量(避免硬编码)进行设置.前端所传的数据和POJO属性差别较大时,编写DTO进行数据封装
功能测试进行前后端联调测试,之前需要获取Jwt令牌
完善需要避免用户名重复 处理异常.
通过全局异常处理类进行处理,捕获到用户名重复的异常然后进行加工返回.
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'lans' for key 'idx_username'
String message=ex.getMessage();
if(message.contains("Duplicate entry")){
//创建数组 通过空格分隔成一个个对象
String[] split=message.split(" ");
//取出第三个元素 即username
String username=split[2];
//作为提示信息拼接
String msg=username+ MessageConstant.ALREADY_EXISTS;
return Result.success(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
处理异常.完善创建人id和修改人id
从携带的token令牌中解析获取其中的id然后放入ThreadLocal(客户端每一次发起的请求都是一个线程)存储空间,需要id时取出
员工分页查询
通过PageHelper插件实现分页查询的功能.
Query参数是一种在HTTP请求中用于传递额外信息的参数类型,它具有直观易懂、便于传递简单参数等特点。
- Body Parameters通常用于POST、PUT等请求中,以传递复杂的数据结构(如JSON、XML等)。
- Query Parameters则更适合传递简单的键值对参数。
请求参数是Query,他直接拼接在URL后面,通过DTO进行封装成EmployeePageQueryDTO,三个参数name(不一定有),page,pageSize.接收的参数类型就是EmployeePageQueryDTO.
name就需要用到模糊查询+分页查询的方式,需要用到动态SQL,不用注解来对数据库来操作,而是用到xml文件,另外返回结果是总记录数和当前页面数据的集合,通过再次封装一个返回类来作为返回值.
@Data //get set
@AllArgsConstructor //有参构造
@NoArgsConstructor //无参构造
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
那么Controller层返回的就是返回的是一个包含 PageResult
类型的 Result
对象/
service层利用PageHelper实现分页查询,只需要开启分页查询并传入page和pageSize两个参数即可.调用mapper层方法对数据库进行操作.剩下的就是对返回值的处理.和编写SQL语句.由于是动态查询,普通注解无法满足要求,通过xml文件进行配置动态sql.
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询 利用PageHelper
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
Page<Employee> page=employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> result = page.getResult();
return new PageResult(total,result);
}
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.EmployeeMapper">
<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>
</mapper>
前后端联调发现,最后操作时间格式不对,需进行代码完善
启用禁用员工账号
根据前端传过来的用户id修改用户账号状态.
采用@PostMapping("/status/{status}")和@PathVariable Integer status动态接收前端传入一个status
编写动态SQL update 可后续编辑员工再次使用该方法.
编辑员工
先根据id查询用户信息,用户点击修改按钮时执行此功能,然后进行修改.通过@RequestBody接收请求体中的数据通过反序列化封装在EmployeeDTO中进行操作
分类管理
基本与员工管理逻辑相同,不在赘述
菜品管理
公共字段自动填充
每次向这些表中插入字段的时候每次都要编写相同的代码,这样比较冗余而且后期不方便维护.
通过切面来统一进行处理公共字段,进行赋值.
首先进行自定义注解@AutoFill方便标识哪些方法需要进行自动字段填充.即insert和update方法.可通过枚举类来固定数据库操作类型
/**
* @author 刘宇
* 自定义注解,用于标识某个方法的功能字段需要进行自动填充
*/
@Target(ElementType.METHOD) //该注解用在方法上
@Retention(RetentionPolicy.RUNTIME) //运行时生效
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
然后定义一个切面类,通过拦截执行加入了该注解的方法,对拦截下的update和insert进行增强,实现自动填充字段
package com.sky.aspect;
import ...
/**
* @author 刘宇
* 自定义切面,实现公共字段自动填充
*/
@Component
@Aspect
@Slf4j
public class AutoFillAspect {
/*
切入点
对execution 这个包进行和添加了该注解的进行自动填充字段
拦截
*/
@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 setUpdateTime=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
Method setUpdateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
Method setCreateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER,Long.class);
//赋值
setCreateTime.invoke(entity,now);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
setCreateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}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) {
throw new RuntimeException(e);
}
}
}
}
新增菜品
新增菜品需要加入菜品图片,这就需要一个上传文件的功能.
需要使用到阿里云服务
然后新增菜品需要对两张表进行操作,即口味表和菜品表,
SpringBoot新增菜品模块开发(事务管理+批量插入+主键回填)
菜品分页
主要涉及到多表查询 因为每道菜有他自己的口味,但口味表和菜品表不在一个表中,故需要用到多表查询,而且一个菜品可能有多种口味也可以不设置口味,故还需要用到外连接去查所有的菜品表.然后会有查询条件,需要用到动态SQL,最后可以排个序
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*,c.name as categoryName from dish as d left outer join category as 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=#{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>
删除菜品
涉及到多表操作,需要进入一个事务注解 @Transactional//事务的一致性来保证事务的一致性 不能删除的可以通过异常抛出,定义自定义异常然后把常量放进去抛出
用户可批量或单个删除菜品.注意该菜品1.不能是在售状态 2.在售套餐中不能包含该菜品 3.该菜品关联口味也要删除.
业务层中需要进行如上判断,不能是在售可以根据前端传来的id进行判断.套餐中是否存在可通过查询表中是否有对应数据,不为空就说明关联了套餐.查询套餐需要用到一个动态SQL,通过foreach循环解析出所有需要删除菜品的id,然后进行查询是否在套餐表中,口味删除通过菜品id即可
/**
* 菜品删除
* @param ids
*/
@Override
public void deleteById(List<Long> ids) {
//判断当前菜品是否能够删除?是否正在起售中
for(Long id:ids){
Dish dish=dishMapper.getById(id);
if(dish.getStatus()== StatusConstant.ENABLE){
//起售中不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//是否套餐关联了
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds!=null&&setmealIds.size()>0){
//当前菜品已被关联不能删
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中的数据
for (Long id : ids) {
dishMapper.deleteById(ids);
//删除口味表中的数据
dishFlavorMapper.deleteByDishId(id);
}
}
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>
代码优化:每次删除单个菜品都需要操作一次数据库,如果操作大量数据就导致性能方面的问题,把单个删除变成多个删除
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</delete>
菜品的同理
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" separator="," item="dishId" open="(" close=")">
#{dishId}
</foreach>
</delete>
修改菜品
分为四个接口,先根据id来查询数据进行回显操作,根据类型查询菜品分类(用于修改分类),然后文件上传,修改菜品
@Override
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//修改基本信息
dishMapper.update(dish);
//删除所有的口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());
//重新插入口味数据
List<DishFlavor>flavors=dishDTO.getFlavors();
if(flavors!=null&&flavors.size()>0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
dishFlavorMapper.insertBatch(flavors);
}
}
店铺营业状态设置
营业状态数据的存储方式:基于Redis的字符串来进行存储.
1表示营业 0表示打样
管理端和用户端都应该能查询到店铺的营业状态,但用户端不能设置营业状态.
两者的请求路径不同.控制器名称若设置相同可以通过@RestController("adminShopController")和@RestController("userShopController")来区分
基于Redis就没有只有一层了即控制层
套餐管理
新增套餐
首先需要编写查询套餐分类的接口.
添加菜品时,结合产品原型来看,根据用户是否进行搜索和是否进行选择菜品分类来动态编写SQL.其中name部分需要进行模糊查询
由于文件上传部分已经完成,只需编写一个新增套餐的方法.
新增套餐时需要注意,要保证套餐和菜品的关联关系.
通过SQL自己生成的套餐id进行关联
<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>
- useGeneratedKeys:
-
- 这个属性用于指示MyBatis是否应该使用JDBC的getGeneratedKeys方法来获取数据库自动生成的主键值(例如,自增主键)。
- 当设置为
true
时,MyBatis会在执行插入操作后,通过JDBC的getGeneratedKeys方法获取数据库生成的主键值,并将其赋值给指定的属性。
- keyProperty:
-
- 这个属性用于指定哪个属性应该接收由数据库生成的主键值。
- 通常,这个属性应该对应你的Java对象(在这个例子中是
Setmeal
对象)中用于存储主键的字段名。 - 当
useGeneratedKeys
设置为true
时,MyBatis会将获取到的主键值设置到这个指定的属性中。
套餐分页查询
分页查询 连表查询 需要用到左外连接和动态SQL
删除套餐
起售中的套餐不能删除
修改套餐
查询套餐
修改:删除原有套餐 新增套餐 删除原有关联关系 新增关联关系
套餐起售停售
修改状态即可.注意起售套餐时,套餐中不能存在停售的菜品
商品浏览
查询菜品
根据分类id查询菜品
查询套餐
查询套餐相关联的菜品
缓存
缓存菜品
用户端小程序展示的菜品数据都是通过查询数据库获得的,当用户端访问量较大时,数据库访问压力也随之增大.而Redis数据库是通过内存来保存数据的,查询数据库本质上时磁盘IO操作,内存操作相对于磁盘操作性能高很多,可以通过Redis来缓存菜品数据,减少数据库查询操作.
缓存逻辑:
- 每个分类下的菜品保存一份缓存数据
- 数据库中的菜品数据有变更时清理缓存数据
改造查询方法,具体实现
/**
* 根据分类id查询菜品
* 利用Redis缓存数据
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//构建Redis中的key,规则 dish_分类id
String key="dish_"+categoryId;
//查询Redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if (list != null && list.size() > 0) {
//存在 直接返回 无需查询数据库
return Result.success(list);
}
//不存在 查询数据库
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
清理缓存数据
修改数据库后,要及时清理缓存,保证数据一致.
包括:起售停售菜品,修改菜品,删除操作菜品,新建菜品
通过Spring Cache框架 用注解进行缓存操作
@CacheEvict(cacheNames = "setmealCache",allEntries=true)清除名为setmealCache的缓存中的所有内容
@CacheEvict(cacheNames="setmealCache",key = "#setmealDTO.categoryId")精确根据传入的 setmealDTO
对象的 categoryId
属性值,从 setmealCache
缓存中移除对应的条目。
@Cacheable(cacheNames = "setmealCache",key = "#categoryId")
如果缓存 setmealCache
中已经存在以 categoryId
为键的数据,则直接返回该数据,否则执行该方法并将结果存入 setmealCache
缓存中,键为 categoryId
。
添加购物车
创建购物车表
用户端发送的请求会携带token令牌,拦截器中对令牌进行解析,并获得用户id,可通过ThreadLocal.getCurrentId获得userId.
前端所传过来的DTO包含三个动态参数 dishId setmealId dishFlavor
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);//动态SQL对三个字段进行
//判断当前加入到购物车中的商品是否已经存在了
if (list != null && list.size() > 0) {
ShoppingCart shoppingCart1 = list.get(0);
shoppingCart1.setNumber(shoppingCart.getNumber() + 1);//update
shoppingCartMapper.updateNumberById(shoppingCart1);
} else {//不存在
//菜品
Long dishId = shoppingCartDTO.getDishId();
if (dishId != null && dishId > 0) {
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
} else {
//dishId为null,setmealId就一定不为null 套餐
Long setmealId = shoppingCartDTO.getSetmealId();
Setmeal setmeal = setmealMapper.getById(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
//插入购物车表
shoppingCartMapper.insert(shoppingCart);
}
}
查看购物车
根据userId查询数据库即可
清空购物车
删除数据库中购物车内容,同样从ThreadLocal中获得userId
地址模块
基本的增删改查
注意设置默认地址时,把原本所有的地址都设为非默认,再把这个设为默认.
用户下单
1.需要进行业务异常处理,购物车为空,地址簿为空需要抛出相应异常
2.向订单表插入一条数据
3.向订单细节表插入n条数据
4.清空购物车
5.封装返回值
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
//处理各种业务异常 购物车数据为空 地址簿为空
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if(addressBook == null){
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//1.根据用户id查询购物车数据
ShoppingCart shoppingCart =new ShoppingCart();
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if(list==null||list.size()==0){
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//2.向订单表发送插入1条数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO,orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);//未支付
orders.setStatus(Orders.PENDING_PAYMENT);//待付款
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setUserId(userId);
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());//收货人
orderMapper.insert(orders);//返回主键值订单id给订单细节表
//3.向订单细节表插入n条数据
List<OrderDetail> orderDetailList = new ArrayList<>();
for (ShoppingCart cart : list) {
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(cart,orderDetail);
orderDetail.setOrderId(orders.getId());
orderDetailList.add(orderDetail);
}
orderDetailMapper.insertBatch(orderDetailList);
//4.清空当前用户的购物车数据
shoppingCartMapper.deleteById(userId);
//5.封装VO返回结果
OrderSubmitVO orderSubmitVO = new OrderSubmitVO();
orderSubmitVO.setId(orders.getId());
orderSubmitVO.setOrderTime(orders.getOrderTime());
orderSubmitVO.setOrderAmount(orders.getAmount());
orderSubmitVO.setOrderNumber(orders.getNumber());
return orderSubmitVO;
}
订单支付
???
C端订单接口
查询历史订单
是一个分页动态查询,根据前端动态传来的status等条件进行动态查询,由于会涉及到时间,如订单创建时间和订单结束时间来查询,在xml配置映射文件时会用到<>,注意转义字符的使用. >大于 < 小于
<if test="beginTime !=null">
and begin_time>=#{beginTime}
</if>
<if test="endTime!=null">
and end_time<=#{endTime}
</if>
查看订单详细
通过订单号查询订单详细然后将数据封装为VO返回
public OrderVO details(Long id) {
//根据订单id查询订单orders
Orders orders = orderMapper.getById(id);
//根据订单id查询订单详细信息
List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(id);
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders,orderVO);
orderVO.setOrderDetailList(orderDetails);
return orderVO;
}
用户取消订单
不能取消订单的情况:订单不能为空,订单状态必须为1待付款 或 2待接单
待付款情况下需要进行退款 然后更新订单状态,取消原因和取消时间
public void userCancelById(Long id) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
if (ordersDB.getStatus() > 2) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Orders orders = new Orders();
orders.setId(ordersDB.getId());
// 订单处于待接单状态下取消,需要进行退款
if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
//调用微信支付退款接口
weChatPayUtil.refund(
ordersDB.getNumber(), //商户订单号
ordersDB.getNumber(), //商户退款单号
new BigDecimal(0.01),//退款金额,单位 元
new BigDecimal(0.01));//原订单金额
//支付状态修改为 退款
orders.setPayStatus(Orders.REFUND);
}
// 更新订单状态、取消原因、取消时间
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("用户取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
再来一单
通过订单id查询订单详细信息,然后将订单的详细信息转化为购物车对象,将菜品信息复制进去然后将购物车对象批量添加到数据库中去
订单管理
搜索订单
管理端通过开始时间结束时间等条件进行查询,返回的数据包括
查看订单详细
根据订单id来查看订单详细信息的集合返回
统计各个订单数量
用到统计函数,sql:select count(*) from orders where status=#{status}
接单
将订单id和修改的订单状态封装进Orders然后修改即可
拒单
根据id查询订单,只有订单状态为待接单才能拒单,若用户已支付需要进行退款.需要设置退款原因,修改订单状态,更新取消时间
派送订单
根据id查询订单状态,处于已接单状态才可进行下步操作,修改状态 更新数据库
完成订单
根据id查询订单状态,处于派送中状态才可进行下步操作,修改状态 更新数据库
定时处理
用户下单后一直处于待支付状态,要进行一个定时处理
用户收到货后商家没有点击完成按钮,订单一直处于派送中,要进行一个定时处理
package com.sky.task;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author 刘宇
*/
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 处理超时订单
*/
@Scheduled(cron="0 * * * * ? ")
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
// 待付款状态 and orderTime < (当前时间-15min)
LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(-15);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, localDateTime);
if(ordersList!=null&&ordersList.size()>0){
for(Orders order:ordersList){
order.setStatus(Orders.CANCELLED);
order.setCancelReason("订单超时,自动取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
}
}
}
/**
* 处理一直处于派送中的订单
*/
@Scheduled(cron="0 0 1 * * *")
public void processDeliveryOrder(){
log.info("处理一直处于派送中的订单:{}", LocalDateTime.now());
//select * from orders where status=#{status}
LocalDateTime time=LocalDateTime.now().plusMinutes(-60);
List<Orders> orders = orderMapper.getDeliverying(Orders.DELIVERY_IN_PROGRESS,time);
if(orders!=null&&orders.size()>0){
for(Orders order:orders){
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
}
}
}
}
来单提醒
用户下单并支付后,系统需要通知商家,语音播报,弹出提示框.
// 通过websocket向客户端浏览器推送消息type orderId content
Map map =new HashMap<>();
map.put("type",1);//1表示来单提醒
map.put("orderId",ordersDB.getId());
map.put("content","订单号"+outTradeNo);
//将map集合转为JSON字符串
String jsonString = JSONObject.toJSONString(map);
webSocketServer.sendToAllClient(jsonString);
用户催单
类似
@Override
public void reminder(Long id) {
//查看订单是否存在
Orders orders=orderMapper.getById(id);
if(orders==null){
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Map map = new HashMap();
map.put("type",2);//2表示客户催单
map.put("orderId",id);
map.put("content","订单号"+orders.getNumber());
String json = JSONObject.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
数据统计
营业额统计
合计订单状态为已完成的订单金额.基于折线图展示营业额数据.根据时间选择区间,展示每天的营业额数据.
提交给前端的为一个日期和营业额的字符串集合,并以逗号分隔
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO implements Serializable {
//日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
private String dateList;
//营业额,以逗号分隔,例如:406.0,1520.0,75.0
private String turnoverList;
}
前端传入一个开始和结束日期(近七日...近半月),需要将开始到结束日期所有日子获得并存放到集合中,然后通过查询订单表获取营业额总和.
/**
* 指定时间区间的营业额数据
* @param startDate
* @param endDate
* @return
*/
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate startDate, LocalDate endDate) {
//计算日期
//用于存放从begin到end范围内的每天的日期
List<LocalDate> dateList = new ArrayList<>();
dateList.add(startDate);
while(!startDate.equals(endDate)){
//计算指定日期后一天的日期
startDate = startDate.plusDays(1);
dateList.add(startDate);
}
//获得营业额数据
//存放每天的营业额
List<Double> turnoverList = new ArrayList<>();
for (LocalDate localDate : dateList) {
//select sum(amount) from orders where order_time>begin and order_time<end_time and status=5
//开始时间 编写为年月日时分秒格式
LocalDateTime begin = LocalDateTime.of(localDate, LocalTime.MIN);
LocalDateTime end=LocalDateTime.of(localDate,LocalTime.MAX);
Map map = new HashMap();
map.put("begin",begin);
map.put("end",end);
map.put("status", Orders.COMPLETED);
Double turnover = orderMapper.sumByMap(map);
turnover=turnover==null?0.0:turnover;
turnoverList.add(turnover);
}
TurnoverReportVO turnoverReportVO = new TurnoverReportVO();
turnoverReportVO.setDateList(StringUtils.join(dateList,","));
turnoverReportVO.setTurnoverList(StringUtils.join(turnoverList,","));
return turnoverReportVO;
}
用户统计
统计用户的数量,需要统计总用户量和新增用户量.
总用户量满足 用户账号创建时间在这一天前即可
新用户要求 用户创建时间在这一天中
@Override
public UserReportVO getUserStatistics(LocalDate startDate, LocalDate endDate) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(startDate);
while(!startDate.equals(endDate)){
startDate = startDate.plusDays(1);
dateList.add(startDate);
}
//存放每天的新增的用户数量 select count(id)from user where create_time <? and create_time>?
List<Integer> newUserList = new ArrayList<>();
//存放每天总的用户数量 select count(id) from user where create_time < ?
List<Integer> totalUserList = new ArrayList<>();
//获得当前日期的初始和结束
for (LocalDate localDate : dateList) {
LocalDateTime begin=LocalDateTime.of(localDate,LocalTime.MIN);
LocalDateTime end=LocalDateTime.of(localDate,LocalTime.MAX);
Map map =new HashMap();
map.put("end",end);
Integer totalUser = userMapper.countByMap(map);
//总用户数量
map.put("begin",begin);
Integer newUser=userMapper.countByMap(map);
totalUser=totalUser==null?0:totalUser;
totalUserList.add(totalUser);
newUser=newUser==null?0:newUser;
newUserList.add(newUser);
}
UserReportVO userReportVO = new UserReportVO();
userReportVO.setDateList(StringUtils.join(dateList,","));
userReportVO.setTotalUserList(StringUtils.join(totalUserList,","));
userReportVO.setNewUserList(StringUtils.join(newUserList,","));
return userReportVO;
}
订单统计
与前面类似
销量排名统计
通过柱形图展示销量排名,包括菜品和套餐
通过连表查询进行查询数据并统计排名
select od.name,sum(od.number) as number from order_detail od,orders o
where od.id=o.id and o.status = 5 and order_time>? and order_time <?
group by od.name order by number desc limit 0,10
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
LocalDateTime beginTime = LocalDateTime.of(begin,LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end,LocalTime.MAX);
List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime,endTime);
List<String>names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
String nameList = StringUtils.join(names,",");
List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
String numberList = StringUtils.join(numbers, ",");
return SalesTop10ReportVO
.builder()
.nameList(nameList)
.numberList(numberList)
.build();
}
工作台
Excel报表
微信小程序开发
HttpClient
导入阿里云的start包的时候已经引入了HttpClient的jar包了,无需手动再导入
GET请求
@Test
public void testGET() throws IOException {
//创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//发送请求,接收返回结果
CloseableHttpResponse response = httpClient.execute(httpGet);
//获得服务器返回的状态码
int statusCode=response.getStatusLine().getStatusCode();
System.out.println("返回给服务端的状态码:"+statusCode);
HttpEntity entity=response.getEntity();
String body= EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:"+body);
//关闭数据
response.close();
httpClient.close();
}
POST请求
@Test
public void testPost() throws IOException {
//创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpPost httpPost=new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject=new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity=new StringEntity(jsonObject.toString());
//指定请求编码方式
entity.setContentEncoding("UTF-8");
//数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
//发送请求
CloseableHttpResponse response=httpClient.execute(httpPost);
//解析返回结果
int statusCode=response.getStatusLine().getStatusCode();
System.out.println("响应码为:"+statusCode);
HttpEntity entity1=response.getEntity();
String body=EntityUtils.toString(entity1);
System.out.println("相应数据为:"+body);
//关闭资源
response.close();
httpClient.close();
}
微信小程序开发
首先注册小程序,通过开发者工具完成开发
微信登录流程
需求:基于微信登录实现小程序的登录功能 如果是新用户就需要自动注册
小程序段发送请求并携带授权码,通过授权码调用微信的接口服务,返回令牌包含用户唯一标识.
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信用户登录:{}", userLoginDTO);
User user=userService.weLogin(userLoginDTO);
//为微信用户生成Jwt令牌
Map<String,Object>claims=new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID,user.getId());
//读取配置文件 调用方法生成Jwt令牌
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);
//封装返回值
UserLoginVO userLoginVO=new UserLoginVO();
userLoginVO.setToken(token);
userLoginVO.setId(user.getId());
userLoginVO.setOpenid(user.getOpenid());//前端携带过来的id
return Result.success(userLoginVO);
}
然后再业务层通过该方法获取用户openid
private String getOpenid(String code){
//调用微信接口服务获得当前用户的openId
Map<String,String>map=new HashMap<>();
map.put("appid", weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code",code);
map.put("grant_type","authorization_code");
String json=HttpClientUtil.doGet(WX_LOGIN,map);
JSONObject jsonObject = JSON.parseObject(json);
String openid=jsonObject.getString("openid");
return openid;
}
商品浏览功能代码
所学
Apache POI
应用场景:
- 银行网银系统交易到处交易明细
- 各种业务系统到处Excel报表
- 批量到处业务数据
Apache ECharts-数据可视化技术
WebSocket协议
应用场景:
- 视频弹幕
- 网页聊天
- 体育实况更新
- 股票基金报价时事更新
任务调度工具 Spring Task
Spring Task是Spring框架提供的一个轻量级的任务调度工具,它允许开发者在Spring应用中方便地实现定时任务、异步任务等功能,无需引入额外的复杂的任务调度框架
cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格隔开,每个域代表一个含义
每个域的含义分别为:秒,分钟,小时,日,月,周,年(可选)..日期可能和星期冲突,只有写一个,另一个写?
可访问在线Cron表达式生成器来在线生成cron表达式
package com.sky.task;
import ...
/**
* @author 刘宇
*/
@Component
@Slf4j
public class MyTask {
/**
* 定时任务
*/
@Scheduled(cron="0/5 * * * * ?")
public void executeTask(){
log.info("定时任务开始执行:{}",new Date());
}
}
缓存框架 Spring Cache
在Spring框架中,缓存抽象提供了一种简化缓存使用的机制,使得开发者能够更专注于业务逻辑,而不用过多关注缓存的具体实现。你提到的几个注解(@CacheEvict
和 @Cacheable
)是Spring Cache提供的关键注解,用于管理缓存中的数据。
- @CacheEvict:
-
-
用于从缓存中移除数据。
-
cacheNames
或value
属性指定了要操作的缓存的名称。 -
allEntries
属性为true
时,表示清除缓存中的所有条目。 -
key
属性用于指定要移除的具体缓存项的键。 -
清除所有缓存项:
java复制代码
@CacheEvict(cacheNames = "setmealCache", allEntries = true)
-
这行代码会清除名为 setmealCache
的缓存中的所有条目。
-
-
精确清理缓存项:
java复制代码
@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")
-
这行代码会根据传入的 setmealDTO
对象的 categoryId
属性值,从 setmealCache
缓存中移除对应的条目。
- @Cacheable:
-
-
用于标记一个方法的返回值是可缓存的。如果缓存中存在指定键的数据,则直接返回缓存中的数据,否则执行方法并将结果存入缓存。
-
cacheNames
或value
属性指定了要使用的缓存的名称。 -
key
属性用于指定缓存项的键。 -
缓存方法返回值:
java复制代码
@Cacheable(cacheNames = "setmealCache", key = "#categoryId")
-
这行代码表示,如果缓存 setmealCache
中已经存在以 categoryId
为键的数据,则直接返回该数据,否则执行该方法并将结果存入 setmealCache
缓存中,键为 categoryId
。
Redis
利用Redis进行缓存
Redis数据库是通过内存来保存数据的,查询数据库本质上时磁盘IO操作,内存操作相对于磁盘操作性能高很多,可以通过Redis来缓存菜品数据,减少数据库查询操作.
将第一次查询数据库所得到的数据,利用合适的方式存入Redis即可.
/**
* 根据分类id查询菜品
* 利用Redis缓存数据
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//构建Redis中的key,规则 dish_分类id
String key="dish_"+categoryId;
//查询Redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if (list != null && list.size() > 0) {
//存在 直接返回 无需查询数据库
return Result.success(list);
}
//不存在 查询数据库
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
常用方法??
BeanUtils.copyProperties(a,b);将a中的属性拷贝到b对象中
PageHelper.startPage(Page,PageSize);开启分页查询
事务
在启动类上加@EnableTransactionManagement //开启注解方式的事务管理
然后就可以通过注解设置
lombok
PageHelper
阿里云服务
上传文件
把存储的图片上传到云服务器,数据库存储的是该图片的访问地址.需要通过Maven加入阿里云依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun.sdk.oss}</version>
</dependency>
然后通过java代码实现,但为了解耦,采用一种更优雅的方式.即通过将Access Key ID和Access Key Secret等数据配置到配置文件中.
然后通过@ConfigurationProperties(prefix="sky.alioss")注解将配置文件中的属性绑定到java对象中
package com.sky.properties;
import ...
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
,然后把具体实现的代码放入工具类中
package com.sky.utils;
import ...
@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();
}
}
然后通过配置类初始化工具类对象放入容器中进行统一管理.
package com.sky.config;
import ...
/**
* @author 刘宇
* 这是一个配置类,用于初始化AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getBucketName());
}
}
编写接口的时候,同样需注意上传的文件名需要进行UUID处理防止重名
//原式文件名
String originalFileName=file.getOriginalFilename();
//截取文件名的扩展名
String extension=originalFileName.substring(originalFileName.lastIndexOf("."));
//通过UUID防止重名
String objectName= UUID.randomUUID().toString()+extension;
//返回文件的请求路径
String filePath=aliOssUtil.upload(file.getBytes(),objectName);
return Result.success(filePath);
处理请求参数
1. @PathVariable
用途:用于从 URL 路径中提取变量。
适用场景:当您需要从 URL 路径中动态获取某些值时,例如获取资源的 ID 或其他标识符。
示例:
java复制代码
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
// 根据 id 查找用户
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
在这个例子中,{id}
是一个路径变量,@PathVariable Long id
用于将其值提取为方法参数 id
。
2. @RequestBody
用途:用于将请求体(通常是 JSON 或 XML)中的数据反序列化为 Java 对象。
适用场景:当您需要从客户端接收复杂的对象或数据结构时,例如创建或更新资源时的表单数据。
示例:
java复制代码
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
// 创建新用户
User createdUser = userService.create(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
在这个例子中,请求体中的数据被反序列化为 User
对象,并作为方法参数 user
传递。
3. @RequestParam
用途:用于从请求参数(查询字符串)中获取数据。
适用场景:当您需要从 URL 的查询字符串中获取简单的数据(如字符串、数字等)时。
示例:
java复制代码
@GetMapping("/users")
public ResponseEntity<List<User>> getUsersByPage(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
// 根据分页参数获取用户列表
Page<User> userPage = userService.findAll(PageRequest.of(page, size));
return ResponseEntity.ok(userPage.getContent());
}
在这个例子中,page
和 size
是查询字符串中的参数,@RequestParam
注解用于将它们提取为方法参数。
总结
- @PathVariable:用于从 URL 路径中提取变量,通常用于获取资源的 ID 或其他标识符。
- @RequestBody:用于将请求体中的数据反序列化为 Java 对象,通常用于处理复杂的表单数据。
- @RequestParam:用于从查询字符串中获取数据,通常用于处理简单的请求参数。
动态查询
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sky.entity
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
这段配置文件是用于配置MyBatis框架的,通常放在Spring Boot项目的application.yml
或application.properties
文件中。MyBatis是一个支持普通SQL查询、存储过程和高级映射的持久层框架。它消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(Plain Old Java Objects,简单的Java对象)映射成数据库中的记录。
下面是对这段配置文件的详细解释:
- mapper-locations:
-
classpath:mapper/*.xml
指定了MyBatis的mapper文件的位置。这些mapper文件包含了SQL语句和映射规则,用于将数据库查询结果映射到Java对象中。这里的配置表示mapper文件位于项目的classpath
下的mapper
目录中,且文件扩展名为.xml
。
- type-aliases-package:
-
com.sky.entity
指定了MyBatis的类型别名包。这意味着MyBatis会扫描这个包下的所有Java类,并将它们的简单类名(首字母小写)注册为别名。例如,如果有一个名为User
的类在com.sky.entity
包下,那么你可以在MyBatis的mapper文件中使用user
作为这个类的别名。
- configuration:
-
- 这是MyBatis的核心配置部分,用于设置MyBatis的行为。
- map-underscore-to-camel-case:
-
-
true
表示开启驼峰命名自动映射。在数据库设计中,很多表字段使用下划线(如user_name
)来分隔单词,而在Java的POJO中,通常使用驼峰命名法(如userName
)。开启这个选项后,MyBatis会自动将数据库中的下划线命名转换为Java对象中的驼峰命名,从而避免了手动编写大量的映射规则。
-
总的来说,这段配置文件通过指定mapper文件的位置、类型别名的包以及MyBatis的核心配置(如驼峰命名转换),为MyBatis的使用提供了必要的配置信息。这使得开发者能够更加方便地使用MyBatis进行数据库操作,而不需要关心底层的JDBC代码和复杂的映射规则。
ThreadLocal
API文档管理工具 YApi和Swagger
YApi
- 定义:YApi是一个现代化的、快速、免费且开源的API文档管理平台。它旨在提供更高效、更友好的接口管理服务,支持团队协作,帮助团队更好地管理、分享和使用API文档。
- 功能:
-
- 接口管理:支持接口的创建、修改、删除,以及版本控制功能。
- 接口调试:提供在线调试接口的功能,方便查看请求和响应的详细信息。
- 接口测试:提供接口测试功能,支持断言、参数化等测试技术。
- 文档生成:自动生成接口文档,支持Markdown格式,方便团队协作。
- 团队协作:支持多用户协作,共同管理和维护API文档。
- 特点:YApi提供了一个可视化的界面,使得接口的管理和使用变得更加直观和便捷。此外,它还支持从Swagger导入接口数据,方便用户在不同工具之间进行迁移。
Swagger
Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
- 定义:Swagger是一个用于设计、构建和文档化RESTful API的工具集。它提供了一系列工具,如Swagger Editor(用于编辑Swagger规范)、Swagger UI(用于可视化API文档)和Swagger Codegen(用于根据API定义生成客户端库、server stubs等)。
- 功能:
-
- API设计:支持定义API的结构、参数、请求和响应格式等信息,帮助开发者更轻松地创建和管理API。
- 文档生成:根据API的定义自动生成易于理解的文档,支持多种格式的输出。
- 在线调试:提供在线接口调试页面,方便开发者进行接口测试和调试。
- 特点:Swagger通过定义API的规范,使得API的设计、构建和文档化变得更加标准化和自动化。它还提供了一套可视化的工具,使得API的查看、测试和调试变得更加方便。此外,Swagger与多种编程语言和框架都具有良好的兼容性,使得它在实际开发中得到了广泛的应用。
应用knife4j
使用swagger定义接口及接口相关信息,可以生成接口文档以及在线接口调试界面
Knife4j是为java MVC框架集成Swagger生成API文档的增强解决方案
1.导入knife4j的Maven坐标
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j}</version>
</dependency>
2.在配置类中加入knife4j相关配置
package com.sky.config;
import ...
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
}
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket() {
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;
}
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
3.设置静态资源映射,否则接口文档页面无法访问
YApi是设计阶段使用的工具,管理和维护接口
常用注解
TODO
在IDEA中设置TODO如// TODO 后期需要进行md5加密,然后再进行比对
就可以快捷的查找需要完善的功能
异常处理器
通过@RestControllerAdvice @ExceptionHandler注解编写异常处理类
@RestControllerAdvice
是一个方便的注解,用于定义一个全局的控制器增强器(Controller Advice)。它主要用来处理全局异常、全局数据绑定等
@ExceptionHandler
注解用于定义一个方法,该方法用于处理特定类型的异常。可以在控制器类(Controller)中单独使用,也可以在通过 @ControllerAdvice
或 @RestControllerAdvice
注解的类中全局使用。
handler包下的异常处理器,编写全局异常处理器处理异常,根据异常的类型进行处理并返回特定信息
实例
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'lans' for key 'idx_username'
String message=ex.getMessage();
if(message.contains("Duplicate entry")){
//创建数组 通过空格分隔成一个个对象
String[] split=message.split(" ");
//取出第三个元素 即username
String username=split[2];
//作为提示信息拼接
String msg=username+ MessageConstant.ALREADY_EXISTS;
return Result.success(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
可自定义异常类然后进行统一处理
拦截器
自定义拦截器后需要再进行注册
package com.sky.interceptor;
import ...
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前用户id:", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
package com.sky.config;
import ...
import java.util.List;
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
/**
* 通过knife4j生成管理端接口文档
* @return
*/
@Bean
public Docket docket1() {
log.info("准备生成管理端接口文档...");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 通过knife4j生成用户端接口文档
* @return
*/
@Bean
public Docket docket2() {
log.info("准备生成用户端接口文档...");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("用户端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射...");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 扩展SpringMVC框架的消息转换器
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入到容器中
converters.add(0,converter);
}
}
自定义注解
类用法
常量类
将所有的提示信息封装到一个常量类里面,设置一系列常量
通过常量可以避免硬编码,方便后期维护.
所有返回的常量结果都可以设置对应的常量类
返回结果类
定义一个类来作为返回后端统一的返回结果
package com.sky.result;
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
DTO VO
前端所提交的数据和实体类中对应的属性差别比较大时,建议用DTO来封装数据
VO用于操作数据库后返回给前端所封装的数据,通常需要继承序列化接口.Serializable
配置类
自定义配置类
通过配置属性类这种方式,把配置项封装成一个java对象通过Spring注入
通过使用@ConfigurationProperties注解
@ConfigurationProperties(prefix = "sky.jwt")
是 Spring Boot 中的一个注解,用于简化配置属性的绑定。这个注解通常被用在类定义上,表示该类的一个或多个字段将会绑定到配置文件(如 application.properties
或 application.yml
)中指定的前缀下的属性上。
具体到这个注解:
@ConfigurationProperties
:这是主注解,用于启用配置属性的绑定功能。prefix = "sky.jwt"
:这个属性指定了配置文件中属性的前缀。也就是说,Spring Boot 将会查找所有以sky.jwt
开头的配置项,并将它们自动绑定到标注了这个注解的类的对应字段上。
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
应用配置类
自定义拦截器,消息转换器
package com.sky.config;
import ...
import java.util.List;
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
}
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket() {
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;
}
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 扩展SpringMVC框架的消息转换器
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建一个消息转换器
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入到容器中
converters.add(0,converter);
}
}