西双版纳傣族自治州网站建设_网站建设公司_博客网站_seo优化
2025/12/22 20:10:59 网站建设 项目流程

苍穹外卖

菜品管理

公共字段自动填充

image-20251222121459437

image-20251222122019387

技术栈

  1. 基于切面编程AOP,定义注解实现为公共字段赋值(定义在sky-server)
  2. 枚举类
  3. 定义AOP切面类(定义在sky-server,用aspect包)
  4. 基于反射获取和设置数据
1.基于切面编程AOP,定义注解实现为公共字段赋值(定义在sky-server)
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 {//数据库操作类型:UPDATE INSERTOperationType value();
}
2.枚举类
package com.sky.enumeration;/*** 数据库操作类型*/
public enum OperationType {/*** 更新操作*/UPDATE,/*** 插入操作*/INSERT}
3.定义AOP切面类(定义在sky-server,用aspect包)
4.基于反射获取和设置数据(62-65行,68-71行)
package com.sky.aspect;import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.time.LocalDateTime;/*** 自定义切面,实现公共字段的字段填充*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段赋值*/@Before("autoFillPointCut()")public  void  autoFill(JoinPoint joinPoint){log.info("开始进行公共字段的自动填充");//获取当前被拦截的方法上的数据库操作类型MethodSignature signature = (MethodSignature) joinPoint.getSignature();//父接口强转为子接口,方法签名对象AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象OperationType operationType = autoFill.value();//获得数据库操作类型//获取当前被拦截的方法的参数--实体对象Object[] args = joinPoint.getArgs();if (args == null || args.length == 0){return;}Object entity = args[0];//准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();//根据当前不同操作类型,为对应的属性通过反射赋值if (operationType == OperationType.INSERT){//为四个公共字段赋值try {Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, long.class);//通过反射为对象赋值setCreateTime.invoke(entity,now);setCreateUser.invoke(entity,currentId);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}} else if (operationType == OperationType.UPDATE) {//为两个公共字段赋值try {Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, long.class);//通过反射为对象赋值setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}}
}

通过AOP切面赋值后需要在mapper层加上对应注解

EmployeeMapper为例,第27和第43行

package com.sky.mapper;import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.entity.Employee;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;@Mapper
public interface EmployeeMapper {/*** 根据用户名查询员工* @param username* @return*/@Select("select * from employee where username = #{username}")Employee getByUsername(String username);/*** 插入员工数据* @param employee*/@AutoFill(value = OperationType.INSERT)//OperationType是枚举类@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +"values (#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")void insert(Employee employee);/*** 分页查询* @param employeePageQueryDTO* @return*/Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);/*** 根据主键来动态修改属性* @param employee*/@AutoFill(value = OperationType.UPDATE)//OperationType是枚举类void update(Employee employee);/*** 根据id查询员工信息* @param id* @return*/@Select("select * from employee where id = #{id}")Employee getById(Long id);
}

技术问题

问题1:@Pointcut 和 @Before 的作用及设计意图
  • @Pointcut (切入点)
    • 作用:定义“哪些方法”需要被拦截。它像是一个过滤器或搜索条件。你代码中的表达式指定了:只要是 mapper 包下的方法,且贴了 @AutoFill 标签,就进入我们的视野。
    • 设计意图复用与统一管理。如果你有多个通知(比如 @Before@After)都要拦截同样的逻辑,只需要引用这个切入点方法名即可,不用到处重复写长长的表达式。
  • @Before (前置通知)
    • 作用:定义“什么时候”干活。它指定在目标方法(Mapper 里的 SQL 操作)执行之前,先执行这段自动填充逻辑。
    • 设计意图非侵入式增强。数据库操作需要完整的数据。在执行 SQL 之前把缺失的 create_time 等补齐,这样 Mapper 层的代码就不用写这些琐碎的赋值,实现了业务逻辑和公共逻辑的分离。

问题2:JoinPoint 是什么?
  • 定义JoinPoint 代表“连接点”,通俗地说,它就是被拦截到的那个“犯罪现场”。它包含了被拦截方法的所有信息。
  • 常用属性/方法
    1. getSignature():获取被拦截方法的签名(包括方法名、参数类型、返回类型等)。
    2. getArgs():获取调用该方法时传入的实际参数值(比如你传入的 Employee 对象)。
    3. getTarget():获取被拦截的目标对象(即具体的 Mapper 实例)。

问题3:MethodSignature 与 AutoFill
  • MethodSignature
    • 它是 Signature 的子接口。因为 AOP 可以拦截方法、构造函数等,Signature 比较通用。
    • 作用:专门用于获取方法层面的详细信息。
    • 常用方法getMethod()(获取 Method 对象,从而拿到注解)、getReturnType()(返回类型)、getParameterNames()(参数名)。
  • AutoFill
    • 这就是你之前定义的那个自定义注解
    • 属性/方法:在你的代码里它有一个 value() 方法,返回的是 OperationType(INSERT 或 UPDATE)。
    • 之所以在 getAnnotation() 后面加上 AutoFill.class,主要有两个原因:唯一性指向和类型安全(泛型转换)。

