苍穹外卖项目总结(一)[MyBatis-Plus,文件上传,Redis]

苍穹外卖项目复习笔记

一、 MyBatis-Plus (MP) 核心应用

这部分是开发数据持久层的核心,重点在于理解MP如何简化开发以及如何处理复杂场景。

1. 基础 CRUD 与架构关系

  • BaseMapper vs ServiceImpl :
    • BaseMapper<T>: 位于DAO层。提供了最底层的数据库原子操作(insert, deleteById, update, selectList等)。直接操作数据库。
    • ServiceImpl<M, T> : 位于Service层。实现了IService<T>接口,内部默认注入了BaseMapper。它在BaseMapper的基础上进行了封装,提供了更高级的业务逻辑支持(如批量操作、链式调用)。
    • 总结: 简单SQL直接调Mapper,复杂业务逻辑或批量操作调Service。

2. 分页查询

  • 单表分页 :
    • 配置 : 必须配置MybatisPlusInterceptor并添加PaginationInnerInterceptor拦截器,否则分页无效(只会查全部)。
    • 实现 : 使用Page<T>对象作为参数传入BaseMapper的查询方法中。
  • 多表关联分页查询 :
    • 难点: MP的Wrapper主要针对单表。
    • 解决: 手写SQL(XML或注解)。
    • 关键 : 只要Service方法定义的第一个参数是Page对象,MP拦截器会自动拦截该SQL,执行SELECT count(0)查询总数,然后注入LIMIT语句,无需手动写分页逻辑。

具体实现

自定义 SQL (XML 方式) ------ 推荐

核心思路:编写一个 SQL,使用 LEFT JOIN 连接菜品表和分类表,直接查出你需要的所有字段,让 MP 帮你做分页。

  1. 定义 DishVO, 确保你的 DishVO 里有 categoryName 字段。
java 复制代码
@Data
public class DishVO extends Dish {
    // 继承了 Dish 的所有属性
    
    // 额外添加分类名称
    private String categoryName; 
    
    // 可能还需要口味列表,根据你的业务需求决定是否在这里查,或者分开查
    // private List<DishFlavor> flavors; 
}
  1. 修改 Mapper 接口
    DishMapper.java 中定义一个方法。 注意:
  • 入参必须包含 IPage,MP 会自动识别它并进行分页拦截。
  • 返回值也是 IPage。
java 复制代码
@Mapper
public interface DishMapper extends BaseMapper<Dish> {

    /**
     * 自定义分页查询 DishVO
     * @param page MP 分页对象,必须作为第一个参数
     * @param dishPageQueryDTO 查询条件
     * @return 分页结果
     */
    Page<DishVO> pageQuery(IPage<DishVO> page, @Param("dto") DishPageQueryDTO dishPageQueryDTO);
}
  1. 编写 Mapper XML,在 DishMapper.xml 中编写 SQL。
