苍穹外卖-总结

总结

总的来说苍穹外卖这个项目的核心在于他的三层架构CURD,下面详细总结一下这两个方面和其他的重要内容。



三层架构

将每次数据的增删查改通过三层架构的方式来实现,实现的分层解耦的思想

Controller(控制层)

  • 定位:相当于三层架构中的 “表示层”,直接对接前端请求。
  • 核心职责
    • 接收前端发送的 HTTP 请求(如 GET/POST 请求)。
    • 负责参数校验(如非空检查、格式验证)。
    • 调用 Service 层的方法处理业务逻辑。
    • 将处理结果封装成 JSON/XML 等格式返回给前端。
  • 技术特点
    • 在 Spring 框架中,通常用@RestController(RESTful 接口)或@Controller(带视图)注解标识。
    • 通过@RequestMapping@GetMapping等注解映射请求路径。
    • 不处理复杂业务逻辑,仅做 “请求转发” 和 “结果封装”。


核心注解

@RestController

@RestController 本质上是 Spring 的一个组件注解,Spring 会自动扫描并实例化被该注解标记的类,将其纳入 IoC 容器管理。这使得该类能够接收和处理前端发送的 HTTP 请求(如 GET、POST、PUT、DELETE 等),成为前端与后端交互的 “入口”。



@RequestMapping

@RequestMapping 是 Spring MVC 中的核心注解之一,主要用于映射 HTTP 请求到控制器(Controller)的方法或类上,定义了接口的访问路径、请求方法、参数等规则。它是 Controller 层处理请求的 “路由导航” 工具。作为Controller层中所有方法的路径前缀



@RequestBody

HTTP 请求体(Request Body)中的数据绑定到控制器方法的参数上

简单来讲就是接收前端发送的JSON/XML数据



@PathVariable

@PathVariable 是 Spring MVC 中的一个注解,用于从 URL 路径中提取参数值并绑定到控制器方法的参数上。它主要用于处理RESTful 风格的 URL 路径参数,例如 /users/123 中的 123(用户 ID)。



其他特定处理方法

PostMapping

GetMapping

PutMapping

DeleteMapping

PatchMapping



员工登录

/**
 * 登录
 *
 * @param employeeLoginDTO
 * @return
 */
@PostMapping("/login")
@ApiOperation(value = "员工登录")
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);
}


分页查询

/**
 * 员工分页查询
 * @param employeePageQueryDTO
 * @return
 */
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
    log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
    PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
    return Result.success(pageResult);
}


PageResult

在分页查询场景中,PageResult 是一个专门用于封装分页查询结果的通用数据结构(通常是一个 Java 类),主要作用是将分页查询的核心信息(如当前页数据、总条数、总页数等)统一封装,方便前端展示分页效果。



Service 层(业务逻辑层)

  • 定位:三层架构中的 “业务逻辑层”,是应用的核心层。
  • 核心职责
    • 实现具体的业务逻辑(如用户注册、订单结算等流程)。
    • 协调多个 Dao 层操作完成复杂业务(如转账需操作两个账户的 Dao)。
    • 处理事务(如@Transactional注解控制事务提交 / 回滚)。
    • 对 Controller 层传入的数据进行二次校验或加工。
  • 技术特点
    • 通常定义接口(UserService)和实现类(UserServiceImpl),便于扩展和测试。
    • 依赖 Dao 层提供的数据访问能力,但不直接与数据库交互。


核心注解

@Service

@Service 在 Service 层中的核心作用是:标识该类为业务逻辑组件,交由 Spring 管理,支撑依赖注入和事务管理,同时明确分层定位。它是 Service 层能够承载核心业务逻辑、与其他层高效协作的基础。

一般来说Service层先定义接口Serivce然后再由实现类ServiceImpl来实现特定功能



员工登录

接口

/**
 * 员工登录
 * @param employeeLoginDTO
 * @return
 */
Employee login(EmployeeLoginDTO employeeLoginDTO);


实现类

/**
 * 员工登录
 *
 * @param employeeLoginDTO
 * @return
 */
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);
    }

    //密码比对
    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;
}


分页查询

接口

PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);


实现类

/**
 * 分页查询
 * @param employeePageQueryDTO
 * @return
 */
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
    //select *from employee limit 0, 10
    //开始分页查询
    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);
}