问题4:getDeclaredMethod 的原理
  • getDeclaredMethod 是什么: 这是 Java 反射 API。它通过方法名在类中查找对应的 Method 对象。
  • 为什么要加 LocalDateTime.class: 在 Java 中,方法重载(方法名相同,参数不同)是允许的。
    • 比如类里可能有 setCreateTime(LocalDateTime t),也可能有 setCreateTime(String t)
    • 只给方法名 SET_CREATE_TIME 是不够的,必须告诉反射机制:“我要找的是那个参数类型为 LocalDateTime 的方法”。这就是唯一确定一个方法的方式。

问题5:反射赋值的其他方法

除了 method.invoke(entity, value) 这种调用 setter 方法的方式,确实还有其他手段:

  1. 直接修改成员变量(Field): 不找 set 方法,直接找 createTime 属性:

    Java

    Field field = entity.getClass().getDeclaredField("createTime");
    field.setAccessible(true); // 暴力反射,允许访问私有变量
    field.set(entity, now);
    
    • 对比invoke 调用 setter 更安全、更符合 Java 规范;直接操作 Field 则更暴力,不需要类里定义 setter 方法也能成功。
  2. 使用 Spring 提供的工具类: Spring 封装了 BeanUtilsReflectionUtils,可以让代码更简洁:

    Java

    ReflectionUtils.invokeMethod(method, entity, now);
    
  3. 使用第三方库(如 Hutool 或 BeanCopier): 这些库底层也是反射,但处理了缓存,性能更好。

新增菜品

image-20251222135409211

image-20251222135458044

image-20251222135535042

image-20251222135755884

image-20251222135817780

image-20251222135903475

image-20251222140352471

image-20251222140451113

文件上传(阿里云)

技术栈

  1. 阿里云配置,定义在配置属性类com.sky.properties里面(来加载),需要在yml配置文件中统一定义,并定义一个对应的工具类如AliOssUtil,配置类OssConfiguration用于配置AliOssUtil对象(来创建对象)
1.阿里云配置,定义在配置属性类文件com.sky.properties里面,需要在yml配置文见中统一定义
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;}
1.yml配置文件一般不会暴露具体信息,会封装到生产yml也就是dev.yml
2.application-dev.yml
sky:datasource:driver-class-name: com.mysql.cj.jdbc.Driverhost: localhostport: 3306database: sky_take_outusername: rootpassword: rootalioss:endpoint: oss-cn-beijing.aliyuncs.comaccess-key-id: LTAI5t5hEbLq6RPAc5ihjWEHaccess-key-secret: vY9VD0hh9q7TcFU2H9AP9x8FPEA8hrbucket-name: sky-itcast-david-ai
3.application.yml(40-44行)
server:port: 8080spring:profiles:active: devmain:allow-circular-references: truedatasource:url: jdbc:mysql://localhost:3306/sky_take_outdriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 1234mybatis:#mapper配置文件mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.sky.entityconfiguration:#开启驼峰命名map-underscore-to-camel-case: truelogging:level:com:sky:mapper: debugservice: infocontroller: infosky:jwt:# 设置jwt签名加密时使用的秘钥admin-secret-key: itcast# 设置jwt过期时间admin-ttl: 7200000# 设置前端传递过来的令牌名称admin-token-name: token#阿里云OSSalioss:endpoint: ${sky.alioss.endpoint}access-key-id: ${sky.alioss.access-key-id}access-key-secret: ${sky.alioss.access-key-secret}bucket-name: ${sky.alioss.bucket-name}
4.配置类OssConfiguration用于配置AliOssUtil对象(来创建对象)
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//当没有这个bean再创建public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());}
}
5.AliOssUtil对象(工具类)用于在controller直接执行上传操作(老师提供的)
package com.sky.utils;import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;/*** 文件上传** @param bytes* @param objectName* @return*/public String upload(byte[] bytes, String objectName) {// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);try {// 创建PutObject请求。ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}//文件访问路径规则 https://BucketName.Endpoint/ObjectNameStringBuilder stringBuilder = new StringBuilder("https://");stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);log.info("文件上传到:{}", stringBuilder.toString());return stringBuilder.toString();}
}
6.controller层开发:涉及注入AliOssUtil,UUID使用
package com.sky.controller;import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.util.UUID;/*** 通用接口*/
@Slf4j
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
public class CommonController {@Autowiredprivate AliOssUtil aliOssUtil;/*** 文件上传* @param file* @return*/@ApiOperation("文件上传")@PostMapping("/upload")public Result<String> upload(MultipartFile file){//file参数名需要跟前端提交的参数名保持一致log.info("文件上传:{}",file);try {//原始文件名String originalFilename = file.getOriginalFilename();//截取原始文件名的后缀String extension = originalFilename.substring(originalFilename.lastIndexOf("."));String objectName = UUID.randomUUID().toString() + extension;//文件的请求路径String filePath = aliOssUtil.upload(file.getBytes(), objectName);return Result.success(filePath);} catch (IOException e) {log.error("文件上传失败:{}",e);}return Result.error(MessageConstant.UPLOAD_FAILED);}
}

技术栈

