work.md 31 KB

服务端开发工作记录


阶段零:项目初始化

步骤 0.1 — 更新 pom.xml

操作:

  1. 在 pom.xml 中新增以下依赖:

    <!-- MyBatis 分页插件 -->
    <dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>2.1.0</version>
    </dependency>
    
    <!-- 参数校验 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 无需添加密码加密依赖 — 使用 Spring 内置 org.springframework.util.DigestUtils(MD5)

验证:mvn dependency:tree 无报错


步骤 0.2 — 创建新包结构 + ServerApplication

操作:

  1. 在 src/main/java/com/campus/trade/ 下创建子包目录:

    com.campus.trade
    ├── config
    ├── common
    ├── entity
    ├── dto
    ├── vo
    ├── mapper
    ├── service
    │   └── admin
    └── controller
    └── admin
    
  2. 新建 src/main/java/com/campus/trade/ServerApplication.java:

    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 更新为:

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 返回此对象。

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

说明:业务状态码常量。

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

说明:自定义运行时异常,携带业务状态码。

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 格式。

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/ 目录,校验图片格式。

package com.campus.trade.common;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.*;

@Component
public class FileUploadUtil {

    @Value("${upload.dir:./upload}")
    private String uploadDir;

    // 上传单个文件,返回访问路径 /upload/{filename}
    public String upload(MultipartFile file) {
        if (file.isEmpty()) {
            throw new BusinessException(StatusCode.BAD_REQUEST, "文件不能为空");
        }
        // 校验文件扩展名
        String originalFilename = file.getOriginalFilename();
        String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
        List<String> allowed = Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".webp");
        if (!allowed.contains(ext.toLowerCase())) {
            throw new BusinessException(StatusCode.BAD_REQUEST, "不支持的图片格式");
        }
        // UUID 重命名防止覆盖
        String filename = UUID.randomUUID() + ext;
        File dest = new File(uploadDir, filename);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        try {
            file.transferTo(dest);
        } catch (IOException e) {
            throw new BusinessException(StatusCode.SERVER_ERROR, "文件上传失败");
        }
        return "/upload/" + filename;
    }

    // 批量上传
    public List<String> uploadBatch(List<MultipartFile> files) {
        if (files == null) return Collections.emptyList();
        return files.stream().map(this::upload).collect(Collectors.toList());
    }
}

步骤 1.6 — config/WebMvcConfig.java

路径:src/main/java/com/campus/trade/config/WebMvcConfig.java

说明:静态资源映射 + 注册拦截器。

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 分页插件配置。

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 登录拦截器,白名单放行无需登录的路径。

package com.campus.trade.interceptor;

import com.campus.trade.common.Result;
import com.campus.trade.common.StatusCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Arrays;
import java.util.List;

@Component
public class AuthInterceptor implements HandlerInterceptor {

    // 无需登录的白名单路径
    private static final List<String> WHITE_LIST = Arrays.asList(
            "/auth/register",
            "/auth/login",
            "/items",
            "/items/search",
            "/items/category/",
            "/upload/",
            "/v3/api-docs",
            "/swagger-ui",
            "/api-docs"
    );

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        String uri = request.getRequestURI();

        // 检查是否在白名单中
        for (String white : WHITE_LIST) {
            if (uri.startsWith(white)) {
                return true;
            }
        }

        // 检查 Session 中是否有 userId
        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute("userId") == null) {
            response.setContentType("application/json;charset=utf-8");
            ObjectMapper mapper = new ObjectMapper();
            response.getWriter().write(
                    mapper.writeValueAsString(Result.error(StatusCode.UNAUTHORIZED, "请先登录"))
            );
            return false;
        }
        return true;
    }
}

步骤 1.9 — common/OrderStatusMachine.java

路径:src/main/java/com/campus/trade/common/OrderStatusMachine.java

说明:订单状态机,防止非法状态流转。

package com.campus.trade.common;

import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class OrderStatusMachine {

    // 定义每个状态允许的下一个状态
    private static final Map<String, List<String>> ALLOWED_TRANSITIONS = new HashMap<>();

    static {
        ALLOWED_TRANSITIONS.put("PENDING", Arrays.asList("CONFIRMED", "CANCELLED"));
        ALLOWED_TRANSITIONS.put("CONFIRMED", Arrays.asList("DELIVERED", "CANCELLED"));
        ALLOWED_TRANSITIONS.put("DELIVERED", Arrays.asList("COMPLETED", "CANCELLED"));
        ALLOWED_TRANSITIONS.put("COMPLETED", Collections.emptyList());
        ALLOWED_TRANSITIONS.put("CANCELLED", Collections.emptyList());
    }

    // 校验从当前状态到目标状态是否允许
    public void validateTransition(String currentStatus, String targetStatus) {
        List<String> allowed = ALLOWED_TRANSITIONS.get(currentStatus);
        if (allowed == null || !allowed.contains(targetStatus)) {
            throw new BusinessException(StatusCode.BAD_REQUEST,
                    "订单状态不允许从 " + currentStatus + " 转移到 " + targetStatus);
        }
    }
}

阶段二:数据层

步骤 2.1 — 执行 DDL

在 MySQL 中执行 6 张表的建表 SQL(DDL 内容详见 detailDesign.md 第 1.3 节,或执行 server/src/main/resources/init.sql)。

表清单:user、category、item、order、message、review


步骤 2.2 — 步骤 2.7:创建 6 个 Entity

路径:src/main/java/com/campus/trade/entity/

