深入苍穹外卖项目之菜品管理:公共字段填充探秘

#AI提效·半月创作挑战赛#

深入苍穹外卖项目之菜品管理:公共字段填充探秘

引言

在当今数字化时代,外卖行业蓬勃发展,苍穹外卖项目应运而生,旨在为餐饮企业提供高效、便捷的菜品管理解决方案。菜品管理作为苍穹外卖项目的核心模块之一,承担着菜品信息的新增、查询、修改和删除等重要功能,对于提升餐饮企业的运营效率和用户体验起着关键作用。

在菜品管理的诸多功能中,公共字段填充功能尤为重要。它通过自动化的方式,为菜品实体类中的公共字段(如创建时间、创建人、更新时间、更新人等)赋予相应的值,不仅提高了开发效率,还确保了数据的一致性和准确性。而实现这一功能的底层技术 —— 枚举、注解、AOP 和反射,更是 Java 开发中的核心技术点,它们相互协作,共同构建了一个高效、灵活的公共字段填充机制。接下来,让我们深入探究这些技术在苍穹外卖项目中的具体应用。

一、苍穹外卖项目概述

1.1 项目背景与架构

苍穹外卖项目旨在打造一个功能全面、高效稳定的外卖服务平台,满足商家和用户在餐饮外卖领域的多样化需求。随着外卖市场的迅速扩张,传统的餐饮管理模式难以应对日益增长的业务量和复杂的业务流程。苍穹外卖项目应运而生,通过数字化手段实现菜品管理、订单处理、配送调度等核心业务的自动化和智能化,提升餐饮企业的运营效率和服务质量。

该项目采用先进的前后端分离架构,前端基于 Vue.js 框架构建,为用户提供流畅、直观的交互界面;后端基于 Spring Boot 框架搭建,利用其强大的依赖管理和快速开发特性,确保服务的高性能和稳定性。数据库选用 MySQL,负责存储海量的业务数据,包括菜品信息、用户信息、订单信息等。同时,引入 Redis 作为缓存层,有效减轻数据库压力,提升系统响应速度;采用 Nginx 实现反向代理与负载均衡,保障系统在高并发场景下的稳定运行。

1.2 菜品管理功能在项目中的角色

菜品管理功能是苍穹外卖项目的核心模块之一,它直接关系到商家的菜品展示、用户的点餐体验以及整个外卖业务的流畅运作。对于商家而言,通过菜品管理功能,能够方便快捷地进行菜品的新增、修改、删除操作,实时更新菜品信息,如菜品名称、价格、口味、图片等,确保用户看到的菜品信息准确无误。同时,还能对菜品进行分类管理,根据不同的菜系、口味偏好等维度,将菜品划分为不同的类别,便于用户浏览和搜索。

对于用户来说,清晰、准确的菜品信息展示是吸引他们下单的关键因素之一。菜品管理功能确保了用户能够在平台上轻松找到自己喜爱的菜品,查看菜品详情,包括菜品的口味描述、食材组成、营养成分等,从而做出更加明智的点餐决策。此外,菜品管理功能还与订单管理、库存管理等模块紧密关联,实现了业务数据的实时同步和交互。当用户下单后,系统会自动更新菜品的库存信息,避免超卖现象的发生;商家在调整菜品价格或库存时,也能及时反馈到用户端,保证订单处理的准确性和及时性。

二、公共字段填充功能剖析

2.1 公共字段填充的需求与意义

在苍穹外卖项目中,菜品管理涉及多个业务操作,如新增菜品、修改菜品等。在这些操作过程中,存在一些公共字段,如创建时间(createTime)、修改时间(updateTime)、创建人(createUser)、修改人(updateUser)等,需要在不同的业务逻辑中进行赋值操作。以新增菜品为例,当商家在平台上添加一道新菜品时,系统需要记录下该菜品的创建时间以及创建该菜品的商家账号 ID,即创建人信息;而在后续对菜品信息进行修改时,也需要实时更新修改时间和修改人的信息。

这些公共字段对于数据管理具有重要意义。从数据完整性角度来看,它们提供了数据操作的上下文信息,使得每一条菜品数据都具备完整的生命周期记录。通过创建时间和修改时间,我们可以清晰地了解菜品信息的变化历程,便于进行数据追溯和审计。在分析菜品的销售趋势时,结合创建时间,可以判断新菜品的市场接受度和销售增长情况;通过对比不同时间段的修改记录,可以了解商家对菜品的优化策略和市场反馈的响应速度。

