# java-pro-01 **Repository Path**: sun-weichen-1/java-pro-01 ## Basic Information - **Project Name**: java-pro-01 - **Description**: 实战--外卖项目 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-11-09 - **Last Updated**: 2024-11-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 创建项目工程 ## 1. 创建项目工程 ### 1.1 项目分模块开发 - pojo 数据模型 - common 公共模块 - service 服务模块 - web 前端模块 ## 2. 配置文件 ### 2.1 配置父级pom文件 - 1、配置parent继承spring-boot-starter-parent - 2、配置检查modules的子模块是否齐全 - **父级pom只起到统一管理依赖版本作用,不引入依赖** ### 2.2 配置子模块pom文件 - 子模块用什么就加什么 ## 3、补充说明 - 1、pojo模块: - domain:实体类 - vo:视图对象 - dto:传输对象 - 2、common模块: - results:统一封装返回结果 - utils :工具类 - exception:自定义异常 - constant:常量 - context:get和set方法等封装 - json:json工具类 - propertis:自定义配置文件 - 3、service模块: - handler:异常处理 - interceptor:自定义拦截器 - config:配置类 - 注册自定义拦截器 # 补充 ### 1、接口文档四要素: - 请求地址 - 请求方式 - 请求参数 - 请求响应 ### 接口文档 --- SL: - 1、接口文档的请求参数是否是对象,是对象封装到pojo模块的dto实体类中。 - 2、接口文档的请求响应,需要什么就给什么数据,也封装到pojo模块的vo实体类中。 - 注:dto --- 控制层接收前端参数的对象实体类;vo是业务层处理好数据返回给前端的对象模型。 # 登录功能 ## 分析 - 1、**Path:** /admin/employee/login - 2、**Method:** POST - 3、**请求参数**: | | | | | | | | -------- | ------ | -------- | ------ | ------ | ---- | | 名称 | 类型 | 是否必须 | 默认值 | 备注 | | | password | string | 必须 | | 密码 | | | username | string | 必须 | | 用户名 | | - 4、**返回数据** | | | | | | | | ----------- | ------- | -------- | ------ | ---------------------- | ------------- | | 名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 | | code | integer | 必须 | | | format: int32 | | data | object | 非必须 | | 员工登录返回的数据格式 | | | ├─ id | integer | 非必须 | | 主键值 | format: int64 | | ├─ name | string | 非必须 | | 姓名 | | | ├─ token | string | 非必须 | | jwt令牌 | | | ├─ userName | string | 非必须 | | 用户名 | | | msg | string | 非必须 | | | | ## 请求参数 -- 封装成dto * DTO类都要继承Serializable接口,进行序列化操作。 ```java package com.weichen.dto; import lombok.Data; import java.io.Serializable; @Data public class EmployeeLoginDTO implements Serializable { private String username; private String password; } ``` ## 请求响应 --- 封装成VO对象 - vo类继承Serializable接口,进行序列化操作。 ``` package com.weichen.vo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class EmployeeLoginVO implements Serializable { private Long id; private String name; private String token; private String userName; } ``` ## 登录逻辑 - 1、查询是否有该用户名; - 2、判断数据库查询结果是否为null; - null则进行未查到该用户名的异常处理; - 非null则进行密码比对; - 比对时先把请求参数密码转成md5再比对数据库密码;(因为密码是md5加密后存入数据库) - 密码错误则进行密码不正确异常处理; - 账户禁用或启用状态异常处理; - 密码正确直接将实体对象return出去; - 登陆成功后需要生成JWT令牌,且存入到token; - 拿到处理完后的数据,封装成VO对象,将VO对象返回给前端。 - 总结: - controller层有返回值就直接返回的对象是VO封装好的实体类。 - 借助工具JWTUtils工具生成令牌token过程: - ```java Map claims = new HashMap<>(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT( jwtProperties.getAdminSecretKey() , jwtProperties.getAdminTtl() , claims); ``` - 异常集中管理 - 先设置基础异常类 继承RuntimeException; - ```java package com.weichen.exception; /** * 业务异常 */ public class BaseException extends RuntimeException { public BaseException() { } public BaseException(String msg) { super(msg); } } ``` - 根据不同异常,分别设置一个类进行返回。 - 账号不存在异常 - 账号被锁定异常 - 密码不正确异常 - ... # 新增员工 ## 分析 - 1、**Path:** /admin/employee - 2、**Method:** POST - 3、请求参数 | | | | | | | | -------- | ------- | -------- | ------ | ------ | ------------- | | 名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 | | id | integer | 非必须 | | 员工id | format: int64 | | idNumber | string | 必须 | | 身份证 | | | name | string | 必须 | | 姓名 | | | phone | string | 必须 | | 手机号 | | | sex | string | 必须 | | 性别 | | | username | string | 必须 | | 用户名 | | + 4、请求响应 | | | | | | | ---- | ------- | -------- | ------ | ---- | | 名称 | 类型 | 是否必须 | 默认值 | 备注 | | code | integer | 必须 | | | | data | object | 非必须 | | | | msg | string | 非必须 | | | ## 请求参数 --- 封装成DTO对象 ```java package com.weichen.dto; import lombok.Data; @Data public class EmployeeDTO { private Long id; private String username; private String name; private String phone; private String sex; private String idNumber; } ``` ## 请求响应 --- 无返回的数据,不用封装 ... ## 新增员工逻辑 - 1、调用业务层新增方法 - 2、补全缺失的属性值: - 1、创建一个员工实体类Emploee - 2、调用BeanUtils方法将收到的前端参数复制给Emploee - 3、开始补全实体类剩下的null的缺失属性值 - 其中的CreateUser和UpdateUser的ID,获取请求头中token里的id。 - 将token的id存入到ThreadLocal线程变量中(本项目将线程set和get方法封装到context中。) - 在补全的时候,调用get方法获取id赋值给实体对象即可。 - 4、调用mapper新增方法将员工对象employee存入数据库表中。 - 5、完善代码,处理(员工已存在)异常 - ```java @ExceptionHandler public Result doSQLException(SQLIntegrityConstraintViolationException ex) { log.error("异常信息:{}", ex.getMessage()); String message = ex.getMessage(); if (message.contains("Duplicate")) { return Result.error(message.split(" ")[2]+ MessageConstant.USERNAME_ALREADY_EXIST); } return Result.error(MessageConstant.UNKNOWN_ERROR); } ``` # 退出登录 ## 分析 - 1、**Path:** /admin/employee/logout - 2、**Method:** POST - 3、**请求参数**:无 - 4、**请求响应**: | | | | | | | ---- | ------- | -------- | ------ | ---- | | 名称 | 类型 | 是否必须 | 默认值 | 备注 | | code | integer | 必须 | | | | data | string | 非必须 | | | | msg | string | 非必须 | | | ## 请求参数 --- 无 ## 请求响应 --- 无 # 员工分页查询 ## 分析 - 1、**Path:** /admin/employee/page - 2、**Method:** GET - 3、**请求参数**: | 参数名称 | 是否必须 | 示例 | 备注 | | -------- | -------- | ---- | ---------- | | name | 否 | 张三 | 员工姓名 | | page | 是 | 1 | 页码 | | pageSize | 是 | 10 | 每页记录数 | - 4、**请求响应**: | | | | | | | ------------- | ----------- | -------- | ------ | ---- | | 名称 | 类型 | 是否必须 | 默认值 | 备注 | | code | number | 必须 | | | | msg | null | 非必须 | | | | data | object | 必须 | | | | ├─ total | number | 必须 | | | | ├─ records | object [] | 必须 | | | | ├─ id | number | 必须 | | | | ├─ username | string | 必须 | | | | ├─ name | string | 必须 | | | | ├─ password | string | 必须 | | | | ├─ phone | string | 必须 | | | | ├─ sex | string | 必须 | | | | ├─ idNumber | string | 必须 | | | | ├─ status | number | 必须 | | | | ├─ createTime | string,null | 必须 | | | | ├─ updateTime | string | 必须 | | | | ├─ createUser | number,null | 必须 | | | | ├─ updateUser | number | 必须 | | | ## 请求参数 --- 封装DTO对象 ```java package com.sky.dto; import lombok.Data; import java.io.Serializable; @Data public class EmployeePageQueryDTO implements Serializable { //员工姓名 private String name; //页码 private Integer page = 1; //每页显示记录数 private Integer pageSize = 10; } ``` ## 请求响应 --- 封装PageResult对象 ```java package com.sky.result; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; /** * 封装分页查询结果 */ @Data @AllArgsConstructor @NoArgsConstructor public class PageResult implements Serializable { private long total; //总记录数 private List records; //当前页数据集合 } ``` ## 员工分页查询逻辑 - 1、收到请求参数,调用业务层查询方法,得到结果封装成PageResult对象格式,返回给前端。 - 2、业务层中,使用PageHelper分页组件,传入页数和行数。 - 3、使用Page封装mapper查询的结果,封装成PageResult对象格式返回给控制层。 - 4、查询mapper需要动态查询,实现模糊查询效果、 - 5、其中mapper放在resource资源中,在yml中配置```mapper-locations: classpath:mapper/*.xml```指定mapper路径。 - 分页查询总结: - 会和模糊查询、分页组件一起使用 - 模糊查询一定会用动态SQL查询实现 - 需要封装分页查询响应结果的统一格式total和records ## 分页查询日期格式问题 - 在config配置了继承类WebMvcConfigurationSupport,会自动序列化,导致日期格式不对。 - 两种解决方式: - 1、注解 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - 简单书写,但是后期每一个controller都要重复写一遍,比较繁琐,忘记写容易导致日期格式问题。 - 2、扩展消息转换器,统一自动转换日期格式。 - 手动扩展springMVC的消息转换器,统一对日期进行格式化处理: - 封装JacksonObjectMapper对象映射器 ```java package com.weichen.json; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; /** * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] */ public class JacksonObjectMapper extends ObjectMapper { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; public JacksonObjectMapper() { super(); //收到未知属性时不报异常 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); //反序列化时,属性不存在的兼容处理 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule() .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); //注册功能模块 例如,可以添加自定义序列化器和反序列化器 this.registerModule(simpleModule); } } ``` + 在继承WebMvcConfigurationSupport类的WebConfiguration中重写方法extendMessageConverters实现日期格式转换 ```java /** * 扩展SpringMVC消息转换器 * @param converters */ protected void extendMessageConverters(List> converters) { log.info("开始自定义消息转换器..."); // 创建消息转换器对象 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); // 设置自定义的消息对象转换器,底层使用Jackson将Java对象转为json converter.setObjectMapper(new JacksonObjectMapper()); // 将自定义的消息对象转换器设置为优先级最高 converters.add(0, converter); } ``` # 员工的启用和禁用 ## 分析 - 1、**Path:** /admin/employee/status/{status} - 2、**Method:** POST - 3、**请求参数**: - **路径参数** | 参数名称 | 示例 | 备注 | | -------- | ---- | --------------------- | | status | 1 | 状态,1为启用 0为禁用 | - query参数 | 参数名称 | 是否必须 | 示例 | 备注 | | -------- | -------- | ---- | ------ | | id | 是 | | 员工id | - 4、**请求响应**: | 名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 | | ---- | ------- | -------- | ------ | ---- | ------------- | | code | integer | 必须 | | | format: int32 | | data | string | 非必须 | | | | | msg | string | 非必须 | | | | ## 请求参数 --- 参数少 不用封装 ## 请求响应 --- 无返回值 ## 禁用启用逻辑 - 是属于修改操作,考虑后面有修改员工信息,将其修改接口和后面的员工修改信息接口结合,不用重复写。 - 路径参数类似/{xxx},形参前面需要加注解,query传参的id直接写形参中即可。```@PathVariable Integer status, Long id``` - 使用builder注解,链式赋值简化赋值操作。 - ```java Employee employee = Employee.builder() .status(status) .id(id) .build(); ``` # 编辑员工信息 ## 分析 - 1、**Path:** /admin/employee - 2、**Method:** PUT - 3、请求参数: | | | | | | | | -------- | ------- | -------- | ------ | ---- | ------------- | | 名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 | | id | integer | 必须 | | | format: int64 | | idNumber | string | 必须 | | | | | name | string | 必须 | | | | | phone | string | 必须 | | | | | sex | string | 必须 | | | | | username | string | 必须 | | | | - 4、请求响应:无 ## 请求参数 --- 封装成DTO对象(前面已经用过,直接用即可) ```java package com.weichen.dto; import lombok.Data; @Data public class EmployeeDTO { private Long id; private String username; private String name; private String phone; private String sex; private String idNumber; } ``` ## 请求响应 --- 无返回值 ## 编辑员工逻辑 - 修改编辑操作,永远都有更新时间和更新修改人id操作。 - 修改编辑操作,肌肉记忆:创建类实体,用BeanUtils复制参数的属性值,再补充属性。 - 类似更新修改操作,考虑集中用update方法,和前面的禁用和启用共用一个mapper,简化代码操作。 # 分类模块 ## 业务设计 - 分类名称唯一 - 分类两种:菜品和套餐分类 - 新增的分类状态默认为禁用状态 ## 接口设计 - 新增 - 查询 - id删除 - 注:判断分类下是否有菜品,有就不能删 - 修改 - 启用禁用 - 根据类型查询 和上面的员工模块差不多操作。此模块直接省略不写笔记。直接参考员工模块即可。 .............................................................................. ## 总结 - 删除操作时,先判断该分类是否关联其他外键,有就不可删除,没有就可以删除、 - 多个表一一判断是否有关联外键的,count直接覆盖写法即可。 ```java public void deleteById(Long id) { // 查询该分类下面是否有菜品或者套餐 有就不能删除 Integer count = dishMapper.countByCategoryId(id); if (count > 0){ log.info("该分类下有菜品,不能删除"); throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH); } count = setmealMapper.countByCategoryId(id); if (count>0){ log.info("该分类下有套餐,不能删除"); throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL); } categoryMapper.deleteById(id); } ``` ## 补充优化代码(创建、更新时间等公共字段自动填充) 业务表中,新增修改操作,都会涉及到添加和更新公共字段。(create_time和update_time) - 思路:抽取共同逻辑方法 --- AOP切面思想 - 使用AOP --- 先考虑选择哪一种通知类型?(环绕、前置、后置、异常等类型) - 对mapper层进行增强,使用AOP的前置通知类型。 - 调用mapper之前,将缺失的公共字段属性值自动补充后再触发mapper内部的方法。 - 用哪一个切入点表达式? - 注解@annotation方式 --- 因为可能因为不同人命名规范不同,所以用自定义命名方式来解决该问题。 - 确定好AOP的通知类型和切入点表达式后,还需要考虑判断是新增还是更新操作。 - 判断方法上的注解的属性值即可 - 其中新增就自动补充字段create_time和update_time - 如果是更新修改就自动填充字段update_time - 代码实现 - 自定义注解AutoFill --- 表示需要进行公共字段自动填充的方法 ![1731246405909](D:\A-weichen_sun\Java\weichen-project-01\assets\1731246405909.png) - 自定义切面类AutoFillAspect,统一拦截加入AutoFill注解的方法,利用反射来给公共字段进行赋值操作。 ![1731246455117](D:\A-weichen_sun\Java\weichen-project-01\assets\1731246455117.png) - 在Mapper方法上加入AutoFill注解 ![1731246484384](D:\A-weichen_sun\Java\weichen-project-01\assets\1731246484384.png) ## 公共字段自动填充切面类 ```java package com.weichen.aspect; import com.weichen.annotation.AutoFill; import com.weichen.constant.AutoFillConstant; import com.weichen.context.BaseContext; import com.weichen.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.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.LocalDateTime; // 公共字段自动填充切面类 @Slf4j @Aspect @Component public class AutoFillAspect { @Before("@annotation(com.weichen.annotation.AutoFill)") // JoinPoint 包含了目标的所有方法信息 public void autoFill(JoinPoint joinPoint){ log.info("开始公共字段自动填充"); // 1、先获取目标方法中的注解,并且拿到注解中的属性值 // 目标方法签名 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 拿到目标方法 Method method = methodSignature.getMethod(); // 拿到注解 AutoFill autoFill = method.getAnnotation(AutoFill.class); // 拿到注解中的属性值 (INSERT OR UPDATE) OperationType operationType = autoFill.value(); // 2、获取到目标方法的参数对象 Object[] args = joinPoint.getArgs(); // 拿到实体对象 domain if (args == null || args.length == 0){ return; } Object domain = args[0]; // 拿到实体对象domain后进行字段填充 try { if (operationType == OperationType.INSERT){ // 3、如果是INSERT,自动填充4个字段(创建、更新时间和创建、更新人id) log.info("开始进行公共字段自动填充"); Method setCreateTime = domain.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = domain.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME ,LocalDateTime.class); Method setCreateUser = domain.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateUser = domain.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setCreateTime.invoke(domain, LocalDateTime.now()); setUpdateTime.invoke(domain, LocalDateTime.now()); setCreateUser.invoke(domain, BaseContext.getCurrentId()); setUpdateUser.invoke(domain, BaseContext.getCurrentId()); }else if (operationType == OperationType.UPDATE){ // 4、如果是UPDATE,自动填充一个字段(更新时间、更新人id) log.info("开始进行公共字段自动填充"); Method setUpdateTime = domain.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME ,LocalDateTime.class); Method setUpdateUser = domain.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setUpdateTime.invoke(domain, LocalDateTime.now()); setUpdateUser.invoke(domain, BaseContext.getCurrentId()); } } catch (Exception e) { throw new RuntimeException(e); } } } ``` # 菜品管理模块 ## 菜品管理 - 文件上传 - 用到阿里云OSS对象存储,配置文件 ```xml #阿里云oss配置 alioss: endpoint: ${weichen.alioss.endpoint} access-key-id: ${weichen.alioss.access-key-id} access-key-secret: ${weichen.alioss.access-key-secret} bucket-name: ${weichen.alioss.bucket-name} ``` - 封装工具类AliOssUtil ```java package com.weichen.utiles; 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/ObjectName StringBuilder stringBuilder = new StringBuilder("https://"); stringBuilder .append(bucketName) .append(".") .append(endpoint) .append("/") .append(objectName); log.info("文件上传到:{}", stringBuilder.toString()); return stringBuilder.toString(); } } ``` - 扫描配置文件类 ![1731307433144](D:\A-weichen_sun\Java\weichen-project-01\assets\1731307433144.png) - 设置创建OSS环境对象调用文件上传工具类对象 ![1731307548781](D:\A-weichen_sun\Java\weichen-project-01\assets\1731307548781.png) - 文件上传接口设计 ```java package com.weichen.controller; import com.weichen.result.Result; import com.weichen.utiles.AliOssUtil; 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.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") public class CommonController { @Autowired private AliOssUtil aliOssUtil; @PostMapping("/upload") public Result upload(MultipartFile file) throws IOException { log.info("文件上传:{}", file.getOriginalFilename()); String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String url = null; ty { String objectName = UUID.randomUUID().toString() + suffix; url = aliOssUtil.upload(file.getBytes(),objectName); } catch (IOException e) { log.info("文件上传失败:{}", e.getMessage()); return Result.error("上传失败"); } return Result.success(url); } } ``` ## 菜品管理 - 新增菜品 - 此次的新增,设计两张表,多表增删改需要加事务管理注解@Transactional - 前端返回的参数,涉及两张表时,封装的DTO属性中另外多加一个附表list集合private List flavors - 用BeanUtils.copyProperties(dishDTO, dish);补充属性 - 剩下的属性用公共自动填充字段 - 创建封装附表集合,将前端参数中的list集合复制过来 - 进行判断list集合是否为空 - 涉及到附表集合时,首先做的是循环foreach集合来关联id - 在进行mapper查询 ```java @Transactional // 涉及到多张表的操作,需要开启事务 @Override public void addDish(DishDTO dishDTO) { // 构造菜品基本信息 Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); dishMapper.insert(dish); // 构造菜品口味基本信息 List dishFlavors = dishDTO.getFlavors(); if (dishFlavors!=null && dishFlavors.size()>0){ // 关联菜品id dishFlavors.forEach(item -> { item.setDishId(dish.getId()); }); dishFlavorsMapper.insertDishFlavorBatch(dishFlavors); } } ``` ## 菜品管理 - 分页查询 多表查询时,其中模糊查询的条件有几个就写几个if判断 分页查询业务层:三元老(PageHelper+Page+return new PageResult( ) ) 如果查询的结果不是全部,是表中的某部分,考虑封装成VO对象集合,前端要什么数据就给什么属性和属性值。 ## 菜品管理 - 批量删除菜品 - 涉及到批量操作,开启事务@Transactional - 涉及到批量操作,参数是数组形式的ids - 业务层操作,需要遍历ids - 遍历ids,查询是否存在状态为1(启售状态)的菜品,有就不能删除,抛出异常。 - 如果没有,进一步判断是否有关联其他表的外键,用count记录数量,有则不能删除,抛出异常。 - 正常删除菜品 - 正常删除菜品中的对应菜品的口味集合。 - ```java @Transactional // 涉及到多张表的操作,需要开启事务 @Override public void deleteByIds(List ids) { log.info("批量删除菜品,id为:{}",ids); // 删除菜品之前,判断菜品是否启售,启售则不能删除 ids.forEach(id -> { Dish dish = dishMapper.getById(id); if(dish.getStatus() == StatusConstant.ENABLE){ throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } }); // 2.需要判断菜品是否被套餐关联,关联了也不允许删除 Integer count = setmealMapper.countByDishId(ids); if (count>0){ throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } dishMapper.deleteByIds(ids); dishFlavorsMapper.deleteByDishId(ids); } ``` ## 菜品管理 - 修改菜品信息 ### 修改之前 -- 回显操作 - 涉及到修改操作,都要先根据id进行基本信息查询,实现回显操作。 - 涉及到修改操作,mapper增强加上注解自动填充公共字段。 - 多表有n张表,一般就写n个增删改查SQL语句 ### 修改操作 - 修改表中的附表时,数据可能增加、可能删除、还可能修改数据的值,涉及到增删改操作,所以先全部删除旧数据,再添加新数据。 - ```java // 修改菜品信息 @Override public void update(DishDTO dishDTO) { log.info("修改菜品信息:{}",dishDTO); Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO,dish); dishMapper.update(dish); // 口味数据可能增加、可能删除、还可能修改口味的值,涉及到增删改操作,所以先全部删除旧数据,再添加新数据 // 先删除菜品口味表中的数据 dishFlavorsMapper.deleteFlavarByDishId(dishDTO.getId()); // 再从前端参数中获取flavors列表数据,添加新的菜品口味数据 List flavors = dishDTO.getFlavors(); // 如果不为空,就关联菜品id 并且调用批量插入方法 if(flavors!=null && flavors.size()>0){ flavors.forEach(item -> { item.setDishId(dish.getId()); }); log.info("菜品口味数据:{}",flavors); dishFlavorsMapper.insertDishFlavorBatch(flavors); } } ```