  1. 事务管理@Transactional(新增菜品),需要在启动类通过@EnableTransactionManagement ,开启注解方式的事务管理
  2. 动态SQL,for循环插入
  3. 主键返回获得dishId,useGeneratedKeys="true" keyProperty="id"
1.事务管理@Transactional(新增菜品),需要在启动类通过@EnableTransactionManagement ,开启注解方式的事务管理
package com.sky;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
public class SkyApplication {public static void main(String[] args) {SpringApplication.run(SkyApplication.class, args);log.info("server started");}
}

serviceimpl层进行事务管理

/*** 新增菜品和对应的口味* @param dishDTO*/
@Transactional
@Override
public void saveWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO,dish);//向菜品表插入一条数据dishMapper.insert(dish);//主键返回获得dishId//获取insert语句里面生成的主键值Long dishId = dish.getId();List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() >0){flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishId);});//向口味表插入n条数据dishFlavorsMapper.insertBatch(flavors);}
}
2.动态SQL,for循环插入在xml映射文件里面
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorsMapper"><insert id="insertBatch">insert into dish_flavor (dish_id, name, value)values<foreach collection="flavors" item="flavor" separator=",">(#{flavor.dishId},#{flavor.name},#{flavor.value})</foreach></insert>
</mapper>
3.主键返回获得dishId,useGeneratedKeys="true" keyProperty="id"(第7行)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper"><insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into dish (name, category_id, price, image, description, create_time, update_time, create_user, update_user, status)values(#{name},#{categoryId},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})</insert>
</mapper>

菜品分页查询

image-20251222163130807

image-20251222163200961

image-20251222163334750

image-20251222163418457

image-20251222163506399

技术栈

  1. 多表联查

菜品分页查询-controller层

/*** 菜品分页查询* @param dishPageQueryDTO* @return*/
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){log.info("菜品分页查询");PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);return Result.success(pageResult);
}

菜品分页查询-serviceimpl层

/*** 菜品分页查询* @param dishPageQueryDTO* @return*/
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);return new PageResult(page.getTotal(),page.getResult());
}

菜品分页查询-mapper层

/*** 菜品分页查询* @param dishPageQueryDTO* @return*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
1.多表联查
<select id="pageQuery" resultType="com.sky.vo.DishVO">select d.*,c.name category_name from dish d left outer join category c on d.category_id = c.id<where><if test="name != null">and d.name like concat('%',#{name},'%')</if><if test="categoryId != null">and d.category_id = #{categoryId}</if><if test="status != null">and d.status = #{status}</if></where>order by d.create_time desc
</select>

删除菜品

image-20251222170247004

image-20251222170609593

image-20251222170733674

image-20251222170751617

技术栈

  1. 前端传来的String类型数字通过@RequestParam 转换为List
  2. foreach的SQL生成数组,通过open="(" close=")"
1.前端传来的String类型数字通过@RequestParam 转换为List
/*** 菜品的批量删除* @param ids* @return*/
@DeleteMapping
@ApiOperation("菜品的批量删除")
public Result delete(@RequestParam List<Long> ids){//这里 @RequestParam 的作用就是将前端传来的字符串(如 1,2,3)或多个同名参数解析并填充到 List<Long> 集合中。log.info("菜品批量删除:{}",ids);dishService.deleteBatch(ids);return Result.success();
}
2.foreach的SQL生成数组,通过open="(" close=")"
<delete id="deleteByIds">delete from dish where id in<foreach collection="ids" item="id" separator="," open="(" close=")">#{id}</foreach>
</delete>

修改菜品

image-20251222190219965

image-20251222190237503

image-20251222190324439

image-20251222190407076

修改菜品-controller层

/*** 根据id修改菜品基本信息和对应的口味信息* @param dishDTO* @return*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO  dishDTO){log.info("修改菜品:{}",dishDTO);dishService.updateWithFlavor(dishDTO);return Result.success();
}

修改菜品-serviceimpl层

/*** 根据id修改菜品基本信息和对应的口味信息* @param dishDTO*/
@Transactional
@Override
public void updateWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO,dish);//修改菜品表基本信息dishMapper.update(dish);//删除原有的口味数据dishFlavorsMapper.deleteByDishId(dishDTO.getId());//重新插入口味数据List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() >0){flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishDTO.getId());});//向口味表插入n条数据dishFlavorsMapper.insertBatch(flavors);}
}

修改菜品-mapper层

老代码:flavor先删后添加,先delete然后insert

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询