对应黑马苍穹教程使用寒夜外卖项目代码学习
一整个后端项目内包含三个模块,项目有父pom(统一管理依赖版本,聚合子pom),每个模块有单独子pom
common模块(放乱七八糟杂的)
constant目录
变量类(用于统一管理项目中的变量的固定值,避免硬编码,提高代码可维护性和可读性)
context目录(用于在同一个线程中共享数据,线程局部存储、获取、清除当前用户信息,确保在同一个请求链路中可以访问到用户信息的BasaContext类)
enumeration目录
枚举类(用于定义一组固定的业务状态或类型,提供类型安全的值集合,避免使用字符串或数字常量带来错误的OperationType类)
exception目录
异常类(处理业务异常的自定义异常类)
json目录(用于json数据序列化与反序列化的JacksonObjectMapper类)
log目录
日志类(用于敏感数据脱敏处理、控制日志输出的SensitiveDataConverter类)
properties目录(包含JWT 相关配置的JwtProperties类,包含微信支付或登录相关配置的WeChatProperties类)
配置类
result目录
封装类(封装响应数据格式如code、msg、data的result封装类,封装分页查询得到的数据的PageResult类)
utils目录
工具类(发送http各种方式请求的HttpClientUtil类,jwt令牌加密解密的JwtUtil类,微信支付下单退款的WeChatPayUtil类)
web.filter目录
请求ID过滤器(通过 @WebFilter 或配置类注册的过滤器,为每个HTTP请求生成唯一ID并添加到日志和响应头中的过滤器,用于请求追踪和问题排查的RequestIdFilter类)
pojo模块
dto目录
dto类(作为接收不同属性组合的构造器,通常用于Controller层接收参数,有些里面写着page字段和pageSize字段用于分页查询)
entity目录
存放实体类(对应数据库字段所写的后端属性的对象类)
vo目录
vo类(用于返回给前端的数据封装,当实体类属性不满足响应前端需求时,基于原本响应数据,扩展前端需要的额外属性用的,也可以直接封装dto或实体类再加上扩展属性,也可以封装多个实体类一起响应给前端,也可以只传实体类没有的计算总和的属性)
server(服务器)模块
controller接收请求层在
annotation目录
自定义注解类(用于标识某个方法需要进行功能字段自动填充处理的AutoFill类)
aspect目录
切面类(用来aop在方法前置通知中进行公共字段赋值的AutoFillAspect类)
config目录
配置类(用于配置Redis连接和序列化方式的RedisConfiguration类,配置Web MVC(拦截器和消息转换器)和用于配置WebSocket服务端点的WebMvcConfiguration类,用于配置WebSocket服务端点的WebSocketConfiguration类)
controller目录
表现层(用来确定后端请求接口,调用service层,响应前端数据的)
handler目录
全局异常处理器(所有报错都按这个格式打印的GlobalExceptionHandler类)
interceptor目录
拦截器(校验管理员jwt令牌的JwtTokenAdminInterceptor类,校验用户jwt令牌的JwtTokenUserInterceptor类)
mapper目录
数据访问层(用来给后端对数据库进行数据增删改查写sql语句的,这个目录放的是接口或静态sql,动态sql在resources目录的mapper目录里面)
service目录
业务层(用来处理业务逻辑,调用mapper层,管理事务的,直接目录下的是接口,serviceImpl里面的是实现类)
task目录
定时任务类(用来存放定时任务的OrderTask类)
websocket目录
服务端组件(用于实现实时通信,让服务器能够主动推送消息给客户端,主要用于订单状态的实时更新的WebSocketServer类)
resources 目录,存放配置文件、静态资源和模板文件。
测试代码通常放在 src/test 目录下
还能优化,让项目结构更加完整:
1. common模块补充
- properties目录:存放配置属性类,用于绑定application.yml中的配置
- filter目录:存放过滤器类,用于处理HTTP请求和响应
- interceptor目录:公共的拦截器(如果多个模块都需要使用)
- aop目录:存放AOP切面类(如果其他模块也需要AOP功能)
2. pojo模块补充
分页查询还能单独一个目录,与dto和vo同一级别
- bo目录:业务对象(Business Object),用于service层返回给controller层的对象
- convert目录:存放对象转换工具类(如DTO、VO、Entity之间的转换)
- (Order实体类可以优化)如果使用枚举方式,通常建议将枚举类单独放在一个专门的包中,比如
enums或constant包下,这样可以更好地组织代码。例如:src/main/java/ └── fun/cyhgraph/ ├── entity/ # 实体类 ├── enums/ # 枚举类 │ ├── OrderStatus.java │ └── PayStatus.java └── ...
(已经修正删除这个实体类)组合展示层需要的数据 不是dto的工作吗,干嘛要在实体类搞多一个,您说得对,从严格的分层架构来看,SetmealDishWithPic 确实更适合作为 DTO 放在 dto包下,而不是放在 entity包中。让我解释一下当前设计和更优的做法:
当前设计的问题:
- 类名中的 WithPic 暗示了这是一个特定场景下的数据传输对象放在
entity - 包中会让人误以为它对应数据库表
- 不符合单一职责原则,实体类应该只关注数据持久化
更优的做法:
包结构调整:将 SetmealDishWithPic 移到 dto或 vo 包下,可以重命名为 SetmealDishDTO或 SetmealDishVO以明确其用途
职责划分:entity/SetmealDish:只负责数据库映射
dto/SetmealDishDTO:负责传输数据,可以包含关联数据
3. server模块补充
- listener目录:存放Spring事件监听器
- properties目录:模块特定的配置属性类
- security目录:如果使用Spring Security,相关的配置类可以放在这里
- validation目录:自定义校验注解和校验器
从0开始的话,先搞数据库,再搞实体类
数据库一般用int不用integer
java后端实体类dto类vo类全用integer吧(包括页码和每页记录数)
数据库字段命名用下划线法
后端实体类变量命名用驼峰命名
配置文件写配置自动转换变量名
实体类里面(比如Order类)
-
@Data:Lombok 注解,自动生成getter、setter、toString、equals、hashCode方法(不生成构造器)。 -
@NoArgsConstructor:生成无参构造器。 -
@AllArgsConstructor:生成全参构造器(所有字段作为参数)。 -
@Builder:生成构建者模式(Builder类),但需要依赖目标类的某个构造器来创建实例,需要构造不可变对象(final字段)。
以下是 @Builder 带来的核心优势,结合实际场景说明:
1. 简化复杂对象的构造过程
传统方式构造一个多字段对象时,若存在大量可选参数(例如用户信息中的 name、age、email、address、phone 等,其中部分参数可能不需要每次都设置),通常有两种选择:
传统方式(无参构造 + setter):
@Builder 方式:
@Serial 注解:
这是 Java 14 引入的注解
用于标记与序列化相关的字段和方法
主要是为了代码的可读性和可维护性
编译器会检查被标记的字段或方法是否符合序列化规范
序列化是什么?
想象你有一个乐高玩具(Java对象),现在你想把它邮寄给朋友(比如通过网络发送或保存到文件)。但是你不能直接把实物玩具放进电脑里,所以你需要:
-
序列化:把乐高玩具拆解成零件清单(把Java对象转换成字节序列)
- 比如:1个红色2x4积木,2个蓝色2x2积木...
-
反序列化:朋友收到后,按照清单重新组装出完全一样的乐高玩具(把字节序列恢复成Java对象)
实际应用场景:
- 网络传输:比如微信发送消息时,需要把消息对象转换成可以在网上传输的数据
- 保存到文件:把游戏进度保存到硬盘
- 缓存:把数据存入Redis等缓存系统
serialVersionUID 的作用:
就像乐高套装的版本号。假设你买了两套一样的乐高:
- 如果两套的说明书版本号相同,拼出来的东西就完全一样
- 如果版本号不同,可能某些零件对不上,就拼不起来了
简单例子:
// 这是一个简单的用户类 public class User implements Serializable { // 版本号,确保序列化/反序列化时版本匹配 private static final long serialVersionUID = 1L; private String name; private int age; // 构造方法、getter、setter... }什么时候需要序列化?
- 当你想把对象保存到文件时
- 当你想通过网络发送对象时
- 当你想把对象存入缓存(如Redis)时
不序列化会怎样?
- 程序会报错,因为计算机不知道如何把你的对象变成可以存储或传输的格式
简单来说,序列化就是把对象"打包"成可以存储或传输的格式,而serialVersionUID就是确保打包和解包时使用相同的"打包规则"。
1. 订单状态(Order Status)
// 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 public static final Integer PENDING_PAYMENT = 1; // 待付款 public static final Integer TO_BE_CONFIRMED = 2; // 待接单 public static final Integer CONFIRMED = 3; // 已接单 public static final Integer DELIVERY_IN_PROGRESS = 4; // 派送中 public static final Integer COMPLETED = 5; // 已完成 public static final Integer CANCELLED = 6; // 已取消2. 支付状态(Payment Status)
// 支付状态 0未支付 1已支付 2退款 public static final Integer UN_PAID = 0; // 未支付 public static final Integer PAID = 1; // 已支付 public static final Integer REFUND = 2; // 退款实际使用示例
// 创建新订单 Order order = new Order(); order.setStatus(Order.PENDING_PAYMENT); // 设置状态为"待付款" order.setPayStatus(Order.UN_PAID); // 设置支付状态为"未支付" // 判断订单状态 if (order.getStatus() == Order.PENDING_PAYMENT) { System.out.println("订单待支付"); } // 更新订单状态 if (paymentSuccess) { order.setStatus(Order.TO_BE_CONFIRMED); // 支付成功后改为"待接单" order.setPayStatus(Order.PAID); // 更新支付状态为"已支付" }为什么这样设计?
可读性:Order.PENDING_PAYMENT 比直接写1更容易理解
- 可维护性:如果状态值需要修改,只需改一个地方
- 避免错误:防止拼写错误,IDE会自动提示常量名
在数据库中的存储
status字段存储的是数字(1-6)
pay_status字段存储的是数字(0-2)
数据库查询时也是用这些常量值
注意事项
- 这些常量的值必须与数据库中的值保持一致
- 不要直接使用数字,始终使用这些常量
- 如果状态需要增加或修改,要同步更新注释和数据库
这样设计让代码更清晰,也更容易维护。
private BigDecimal amount; // 实收金额 中的 BigDecimal 是 Java 中用于高精度十进制运算的类,属于 java.math 包。它主要用于处理需要精确计算的数值场景(如金融金额、货币计算等),避免基本数据类型(如 double、float)因浮点数精度丢失导致的问题。
SetmealDishWithPic实体类
为什么没有对应的数据库表?
- 这是一个数据传输对象(DTO)或视图对象(VO),而不是直接映射数据库表的实体类
- 从类名
SetmealDishWithPic可以看出,它是 SetmealDish 的扩展版本,额外包含了pic(菜品图片)字段 - 这种类通常用于:
- 组合多个表的数据
- 返回给前端的视图模型
- 避免暴露数据库实体类的敏感信息
为什么不直接在数据库放图片而是多弄一个类
在数据库设计中,通常不会直接存储图片数据,而是存储图片的路径或URL。SetmealDishWithPic 这个类的设计有以下几个优点:
-
数据冗余与一致性:
- SetmealDish 表只存储菜品ID和套餐ID的关联关系
- 菜品图片存储在
dish表中 - 通过 SetmealDishWithPic DTO 将关联的菜品图片信息组合起来
-
性能优化:
避免在
setmeal_dish- 表中重复存储图片路径
- 减少数据冗余,节省存储空间
- 当菜品图片更新时,只需要更新
dish表中的记录 -
单一职责原则:
- SetmealDish 只负责维护套餐和菜品的关联关系
- Dish 负责维护菜品的基本信息(包括图片)
- SetmealDishWithPic 负责组合展示层需要的数据
-
灵活性:
- 如果将来需要展示更多菜品信息(如描述、分类等),只需扩展 DTO 类
- 不需要修改数据库表结构
-
查询优化:
- 通过 join 查询获取关联数据
- 可以灵活控制返回的字段,避免不必要的数据传输
这种设计符合数据库设计规范,也是实际开发中常用的模式。图片等大字段通常不会直接存储在关系型数据库中,而是存储文件路径或URL,文件本身则存储在文件系统或对象存储服务中。
-
DTO (Data Transfer Object) - 数据传输对象:
- 主要用于接收前端传来的数据
- 通常在Controller层接收请求参数
- 包含前端提交的数据
- 用于数据的输入
- 例如:SetmealDTO、DishDTO、UserDTO
-
VO (Value Object) - 值对象:
- 主要用于将后端数据传递给前端
- 通常在Controller层返回给前端
- 包含需要展示给前端的数据
- 用于数据的输出
- 例如:SetmealVO、DishVO、OrderVO
dto目录的OrderPageDTO类
这是什么意思 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime beginTime; // 开始时间 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime endTime; // 结束时间
这段代码是用于处理日期时间格式的Spring注解和字段声明:
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") 是Spring框架提供的注解,用于指定日期时间字符串的格式:
yyyy:4位数的年份(如2023)
MM:2位数的月份(01-12)
dd:2位数的日期(01-31)
HH:24小时制的小时(00-23)
mm:分钟(00-59)
ss:秒(00-59)
这两个字段用于接收前端传来的时间范围参数
beginTime:开始时间
endTime:结束时间
使用场景:
当需要按时间范围查询订单时,前端会传递类似 beginTime=2023-01-01 00:00:00&endTime=2023-12-31 23:59:59的参数
Spring会自动将字符串参数转换为LocalDateTime对象
在Service层可以直接使用这些时间对象进行数据库查询
DishDTO类
private List<DishFlavor> flavors = new ArrayList<>();
是一个用于存储菜品口味信息的列表字段。让我详细解释它的作用和使用场景:
1. 字段定义解析
private List<DishFlavor> flavors = new ArrayList<>();
- private:访问修饰符,表示该字段仅在当前类中可访问
- List<DishFlavor>:泛型列表,表示这是一个
DishFlavor
对象的集合
- flavors:字段名,表示菜品口味
- = new ArrayList<>():初始化一个空的 ArrayList,避免空指针异常
2. 业务含义
这个字段用于存储一个菜品可能具有的多种口味选项。例如:
- 对于"麻辣香锅"这道菜:
- 口味1:辣度 - 微辣/中辣/特辣
- 口味2:配菜 - 加豆腐/加金针菇/加牛肉
vo目录的OrderVo类
@EqualsAndHashCode(callSuper = true)
public class OrderVO extends Order implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String orderDishes; // 订单菜品信息(JSON字符串格式)
private List<OrderDetail> orderDetailList; // 订单详情列表
}
@EqualsAndHashCode(callSuper = true) 在 OrderVO 类中的详细解释
@EqualsAndHashCode(callSuper = true) 是 Lombok 提供的注解,用于自动生成 equals() 和 hashCode() 方法,并明确要求这两个方法在比较对象时包含父类的字段。结合 OrderVO 类,我们可以从以下几个角度理解其作用:
一、基础作用:生成 equals 和 hashCode 方法
@EqualsAndHashCode 注解的核心功能是为类自动生成 equals(Object o) 和 hashCode() 方法。这两个方法是 Java 中用于对象比较和哈希计算的核心方法,广泛用于集合(如 HashSet、HashMap)、缓存、状态校验等场景。
二、callSuper = true 的特殊含义
默认情况下(不指定 callSuper 或设为 false),@EqualsAndHashCode 只会比较当前类中声明的字段。但如果父类(如 Order)包含关键业务字段(如订单ID、订单号),这些字段不会被自动包含在比较中,可能导致对象比较逻辑不完整。
callSuper = true 的作用是:生成的 equals() 和 hashCode() 方法会先调用父类的 equals() 和 hashCode() 方法,从而将父类的字段纳入比较范围。
登录逻辑的jwt令牌以及md5加密

浙公网安备 33010602011771号