XML 复制代码
<mapper namespace="com.sky.mapper.DishMapper">

    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        SELECT 
            d.*, 
            c.name AS category_name 
        FROM dish d
        LEFT JOIN category c ON d.category_id = c.id
        <where>
            <if test="dto.name != null and dto.name != ''">
                AND d.name LIKE CONCAT('%', #{dto.name}, '%')
            </if>
            <if test="dto.categoryId != null">
                AND d.category_id = #{dto.categoryId}
            </if>
            <if test="dto.status != null">
                AND d.status = #{dto.status}
            </if>
        </where>
        ORDER BY d.create_time DESC
    </select>

</mapper>
  1. Service 层调用
    直接调用 Mapper 即可,非常简洁。
Java 复制代码
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
    // 1. 准备分页对象
    Page<DishVO> page = new Page<>(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
    
    // 2. 调用 Mapper 自定义方法
    // MP 会自动执行两条 SQL:
    // 第一条:SELECT COUNT(*) ... LEFT JOIN ...
    // 第二条:SELECT d.*, c.name ... LEFT JOIN ... LIMIT ?, ?
    Page<DishVO> voPage = dishMapper.pageQuery(page, dishPageQueryDTO);

    // 3. 封装结果
    return new PageResult(voPage.getTotal(), voPage.getRecords());
}

3. 批量操作与链式调用 (Point 6)

  • 批量操作 : 使用IService接口提供的方法,如saveBatch(List<T>)updateBatchById(List<T>)。底层是基于JDBC的batch操作,性能优于循环调用Mapper。
  • 链式调用 (LambdaQueryChainWrapper) :
    • 场景: 避免创建繁琐的Wrapper对象。
    • 写法 : lambdaQuery().eq(User::getId, id).one();
    • 优点: 代码更优雅,可读性更强。

4. 公共字段填充 (Point 4)

  • MP 原生方式 (MetaObjectHandler) :
    • 原理 : 实现MetaObjectHandler接口,重写insertFillupdateFill方法。
    • 配置 : 在实体类字段上添加@TableField(fill = FieldFill.INSERT)等注解。
    • 场景: 适用于纯数据库层面的统一赋值(如create_time, update_time)。
为什么推荐 MetaObjectHandler?
  • 更底层: 它直接介入 MyBatis-Plus 的 CRUD 操作,是专门为数据填充设计的。

  • 更简单: 避免了复杂的 AOP 反射和切入点配置。

  • 更清晰: 逻辑集中在一个处理器类中,明确区分了 插入更新 两种场景。

实现步骤
步骤 1:创建元对象处理器

创建一个类继承自 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler,并实现 insertFillupdateFill 方法。

java 复制代码
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    // 假设您有一个工具类来获取当前用户ID
    private Long getCurrentUserId() {
        // 实际应用中:从 ThreadLocal 或 SecurityContext 获取当前登录用户ID
        return 1L; // 示例值
    }

    /**
     * 插入操作时自动填充
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        // 1. 填充创建时间
        if (metaObject.hasSetter("createTime")) {
            this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        }
        
        // 2. 填充创建人
        if (metaObject.hasSetter("createUser")) {
            this.strictInsertFill(metaObject, "createUser", Long.class, getCurrentUserId());
        }
        
        // 3. 插入时通常也填充更新时间和更新人
        if (metaObject.hasSetter("updateTime")) {
            this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        }
        if (metaObject.hasSetter("updateUser")) {
            this.strictInsertFill(metaObject, "updateUser", Long.class, getCurrentUserId());
        }
    }

    /**
     * 更新操作时自动填充
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        // 1. 填充更新时间
        if (metaObject.hasSetter("updateTime")) {
            this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        }
        
        // 2. 填充更新人
        if (metaObject.hasSetter("updateUser")) {
            this.strictUpdateFill(metaObject, "updateUser", Long.class, getCurrentUserId());
        }
    }
}
步骤2:在实体类上添加注解

在您的实体类(如 Employee)中,只需在需要自动填充的字段上添加 @TableField 注解,指定填充策略。

java 复制代码
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import java.time.LocalDateTime;

public class Employee {
    // ... 其他字段

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    // 插入和更新时都需要填充
    @TableField(fill = FieldFill.INSERT_UPDATE) 
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
}
  • AOP 方式 :
    • 原理: 自定义注解 + Aspect切面。在切面中通过反射给参数对象的属性赋值。
    • 场景 : 适用于需要获取上下文信息(如从ThreadLocal获取当前登录用户的ID)并填充到实体的场景。苍穹外卖中常结合两者使用。
苍穹外卖中AOP机制的分析

切面配置使用了 前置通知 和一个非常精确的 切入点表达式。

A. 切面类:AutoFillAspect

  • 在项目中,AutoFillAspect 是一个使用 @Aspect 注解的 Spring Bean。它很可能使用了 前置通知 (@Before) 来实现公共字段填充。
  • 执行逻辑: 在 Mapper 方法执行之前,通过反射获取方法参数(通常是实体对象),然后根据当前操作类型(INSERT 或 UPDATE)和当前登录的用户信息,为实体对象的 createTime、createUser、updateTime、updateUser 字段赋值。

B. 切入点表达式解析

切入点表达式是:
com.sky.mapper.*.*(..) && @annotation(com.sky.annotation.autoFill)\text{com.sky.mapper.*.*(..) \&\& @annotation(com.sky.annotation.autoFill)}com.sky.mapper.*.*(..) && @annotation(com.sky.annotation.autoFill)方法匹配部分:com.sky.mapper.*.*(..)com.sky.mapper.*: 匹配 com.sky.mapper 包下的所有类 (通常是 Mapper 接口)。.*(...): 匹配这些类中的所有方法 (方法名和参数数量不限)。

效果: 限制了切面只在 mapper 包下的方法上查找连接点。

注解匹配部分:@annotation(com.sky.annotation.autoFill)@annotation(...): 匹配所有被括号内指定的注解标注的方法。

效果: 只有当 com.sky.mapper 包下的某个方法同时被 @AutoFill 注解标记时,切面才会被织入。

总结: 只有当您在 Mapper 接口中定义的方法(如 insert 或 update 方法)上同时使用了 @AutoFill 注解时,AutoFillAspect 的逻辑才会在该方法执行时被触发。


二、 架构设计与难点解决

这部分涉及企业级开发的规范和常见"坑"的解决方案。

1. 雪花算法精度丢失问题

  • 现象 : 后端ID为Long类型(19位),前端JS的Number类型最大安全整数是253−12^{53}-1253−1(约16位)。传递到前端时,后几位会被四舍五入,导致ID不一致。
  • 解决方案 : 后端将Long转为String传输
  • 实现方式 :
    • 局部处理 : 在实体类ID字段上加注解 @JsonSerialize(using = ToStringSerializer.class)
    • 全局处理 : 配置Jackson的消息转换器 (MappingJackson2HttpMessageConverter),将所有Long类型序列化为String。

2. 企业级文件上传设计

  • 技术栈: 阿里云 OSS (Object Storage Service)。
  • 设计模式 :
    • XXXProperties : 使用@ConfigurationProperties读取application.yml中的配置(endpoint, key, secret, bucket),实现配置与代码分离。
    • XXXConfiguration : 使用@Configuration@Bean,利用Properties创建OSS客户端对象,交给Spring容器管理。
    • 接口设计 : 定义标准的FileStorageService接口,包含upload方法。
    • 多实现类: 如果未来切换到腾讯云COS或MinIO,只需新增实现类并修改配置,无需修改业务代码(符合开闭原则)。

三、 中间件集成 (Redis)

StringRedisTemplate vs RedisTemplate

特性 RedisTemplate StringRedisTemplate
序列化方式 默认使用 JdkSerializationRedisSerializer 默认使用 StringRedisSerializer
存储格式 二进制流(乱码状,不可读) 纯字符串(清晰可读)
占用空间 较大(包含类信息) 较小
适用场景 存储复杂的Java对象,且不关心Redis中数据的可读性 存储简单的KV结构,或者需要与其他语言交互,或者需要人工排查Redis数据
推荐 一般不推荐直接用默认的。通常会自定义配置JSON序列化器。 推荐使用。手动将对象转为JSON字符串存入,取出来再转回对象。
相关推荐
CNRio2 小时前
Redis:内存中的数据引擎,架构解析与设计指南
数据库·redis·架构
hans汉斯2 小时前
【软件工程与应用】基于大数据的应急救援云平台构建应用研究
大数据·数据库·人工智能·物联网·系统架构·云计算·汉斯出版社
流绪染梦2 小时前
多表联查时处理一对多的信息,将子表字段放入数组
java·数据库
悦数图数据库2 小时前
国产图数据库:开启数据新“视”界 悦数科技
数据库·人工智能
啊巴矲2 小时前
小白从零开始勇闯人工智能Linux初级篇(Navicat Premium及MySQL库(安装与环境配置))
数据库·人工智能·mysql
en-route2 小时前
Redis 作为消息队列的三种使用方式与 Spring Boot 实践
数据库·spring boot·redis
GreatSQL社区2 小时前
GreatSQL MGR三节点基于时间点恢复
数据库·oracle
小坏讲微服务2 小时前
Spring Boot 4.0 + MyBatis-Plus 实战响应式编程的能力实战
java·spring boot·后端·mybatis
李慕婉学姐2 小时前
Springboot遇见宠物生活馆系统设计与实现n6ea5118(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·宠物