Parcourir la source

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

yangyi il y a 4 mois
Parent
commit
19f7bdef35
8 fichiers modifiés avec 3530 ajouts et 228 suppressions
  1. 39 0
      AGENTS.md
  2. 298 228
      README.md
  3. 222 0
      doc/blog-upload-path-issue.md
  4. 32 0
      doc/demand.md
  5. BIN
      doc/demand.xlsx
  6. 1956 0
      doc/detailDesign.md
  7. 438 0
      doc/fristDesign.md
  8. 545 0
      doc/work.md

+ 39 - 0
AGENTS.md

@@ -0,0 +1,39 @@
+# AGENTS.md
+
+## Architecture
+
+- **Stack**: Vue 3 `<script setup>` + TypeScript (strict) + Vite 6 + Element Plus + Pinia + Vue Router 4
+- **Entry**: `src/main.ts` — mounts Element Plus, router, and Pinia (with `pinia-plugin-persistedstate`)
+- **API layer**: Orval-generated only. Never write raw axios/fetch calls.
+  - Config: `orval.config.ts` — reads `http://localhost:8090/v3/api-docs`, writes to `src/api/endpoints/` and `src/api/models/`
+  - Generated controllers use factory pattern: `getItems()`, `getOrders()`, etc., each returning `{ methodName: () => Promise<T> }`
+  - Endpoint stubs at `src/api/endpoints/` are Orval-generated — run `npm run api:gen` to regenerate
+  - Custom mutator: `src/util/axios-instance.ts` (session-based, `withCredentials: true`, no Bearer token, 401 redirect via router)
+  - Response types: each endpoint returns a typed Result wrapper (e.g., `ResultPageResultItemVO`, `ResultUserProfileVO`)
+- **Routing**: `src/router/index.ts` — flat list with `requiresAuth` / `requiresAdmin` / `requiresVerified` / `noAuth` meta guards, all lazy-loaded
+  - C端: `/home`, `/item/:id`, `/search`, `/publish`, `/my/items`, `/my/orders`, `/order/:id`, `/messages`, `/messages/:userId`, `/profile`, `/profile/password`
+  - B端: `/admin/review/user`, `/admin/review/item`, `/admin/users`, `/admin/categories`
+- **Stores**: `useUserStore` (auth, non-persisted — session-based) and `useMetaStore` (site config, persisted), both re-exported from `src/store/index.ts`
+- **Auth**: Session-based (`withCredentials: true`) — `login()` in store sets flat state; `logout()` clears state
+- **Path alias**: `@/` → `./src/` (configured in both `vite.config.ts` and `tsconfig.app.json`)
+- **Components**: `src/components/ImageUploader.vue` (el-upload wrapper) and `EmptyState.vue` (empty placeholder)
+
+## Commands
+
+| Command | What it does |
+|---------|-------------|
+| `npm run dev` | Start Vite dev server (proxies `/api` → `http://localhost:8090`, strips `/api` prefix) |
+| `npm run build` | **vue-tsc -b** then `vite build` (typecheck must pass first) |
+| `npm run preview` | Preview production build |
+| `npm run api:gen` | Regenerate Orval API code (requires backend at `localhost:8090/v3/api-docs`) |
+
+## Important
+
+- **No lint / test / format scripts** beyond what Orval runs (`prettier --write` after codegen). No ESLint. No test framework.
+- **TypeScript strict mode** — `noUnusedLocals`, `noUnusedParameters`, `erasableSyntaxOnly` all on.
+- **Responsive design** — every view must work on mobile and desktop. Use Element Plus responsive utilities and CSS media queries. UserLayout has a bottom TabBar visible on mobile (< 768px).
+- **`work.md`** was used as the implementation plan and is now fully implemented (phases 1–7). Keep as historical reference.
+- **API endpoint stubs** at `src/api/endpoints/` are Orval-generated from `http://localhost:8090/v3/api-docs`. Re-run `npm run api:gen` when the backend spec changes.
+- **Build artifacts**: `dist/` is gitignored. The large `index` chunk (Element Plus) can be split via `build.rollupOptions.output.manualChunks` if needed.
+- **Views**: All views are lazy-loaded via `const Xxx = () => import('@/view/Xxx.vue')`. Admin views are under `src/view/admin/`.
+- **`src/type/`** was removed — all type definitions now come from `src/api/models/` (Orval-generated) or are inlined in endpoint stubs.

+ 298 - 228
README.md