PageHelper

PageHelper 是 MyBatis 生态中一款非常流行的 分页插件,它的核心作用是简化数据库分页查询的实现,无需手动编写复杂的分页 SQL 语句(如 MySQL 的 LIMIT、Oracle 的 ROWNUM 等),只需简单配置和调用即可完成分页。

PageHelper.startPage(pageNum, pageSize) 即可开启分页



Dao 层(数据访问层)

  • 定位:三层架构中的 “数据访问层”,又叫持久层,负责与数据存储交互。
  • 核心职责
    • 执行具体的 CRUD 操作(增删查改),如查询数据库、写入文件等。
    • 封装数据访问细节(如 SQL 语句、数据库连接)。
    • 为 Service 层提供统一的数据操作接口。
  • 技术特点
    • 在 Java 中,通常用@RepositoryMapper注解标识。
    • 可通过 JDBC、MyBatis(XML / 注解 SQL)、JPA 等技术实现。
    • 只关注 “怎么读写数据”,不涉及业务逻辑。

通常是以接口Mapper + 实现类XML文件的形式



核心注解

@Mapper

@Mapper 是 MyBatis 框架中 Dao 层的核心注解,它的作用是让 MyBatis 为接口生成数据库操作的代理实现类,从而简化数据访问层的开发(无需手动编写 JDBC 代码或实现类)。通过 @Mapper,开发者可以专注于定义 SQL 语句和接口方法,大幅提升数据库操作的开发效率。



@Repository

@Repository 是 Spring 框架中的一个注解,主要用于标识 数据访问层(Dao 层)的组件,其核心作用是明确该类的职责并整合 Spring 的数据访问支持。Spring 会自动扫描并将被 @Repository 标记的类纳入 IoC 容器管理,使其可以被 Service 层通过 @Autowired 注入使用。



@Repository 的 @Mapper 区别
  • @Repository 是 Spring 的注解,核心作用是标识 Dao 层组件并处理异常转换。
  • @Mapper 是 MyBatis 的注解,用于让 MyBatis 为接口生成代理实现类(专注于 SQL 执行)。


查询员工信息

接口

/**
 * 根据用户名查询员工
 * @param username
 * @return
 */
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);


分页查询

接口

/**
 * 分页查询
 * @param employeePageQueryDTO
 * @return
 */
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);


XML文件

<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>


CURD

增(Create)

insert

基本的MySQL语法

insert into 表名 (...) values (...)

@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
        " VALUES" +
        " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")


insertBatch

foreach语句进行批量插入,减少了SQL语句的执行次数,一般写在XML文件中

