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