@@ -1,237 +1,307 @@
-# Vue 3 + TypeScript + Vite
+# 校园闲置物品交易平台 - 前端
+
+> Campus Idle Item Trading Platform - Frontend
+
+一个专为校园场景设计的二手物品交易平台前端项目,提供完整的C端交易功能和B端管理后台。
+
+## 技术栈
+
+| 类别 | 技术 |
+|------|------|
+| 核心框架 | Vue 3 + TypeScript (严格模式) |
+| 构建工具 | Vite 6 |
+| UI 组件库 | Element Plus 2.13+ |
+| 状态管理 | Pinia 3.0 + pinia-plugin-persistedstate |
+| 路由 | Vue Router 4 |
+| API 生成 | Orval 7.0 (OpenAPI自动生成) |
+| HTTP 客户端 | Axios |
+
+## 功能特性
+
+### C端用户功能
+
+| 模块 | 功能描述 |
+|------|----------|
+| 首页 | 搜索栏、分类Tabs、物品网格展示、分页 |
+| 物品详情 | 图片轮播、物品信息、卖家信息、联系卖家、创建订单 |
+| 搜索 | 关键词搜索、分类筛选、结果列表 |
+| 发布物品 | 标题、描述、价格、分类、交易方式、多图上传 |
+| 我的物品 | Tabs筛选(售卖中/已下架/已售出)、下架/删除操作 |
+| 我的订单 | Tabs筛选(我买到的/我卖出的)、订单状态标签 |
+| 订单详情 | 订单信息、状态时间线、操作按钮(接单/拒单/确认交货/确认收货) |
+| 消息中心 | 会话列表、未读红点、最后消息预览 |
+| 即时聊天 | 消息气泡、文本/图片消息、自动滚动 |
+| 个人资料 | 头像修改、昵称/个性签名编辑 |
+| 修改密码 | 旧密码验证、新密码设置 |
+
+### B端管理后台
+
+| 模块 | 功能描述 |
+|------|----------|
+| 实名审核 | 待审核用户列表、通过/驳回操作、校园卡照片预览 |
+| 物品审核 | 待审核物品列表、上架/驳回操作、物品详情查看 |
+| 用户管理 | 用户列表、搜索、封禁/解封、重置密码 |
+| 分类管理 | 分类列表、新增/编辑、排序、启用/禁用开关 |
+
+## 项目结构
+
+```
+src/
+├── api/
+│   ├── endpoints/          # Orval生成的API端点
+│   │   ├── auth.ts
+│   │   ├── items.ts
+│   │   ├── orders.ts
+│   │   ├── messages.ts
+│   │   ├── reviews.ts
+│   │   ├── users.ts
+│   │   ├── user-management.ts
+│   │   ├── categories.ts
+│   │   └── attachment.ts
+│   └── models/             # Orval生成的TypeScript类型
+├── components/
+│   ├── ImageUploader.vue   # 图片上传组件
+│   └── EmptyState.vue      # 空状态组件
+├── layout/
+│   ├── UserLayout.vue      # C端布局(含移动端TabBar)
+│   └── AdminLayout.vue     # B端管理后台布局
+├── router/
+│   └── index.ts            # 路由配置
+├── store/
+│   ├── user.ts             # 用户状态管理
+│   ├── meta.ts             # 站点配置
+│   └── index.ts            # Store导出
+├── util/
+│   ├── axios-instance.ts   # Axios实例配置
+│   └── index.ts            # 工具函数
+├── view/                   # 页面视图
+│   ├── Home.vue            # 首页
+│   ├── ItemDetail.vue      # 物品详情
+│   ├── Search.vue          # 搜索
+│   ├── Publish.vue         # 发布物品
+│   ├── MyItems.vue         # 我的物品
+│   ├── MyOrders.vue        # 我的订单
+│   ├── OrderDetail.vue     # 订单详情
+│   ├── Messages.vue        # 消息中心
+│   ├── Chat.vue            # 即时聊天
+│   ├── Profile.vue         # 个人资料
+│   ├── ChangePassword.vue  # 修改密码
+│   ├── Login.vue           # 登录
+│   └── admin/              # 管理后台
+│       ├── ReviewUser.vue  # 实名审核
+│       ├── ReviewItem.vue  # 物品审核
+│       ├── UserManage.vue  # 用户管理
+│       └── CategoryManage.vue # 分类管理
+├── App.vue
+├── main.ts
+└── style.css
+```
+
+## 快速开始
+
+### 环境要求
+
+- Node.js 18+
+- 后端服务运行在 `http://localhost:8090`
+
+### 安装依赖
+
+```bash
+npm install
+```
+
+### 开发模式
+
+```bash
+npm run dev
+```
+
+服务将启动在 `http://localhost:3000`,API请求会自动代理到后端 `http://localhost:8090`。
+
+### 构建生产环境
+
+```bash
+npm run build
+```
+
+TypeScript类型检查通过后,构建产物将输出到 `dist/` 目录。
+
+### 预览生产构建
+
+```bash
+npm run preview
+```
+
+## API 代码生成
+
+本项目使用 [Orval](https://orval.dev/) 根据后端 OpenAPI/Swagger 文档自动生成 API 调用代码。
+
+### 重新生成 API
+
+```bash
+npm run api:gen
+```
+
+此命令会:
+1. 从 `http://localhost:8090/v3/api-docs` 读取 OpenAPI 规范
+2. 生成 `src/api/endpoints/` 下的 API 端点
+3. 生成 `src/api/models/` 下的 TypeScript 类型定义
+4. 使用 Prettier 自动格式化生成的代码
+
+### 配置
+
+Orval 配置位于 `orval.config.ts`:
+
+```typescript
+export default defineConfig({
+  campusTrade: {
+    input: {
+      target: 'http://localhost:8090/v3/api-docs',
+    },
+    output: {
+      mode: 'tags',           // 按标签拆分文件
+      client: 'axios',        // 使用 axios
+      target: './src/api/endpoints/',
+      schemas: './src/api/models',
+      clean: true,
+      override: {
+        mutator: {
+          path: './src/util/axios-instance.ts',
+          name: 'customAxiosInstance',
+        },
+      },
+    },
+  },
+})
+```
+
+## 路由结构
+
+### 公开页面
+
+| 路径 | 名称 | 说明 |
+|------|------|------|
+| `/login` | login | 登录页 |
+
+### C端页面(需登录)
+
+| 路径 | 名称 | 权限要求 |
+|------|------|----------|
+| `/home` | home | 公开 |
+| `/item/:id` | itemDetail | 公开 |
+| `/search` | search | 公开 |
+| `/publish` | publish | 需登录 + 需实名认证 |
+| `/my/items` | myItems | 需登录 |
+| `/my/orders` | myOrders | 需登录 |
+| `/order/:id` | orderDetail | 需登录 |
+| `/messages` | messages | 需登录 |
+| `/messages/:userId` | chat | 需登录 |
+| `/profile` | profile | 需登录 |
+| `/profile/password` | changePassword | 需登录 |
 
-This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+### B端管理后台(需管理员权限)
 
-Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
+| 路径 | 名称 | 说明 |
+|------|------|------|
+| `/admin/review/user` | reviewUser | 实名审核 |
+| `/admin/review/item` | reviewItem | 物品审核 |
+| `/admin/users` | userManage | 用户管理 |
+| `/admin/categories` | categoryManage | 分类管理 |
 
-# 前端模板项目
+## 认证机制
 
-> node18
->
-> Vue 3 + TypeScript + Vite6
+项目采用 **Session-based** 认证方式:
 
-## 构建过程
+- 使用 `withCredentials: true` 自动携带 Session Cookie
+- 无 Token 存储逻辑
+- 401 响应自动跳转登录页
+- 用户状态通过 Pinia 管理并持久化到 localStorage
 
-1. 项目初始化
+### 用户状态结构
 
-   ```shell
-   npm create vite@6
-   ```
+```typescript
+{
+  userId: number | null,
+  nickname: string,
+  signature: string,
+  avatarUrl: string,
+  role: string,           // 'ADMIN' | 'USER'
+  authStatus: string,     // 'AUTHENTICATED' | 'PENDING' | 'UNAUTHENTICATED'
+  isLoggedIn: boolean
+}
+```
+
+## 开发规范
+
+### TypeScript 严格模式
+
+- `noUnusedLocals`: true
+- `noUnusedParameters`: true
+- `erasableSyntaxOnly`: true
+
+### 响应式设计
+
+- 使用 Element Plus 响应式工具类
+- 移动端底部 TabBar(< 768px 显示)
+- 网格布局自适应:xs=24 / sm=12 / md=8 / lg=6
+
+### API 使用示例
+
+```typescript
+// 使用 Orval 生成的 API
+import { getItems } from '@/api/endpoints/items'
+
+const { getItemsList } = getItems()
+
+// 调用 API
+const response = await getItemsList({ keyword: '书籍', page: 1, size: 10 })
+// response 类型为 ResultPageResultItemVO
+```
+
+## 代理配置
+
+Vite 开发服务器配置了 API 代理:
+
+```typescript
+// vite.config.ts
+server: {
+  port: 3000,
+  proxy: {
+    '/api': {
+      target: 'http://localhost:8090',
+      changeOrigin: true,
+      rewrite: (path) => path.replace(/^\/api/, '')
+    }
+  }
+}
+```
 
-   
+## 构建与部署
 
-2. 添加vue-router和elementPlus依赖
-
-   - 安装
-
-   ```shell
-   npm install vue-router@4
-   npm install element-plus --save
-   ```
-
-   - 配置
-
-   ```typescript
-   const routes:RouteRecordRaw[] = [
-       {
-           path: '/',
-           name: 'commonLayout',
-           component: CommonLayout,
-           children: [
-               {
-                   path: '/',
-                   name: 'home',
-                   component: HomeView
-               },
-               {
-                   path: '/about',
-                   name: 'about',
-                   component: AboutView
-               },
-           ],
-       },
-   ]
-   
-   const router : Router = createRouter({
-       history: createWebHistory(),
-       routes: routes,
-   });
-   export default router
-   ```
-
-   
-
-   - 使用
-
-   ```typescript
-   app.use(router)
-   app.use(ElementPlus)
-   ```
-
-   
-
-3. 添加状态管理依赖
-
-   - 安装
-
-   ```shell
-   npm install pinia
-   ```
-
-   - 配置
-
-   ```
-   
-   ```
-
-   
-
-   - 使用
-
-   ```typescript
-   import { createPinia } from 'pinia'
-   const pinia = createPinia()
-   app.use(pinia)
-   ```
-
-   
-
-4. 添加axios依赖
-
-   ```shell
-   npm install axios
-   ```
-
-   配置axios-instance.ts
-
-   ```typescript
-   import axios, {type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
-   
-   // 自定义配置接口
-   export interface CustomInstanceConfig extends AxiosRequestConfig {
-       // 可以添加自定义配置
-       skipAuth?: boolean; // 是否跳过鉴权
-   }
-   
-   // 创建 Axios 实例
-   const axiosInstance: AxiosInstance = axios.create({
-       baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
-       timeout: 30000,
-       headers: {
-           'Content-Type': 'application/json',
-       },
-   });
-   
-   // 请求拦截器
-   axiosInstance.interceptors.request.use(
-       (config) => {
-           // 添加 token
-           const token = localStorage.getItem('token');
-           if (token) {
-               config.headers.Authorization = `Bearer ${token}`;
-           }
-           return config;
-       },
-       (error) => {
-           return Promise.reject(error);
-       }
-   );
-   
-   // 响应拦截器
-   axiosInstance.interceptors.response.use(
-       (response: AxiosResponse) => {
-           // 根据后端返回格式调整
-           if (response.data.code !== 0) {
-               return Promise.reject(new Error(response.data.message || '请求失败'));
-           }
-           return response.data;
-       },
-       (error) => {
-           // 统一错误处理
-           if (error.response?.status === 401) {
-               // 未授权,跳转登录
-               window.location.href = '/login';
-           }
-           return Promise.reject(error);
-       }
-   );
-   
-   // 自定义实例函数(供 Orval 使用)
-   export const customAxiosInstance = <T>(config: CustomInstanceConfig): Promise<T> => {
-       return axiosInstance(config);
-   };
-   
-   // 也可以导出第二参数形式(如需处理完整响应)
-   export const customAxiosInstanceWithFullResponse = <T>(
-       config: CustomInstanceConfig
-   ): Promise<AxiosResponse<T>> => {
-       return axiosInstance(config);
-   };
-   ```
-
-   
-
-5. openAPI自动生成接口调用
-
-   使用orval进行API生成
-
-   ```shell
-   npm install orval
-   ```
-
-   编写orval.config.ts配置文件
-
-   ```typescript
-   import { defineConfig } from 'orval';
-   
-   export default defineConfig({
-       'your-api': {
-           // OpenAPI 规范文件路径(支持本地文件或 URL)
-           input: {
-               target: 'http://localhost:8080/v3/api-docs', // 可改为你的 Swagger JSON 地址,如:http://localhost:8000/openapi.json
-           },
-           output: {
-               // 生成代码的模式:tags-按接口标签拆分文件
-               mode: 'tags',
-               // 客户端类型:使用 axios
-               client: 'axios',
-               // 生成 vue-query hooks
-               target: './src/api/client.ts',
-               // 生成的 TypeScript 类型存放目录
-               schemas: './src/api/models',
-               // 每次生成前自动清理旧文件
-               clean: true,
-               // 覆盖默认配置
-               override: {
-                   // 自定义 axios 实例
-                   mutator: {
-                       path: './src/util/axios-instance.ts',
-                       name: 'customAxiosInstance',
-                   },
-                   // 为每个接口生成 vue-query hooks
-                   query: {
-                       useQuery: true,      // 生成 useGetXXX hooks
-                       useMutation: true,   // 生成 usePostXXX hooks
-                   },
-               },
-           },
-           // 生成后自动格式化代码
-           hooks: {
-               afterAllFilesWrite: 'prettier --write',
-           },
-       },
-   });
-   ```
-
-   配置package.json文件中
-
-   ```json
-   {
-     "scripts": {
-      	...,
-       "api:gen": "orval"
-     },
-   }
-   ```
-
-   
+### 生产构建
+
+```bash
+npm run build
+```
+
+构建流程:
+1. `vue-tsc -b` - TypeScript 类型检查
+2. `vite build` - Vite 生产构建
+
+### 构建产物
+
+- 输出目录: `dist/`
+- 静态资源: `dist/assets/`
+- 入口文件: `dist/index.html`
+
+## 相关链接
+
+- [Vue 3 文档](https://vuejs.org/)
+- [Element Plus 文档](https://element-plus.org/)
+- [Orval 文档](https://orval.dev/)
+- [Pinia 文档](https://pinia.vuejs.org/)
+- [Vue Router 文档](https://router.vuejs.org/)
+
+## 许可证
+
+[MIT License](LICENSE)

+ 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端)
+- 实名认证审核(通过/驳回,驳回需填原因)
+- 物品上架审核(通过上架/驳回下架)
+- 用户账号管理(封禁/解封)
+- 用户密码重置(管理员手动重置)
+- 物品分类管理(新增/修改/禁用)

BIN
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
+```

+ 545 - 0
doc/work.md

@@ -0,0 +1,545 @@
+# 客户端开发工作记录
+
+---
+
+## 阶段一:认证方案改造(Session 替换 Token)
+
+### 步骤 1.1 — 修改 axios 实例
+
+**文件**:`src/util/axios-instance.ts`
+
+**操作**:
+1. 移除 request 拦截器中从 `localStorage` 读取 token 并设置 `Authorization` header 的逻辑
+2. 新增 `withCredentials: true`(携带 Session Cookie)
+3. 响应拦截器中的 401 跳转 `/login` 逻辑保留
+4. 实例名保持 `customAxiosInstance`(与 orval.config.ts 的 mutator 配置一致)
+
+```typescript
+// 修改后的核心代码
+const axiosInstance: AxiosInstance = axios.create({
+    baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
+    timeout: 30000,
+    withCredentials: true,   // 携带 Session Cookie
+    headers: {
+        'Content-Type': 'application/json',
+    },
+});
+
+// 请求拦截器 — 移除 token 逻辑,仅保留必要处理
+axiosInstance.interceptors.request.use(
+    (config) => {
+        return config;
+    },
+    (error) => Promise.reject(error)
+);
+
+// 响应拦截器 — 根据后端 Result 格式调整
+axiosInstance.interceptors.response.use(
+    (response: AxiosResponse) => {
+        const { code, message } = response.data;
+        if (code === 200) {
+            return response.data;
+        }
+        return Promise.reject(new Error(message || '请求失败'));
+    },
+    (error) => {
+        if (error.response?.status === 401) {
+            window.location.href = '/login';
+        }
+        return Promise.reject(error);
+    }
+);
+```
+
+---
+
+### 步骤 1.2 — 重构用户 Store
+
+**文件**:`src/store/user.ts`
+
+**操作**:
+1. 重命名 `useLoginUserStore` → `useUserStore`
+2. 状态结构从嵌套改为扁平:
+
+```typescript
+// 新状态结构
+const state = reactive({
+    userId: null as number | null,
+    nickname: '',
+    avatarUrl: '',
+    role: '',
+    authStatus: '',
+    isLoggedIn: false,
+});
+```
+
+3. Actions 重命名:
+   - `loginSuccess(user)` → `login(user)`:登录成功后设置用户信息
+   - `logoutUser()` → `logout()`:清空状态
+   - `updateUser(user)` → `updateProfile(profile)`:更新资料
+
+4. 移除 `localStorage.removeItem('token')` 调用
+5. Getters:`isAuthenticated`(authStatus === 'AUTHENTICATED')、`isAdmin`(role === 'ADMIN')
+
+**更新 import 引用**:`store/index.ts`、`layout/UserLayout.vue`、`layout/AdminLayout.vue`、`router/index.ts`、`Login.vue`
+
+---
+
+### 步骤 1.3 — 更新 store 导出
+
+**文件**:`src/store/index.ts`
+
+```typescript
+export { useMetaStore } from "./meta.ts";
+export { useUserStore } from "./user.ts";   // 改名
+```
+
+---
+
+### 步骤 1.4 — 清理类型文件
+
+**文件**:`src/type/index.ts`
+
+**操作**:移除 `LoginUser` 接口中的 `token?: string` 字段(后续阶段六统一使用 Orval 生成类型,这里仅做最低限度清理)
+
+```typescript
+export interface User {
+    id: string,
+    name: string,
+    avatar?: string
+    role: 'admin' | 'user' | 'guest'
+}
+export interface LoginUser {
+    isLogin: boolean,
+    // token?: string   ← 移除
+    user?: User
+}
+```
+
+---
+
+## 阶段二:布局与路由改造
+
+### 步骤 2.1 — 重写路由表
+
+**文件**:`src/router/index.ts`
+
+**操作**:
+1. 重写路由表,按详细设计定义所有路径:
+
+```typescript
+const routes = [
+    // 公开页面
+    { path: '/login', name: 'login', component: () => import('@/view/Login.vue'), meta: { noAuth: true } },
+    { path: '/', redirect: '/home' },
+    { path: '/home', name: 'home', component: () => import('@/view/Home.vue') },
+    { path: '/item/:id', name: 'itemDetail', component: () => import('@/view/ItemDetail.vue') },
+    { path: '/search', name: 'search', component: () => import('@/view/Search.vue') },
+
+    // C端 — 需登录
+    { path: '/publish', name: 'publish', component: () => import('@/view/Publish.vue'), meta: { requiresAuth: true } },
+    { path: '/my/items', name: 'myItems', component: () => import('@/view/MyItems.vue'), meta: { requiresAuth: true } },
+    { path: '/my/orders', name: 'myOrders', component: () => import('@/view/MyOrders.vue'), meta: { requiresAuth: true } },
+    { path: '/order/:id', name: 'orderDetail', component: () => import('@/view/OrderDetail.vue'), meta: { requiresAuth: true } },
+    { path: '/messages', name: 'messages', component: () => import('@/view/Messages.vue'), meta: { requiresAuth: true } },
+    { path: '/messages/:userId', name: 'chat', component: () => import('@/view/Chat.vue'), meta: { requiresAuth: true } },
+    { path: '/profile', name: 'profile', component: () => import('@/view/Profile.vue'), meta: { requiresAuth: true } },
+    { path: '/profile/password', name: 'changePassword', component: () => import('@/view/ChangePassword.vue'), meta: { requiresAuth: true } },
+
+    // B端 — 需管理员权限
+    { path: '/admin/login', name: 'adminLogin', component: () => import('@/view/admin/Login.vue'), meta: { noAuth: true } },
+    { path: '/admin', redirect: '/admin/review/user' },
+    { path: '/admin/review/user', name: 'reviewUser', component: () => import('@/view/admin/ReviewUser.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+    { path: '/admin/review/item', name: 'reviewItem', component: () => import('@/view/admin/ReviewItem.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+    { path: '/admin/users', name: 'userManage', component: () => import('@/view/admin/UserManage.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+    { path: '/admin/categories', name: 'categoryManage', component: () => import('@/view/admin/CategoryManage.vue'), meta: { requiresAuth: true, requiresAdmin: true } },
+];
+```
+
+2. 路由守卫改用 `useUserStore`:
+
+```typescript
+router.beforeEach((to, _from, next) => {
+    const userStore = useUserStore();
+    if (to.meta.requiresAdmin) {
+        if (!userStore.isLoggedIn) {
+            next({ path: '/login', query: { redirect: to.fullPath } });
+        } else if (!userStore.isAdmin) {
+            next('/');
+        } else {
+            next();
+        }
+    } else if (to.meta.requiresAuth) {
+        if (!userStore.isLoggedIn) {
+            next({ path: '/login', query: { redirect: to.fullPath } });
+        } else {
+            next();
+        }
+    } else {
+        next();
+    }
+});
+```
+
+---
+
+### 步骤 2.2 — 更新用户布局
+
+**文件**:`src/layout/UserLayout.vue`
+
+**操作**:
+1. 所有 `useLoginUserStore` 引用改为 `useUserStore`
+2. 访问路径适配扁平结构:`loginUserStore.loginUser.isLogin` → `userStore.isLoggedIn`
+3. 退出登录改为调用 `userStore.logout()`,移除 `localStorage.removeItem('token')`
+4. 保留 Logo、标题、导航菜单
+5. 新增底部 TabBar(移动端):首页 / 发布 / 消息 / 我的
+
+---
+
+### 步骤 2.3 — 更新管理后台布局
+
+**文件**:`src/layout/AdminLayout.vue`
+
+**操作**:
+1. 适配新的扁平 store 结构
+2. 侧边栏菜单改为:
+   - 实名审核(`/admin/review/user`)
+   - 物品审核(`/admin/review/item`)
+   - 用户管理(`/admin/users`)
+   - 分类管理(`/admin/categories`)
+
+---
+
+### 步骤 2.4 — 视图文件重命名
+
+| 原文件名 | 新文件名 | 操作 |
+|---------|---------|------|
+| `view/LoginView.vue` | `view/Login.vue` | 重命名 + 更新 router import |
+| `view/HomeView.vue` | `view/Home.vue` | 重命名 + 更新 router import |
+| `view/AboutView.vue` | — | 删除(设计文档无此页面) |
+| `view/UserView.vue` | `view/admin/UserManage.vue` | 移动并重命名,后续适配新 API |
+
+---
+
+## 阶段三:核心页面开发
+
+### 步骤 3.1 — 首页 Home.vue
+
+**文件**:`src/view/Home.vue`
+
+**功能**:
+- 搜索栏(el-input,支持回车搜索跳转 `/search`)
+- 分类 Tabs(el-tabs,从 API 加载分类列表)
+- 物品网格(el-row/el-col,响应式:xs=24/sm=12/md=8/lg=6)
+- 物品卡片(el-card):图片 + 标题 + 价格 + 卖家昵称
+- 分页(el-pagination)
+- 使用 Orval 生成的 `getItems()`、`getCategories()` API
+
+---
+
+### 步骤 3.2 — 物品详情 ItemDetail.vue
+
+**文件**:`src/view/ItemDetail.vue`
+
+**功能**:
+- 物品图片轮播(el-carousel)
+- 物品标题、价格、描述、交易方式
+- 卖家信息(头像、昵称)
+- 操作按钮:
+  - "联系卖家" → 跳转聊天
+  - "我想要" → 创建订单(需登录,弹窗确认)
+
+---
+
+### 步骤 3.3 — 搜索 Search.vue
+
+**文件**:`src/view/Search.vue`
+
+**功能**:
+- 搜索框(从 URL query 读取初始 keyword)
+- 分类筛选下拉
+- 搜索结果列表(与首页一致)
+- 分页
+
+---
+
+### 步骤 3.4 — 发布物品 Publish.vue
+
+**文件**:`src/view/Publish.vue`
+
+**功能**:
+- 表单:标题、描述、价格、分类选择、交易方式(el-radio-group)
+- 图片上传组件(使用步骤 5.1 的 ImageUploader)
+- 提交按钮 → 调用 `publishItem()`(multipart/form-data)
+
+---
+
+### 步骤 3.5 — 我的发布 MyItems.vue
+
+**文件**:`src/view/MyItems.vue`
+
+**功能**:
+- Tabs:售卖中 / 已下架 / 已售出
+- 物品列表(每个带图片缩略图)
+- 操作:下架(售卖中)、删除
+
+---
+
+### 步骤 3.6 — 我的订单 MyOrders.vue
+
+**文件**:`src/view/MyOrders.vue`
+
+**功能**:
+- Tabs:我买到的 / 我卖出的
+- 订单卡片列表:物品缩略图 + 标题 + 价格 + 状态标签
+- 订单状态按设计文档的映射:
+  - 待接单 → 黄色标签
+  - 待面交 → 蓝色标签
+  - 已完成 → 绿色标签
+  - 已取消 → 灰色标签
+
+---
+
+### 步骤 3.7 — 订单详情 OrderDetail.vue
+
+**文件**:`src/view/OrderDetail.vue`
+
+**功能**:
+- 订单信息卡片(物品信息、交易方式、创建时间)
+- 状态标签(el-tag,按状态显示不同颜色)
+- 时间线(el-timeline)展示状态变更
+- 操作按钮(按状态和角色条件渲染):
+  - 卖家视角:待接单 → 接单/拒单;待面交 → 确认交货
+  - 买家视角:待收货 → 确认收货
+- "联系对方"按钮
+
+---
+
+### 步骤 3.8 — 消息列表 Messages.vue
+
+**文件**:`src/view/Messages.vue`
+
+**功能**:
+- 会话列表:对方头像 + 昵称 + 最后一条消息 + 时间 + 未读红点
+- 点击进入聊天(`/messages/{userId}`)
+- 使用 Orval 生成的 `getConversations()` API
+
+---
+
+### 步骤 3.9 — 聊天 Chat.vue
+
+**文件**:`src/view/Chat.vue`
+
+**功能**:
+- 消息气泡列表(自己的在右侧,对方的在左侧)
+- 输入区:文本输入框 + 图片上传按钮
+- 发送按钮
+- 自动滚动到底部
+- 使用 Orval 生成的 `getMessageHistory()`、`sendMessage()` API
+- 进入时标记已读
+
+---
+
+### 步骤 3.10 — 个人资料 + 修改密码
+
+**文件**:`src/view/Profile.vue`、`src/view/ChangePassword.vue`
+
+**Profile.vue**:
+- 头像展示 + 修改(点击上传)
+- 昵称编辑
+- 个性签名编辑
+- 保存按钮 → 调用 `updateProfile()`(multipart/form-data)
+
+**ChangePassword.vue**:
+- 旧密码输入
+- 新密码 + 确认新密码
+- 提交 → 调用 `changePassword()`
+
+---
+
+## 阶段四:管理后台页面开发
+
+### 步骤 4.1 — 实名审核 ReviewUser.vue
+
+**文件**:`src/view/admin/ReviewUser.vue`
+
+**功能**:
+- 待审核用户列表(分页表格)
+- 每行显示:学号、姓名、昵称、注册时间
+- 操作:通过 / 驳回
+- 驳回时弹出对话框填写原因
+- 可点击查看校园卡照片(el-image 大图预览)
+
+---
+
+### 步骤 4.2 — 物品审核 ReviewItem.vue
+
+**文件**:`src/view/admin/ReviewItem.vue`
+
+**功能**:
+- 待审核物品列表(分页表格)
+- 每行显示:标题、价格、卖家、发布时间
+- 操作:通过上架 / 驳回下架
+- 可点击查看物品详情和图片
+
+---
+
+### 步骤 4.3 — 分类管理 CategoryManage.vue
+
+**文件**:`src/view/admin/CategoryManage.vue`
+
+**功能**:
+- 分类列表(带排序)
+- 新增分类对话框(名称 + 排序)
+- 编辑分类
+- 启用/禁用开关(el-switch)
+
+---
+
+### 步骤 4.4 — 用户管理适配 UserManage.vue
+
+**文件**:`src/view/admin/UserManage.vue`
+
+**操作**:
+- 将原 `UserView.vue` 迁移至此
+- 适配新的 Orval 生成的 API(使用 `getUserManageController()`)
+- 功能保留:用户列表、搜索、新增/编辑(简化,改为管理员视角)、封禁/解封、重置密码
+
+---
+
+## 阶段五:通用组件开发
+
+### 步骤 5.1 — 图片上传组件
+
+**文件**:`src/components/ImageUploader.vue`
+
+**功能**:
+- 封装 el-upload
+- 支持多图上传
+- 图片预览(点击放大)
+- 图片删除
+- 返回 URL 列表
+- 限制格式和大小
+
+```vue
+<template>
+  <el-upload
+    :action="uploadUrl"
+    :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>
+```
+
+---
+
+### 步骤 5.2 — 空状态组件
+
+**文件**:`src/components/EmptyState.vue`
+
+**功能**:
+- 统一的空状态占位
+- 可配置图标、提示文字、操作按钮
+
+---
+
+## 阶段六:Orval 重新生成 API
+
+### 步骤 6.1 — 启动后端
+
+```bash
+cd server && mvn spring-boot:run
+```
+
+确认 `http://localhost:8080/api/v3/api-docs` 可访问。
+
+---
+
+### 步骤 6.2 — 运行 Orval 生成
+
+```bash
+cd ui && npm run api:gen
+```
+
+生成产出:
+- `src/api/auth-controller.ts`(认证模块)
+- `src/api/user-controller.ts`(用户模块)
+- `src/api/item-controller.ts`(物品模块)
+- `src/api/order-controller.ts`(订单模块)
+- `src/api/message-controller.ts`(消息模块)
+- `src/api/review-controller.ts`(审核模块)
+- `src/api/user-manage-controller.ts`(用户管理)
+- `src/api/category-controller.ts`(分类管理)
+- `src/api/models/`(全部 TypeScript 类型)
+
+---
+
+### 步骤 6.3 — 统一类型系统
+
+**操作**:
+1. 删除 `src/type/index.ts` 中的手动接口
+2. 所有组件改用 `import type { UserVO, ItemVO, OrderVO } from '@/api/models'`
+3. 更新所有 API 调用方式为 Orval 工厂模式
+
+---
+
+## 阶段七:集成与样式适配
+
+### 步骤 7.1 — 全局样式清理
+
+**文件**:`src/style.css`
+
+**操作**:
+1. 移除 Vite 默认样式(暗色主题、`#app` 居中布局、`a` 标签颜色等)
+2. 替换为:
+
+```css
+* { margin: 0; padding: 0; box-sizing: border-box; }
+html, body, #app { height: 100%; }
+```
+
+3. HTML 文件添加 viewport 设置:
+
+```html
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+```
+
+---
+
+### 步骤 7.2 — 统一 loading 和错误处理
+
+确保每个页面:
+- 数据加载时显示 el-skeleton 或 v-loading
+- API 错误时使用 ElMessage 提示(由 axios 拦截器兜底,页面层也可补充)
+- 列表无数据时使用 EmptyState 组件
+
+---
+
+### 步骤 7.3 — 移动端兼容验证
+
+使用 Chrome DevTools 验证:
+- 375px(iPhone SE):底部 TabBar 可点、卡片占满宽度、弹窗适配
+- 768px(iPad):布局正常、导航折叠
+- 1440px(桌面):多列网格、侧边栏正常
+
+---
+
+## 各阶段文件变更汇总
+
+| 阶段 | 新建文件 | 修改文件 | 删除文件 |
+|------|---------|---------|---------|
+| 一 | 0 | `axios-instance.ts`, `store/user.ts`, `store/index.ts`, `type/index.ts` | 0 |
+| 二 | 0 | `router/index.ts`, `UserLayout.vue`, `AdminLayout.vue` | `AboutView.vue` |
+| 三 | `Home.vue`, `ItemDetail.vue`, `Search.vue`, `Publish.vue`, `MyItems.vue`, `MyOrders.vue`, `OrderDetail.vue`, `Messages.vue`, `Chat.vue`, `Profile.vue`, `ChangePassword.vue` | 0 | 0 |
+| 四 | `ReviewUser.vue`, `ReviewItem.vue`, `CategoryManage.vue` | `UserManage.vue`(迁移) | `UserView.vue` |
+| 五 | `ImageUploader.vue`, `EmptyState.vue` | 0 | 0 |
+| 六 | 0 | Orval 自动覆盖生成 | `type/index.ts`(部分) |
+| 七 | 0 | `style.css`, `index.html` | 0 |
+| **合计** | **16** | **11** | **2** |