<insert id="insertBatch">
    insert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, number, amount)
       values
    <foreach collection="orderDetailList" item="od" separator=",">
        (#{od.name}, #{od.image}, #{od.orderId}, #{od.dishId}, #{od.setmealId}, #{od.dishFlavor}, #{od.number}, #{od.amount})
    </foreach>
</insert>


删(Delete)

deleteById

基本的MySQL语法

delete from 表名 where (条件)

delete from category where id = #{id}


deleteByIds

foreach语句进行批量插入,减少了SQL语句的执行次数,一般写在XML文件中

<delete id="deleteByDishIds">
    delete from dish_flavor where dish_id in
    <foreach collection="dishIds" open="(" close=")" separator="," item="dishId">
        #{dishId}
    </foreach>
</delete>


查(Select)

基础条件查询

基本的MySQL语法

select *from dish_flavor where dish_id = #{dishId}


分页查询

语句,使用动态SQL查询,一般放在XML文件中

<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>


改(Update)

语句,使用动态SQL查询,一般放在XML文件中

<update id="update" parameterType="addressBook">
    update address_book
    <set>
        <if test="consignee != null">
            consignee = #{consignee},
        </if>
        <if test="sex != null">
            sex = #{sex},
        </if>
        <if test="phone != null">
            phone = #{phone},
        </if>
        <if test="detail != null">
            detail = #{detail},
        </if>
        <if test="label != null">
            label = #{label},
        </if>
        <if test="isDefault != null">
            is_default = #{isDefault},
        </if>
    </set>
    where id = #{id}
</update>


AOP

AOP 是对 OOP(面向对象编程)的补充,OOP 通过 “继承” 和 “封装” 纵向组织代码,AOP 则通过 “切面” 横向抽取通用逻辑。它的核心价值是解耦—— 让业务代码只关注业务本身,通用功能通过切面统一管理,大幅提升代码质量和开发效率。

AOP 的核心概念

  1. 切面(Aspect)
    封装的通用功能模块(如日志切面、事务切面),包含了 “要做什么”(通知)和 “在哪里做”(切入点)。
    例:AutoFillAspect(上文中的公共字段填充切面)就是一个切面。
  2. 连接点(JoinPoint)
    程序执行过程中的 “关键时刻”(如方法调用前、调用后、抛出异常时),是 AOP 可以插入逻辑的位置。
    例:Service 方法执行前、Mapper 方法执行后都是连接点。
  3. 切入点(Pointcut)
    从所有连接点中 “筛选出需要拦截的位置”,通常通过表达式定义(如 “拦截所有 Service 层的方法”)。
    例:execution(* com.sky.service.*.*(..)) 就是一个切入点表达式,匹配 Service 层所有方法。
  4. 通知(Advice)
    切面在切入点执行的具体逻辑,包括 5 种类型:
    • @Before:在切入点方法执行前执行(如上文的自动填充逻辑)。
    • @After:在切入点方法执行后执行(无论是否异常)。
    • @AfterReturning:在切入点方法成功执行后执行(如日志记录返回结果)。
    • @AfterThrowing:在切入点方法抛出异常后执行(如异常处理)。
    • @Around:包围切入点方法,可在执行前后自定义逻辑(功能最强大)。
  5. 目标对象(Target)
    被切面拦截的对象(如 Service 实例、Mapper 实例)。
  6. 代理对象(Proxy)
    AOP 生成的目标对象的代理对象,实际执行时会先调用切面逻辑,再调用目标对象的方法。


核心注解

@Aspect

必须配合 @Component 使用

@Aspect 是 Spring 框架中用于标识切面类(Aspect) 的核心注解,它是实现 AOP(面向切面编程)的关键标识。

@Aspect 的主要作用是告诉 Spring:“这个类是一个切面类,包含了需要横向切入到业务逻辑中的通用功能(如日志、事务、权限校验等)”

一个被 @Aspect 标记的类,通常会包含两部分核心内容:

  1. 切入点(Pointcut):定义 “在哪些地方执行切面逻辑”(通过 @Pointcut 注解声明)。
  2. 通知(Advice):定义 “在切入点执行什么逻辑”(通过 @Before@After@Around 等注解声明)。


@Component

@Component 是 Spring 框架中最基础的组件注解,用于标识一个类为Spring 管理的组件(Bean),让 Spring 能够自动扫描并将其纳入 IoC(Inversion of Control,控制反转)容器中进行管理。



自定义注解

package com.sky.annotation;

import com.sky.enumeration.OperationType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型
    OperationType value(); //// 定义一个名为"value"的属性,类型为OperationType
}

在 Java 中,@interface 是用于定义自定义注解(Annotation) 的关键字。

注解目标@Target(ElementType.METHOD) 指定该注解只能用于方法上

保留策略:通过 @Retention(RetentionPolicy.RUNTIME) 指定该注解在运行时仍然可见,这意味着可以通过反射机制在程序运行时获取到该注解的信息

注解属性:定义了一个名为 value 的属性,类型为 OperationType 枚举,用于指定数据库操作的类型(比如新增、修改等)



切入点

/**
 * 切入点
 */
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}

@Pointcut

定义切入点

@execution

用于根据方法的访问修饰符、返回值类型、包路径、类名、方法名、参数等信息匹配方法。

@annotation

用于匹配被指定注解标记的方法。-------> @AutoFill就是指定注解,当遇到这个注解就会被拦截



通知

@Before("autoFillPointCut()")

这是一个前置通知,在被拦截的 Mapper 方法执行之前



连接点

public void autoFill(JoinPoint joinPoint)
  • JoinPoint:连接点对象,用于获取被拦截方法的信息(如参数、方法签名等)。


核心逻辑步骤