从数据一致性角度来说,统一的公共字段赋值规则确保了在整个项目中,相同含义的字段具有一致的数据格式和来源。这避免了因不同业务方法中赋值逻辑不一致而导致的数据混乱,提高了数据的可靠性和可用性。在多用户协作的环境下,保证了所有用户对菜品数据的操作记录都遵循相同的规范,使得数据在各个模块之间的传递和共享更加顺畅。

2.2 传统方式的痛点

在未引入公共字段自动填充机制之前,传统的做法是在每个涉及公共字段赋值的业务方法中,手动编写赋值代码。以新增菜品和修改菜品的业务方法为例:


// 新增菜品业务方法
public void addDish(Dish dish) {
    dish.setCreateTime(LocalDateTime.now());
    dish.setCreateUser(getCurrentUserId());
    dish.setUpdateTime(LocalDateTime.now());
    dish.setUpdateUser(getCurrentUserId());
    // 其他业务逻辑,如插入数据库等
    dishMapper.insert(dish);
}

// 修改菜品业务方法
public void updateDish(Dish dish) {
    dish.setUpdateTime(LocalDateTime.now());
    dish.setUpdateUser(getCurrentUserId());
    // 其他业务逻辑,如更新数据库等
    dishMapper.update(dish);
}

这种方式存在诸多痛点。首先,代码冗余严重。在多个业务方法中,都需要重复编写类似的公共字段赋值代码,不仅增加了代码量,还使得代码的可读性和可维护性降低。当公共字段的赋值逻辑发生变化时,例如需要修改时间格式或者获取当前用户 ID 的方式发生改变,就需要在所有涉及的业务方法中逐一进行修改,工作量巨大且容易遗漏,增加了出错的风险。

其次,这种分散的赋值方式不利于代码的复用和扩展。如果项目中新增了一个公共字段,或者需要对公共字段的赋值逻辑进行统一的增强(如添加日志记录),就需要对大量的业务方法进行修改,违背了软件设计中的开闭原则,使得系统的扩展性和灵活性大打折扣。

三、实现技术与原理

3.1 自定义注解(@AutoFill)

在苍穹外卖项目中,自定义注解 @AutoFill 扮演着至关重要的角色,它是实现公共字段自动填充功能的关键起点。该注解的主要作用是标记那些需要进行公共字段自动填充的方法,通过在方法上添加这个注解,我们能够精准地识别出哪些业务操作涉及公共字段的赋值,从而触发后续的自动填充逻辑。

从定义方式来看,@AutoFill 注解使用了 Java 的元注解来精确控制其行为和生命周期。它通过 @Target (ElementType.METHOD) 指定该注解只能应用于方法上,这就明确限定了注解的作用范围,避免了在不相关的代码元素(如类、字段等)上使用,确保了注解使用的准确性和规范性。@Retention (RetentionPolicy.RUNTIME) 则规定了注解在运行时仍然有效,这使得我们在程序运行过程中能够通过反射机制获取到注解的信息,进而实现对标记方法的动态处理。


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();
}

在上述代码中,我们还可以看到 @AutoFill 注解包含一个 value 属性,其类型为 OperationType,这是一个自定义的枚举类型。通过这个属性,我们可以为注解标记的方法指定具体的数据库操作类型(如插入 INSERT 或更新 UPDATE),以便在后续的处理中根据不同的操作类型执行相应的公共字段填充逻辑。例如,在新增菜品的业务方法上使用 @AutoFill (OperationType.INSERT),就能明确告知系统该方法执行的是插入操作,需要按照插入操作的规则填充公共字段。这种设计方式使得注解更加灵活和智能,能够适应不同业务场景下公共字段填充的需求。

3.2 枚举类(OperationType)

OperationType 枚举类在公共字段自动填充功能中起着不可或缺的作用,它主要用于限定 @AutoFill 注解的属性值,确保在使用注解时,只能赋予其预定义的、合法的操作类型。这种限定机制极大地增强了代码的健壮性和可读性,有效避免了因属性值错误或随意赋值而导致的潜在错误。


public enum OperationType {
    UPDATE,
    INSERT
}

