# 06苍穹外卖
**Repository Path**: wang110110/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-03-09
- **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 格式
```
():