(1)获取操作类型(新增 / 更新)
// 获取方法签名(包含方法的注解、参数等信息)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法上的 @AutoFill 注解
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
// 从注解中获取操作类型(INSERT 或 UPDATE)
OperationType operationType = autoFill.value();
  • @AutoFill 是一个自定义注解,用于标记需要自动填充的方法,并指定操作类型(新增 / 更新)。
  • OperationType 是枚举类,包含 INSERT(新增)和 UPDATE(更新)两个值。
(2)获取目标实体对象
// 获取被拦截方法的参数(通常 Mapper 方法的第一个参数是实体对象)
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0) {
    return; // 无参数则直接返回
}
Object entity = args[0]; // 实体对象(如 User、Order 等)
  • 假设 Mapper 方法的第一个参数是需要操作的实体对象(如 insert(User user) 中的 user)。
(3)准备填充的数据
LocalDateTime now = LocalDateTime.now(); // 当前时间(用于 createTime 和 updateTime)
Long currentId = BaseContext.getCurrentId(); // 当前登录用户 ID(从 ThreadLocal 中获取)
  • BaseContext 是一个工具类,通过 ThreadLocal 存储当前登录用户的 ID,确保多线程环境下的线程安全。
(4)根据操作类型自动填充字段

通过反射调用实体类的 setter 方法,为公共字段赋值:

  • 新增操作(INSERT):需要设置 4 个字段

    // 获取实体类的 setCreateTime 方法(参数为 LocalDateTime)
    Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
    // 调用方法,为 createTime 赋值为当前时间
    setCreateTime.invoke(entity, now);
    
    // 同理设置 createUser、updateTime、updateUser
    
  • 更新操作(UPDATE):需要更新 2 个字段

    // 只需要设置 updateTime 和 updateUser
    Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
    Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
    setUpdateTime.invoke(entity, now)
    setUpdateUser.invoke(entity, currentId);
    


Redis

Redis是一个基于内存的key-value结构数据库

核心数据结构

数据结构 特点 典型命令 应用场景
String(字符串) 存储文本 / 二进制数据(最大 512MB) SET key valueGET keyINCR key 缓存、计数器、分布式锁
Hash(哈希) 键值对集合(类似 JSON 对象) HSET key field valueHGET key field 存储对象(如用户信息、商品属性)
List(列表) 有序可重复的字符串集合(双向链表) LPUSH key valueRPOP keyLRANGE key start end 消息队列、最新消息列表
Set(集合) 无序不可重复的字符串集合 SADD key memberSMEMBERS keySINTER key1 key2 去重、交集(如共同好友)
Sorted Set(有序集合) 带分数的 Set,按分数排序 ZADD key score memberZRANGE key start end 排行榜(如游戏积分、商品销量)


package com.sky.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}


文件上传

package com.sky.config;

import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置类,用于创建AliOssUtil
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}

在 Spring 框架中,@Configuration 是一个用于标识配置类的注解。被该注解标记的类相当于传统 Spring 配置中的 XML 配置文件,用于定义和配置 Spring 容器中的 Bean。

主要作用

  1. 替代 XML 配置:通过 Java 代码的方式定义 Bean,替代传统的 <bean> 标签配置
  2. 标识配置类:告诉 Spring 这是一个配置类,需要扫描并加载其中的配置
  3. 支持组件扫描:可以结合 @ComponentScan 注解指定需要扫描的包路径


@ConditionalOnMissingBean 是 Spring Boot 中的一个条件注解,用于控制 Bean 的创建条件:当容器中不存在指定类型的 Bean 时,才会创建当前注解所标注的 Bean

package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}

@ConfigurationProperties(prefix = "sky.alioss") 是 Spring Boot 中的一个注解,用于将配置文件(如 application.ymlapplication.properties)中指定前缀的配置项绑定到 Java 类的属性上,实现配置的自动注入。



ThreadLocal

ThreadLocal 是 Java 中的一个线程本地存储工具类,它允许每个线程拥有自己独立的变量副本,实现了线程间的数据隔离。简单来说,就是每个线程都可以通过 ThreadLocal 存储和获取只属于自己的数据,而不会与其他线程的同名数据产生冲突

package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }
    
    //常常用这个方法来获取主键Id
    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}
posted @ 2025-07-25 12:18  li_hc  阅读(542)  评论(0)    收藏  举报