在这个枚举类中,定义了两个枚举常量:UPDATE 和 INSERT,分别代表数据库的更新操作和插入操作。当我们在使用 @AutoFill 注解时,其 value 属性只能从这两个枚举常量中取值,如 @AutoFill (OperationType.UPDATE) 或 @AutoFill (OperationType.INSERT)。这样一来,在进行公共字段填充时,系统能够根据注解中指定的操作类型,准确判断当前业务方法执行的是插入还是更新操作,从而执行相应的公共字段填充逻辑。

在新增菜品的业务方法上标记 @AutoFill (OperationType.INSERT),当系统执行到该方法时,通过获取注解中的 OperationType.INSERT,就能明确知道这是一个插入操作,进而按照插入操作的逻辑,为菜品实体类的 createTime、createUser、updateTime、updateUser 等公共字段赋予相应的值,如将 createTime 和 updateTime 设置为当前时间,将 createUser 和 updateUser 设置为当前登录用户 ID。而在修改菜品的业务方法上标记 @AutoFill (OperationType.UPDATE),系统则会依据更新操作的逻辑,仅对 updateTime 和 updateUser 字段进行赋值更新。通过这种方式,OperationType 枚举类与 @AutoFill 注解紧密协作,实现了公共字段填充逻辑的精准控制和灵活应用。

3.3 AOP(面向切面编程)

3.3.1 AOP 概念与原理

AOP,即面向切面编程,是一种与传统的面向对象编程(OOP)相辅相成的编程范式,它旨在解决在软件开发过程中横切关注点(Cross-Cutting Concerns)的问题。在传统的 OOP 中,我们主要关注的是业务逻辑的模块化和封装,通过类和对象来组织和管理代码。然而,在实际的项目开发中,存在一些功能或行为,它们并不属于特定的业务逻辑模块,但却贯穿于多个业务模块之中,例如日志记录、事务管理、权限校验、公共字段填充等。这些功能被称为横切关注点,如果在每个业务方法中都重复编写实现这些横切关注点的代码,不仅会导致代码的大量冗余,还会使业务逻辑代码变得复杂和难以维护。

AOP 的核心思想是将这些横切关注点从业务逻辑中分离出来,形成独立的切面(Aspect),然后通过一种称为 “织入(Weaving)” 的机制,在运行时将这些切面动态地插入到业务逻辑的特定连接点(Join Point)上,从而实现对业务逻辑的增强和扩展。连接点是程序执行过程中的特定位置,如方法调用、异常抛出等;切点(Pointcut)则是一组连接点的集合,通过定义切点表达式,我们可以精确地指定哪些连接点需要应用切面;通知(Advice)是切面在切点处执行的具体逻辑,根据执行时机的不同,通知可以分为前置通知(Before Advice)、后置通知(After Advice)、环绕通知(Around Advice)、异常通知(After Throwing Advice)和最终通知(After Finally Advice)等。

以苍穹外卖项目中的公共字段填充功能为例,公共字段填充的逻辑就是一个横切关注点,它涉及到多个菜品管理的业务方法,如新增菜品、修改菜品等。通过 AOP,我们可以将公共字段填充的逻辑封装在一个切面类中,然后定义一个切点表达式,匹配所有需要进行公共字段填充的业务方法。在运行时,当程序执行到这些被匹配的业务方法时,AOP 框架会自动将公共字段填充的逻辑(即通知)织入到方法执行的相应位置,实现公共字段的自动填充,而无需在每个业务方法中手动编写填充代码。这样不仅减少了代码冗余,提高了代码的可维护性和可扩展性,还使得业务逻辑代码更加简洁和专注于核心业务功能的实现。

3.3.2 AOP 在公共字段填充中的应用

在苍穹外卖项目中,AOP 通过切面类 AutoFillAspect 实现了公共字段的自动填充,有效地将公共字段填充这一横切关注点从业务逻辑中分离出来,极大地提高了代码的简洁性和可维护性。下面我们来详细剖析其实现过程。


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    @Pointcut("@annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointcut() {}

    @Around("autoFillPointcut()")
    public Object autoFill(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("开始公共字段的自动填充");

        // 获取方法参数中的实体对象
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) {
            return joinPoint.proceed();
        }
        Object entity = args[0];

        // 获取当前登录用户ID(假设从ThreadLocal中获取)
        Long currentUserId = BaseContext.getCurrentId();

        // 获取操作类型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        AutoFill autoFill = method.getAnnotation(AutoFill.class);
        OperationType operationType = autoFill.value();

        if (operationType == OperationType.INSERT) {
            // 反射调用setter方法设置公共字段值
            Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
            Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
            Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

            setCreateTime.invoke(entity, LocalDateTime.now());
            setCreateUser.invoke(entity, currentUserId);
            setUpdateTime.invoke(entity, LocalDateTime.now());
            setUpdateUser.invoke(entity, currentUserId);
        } else if (operationType == OperationType.UPDATE) {
            // 反射调用setter方法设置公共字段值
            Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

            setUpdateTime.invoke(entity, LocalDateTime.now());
            setUpdateUser.invoke(entity, currentUserId);
        }

        // 执行原方法
        return joinPoint.proceed();
    }
}

