苍穹外卖-总结
总结
总的来说苍穹外卖这个项目的核心在于他的三层架构和CURD,下面详细总结一下这两个方面和其他的重要内容。
三层架构
将每次数据的增删查改通过三层架构的方式来实现,实现的分层解耦的思想
Controller(控制层)
- 定位:相当于三层架构中的 “表示层”,直接对接前端请求。
- 核心职责
- 接收前端发送的 HTTP 请求(如 GET/POST 请求)。
- 负责参数校验(如非空检查、格式验证)。
- 调用 Service 层的方法处理业务逻辑。
- 将处理结果封装成 JSON/XML 等格式返回给前端。
- 技术特点
- 在 Spring 框架中,通常用
@RestController(RESTful 接口)或@Controller(带视图)注解标识。 - 通过
@RequestMapping、@GetMapping等注解映射请求路径。 - 不处理复杂业务逻辑,仅做 “请求转发” 和 “结果封装”。
- 在 Spring 框架中,通常用
核心注解
@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 中,通常用
@Repository或Mapper注解标识。 - 可通过 JDBC、MyBatis(XML / 注解 SQL)、JPA 等技术实现。
- 只关注 “怎么读写数据”,不涉及业务逻辑。
- 在 Java 中,通常用
通常是以接口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}
分页查询
<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)
<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 的核心概念
- 切面(Aspect)
封装的通用功能模块(如日志切面、事务切面),包含了 “要做什么”(通知)和 “在哪里做”(切入点)。
例:AutoFillAspect(上文中的公共字段填充切面)就是一个切面。 - 连接点(JoinPoint)
程序执行过程中的 “关键时刻”(如方法调用前、调用后、抛出异常时),是 AOP 可以插入逻辑的位置。
例:Service 方法执行前、Mapper 方法执行后都是连接点。 - 切入点(Pointcut)
从所有连接点中 “筛选出需要拦截的位置”,通常通过表达式定义(如 “拦截所有 Service 层的方法”)。
例:execution(* com.sky.service.*.*(..))就是一个切入点表达式,匹配 Service 层所有方法。 - 通知(Advice)
切面在切入点执行的具体逻辑,包括 5 种类型:@Before:在切入点方法执行前执行(如上文的自动填充逻辑)。@After:在切入点方法执行后执行(无论是否异常)。@AfterReturning:在切入点方法成功执行后执行(如日志记录返回结果)。@AfterThrowing:在切入点方法抛出异常后执行(如异常处理)。@Around:包围切入点方法,可在执行前后自定义逻辑(功能最强大)。
- 目标对象(Target)
被切面拦截的对象(如 Service 实例、Mapper 实例)。 - 代理对象(Proxy)
AOP 生成的目标对象的代理对象,实际执行时会先调用切面逻辑,再调用目标对象的方法。
核心注解
@Aspect
必须配合 @Component 使用
@Aspect 是 Spring 框架中用于标识切面类(Aspect) 的核心注解,它是实现 AOP(面向切面编程)的关键标识。
@Aspect 的主要作用是告诉 Spring:“这个类是一个切面类,包含了需要横向切入到业务逻辑中的通用功能(如日志、事务、权限校验等)”。
一个被 @Aspect 标记的类,通常会包含两部分核心内容:
- 切入点(Pointcut):定义 “在哪些地方执行切面逻辑”(通过
@Pointcut注解声明)。 - 通知(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 value、GET key、INCR key |
缓存、计数器、分布式锁 |
| Hash(哈希) | 键值对集合(类似 JSON 对象) | HSET key field value、HGET key field |
存储对象(如用户信息、商品属性) |
| List(列表) | 有序可重复的字符串集合(双向链表) | LPUSH key value、RPOP key、LRANGE key start end |
消息队列、最新消息列表 |
| Set(集合) | 无序不可重复的字符串集合 | SADD key member、SMEMBERS key、SINTER key1 key2 |
去重、交集(如共同好友) |
| Sorted Set(有序集合) | 带分数的 Set,按分数排序 | ZADD key score member、ZRANGE 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。
主要作用
- 替代 XML 配置:通过 Java 代码的方式定义 Bean,替代传统的
<bean>标签配置 - 标识配置类:告诉 Spring 这是一个配置类,需要扫描并加载其中的配置
- 支持组件扫描:可以结合
@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.yml 或 application.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();
}
}
浙公网安备 33010602011771号