Jelajahi Sumber

feat:更新readme文件以及其他相关文件

yangyi 4 bulan lalu
induk
melakukan
99795611cb
8 mengubah file dengan 3893 tambahan dan 0 penghapusan
  1. 42 0
      AGENTS.md
  2. 283 0
      README.md
  3. 222 0
      doc/blog-upload-path-issue.md
  4. 32 0
      doc/demand.md
  5. TEMPAT SAMPAH
      doc/demand.xlsx
  6. 1956 0
      doc/detailDesign.md
  7. 438 0
      doc/fristDesign.md
  8. 920 0
      doc/work.md

+ 42 - 0
AGENTS.md

@@ -0,0 +1,42 @@
+# CampusIdleItemTradingPlatform — server
+
+## Build & dev
+
+```bash
+./mvnw spring-boot:run          # start dev server on :8080
+./mvnw compile                  # compile
+./mvnw test                     # run tests (none exist yet)
+```
+
+## Prerequisites
+
+- **Java 17+**, Maven wrapper included (`./mvnw`)
+- **MySQL** at `localhost:3306` — execute `src/main/resources/init.sql` to init DB
+  - DB: `campus_idle_item_trading_platform`, user: `root` / `root`
+- App uses **MySQL HikariCP** connection pool (max 20)
+
+## Architecture
+
+- **Package**: `com.campus.trade.*` — **not** the old `space.anyi...` namespace
+- **Entrypoint**: `com.campus.trade.ServerApplication` (`scanBasePackages = "com.campus.trade"`)
+- **Layered**: `controller → service[/admin] → mapper → entity`, with DTO/VO at boundaries
+- **Auth**: Session-based (`HttpSession`). `AuthInterceptor` guards all paths; whitelist for `/auth/register`, `/auth/login`, `/items(/**) GET`, `/upload/`, `/v3/api-docs`, `/swagger-ui`
+  - Session attributes: `userId` (Long), `role` (String)
+  - Controller reads user via `session.getAttribute("userId")`
+- **Context-path**: `/` (plain), not `/api`
+- **OpenAPI docs**: `http://localhost:8080/swagger-ui.html` / `/v3/api-docs`
+- **Password**: MD5 via `org.springframework.util.DigestUtils`
+- **File upload**: to `./upload/` (created at runtime), served at `/upload/`
+  - Multipart requests: `consumes = MediaType.MULTIPART_FORM_DATA_VALUE`
+- **Pagination**: PageHelper (`PageHelper.startPage(pageNum, pageSize)` in Service layer)
+
+## Coding conventions
+
+- **OpenAPI annotations** on every controller API — `@Operation`, `@Parameter`, `@Schema` from `io.swagger.v3.oas.annotations`; class-level `@Tag`
+- **DTO / VO** for params/responses — never expose entities in controller signatures
+- **Business logic in Service layer** — Controller only delegates; Service methods with `@Transactional`
+- **Entity field comments** — every field gets a `// inline comment`
+- **Business logic comments** — inline comments at key steps explaining the approach
+- **MyBatis XML** — `#{}` parameter binding; `resultType` uses type-alias (`com.campus.trade.entity`)
+- **Status enums** as plain strings: e.g. `"PENDING"`, `"ON_SALE"`, `"AUTHENTICATED"`, `"ENABLED"`
+- **Order status machine** in `OrderStatusMachine` validates all transitions

+ 283 - 0
README.md