首先,通过 @Aspect 注解将 AutoFillAspect 类标记为一个切面类,表明它将用于处理横切关注点。@Component 注解则将该切面类纳入 Spring 容器的管理,使其能够被正确地实例化和使用。

@Pointcut 注解定义了一个切点表达式,这里使用 @annotation (com.sky.annotation.AutoFill),表示切点为所有被 @AutoFill 注解标记的方法。这个切点表达式精准地定位了需要进行公共字段自动填充的业务方法,只有被 @AutoFill 注解标记的方法才会触发后续的自动填充逻辑。

@Around 注解定义了环绕通知,这是一种功能强大的通知类型,它可以在目标方法执行前后都执行自定义逻辑,并且可以控制目标方法是否执行以及如何执行。在 autoFill 方法中,首先获取方法参数中的实体对象,这里假设业务方法的第一个参数就是需要填充公共字段的实体对象。然后从 ThreadLocal 中获取当前登录用户 ID,ThreadLocal 是一种线程局部变量,它可以在同一个线程的不同方法中共享数据,这里用于存储当前登录用户的信息,确保在公共字段填充时能够准确获取到当前用户 ID。

接下来,通过反射获取被调用方法的签名(MethodSignature),进而获取方法对象(Method),并从方法上获取 @AutoFill 注解,从而确定当前的操作类型(OperationType)。根据不同的操作类型,使用反射调用实体类的相应 setter 方法,为公共字段赋值。在插入操作(INSERT)中,为 createTime、createUser、updateTime、updateUser 字段赋值;在更新操作(UPDATE)中,仅为 updateTime 和 updateUser 字段赋值。

最后,通过 joinPoint.proceed () 方法执行原业务方法,确保业务逻辑的正常执行,并返回原方法的执行结果。通过这种方式,AOP 在苍穹外卖项目中实现了公共字段填充逻辑与业务逻辑的解耦,使得代码结构更加清晰,维护更加方便。当公共字段的填充逻辑发生变化时,只需在切面类中进行修改,而无需逐一修改各个业务方法,大大提高了代码的可维护性和可扩展性。

3.4 反射机制

3.4.1 反射基础

Java 反射机制是 Java 语言提供的一种强大的功能,它允许程序在运行时动态地获取类的信息,并对类的成员(如字段、方法、构造函数等)进行操作。在传统的 Java 编程中,我们在编译阶段就已经确定了要使用的类和方法,程序按照预先定义好的逻辑执行。而反射机制打破了这种限制,它使得我们可以在运行时根据实际需求来加载类、创建对象、调用方法,甚至可以修改类的成员变量的值,这种动态性为 Java 程序的开发带来了极大的灵活性和扩展性。

通过反射,我们可以获取到类的 Class 对象,它是 Java 反射的核心入口。每个类在被加载到 JVM 中时,都会创建一个对应的 Class 对象,通过这个 Class 对象,我们可以获取类的各种信息,如类的名称、包名、父类、实现的接口、字段、方法等。获取 Class 对象的方式有多种,常见的有以下几种:

  • 使用类的 class 属性:Class clazz = String.class;

  • 使用对象的 getClass () 方法:String str = "hello"; Class clazz = str.getClass();

  • 使用 Class.forName () 方法:Class clazz = Class.forName("java.lang.String");

一旦获取到了 Class 对象,我们就可以利用它来获取类的各种信息和进行相应的操作。例如,通过clazz.getMethods()方法可以获取类的所有公共方法,通过clazz.getDeclaredFields()方法可以获取类的所有声明字段(包括私有字段),通过clazz.getConstructor()方法可以获取类的构造函数等。获取到这些成员信息后,我们可以通过反射来调用方法、访问和修改字段的值、创建对象等。反射机制虽然强大,但由于它是在运行时动态解析和执行,相比于直接调用方法和访问字段,会带来一定的性能开销,因此在使用时需要谨慎权衡,避免过度使用影响程序性能。

