D3
本文记录「苍穹外卖」项目开发中的关键技术实践与踩坑思考,包含个人在实际开发中的具体过程 与遇到的问题 以及知识点总结
希望可以给一起学习的大家带来帮助
文章目录
1.公共字段自动填充
问题分析

实现思路

- 自定义注解,用于标识需要进行公共字段自动填充的方法
- 自定义切面类,统一拦截加入了该注解的方法,通过反射为公共字段赋值
- 在Mapper的方法上加入该注解
技术点:枚举、注解、AOP、反射
1.自定义注解@AutoFill
java
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 INSERT
OperationType value();
}
为什么用
value()这种"方法"形式?→ 这是 Java 为注解设计的特殊语法规则:用无参方法声明属性
2.自定义切面类
java
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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()")
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("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,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("setUpdateTime", LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else {}
}
}
3.剩余操作:
在Mapper层相关方法上加上注解
eg:@AutoFill(value = OperationType.UPDATE)
删除Service层多余代码
2.新增菜品
业务分析:
- 菜品名称必须唯一
- 菜品必须属于某分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
接口设计:
- 根据类型查询分类
- 文件上传
- 新增菜品
2.1文件上传
在application.yml文件里添加阿里云配置
yml
sky:
//... ...
alioss:
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}
在-dev文件配置各项具体参数
AliOssProperties作用是 :自动把 application.yml 中 sky.alioss 开头的配置,读取并绑定到 Java 对象里
java
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
AliOssUtil作用是:上传文件到阿里云
具体操作步骤为:
- 创建 OSS 客户端对象
→ 用endpoint、AccessKeyId、AccessKeySecret构建一个OSS实例(相当于"登录"到 OSS) - 发起上传请求
→ 调用ossClient.putObject(bucketName, objectName, inputStream)
→ 相当于告诉 OSS:"把这段数据存到这个桶里的这个路径" - 拼接公开访问 URL(可选)
→ 按照格式:https://<bucketName>.<endpoint>/<objectName>
→ 这个链接就能在浏览器直接访问文件(前提是 bucket 是 public-read)
java
@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/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
OssConfiguration配置类作用是:把AliOssUtil的对象注入IOC容器,在此过程中把id和密钥啥的传给对象
java
//配置类用于创建AliOssUtil
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean //只有当Spring容器中还没有这个Bean时,才创建
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象");
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
注解 角色 类比 @ConfigurationProperties配置数据搬运工 把 YML 里的值"搬进"Java 对象 @ConfigurationBean 工厂 用这些值"生产"出可用的组件
Controller层
新建一个CommonController,编写操作文件上传的方法
java
RestController
@RequestMapping("/admin/common")
@Slf4j
public class CommonController{
@Autowired
AliOssUtil aliOssUtil;
//文件上传
@PostMapping("/upload")
public Result<String> upload(MultipartFile 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 null;
}
}
问题 答案 Mapper 为什么必须是接口? 因为 MyBatis 依赖 JDK 动态代理,只能代理接口 写成 class 会怎样? MyBatis 无法识别,不能注入,XML 不生效,方法调用无效 还能链接 XML 吗? ❌ 不能!XML 的 namespace只对接口生效
2.2新增菜品
插入菜品
1.创建一个DishController
java
@RestController
@RequestMapping("/admin/dish")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
//新增菜品
@PostMapping
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}",dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
2.创建DishService接口和实现类DishServiceImpl
java
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
//新增菜品和对应的口味
@Transactional
@Override
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//向菜品表插入1条数据
dishMapper.insert(dish);
//获取刚才插入的菜品的id
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0){
//向口味表插入n条数据
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
dishFlavorMapper.insertBatch(flavors);
}
}
}
3.实现Mapper接口
java
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
//插入菜品数据
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
}
4.编写xml文件
java
<?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, status, create_time, update_time, create_user, update_user)
values
(#{name},#{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>
</mapper>
插入口味
1.新增DishFlavorMapper接口
java
@Mapper
public interface DishFlavorMapper {
//批量插入口味数据
void insertBatch(List<DishFlavor> flavors);
}
2.编写DishFlavorMapper的xml文件
批量插入口味
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.DishFlavorMapper">
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) values
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
3.菜品分页查询
1.在DishMapper增加分页查询的接口
java
//菜品分页查询
@GetMapping("/page")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
2.在DishServiceImpl里增加
java
//菜品分页查询
@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());
}
3.在Mapper层增加
java
//菜品分页查询
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
4.在xml文件中增加
xml
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.* , c.name as categoryName 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>
4.删除菜品
业务规则:
- 可以一次删除一个菜品,也可以批量删除多个菜品
- 起售的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除
1.Controller层
java
//菜品删除
@DeleteMapping
public Result delete(@RequestParam List<Long> ids){
log.info("菜品批量删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
@RequestParam
功能 说明 参数绑定 将请求参数(query string / form data)映射到方法参数 类型转换 自动把字符串转成 int、Long、Date等(失败会报 400)
2.Service层
java
//菜品批量删除
@Transactional
@Override
public void deleteBatch(List<Long> ids) {
//判断能否删除-是否存在起售中的菜品
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if(dish.getStatus() == StatusConstant.ENABLE){
//起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断能否删除-是否被套餐关联
List<Long> setmealIds = setmealDishmapper.getSetmealIdsByDishId(ids);
if(setmealIds != null && setmealIds.size() > 0){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
//删除菜品表中的数据
for (Long id : ids) {
dishMapper.deleteById(id);
//删除口味表的数据
dishFlavorMapper.deleteByDishId(id);
}
}
3.DishMapper
java
//根据主键查询菜品
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
//根据id删除数据
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);
4.增加SetmealDishMapper
java
@Mapper
public interface SetmealDishMapper {
//根据菜品id查对应套餐id
public List<Long> getSetmealIdsByDishId(List<Long> dishIds);
}
5.DishFlavorMapper
java
//根据菜品id删除数据
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long dishId);
5.修改菜品
- 根据id查询菜品
- 根据类型查询分类
- 文件上传
- 修改菜品
- 修改菜品基本数据
- 修改口味------先删除再插入
对象 全称 作用 使用方向 DishDTOData Transfer Object(数据传输对象) 接收前端传来的请求数据 前端 → 后端(输入) DishVOView Object(视图对象) 封装返回给前端的响应数据 后端 → 前端(输出)
1.Controller层
java
//修改菜品
@PutMapping
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
2.ServiceImpl
java
//修改菜品基本信息和口味信息
@Transactional
@Override
public void updateWithFlavor(DishDTO dishDTO) {
//修改菜品基本信息
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
dishMapper.update(dish);
//修改口味信息-先删除再修改
dishFlavorMapper.deleteByDishId(dishDTO.getId());
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
}
dishFlavorMapper.insertBatch(flavors);
}
3.DishMapper
java
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);
4.xml文件
xml
<update id="update">
update dish
<set>
<if test="name != null">name = #{name},</if>
<if test="categoryId != null">category_id = #{categoryId},</if>
<if test="price != null">price = #{price},</if>
<if test="image != null">image = #{image},</if>
<if test="description != null">description = #{description},</if>
<if test="status != null">status = #{status},</if>
<if test="updateTime != null">update_Time = #{updateTime},</if>
<if test="updateUser != null">update_User = #{updateUser},</if>
</set>
where id = #{id}
</update>
5.DishFlavorMapper
java
//根据菜品id删除数据
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long dishId);
6.起售停售
- DishController
java
/**
* 菜品起售停售
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品起售停售")
public Result<String> startOrStop(@PathVariable Integer status, Long id){
dishService.startOrStop(status,id);
return Result.success();
}
- DishService
java
/**
* 菜品起售停售
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
- DishServiceImpl
java
/**
* 菜品起售停售
*
* @param status
* @param id
*/
@Transactional
public void startOrStop(Integer status, Long id) {
Dish dish = Dish.builder()
.id(id)
.status(status)
.build();
dishMapper.update(dish);
if (status == StatusConstant.DISABLE) {
// 如果是停售操作,还需要将包含当前菜品的套餐也停售
List<Long> dishIds = new ArrayList<>();
dishIds.add(id);
// select setmeal_id from setmeal_dish where dish_id in (?,?,?)
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
if (setmealIds != null && setmealIds.size() > 0) {
for (Long setmealId : setmealIds) {
Setmeal setmeal = Setmeal.builder()
.id(setmealId)
.status(StatusConstant.DISABLE)
.build();
setmealMapper.update(setmeal);
}
}
}
}
- SetmealMapper
java
/**
* 根据id修改套餐
*
* @param setmeal
*/
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
- SetmealMapper.xml
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.SetmealMapper">
<update id="update" parameterType="Setmeal">
update setmeal
<set>
<if test="name != null">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>
</mapper>
