# 06苍穹外卖 **Repository Path**: wgldream/06-sky-delivery ## Basic Information - **Project Name**: 06苍穹外卖 - **Description**: 项目一苍穹 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-09-21 - **Last Updated**: 2026-09-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 苍穹外卖项目 - 开发实施记录文档 **项目名称:** 苍穹外卖(Sky Take-Out) **技术栈:** Spring Boot + MyBatis + MySQL + Nginx + JWT **开发周期:** Day01-Day02(持续更新中) **最后更新时间:** 2026-03-16 --- ## 📁 项目结构说明 ``` sky-take-out/ ├── sky-commons/ # 公共模块(工具类、常量、异常、配置等) ├── sky-pojo/ # 实体类模块(DTO、VO、POJO) ├── sky-server/ # 核心业务模块(Controller、Service、Mapper) ├── 文档/ # 项目文档和讲义 ├── 项目开发进度文档/ # 进度记录文档 └── README.md # 本文件 ``` --- ## 📋 Day01 开发记录 - 环境搭建与员工管理模块 ### 一、开发环境搭建(完成时间:2026-03-13) #### 1.1 前端环境配置 **步骤 1:安装 Nginx** - 下载版本:nginx-1.20.2 - 安装路径:`E:\黑马学习学习\06code苍穹外卖\nginx` - 配置文件:`conf/nginx.conf` **步骤 2:配置反向代理** ```nginx server { listen 80; server_name localhost; location / { root html; index index.html index.htm; } # 反向代理配置 location /api/ { proxy_pass http://localhost:8080/admin/; } } ``` **步骤 3:启动 Nginx** ```cmd cd E:\黑马学习学习\06code苍穹外卖\nginx start nginx ``` **验证:** 访问 http://localhost:80 ✅ --- #### 1.2 后端环境配置 **步骤 1:创建 Maven 父工程** - 项目名称:sky-take-out - 打包方式:pom - 依赖管理:集中管理版本 **步骤 2:创建子模块** | 模块名 | 用途 | 主要依赖 | |--------|------|----------| | sky-commons | 工具类、常量、异常 | Lombok, Fastjson | | sky-pojo | 实体类、DTO、VO | Lombok | | sky-server | 业务逻辑层 | Spring Boot, MyBatis, PageHelper | **步骤 3:配置 pom.xml** ```xml org.springframework.boot spring-boot-starter-parent 2.7.3 org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0 mysql mysql-connector-java 8.0.30 com.github.pagehelper pagehelper-spring-boot-starter 1.3.0 com.github.xiaoymin knife4j-spring-boot-starter 3.0.2 io.jsonwebtoken jjwt 0.9.1 org.projectlombok lombok true ``` --- #### 1.3 数据库环境配置 **步骤 1:执行 SQL 脚本** ```sql -- 创建数据库 CREATE DATABASE IF NOT EXISTS sky_take_out_new DEFAULT CHARACTER SET utf8mb4; -- 使用数据库 USE sky_take_out_new; -- 执行初始化脚本(共 14 张表) source E:\黑马学习学习\06code苍穹外卖\sky_take_out.sql; ``` **步骤 2:验证表结构** ```sql SHOW TABLES; -- 应该显示 14 张表:employee, category, dish, setmeal, orders 等 ``` --- #### 1.4 配置文件设置 **application.yml 核心配置:** ```yaml server: port: 8080 spring: datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sky_take_out_new?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8 username: root password: 12345678 mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.sky.pojo configuration: map-underscore-to-camel-case: true # 开启驼峰命名 logging: level: com.sky.mapper: debug com.sky.service: info com.sky.controller: info ``` --- ### 二、第 6 章:新增员工功能(完成时间:2026-03-13) #### 6.1 需求分析 **业务规则:** 1. 账号必须唯一 2. 手机号为合法 11 位手机号码 3. 身份证号为合法 18 位身份证号码 4. 密码默认为 123456(MD5 加密) **接口设计:** - 请求方式:POST - 请求路径:`/admin/employee` - 请求参数:EmployeeDTO(Body参数) - 响应数据:Result --- #### 6.2 实施步骤 ##### Step 1:创建 DTO 类(sky-pojo 模块) **文件路径:** `sky-pojo/src/main/java/com/sky/dto/EmployeeDTO.java` ```java package com.sky.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; @Data @ApiModel(description = "员工 DTO") public class EmployeeDTO implements Serializable { @ApiModelProperty("姓名") private String name; @ApiModelProperty("账号") private String username; @ApiModelProperty("密码") private String password; @ApiModelProperty("手机号") private String phone; @ApiModelProperty("性别") private String sex; @ApiModelProperty("身份证号") private String idNumber; } ``` **关键点:** - ✅ 实现 Serializable 接口 - ✅ 添加 Swagger 注解 - ✅ 使用 Lombok 简化代码 --- ##### Step 2:创建 Controller 层(sky-server 模块) **文件路径:** `sky-server/src/main/java/com/sky/controller/admin/EmployeeController.java` ```java package com.sky.controller.admin; import com.sky.dto.EmployeeDTO; import com.sky.result.Result; import com.sky.service.admin.EmployeeService; 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.*; @Api(tags = "员工管理接口") @RestController @RequestMapping("/admin/employee") @Slf4j public class EmployeeController { @Autowired private EmployeeService employeeService; /** * 新增员工 */ @PostMapping @ApiOperation("新增员工") public Result save(@RequestBody EmployeeDTO employeeDTO){ log.info("新增员工:{}", employeeDTO); employeeService.save(employeeDTO); return Result.success(); } } ``` **关键点:** - ✅ 使用 `@RequestBody` 接收 JSON 数据 - ✅ 统一返回 Result 格式 - ✅ 添加日志记录 --- ##### Step 3:创建 Service 层(sky-server 模块) **接口文件:** `sky-server/src/main/java/com/sky/service/admin/EmployeeService.java` ```java package com.sky.service.admin; import com.sky.dto.EmployeeDTO; public interface EmployeeService { /** * 新增员工 * @param employeeDTO */ void save(EmployeeDTO employeeDTO); } ``` **实现类文件:** `sky-server/src/main/java/com/sky/service/admin/impl/EmployeeServiceImpl.java` ```java package com.sky.service.admin.impl; import com.sky.constant.PasswordConstant; import com.sky.context.BaseContext; import com.sky.dto.EmployeeDTO; import com.sky.exception.EmployeeNameExistException; import com.sky.mapper.admin.EmployeeMapper; import com.sky.pojo.Employee; import com.sky.service.admin.EmployeeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.time.LocalDateTime; @Service @Slf4j public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override public void save(EmployeeDTO employeeDTO) { // 1. 判断用户名是否存在 Employee dbEmployee = employeeMapper.getByUsername(employeeDTO.getUsername()); if(dbEmployee != null){ throw new EmployeeNameExistException(dbEmployee.getName() + "已存在"); } // 2. 对象属性拷贝 Employee employee = new Employee(); BeanUtils.copyProperties(employeeDTO, employee); // 3. 设置默认状态(启用) employee.setStatus(1); // 4. MD5 加密默认密码 employee.setPassword(DigestUtils.md5DigestAsHex( PasswordConstant.DEFAULT_PASSWORD.getBytes())); // 5. 设置时间戳 employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); // 6. 通过 ThreadLocal 获取当前用户 ID employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId()); // 7. 插入数据库 employeeMapper.insert(employee); } } ``` **核心逻辑:** 1. ✅ 用户名重复校验 2. ✅ 对象属性拷贝 3. ✅ 默认密码 MD5 加密 4. ✅ ThreadLocal 动态获取创建人 ID --- ##### Step 4:创建 Mapper 层(sky-server 模块) **接口文件:** `sky-server/src/main/java/com/sky/mapper/admin/EmployeeMapper.java` ```java package com.sky.mapper.admin; import com.sky.pojo.Employee; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface EmployeeMapper { /** * 根据用户名查询员工 */ @Select("select * from employee where username = #{username}") Employee getByUsername(String username); /** * 插入员工数据 */ @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); } ``` --- ##### Step 5:自定义异常类(sky-commons 模块) **文件路径:** `sky-commons/src/main/java/com/sky/exception/EmployeeNameExistException.java` ```java package com.sky.exception; public class EmployeeNameExistException extends BaseException { public EmployeeNameExistException(String msg) { super(msg); } } ``` **异常体系:** ``` Throwable └── Exception └── BaseException(自定义基类) └── EmployeeNameExistException ``` --- ##### Step 6:ThreadLocal 工具类(sky-commons 模块) **文件路径:** `sky-commons/src/main/java/com/sky/context/BaseContext.java` ```java package com.sky.context; public class BaseContext { public static ThreadLocal threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } } ``` **作用:** - 线程隔离存储用户 ID - 避免参数显式传递 - 防止内存泄漏(需及时清理) --- ##### Step 7:JWT 拦截器改造(sky-server 模块) **文件路径:** `sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java` ```java @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 获取请求头中的令牌 String token = request.getHeader(jwtProperties.getAdminTokenName()); // 2. 解析令牌,获取用户 ID Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); // 3. 将用户 id 存储到 ThreadLocal BaseContext.setCurrentId(empId); return true; // 放行 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 4. 清理 ThreadLocal 中的用户 ID,防止内存泄漏 BaseContext.removeCurrentId(); } ``` **关键点:** - ✅ `preHandle`:存储用户 ID - ✅ `afterCompletion`:清理 ThreadLocal --- ##### Step 8:测试验证 **测试方式 1:Swagger 接口文档** 1. 启动项目 2. 访问:http://localhost:8080/doc.html 3. 先调用登录接口获取 JWT 令牌 4. 在新增员工接口中添加 Header:`token: xxx` 5. 填写员工信息,发送请求 **测试方式 2:前后端联调** 1. 启动 Nginx:`start nginx` 2. 访问:http://localhost 3. 登录后台管理系统 4. 进入员工管理页面 5. 点击"添加员工",填写信息并提交 **测试结果:** ✅ 通过 --- ### 三、第 7 章:员工分页查询功能(完成时间:2026-03-13) #### 7.1 需求分析 **业务规则:** 1. 根据页码展示员工信息 2. 每页展示 10 条数据 3. 支持按员工姓名模糊查询 **接口设计:** - 请求方式:GET - 请求路径:`/admin/employee/page` - 请求参数:EmployeePageQueryDTO(Query参数) - 响应数据:Result --- #### 7.2 实施步骤 ##### Step 1:创建分页查询 DTO(sky-pojo 模块) **文件路径:** `sky-pojo/src/main/java/com/sky/dto/EmployeePageQueryDTO.java` ```java package com.sky.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; @Data @ApiModel(description = "员工分页查询 DTO") public class EmployeePageQueryDTO implements Serializable { @ApiModelProperty("员工姓名") private String name; @ApiModelProperty("页码") private int page; @ApiModelProperty("每页显示记录数") private int pageSize; } ``` --- ##### Step 2:创建分页结果 VO(sky-commons 模块) **文件路径:** `sky-commons/src/main/java/com/sky/result/PageResult.java` ```java package com.sky.result; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class PageResult implements Serializable { private long total; // 总记录数 private List records; // 当前页数据列表 } ``` --- ##### Step 3:Controller 层添加分页查询方法 **文件路径:** `sky-server/src/main/java/com/sky/controller/admin/EmployeeController.java` ```java /** * 分页查询员工 */ @GetMapping("/page") @ApiOperation("分页查询员工") public Result page(EmployeePageQueryDTO employeePageQueryDTO) { log.info("分页查询员工:{}", employeePageQueryDTO); PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO); return Result.success(pageResult); } ``` **注意:** GET 请求不需要 `@RequestBody` 注解 --- ##### Step 4:Service 层添加分页查询方法 **接口文件:** `sky-server/src/main/java/com/sky/service/admin/EmployeeService.java` ```java /** * 分页查询 * @param employeePageQueryDTO * @return */ PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO); ``` **实现类文件:** `sky-server/src/main/java/com/sky/service/admin/impl/EmployeeServiceImpl.java` ```java @Override public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) { // 1. 开启分页:传入当前页码、每页显示条数 PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize()); // 2. 调用 Mapper 层执行分页查询 Page page = employeeMapper.pageQuery(employeePageQueryDTO); // 3. 获取总记录数 long total = page.getTotal(); // 4. 获取当前页的数据列表 List result = page.getResult(); // 5. 封装并返回分页结果 return new PageResult(total, result); } ``` **关键点:** - ✅ 使用 PageHelper 分页插件 - ✅ 返回 Page 对象(包含 total 和 result) --- ##### Step 5:Mapper 层添加分页查询方法 **接口文件:** `sky-server/src/main/java/com/sky/mapper/admin/EmployeeMapper.java` ```java /** * 分页查询 * @param employeePageQueryDTO * @return */ Page pageQuery(EmployeePageQueryDTO employeePageQueryDTO); ``` **XML 映射文件:** `src/main/resources/mapper/admin/EmployeeMapper.xml` ```xml ``` **动态 SQL 说明:** - ``:自动处理 AND/OR 前缀 - ``:条件判断,非空才拼接 SQL - `concat('%',#{name},'%')`:模糊查询 --- ##### Step 6:测试验证 **测试用例:** ```json // 请求参数 { "name": "zhang", "page": 1, "pageSize": 10 } // 预期响应 { "code": 1, "msg": "success", "data": { "total": 25, "records": [ { "id": 1, "name": "张三", "username": "zhangsan", "phone": "13800138000", ... } ] } } ``` --- ### 四、配置类实现(完成时间:2026-03-13) #### 8.1 WebMvcConfiguration 配置类 **文件路径:** `sky-server/src/main/java/com/sky/config/WebMvcConfiguration.java` ##### 配置项 1:Swagger/Knife4j 配置 ```java @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; } ``` ##### 配置项 2:静态资源映射 ```java @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); } ``` ##### 配置项 3:消息转换器扩展(日期格式化) ```java @Override protected void extendMessageConverters(List> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(new JacksonObjectMapper()); converters.add(0, converter); } ``` **JacksonObjectMapper 配置:** - 日期格式:`yyyy-MM-dd HH:mm:ss` - 时间格式:`HH:mm:ss` - 序列化 LocalTime、LocalDate、LocalDateTime ##### 配置项 4:拦截器注册 ```java @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/employee/login") .excludePathPatterns("/admin/order/completeByNumber") .excludePathPatterns("/admin/order/reminderNewOrders") .excludePathPatterns("/admin/order/reminder") .excludePathPatterns("/admin/merchant/getShopById") .excludePathPatterns("/admin/order/changeOrderStatus") .excludePathPatterns("/admin/order/getAddressByOrderNumber"); } ``` --- #### 8.2 全局异常处理器 **文件路径:** `sky-server/src/main/java/com/sky/handler/GlobalExceptionHandler.java` ```java package com.sky.handler; import com.sky.exception.BaseException; import com.sky.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 处理业务异常 */ @ExceptionHandler public Result exceptionHandler(BaseException ex){ log.error("异常信息:{}", ex.getMessage()); return Result.error(ex.getMessage()); } } ``` **特点:** - ✅ 使用 `@RestControllerAdvice` 全局捕获 - ✅ 使用 `@ExceptionHandler` 指定异常类型 - ✅ 统一返回 Result.error() 格式 --- ### 五、常见问题及解决方案 #### 问题 1:401 Unauthorized **现象:** 调用接口返回 401 **原因:** 未携带 JWT 令牌 **解决方案:** ```javascript // 前端请求头添加 token headers: { 'token': localStorage.getItem('token') } ``` --- #### 问题 2:用户名重复校验失败 **现象:** 保存员工时报"用户名已存在" **原因:** Service 层未进行校验 **解决方案:** ```java // 在 save 方法中添加校验 Employee dbEmployee = employeeMapper.getByUsername(username); if(dbEmployee != null){ throw new EmployeeNameExistException("用户名已存在"); } ``` --- #### 问题 3:创建人 ID 为 null **现象:** 数据库中 create_user 字段为 null **原因:** 未使用 ThreadLocal 动态获取 **解决方案:** ```java // 在拦截器中存储 BaseContext.setCurrentId(empId); // 在 Service 中获取 employee.setCreateUser(BaseContext.getCurrentId()); // 在拦截器 afterCompletion 中清理 BaseContext.removeCurrentId(); ``` --- #### 问题 4:GET 请求参数接收不到 **现象:** employeePageQueryDTO 所有字段为 null **原因:** GET 请求使用了 `@RequestBody` **解决方案:** ```java // ❌ 错误 public Result page(@RequestBody EmployeePageQueryDTO dto) // ✅ 正确 public Result page(EmployeePageQueryDTO dto) ``` --- #### 问题 5:MyBatis 驼峰命名不生效 **现象:** 查询结果字段为 null **原因:** 未开启驼峰命名转换 **解决方案:** ```yaml # application.yml mybatis: configuration: map-underscore-to-camel-case: true ``` --- ### 六、技术要点总结 #### 1. Spring Boot 分层架构 ``` Controller 层 ↓ Service 层 ↓ Mapper 层 ↓ Database ``` **各层职责:** - **Controller**:接收请求,调用 Service,返回结果 - **Service**:业务逻辑处理,事务控制 - **Mapper**:数据访问,SQL 操作 --- #### 2. MyBatis + PageHelper 分页 **使用步骤:** ```java // 1. 开启分页 PageHelper.startPage(page, pageSize); // 2. 执行查询 Page page = mapper.query(param); // 3. 获取结果 long total = page.getTotal(); List records = page.getResult(); ``` **原理:** 基于 MyBatis 拦截器,自动拼接 LIMIT 语句 --- #### 3. JWT 令牌认证流程 ```mermaid graph LR A[用户登录] --> B[生成 JWT 令牌] B --> C[返回给前端] C --> D[前端存储令牌] D --> E[后续请求携带令牌] E --> F[拦截器解析令牌] F --> G[获取用户 ID] G --> H[执行业务逻辑] ``` **令牌结构:** ``` Header.Payload.Signature ``` **Payload 内容:** - empId:员工 ID - exp:过期时间 --- #### 4. ThreadLocal 线程隔离 **应用场景:** - 用户 ID 传递 - 数据库连接管理 - 事务管理 **使用规范:** ```java // 1. 存储 threadLocal.set(value); // 2. 获取 value = threadLocal.get(); // 3. 清理(重要!) threadLocal.remove(); ``` **内存泄漏风险:** - 线程池场景下,线程复用 - 如果不清理,会导致内存泄漏 - 必须在 `afterCompletion` 中清理 --- #### 5. 统一返回结果格式 **Result 类结构:** ```java @Data public class Result { private Integer code; // 状态码 private String msg; // 消息 private T data; // 数据 } ``` **使用示例:** ```java // 成功 return Result.success(data); // 失败 return Result.error("错误信息"); ``` --- ## 📊 Day02 开发记录 - 分类管理与菜品管理 ### 一、分类管理模块(完成时间:2026-03-14) #### 1.1 新增分类功能 **DTO 类:** `CategoryDTO.java` ```java @Data @ApiModel(description = "分类 DTO") public class CategoryDTO implements Serializable { @ApiModelProperty("分类类型") private Integer type; @ApiModelProperty("分类名称") private String name; @ApiModelProperty("排序") private Integer sort; } ``` **Controller 层:** ```java @PostMapping @ApiOperation("新增分类") public Result save(@RequestBody CategoryDTO categoryDTO){ log.info("新增分类:{}", categoryDTO); categoryService.save(categoryDTO); return Result.success(); } ``` **Service 层:** ```java @Override public void save(CategoryDTO categoryDTO) { Category category = new Category(); BeanUtils.copyProperties(categoryDTO, category); // 设置状态为禁用 category.setStatus(StatusConstant.DISABLE); // 设置时间 category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); // 获取创建人 ID category.setCreateUser(BaseContext.getCurrentId()); category.setUpdateUser(BaseContext.getCurrentId()); categoryMapper.insert(category); } ``` --- #### 1.2 分类分页查询 **DTO 类:** `CategoryPageQueryDTO.java` ```java @Data @ApiModel(description = "分类分页查询 DTO") public class CategoryPageQueryDTO implements Serializable { @ApiModelProperty("页码") private int page; @ApiModelProperty("每页记录数") private int pageSize; @ApiModelProperty("分类名称") private String name; @ApiModelProperty("分类类型") private Integer type; } ``` **Mapper XML:** ```xml ``` --- #### 1.3 启用/禁用分类 **Controller 层:** ```java @PostMapping("/status/{status}") @ApiOperation("启用禁用分类") public Result startOrStop(@PathVariable Integer status, Long id){ categoryService.startOrStop(status, id); return Result.success(); } ``` **Service 层:** ```java @Override public void startOrStop(Integer status, Long id) { Category category = Category.builder() .id(id) .status(status) .updateTime(LocalDateTime.now()) .updateUser(BaseContext.getCurrentId()) .build(); categoryMapper.update(category); } ``` **注意事项:** - ✅ 使用 `@PathVariable` 接收路径参数 - ✅ 使用 Builder 模式构建对象 - ✅ 只更新需要修改的字段 --- ### 二、菜品管理模块(完成时间:2026-03-16) #### 2.1 菜品分页查询 **DTO 类:** `DishPageQueryDTO.java` ```java @Data @ApiModel(description = "菜品分页查询 DTO") public class DishPageQueryDTO implements Serializable { @ApiModelProperty("页码") private int page; @ApiModelProperty("每页记录数") private int pageSize; @ApiModelProperty("菜品名称") private String name; @ApiModelProperty("分类 ID") private Long categoryId; @ApiModelProperty("状态") private Integer status; } ``` **VO 类:** `DishVO.java` ```java @Data @ApiModel(description = "菜品 VO") public class DishVO implements Serializable { @ApiModelProperty("菜品 ID") private Long id; @ApiModelProperty("菜品名称") private String name; @ApiModelProperty("分类 ID") private Long categoryId; @ApiModelProperty("分类名称") private String categoryName; @ApiModelProperty("价格") private BigDecimal price; @ApiModelProperty("图片") private String image; @ApiModelProperty("状态") private Integer status; } ``` **Controller 层:** ```java @GetMapping("/page") @ApiOperation("菜品分页查询") public Result page(DishPageQueryDTO dishPageQueryDTO) { log.info("菜品分页查询:{}", dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); } ``` **Service 层:** ```java @Override public PageResult pageQuery(DishPageQueryDTO dishDTO) { // 开启分页 PageHelper.startPage(dishDTO.getPage(), dishDTO.getPageSize()); // 执行分页查询 Page page = dishMapper.pageQuery(dishDTO); return new PageResult(page.getTotal(), page.getResult()); } ``` **Mapper XML:** ```xml ``` --- #### 2.2 文件上传功能(阿里云 OSS) ##### Step 1:创建配置类 **AliOssProperties.java:** ```java @Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; private String dirName; } ``` **OssConfiguration.java:** ```java @Configuration @Slf4j public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){ log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties); // 参数校验 if (aliOssProperties.getEndpoint() == null || aliOssProperties.getAccessKeyId() == null || aliOssProperties.getAccessKeySecret() == null || aliOssProperties.getBucketName() == null) { throw new RuntimeException("OSS 配置不完整"); } return new AliOssUtil( aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName() ); } } ``` ##### Step 2:配置文件 **application.yml:** ```yaml sky: alioss: endpoint: ${ALIYUN_OSS_ENDPOINT:oss-cn-beijing.aliyuncs.com} access-key-id: ${ALIYUN_OSS_ACCESS_KEY_ID} access-key-secret: ${ALIYUN_OSS_ACCESS_KEY_SECRET} bucket-name: whh0001 ``` **环境变量设置(Windows):** ```cmd set ALIYUN_OSS_ACCESS_KEY_ID=LTAI5tRfXXXXXXXXXX set ALIYUN_OSS_ACCESS_KEY_SECRET=LPyk6qxXXXXXXXXXXXXXXX ``` ##### Step 3:CommonController 文件上传 ```java @RestController @RequestMapping("/admin/common") @Api(tags = "通用接口") @Slf4j public class CommonController { @Autowired private AliOssUtil aliOssUtil; /** * 文件上传 */ @PostMapping("/upload") @ApiOperation("文件上传") public Result upload(MultipartFile file){ log.info("文件上传:{}", file); try { // 1. 生成文件名 String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); String objectName = UUID.randomUUID().toString() + extension; // 2. 上传到 OSS String filePath = aliOssUtil.upload(file.getBytes(), objectName); log.info("文件上传成功:{}", filePath); return Result.success(filePath); } catch (Exception e) { log.error("文件上传失败:{}", e); return Result.error("文件上传失败"); } } } ``` --- ## 🎯 项目开发规范 ### 一、代码规范 #### 1. 命名规范 | 类型 | 规范 | 示例 | |------|------|------| | 类名 | 大驼峰 | EmployeeController | | 方法名 | 小驼峰 | saveEmployee | | 变量名 | 小驼峰 | employeeDTO | | 常量名 | 全大写 | DEFAULT_PASSWORD | | 包名 | 全小写 | com.sky.controller | #### 2. 注释规范 ```java /** * 新增员工 * @param employeeDTO 员工信息 * @return 无 */ @PostMapping @ApiOperation("新增员工") public Result save(@RequestBody EmployeeDTO employeeDTO){ // 方法实现 } ``` #### 3. 异常处理规范 ```java // ✅ 推荐:抛出业务异常 if(condition){ throw new EmployeeNameExistException("用户名已存在"); } // ❌ 不推荐:直接返回错误信息 if(condition){ return Result.error("用户名已存在"); } ``` --- ### 二、Git 提交规范 #### Commit Message 格式 ``` ():