3.4.2 反射在公共字段填充中的应用

在苍穹外卖项目的公共字段填充功能中,反射机制发挥了关键作用,它使得我们能够在运行时动态地获取实体类的结构信息,并调用其 setter 方法为公共字段赋值,实现了公共字段填充逻辑的通用性和灵活性。下面结合代码详细说明其应用过程。


if (operationType == OperationType.INSERT) {
    // 反射调用setter方法设置公共字段值
    Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
    Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
    Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
    Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

    setCreateTime.invoke(entity, LocalDateTime.now());
    setCreateUser.invoke(entity, currentUserId);
    setUpdateTime.invoke(entity, LocalDateTime.now());
    setUpdateUser.invoke(entity, currentUserId);
} else if (operationType == OperationType.UPDATE) {
    // 反射调用setter方法设置公共字段值
    Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
    Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

    setUpdateTime.invoke(entity, LocalDateTime.now());
    setUpdateUser.invoke(entity, currentUserId);
}

在上述代码中,首先根据操作类型(operationType)判断是插入操作还是更新操作。当是插入操作时,通过反射获取实体类(entity.getClass ())的 setCreateTime、setCreateUser、setUpdateTime、setUpdateUser 方法。这里使用 getDeclaredMethod 方法,它可以获取类中声明的指定方法,包括私有方法,参数分别为方法名和方法参数类型的 Class 对象。例如,entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class)表示获取实体类中名为 setCreateTime,参数类型为 LocalDateTime 的方法。

获取到这些方法后,通过 invoke 方法来调用它们,为公共字段赋值。invoke 方法的第一个参数是方法的调用对象,这里为实体对象 entity;后面的参数是方法的实际参数值,如setCreateTime.invoke(entity, LocalDateTime.now())表示调用实体对象的 setCreateTime 方法,将当前时间(LocalDateTime.now ())作为参数传入,从而为实体类的 createTime 字段赋值。同理,为其他公共字段赋值也是通过类似的方式。

当是更新操作时,逻辑类似,只是只需要获取和调用 setUpdateTime 和 setUpdateUser 方法,为 updateTime 和 updateUser 字段赋值。通过这种方式,反射机制使得公共字段填充功能能够适应不同的实体类,只要实体类中定义了相应的公共字段和 setter 方法,就可以通过反射动态地为这些字段赋值,而无需为每个实体类编写特定的赋值代码,大大提高了代码的复用性和可维护性。即使在项目后期对实体类进行了修改或新增公共字段,也只需要确保实体类有相应的 setter 方法,公共字段填充功能就能自动适应这些变化,无需对公共字段填充的核心逻辑进行大规模修改。

四、公共字段填充功能的代码实现

4.1 自定义注解的代码实现

在苍穹外卖项目中,自定义注解 @AutoFill 是实现公共字段自动填充的关键起点,它的代码定义如下:


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();
}

在这段代码中,@Target (ElementType.METHOD) 明确指定了该注解只能应用于方法上,确保注解的使用范围精准无误,避免了在类、字段等其他元素上的误用,使得代码结构更加清晰,注解的语义更加明确。@Retention (RetentionPolicy.RUNTIME) 则规定了此注解在运行时仍然有效,这为后续通过反射机制获取注解信息并进行动态处理提供了必要条件。在运行时,JVM 能够识别并读取该注解,从而触发公共字段自动填充的逻辑。

value 属性是 @AutoFill 注解的核心属性,其类型为 OperationType,这是一个自定义的枚举类型。通过这个属性,我们可以为注解标记的方法指定具体的数据库操作类型,如插入(INSERT)或更新(UPDATE)。在使用 @AutoFill 注解时,必须为 value 属性赋值,例如 @AutoFill (OperationType.INSERT),这样系统就能根据不同的操作类型,准确地执行相应的公共字段填充逻辑,实现了公共字段填充的灵活性和针对性。

4.2 切面类的代码实现