@@ -0,0 +1,283 @@
+# 校园闲置物品交易平台 (Campus Idle Item Trading Platform)
+
+一个面向校园用户的二手物品交易平台,支持物品发布、订单管理、站内私信等功能。
+
+## 技术栈
+
+- **后端框架**: Spring Boot 3.5.15
+- **Java 版本**: Java 17+
+- **数据持久层**: MyBatis 3.0.5 + MyBatis-Spring-Boot-Starter
+- **数据库**: MySQL 8.0+ (HikariCP 连接池)
+- **API 文档**: SpringDoc OpenAPI (Swagger UI)
+- **分页插件**: PageHelper
+- **参数校验**: Spring Validation
+- **安全**: MD5 密码加密 + Session 认证
+
+## 项目结构
+
+```
+server/
+├── pom.xml                              # Maven 配置文件
+├── sql/
+│   └── init.sql                         # 数据库初始化脚本
+└── src/main/
+    ├── java/com/campus/trade/
+    │   ├── ServerApplication.java       # 应用入口
+    │   ├── config/                      # 配置类
+    │   │   ├── WebMvcConfig.java        # Web MVC 配置
+    │   │   └── MyBatisConfig.java       # MyBatis 配置
+    │   ├── controller/                  # 控制器层
+    │   │   ├── AuthController.java      # 认证相关 API
+    │   │   ├── ItemController.java      # 物品相关 API
+    │   │   ├── OrderController.java     # 订单相关 API
+    │   │   ├── MessageController.java   # 消息相关 API
+    │   │   ├── UserController.java      # 用户资料 API
+    │   │   ├── CategoryController.java  # 分类相关 API
+    │   │   └── admin/                   # 后台管理 API
+    │   ├── service/                     # 业务逻辑层
+    │   │   ├── impl/                    # 服务实现类
+    │   │   └── admin/                   # 后台管理服务
+    │   ├── mapper/                      # 数据访问层 (MyBatis Mapper)
+    │   ├── entity/                      # 实体类
+    │   ├── dto/                         # 数据传输对象 (请求参数)
+    │   ├── vo/                          # 视图对象 (响应数据)
+    │   ├── common/                      # 公共工具类
+    │   │   ├── Result.java              # 统一响应封装
+    │   │   ├── StatusCode.java          # 状态码定义
+    │   │   ├── BusinessException.java   # 业务异常
+    │   │   ├── GlobalExceptionHandler.java # 全局异常处理
+    │   │   ├── OrderStatusMachine.java  # 订单状态机
+    │   │   └── FileUploadUtil.java      # 文件上传工具
+    │   └── interceptor/                 # 拦截器
+    │       ├── AuthInterceptor.java     # 登录认证拦截器
+    │       └── AdminInterceptor.java    # 管理员权限拦截器
+    └── resources/
+        ├── application.yaml             # 应用配置文件
+        └── mapper/                      # MyBatis XML 映射文件
+```
+
+## 核心功能模块
+
+### 1. 用户认证模块
+- 用户注册(学号 + 姓名 + 密码 + 校园卡照片)
+- 用户登录/登出(Session 认证)
+- 实名认证(上传校园卡照片,管理员审核)
+- 密码修改(需验证旧密码)
+
+### 2. 物品管理模块
+- 物品发布(需实名认证)
+- 物品浏览(首页列表、分页)
+- 物品搜索(标题/描述模糊匹配)
+- 分类筛选
+- 物品详情
+- 我的发布列表
+- 物品下架/删除
+
+### 3. 订单管理模块
+- 订单创建(买家点击"我想要")
+- 订单状态流转:
+  ```
+  PENDING → CONFIRMED → DELIVERED → COMPLETED
+       ↓
+   CANCELLED (卖家拒单)
+  ```
+- 买家视角:我买到的订单
+- 卖家视角:我卖出的订单
+- 订单操作:接单、拒单、确认交货、确认收货
+
+### 4. 站内消息模块
+- 会话列表(显示最后一条消息)
+- 聊天记录(分页加载)
+- 发送消息(文本 + 图片)
+- 消息已读状态
+
+### 5. 后台管理模块
+- 用户管理(列表、封禁/解封、查看详情)
+- 实名认证审核
+- 物品审核
+- 分类管理(增删改查、排序)
+
+## 数据库设计
+
+### 数据表清单
+
+| 表名 | 说明 | 主要字段 |
+|------|------|----------|
+| `user` | 用户表 | id, student_id, name, password_hash, auth_status, role, status |
+| `category` | 分类表 | id, name, status, sort_order |
+| `item` | 物品表 | id, seller_id, category_id, title, description, price, images, status |
+| `order` | 订单表 | id, item_id, buyer_id, seller_id, trade_method, status |
+| `message` | 消息表 | id, from_user_id, to_user_id, item_id, content, is_read |
+| `review` | 审核记录表 | id, admin_id, review_type, target_id, result, reason |
+
+### 状态枚举说明
+
+**用户认证状态 (auth_status)**
+- `UNAUTHENTICATED` - 未认证
+- `PENDING` - 审核中
+- `AUTHENTICATED` - 已认证
+
+**用户账号状态 (status)**
+- `NORMAL` - 正常
+- `BANNED` - 已封禁
+
+**物品状态 (status)**
+- `PENDING` - 待审核
+- `ON_SALE` - 在售中
+- `OFF_SHELF` - 已下架
+- `SOLD` - 已售出
+
+**订单状态 (status)**
+- `PENDING` - 待确认
+- `CONFIRMED` - 已接单
+- `DELIVERED` - 已交货
+- `COMPLETED` - 已完成
+- `CANCELLED` - 已取消
+
+**分类状态 (status)**
+- `ENABLED` - 启用
+- `DISABLED` - 禁用
+
+## 快速开始
+
+### 环境要求
+
+- Java 17+
+- Maven 3.6+
+- MySQL 8.0+
+
+### 1. 数据库初始化
+
+```bash
+# 登录 MySQL
+mysql -u root -p
+
+# 执行初始化脚本
+source sql/init.sql
+```
+
+默认数据库配置:
+- 数据库名:`campus_idle_item_trading_platform`
+- 用户名:`root`
+- 密码:`root`
+
+### 2. 配置文件修改
+
+编辑 `src/main/resources/application.yaml`:
+
+```yaml
+spring:
+  datasource:
+    url: jdbc:mysql://localhost:3306/campus_idle_item_trading_platform?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
+    username: your_username
+    password: your_password
+```
+
+### 3. 构建与运行
+
+```bash
+# 编译项目
+./mvnw compile
+
+# 开发模式运行(热部署)
+./mvnw spring-boot:run
+
+# 打包
+./mvnw package
+
+# 运行 jar 包
+java -jar target/CampusIdleItemTradingPlatform-0.0.1-SNAPSHOT.jar
+```
+
+### 4. 访问应用
+
+- 应用地址:http://localhost:8090
+- API 文档:http://localhost:8090/swagger-ui.html
+- OpenAPI JSON:http://localhost:8090/v3/api-docs
+
+### 默认管理员账号
+
+- 学号:`admin`
+- 密码:`admin` (MD5: `e10adc3949ba59abbe56e057f20f883e`)
+
+## API 接口概览
+
+### 公开接口(无需登录)
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/auth/register` | POST | 用户注册 |
+| `/auth/login` | POST | 用户登录 |
+| `/items` | GET | 物品列表 |
+| `/items/{id}` | GET | 物品详情 |
+| `/items/search` | GET | 搜索物品 |
+| `/items/category/{id}` | GET | 按分类筛选 |
+| `/categories` | GET | 分类列表 |
+
+### 需要登录的接口
+
+| 接口前缀 | 说明 |
+|----------|------|
+| `/auth/logout` | 用户登出 |
+| `/auth/submit-auth` | 提交实名认证 |
+| `/auth/change-password` | 修改密码 |
+| `/user/profile` | 个人资料管理 |
+| `/items` (POST) | 发布物品 |
+| `/items/my` | 我的发布 |
+| `/orders/**` | 订单相关 |
+| `/messages/**` | 站内消息 |
+
+### 管理员接口
+
+| 接口前缀 | 说明 |
+|----------|------|
+| `/admin/users/**` | 用户管理 |
+| `/admin/reviews/**` | 审核管理 |
+| `/admin/categories/**` | 分类管理 |
+
+## 响应格式
+
+统一使用 `Result<T>` 包装:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": { ... }
+}
+```
+
+### 状态码
+
+| 状态码 | 说明 |
+|--------|------|
+| 200 | 成功 |
+| 400 | 参数错误 |
+| 401 | 未登录 |
+| 403 | 无权限 |
+| 404 | 资源不存在 |
+| 409 | 业务冲突(如学号已注册) |
+| 500 | 服务器内部错误 |
+
+## 技术亮点
+
+1. **分层架构**:严格遵循 Controller → Service → Mapper → Entity 分层
+2. **状态机**:订单状态流转通过 `OrderStatusMachine` 统一管理
+3. **统一响应**:全局异常处理 + 统一 Result 封装
+4. **API 文档**:基于注解的 Swagger/OpenAPI 自动生成
+5. **数据校验**:使用 Bean Validation 进行参数校验
+6. **分页支持**:PageHelper 实现分页查询
+7. **文件上传**:支持图片上传,配置大小限制
+
+## 开发约定
+
+- 实体类字段必须有注释说明
+- Controller 必须使用 OpenAPI 注解描述 API
+- 使用 DTO 接收请求参数,VO 返回响应数据
+- 业务逻辑放在 Service 层,Controller 只负责转发
+- MyBatis XML 使用 `#{}` 参数绑定
+- 状态使用字符串枚举(如 `PENDING`, `ON_SALE`)
+
+## 许可证
+
+本项目仅供学习交流使用。

+ 222 - 0
doc/blog-upload-path-issue.md

@@ -0,0 +1,222 @@
+# Spring Boot 文件上传路径血泪史:一次因相对路径引发的"灵异"Bug
+
+## 引言
+
+在开发校园闲置物品交易平台时,遇到了一个令人抓狂的 Bug:上传的图片明明保存在了指定目录,但前端访问时却总是 `404` 或 `FileNotFoundException`。错误日志指向了一个匪夷所思的路径:
+
+```
+java.io.FileNotFoundException: /tmp/tomcat.8090.17561196457261115842/
+  work/Tomcat/localhost/ROOT/./upload/dcb42730-0298-43f8-982f-3f37792ddbff.png
+  (No such file or directory)
+```
+
+Tomcat 为什么会跑到 `/tmp/` 目录下去找上传文件?本文详细记录了问题的排查过程、根本原因和最终解决方案。
+
+---
+
+## 一、现象复现
+
+应用基于 Spring Boot 2.x + MyBatis,通过内嵌 Tomcat 运行在 `localhost:8090`。功能很简单:
+
+- 用户上传图片,后端保存到 `./upload/` 目录
+- 前端通过 `/upload/{文件名}` 访问图片
+
+上传接口正常返回了图片 URL(如 `/upload/xxx.jpg`),但浏览器访问该 URL 时报错,后端日志输出上述 `FileNotFoundException`。
+
+---
+
+## 二、代码现状
+
+项目中与文件上传相关的代码有两处:
+
+### 2.1 文件保存 — `FileUploadUtil.java`
+
+```java
+@Component
+public class FileUploadUtil {
+
+    @Value("${upload.dir:./upload}")
+    private String uploadDir;
+
+    public String upload(MultipartFile file) {
+        // ... 校验逻辑 ...
+        String filename = UUID.randomUUID() + ext;
+        File dest = new File(uploadDir, filename);
+        if (!dest.getParentFile().exists()) {
+            dest.getParentFile().mkdirs();
+        }
+        file.transferTo(dest);
+        return "/upload/" + filename;
+    }
+}
+```
+
+### 2.2 静态资源映射 — `WebMvcConfig.java`
+
+```java
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler("/upload/**")
+                .addResourceLocations("file:./upload/");
+    }
+}
+```
+
+### 2.3 配置文件 — `application.yaml`
+
+```yaml
+upload:
+  dir: ./upload
+
+spring:
+  mvc:
+    static-path-pattern: /upload/**
+    static-locations: file:./upload/
+```
+
+第一眼看过去,所有路径都统一写着 `./upload/`,似乎没有任何问题。
+
+---
+
+## 三、排查过程
+
+### 第一步:确认文件是否真的存在
+
+```bash
+$ ls -la ./upload/
+total 24
+drwxr-xr-x  2 root root  4096 ...
+-rw-r--r--  1 root root 12345 ... dcb42730-0298-43f8-982f-3f37792ddbff.png
+```
+
+文件确实在项目根目录的 `./upload/` 下,没有丢失。
+
+### 第二步:分析错误路径
+
+错误信息中,Tomcat 找的是:
+
+```
+/tmp/tomcat.8090.17561196457261115842/work/Tomcat/localhost/ROOT/./upload/xxx.png
+```
+
+而我们的文件实际在:
+
+```
+/home/user/project/upload/xxx.png
+```
+
+路径完全不一致。
+
+### 第三步:定位问题来源
+
+`/tmp/tomcat.8090.xxxxx/` 是 Spring Boot 内嵌 Tomcat 运行时创建的临时工作目录。这意味着 `file:./upload/` 这个相对路径,被 Tomcat 以自己的**临时工作目录**为基准进行了解析,而不是以**项目根目录**为基准。
+
+---
+
+## 四、根因分析
+
+这是典型的 **相对路径解析上下文不一致** 问题。
+
+| 组件 | 路径写法 | 解析基准 | 结果 |
+|------|---------|---------|------|
+| `FileUploadUtil` | `new File("./upload", name)` | JVM `user.dir`(项目根目录) | ✅ 文件保存到项目根目录下的 `upload/` |
+| `WebMvcConfig` | `file:./upload/` | Tomcat 临时工作目录 | ❌ 解析到 `/tmp/tomcat.xxx/./upload/` |
+| `application.yaml` | `file:./upload/` | Tomcat 临时工作目录 | ❌ 同上 |
+
+Spring Boot 应用启动时,`./` 这个相对路径在不同阶段指向不同的"当前目录":
+
+- **应用代码层**(`new File()`):以 JVM 启动时的 `user.dir` 为基准
+- **内嵌 Tomcat 静态资源处理层**(`ResourceHandler` / `static-locations`):以 Tomcat 自己的工作目录为基准
+
+两者不一致,导致文件写到了 A 处,读取时却从 B 处找,自然找不到。
+
+> 注意:并非所有环境都会复现此问题。在部分 IDE 或直接 `java -jar` 运行时,两者可能恰好一致,让人误以为配置没问题。一旦部署到特定环境(如通过 `spring-boot:run` 插件启动、或部署到外部 Tomcat),问题就会暴露。
+
+---
+
+## 五、解决方案
+
+### 核心思路:使用绝对路径统一两边的解析基准
+
+利用 `${user.dir}` 这个 JVM 系统属性,固定指向项目根目录,让所有组件都使用同一个绝对路径。
+
+#### 1. 修改 `application.yaml`
+
+```yaml
+upload:
+  dir: ${user.dir}/upload
+```
+
+移除与 `WebMvcConfig` 重复的静态资源配置(二选一即可,重复注册可能导致优先级混乱):
+
+```yaml
+spring:
+  servlet:
+    multipart:
+      max-file-size: 10MB
+      max-request-size: 50MB
+  # 下面的配置已移除,由 WebMvcConfig 统一管理
+  # mvc:
+  #   static-path-pattern: /upload/**
+  #   static-locations: file:./upload/
+```
+
+#### 2. 修改 `WebMvcConfig.java`
+
+注入配置属性,拼接绝对路径:
+
+```java
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Value("${upload.dir}")
+    private String uploadDir;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler("/upload/**")
+                .addResourceLocations("file:" + uploadDir + "/");
+    }
+}
+```
+
+#### 3. `FileUploadUtil.java` 无需修改
+
+它已经在读取 `@Value("${upload.dir}")`,`application.yaml` 改为绝对路径后自动生效。
+
+---
+
+## 六、修复后的效果
+
+修复后,三个组件都使用同一个绝对路径 `${user.dir}/upload`,无论 Tomcat 的工作目录是什么,文件写入和读取都指向同一个位置:
+
+```bash
+# user.dir = /home/user/project
+# upload.dir = /home/user/project/upload
+```
+
+- `FileUploadUtil` 保存 → `/home/user/project/upload/xxx.png` ✅
+- `WebMvcConfig` 读取 → `file:/home/user/project/upload/` ✅
+- 前端访问 → `http://localhost:8090/upload/xxx.png` ✅
+
+---
+
+## 七、经验教训
+
+1. **永远不要在 Spring Boot 中使用 `./` 相对路径** — 不同组件的解析基准可能不同,应使用 `${user.dir}` 或硬编码绝对路径
+2. **避免重复配置** — `spring.mvc.static-locations` 和自定义 `WebMvcConfigurer.addResourceHandlers` 功能重叠,选其一即可,否则容易互相干扰
+3. **Spring Boot 配置属性可以引用 JVM 系统属性** — `${user.dir}`、`${java.home}` 等系统属性可以在 `application.yaml` 中直接使用,非常方便
+4. **Bug 复现的环境依赖性** — 有时候不是因为代码"对了",而是因为环境"恰好对了"。跨环境运行时,这类路径问题就会暴露出来
+
+---
+
+## 结语
+
+一个 `./` 引发的 Bug,排查起来说难不难,说简单也花了大半天。归根结底是对 Spring Boot 内部路径解析机制的理解不够深入。如果你也遇到了类似的"文件保存了但访问不到"的问题,不妨检查一下项目中是否存在四处散落的相对路径 — 把它们统一改为绝对路径,问题很可能就迎刃而解了。
+
+---
+
+*本文发布于 2026-06-27*

+ 32 - 0
doc/demand.md

@@ -0,0 +1,32 @@
+### 游客(未认证用户)
+- 学号注册(学号+姓名+密码+校园卡照片上传)
+- 学号登录(学号+密码)
+- 浏览首页物品列表(按发布时间倒序)
+- 关键词搜索(按标题/描述模糊匹配)
+- 分类筛选
+- 查看物品详情(图文/价格/卖家昵称/交易方式)
+
+### 认证学生用户 - 卖家
+- 发布闲置(图片/标题/描述/价格/分类/交易方式:自提或送货上门)
+- 管理我的发布(列表展示:售卖中/已下架/已售出,支持下架/删除)
+- 处理订单(确认接单/拒单)
+- 确认已交货(线下面交完成后)
+- 站内私信回复(文本+图片)
+
+### 认证学生用户 - 买家
+- 联系卖家(站内私信)
+- 生成订单(点击"我想要"自动生成,继承卖家设定的交易方式)
+- 确认收货(线下收到物品后)
+- 我的订单(双视角:我买到的/我卖出的,状态:待接单→待面交→已完成/已取消)
+
+### 认证学生用户 - 通用
+- 修改个人资料(头像/昵称/个性签名)
+- 修改密码(旧密码+新密码)
+- 查看我的发布(入口)
+
+### 后台管理员(Web端)
+- 实名认证审核(通过/驳回,驳回需填原因)
+- 物品上架审核(通过上架/驳回下架)
+- 用户账号管理(封禁/解封)
+- 用户密码重置(管理员手动重置)
+- 物品分类管理(新增/修改/禁用)

TEMPAT SAMPAH
doc/demand.xlsx


+ 1956 - 0
doc/detailDesign.md

@@ -0,0 +1,1956 @@
+# 校园闲置物品交易平台 — 详细设计
+
+---
+
+## 第1章 服务端详细设计
+
+### 1.1 技术栈明细
+
+| 层次 | 技术 | 版本 | 用途 |
+|------|------|------|------|
+| 框架 | SpringBoot | 2.7.x | 应用容器 |
+| ORM | MyBatis + MyBatis-Spring | 2.x | 数据访问 |
+| 分页 | PageHelper | 5.x | 分页插件 |
+| 数据库 | MySQL | 8.x | 关系型数据库 |
+| 连接池 | HikariCP | 内置 | 数据库连接池 |
+| API 文档 | springdoc-openapi | 2.x | OpenAPI 3 文档自动生成 |
+| 密码加密 | MD5 (Spring DigestUtils) | 内置 | 密码哈希 |
+| JSON | Jackson | 内置 | 序列化/反序列化 |
+| 文件上传 | MultipartFile | Spring 内置 | 文件上传 |
+| 校验 | Hibernate Validator | 6.x | 参数校验 |
+
+### 1.2 关键 Maven 依赖
+
+```xml
+<!-- pom.xml 关键依赖 -->
+<!-- SpringDoc OpenAPI 文档 -->
+<dependency>
+    <groupId>org.springdoc</groupId>
+    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+    <version>2.8.16</version>
+</dependency>
+
+<!-- MyBatis 分页插件 -->
+<dependency>
+    <groupId>com.github.pagehelper</groupId>
+    <artifactId>pagehelper-spring-boot-starter</artifactId>
+    <version>2.1.0</version>
+</dependency>
+
+<!-- 参数校验 @Valid @NotBlank -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-validation</artifactId>
+</dependency>
+
+<!-- 密码加密使用 Spring 内置 DigestUtils(MD5),无需额外依赖 -->
+```
+
+```yaml
+# application.yml 增加 springdoc 配置
+springdoc:
+  api-docs:
+    path: /v3/api-docs          # OpenAPI JSON 路径
+  swagger-ui:
+    path: /swagger-ui.html      # Swagger UI 路径
+    enabled: true
+  packages-to-scan: com.campus.trade.controller
+  default-produces-media-type: application/json
+```
+
+访问路径:
+- OpenAPI JSON:`http://localhost:8080/v3/api-docs`
+- Swagger UI:`http://localhost:8080/swagger-ui.html`
+
+### 1.3 数据库 DDL
+
+```sql
+-- 用户表
+CREATE TABLE `user` (
+  `id`            BIGINT       NOT NULL AUTO_INCREMENT,
+  `student_id`    VARCHAR(20)  NOT NULL,
+  `name`          VARCHAR(50)  NOT NULL,
+  `password_hash` VARCHAR(255) NOT NULL,
+  `nickname`      VARCHAR(50)  DEFAULT NULL,
+  `avatar_url`    VARCHAR(255) DEFAULT NULL,
+  `signature`     VARCHAR(200) DEFAULT NULL,
+  `campus_card_url` VARCHAR(255) DEFAULT NULL,
+  `auth_status`   VARCHAR(20)  NOT NULL DEFAULT 'UNAUTHENTICATED',
+  `status`        VARCHAR(20)  NOT NULL DEFAULT 'NORMAL',
+  `role`          VARCHAR(20)  NOT NULL DEFAULT 'USER',
+  `created_at`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_student_id` (`student_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 分类表
+CREATE TABLE `category` (
+  `id`         BIGINT      NOT NULL AUTO_INCREMENT,
+  `name`       VARCHAR(50) NOT NULL,
+  `status`     VARCHAR(20) NOT NULL DEFAULT 'ENABLED',
+  `sort_order` INT         NOT NULL DEFAULT 0,
+  `created_at` DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 物品表
+CREATE TABLE `item` (
+  `id`           BIGINT        NOT NULL AUTO_INCREMENT,
+  `seller_id`    BIGINT        NOT NULL,
+  `category_id`  BIGINT        DEFAULT NULL,
+  `title`        VARCHAR(100)  NOT NULL,
+  `description`  TEXT,
+  `price`        DECIMAL(10,2) NOT NULL,
+  `images`       TEXT,
+  `trade_method` VARCHAR(20)   NOT NULL,
+  `status`       VARCHAR(20)   NOT NULL DEFAULT 'PENDING',
+  `created_at`   DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at`   DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_seller_id` (`seller_id`),
+  KEY `idx_category_id` (`category_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 订单表
+CREATE TABLE `order` (
+  `id`           BIGINT      NOT NULL AUTO_INCREMENT,
+  `item_id`      BIGINT      NOT NULL,
+  `buyer_id`     BIGINT      NOT NULL,
+  `seller_id`    BIGINT      NOT NULL,
+  `trade_method` VARCHAR(20) NOT NULL,
+  `status`       VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+  `created_at`   DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at`   DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_item_id` (`item_id`),
+  KEY `idx_buyer_id` (`buyer_id`),
+  KEY `idx_seller_id` (`seller_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 消息表
+CREATE TABLE `message` (
+  `id`           BIGINT       NOT NULL AUTO_INCREMENT,
+  `from_user_id` BIGINT       NOT NULL,
+  `to_user_id`   BIGINT       NOT NULL,
+  `item_id`      BIGINT       DEFAULT NULL,
+  `content`      TEXT,
+  `image_url`    VARCHAR(255) DEFAULT NULL,
+  `is_read`      TINYINT(1)   NOT NULL DEFAULT 0,
+  `created_at`   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_from_user_id` (`from_user_id`),
+  KEY `idx_to_user_id` (`to_user_id`),
+  KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 审核记录表
+CREATE TABLE `review` (
+  `id`           BIGINT       NOT NULL AUTO_INCREMENT,
+  `admin_id`     BIGINT       NOT NULL,
+  `review_type`  VARCHAR(20)  NOT NULL,
+  `target_id`    BIGINT       NOT NULL,
+  `result`       VARCHAR(20)  NOT NULL,
+  `reason`       VARCHAR(255) DEFAULT NULL,
+  `created_at`   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_review_type_target` (`review_type`, `target_id`),
+  KEY `idx_admin_id` (`admin_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+```
+
+### 1.4 Entity 类设计
+
+#### User.java
+
+```java
+package com.campus.trade.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@Schema(description = "用户实体")
+public class User {
+    private Long id;
+    private String studentId;
+    private String name;
+    private String passwordHash;
+    private String nickname;
+    private String avatarUrl;
+    private String signature;
+    private String campusCardUrl;
+    private String authStatus;    // UNAUTHENTICATED / PENDING / AUTHENTICATED
+    private String status;        // NORMAL / BANNED
+    private String role;          // USER / ADMIN
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+    // getters & setters
+}
+```
+
+#### Item.java
+
+```java
+package com.campus.trade.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "物品实体")
+public class Item {
+    private Long id;
+    private Long sellerId;
+    private Long categoryId;
+    private String title;
+    private String description;
+    private BigDecimal price;
+    private String images;
+    private String tradeMethod;     // PICKUP / DELIVERY
+    private String status;          // PENDING / ON_SALE / OFF_SHELF / SOLD
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+    // getters & setters
+}
+```
+
+#### Order.java / Message.java / Review.java / Category.java
+
+结构与概要设计一致,所有 Entity 添加 `@Schema(description = "...")` 类注解。
+
+### 1.5 DTO / VO 设计(含 SpringDoc Schema 注解)
+
+```java
+// -------- 请求 DTO --------
+
+@Schema(description = "登录请求")
+public class LoginRequest {
+    @Schema(description = "学号", example = "2021001001", required = true)
+    @NotBlank private String studentId;
+
+    @Schema(description = "密码", example = "123456", required = true)
+    @NotBlank private String password;
+}
+
+@Schema(description = "注册请求(multipart/form-data)")
+public class RegisterRequest {
+    @Schema(description = "学号", example = "2021001001", required = true)
+    @NotBlank private String studentId;
+
+    @Schema(description = "姓名", example = "张三", required = true)
+    @NotBlank private String name;
+
+    @Schema(description = "密码", example = "123456", required = true)
+    @NotBlank private String password;
+
+    @Schema(description = "校园卡照片文件")
+    private MultipartFile campusCardImage;
+}
+
+@Schema(description = "发布物品请求(multipart/form-data)")
+public class ItemPublishRequest {
+    @Schema(description = "标题", example = "全新高数课本", required = true)
+    @NotBlank private String title;
+
+    @Schema(description = "描述", example = "九成新,无涂写")
+    private String description;
+
+    @Schema(description = "价格", example = "25.00", required = true)
+    @NotNull private BigDecimal price;
+
+    @Schema(description = "分类ID", example = "1")
+    private Long categoryId;
+
+    @Schema(description = "交易方式", allowableValues = {"PICKUP", "DELIVERY"}, example = "PICKUP", required = true)
+    @NotBlank private String tradeMethod;
+
+    @Schema(description = "物品图片列表")
+    private List<MultipartFile> images;
+}
+
+@Schema(description = "修改密码请求")
+public class ChangePasswordRequest {
+    @Schema(description = "旧密码", required = true) @NotBlank private String oldPassword;
+    @Schema(description = "新密码", required = true) @NotBlank private String newPassword;
+}
+
+@Schema(description = "修改个人资料请求(multipart/form-data)")
+public class UserProfileRequest {
+    @Schema(description = "昵称", example = "小明") private String nickname;
+    @Schema(description = "个性签名", example = "热爱生活") private String signature;
+    @Schema(description = "头像文件") private MultipartFile avatar;
+}
+
+@Schema(description = "审核请求")
+public class ReviewRequest {
+    @Schema(description = "审核结果", allowableValues = {"APPROVED", "REJECTED"}, required = true)
+    @NotBlank private String result;
+    @Schema(description = "驳回原因(驳回时必填)", example = "照片不清晰")
+    private String reason;
+}
+
+@Schema(description = "创建订单请求")
+public class CreateOrderRequest {
+    @Schema(description = "物品ID", required = true, example = "1")
+    @NotNull private Long itemId;
+}
+
+@Schema(description = "发送消息请求")
+public class SendMessageRequest {
+    @Schema(description = "接收方用户ID", required = true, example = "2")
+    @NotNull private Long toUserId;
+    @Schema(description = "关联物品ID", example = "1")
+    private Long itemId;
+    @Schema(description = "文本内容")
+    private String content;
+    @Schema(description = "图片URL")
+    private String imageUrl;
+}
+
+@Schema(description = "分类请求")
+public class CategoryRequest {
+    @Schema(description = "分类名称", example = "教材", required = true)
+    @NotBlank private String name;
+    @Schema(description = "排序", example = "1")
+    private Integer sortOrder;
+}
+
+// -------- 响应 VO --------
+
+@Schema(description = "用户信息")
+public class UserProfileVO {
+    @Schema(description = "用户ID", example = "1")       private Long id;
+    @Schema(description = "学号", example = "2021001001") private String studentId;
+    @Schema(description = "姓名")                          private String name;
+    @Schema(description = "昵称")                          private String nickname;
+    @Schema(description = "头像URL")                      private String avatarUrl;
+    @Schema(description = "个性签名")                      private String signature;
+    @Schema(description = "认证状态")                      private String authStatus;
+    @Schema(description = "账号状态")                      private String status;
+    @Schema(description = "角色")                          private String role;
+    @Schema(description = "创建时间")                      private LocalDateTime createdAt;
+}
+
+@Schema(description = "物品信息")
+public class ItemVO {
+    @Schema(description = "物品ID", example = "1")        private Long id;
+    @Schema(description = "卖家ID")                       private Long sellerId;
+    @Schema(description = "卖家昵称")                     private String sellerNickname;
+    @Schema(description = "卖家头像")                     private String sellerAvatar;
+    @Schema(description = "分类名称")                     private String categoryName;
+    @Schema(description = "标题")                         private String title;
+    @Schema(description = "描述")                         private String description;
+    @Schema(description = "价格")                         private BigDecimal price;
+    @Schema(description = "图片列表")                     private List<String> images;
+    @Schema(description = "交易方式")                     private String tradeMethod;
+    @Schema(description = "状态")                         private String status;
+    @Schema(description = "发布时间")                     private LocalDateTime createdAt;
+}
+
+@Schema(description = "订单信息")
+public class OrderVO {
+    @Schema(description = "订单ID")                       private Long id;
+    @Schema(description = "物品ID")                       private Long itemId;
+    @Schema(description = "物品标题")                     private String itemTitle;
+    @Schema(description = "物品缩略图")                   private String itemImage;
+    @Schema(description = "物品价格")                     private BigDecimal itemPrice;
+    @Schema(description = "买家ID")                       private Long buyerId;
+    @Schema(description = "买家昵称")                     private String buyerNickname;
+    @Schema(description = "卖家ID")                       private Long sellerId;
+    @Schema(description = "卖家昵称")                     private String sellerNickname;
+    @Schema(description = "交易方式")                     private String tradeMethod;
+    @Schema(description = "订单状态")                     private String status;
+    @Schema(description = "创建时间")                     private LocalDateTime createdAt;
+    @Schema(description = "更新时间")                     private LocalDateTime updatedAt;
+}
+
+@Schema(description = "消息信息")
+public class MessageVO {
+    @Schema(description = "消息ID")                       private Long id;
+    @Schema(description = "发送方ID")                     private Long fromUserId;
+    @Schema(description = "发送方昵称")                   private String fromUserNickname;
+    @Schema(description = "发送方头像")                   private String fromUserAvatar;
+    @Schema(description = "接收方ID")                     private Long toUserId;
+    @Schema(description = "接收方昵称")                   private String toUserNickname;
+    @Schema(description = "文本内容")                     private String content;
+    @Schema(description = "图片URL")                      private String imageUrl;
+    @Schema(description = "是否已读")                     private Boolean isRead;
+    @Schema(description = "发送时间")                     private LocalDateTime createdAt;
+}
+
+@Schema(description = "会话信息")
+public class ConversationVO {
+    @Schema(description = "对方用户ID")                   private Long userId;
+    @Schema(description = "对方昵称")                     private String nickname;
+    @Schema(description = "对方头像")                     private String avatarUrl;
+    @Schema(description = "最后一条消息内容")              private String lastMessage;
+    @Schema(description = "最后一条消息图片")              private String lastMessageImage;
+    @Schema(description = "最后消息时间")                  private LocalDateTime lastMessageTime;
+    @Schema(description = "未读数")                       private Integer unreadCount;
+}
+
+// -------- 统一分页结果 --------
+
+@Schema(description = "分页结果")
+public class PageResult<T> {
+    @Schema(description = "数据列表")   private List<T> list;
+    @Schema(description = "当前页码")   private int pageNum;
+    @Schema(description = "每页条数")   private int pageSize;
+    @Schema(description = "总记录数")   private long total;
+    @Schema(description = "总页数")     private int pages;
+}
+```
+
+### 1.6 Controller 层设计(含 SpringDoc 注解)
+
+```java
+@RestController
+@RequestMapping("/api/auth")
+@Tag(name = "认证模块", description = "注册 / 登录 / 登出 / 修改密码")
+public class AuthController {
+
+    @Operation(summary = "用户注册", description = "学号+姓名+密码+校园卡照片,提交后进入待审核状态")
+    @ApiResponse(responseCode = "200", description = "注册成功")
+    @ApiResponse(responseCode = "409", description = "学号已注册")
+    @PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Result register(@Valid RegisterRequest req) { }
+
+    @Operation(summary = "用户登录", description = "学号+密码登录,创建 Session 会话")
+    @PostMapping("/login")
+    public Result login(@Valid @RequestBody LoginRequest req, HttpSession session) { }
+
+    @Operation(summary = "用户登出", description = "销毁当前 Session")
+    @PostMapping("/logout")
+    public Result logout(HttpSession session) { }
+
+    @Operation(summary = "修改密码", description = "需提供旧密码验证")
+    @PostMapping("/change-password")
+    public Result changePassword(@Valid @RequestBody ChangePasswordRequest req, HttpSession session) { }
+}
+
+@RestController
+@RequestMapping("/api/user")
+@Tag(name = "用户模块", description = "个人资料管理")
+public class UserController {
+
+    @Operation(summary = "获取个人资料")
+    @GetMapping("/profile")
+    public Result profile(HttpSession session) { }
+
+    @Operation(summary = "修改个人资料", description = "支持修改昵称、个性签名、头像")
+    @PutMapping(value = "/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Result updateProfile(@Valid UserProfileRequest req, HttpSession session) { }
+}
+
+@RestController
+@RequestMapping("/api/items")
+@Tag(name = "物品模块", description = "物品发布、浏览、搜索、管理")
+public class ItemController {
+
+    @Operation(summary = "首页物品列表", description = "按发布时间倒序,分页返回")
+    @GetMapping
+    public Result list(@RequestParam(defaultValue = "1") int pageNum,
+                       @RequestParam(defaultValue = "10") int pageSize) { }
+
+    @Operation(summary = "搜索物品", description = "按标题/描述模糊匹配")
+    @GetMapping("/search")
+    public Result search(@RequestParam String keyword,
+                         @RequestParam(defaultValue = "1") int pageNum,
+                         @RequestParam(defaultValue = "10") int pageSize) { }
+
+    @Operation(summary = "按分类筛选")
+    @GetMapping("/category/{id}")
+    public Result byCategory(@PathVariable Long id,
+                             @RequestParam(defaultValue = "1") int pageNum,
+                             @RequestParam(defaultValue = "10") int pageSize) { }
+
+    @Operation(summary = "物品详情")
+    @GetMapping("/{id}")
+    public Result detail(@PathVariable Long id) { }
+
+    @Operation(summary = "发布物品", description = "需要实名认证,图片以 multipart 上传")
+    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Result publish(@Valid ItemPublishRequest req, HttpSession session) { }
+
+    @Operation(summary = "我的发布列表")
+    @GetMapping("/my")
+    public Result myItems(HttpSession session) { }
+
+    @Operation(summary = "下架物品", description = "卖家下架自己的物品")
+    @PutMapping("/{id}/offline")
+    public Result offline(@PathVariable Long id, HttpSession session) { }
+
+    @Operation(summary = "删除物品", description = "卖家删除自己的物品")
+    @DeleteMapping("/{id}")
+    public Result delete(@PathVariable Long id, HttpSession session) { }
+}
+
+@RestController
+@RequestMapping("/api/orders")
+@Tag(name = "订单模块", description = "订单创建、状态流转、双视角查询")
+public class OrderController {
+
+    @Operation(summary = "生成订单", description = "买家点击『我想要』生成订单")
+    @PostMapping
+    public Result create(@RequestBody @Valid CreateOrderRequest req, HttpSession session) { }
+
+    @Operation(summary = "我买到的订单")
+    @GetMapping("/bought")
+    public Result bought(@RequestParam(defaultValue = "1") int pageNum,
+                         @RequestParam(defaultValue = "10") int pageSize,
+                         HttpSession session) { }
+
+    @Operation(summary = "我卖出的订单")
+    @GetMapping("/sold")
+    public Result sold(@RequestParam(defaultValue = "1") int pageNum,
+                       @RequestParam(defaultValue = "10") int pageSize,
+                       HttpSession session) { }
+
+    @Operation(summary = "卖家接单")
+    @PutMapping("/{id}/accept")
+    public Result accept(@PathVariable Long id, HttpSession session) { }
+
+    @Operation(summary = "卖家拒单")
+    @PutMapping("/{id}/reject")
+    public Result reject(@PathVariable Long id, HttpSession session) { }
+
+    @Operation(summary = "卖家确认交货")
+    @PutMapping("/{id}/deliver")
+    public Result deliver(@PathVariable Long id, HttpSession session) { }
+
+    @Operation(summary = "买家确认收货")
+    @PutMapping("/{id}/confirm")
+    public Result confirm(@PathVariable Long id, HttpSession session) { }
+}
+
+@RestController
+@RequestMapping("/api/messages")
+@Tag(name = "消息模块", description = "站内私信,文本+图片")
+public class MessageController {
+
+    @Operation(summary = "会话列表", description = "返回当前用户的所有会话及最后一条消息")
+    @GetMapping("/conversations")
+    public Result conversations(HttpSession session) { }
+
+    @Operation(summary = "聊天记录", description = "与指定用户的聊天记录,分页")
+    @GetMapping("/{userId}")
+    public Result history(@PathVariable Long userId,
+                          @RequestParam(defaultValue = "1") int pageNum,
+                          @RequestParam(defaultValue = "20") int pageSize,
+                          HttpSession session) { }
+
+    @Operation(summary = "发送消息")
+    @PostMapping
+    public Result send(@Valid @RequestBody SendMessageRequest req, HttpSession session) { }
+}
+
+@RestController
+@RequestMapping("/api/admin")
+@Tag(name = "审核模块", description = "管理员实名审核、物品审核")
+public class ReviewController {
+
+    @Operation(summary = "待审核用户列表")
+    @GetMapping("/review/users")
+    public Result pendingUsers(@RequestParam(defaultValue = "1") int pageNum,
+                               @RequestParam(defaultValue = "10") int pageSize,
+                               HttpSession session) { }
+
+    @Operation(summary = "审核用户实名认证")
+    @PostMapping("/review/user/{id}")
+    public Result reviewUser(@PathVariable Long id,
+                             @Valid @RequestBody ReviewRequest req,
+                             HttpSession session) { }
+
+    @Operation(summary = "待审核物品列表")
+    @GetMapping("/review/items")
+    public Result pendingItems(@RequestParam(defaultValue = "1") int pageNum,
+                               @RequestParam(defaultValue = "10") int pageSize,
+                               HttpSession session) { }
+
+    @Operation(summary = "审核物品上架")
+    @PostMapping("/review/item/{id}")
+    public Result reviewItem(@PathVariable Long id,
+                             @Valid @RequestBody ReviewRequest req,
+                             HttpSession session) { }
+}
+
+@RestController
+@RequestMapping("/api/admin")
+@Tag(name = "用户管理", description = "管理员管理用户账号")
+public class UserManageController {
+
+    @Operation(summary = "用户列表")
+    @GetMapping("/users")
+    public Result list(@RequestParam(defaultValue = "1") int pageNum,
+                       @RequestParam(defaultValue = "10") int pageSize,
+                       HttpSession session) { }
+
+    @Operation(summary = "封禁用户")
+    @PutMapping("/users/{id}/ban")
+    public Result ban(@PathVariable Long id, HttpSession session) { }
+
+    @Operation(summary = "解封用户")
+    @PutMapping("/users/{id}/unban")
+    public Result unban(@PathVariable Long id, HttpSession session) { }
+
+    @Operation(summary = "重置用户密码")
+    @PostMapping("/users/{id}/reset-password")
+    public Result resetPassword(@PathVariable Long id, HttpSession session) { }
+}
+
+@RestController
+@RequestMapping("/api/admin/categories")
+@Tag(name = "分类管理", description = "物品分类的增删改查")
+public class CategoryController {
+
+    @Operation(summary = "分类列表")
+    @GetMapping
+    public Result list() { }
+
+    @Operation(summary = "新增分类")
+    @PostMapping
+    public Result create(@Valid @RequestBody CategoryRequest req) { }
+
+    @Operation(summary = "修改分类")
+    @PutMapping("/{id}")
+    public Result update(@PathVariable Long id, @Valid @RequestBody CategoryRequest req) { }
+
+    @Operation(summary = "启用/禁用分类")
+    @PutMapping("/{id}/status")
+    public Result toggleStatus(@PathVariable Long id, @RequestBody StatusRequest req) { }
+}
+```
+
+### 1.7 Service 层核心业务逻辑
+
+#### AuthService — 注册
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant AC as AuthController
+    participant AS as AuthService
+    participant UM as UserMapper
+    participant FU as FileUploadUtil
+
+    C->>AC: POST /api/auth/register (multipart/form-data)
+    AC->>AS: register(request)
+    AS->>UM: selectByStudentId(studentId)
+    UM-->>AS: 返回 User / null
+    AS->>AS: 若已存在,抛异常"学号已注册"
+    AS->>FU: upload(campusCardImage)
+    FU-->>AS: 返回 "/upload/xxx.jpg"
+    AS->>AS: passwordHash = MD5.encode(password)
+    AS->>AS: 构建 User entity (authStatus=PENDING)
+    AS->>UM: insert(user)
+    UM-->>AS: 返回自增ID
+    AS-->>AC: 返回 Result.success(userId)
+    AC-->>C: { code:200, message:"注册成功,等待实名审核" }
+```
+
+#### AuthService — 登录
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant AC as AuthController
+    participant AS as AuthService
+    participant UM as UserMapper
+
+    C->>AC: POST /api/auth/login (学号+密码)
+    AC->>AS: login(studentId, password, session)
+    AS->>UM: selectByStudentId(studentId)
+    UM-->>AS: 返回 User / null
+    AS->>AS: 若为 null,抛异常"学号或密码错误"
+    AS->>AS: 若 user.status == BANNED,抛异常"账号已被封禁"
+    AS->>AS: 验证 MD5(password).equals(user.passwordHash)
+    AS->>AS: 若失败,抛异常"学号或密码错误"
+    AS->>AS: session.setAttribute("userId", user.id)
+    AS->>AS: session.setAttribute("role", user.role)
+    AS-->>AC: 返回 Result.success(userProfileVO)
+    AC-->>C: { code:200, data:{ id, nickname, avatar, role, authStatus } }
+```
+
+#### ItemService — 发布物品
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant IC as ItemController
+    participant IS as ItemService
+    participant UM as UserMapper
+    participant FU as FileUploadUtil
+    participant IM as ItemMapper
+
+    C->>IC: POST /api/items (multipart/form-data)
+    IC->>IS: publish(request, userId)
+    IS->>UM: selectById(userId)
+    UM-->>IS: 返回 User
+    IS->>IS: 校验 user.authStatus != AUTHENTICATED → 抛异常
+    IS->>FU: 遍历 images,逐一上传
+    FU-->>IS: 返回 ["/upload/a.jpg", "/upload/b.jpg"]
+    IS->>IS: 构建 Item entity (status=PENDING)
+    IS->>IM: insert(item)
+    IM-->>IS: 返回自增ID
+    IS-->>IC: 返回 Result.success(itemId)
+    IC-->>C: { code:200, message:"发布成功,等待管理员审核" }
+```
+
+#### ItemService — 首页物品列表
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant IC as ItemController
+    participant IS as ItemService
+    participant IM as ItemMapper
+
+    C->>IC: GET /api/items?pageNum=1&pageSize=10
+    IC->>IS: list(pageNum, pageSize)
+    IS->>IM: PageHelper.startPage(pageNum, pageSize)
+    IM->>IM: selectByStatus("ON_SALE") ORDER BY created_at DESC
+    IM-->>IS: PageInfo<Item>
+    IS->>IS: 补充 sellerNickname, categoryName 等
+    IS-->>IC: 返回 PageResult<ItemVO>
+    IC-->>C: { code:200, data:{ list:[...], total, pageNum, pageSize, pages } }
+```
+
+#### OrderService — 订单状态流转
+
+```mermaid
+stateDiagram-v2
+    [*] --> PENDING: 买家点击"我想要"
+    PENDING --> CONFIRMED: 卖家确认接单
+    PENDING --> CANCELLED: 卖家拒绝接单
+    CONFIRMED --> COMPLETED: 卖家确认交货 + 买家确认收货
+    CONFIRMED --> CANCELLED: 任意方取消
+    COMPLETED --> [*]
+    CANCELLED --> [*]
+```
+
+```mermaid
+sequenceDiagram
+    participant B as 买家
+    participant S as 卖家
+    participant OC as OrderController
+    participant OS as OrderService
+    participant OM as OrderMapper
+    participant IM as ItemMapper
+
+    B->>OC: POST /api/orders { itemId }
+    OC->>OS: create(itemId, buyerId)
+    OS->>IM: selectById(itemId)
+    IM-->>OS: 返回 Item
+    OS->>OS: 校验 item.status == ON_SALE
+    OS->>OS: 构建 Order (status=PENDING)
+    OS->>OM: insert(order)
+    OS->>IM: update item.status = SOLD
+    OS-->>OC: 返回 orderId
+    OC-->>B: 订单已创建,待卖家接单
+
+    S->>OC: PUT /api/orders/{id}/accept
+    OC->>OS: accept(orderId, sellerId)
+    OS->>OM: selectById(orderId) → 校验 → updateStatus(CONFIRMED)
+    OC-->>S: 接单成功,等待线下交易
+
+    S->>OC: PUT /api/orders/{id}/deliver
+    OC->>OS: deliver(orderId, sellerId)
+    OS->>OM: updateStatus(id, DELIVERED)
+    OC-->>S: 已交货,等待买家确认
+
+    B->>OC: PUT /api/orders/{id}/confirm
+    OC->>OS: confirm(orderId, buyerId)
+    OS->>OM: updateStatus(id, COMPLETED)
+    OC-->>B: 交易完成
+```
+
+#### MessageService — 私信
+
+```mermaid
+sequenceDiagram
+    participant B as 买家
+    participant MC as MessageController
+    participant MS as MessageService
+    participant MM as MessageMapper
+
+    B->>MC: POST /api/messages { toUserId, itemId, content }
+    MC->>MS: send(request, fromUserId)
+    MS->>MS: 构建 Message entity (isRead=false)
+    MS->>MM: insert(message)
+    MM-->>MS: 返回 id
+    MS-->>MC: 返回 messageId + createdAt
+    MC-->>B: 消息发送成功
+
+    B->>MC: GET /api/messages/conversations
+    MC->>MS: conversations(userId)
+    MS->>MM: selectConversations(userId)
+    MM-->>MS: 返回会话列表
+    MS-->>MC: 返回 List<ConversationVO>
+    MC-->>B: 会话列表渲染
+
+    B->>MC: GET /api/messages/{userId}?pageNum=1&pageSize=20
+    MC->>MS: history(fromUserId, toUserId, pageNum, pageSize)
+    MS->>MM: PageHelper.startPage → selectHistory
+    MM-->>MS: PageInfo<Message>
+    MS->>MM: markAsRead(fromUserId, toUserId)
+    MS-->>MC: 返回分页消息列表
+    MC-->>B: 聊天记录渲染
+```
+
+#### AdminService — 实名审核
+
+```mermaid
+sequenceDiagram
+    participant A as 管理员
+    participant RC as ReviewController
+    participant RS as ReviewService
+    participant UM as UserMapper
+    participant RM as ReviewMapper
+
+    A->>RC: GET /api/admin/review/users?pageNum=1
+    RC->>RS: pendingUsers(pageNum, pageSize)
+    RS->>UM: selectByAuthStatus(PENDING)
+    UM-->>RS: 返回用户列表(含 campusCardUrl)
+    RS-->>RC: 返回列表
+    RC-->>A: 展示待审核用户
+
+    A->>RC: POST /api/admin/review/user/{id} { result:APPROVED }
+    RC->>RS: reviewUser(id, adminId, request)
+    RS->>UM: selectById(id) → 校验 → updateAuthStatus(AUTHENTICATED)
+    RS->>RM: insert review 记录
+    RC-->>A: 审核通过
+
+    A->>RC: POST /api/admin/review/user/{id} { result:REJECTED, reason:"..." }
+    RC->>RS: reviewUser(id, adminId, request)
+    RS->>UM: updateAuthStatus(UNAUTHENTICATED)
+    RS->>RM: insert review(含 reason)
+    RC-->>A: 已驳回
+```
+
+### 1.8 Mapper 层核心 SQL
+
+```xml
+<!-- UserMapper.xml -->
+<mapper namespace="com.campus.trade.mapper.UserMapper">
+    <select id="selectByStudentId" resultType="User">
+        SELECT * FROM user WHERE student_id = #{studentId}
+    </select>
+    <select id="selectByAuthStatus" resultType="User">
+        SELECT * FROM user WHERE auth_status = #{status} ORDER BY created_at ASC
+    </select>
+    <update id="updateAuthStatus">
+        UPDATE user SET auth_status = #{authStatus} WHERE id = #{id}
+    </update>
+    <update id="updatePassword">
+        UPDATE user SET password_hash = #{passwordHash} WHERE id = #{id}
+    </update>
+</mapper>
+
+<!-- ItemMapper.xml -->
+<mapper namespace="com.campus.trade.mapper.ItemMapper">
+    <select id="selectByStatus" resultType="Item">
+        SELECT * FROM item WHERE status = #{status} ORDER BY created_at DESC
+    </select>
+    <select id="searchByKeyword" resultType="Item">
+        SELECT * FROM item WHERE status = 'ON_SALE'
+          AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%'))
+        ORDER BY created_at DESC
+    </select>
+    <select id="selectByCategoryId" resultType="Item">
+        SELECT * FROM item WHERE status = 'ON_SALE' AND category_id = #{categoryId}
+        ORDER BY created_at DESC
+    </select>
+    <select id="selectBySellerId" resultType="Item">
+        SELECT * FROM item WHERE seller_id = #{sellerId} ORDER BY created_at DESC
+    </select>
+    <update id="updateStatus">
+        UPDATE item SET status = #{status} WHERE id = #{id}
+    </update>
+</mapper>
+
+<!-- OrderMapper.xml -->
+<mapper namespace="com.campus.trade.mapper.OrderMapper">
+    <select id="selectByBuyerId" resultType="Order">
+        SELECT * FROM `order` WHERE buyer_id = #{buyerId} ORDER BY created_at DESC
+    </select>
+    <select id="selectBySellerId" resultType="Order">
+        SELECT * FROM `order` WHERE seller_id = #{sellerId} ORDER BY created_at DESC
+    </select>
+    <update id="updateStatus">
+        UPDATE `order` SET status = #{status} WHERE id = #{id}
+    </update>
+</mapper>
+
+<!-- MessageMapper.xml -->
+<mapper namespace="com.campus.trade.mapper.MessageMapper">
+    <select id="selectConversations" resultType="ConversationVO">
+        SELECT
+            CASE WHEN m.from_user_id = #{userId} THEN m.to_user_id ELSE m.from_user_id END AS userId,
+            u.nickname, u.avatar_url,
+            m.content AS lastMessage, m.image_url AS lastMessageImage,
+            m.created_at AS lastMessageTime,
+            (SELECT COUNT(*) FROM message WHERE to_user_id = #{userId}
+             AND from_user_id = CASE WHEN m.from_user_id = #{userId} THEN m.to_user_id ELSE m.from_user_id END
+             AND is_read = 0) AS unreadCount
+        FROM message m JOIN user u
+          ON u.id = CASE WHEN m.from_user_id = #{userId} THEN m.to_user_id ELSE m.from_user_id END
+        WHERE m.from_user_id = #{userId} OR m.to_user_id = #{userId}
+        GROUP BY userId ORDER BY MAX(m.created_at) DESC
+    </select>
+    <select id="selectHistory" resultType="Message">
+        SELECT * FROM message
+        WHERE (from_user_id = #{userId1} AND to_user_id = #{userId2})
+           OR (from_user_id = #{userId2} AND to_user_id = #{userId1})
+        ORDER BY created_at DESC
+    </select>
+    <update id="markAsRead">
+        UPDATE message SET is_read = 1
+        WHERE to_user_id = #{toUserId} AND from_user_id = #{fromUserId}
+    </update>
+</mapper>
+```
+
+### 1.9 配置文件关键内容
+
+```yaml
+# application.yml
+server:
+  port: 8080
+  servlet:
+    session:
+      timeout: 3600s
+
+spring:
+  datasource:
+    url: jdbc:mysql://localhost:3306/campus_trade?useUnicode=true&characterEncoding=utf8mb4&useSSL=false
+    username: root
+    password: root
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    hikari:
+      maximum-pool-size: 20
+      minimum-idle: 5
+  servlet:
+    multipart:
+      max-file-size: 10MB
+      max-request-size: 50MB
+  mvc:
+    static-path-pattern: /upload/**
+    static-locations: file:./upload/
+
+springdoc:
+  api-docs:
+    path: /api-docs
+  swagger-ui:
+    path: /swagger-ui.html
+    enabled: true
+  packages-to-scan: com.campus.trade.controller
+
+mybatis:
+  mapper-locations: classpath:mapper/*.xml
+  type-aliases-package: com.campus.trade.entity
+  configuration:
+    map-underscore-to-camel-case: true
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+
+pagehelper:
+  helper-dialect: mysql
+  reasonable: true
+  support-methods-arguments: true
+```
+
+### 1.10 拦截器设计
+
+```java
+@Component
+public class AuthInterceptor implements HandlerInterceptor {
+
+    private static final List<String> WHITE_LIST = Arrays.asList(
+        "/api/auth/register", "/api/auth/login",
+        "/api/items", "/api/items/search", "/api/items/category/", "/api/items/",
+        "/upload/",
+        "/api-docs", "/swagger-ui", "/v3/api-docs"   // SpringDoc 放行
+    );
+
+    @Override
+    public boolean preHandle(HttpServletRequest request,
+                             HttpServletResponse response,
+                             Object handler) throws Exception {
+        String uri = request.getRequestURI();
+        if (isWhiteListed(uri)) return true;
+        HttpSession session = request.getSession(false);
+        if (session == null || session.getAttribute("userId") == null) {
+            response.setContentType("application/json;charset=utf-8");
+            response.getWriter().write("{\"code\":401,\"message\":\"请先登录\"}");
+            return false;
+        }
+        return true;
+    }
+
+    private boolean isWhiteListed(String uri) {
+        return WHITE_LIST.stream().anyMatch(uri::startsWith);
+    }
+}
+```
+
+### 1.11 统一响应与异常处理
+
+```java
+public class Result {
+    private int code;
+    private String message;
+    private Object data;
+
+    public static Result success() { return new Result(200, "success", null); }
+    public static Result success(Object data) { return new Result(200, "success", data); }
+    public static Result error(int code, String message) { return new Result(code, message, null); }
+}
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BusinessException.class)
+    public Result handleBusiness(BusinessException e) {
+        return Result.error(e.getCode(), e.getMessage());
+    }
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public Result handleValidation(MethodArgumentNotValidException e) {
+        String msg = e.getBindingResult().getFieldErrors()
+                       .stream().map(FieldError::getDefaultMessage)
+                       .collect(Collectors.joining(", "));
+        return Result.error(400, msg);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public Result handleUnknown(Exception e) {
+        return Result.error(500, "服务器内部错误");
+    }
+}
+```
+
+### 1.12 后端项目结构(补充 SpringDoc)
+
+```
+src/main/java/com/campus/trade
+├── config
+│   ├── WebMvcConfig.java           # 静态资源配置
+│   ├── SessionConfig.java          # Session 配置
+│   └── MyBatisConfig.java          # MyBatis + PageHelper 配置
+├── controller
+│   ├── AuthController.java         # @Tag("认证模块")
+│   ├── UserController.java         # @Tag("用户模块")
+│   ├── ItemController.java         # @Tag("物品模块")
+│   ├── OrderController.java        # @Tag("订单模块")
+│   ├── MessageController.java      # @Tag("消息模块")
+│   └── admin
+│       ├── ReviewController.java   # @Tag("审核模块")
+│       ├── UserManageController.java
+│       └── CategoryController.java # @Tag("分类管理")
+├── service
+├── mapper
+├── entity
+├── dto                             # DTO/VO 含 @Schema 注解
+├── interceptor
+│   └── AuthInterceptor.java
+└── common
+    ├── Result.java
+    ├── StatusCode.java
+    └── FileUploadUtil.java
+```
+
+---
+
+## 第2章 客户端详细设计
+
+### 2.1 技术栈明细
+
+| 技术 | 用途 |
+|------|------|
+| Vue 3 (Composition API) | 前端框架 |
+| ElementPlus | UI 组件库 |
+| Vue Router 4 | 路由管理 |
+| Pinia | 状态管理 |
+| Axios | HTTP 请求 |
+| Vite | 构建工具 |
+| **Orval** | **基于 OpenAPI 3 自动生成 API 客户端代码** |
+
+### 2.2 Orval 集成设计
+
+#### 2.2.1 工作流
+
+```mermaid
+flowchart LR
+    A[SpringBoot 后端启动] --> B[SpringDoc 自动生成<br/>OpenAPI JSON]
+    B --> C[访问 /api-docs<br/>获取 openapi.json]
+    C --> D[Orval 读取 openapi.json<br/>执行 orval 命令]
+    D --> E[自动生成 src/api/endpoints<br/>类型定义 + API 函数]
+    E --> F[前端代码直接调用<br/>生成的 API 函数]
+```
+
+#### 2.2.2 安装 Orval
+
+```bash
+npm install orval --save-dev
+```
+
+#### 2.2.3 orval.config.ts 配置
+
+```typescript
+// orval.config.ts
+import { defineConfig } from 'orval';
+
+export default defineConfig({
+  campusTrade: {
+    input: {
+      // 方式一:开发时直接拉取运行中的后端 OpenAPI JSON
+      target: 'http://localhost:8080/api-docs',
+      // 方式二:或保存为本地文件
+      // target: './openapi.json',
+    },
+    output: {
+      // 生成的 API 代码输出目录
+      target: './src/api/endpoints',
+      // 生成的类型定义文件
+      schemas: './src/api/models',
+      // 客户端类型:axios
+      client: 'axios',
+      // 生成前清空目标目录
+      clean: true,
+      // 自定义 axios 实例路径
+      override: {
+        headerOptions: {
+          // 使用自定义 Axios 实例(含 withCredentials)
+          extraImport: `import { customAxios } from '@/utils/request'`,
+        },
+      },
+    },
+    hooks: {
+      // 生成完成后输出提示
+      afterAllFilesWrite: 'echo "API 代码生成完成"',
+    },
+  },
+});
+```
+
+#### 2.2.4 package.json 脚本
+
+```json
+{
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "api:generate": "orval --config orval.config.ts"
+  }
+}
+```
+
+#### 2.2.5 生成后的目录结构
+
+```
+src/api
+├── endpoints
+│   ├── auth.ts              # 认证 API(自动生成)
+│   ├── user.ts              # 用户 API
+│   ├── item.ts              # 物品 API
+│   ├── order.ts             # 订单 API
+│   ├── message.ts           # 消息 API
+│   └── admin.ts             # 管理员 API
+├── models                   # 类型定义(自动生成)
+│   ├── loginRequest.ts
+│   ├── registerRequest.ts
+│   ├── itemVO.ts
+│   ├── orderVO.ts
+│   ├── pageResult.ts
+│   └── ...
+```
+
+#### 2.2.6 生成的 API 代码示例
+
+```typescript
+// src/api/endpoints/auth.ts(由 Orval 自动生成)
+import { customAxios } from '@/utils/request';
+import type {
+  LoginRequest,
+  RegisterRequest,
+  UserProfileVO,
+  Result,
+} from '@/api/models';
+
+export const authRegister = (
+  registerRequest: RegisterRequest,
+) => {
+  return customAxios.post<Result>('/api/auth/register', registerRequest, {
+    headers: { 'Content-Type': 'multipart/form-data' },
+  });
+};
+
+export const authLogin = (
+  loginRequest: LoginRequest,
+) => {
+  return customAxios.post<Result & { data: UserProfileVO }>('/api/auth/login', loginRequest);
+};
+
+export const authLogout = () => {
+  return customAxios.post<Result>('/api/auth/logout');
+};
+
+export const authChangePassword = (
+  changePasswordRequest: ChangePasswordRequest,
+) => {
+  return customAxios.post<Result>('/api/auth/change-password', changePasswordRequest);
+};
+```
+
+```typescript
+// src/api/endpoints/item.ts(由 Orval 自动生成)
+import { customAxios } from '@/utils/request';
+import type { ItemVO, PageResult, Result } from '@/api/models';
+
+export const getItems = (pageNum?: number, pageSize?: number) => {
+  return customAxios.get<Result & { data: PageResult<ItemVO> }>('/api/items', {
+    params: { pageNum, pageSize },
+  });
+};
+
+export const searchItems = (keyword: string, pageNum?: number, pageSize?: number) => {
+  return customAxios.get<Result & { data: PageResult<ItemVO> }>('/api/items/search', {
+    params: { keyword, pageNum, pageSize },
+  });
+};
+
+export const getItemDetail = (id: number) => {
+  return customAxios.get<Result & { data: ItemVO }>(`/api/items/${id}`);
+};
+
+export const publishItem = (formData: FormData) => {
+  return customAxios.post<Result>('/api/items', formData, {
+    headers: { 'Content-Type': 'multipart/form-data' },
+  });
+};
+```
+
+```typescript
+// src/api/models/itemVO.ts(由 Orval 自动生成)
+export interface ItemVO {
+  id?: number;
+  sellerId?: number;
+  sellerNickname?: string;
+  sellerAvatar?: string;
+  categoryName?: string;
+  title?: string;
+  description?: string;
+  price?: number;
+  images?: string[];
+  tradeMethod?: string;
+  status?: string;
+  createdAt?: string;
+}
+
+// src/api/models/pageResult.ts
+export interface PageResult<T> {
+  list?: T[];
+  pageNum?: number;
+  pageSize?: number;
+  total?: number;
+  pages?: number;
+}
+```
+
+### 2.3 自定义 Axios 实例
+
+```typescript
+// utils/request.ts
+import axios from 'axios';
+import { ElMessage } from 'element-plus';
+import router from '@/router';
+
+export const customAxios = axios.create({
+  baseURL: '/api',
+  timeout: 15000,
+  withCredentials: true,   // 携带 Session Cookie
+  // 不设置全局 headers['Content-Type'],由 Orval 生成的代码按需指定
+});
+
+customAxios.interceptors.response.use(
+  (response) => {
+    const { code, message, data } = response.data;
+    if (code === 200) return response.data;
+    ElMessage.error(message);
+    if (code === 401) router.push('/login');
+    return Promise.reject(new Error(message));
+  },
+  (error) => {
+    ElMessage.error('网络错误');
+    return Promise.reject(error);
+  },
+);
+```
+
+### 2.4 在 Vue 组件中使用 Orval 生成的 API
+
+```vue
+<!-- Home.vue — 使用 Orval 生成的 API -->
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { getItems } from '@/api/endpoints/item';
+import type { ItemVO } from '@/api/models/itemVO';
+
+const itemList = ref<ItemVO[]>([]);
+const total = ref(0);
+const pageNum = ref(1);
+
+const loadItems = async () => {
+  const res = await getItems(pageNum.value, 10);
+  itemList.value = res.data.list ?? [];
+  total.value = res.data.total ?? 0;
+};
+
+onMounted(loadItems);
+</script>
+```
+
+```vue
+<!-- Login.vue — 使用 Orval 生成的 API -->
+<script setup lang="ts">
+import { ref } from 'vue';
+import { authLogin } from '@/api/endpoints/auth';
+import { useUserStore } from '@/stores/user';
+import { useRouter } from 'vue-router';
+
+const studentId = ref('');
+const password = ref('');
+const store = useUserStore();
+const router = useRouter();
+
+const handleLogin = async () => {
+  const res = await authLogin({ studentId: studentId.value, password: password.value });
+  store.$patch({ ...res.data, isLoggedIn: true });
+  await router.push('/home');
+};
+</script>
+```
+
+### 2.5 项目路由设计
+
+```typescript
+// router/index.ts
+const routes = [
+  // ===== C端 =====
+  { path: '/login',            component: () => import('@/views/Login.vue'), meta: { noAuth: true } },
+  { path: '/register',         component: () => import('@/views/Register.vue'), meta: { noAuth: true } },
+  { path: '/',                 redirect: '/home' },
+  { path: '/home',             component: () => import('@/views/Home.vue') },
+  { path: '/item/:id',         component: () => import('@/views/ItemDetail.vue') },
+  { path: '/search',           component: () => import('@/views/Search.vue') },
+  { path: '/publish',          component: () => import('@/views/Publish.vue'), meta: { requiresAuth: true, requiresVerified: true } },
+  { path: '/my/items',         component: () => import('@/views/MyItems.vue'), meta: { requiresAuth: true } },
+  { path: '/my/orders',        component: () => import('@/views/MyOrders.vue'), meta: { requiresAuth: true } },
+  { path: '/order/:id',        component: () => import('@/views/OrderDetail.vue'), meta: { requiresAuth: true } },
+  { path: '/messages',         component: () => import('@/views/Messages.vue'), meta: { requiresAuth: true } },
+  { path: '/messages/:userId', component: () => import('@/views/Chat.vue'), meta: { requiresAuth: true } },
+  { path: '/profile',          component: () => import('@/views/Profile.vue'), meta: { requiresAuth: true } },
+  { path: '/profile/password', component: () => import('@/views/ChangePassword.vue'), meta: { requiresAuth: true } },
+
+  // ===== B端 =====
+  { path: '/admin/login',              component: () => import('@/views/admin/Login.vue'), meta: { noAuth: true } },
+  { path: '/admin',                    redirect: '/admin/review/user' },
+  { path: '/admin/review/user',        component: () => import('@/views/admin/ReviewUser.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+  { path: '/admin/review/item',        component: () => import('@/views/admin/ReviewItem.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+  { path: '/admin/users',              component: () => import('@/views/admin/UserManage.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+  { path: '/admin/categories',         component: () => import('@/views/admin/CategoryManage.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+];
+```
+
+```mermaid
+graph TB
+    subgraph 路由守卫
+        direction LR
+        R[路由跳转] --> G1{meta.noAuth?}
+        G1 -->|是| Allow[放行]
+        G1 -->|否| G2{已登录?}
+        G2 -->|否| Login[跳转 /login]
+        G2 -->|是| G3{requiresAdmin?}
+        G3 -->|否| Allow
+        G3 -->|是| G4{role==ADMIN?}
+        G4 -->|否| Home[跳转 /home]
+        G4 -->|是| Allow
+    end
+```
+
+### 2.6 Pinia Store 设计
+
+```typescript
+// stores/user.ts
+export const useUserStore = defineStore('user', {
+  state: () => ({
+    userId: null as number | null,
+    nickname: '',
+    avatarUrl: '',
+    role: '',
+    authStatus: '',
+    isLoggedIn: false,
+  }),
+  getters: {
+    isAuthenticated: (state) => state.authStatus === 'AUTHENTICATED',
+    isAdmin: (state) => state.role === 'ADMIN',
+  },
+  actions: {
+    async login(studentId: string, password: string) {
+      const { authLogin } = await import('@/api/endpoints/auth');
+      const res = await authLogin({ studentId, password });
+      this.$patch({ ...res.data, isLoggedIn: true });
+    },
+    async logout() {
+      const { authLogout } = await import('@/api/endpoints/auth');
+      await authLogout();
+      this.$reset();
+    },
+    async fetchProfile() {
+      const { getUserProfile } = await import('@/api/endpoints/user');
+      const res = await getUserProfile();
+      this.$patch({ ...res.data, isLoggedIn: true });
+    },
+  },
+});
+```
+
+### 2.7 核心页面组件结构
+
+#### Home.vue — 首页
+
+```mermaid
+graph TB
+    Home[Home.vue]
+    SearchBar[搜索栏 el-input]
+    CategoryTabs[分类切换 el-tabs]
+    ItemGrid[物品网格 el-row/el-col]
+    ItemCard[物品卡片 el-card]
+    Pagination[分页 el-pagination]
+    Loading[加载骨架屏 el-skeleton]
+
+    Home --> SearchBar & CategoryTabs & ItemGrid & Pagination & Loading
+    ItemGrid --> ItemCard
+
+    subgraph ItemCard
+        Img[商品图片 el-image]
+        Title[标题]
+        Price[价格 ¥]
+        Seller[卖家昵称]
+    end
+```
+
+```vue
+<!-- Home.vue — 使用 Orval 生成的 API -->
+<template>
+  <div class="home">
+    <el-input v-model="keyword" placeholder="搜索闲置物品" @keyup.enter="onSearch" />
+    <el-tabs v-model="activeCategory" @tab-change="onCategoryChange">
+      <el-tab-pane label="全部" name="" />
+      <el-tab-pane v-for="cat in categories" :key="cat.id" :label="cat.name" :name="cat.id" />
+    </el-tabs>
+    <el-row :gutter="12">
+      <el-col v-for="item in itemList" :key="item.id" :span="12">
+        <el-card @click="goDetail(item.id)">
+          <el-image :src="item.images?.[0]" fit="cover" />
+          <h4>{{ item.title }}</h4>
+          <span class="price">¥{{ item.price }}</span>
+          <span class="seller">{{ item.sellerNickname }}</span>
+        </el-card>
+      </el-col>
+    </el-row>
+    <el-pagination v-model:current-page="pageNum" :total="total" :page-size="pageSize" @current-change="loadItems" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { getItems, searchItems } from '@/api/endpoints/item';
+import { getCategories } from '@/api/endpoints/admin';
+import type { ItemVO } from '@/api/models/itemVO';
+import type { Category } from '@/api/models/category';
+
+const keyword = ref('');
+const activeCategory = ref('');
+const itemList = ref<ItemVO[]>([]);
+const categories = ref<Category[]>([]);
+const total = ref(0);
+const pageNum = ref(1);
+const pageSize = 10;
+
+const loadItems = async () => {
+  const res = activeCategory.value
+    ? await getItems(activeCategory.value, pageNum.value, pageSize)
+    : await getItems(pageNum.value, pageSize);
+  itemList.value = res.data.list ?? [];
+  total.value = res.data.total ?? 0;
+};
+
+const onSearch = async () => {
+  const res = await searchItems(keyword.value, 1, pageSize);
+  itemList.value = res.data.list ?? [];
+};
+
+onMounted(async () => {
+  await loadItems();
+  const catRes = await getCategories();
+  categories.value = catRes.data ?? [];
+});
+</script>
+```
+
+#### OrderDetail.vue — 订单详情(含流程操作)
+
+```mermaid
+graph TB
+    OD[OrderDetail.vue]
+    Info[订单信息卡片]
+    StatusTag[状态标签 el-tag]
+    Timeline[时间线 el-timeline]
+    ActionBar[操作按钮区]
+    ChatBtn[联系对方 el-button]
+
+    OD --> Info & Timeline & ActionBar & ChatBtn
+    Info --> StatusTag
+
+    subgraph ActionBar
+        direction LR
+        PENDING[卖家: 接单/拒单]
+        CONFIRMED[卖家: 确认交货<br/>买家: 取消]
+        DELIVERED[买家: 确认收货]
+    end
+    StatusTag -->|待接单| PENDING
+    StatusTag -->|待面交| CONFIRMED
+    StatusTag -->|待收货| DELIVERED
+```
+
+#### MyOrders.vue — 双视角订单
+
+```mermaid
+graph TB
+    MO[MyOrders.vue]
+    Tab[Tabs: 我买到的 / 我卖出的]
+    List[订单列表]
+    OrderCard[订单卡片]
+    Pagination[分页]
+
+    MO --> Tab & List
+    Tab --> List
+    List --> OrderCard & Pagination
+
+    subgraph OrderCard
+        ItemImg[物品缩略图]
+        Info[标题+价格+交易方式]
+        Status[状态标签]
+        Action[操作按钮]
+    end
+```
+
+### 2.8 组件树总览
+
+```
+App.vue
+├── C端布局 (el-container)
+│   ├── 头部导航栏 (el-header)
+│   │   ├── 站名/Logo
+│   │   ├── 搜索入口
+│   │   ├── 消息通知
+│   │   └── 用户头像+下拉菜单
+│   ├── 页面主体 (el-main)
+│   │   ├── Login.vue
+│   │   ├── Register.vue
+│   │   ├── Home.vue
+│   │   ├── ItemDetail.vue
+│   │   ├── Search.vue
+│   │   ├── Publish.vue
+│   │   │   └── ImageUploader (el-upload)
+│   │   ├── MyItems.vue
+│   │   │   └── ItemCard
+│   │   ├── MyOrders.vue
+│   │   │   └── OrderCard
+│   │   ├── OrderDetail.vue
+│   │   │   └── ActionBar
+│   │   ├── Messages.vue
+│   │   │   └── ConversationItem
+│   │   ├── Chat.vue
+│   │   │   ├── MessageBubble
+│   │   │   └── InputArea (文本+图片)
+│   │   ├── Profile.vue
+│   │   └── ChangePassword.vue
+│   └── 底部导航栏 (el-footer 移动端适配)
+│       ├── 首页 | 发布 | 消息 | 我的
+│
+├── B端布局 (el-container)
+│   ├── 侧边栏 (el-aside)
+│   │   ├── 实名审核 | 物品审核 | 用户管理 | 分类管理
+│   └── 页面主体 (el-main)
+│       ├── ReviewUser.vue | ReviewItem.vue | UserManage.vue | CategoryManage.vue
+```
+
+### 2.9 移动端适配方案
+
+1. **Viewport 设置**
+   ```html
+   <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+   ```
+
+2. **ElementPlus 响应式栅格**
+   ```vue
+   <el-row :gutter="12">
+     <el-col :xs="24" :sm="12" :md="8" :lg="6">
+       <!-- 移动端占满一列,桌面端分多列 -->
+     </el-col>
+   </el-row>
+   ```
+
+3. **CSS 媒体查询**
+   ```scss
+   .home-card {
+     width: 100%;
+     @media (min-width: 768px) { width: 50%; }
+     @media (min-width: 1200px) { width: 33%; }
+   }
+   ```
+
+4. **触摸友好**:按钮最小高度 44px,列表间距 >= 12px
+
+5. **底部 TabBar(C端)**
+   ```vue
+   <el-footer v-if="!isAdminRoute">
+     <el-row>
+       <el-col :span="6" @click="router.push('/home')">
+         <el-icon><HomeFilled /></el-icon><span>首页</span>
+       </el-col>
+       <el-col :span="6" @click="router.push('/publish')">
+         <el-icon><Plus /></el-icon><span>发布</span>
+       </el-col>
+       <el-col :span="6" @click="router.push('/messages')">
+         <el-icon><ChatDotSquare /></el-icon><span>消息</span>
+       </el-col>
+       <el-col :span="6" @click="router.push('/profile')">
+         <el-icon><User /></el-icon><span>我的</span>
+       </el-col>
+     </el-row>
+   </el-footer>
+   ```
+
+### 2.10 核心交互流程
+
+#### 买家下单流程
+
+```mermaid
+sequenceDiagram
+    participant B as 买家
+    participant FE as 前端
+    participant API as 后端API
+
+    B->>FE: 进入物品详情页 /item/:id
+    FE->>API: GET /api/items/{id}
+    API-->>FE: 返回 ItemVO
+    FE-->>B: 渲染图文详情+卖家信息
+
+    B->>FE: 点击"我想要"
+    FE->>FE: 确认弹窗
+    B->>FE: 确认
+    FE->>API: POST /api/orders { itemId }
+    API-->>FE: { code:200, data:{ orderId } }
+    FE-->>B: 跳转订单详情页
+
+    B->>FE: 点击"联系卖家"
+    FE->>API: POST /api/messages { toUserId, itemId }
+    FE-->>B: 进入聊天界面
+```
+
+#### 卖家处理订单流程
+
+```mermaid
+sequenceDiagram
+    participant S as 卖家
+    participant FE as 前端
+    participant API as 后端API
+
+    S->>FE: 进入"我的订单" → 我卖出的
+    FE->>API: GET /api/orders/sold?pageNum=1
+    API-->>FE: 返回待接单订单列表
+    FE-->>S: 展示订单卡片
+
+    S->>FE: 点击"接单"
+    FE->>API: PUT /api/orders/{id}/accept
+    API-->>FE: 成功
+    FE-->>S: 状态变为"待面交"
+
+    S->>FE: 线下交易完成,点击"确认交货"
+    FE->>API: PUT /api/orders/{id}/deliver
+    API-->>FE: 成功
+    FE-->>S: 等待买家确认收货
+```
+
+#### 管理员审核流程
+
+```mermaid
+sequenceDiagram
+    participant A as 管理员
+    participant FE as 管理后台
+    participant API as 后端API
+
+    A->>FE: 进入"实名审核"页面
+    FE->>API: GET /api/admin/review/users?pageNum=1
+    API-->>FE: 返回待审核用户列表
+    FE-->>A: 展示用户列表
+
+    A->>FE: 点击"通过"
+    FE->>API: POST /api/admin/review/user/{id} { result:"APPROVED" }
+    API-->>FE: 成功
+    FE-->>A: 用户移出待审核列表
+
+    A->>FE: 点击"驳回" → 填写原因 → 提交
+    FE->>API: POST /api/admin/review/user/{id} { result:"REJECTED", reason:"..." }
+    API-->>FE: 成功
+    FE-->>A: 用户移出待审核列表
+```
+
+---
+
+## 第3章 核心业务状态枚举与常量定义
+
+```java
+public class AuthStatus {
+    public static final String UNAUTHENTICATED = "UNAUTHENTICATED";
+    public static final String PENDING        = "PENDING";
+    public static final String AUTHENTICATED  = "AUTHENTICATED";
+}
+
+public class UserStatus {
+    public static final String NORMAL = "NORMAL";
+    public static final String BANNED = "BANNED";
+}
+
+public class UserRole {
+    public static final String USER  = "USER";
+    public static final String ADMIN = "ADMIN";
+}
+
+public class ItemStatus {
+    public static final String PENDING   = "PENDING";
+    public static final String ON_SALE   = "ON_SALE";
+    public static final String OFF_SHELF = "OFF_SHELF";
+    public static final String SOLD      = "SOLD";
+}
+
+public class TradeMethod {
+    public static final String PICKUP   = "PICKUP";
+    public static final String DELIVERY = "DELIVERY";
+}
+
+public class OrderStatus {
+    public static final String PENDING    = "PENDING";
+    public static final String CONFIRMED  = "CONFIRMED";
+    public static final String DELIVERED  = "DELIVERED";
+    public static final String COMPLETED  = "COMPLETED";
+    public static final String CANCELLED  = "CANCELLED";
+}
+
+public class ReviewType {
+    public static final String AUTH = "AUTH";
+    public static final String ITEM = "ITEM";
+}
+
+public class ReviewResult {
+    public static final String APPROVED = "APPROVED";
+    public static final String REJECTED = "REJECTED";
+}
+
+public class CategoryStatus {
+    public static final String ENABLED  = "ENABLED";
+    public static final String DISABLED = "DISABLED";
+}
+
+public class StatusCode {
+    public static final int SUCCESS      = 200;
+    public static final int BAD_REQUEST  = 400;
+    public static final int UNAUTHORIZED = 401;
+    public static final int FORBIDDEN    = 403;
+    public static final int NOT_FOUND    = 404;
+    public static final int CONFLICT     = 409;
+    public static final int SERVER_ERROR = 500;
+}
+```
+
+---
+
+## 第4章 文件上传工具类
+
+```java
+@Component
+public class FileUploadUtil {
+
+    @Value("${upload.dir:./upload}")
+    private String uploadDir;
+
+    public String upload(MultipartFile file) {
+        if (file.isEmpty()) {
+            throw new BusinessException(400, "文件不能为空");
+        }
+        String originalFilename = file.getOriginalFilename();
+        String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
+        List<String> allowed = Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".webp");
+        if (!allowed.contains(ext.toLowerCase())) {
+            throw new BusinessException(400, "不支持的图片格式");
+        }
+        String filename = UUID.randomUUID() + ext;
+        File dest = new File(uploadDir, filename);
+        if (!dest.getParentFile().exists()) {
+            dest.getParentFile().mkdirs();
+        }
+        try {
+            file.transferTo(dest);
+        } catch (IOException e) {
+            throw new BusinessException(500, "文件上传失败");
+        }
+        return "/upload/" + filename;
+    }
+
+    public List<String> uploadBatch(List<MultipartFile> files) {
+        if (files == null) return Collections.emptyList();
+        return files.stream().map(this::upload).collect(Collectors.toList());
+    }
+}
+```
+
+---
+
+## 第5章 订单状态机校验
+
+```java
+@Component
+public class OrderStatusMachine {
+
+    private static final Map<String, List<String>> ALLOWED_TRANSITIONS = new HashMap<>();
+
+    static {
+        ALLOWED_TRANSITIONS.put(OrderStatus.PENDING,   Arrays.asList(OrderStatus.CONFIRMED, OrderStatus.CANCELLED));
+        ALLOWED_TRANSITIONS.put(OrderStatus.CONFIRMED, Arrays.asList(OrderStatus.DELIVERED, OrderStatus.CANCELLED));
+        ALLOWED_TRANSITIONS.put(OrderStatus.DELIVERED, Arrays.asList(OrderStatus.COMPLETED, OrderStatus.CANCELLED));
+        ALLOWED_TRANSITIONS.put(OrderStatus.COMPLETED, Collections.emptyList());
+        ALLOWED_TRANSITIONS.put(OrderStatus.CANCELLED, Collections.emptyList());
+    }
+
+    public void validateTransition(String currentStatus, String targetStatus) {
+        List<String> allowed = ALLOWED_TRANSITIONS.get(currentStatus);
+        if (allowed == null || !allowed.contains(targetStatus)) {
+            throw new BusinessException(400,
+                "订单状态不允许从 " + currentStatus + " 转移到 " + targetStatus);
+        }
+    }
+}
+```
+
+```java
+// 使用示例
+public void accept(Long orderId, Long sellerId) {
+    Order order = orderMapper.selectById(orderId);
+    if (!order.getSellerId().equals(sellerId)) {
+        throw new BusinessException(403, "无权操作该订单");
+    }
+    statusMachine.validateTransition(order.getStatus(), OrderStatus.CONFIRMED);
+    orderMapper.updateStatus(orderId, OrderStatus.CONFIRMED);
+}
+```
+
+---
+
+## 第6章 前端文件上传组件
+
+```vue
+<template>
+  <el-upload
+    :action="uploadAction"
+    :on-success="handleSuccess"
+    :on-remove="handleRemove"
+    :file-list="fileList"
+    list-type="picture-card"
+    :multiple="multiple"
+    :limit="limit"
+    :before-upload="beforeUpload"
+  >
+    <el-icon><Plus /></el-icon>
+  </el-upload>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { ElMessage } from 'element-plus';
+
+const props = defineProps({
+  multiple: { type: Boolean, default: false },
+  limit: { type: Number, default: 1 },
+  modelValue: { type: Array<string>, default: () => [] },
+});
+
+const emit = defineEmits(['update:modelValue']);
+const uploadAction = '/api/upload';
+const fileList = ref(props.modelValue.map(url => ({ url })));
+
+const beforeUpload = (file: File) => {
+  if (!file.type.startsWith('image/')) { ElMessage.error('只能上传图片'); return false; }
+  if (file.size > 10 * 1024 * 1024) { ElMessage.error('图片大小不能超过 10MB'); return false; }
+  return true;
+};
+
+const handleSuccess = (res: any) => {
+  if (res.code === 200) {
+    fileList.value.push({ url: res.data.url });
+    emit('update:modelValue', fileList.value.map(f => f.url));
+  }
+};
+
+const handleRemove = (file: any) => {
+  fileList.value = fileList.value.filter(f => f.url !== file.url);
+  emit('update:modelValue', fileList.value.map(f => f.url));
+};
+</script>
+```
+
+---
+
+## 第7章 安全与校验补充
+
+| 场景 | 措施 |
+|------|------|
+| SQL 注入 | MyBatis 参数绑定(#{}),无拼接 SQL |
+| XSS | Vue3 默认转义,后端输出编码 |
+| 文件上传 | 白名单后缀 + 重命名 + 限制大小 |
+| 越权操作 | 每个接口校验当前用户 ID 与资源所有者匹配 |
+| Session 安全 | HttpOnly Cookie,设置 secure(HTTPS) |
+| 密码安全 | MD5 加密 |
+| API 文档安全 | 生产环境关闭 springdoc 暴露 |
+
+---
+
+## 第8章 开发与协作流程
+
+```mermaid
+flowchart TB
+    subgraph 服务端
+        S1[编写 Controller / DTO<br/>添加 @Tag @Operation @Schema] --> S2[启动 SpringBoot]
+        S2 --> S3[访问 /api-docs<br/>获取 OpenAPI JSON]
+    end
+
+    subgraph 客户端
+        C1[运行 orval<br/>orval --config orval.config.ts] --> C2[自动生成<br/>src/api/endpoints/*.ts<br/>src/api/models/*.ts]
+        C2 --> C3[组件中直接调用<br/>import { getItems } from '@/api/endpoints/item']
+    end
+
+    S3 -->|http://localhost:8080/api-docs| C1
+    S3 -->|Swagger UI 调试| S4[开发者通过<br/>swagger-ui.html 调试]
+```
+
+### 8.1 典型开发流程
+
+1. 后端开发者在 Controller 中添加 `@Tag`、`@Operation`、`@ApiResponse` 注解,在 DTO/VO 中添加 `@Schema` 注解
+2. 启动 SpringBoot 后,访问 `http://localhost:8080/api-docs` 获取 OpenAPI JSON
+3. 前端开发者运行 `npm run api:generate`(即 `orval`),自动从 OpenAPI JSON 生成 TypeScript 类型定义和 API 函数
+4. 前端在 Vue 组件中直接 import 生成的 API 函数,无需手动编写任何 `api/*.js` 文件
+5. 后端 API 发生变化时,只需重新运行 `npm run api:generate` 即可同步

+ 438 - 0
doc/fristDesign.md

@@ -0,0 +1,438 @@
+# 校园闲置物品交易平台 — 概要设计
+
+## 1. 系统架构
+
+采用前后端分离架构:
+
+- **前端**:Vue3 + ElementPlus,Web 端适配移动端
+- **后端**:SpringBoot + MyBatis + PageHelper
+- **存储层**:关系型数据库 + 本地文件存储(`./upload`)
+- **认证方案**:Session
+
+```mermaid
+graph TB
+    subgraph 客户端层
+        C[C端 买家/卖家<br/>Vue3 + ElementPlus]
+        B[B端 后台管理员<br/>Vue3 + ElementPlus]
+    end
+    subgraph 服务端层
+        API[API 接口层<br/>SpringBoot]
+        US[用户服务]
+        IS[物品服务]
+        OS[订单服务]
+        MS[消息服务]
+        RS[审核服务]
+    end
+    subgraph 存储层
+        DB[(MySQL 数据库)]
+        FS[本地文件存储<br/>./upload]
+    end
+    C --> API
+    B --> API
+    API --> US & IS & OS & MS & RS
+    US --> DB
+    IS --> DB & FS
+    OS --> DB
+    MS --> DB & FS
+    RS --> DB
+```
+
+---
+
+## 2. 功能模块划分
+
+### 2.1 用户模块
+| 子功能 | 说明 |
+|--------|------|
+| 注册 | 学号 + 姓名 + 密码 + 校园卡照片上传 |
+| 登录 | 学号 + 密码,Session 会话管理 |
+| 个人资料管理 | 头像/昵称/个性签名修改 |
+| 密码管理 | 修改密码(旧密码验证) |
+| 认证状态 | 标记未认证/待审核/已认证 |
+
+### 2.2 物品模块
+| 子功能 | 说明 |
+|--------|------|
+| 物品发布 | 图片/标题/描述/价格/分类/交易方式(自提/送货上门) |
+| 物品列表 | 首页按发布时间倒序展示,PageHelper 分页 |
+| 物品搜索 | 按标题/描述模糊匹配 |
+| 分类筛选 | 按分类查询物品 |
+| 物品详情 | 图文/价格/卖家昵称/交易方式 |
+| 物品状态 | 售卖中/已下架/已售出 |
+| 我的发布 | 卖家查看和管理自己的发布(下架/删除) |
+
+### 2.3 订单模块
+| 子功能 | 说明 |
+|--------|------|
+| 生成订单 | 买家点击"我想要",继承卖家交易方式 |
+| 订单状态机 | 待接单 → 待面交 → 已完成 / 已取消 |
+| 卖家接单/拒单 | 确认接单或拒绝订单 |
+| 卖家确认交货 | 线下面交完成后标记 |
+| 买家确认收货 | 线下收到物品后确认 |
+| 我的订单 | 双视角:我买到的 / 我卖出的 |
+
+### 2.4 消息模块
+| 子功能 | 说明 |
+|--------|------|
+| 站内私信 | 文本 + 图片 |
+| 联系卖家 | 买家从物品详情页发起会话 |
+
+### 2.5 审核模块(后台 Web)
+| 子功能 | 说明 |
+|--------|------|
+| 实名认证审核 | 查看校园卡照片,通过/驳回(驳回需填原因) |
+| 物品上架审核 | 通过上架/驳回下架 |
+| 用户管理 | 封禁/解封用户 |
+| 密码重置 | 管理员手动重置用户密码 |
+| 分类管理 | 新增/修改/禁用物品分类 |
+
+---
+
+## 3. 数据库概念设计
+
+### 3.1 核心实体关系
+
+```mermaid
+erDiagram
+    User ||--o{ Item : "发布"
+    User ||--o{ Order : "买家"
+    User ||--o{ Order : "卖家"
+    User ||--o{ Message : "发送"
+    User ||--o{ Message : "接收"
+    User ||--o{ Review : "审核对象"
+    User ||--o{ Review : "审核人"
+    Category ||--o{ Item : "属于"
+    Item ||--o{ Order : "生成"
+    Item ||--o{ Message : "关联"
+```
+
+### 3.2 主要数据表
+
+> 所有外键均为 **逻辑外键**,不建立物理外键约束。
+
+#### 用户表(user)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK | 用户ID |
+| student_id | VARCHAR(20) UNIQUE | 学号 |
+| name | VARCHAR(50) | 姓名 |
+| password_hash | VARCHAR(255) | bcrypt 密码哈希 |
+| nickname | VARCHAR(50) | 昵称 |
+| avatar_url | VARCHAR(255) | 头像 URL(`/upload/...`) |
+| signature | VARCHAR(200) | 个性签名 |
+| campus_card_url | VARCHAR(255) | 校园卡照片 URL |
+| auth_status | VARCHAR(20) | 未认证/待审核/已认证 |
+| status | VARCHAR(20) | 正常/封禁 |
+| role | VARCHAR(20) | 普通用户/管理员 |
+| created_at | DATETIME | 创建时间 |
+| updated_at | DATETIME | 更新时间 |
+
+#### 分类表(category)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK | 分类ID |
+| name | VARCHAR(50) | 分类名称 |
+| status | VARCHAR(20) | 启用/禁用 |
+| sort_order | INT | 排序 |
+| created_at | DATETIME | 创建时间 |
+
+#### 物品表(item)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK | 物品ID |
+| seller_id | BIGINT | 卖家ID(逻辑外键 → user.id) |
+| category_id | BIGINT | 分类ID(逻辑外键 → category.id) |
+| title | VARCHAR(100) | 标题 |
+| description | TEXT | 描述 |
+| price | DECIMAL(10,2) | 价格 |
+| images | TEXT | 图片 URL 列表(JSON 或逗号分隔) |
+| trade_method | VARCHAR(20) | 自提/送货上门 |
+| status | VARCHAR(20) | 待审核/售卖中/已下架/已售出 |
+| created_at | DATETIME | 发布时间 |
+| updated_at | DATETIME | 更新时间 |
+
+#### 订单表(order)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK | 订单ID |
+| item_id | BIGINT | 物品ID(逻辑外键 → item.id) |
+| buyer_id | BIGINT | 买家ID(逻辑外键 → user.id) |
+| seller_id | BIGINT | 卖家ID(逻辑外键 → user.id) |
+| trade_method | VARCHAR(20) | 自提/送货上门 |
+| status | VARCHAR(20) | 待接单/待面交/已完成/已取消 |
+| created_at | DATETIME | 创建时间 |
+| updated_at | DATETIME | 更新时间 |
+
+#### 消息表(message)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK | 消息ID |
+| from_user_id | BIGINT | 发送方(逻辑外键 → user.id) |
+| to_user_id | BIGINT | 接收方(逻辑外键 → user.id) |
+| item_id | BIGINT NULL | 关联物品(逻辑外键 → item.id) |
+| content | TEXT | 文本内容 |
+| image_url | VARCHAR(255) NULL | 图片 URL |
+| is_read | TINYINT(1) | 是否已读 |
+| created_at | DATETIME | 发送时间 |
+
+#### 审核记录表(review)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK | 审核ID |
+| admin_id | BIGINT | 管理员ID(逻辑外键 → user.id) |
+| review_type | VARCHAR(20) | 实名认证/物品上架 |
+| target_id | BIGINT | 关联ID(用户ID/物品ID) |
+| result | VARCHAR(20) | 通过/驳回 |
+| reason | VARCHAR(255) | 驳回原因 |
+| created_at | DATETIME | 审核时间 |
+
+---
+
+## 4. 核心业务流程设计
+
+### 4.1 用户认证流程
+
+```mermaid
+flowchart LR
+    A[填写注册信息<br/>学号+姓名+密码+校园卡照片] --> B[提交注册]
+    B --> C[状态: 待审核]
+    C --> D{管理员审核}
+    D -->|通过| E[状态: 已认证]
+    D -->|驳回+原因| F[状态: 未认证<br/>用户可重新提交]
+```
+
+### 4.2 物品发布与交易流程
+
+```mermaid
+flowchart TB
+    A[卖家发布闲置] --> B[状态: 待审核]
+    B --> C{管理员审核}
+    C -->|通过| D[状态: 售卖中]
+    C -->|驳回| E[状态: 已下架]
+    D --> F[买家浏览并点击"我想要"]
+    F --> G[生成订单 状态: 待接单]
+    G --> H{卖家处理}
+    H -->|接单| I[状态: 待面交]
+    H -->|拒单| J[状态: 已取消]
+    I --> K[线下交易完成<br/>卖家确认交货]
+    K --> L[买家确认收货]
+    L --> M[状态: 已完成]
+```
+
+### 4.3 订单状态机
+
+```mermaid
+stateDiagram-v2
+    [*] --> 待接单: 买家下单
+    待接单 --> 待面交: 卖家接单
+    待接单 --> 已取消: 卖家拒单
+    待面交 --> 已完成: 卖家确认交货 + 买家确认收货
+    待面交 --> 已取消: 任意方取消
+    已完成 --> [*]
+    已取消 --> [*]
+```
+
+---
+
+## 5. 前端页面结构
+
+### 5.1 C端(买家/卖家)— Vue3 + ElementPlus
+
+| 页面 | 路由 | 说明 |
+|------|------|------|
+| 登录页 | /login | 学号+密码登录,Session 会话 |
+| 注册页 | /register | 学号+姓名+密码+校园卡上传 |
+| 首页 | /home | 物品列表(时间倒序,ElementPlus 分页组件) |
+| 物品详情 | /item/:id | 图文详情、联系卖家、我想要 |
+| 物品搜索 | /search | 关键词搜索+分类筛选 |
+| 物品发布 | /publish | 发布闲置表单(ElementPlus 表单组件) |
+| 我的发布 | /my/items | 售卖中/已下架/已售出 Tab 切换 |
+| 我的订单 | /my/orders | 我买到的/我卖出的双视角 |
+| 订单详情 | /order/:id | 订单操作按钮 |
+| 消息列表 | /messages | 会话列表 |
+| 消息聊天 | /messages/:userId | 聊天界面 |
+| 个人资料 | /profile | 查看/编辑资料 |
+| 修改密码 | /profile/password | 旧密码+新密码 |
+
+### 5.2 B端(后台Web端)— Vue3 + ElementPlus
+
+| 页面 | 路由 | 说明 |
+|------|------|------|
+| 登录 | /admin/login | 管理员登录 |
+| 实名审核 | /admin/review/user | 待审核列表+通过/驳回操作 |
+| 物品审核 | /admin/review/item | 物品上架审核 |
+| 用户管理 | /admin/users | 封禁/解封/密码重置 |
+| 分类管理 | /admin/categories | 新增/修改/禁用分类 |
+
+---
+
+## 6. API 接口概览
+
+### 用户相关
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /api/auth/register | 注册 |
+| POST | /api/auth/login | 登录(创建 Session) |
+| POST | /api/auth/logout | 登出(销毁 Session) |
+| POST | /api/auth/change-password | 修改密码 |
+| GET | /api/user/profile | 获取个人资料 |
+| PUT | /api/user/profile | 修改个人资料 |
+
+### 物品相关
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /api/items | 首页物品列表(PageHelper 分页+时间倒序) |
+| GET | /api/items/search | 关键词搜索 |
+| GET | /api/items/category/{id} | 分类筛选 |
+| GET | /api/items/{id} | 物品详情 |
+| POST | /api/items | 发布物品 |
+| GET | /api/items/my | 我的发布列表 |
+| PUT | /api/items/{id}/offline | 下架物品 |
+| DELETE | /api/items/{id} | 删除物品 |
+
+### 订单相关
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /api/orders | 生成订单 |
+| GET | /api/orders/bought | 我买到的(PageHelper 分页) |
+| GET | /api/orders/sold | 我卖出的(PageHelper 分页) |
+| PUT | /api/orders/{id}/accept | 卖家接单 |
+| PUT | /api/orders/{id}/reject | 卖家拒单 |
+| PUT | /api/orders/{id}/deliver | 卖家确认交货 |
+| PUT | /api/orders/{id}/confirm | 买家确认收货 |
+
+### 消息相关
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /api/messages/conversations | 会话列表 |
+| GET | /api/messages/{userId} | 聊天记录 |
+| POST | /api/messages | 发送消息 |
+
+### 管理员相关
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /api/admin/review/users | 待审核用户列表 |
+| POST | /api/admin/review/user/{id} | 用户审核(通过/驳回) |
+| GET | /api/admin/review/items | 待审核物品列表 |
+| POST | /api/admin/review/item/{id} | 物品审核(通过/驳回) |
+| GET | /api/admin/users | 用户管理列表 |
+| PUT | /api/admin/users/{id}/ban | 封禁用户 |
+| PUT | /api/admin/users/{id}/unban | 解封用户 |
+| POST | /api/admin/users/{id}/reset-password | 密码重置 |
+| GET | /api/admin/categories | 分类列表 |
+| POST | /api/admin/categories | 新增分类 |
+| PUT | /api/admin/categories/{id} | 修改分类 |
+| PUT | /api/admin/categories/{id}/status | 启用/禁用分类 |
+
+---
+
+## 7. 非功能性设计要点
+
+| 方面 | 方案 |
+|------|------|
+| 图片存储 | 本地 `./upload` 目录,URL 为 `/upload/{filename}` |
+| 密码安全 | bcrypt 哈希存储 |
+| 认证鉴权 | Session + Cookie |
+| 分页 | PageHelper 插件,统一分页参数(pageNum, pageSize) |
+| 接口规范 | 统一响应格式 `{ code, message, data }` |
+| 状态码 | HTTP 200 + 业务 code(如 401 未登录,403 无权限) |
+
+---
+
+## 8. 后端项目结构(SpringBoot + MyBatis)
+
+```
+src/main/java/com/campus/trade
+├── config
+│   ├── WebMvcConfig.java          # 静态资源配置(/upload 映射)
+│   ├── SessionConfig.java         # Session 配置
+│   └── MyBatisConfig.java         # MyBatis + PageHelper 配置
+├── controller
+│   ├── AuthController.java
+│   ├── UserController.java
+│   ├── ItemController.java
+│   ├── OrderController.java
+│   ├── MessageController.java
+│   └── admin
+│       ├── ReviewController.java
+│       ├── UserManageController.java
+│       └── CategoryController.java
+├── service
+│   ├── AuthService.java
+│   ├── UserService.java
+│   ├── ItemService.java
+│   ├── OrderService.java
+│   ├── MessageService.java
+│   └── admin
+│       ├── ReviewService.java
+│       ├── UserManageService.java
+│       └── CategoryService.java
+├── mapper
+│   ├── UserMapper.java
+│   ├── ItemMapper.java
+│   ├── OrderMapper.java
+│   ├── MessageMapper.java
+│   ├── ReviewMapper.java
+│   └── CategoryMapper.java
+├── entity
+│   ├── User.java
+│   ├── Item.java
+│   ├── Order.java
+│   ├── Message.java
+│   ├── Review.java
+│   └── Category.java
+├── dto
+│   ├── LoginRequest.java
+│   ├── RegisterRequest.java
+│   ├── ItemPublishRequest.java
+│   └── PageResult.java
+├── interceptor
+│   └── AuthInterceptor.java       # Session 登录校验拦截器
+├── common
+│   ├── Result.java                # 统一响应体
+│   ├── StatusCode.java            # 业务状态码枚举
+│   └── FileUploadUtil.java        # 本地文件上传工具类
+```
+
+---
+
+## 9. 前端项目结构(Vue3 + ElementPlus)
+
+```
+src
+├── api
+│   ├── auth.js
+│   ├── user.js
+│   ├── item.js
+│   ├── order.js
+│   ├── message.js
+│   └── admin.js
+├── views
+│   ├── Login.vue
+│   ├── Register.vue
+│   ├── Home.vue
+│   ├── ItemDetail.vue
+│   ├── Search.vue
+│   ├── Publish.vue
+│   ├── MyItems.vue
+│   ├── MyOrders.vue
+│   ├── OrderDetail.vue
+│   ├── Messages.vue
+│   ├── Chat.vue
+│   ├── Profile.vue
+│   ├── ChangePassword.vue
+│   └── admin
+│       ├── Login.vue
+│       ├── ReviewUser.vue
+│       ├── ReviewItem.vue
+│       ├── UserManage.vue
+│       └── CategoryManage.vue
+├── router
+│   └── index.js
+├── stores
+│   └── user.js                    # Pinia 用户状态管理
+├── utils
+│   └── request.js                 # Axios 封装(携带 Session Cookie)
+├── App.vue
+└── main.js
+```

+ 920 - 0
doc/work.md

@@ -0,0 +1,920 @@
+# 服务端开发工作记录
+
+---
+
+## 阶段零:项目初始化
+
+### 步骤 0.1 — 更新 pom.xml
+
+**操作**:
+1. 在 `pom.xml` 中新增以下依赖:
+
+```xml
+<!-- MyBatis 分页插件 -->
+<dependency>
+    <groupId>com.github.pagehelper</groupId>
+    <artifactId>pagehelper-spring-boot-starter</artifactId>
+    <version>2.1.0</version>
+</dependency>
+
+<!-- 参数校验 -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-validation</artifactId>
+</dependency>
+```
+
+2. 无需添加密码加密依赖 — 使用 Spring 内置 `org.springframework.util.DigestUtils`(MD5)
+
+**验证**:`mvn dependency:tree` 无报错
+
+---
+
+### 步骤 0.2 — 创建新包结构 + ServerApplication
+
+**操作**:
+1. 在 `src/main/java/com/campus/trade/` 下创建子包目录:
+
+```
+com.campus.trade
+├── config
+├── common
+├── entity
+├── dto
+├── vo
+├── mapper
+├── service
+│   └── admin
+└── controller
+    └── admin
+```
+
+2. 新建 `src/main/java/com/campus/trade/ServerApplication.java`:
+
+```java
+package com.campus.trade;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication(scanBasePackages = "com.campus.trade")
+public class ServerApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ServerApplication.class, args);
+    }
+}
+```
+
+3. 保留旧 `space.anyi` 包下的入口作为备份,后续删除
+
+**验证**:`mvn compile` 编译通过
+
+---
+
+### 步骤 0.3 — 更新 application.yaml
+
+**操作**:将 `server/src/main/resources/application.yaml` 更新为:
+
+```yaml
+spring:
+  application:
+    name: CampusIdleItemTradingPlatform
+  datasource:
+    url: jdbc:mysql://localhost:3306/campus_idle_item_trading_platform?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
+    username: root
+    password: root
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    hikari:
+      maximum-pool-size: 20
+      minimum-idle: 5
+  servlet:
+    multipart:
+      max-file-size: 10MB
+      max-request-size: 50MB
+  mvc:
+    static-path-pattern: /upload/**
+    static-locations: file:./upload/
+
+mybatis:
+  mapper-locations: classpath:mapper/*.xml
+  type-aliases-package: com.campus.trade.entity
+  configuration:
+    map-underscore-to-camel-case: true
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+
+pagehelper:
+  helper-dialect: mysql
+  reasonable: true
+  support-methods-arguments: true
+
+springdoc:
+  api-docs:
+    path: /v3/api-docs
+  swagger-ui:
+    path: /swagger-ui.html
+    enabled: true
+  packages-to-scan: com.campus.trade.controller
+  default-produces-media-type: application/json
+
+server:
+  servlet:
+    context-path: /api
+    session:
+      timeout: 3600s
+  port: 8080
+```
+
+---
+
+## 阶段一:基础设施层
+
+### 步骤 1.1 — common/Result.java
+
+**路径**:`src/main/java/com/campus/trade/common/Result.java`
+
+**说明**:统一 API 响应体,所有 Controller 返回此对象。
+
+```java
+package com.campus.trade.common;
+
+public class Result {
+    private int code;
+    private String message;
+    private Object data;
+
+    public Result() {}
+
+    public Result(int code, String message, Object data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public static Result success() {
+        return new Result(200, "success", null);
+    }
+
+    public static Result success(Object data) {
+        return new Result(200, "success", data);
+    }
+
+    public static Result error(int code, String message) {
+        return new Result(code, message, null);
+    }
+
+    // getters & setters
+}
+```
+
+---
+
+### 步骤 1.2 — common/StatusCode.java
+
+**路径**:`src/main/java/com/campus/trade/common/StatusCode.java`
+
+**说明**:业务状态码常量。
+
+```java
+package com.campus.trade.common;
+
+public class StatusCode {
+    public static final int SUCCESS = 200;
+    public static final int BAD_REQUEST = 400;
+    public static final int UNAUTHORIZED = 401;
+    public static final int FORBIDDEN = 403;
+    public static final int NOT_FOUND = 404;
+    public static final int CONFLICT = 409;
+    public static final int SERVER_ERROR = 500;
+}
+```
+
+---
+
+### 步骤 1.3 — common/BusinessException.java
+
+**路径**:`src/main/java/com/campus/trade/common/BusinessException.java`
+
+**说明**:自定义运行时异常,携带业务状态码。
+
+```java
+package com.campus.trade.common;
+
+public class BusinessException extends RuntimeException {
+    private int code;
+
+    public BusinessException(int code, String message) {
+        super(message);
+        this.code = code;
+    }
+
+    public int getCode() {
+        return code;
+    }
+}
+```
+
+---
+
+### 步骤 1.4 — common/GlobalExceptionHandler.java
+
+**路径**:`src/main/java/com/campus/trade/common/GlobalExceptionHandler.java`
+
+**说明**:全局异常捕获,统一返回 Result 格式。
+
+```java
+package com.campus.trade.common;
+
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.validation.FieldError;
+
+import java.util.stream.Collectors;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BusinessException.class)
+    public Result handleBusiness(BusinessException e) {
+        return Result.error(e.getCode(), e.getMessage());
+    }
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public Result handleValidation(MethodArgumentNotValidException e) {
+        String msg = e.getBindingResult().getFieldErrors()
+                .stream().map(FieldError::getDefaultMessage)
+                .collect(Collectors.joining(", "));
+        return Result.error(StatusCode.BAD_REQUEST, msg);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public Result handleUnknown(Exception e) {
+        e.printStackTrace();
+        return Result.error(StatusCode.SERVER_ERROR, "服务器内部错误");
+    }
+}
+```
+
+---
+
+### 步骤 1.5 — common/FileUploadUtil.java
+
+**路径**:`src/main/java/com/campus/trade/common/FileUploadUtil.java`
+
+**说明**:上传文件到 `./upload/` 目录,校验图片格式。
+
+```java
+package com.campus.trade.common;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+@Component
+public class FileUploadUtil {
+
+    @Value("${upload.dir:./upload}")
+    private String uploadDir;
+
+    // 上传单个文件,返回访问路径 /upload/{filename}
+    public String upload(MultipartFile file) {
+        if (file.isEmpty()) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "文件不能为空");
+        }
+        // 校验文件扩展名
+        String originalFilename = file.getOriginalFilename();
+        String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
+        List<String> allowed = Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".webp");
+        if (!allowed.contains(ext.toLowerCase())) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "不支持的图片格式");
+        }
+        // UUID 重命名防止覆盖
+        String filename = UUID.randomUUID() + ext;
+        File dest = new File(uploadDir, filename);
+        if (!dest.getParentFile().exists()) {
+            dest.getParentFile().mkdirs();
+        }
+        try {
+            file.transferTo(dest);
+        } catch (IOException e) {
+            throw new BusinessException(StatusCode.SERVER_ERROR, "文件上传失败");
+        }
+        return "/upload/" + filename;
+    }
+
+    // 批量上传
+    public List<String> uploadBatch(List<MultipartFile> files) {
+        if (files == null) return Collections.emptyList();
+        return files.stream().map(this::upload).collect(Collectors.toList());
+    }
+}
+```
+
+---
+
+### 步骤 1.6 — config/WebMvcConfig.java
+
+**路径**:`src/main/java/com/campus/trade/config/WebMvcConfig.java`
+
+**说明**:静态资源映射 + 注册拦截器。
+
+```java
+package com.campus.trade.config;
+
+import com.campus.trade.interceptor.AuthInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private AuthInterceptor authInterceptor;
+
+    // 映射 /upload/** 到本地 ./upload/ 目录
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler("/upload/**")
+                .addResourceLocations("file:./upload/");
+    }
+
+    // 注册拦截器
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authInterceptor)
+                .addPathPatterns("/**");
+    }
+}
+```
+
+---
+
+### 步骤 1.7 — config/MyBatisConfig.java
+
+**路径**:`src/main/java/com/campus/trade/config/MyBatisConfig.java`
+
+**说明**:PageHelper 分页插件配置。
+
+```java
+package com.campus.trade.config;
+
+import com.github.pagehelper.PageInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Properties;
+
+@Configuration
+public class MyBatisConfig {
+
+    @Bean
+    public PageInterceptor pageInterceptor() {
+        PageInterceptor interceptor = new PageInterceptor();
+        Properties props = new Properties();
+        props.setProperty("helperDialect", "mysql");
+        props.setProperty("reasonable", "true");
+        props.setProperty("supportMethodsArguments", "true");
+        interceptor.setProperties(props);
+        return interceptor;
+    }
+}
+```
+
+---
+
+### 步骤 1.8 — interceptor/AuthInterceptor.java
+
+**路径**:`src/main/java/com/campus/trade/interceptor/AuthInterceptor.java`
+
+**说明**:Session 登录拦截器,白名单放行无需登录的路径。
+
+```java
+package com.campus.trade.interceptor;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.common.StatusCode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+public class AuthInterceptor implements HandlerInterceptor {
+
+    // 无需登录的白名单路径
+    private static final List<String> WHITE_LIST = Arrays.asList(
+            "/auth/register",
+            "/auth/login",
+            "/items",
+            "/items/search",
+            "/items/category/",
+            "/upload/",
+            "/v3/api-docs",
+            "/swagger-ui",
+            "/api-docs"
+    );
+
+    @Override
+    public boolean preHandle(HttpServletRequest request,
+                             HttpServletResponse response,
+                             Object handler) throws Exception {
+        String uri = request.getRequestURI();
+
+        // 检查是否在白名单中
+        for (String white : WHITE_LIST) {
+            if (uri.startsWith(white)) {
+                return true;
+            }
+        }
+
+        // 检查 Session 中是否有 userId
+        HttpSession session = request.getSession(false);
+        if (session == null || session.getAttribute("userId") == null) {
+            response.setContentType("application/json;charset=utf-8");
+            ObjectMapper mapper = new ObjectMapper();
+            response.getWriter().write(
+                    mapper.writeValueAsString(Result.error(StatusCode.UNAUTHORIZED, "请先登录"))
+            );
+            return false;
+        }
+        return true;
+    }
+}
+```
+
+---
+
+### 步骤 1.9 — common/OrderStatusMachine.java
+
+**路径**:`src/main/java/com/campus/trade/common/OrderStatusMachine.java`
+
+**说明**:订单状态机,防止非法状态流转。
+
+```java
+package com.campus.trade.common;
+
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+
+@Component
+public class OrderStatusMachine {
+
+    // 定义每个状态允许的下一个状态
+    private static final Map<String, List<String>> ALLOWED_TRANSITIONS = new HashMap<>();
+
+    static {
+        ALLOWED_TRANSITIONS.put("PENDING", Arrays.asList("CONFIRMED", "CANCELLED"));
+        ALLOWED_TRANSITIONS.put("CONFIRMED", Arrays.asList("DELIVERED", "CANCELLED"));
+        ALLOWED_TRANSITIONS.put("DELIVERED", Arrays.asList("COMPLETED", "CANCELLED"));
+        ALLOWED_TRANSITIONS.put("COMPLETED", Collections.emptyList());
+        ALLOWED_TRANSITIONS.put("CANCELLED", Collections.emptyList());
+    }
+
+    // 校验从当前状态到目标状态是否允许
+    public void validateTransition(String currentStatus, String targetStatus) {
+        List<String> allowed = ALLOWED_TRANSITIONS.get(currentStatus);
+        if (allowed == null || !allowed.contains(targetStatus)) {
+            throw new BusinessException(StatusCode.BAD_REQUEST,
+                    "订单状态不允许从 " + currentStatus + " 转移到 " + targetStatus);
+        }
+    }
+}
+```
+
+---
+
+## 阶段二:数据层
+
+### 步骤 2.1 — 执行 DDL
+
+在 MySQL 中执行 6 张表的建表 SQL(DDL 内容详见 `detailDesign.md` 第 1.3 节,或执行 `server/src/main/resources/init.sql`)。
+
+**表清单**:`user`、`category`、`item`、`order`、`message`、`review`
+
+---
+
+### 步骤 2.2 — 步骤 2.7:创建 6 个 Entity
+
+**路径**:`src/main/java/com/campus/trade/entity/`
+
+| 步骤 | 类 | 说明 |
+|------|-----|------|
+| 2.2 | `User.java` | 用户实体,含 id, studentId, name, passwordHash, nickname, avatarUrl, signature, campusCardUrl, authStatus, status, role |
+| 2.3 | `Item.java` | 物品实体,含 id, sellerId, categoryId, title, description, price, images, tradeMethod, status |
+| 2.4 | `Order.java` | 订单实体,含 id, itemId, buyerId, sellerId, tradeMethod, status |
+| 2.5 | `Message.java` | 消息实体,含 id, fromUserId, toUserId, itemId, content, imageUrl, isRead |
+| 2.6 | `Review.java` | 审核记录实体,含 id, adminId, reviewType, targetId, result, reason |
+| 2.7 | `Category.java` | 分类实体,含 id, name, status, sortOrder |
+
+**要求**:
+- 每个字段添加 `// 注释` 说明含义
+- 类添加 `@Schema(description = "...")`
+- 使用 `LocalDateTime` 类型
+- 生成 getter / setter
+
+**示例(User.java)**:
+
+```java
+package com.campus.trade.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@Schema(description = "用户实体")
+public class User {
+    private Long id;                // 用户ID
+    private String studentId;       // 学号
+    private String name;            // 姓名
+    private String passwordHash;    // 密码哈希(MD5)
+    private String nickname;        // 昵称
+    private String avatarUrl;       // 头像 URL
+    private String signature;       // 个性签名
+    private String campusCardUrl;   // 校园卡照片 URL
+    private String authStatus;      // 认证状态:UNAUTHENTICATED/PENDING/AUTHENTICATED
+    private String status;          // 账号状态:NORMAL/BANNED
+    private String role;            // 角色:USER/ADMIN
+    private LocalDateTime createdAt; // 创建时间
+    private LocalDateTime updatedAt; // 更新时间
+
+    // getters & setters ...
+}
+```
+
+---
+
+### 步骤 2.8 — 步骤 2.13:创建 6 个 Mapper 接口 + 6 个 XML
+
+**路径**:
+
+| 步骤 | Mapper 接口 | 对应 XML | 核心方法 |
+|------|-------------|---------|---------|
+| 2.8 | `mapper/UserMapper.java` | `resources/mapper/UserMapper.xml` | selectByStudentId, selectByAuthStatus, updateAuthStatus, updatePassword |
+| 2.9 | `mapper/ItemMapper.java` | `resources/mapper/ItemMapper.xml` | selectByStatus, searchByKeyword, selectByCategoryId, selectBySellerId, updateStatus |
+| 2.10 | `mapper/OrderMapper.java` | `resources/mapper/OrderMapper.xml` | selectByBuyerId, selectBySellerId, updateStatus |
+| 2.11 | `mapper/MessageMapper.java` | `resources/mapper/MessageMapper.xml` | selectConversations, selectHistory, markAsRead |
+| 2.12 | `mapper/ReviewMapper.java` | `resources/mapper/ReviewMapper.xml` | insert, selectByTypeAndTarget |
+| 2.13 | `mapper/CategoryMapper.java` | `resources/mapper/CategoryMapper.xml` | selectAll, selectEnabled, updateStatus |
+
+**要求**:
+- Mapper 接口使用 `@Mapper` 注解或在启动类使用 `@MapperScan`
+- XML 中 SQL 使用 `#{}` 参数绑定(防 SQL 注入)
+- 分页查询配合 PageHelper:`PageHelper.startPage(pageNum, pageSize)` 在 Service 层调用
+
+**示例(UserMapper.java)**:
+
+```java
+package com.campus.trade.mapper;
+
+import com.campus.trade.entity.User;
+import java.util.List;
+
+public interface UserMapper {
+    User selectByStudentId(String studentId);           // 按学号查询用户
+    List<User> selectByAuthStatus(String authStatus);   // 按认证状态查询用户列表
+    int insert(User user);                              // 新增用户
+    int updateAuthStatus(User user);                    // 更新认证状态
+    int updatePassword(User user);                      // 更新密码
+    User selectById(Long id);                           // 按ID查询
+    int updateProfile(User user);                       // 更新个人资料
+}
+```
+
+**示例(UserMapper.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.campus.trade.mapper.UserMapper">
+    <select id="selectByStudentId" resultType="User">
+        SELECT * FROM user WHERE student_id = #{studentId}
+    </select>
+    <select id="selectByAuthStatus" resultType="User">
+        SELECT * FROM user WHERE auth_status = #{authStatus} ORDER BY created_at ASC
+    </select>
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO user (student_id, name, password_hash, nickname, avatar_url, signature,
+                         campus_card_url, auth_status, status, role)
+        VALUES (#{studentId}, #{name}, #{passwordHash}, #{nickname}, #{avatarUrl}, #{signature},
+                #{campusCardUrl}, #{authStatus}, #{status}, #{role})
+    </insert>
+    <update id="updateAuthStatus">
+        UPDATE user SET auth_status = #{authStatus} WHERE id = #{id}
+    </update>
+    <update id="updatePassword">
+        UPDATE user SET password_hash = #{passwordHash} WHERE id = #{id}
+    </update>
+    <select id="selectById" resultType="User">
+        SELECT * FROM user WHERE id = #{id}
+    </select>
+    <update id="updateProfile">
+        UPDATE user SET nickname = #{nickname}, avatar_url = #{avatarUrl}, signature = #{signature}
+        WHERE id = #{id}
+    </update>
+</mapper>
+```
+
+---
+
+## 阶段三:DTO / VO + 业务层
+
+### 步骤 3.1 — 步骤 3.3:创建请求 DTO
+
+**路径**:`src/main/java/com/campus/trade/dto/`
+
+| 步骤 | DTO | 校验注解 | 用途 |
+|------|-----|---------|------|
+| 3.1 | `LoginRequest.java` | `@NotBlank studentId, password` | 登录 |
+| 3.1 | `RegisterRequest.java` | `@NotBlank studentId, name, password` + MultipartFile campusCardImage | 注册 |
+| 3.1 | `ChangePasswordRequest.java` | `@NotBlank oldPassword, newPassword` | 修改密码 |
+| 3.1 | `UserProfileRequest.java` | nickname, signature, MultipartFile avatar | 修改资料 |
+| 3.2 | `ItemPublishRequest.java` | `@NotBlank title, tradeMethod` `@NotNull price` + List images | 发布物品 |
+| 3.2 | `CreateOrderRequest.java` | `@NotNull itemId` | 创建订单 |
+| 3.2 | `SendMessageRequest.java` | `@NotNull toUserId` + content/imageUrl | 发送消息 |
+| 3.3 | `ReviewRequest.java` | `@NotBlank result` + reason | 审核操作 |
+| 3.3 | `CategoryRequest.java` | `@NotBlank name` + sortOrder | 分类管理 |
+| 3.3 | `StatusRequest.java` | `@NotBlank status` | 状态切换 |
+
+**要求**:每个 DTO 添加 `@Schema(description = "...")` 和 `@Schema(description = "...", example = "...")` 字段注解。
+
+---
+
+### 步骤 3.4 — 步骤 3.5:创建响应 VO
+
+**路径**:`src/main/java/com/campus/trade/vo/`
+
+| 步骤 | VO | 说明 |
+|------|-----|------|
+| 3.4 | `UserProfileVO.java` | 用户信息(不含密码) |
+| 3.4 | `ItemVO.java` | 物品详情(含卖家昵称、头像、分类名) |
+| 3.4 | `OrderVO.java` | 订单详情(含物品标题、图片、买卖双方信息) |
+| 3.4 | `MessageVO.java` | 消息信息(含发送方/接收方昵称、头像) |
+| 3.4 | `ConversationVO.java` | 会话列表项(含最后一条消息、未读数) |
+| 3.5 | `PageResult.java` | 泛型分页结果 |
+
+---
+
+### 步骤 3.6 — 步骤 3.10:创建 Service
+
+**路径**:`src/main/java/com/campus/trade/service/` + `service/admin/`
+
+| 步骤 | Service | 核心业务逻辑要点 |
+|------|---------|----------------|
+| 3.7 | **AuthService** | **注册**:学号查重 → 校园卡上传 → MD5 加密密码 → 写入 User(authStatus=PENDING);**登录**:学号查询 → 封禁检查 → MD5 密码比对 → 设置 Session |
+| 3.8 | **UserService** | 查询/修改个人资料(昵称、签名、头像上传) |
+| 3.8 | **ItemService** | 发布(认证检查 → 图片上传 → status=PENDING);列表(ON_SALE 分页倒序);搜索(LIKE 模糊);分类筛选;详情;下架(校验卖家身份);删除 |
+| 3.9 | **OrderService** | 创建(校验物品 ON_SALE → 创建 PENDING 订单 → 物品变 SOLD);接单/拒单(校验卖家 + 状态机);交货(校验卖家 + CONFIRMED → DELIVERED);确认收货(校验买家 + DELIVERED → COMPLETED) |
+| 3.9 | **MessageService** | 发送消息;会话列表(聚合查询含未读数);历史记录(分页 + 标记已读) |
+| 3.10 | **ReviewService** | 实名审核(校验 PENDING → AUTHENTICATED/UNAUTHENTICATED + 记录 Review);物品审核(校验 PENDING → ON_SALE/OFF_SHELF + 记录 Review) |
+| 3.10 | **UserManageService** | 用户列表;封禁/解封;重置密码(重置为默认 MD5 密码) |
+| 3.10 | **CategoryService** | 分类 CRUD;启用/禁用 |
+
+**密码加密核心代码(AuthService)**:
+
+```java
+// 注册时加密密码
+import org.springframework.util.DigestUtils;
+import java.nio.charset.StandardCharsets;
+
+// MD5 加密
+String passwordHash = DigestUtils.md5DigestAsHex(
+    request.getPassword().getBytes(StandardCharsets.UTF_8)
+);
+
+// 登录时比对密码
+boolean matched = DigestUtils.md5DigestAsHex(
+    password.getBytes(StandardCharsets.UTF_8)
+).equals(user.getPasswordHash());
+```
+
+---
+
+## 阶段四:Controller 层
+
+### 步骤 4.1 — 步骤 4.8:创建 8 个 Controller
+
+**路径**:`src/main/java/com/campus/trade/controller/` + `controller/admin/`
+
+| 步骤 | Controller | 类注解 | 方法数 | 关键路径 |
+|------|-----------|--------|--------|---------|
+| 4.1 | `AuthController.java` | `@Tag(name="认证模块")` | 4 | POST `/auth/register`, `/auth/login`, `/auth/logout`, `/auth/change-password` |
+| 4.2 | `UserController.java` | `@Tag(name="用户模块")` | 2 | GET/PUT `/user/profile` |
+| 4.3 | `ItemController.java` | `@Tag(name="物品模块")` | 8 | GET `/items`, `/items/search`, `/items/category/{id}`, `/items/{id}`; POST `/items`; GET `/items/my`; PUT `/items/{id}/offline`; DELETE `/items/{id}` |
+| 4.4 | `OrderController.java` | `@Tag(name="订单模块")` | 7 | POST `/orders`; GET `/orders/bought`, `/orders/sold`; PUT `/orders/{id}/accept/reject/deliver/confirm` |
+| 4.5 | `MessageController.java` | `@Tag(name="消息模块")` | 3 | GET `/messages/conversations`; GET `/messages/{userId}`; POST `/messages` |
+| 4.6 | `admin/ReviewController.java` | `@Tag(name="审核模块")` | 4 | GET `/admin/review/users`, `/admin/review/items`; POST `/admin/review/user/{id}`, `/admin/review/item/{id}` |
+| 4.7 | `admin/UserManageController.java` | `@Tag(name="用户管理")` | 4 | GET `/admin/users`; PUT `/admin/users/{id}/ban`, `/admin/users/{id}/unban`; POST `/admin/users/{id}/reset-password` |
+| 4.8 | `admin/CategoryController.java` | `@Tag(name="分类管理")` | 4 | GET/POST `/admin/categories`; PUT `/admin/categories/{id}`; PUT `/admin/categories/{id}/status` |
+
+**要求**(遵循 AGENTS.md 规范):
+- 每个 Controller 类添加 `@Tag(name = "...", description = "...")`
+- 每个方法添加 `@Operation(summary = "...", description = "...")`
+- 每个方法添加 `@ApiResponse(responseCode = "200", description = "成功")`
+- 参数添加 `@Parameter(description = "...")`
+- Controller 只做参数校验 + 调用 Service,不包含业务逻辑
+- 从 Session 获取当前用户:`HttpSession session` → `session.getAttribute("userId")`
+
+**示例(AuthController.java)**:
+
+```java
+package com.campus.trade.controller;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.LoginRequest;
+import com.campus.trade.dto.RegisterRequest;
+import com.campus.trade.dto.ChangePasswordRequest;
+import com.campus.trade.service.AuthService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpSession;
+import jakarta.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/auth")
+@Tag(name = "认证模块", description = "注册 / 登录 / 登出 / 修改密码")
+public class AuthController {
+
+    @Autowired
+    private AuthService authService;
+
+    @Operation(summary = "用户注册", description = "学号+姓名+密码+校园卡照片,提交后进入待审核状态")
+    @PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Result register(@Valid RegisterRequest req) {
+        return authService.register(req);
+    }
+
+    @Operation(summary = "用户登录", description = "学号+密码登录,创建 Session 会话")
+    @PostMapping("/login")
+    public Result login(@Valid @RequestBody LoginRequest req, HttpSession session) {
+        return authService.login(req, session);
+    }
+
+    @Operation(summary = "用户登出", description = "销毁当前 Session")
+    @PostMapping("/logout")
+    public Result logout(HttpSession session) {
+        session.invalidate();
+        return Result.success();
+    }
+
+    @Operation(summary = "修改密码", description = "需提供旧密码验证")
+    @PostMapping("/change-password")
+    public Result changePassword(@Valid @RequestBody ChangePasswordRequest req, HttpSession session) {
+        return authService.changePassword(req, session);
+    }
+}
+```
+
+---
+
+## 阶段五:验证
+
+### 步骤 5.1 — 启动服务
+
+```bash
+cd server
+mvn spring-boot:run
+```
+
+**验证项**:
+- 控制台无异常启动日志
+- 端口 8080 正常监听
+- Session 超时等配置生效
+
+---
+
+### 步骤 5.2 — 验证 OpenAPI JSON
+
+访问 `http://localhost:8080/api/v3/api-docs`
+
+**验证项**:
+- 返回 JSON 格式的 OpenAPI 3 规范
+- 包含所有 8 个 Controller 的 36 个 API 路径
+- 每个路径包含 `summary`、`parameters`、`requestBody`、`responses` 等完整信息
+
+---
+
+### 步骤 5.3 — 验证 Swagger UI
+
+访问 `http://localhost:8080/api/swagger-ui.html`
+
+**验证项**:
+- Swagger UI 页面正常加载
+- 左侧按 Tag 分组显示所有 API(认证模块、用户模块、物品模块、订单模块、消息模块、审核模块、用户管理、分类管理)
+- 每个 API 可在线测试(Try it out)
+
+---
+
+### 步骤 5.4 — 核心业务流程测试
+
+| 测试用例 | 步骤 | 预期 |
+|---------|------|------|
+| 注册 | POST `/auth/register` 提交注册信息+校园卡照片 | 返回 userId,authStatus=PENDING |
+| 登录 | POST `/auth/login` 用刚注册的学号密码 | 返回用户信息,Session 创建成功 |
+| 发布物品 | POST `/items` 提交物品信息+图片 | 返回 itemId,status=PENDING |
+| 查看列表 | GET `/items?pageNum=1&pageSize=10` | 返回在售物品分页列表 |
+| 搜索物品 | GET `/items/search?keyword=课本` | 返回匹配的物品列表 |
+| 创建订单 | POST `/orders { itemId }` | 返回 orderId,status=PENDING |
+| 接单 | PUT `/orders/{id}/accept` | 订单状态变为 CONFIRMED |
+| 确认收货 | PUT `/orders/{id}/confirm` | 订单状态变为 COMPLETED |
+| 发消息 | POST `/messages { toUserId, content }` | 返回 messageId |
+| 会话列表 | GET `/messages/conversations` | 返回会话列表含未读数 |
+
+---
+
+## 附录:文件产出汇总
+
+| 阶段 | 新建 Java 文件数 | 新建 XML 文件数 | 修改配置文件 |
+|------|-----------------|----------------|-------------|
+| 阶段零 | 1 | 0 | pom.xml, application.yaml |
+| 阶段一 | 8 | 0 | — |
+| 阶段二 | 12 | 6 | init.sql |
+| 阶段三 | 16 | 0 | — |
+| 阶段四 | 8 | 0 | — |
+| **合计** | **45** | **6** | **2** |
+
+### Java 文件清单
+
+```
+com.campus.trade
+├── ServerApplication.java
+├── common/
+│   ├── Result.java
+│   ├── StatusCode.java
+│   ├── BusinessException.java
+│   ├── GlobalExceptionHandler.java
+│   ├── FileUploadUtil.java
+│   └── OrderStatusMachine.java
+├── config/
+│   ├── WebMvcConfig.java
+│   └── MyBatisConfig.java
+├── interceptor/
+│   └── AuthInterceptor.java
+├── entity/
+│   ├── User.java
+│   ├── Item.java
+│   ├── Order.java
+│   ├── Message.java
+│   ├── Review.java
+│   └── Category.java
+├── dto/
+│   ├── LoginRequest.java
+│   ├── RegisterRequest.java
+│   ├── ChangePasswordRequest.java
+│   ├── UserProfileRequest.java
+│   ├── ItemPublishRequest.java
+│   ├── CreateOrderRequest.java
+│   ├── SendMessageRequest.java
+│   ├── ReviewRequest.java
+│   ├── CategoryRequest.java
+│   └── StatusRequest.java
+├── vo/
+│   ├── UserProfileVO.java
+│   ├── ItemVO.java
+│   ├── OrderVO.java
+│   ├── MessageVO.java
+│   ├── ConversationVO.java
+│   └── PageResult.java
+├── mapper/
+│   ├── UserMapper.java
+│   ├── ItemMapper.java
+│   ├── OrderMapper.java
+│   ├── MessageMapper.java
+│   ├── ReviewMapper.java
+│   └── CategoryMapper.java
+├── service/
+│   ├── AuthService.java
+│   ├── UserService.java
+│   ├── ItemService.java
+│   ├── OrderService.java
+│   ├── MessageService.java
+│   └── admin/
+│       ├── ReviewService.java
+│       ├── UserManageService.java
+│       └── CategoryService.java
+└── controller/
+    ├── AuthController.java
+    ├── UserController.java
+    ├── ItemController.java
+    ├── OrderController.java
+    ├── MessageController.java
+    └── admin/
+        ├── ReviewController.java
+        ├── UserManageController.java
+        └── CategoryController.java
+```