步骤 类 说明
2.2 User.java 用户实体,含 id, studentId, name, passwordHash, nickname, avatarUrl, signature, campusCardUrl, authStatus, status, role
2.3 Item.java 物品实体,含 id, sellerId, categoryId, title, description, price, images, tradeMethod, status
2.4 Order.java 订单实体,含 id, itemId, buyerId, sellerId, tradeMethod, status
2.5 Message.java 消息实体,含 id, fromUserId, toUserId, itemId, content, imageUrl, isRead
2.6 Review.java 审核记录实体,含 id, adminId, reviewType, targetId, result, reason
2.7 Category.java 分类实体,含 id, name, status, sortOrder

要求:

  • 每个字段添加 // 注释 说明含义
  • 类添加 @Schema(description = "...")
  • 使用 LocalDateTime 类型
  • 生成 getter / setter

示例(User.java):

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):

package com.campus.trade.mapper;

import com.campus.trade.entity.User;
import java.util.List;

public interface UserMapper {
    User selectByStudentId(String studentId);           // 按学号查询用户
    List<User> selectByAuthStatus(String authStatus);   // 按认证状态查询用户列表
    int insert(User user);                              // 新增用户
    int updateAuthStatus(User user);                    // 更新认证状态
    int updatePassword(User user);                      // 更新密码
    User selectById(Long id);                           // 按ID查询
    int updateProfile(User user);                       // 更新个人资料
}

示例(UserMapper.xml):

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.trade.mapper.UserMapper">
    <select id="selectByStudentId" resultType="User">
        SELECT * FROM user WHERE student_id = #{studentId}
    </select>
    <select id="selectByAuthStatus" resultType="User">
        SELECT * FROM user WHERE auth_status = #{authStatus} ORDER BY created_at ASC
    </select>
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user (student_id, name, password_hash, nickname, avatar_url, signature,
                         campus_card_url, auth_status, status, role)
        VALUES (#{studentId}, #{name}, #{passwordHash}, #{nickname}, #{avatarUrl}, #{signature},
                #{campusCardUrl}, #{authStatus}, #{status}, #{role})
    </insert>
    <update id="updateAuthStatus">
        UPDATE user SET auth_status = #{authStatus} WHERE id = #{id}
    </update>
    <update id="updatePassword">
        UPDATE user SET password_hash = #{passwordHash} WHERE id = #{id}
    </update>
    <select id="selectById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
    <update id="updateProfile">
        UPDATE user SET nickname = #{nickname}, avatar_url = #{avatarUrl}, signature = #{signature}
        WHERE id = #{id}
    </update>
</mapper>

阶段三:DTO / VO + 业务层

步骤 3.1 — 步骤 3.3:创建请求 DTO

路径:src/main/java/com/campus/trade/dto/

步骤 DTO 校验注解 用途
3.1 LoginRequest.java @NotBlank studentId, password 登录
3.1 RegisterRequest.java @NotBlank studentId, name, password + MultipartFile campusCardImage 注册
3.1 ChangePasswordRequest.java @NotBlank oldPassword, newPassword 修改密码
3.1 UserProfileRequest.java nickname, signature, MultipartFile avatar 修改资料
3.2 ItemPublishRequest.java @NotBlank title, tradeMethod @NotNull price + List images 发布物品
3.2 CreateOrderRequest.java @NotNull itemId 创建订单
3.2 SendMessageRequest.java @NotNull toUserId + content/imageUrl 发送消息
3.3 ReviewRequest.java @NotBlank result + reason 审核操作
3.3 CategoryRequest.java @NotBlank name + sortOrder 分类管理
3.3 StatusRequest.java @NotBlank status 状态切换

要求:每个 DTO 添加 @Schema(description = "...") 和 @Schema(description = "...", example = "...") 字段注解。


步骤 3.4 — 步骤 3.5:创建响应 VO

路径:src/main/java/com/campus/trade/vo/

步骤 VO 说明
3.4 UserProfileVO.java 用户信息(不含密码)
3.4 ItemVO.java 物品详情(含卖家昵称、头像、分类名)
3.4 OrderVO.java 订单详情(含物品标题、图片、买卖双方信息)
3.4 MessageVO.java 消息信息(含发送方/接收方昵称、头像)
3.4 ConversationVO.java 会话列表项(含最后一条消息、未读数)
3.5 PageResult.java 泛型分页结果

步骤 3.6 — 步骤 3.10:创建 Service

路径:src/main/java/com/campus/trade/service/ + service/admin/

步骤 Service 核心业务逻辑要点
3.7 AuthService 注册:学号查重 → 校园卡上传 → MD5 加密密码 → 写入 User(authStatus=PENDING);登录:学号查询 → 封禁检查 → MD5 密码比对 → 设置 Session
3.8 UserService 查询/修改个人资料(昵称、签名、头像上传)
3.8 ItemService 发布(认证检查 → 图片上传 → status=PENDING);列表(ON_SALE 分页倒序);搜索(LIKE 模糊);分类筛选;详情;下架(校验卖家身份);删除
3.9 OrderService 创建(校验物品 ON_SALE → 创建 PENDING 订单 → 物品变 SOLD);接单/拒单(校验卖家 + 状态机);交货(校验卖家 + CONFIRMED → DELIVERED);确认收货(校验买家 + DELIVERED → COMPLETED)
3.9 MessageService 发送消息;会话列表(聚合查询含未读数);历史记录(分页 + 标记已读)
3.10 ReviewService 实名审核(校验 PENDING → AUTHENTICATED/UNAUTHENTICATED + 记录 Review);物品审核(校验 PENDING → ON_SALE/OFF_SHELF + 记录 Review)
3.10 UserManageService 用户列表;封禁/解封;重置密码(重置为默认 MD5 密码)
3.10 CategoryService 分类 CRUD;启用/禁用

密码加密核心代码(AuthService):

// 注册时加密密码
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):

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 — 启动服务

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