AutoFillAspect 切面类是实现公共字段自动填充功能的核心组件,它借助 AOP 技术,将公共字段填充的逻辑从业务代码中分离出来,实现了代码的解耦和复用。以下是 AutoFillAspect 切面类的完整代码及详细解释:


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    @Pointcut("@annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointcut() {}

    @Around("autoFillPointcut()")
    public Object autoFill(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("开始公共字段的自动填充");

        // 获取方法参数中的实体对象
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) {
            return joinPoint.proceed();
        }
        Object entity = args[0];

        // 获取当前登录用户ID(假设从ThreadLocal中获取)
        Long currentUserId = BaseContext.getCurrentId();

        // 获取操作类型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        AutoFill autoFill = method.getAnnotation(AutoFill.class);
        OperationType operationType = autoFill.value();

        if (operationType == OperationType.INSERT) {
            // 反射调用setter方法设置公共字段值
            Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
            Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
            Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

            setCreateTime.invoke(entity, LocalDateTime.now());
            setCreateUser.invoke(entity, currentUserId);
            setUpdateTime.invoke(entity, LocalDateTime.now());
            setUpdateUser.invoke(entity, currentUserId);
        } else if (operationType == OperationType.UPDATE) {
            // 反射调用setter方法设置公共字段值
            Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

            setUpdateTime.invoke(entity, LocalDateTime.now());
            setUpdateUser.invoke(entity, currentUserId);
        }

        // 执行原方法
        return joinPoint.proceed();
    }
}
  • 注解声明:@Aspect 注解将 AutoFillAspect 类标记为一个切面类,使其具备处理横切关注点的能力。@Component 注解将该切面类纳入 Spring 容器的管理,确保它能够被正确实例化和使用。@Slf4j 注解则简化了日志记录的操作,方便我们在代码中记录关键信息和调试日志。

  • 切点定义:@Pointcut 注解定义了一个切点表达式 @annotation (com.sky.annotation.AutoFill),表示切点为所有被 @AutoFill 注解标记的方法。这意味着只有被 @AutoFill 注解标记的方法才会触发公共字段自动填充的逻辑,精准地定位了需要进行自动填充的业务方法。

  • 环绕通知:@Around 注解定义了环绕通知 autoFill 方法,它可以在目标方法执行前后执行自定义逻辑。在方法内部,首先记录日志表明公共字段自动填充开始。然后获取方法参数中的实体对象,这里假设业务方法的第一个参数就是需要填充公共字段的实体对象。如果参数为空,则直接执行原方法。接着从 ThreadLocal 中获取当前登录用户 ID,ThreadLocal 能够在同一个线程的不同方法中共享数据,确保获取到的当前用户 ID 准确无误。

通过反射获取被调用方法的签名(MethodSignature),进而得到方法对象(Method),并从方法上获取 @AutoFill 注解,从而确定当前的操作类型(OperationType)。根据不同的操作类型,使用反射调用实体类的相应 setter 方法,为公共字段赋值。在插入操作(INSERT)中,为 createTime、createUser、updateTime、updateUser 字段赋值;在更新操作(UPDATE)中,仅为 updateTime 和 updateUser 字段赋值。最后,通过 joinPoint.proceed () 方法执行原业务方法,并返回原方法的执行结果,确保业务逻辑的正常运行。

4.3 在 Mapper 方法上应用注解

在苍穹外卖项目中,为了触发公共字段自动填充功能,需要在 Mapper 接口的方法上添加 @AutoFill 注解。以 DishMapper 接口为例,假设我们有新增菜品和修改菜品的方法,应用注解后的代码如下:


import com.sky.annotation.AutoFill;
import com.sky.entity.Dish;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface DishMapper {

    @AutoFill(OperationType.INSERT)
    @Insert("INSERT INTO dish (name, category_id, price, description, image, create_time, update_time, create_user, update_user, status) VALUES (#{name}, #{categoryId}, #{price}, #{description}, #{image}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})")
    void insert(Dish dish);

    @AutoFill(OperationType.UPDATE)
    @Update("UPDATE dish SET name = #{name}, category_id = #{categoryId}, price = #{price}, description = #{description}, image = #{image}, update_time = #{updateTime}, update_user = #{updateUser}, status = #{status} WHERE id = #{id}")
    void update(Dish dish);
}

在上述代码中,insert 方法用于新增菜品,通过 @AutoFill (OperationType.INSERT) 注解标记,表明该方法在执行时需要进行插入操作类型的公共字段自动填充。同样,update 方法用于修改菜品,通过 @AutoFill (OperationType.UPDATE) 注解标记,表明该方法在执行时需要进行更新操作类型的公共字段自动填充。这样,当这些 Mapper 方法被调用时,AOP 切面类 AutoFillAspect 中定义的公共字段自动填充逻辑就会被触发,根据操作类型为菜品实体类的相应公共字段赋值,实现了公共字段填充的自动化和智能化,减少了手动赋值的繁琐操作,提高了代码的简洁性和可维护性。

五、其他菜品管理功能简介

5.1 新增菜品功能

新增菜品功能是苍穹外卖项目中菜品管理的重要组成部分,其业务流程主要包括接收前端传来的菜品数据,对数据进行校验和处理后,将菜品信息插入到数据库中。当商家在苍穹外卖平台的后台管理系统中添加新菜品时,前端会将包含菜品名称、分类 ID、价格、描述、图片、口味等详细信息的表单数据发送到后端。

后端首先会对这些数据进行严格的校验,确保数据的完整性和合法性。检查菜品名称是否为空、价格是否为正数、分类 ID 是否有效等。若数据校验通过,会将菜品数据封装成 Dish 对象,并调用 DishService 中的 save 方法。在 save 方法中,会先通过 DishMapper 将菜品的基本信息插入到数据库的 dish 表中。由于一个菜品可能对应多种口味,还会将菜品的口味信息插入到 dish_flavor 表中,通过菜品 ID 建立关联,确保口味信息与菜品的正确对应。 新增菜品功能涉及到数据校验、对象封装、数据库操作等多个技术点,通过各层之间的协作,实现了新菜品信息的准确录入和存储,为用户提供了丰富多样的菜品选择。

5.2 菜品分页查询(PageHelper)

在苍穹外卖项目中,随着菜品数量的不断增加,一次性获取所有菜品数据不仅会导致数据传输量过大,影响系统性能,还会给用户体验带来负面影响。因此,引入分页查询功能至关重要。PageHelper 是一款优秀的开源分页插件,它基于 MyBatis 框架开发,能够方便快捷地实现分页查询功能,极大地简化了开发过程。

使用 PageHelper 实现菜品分页查询,首先需要在项目中引入 PageHelper 的依赖。如果使用 Maven 构建项目,在 pom.xml 文件中添加如下依赖:


<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>最新版本号</version>
</dependency>

添加依赖后,还需要在 Spring 配置文件(如 application.yml 或 application.properties)中进行简单配置,设置分页插件的相关属性,如分页合理化、支持的方言等。以 application.yml 为例,配置如下:


pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql

在 DishService 的实现类中,使用 PageHelper 进行分页查询。假设我们有一个根据条件查询菜品列表并进行分页的方法 pageQuery,代码示例如下:


import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.mapper.DishMapper;
import com.sky.result.PageResult;
import com.sky.service.DishService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;

    @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        // 设置分页参数,pageNum为当前页码,pageSize为每页显示的记录数
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());

        // 执行查询,DishMapper中的list方法根据传入的条件查询菜品列表
        Page<Dish> page = dishMapper.list(dishPageQueryDTO);

        // 返回分页结果,PageResult封装了总记录数和当前页的数据列表
        return new PageResult(page.getTotal(), page.getResult());
    }
}

在上述代码中,首先通过 PageHelper.startPage 方法设置分页参数,包括当前页码和每页显示的记录数。然后调用 DishMapper 中的 list 方法,该方法根据传入的 DishPageQueryDTO 对象中的条件(如菜品名称、分类 ID、状态等)进行查询。PageHelper 会自动在 SQL 语句中添加分页相关的语法,实现分页查询。最后,将查询结果封装成 PageResult 对象返回,其中包含了总记录数和当前页的数据列表,前端可以根据这些数据进行分页展示,提升用户浏览菜品的体验和系统的性能。

5.3 删除菜品功能

删除菜品功能在苍穹外卖项目中用于移除不再销售或不需要的菜品,其实现逻辑需要综合考虑数据完整性和业务规则。当管理员在后台执行删除菜品操作时,系统首先会进行数据校验,判断该菜品是否存在关联数据。菜品可能与套餐存在关联,即该菜品被包含在某些套餐中。如果直接删除关联菜品,可能会导致套餐数据的不完整和异常,影响用户下单和商家运营。因此,在删除菜品之前,系统会查询 setmeal_dish 表,确认该菜品是否被任何套餐引用。若存在关联,会先解除套餐与菜品之间的关联关系,确保套餐数据的完整性。

当解除关联后,系统会从数据库中删除该菜品的相关记录。这包括从 dish 表中删除菜品的基本信息,以及从 dish_flavor 表中删除与该菜品对应的口味信息。在删除操作中,通常会使用事务来确保数据的一致性和完整性。如果在删除过程中出现任何异常,事务会回滚,保证数据库状态不会出现部分删除的情况,避免数据丢失或不一致的问题。通过这样严谨的实现逻辑,苍穹外卖项目的删除菜品功能既能满足业务需求,又能确保数据的稳定和可靠。

5.4 修改菜品功能

修改菜品功能允许管理员对已有的菜品信息进行更新,以满足菜品信息变更、价格调整、口味优化等业务需求。其实现过程主要包括接收前端传来的修改后的菜品数据,根据菜品 ID 查询数据库中的原菜品信息,将新数据与原数据进行合并和更新,最后将更新后的菜品信息保存到数据库中。

当管理员在苍穹外卖平台的后台管理系统中对某一菜品进行修改操作时,前端会将包含修改后菜品信息的 DishDTO 对象发送到后端。后端的 DishController 接收到请求后,会调用 DishService 中的 update 方法。在 update 方法中,首先会根据 DishDTO 中的菜品 ID 从数据库中查询出原菜品信息,包括菜品的基本信息和口味信息。然后,将 DishDTO 中的新数据与原数据进行合并,覆盖需要修改的字段,如菜品名称、价格、描述、口味等。在这个过程中,需要特别注意公共字段的更新逻辑。根据公共字段自动填充功能的设计,当执行更新操作时,系统会自动将 updateTime 设置为当前时间,将 updateUser 设置为当前登录用户 ID。这是通过之前介绍的自定义注解 @AutoFill、AOP 切面编程以及反射机制实现的。在 Mapper 接口的 update 方法上添加 @AutoFill (OperationType.UPDATE) 注解,AOP 切面类 AutoFillAspect 会在该方法执行时自动拦截,通过反射调用实体类的 setUpdateTime 和 setUpdateUser 方法,为公共字段赋值。

完成数据合并和公共字段更新后,调用 DishMapper 的 update 方法将更新后的菜品信息保存到数据库中。如果涉及到口味信息的修改,还会对 dish_flavor 表中的相关记录进行相应的更新操作,确保菜品信息的一致性和完整性。通过这样的实现过程,苍穹外卖项目的修改菜品功能能够准确、高效地更新菜品信息,满足商家和用户的需求。

六、总结与展望

6.1 功能回顾与优势总结

在苍穹外卖项目的菜品管理模块中,公共字段填充功能通过自定义注解、枚举、AOP 和反射的协同作用,实现了公共字段赋值逻辑的自动化和集中化管理。自定义注解 @AutoFill 精准标记需要公共字段填充的方法,枚举类 OperationType 明确操作类型,AOP 切面类 AutoFillAspect 利用反射机制在方法执行前后动态为公共字段赋值。

这种实现方式极大地提升了代码的复用性,避免了在多个业务方法中重复编写公共字段赋值代码,减少了代码冗余,使代码结构更加清晰简洁。同时,增强了代码的可维护性,当公共字段的赋值逻辑发生变化时,只需在切面类中进行修改,无需逐个调整业务方法,降低了维护成本和出错风险。

6.2 未来改进方向

尽管当前的公共字段填充功能已经较为完善,但仍有一些可优化和扩展的方向。在性能优化方面,由于反射操作在运行时解析和执行,存在一定的性能开销。未来可以考虑使用反射缓存机制,将反射获取的方法对象等信息进行缓存,避免重复获取,从而提升系统性能。

在功能扩展上,可以进一步完善公共字段的类型和应用场景。除了目前的创建时间、创建人、更新时间、更新人等字段,还可以根据业务需求,增加如数据版本号、数据状态描述等公共字段,以满足更复杂的业务需求。同时,考虑将公共字段填充功能扩展到更多的业务模块和实体类,实现整个项目层面的公共字段统一管理和自动化填充。

致谢

非常感谢您抽出宝贵的时间阅读这篇关于苍穹外卖项目菜品管理功能的技术博客。在撰写过程中,我力求将复杂的技术原理和实现过程清晰地呈现给大家。如果您在阅读过程中有任何疑问,或者对某些技术点有不同的见解,欢迎在评论区留言交流。您的反馈对我来说非常重要,它能帮助我不断提升内容质量,为大家带来更有价值的技术分享。同时,也希望这篇博客能对您在 Java 开发、尤其是项目中公共字段填充功能的实现方面有所启发和帮助。让我们一起在技术的道路上不断探索,共同进步!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值