Prechádzať zdrojové kódy

feat:服务端API开发完成

yangyi 4 mesiacov pred
rodič
commit
c3c7bc5c21
64 zmenil súbory, kde vykonal 2786 pridanie a 8 odobranie
  1. 16 3
      pom.xml
  2. 113 0
      sql/init.sql
  3. 2 2
      src/main/java/com/campus/trade/ServerApplication.java
  4. 14 0
      src/main/java/com/campus/trade/common/BusinessException.java
  5. 50 0
      src/main/java/com/campus/trade/common/FileUploadUtil.java
  6. 35 0
      src/main/java/com/campus/trade/common/GlobalExceptionHandler.java
  7. 29 0
      src/main/java/com/campus/trade/common/OrderStatusMachine.java
  8. 40 0
      src/main/java/com/campus/trade/common/Result.java
  9. 11 0
      src/main/java/com/campus/trade/common/StatusCode.java
  10. 24 0
      src/main/java/com/campus/trade/config/MyBatisConfig.java
  11. 35 0
      src/main/java/com/campus/trade/config/WebMvcConfig.java
  12. 54 0
      src/main/java/com/campus/trade/controller/AuthController.java
  13. 81 0
      src/main/java/com/campus/trade/controller/ItemController.java
  14. 47 0
      src/main/java/com/campus/trade/controller/MessageController.java
  15. 73 0
      src/main/java/com/campus/trade/controller/OrderController.java
  16. 34 0
      src/main/java/com/campus/trade/controller/UserController.java
  17. 52 0
      src/main/java/com/campus/trade/controller/admin/CategoryController.java
  18. 58 0
      src/main/java/com/campus/trade/controller/admin/ReviewController.java
  19. 50 0
      src/main/java/com/campus/trade/controller/admin/UserManageController.java
  20. 19 0
      src/main/java/com/campus/trade/dto/CategoryRequest.java
  21. 20 0
      src/main/java/com/campus/trade/dto/ChangePasswordRequest.java
  22. 14 0
      src/main/java/com/campus/trade/dto/CreateOrderRequest.java
  23. 46 0
      src/main/java/com/campus/trade/dto/ItemPublishRequest.java
  24. 20 0
      src/main/java/com/campus/trade/dto/LoginRequest.java
  25. 32 0
      src/main/java/com/campus/trade/dto/RegisterRequest.java
  26. 19 0
      src/main/java/com/campus/trade/dto/ReviewRequest.java
  27. 29 0
      src/main/java/com/campus/trade/dto/SendMessageRequest.java
  28. 14 0
      src/main/java/com/campus/trade/dto/StatusRequest.java
  29. 23 0
      src/main/java/com/campus/trade/dto/UserProfileRequest.java
  30. 24 0
      src/main/java/com/campus/trade/entity/Category.java
  31. 43 0
      src/main/java/com/campus/trade/entity/Item.java
  32. 33 0
      src/main/java/com/campus/trade/entity/Message.java
  33. 33 0
      src/main/java/com/campus/trade/entity/Order.java
  34. 30 0
      src/main/java/com/campus/trade/entity/Review.java
  35. 48 0
      src/main/java/com/campus/trade/entity/User.java
  36. 52 0
      src/main/java/com/campus/trade/interceptor/AdminInterceptor.java
  37. 55 0
      src/main/java/com/campus/trade/interceptor/AuthInterceptor.java
  38. 13 0
      src/main/java/com/campus/trade/mapper/CategoryMapper.java
  39. 15 0
      src/main/java/com/campus/trade/mapper/ItemMapper.java
  40. 12 0
      src/main/java/com/campus/trade/mapper/MessageMapper.java
  41. 12 0
      src/main/java/com/campus/trade/mapper/OrderMapper.java
  42. 9 0
      src/main/java/com/campus/trade/mapper/ReviewMapper.java
  43. 16 0
      src/main/java/com/campus/trade/mapper/UserMapper.java
  44. 104 0
      src/main/java/com/campus/trade/service/AuthService.java
  45. 177 0
      src/main/java/com/campus/trade/service/ItemService.java
  46. 80 0
      src/main/java/com/campus/trade/service/MessageService.java
  47. 170 0
      src/main/java/com/campus/trade/service/OrderService.java
  48. 62 0
      src/main/java/com/campus/trade/service/UserService.java
  49. 50 0
      src/main/java/com/campus/trade/service/admin/CategoryService.java
  50. 147 0
      src/main/java/com/campus/trade/service/admin/ReviewService.java
  51. 57 0
      src/main/java/com/campus/trade/service/admin/UserManageService.java
  52. 37 0
      src/main/java/com/campus/trade/vo/ConversationVO.java
  53. 59 0
      src/main/java/com/campus/trade/vo/ItemVO.java
  54. 53 0
      src/main/java/com/campus/trade/vo/MessageVO.java
  55. 62 0
      src/main/java/com/campus/trade/vo/OrderVO.java
  56. 40 0
      src/main/java/com/campus/trade/vo/PageResult.java
  57. 49 0
      src/main/java/com/campus/trade/vo/UserProfileVO.java
  58. 27 3
      src/main/resources/application.yaml
  59. 23 0
      src/main/resources/mapper/CategoryMapper.xml
  60. 35 0
      src/main/resources/mapper/ItemMapper.xml
  61. 35 0
      src/main/resources/mapper/MessageMapper.xml
  62. 21 0
      src/main/resources/mapper/OrderMapper.xml
  63. 13 0
      src/main/resources/mapper/ReviewMapper.xml
  64. 36 0
      src/main/resources/mapper/UserMapper.xml

+ 16 - 3
pom.xml

@@ -9,10 +9,10 @@
         <relativePath/> <!-- lookup parent from repository -->
     </parent>
     <groupId>space.anyi</groupId>
-    <artifactId>server</artifactId>
+    <artifactId>CampusIdleItemTradingPlatform</artifactId>
     <version>0.0.1-SNAPSHOT</version>
-    <name>server</name>
-    <description>server</description>
+    <name>CampusIdleItemTradingPlatform</name>
+    <description>CampusIdleItemTradingPlatform</description>
     <url/>
     <licenses>
         <license/>
@@ -67,6 +67,19 @@
             <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>
+
+        <!-- 参数校验 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

+ 113 - 0
sql/init.sql

@@ -0,0 +1,113 @@
+-- 校园闲置物品交易平台 DDL
+-- 数据库:campus_idle_item_trading_platform
+CREATE DATABASE IF NOT EXISTS campus_idle_item_trading_platform;
+USE campus_idle_item_trading_platform;
+
+-- 用户表
+CREATE TABLE IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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;
+
+-- 插入默认分类数据
+INSERT INTO `category` (`name`, `status`, `sort_order`) VALUES
+('教材', 'ENABLED', 1),
+('电子产品', 'ENABLED', 2),
+('生活用品', 'ENABLED', 3),
+('体育用品', 'ENABLED', 4),
+('衣物', 'ENABLED', 5),
+('其他', 'ENABLED', 6);
+
+-- 插入默认管理员(密码:admin123,MD5加密)
+INSERT INTO `user` (`student_id`, `name`, `password_hash`, `nickname`, `role`, `auth_status`, `status`)
+VALUES ('admin', '管理员', '0192023a7bbd73250516f069df18b500', '管理员', 'ADMIN', 'AUTHENTICATED', 'NORMAL');

+ 2 - 2
src/main/java/space/anyi/campusIdleItemTradingPlatform/server/ServerApplication.java → src/main/java/com/campus/trade/ServerApplication.java

@@ -1,9 +1,9 @@
-package space.anyi.campusIdleItemTradingPlatform.server;
+package com.campus.trade;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
-@SpringBootApplication
+@SpringBootApplication(scanBasePackages = "com.campus.trade")
 public class ServerApplication {
 
     public static void main(String[] args) {

+ 14 - 0
src/main/java/com/campus/trade/common/BusinessException.java

@@ -0,0 +1,14 @@
+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;
+    }
+}

+ 50 - 0
src/main/java/com/campus/trade/common/FileUploadUtil.java

@@ -0,0 +1,50 @@
+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.*;
+import java.util.stream.Collectors;
+
+@Component
+public class FileUploadUtil {
+
+    @Value("${upload.dir:./upload}")
+    private String uploadDir;
+
+    public String upload(MultipartFile file) {
+        if (file.isEmpty()) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "文件不能为空");
+        }
+        // 校验文件扩展名
+        String originalFilename = file.getOriginalFilename();
+        if (originalFilename == null || !originalFilename.contains(".")) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "无效的文件名");
+        }
+        String ext = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
+        List<String> allowed = Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".webp");
+        if (!allowed.contains(ext)) {
+            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());
+    }
+}

+ 35 - 0
src/main/java/com/campus/trade/common/GlobalExceptionHandler.java

@@ -0,0 +1,35 @@
+package com.campus.trade.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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 {
+
+    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+    @ExceptionHandler(BusinessException.class)
+    public Result<Void> handleBusiness(BusinessException e) {
+        return Result.error(e.getCode(), e.getMessage());
+    }
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public Result<Void> 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<Void> handleUnknown(Exception e) {
+        log.error("服务器内部错误", e);
+        return Result.error(StatusCode.SERVER_ERROR, "服务器内部错误");
+    }
+}

+ 29 - 0
src/main/java/com/campus/trade/common/OrderStatusMachine.java

@@ -0,0 +1,29 @@
+package com.campus.trade.common;
+
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class OrderStatusMachine {
+
+    private static final Map<String, List<String>> ALLOWED_TRANSITIONS = Map.of(
+            "PENDING", List.of("CONFIRMED", "CANCELLED"),
+            "CONFIRMED", List.of("DELIVERED", "CANCELLED"),
+            "DELIVERED", List.of("COMPLETED", "CANCELLED"),
+            "COMPLETED", List.of(),
+            "CANCELLED", List.of()
+    );
+
+    public void validateTransition(String currentStatus, String targetStatus) {
+        if (currentStatus == null || targetStatus == null) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "订单状态参数不能为空");
+        }
+        List<String> allowed = ALLOWED_TRANSITIONS.get(currentStatus);
+        if (allowed == null || !allowed.contains(targetStatus)) {
+            throw new BusinessException(StatusCode.BAD_REQUEST,
+                    "订单状态不允许从 " + currentStatus + " 转移到 " + targetStatus);
+        }
+    }
+}

+ 40 - 0
src/main/java/com/campus/trade/common/Result.java

@@ -0,0 +1,40 @@
+package com.campus.trade.common;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "统一响应对象")
+public class Result<T> {
+    @Schema(description = "状态码")
+    private int code;
+    @Schema(description = "消息")
+    private String message;
+    @Schema(description = "响应数据")
+    private T data;
+
+    public Result() {}
+
+    public Result(int code, String message, T data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public static <T> Result<T> success() {
+        return new Result<>(200, "success", null);
+    }
+
+    public static <T> Result<T> success(T data) {
+        return new Result<>(200, "success", data);
+    }
+
+    public static <T> Result<T> error(int code, String message) {
+        return new Result<>(code, message, null);
+    }
+
+    public int getCode() { return code; }
+    public void setCode(int code) { this.code = code; }
+    public String getMessage() { return message; }
+    public void setMessage(String message) { this.message = message; }
+    public T getData() { return data; }
+    public void setData(T data) { this.data = data; }
+}

+ 11 - 0
src/main/java/com/campus/trade/common/StatusCode.java

@@ -0,0 +1,11 @@
+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;
+}

+ 24 - 0
src/main/java/com/campus/trade/config/MyBatisConfig.java

@@ -0,0 +1,24 @@
+package com.campus.trade.config;
+
+import com.github.pagehelper.PageInterceptor;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Properties;
+
+@Configuration
+@MapperScan("com.campus.trade.mapper")
+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;
+    }
+}

+ 35 - 0
src/main/java/com/campus/trade/config/WebMvcConfig.java

@@ -0,0 +1,35 @@
+package com.campus.trade.config;
+
+import com.campus.trade.interceptor.AdminInterceptor;
+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;
+
+    @Autowired
+    private AdminInterceptor adminInterceptor;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler("/upload/**")
+                .addResourceLocations("file:./upload/");
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authInterceptor)
+                .addPathPatterns("/**")
+                .order(0);
+        registry.addInterceptor(adminInterceptor)
+                .addPathPatterns("/admin/**")
+                .order(1);
+    }
+}

+ 54 - 0
src/main/java/com/campus/trade/controller/AuthController.java

@@ -0,0 +1,54 @@
+package com.campus.trade.controller;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.ChangePasswordRequest;
+import com.campus.trade.dto.LoginRequest;
+import com.campus.trade.dto.RegisterRequest;
+import com.campus.trade.service.AuthService;
+import com.campus.trade.vo.UserProfileVO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+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 = "Auth", description = "注册 / 登录 / 登出 / 修改密码")
+public class AuthController {
+
+    @Autowired
+    private AuthService authService;
+
+    @Operation(summary = "用户注册", description = "学号+姓名+密码+校园卡照片,提交后进入待审核状态")
+    @ApiResponse(responseCode = "200", description = "注册成功")
+    @ApiResponse(responseCode = "409", description = "学号已注册")
+    @PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Result<Long> register(@Valid RegisterRequest req) {
+        Long userId = authService.register(req);
+        return Result.success(userId);
+    }
+
+    @Operation(summary = "用户登录", description = "学号+密码登录,创建 Session 会话")
+    @PostMapping("/login")
+    public Result<UserProfileVO> login(@Valid @RequestBody LoginRequest req, HttpSession session) {
+        return Result.success(authService.login(req, session));
+    }
+
+    @Operation(summary = "用户登出", description = "销毁当前 Session")
+    @PostMapping("/logout")
+    public Result<Void> logout(HttpSession session) {
+        session.invalidate();
+        return Result.success();
+    }
+
+    @Operation(summary = "修改密码", description = "需提供旧密码验证")
+    @PostMapping("/change-password")
+    public Result<Void> changePassword(@Valid @RequestBody ChangePasswordRequest req, HttpSession session) {
+        authService.changePassword(req, session);
+        return Result.success();
+    }
+}

+ 81 - 0
src/main/java/com/campus/trade/controller/ItemController.java

@@ -0,0 +1,81 @@
+package com.campus.trade.controller;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.ItemPublishRequest;
+import com.campus.trade.service.ItemService;
+import com.campus.trade.vo.ItemVO;
+import com.campus.trade.vo.PageResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+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.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/items")
+@Tag(name = "Items", description = "物品发布、浏览、搜索、管理")
+public class ItemController {
+
+    @Autowired
+    private ItemService itemService;
+
+    @Operation(summary = "首页物品列表", description = "按发布时间倒序,分页返回")
+    @GetMapping
+    public Result<PageResult<ItemVO>> list(@RequestParam(defaultValue = "1") int pageNum,
+                                            @RequestParam(defaultValue = "10") int pageSize) {
+        return Result.success(PageResult.of(itemService.list(pageNum, pageSize)));
+    }
+
+    @Operation(summary = "搜索物品", description = "按标题/描述模糊匹配")
+    @GetMapping("/search")
+    public Result<PageResult<ItemVO>> search(@RequestParam String keyword,
+                                              @RequestParam(defaultValue = "1") int pageNum,
+                                              @RequestParam(defaultValue = "10") int pageSize) {
+        return Result.success(PageResult.of(itemService.search(keyword, pageNum, pageSize)));
+    }
+
+    @Operation(summary = "按分类筛选")
+    @GetMapping("/category/{id}")
+    public Result<PageResult<ItemVO>> byCategory(@Parameter(description = "分类ID") @PathVariable Long id,
+                                                  @RequestParam(defaultValue = "1") int pageNum,
+                                                  @RequestParam(defaultValue = "10") int pageSize) {
+        return Result.success(PageResult.of(itemService.byCategory(id, pageNum, pageSize)));
+    }
+
+    @Operation(summary = "物品详情")
+    @GetMapping("/{id}")
+    public Result<ItemVO> detail(@Parameter(description = "物品ID") @PathVariable Long id) {
+        return Result.success(itemService.detail(id));
+    }
+
+    @Operation(summary = "发布物品", description = "需要实名认证,图片以 multipart 上传")
+    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Result<Long> publish(@Valid ItemPublishRequest req, HttpSession session) {
+        return Result.success(itemService.publish(req, session));
+    }
+
+    @Operation(summary = "我的发布列表")
+    @GetMapping("/my")
+    public Result<List<ItemVO>> myItems(HttpSession session) {
+        return Result.success(itemService.myItems(session));
+    }
+
+    @Operation(summary = "下架物品", description = "卖家下架自己的物品")
+    @PutMapping("/{id}/offline")
+    public Result<Void> offline(@Parameter(description = "物品ID") @PathVariable Long id, HttpSession session) {
+        itemService.offline(id, session);
+        return Result.success();
+    }
+
+    @Operation(summary = "删除物品", description = "卖家删除自己的物品")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@Parameter(description = "物品ID") @PathVariable Long id, HttpSession session) {
+        itemService.delete(id, session);
+        return Result.success();
+    }
+}

+ 47 - 0
src/main/java/com/campus/trade/controller/MessageController.java

@@ -0,0 +1,47 @@
+package com.campus.trade.controller;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.SendMessageRequest;
+import com.campus.trade.service.MessageService;
+import com.campus.trade.vo.ConversationVO;
+import com.campus.trade.vo.MessageVO;
+import com.campus.trade.vo.PageResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+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.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/messages")
+@Tag(name = "Messages", description = "站内私信,文本+图片")
+public class MessageController {
+
+    @Autowired
+    private MessageService messageService;
+
+    @Operation(summary = "会话列表", description = "返回当前用户的所有会话及最后一条消息")
+    @GetMapping("/conversations")
+    public Result<List<ConversationVO>> conversations(HttpSession session) {
+        return Result.success(messageService.conversations(session));
+    }
+
+    @Operation(summary = "聊天记录", description = "与指定用户的聊天记录,分页")
+    @GetMapping("/{userId}")
+    public Result<PageResult<MessageVO>> history(@Parameter(description = "对方用户ID") @PathVariable Long userId,
+                                                  @RequestParam(defaultValue = "1") int pageNum,
+                                                  @RequestParam(defaultValue = "20") int pageSize,
+                                                  HttpSession session) {
+        return Result.success(PageResult.of(messageService.history(userId, pageNum, pageSize, session)));
+    }
+
+    @Operation(summary = "发送消息")
+    @PostMapping
+    public Result<Long> send(@Valid @RequestBody SendMessageRequest req, HttpSession session) {
+        return Result.success(messageService.send(req, session));
+    }
+}

+ 73 - 0
src/main/java/com/campus/trade/controller/OrderController.java

@@ -0,0 +1,73 @@
+package com.campus.trade.controller;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.CreateOrderRequest;
+import com.campus.trade.service.OrderService;
+import com.campus.trade.vo.OrderVO;
+import com.campus.trade.vo.PageResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+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.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/orders")
+@Tag(name = "Orders", description = "订单创建、状态流转、双视角查询")
+public class OrderController {
+
+    @Autowired
+    private OrderService orderService;
+
+    @Operation(summary = "生成订单", description = "买家点击『我想要』生成订单")
+    @PostMapping
+    public Result<Long> create(@Valid @RequestBody CreateOrderRequest req, HttpSession session) {
+        return Result.success(orderService.create(req.getItemId(), session));
+    }
+
+    @Operation(summary = "我买到的订单")
+    @GetMapping("/bought")
+    public Result<PageResult<OrderVO>> bought(@RequestParam(defaultValue = "1") int pageNum,
+                                               @RequestParam(defaultValue = "10") int pageSize,
+                                               HttpSession session) {
+        return Result.success(PageResult.of(orderService.bought(pageNum, pageSize, session)));
+    }
+
+    @Operation(summary = "我卖出的订单")
+    @GetMapping("/sold")
+    public Result<PageResult<OrderVO>> sold(@RequestParam(defaultValue = "1") int pageNum,
+                                             @RequestParam(defaultValue = "10") int pageSize,
+                                             HttpSession session) {
+        return Result.success(PageResult.of(orderService.sold(pageNum, pageSize, session)));
+    }
+
+    @Operation(summary = "卖家接单")
+    @PutMapping("/{id}/accept")
+    public Result<Void> accept(@Parameter(description = "订单ID") @PathVariable Long id, HttpSession session) {
+        orderService.accept(id, session);
+        return Result.success();
+    }
+
+    @Operation(summary = "卖家拒单")
+    @PutMapping("/{id}/reject")
+    public Result<Void> reject(@Parameter(description = "订单ID") @PathVariable Long id, HttpSession session) {
+        orderService.reject(id, session);
+        return Result.success();
+    }
+
+    @Operation(summary = "卖家确认交货")
+    @PutMapping("/{id}/deliver")
+    public Result<Void> deliver(@Parameter(description = "订单ID") @PathVariable Long id, HttpSession session) {
+        orderService.deliver(id, session);
+        return Result.success();
+    }
+
+    @Operation(summary = "买家确认收货")
+    @PutMapping("/{id}/confirm")
+    public Result<Void> confirm(@Parameter(description = "订单ID") @PathVariable Long id, HttpSession session) {
+        orderService.confirm(id, session);
+        return Result.success();
+    }
+}

+ 34 - 0
src/main/java/com/campus/trade/controller/UserController.java

@@ -0,0 +1,34 @@
+package com.campus.trade.controller;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.UserProfileRequest;
+import com.campus.trade.service.UserService;
+import com.campus.trade.vo.UserProfileVO;
+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("/user")
+@Tag(name = "Users", description = "个人资料管理")
+public class UserController {
+
+    @Autowired
+    private UserService userService;
+
+    @Operation(summary = "获取个人资料")
+    @GetMapping("/profile")
+    public Result<UserProfileVO> profile(HttpSession session) {
+        return Result.success(userService.getProfile(session));
+    }
+
+    @Operation(summary = "修改个人资料", description = "支持修改昵称、个性签名、头像")
+    @PutMapping(value = "/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Result<UserProfileVO> updateProfile(@Valid UserProfileRequest req, HttpSession session) {
+        return Result.success(userService.updateProfile(req, session));
+    }
+}

+ 52 - 0
src/main/java/com/campus/trade/controller/admin/CategoryController.java

@@ -0,0 +1,52 @@
+package com.campus.trade.controller.admin;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.CategoryRequest;
+import com.campus.trade.dto.StatusRequest;
+import com.campus.trade.entity.Category;
+import com.campus.trade.service.admin.CategoryService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/admin/categories")
+@Tag(name = "Categories", description = "物品分类的增删改查")
+public class CategoryController {
+
+    @Autowired
+    private CategoryService categoryService;
+
+    @Operation(summary = "分类列表")
+    @GetMapping
+    public Result<List<Category>> list() {
+        return Result.success(categoryService.list());
+    }
+
+    @Operation(summary = "新增分类")
+    @PostMapping
+    public Result<Long> create(@Valid @RequestBody CategoryRequest req) {
+        return Result.success(categoryService.create(req));
+    }
+
+    @Operation(summary = "修改分类")
+    @PutMapping("/{id}")
+    public Result<Void> update(@Parameter(description = "分类ID") @PathVariable Long id,
+                                @Valid @RequestBody CategoryRequest req) {
+        categoryService.update(id, req);
+        return Result.success();
+    }
+
+    @Operation(summary = "启用/禁用分类")
+    @PutMapping("/{id}/status")
+    public Result<Void> toggleStatus(@Parameter(description = "分类ID") @PathVariable Long id,
+                                      @RequestBody StatusRequest req) {
+        categoryService.toggleStatus(id, req.getStatus());
+        return Result.success();
+    }
+}

+ 58 - 0
src/main/java/com/campus/trade/controller/admin/ReviewController.java

@@ -0,0 +1,58 @@
+package com.campus.trade.controller.admin;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.dto.ReviewRequest;
+import com.campus.trade.entity.User;
+import com.campus.trade.service.admin.ReviewService;
+import com.campus.trade.vo.ItemVO;
+import com.campus.trade.vo.PageResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+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.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/admin")
+@Tag(name = "Reviews", description = "管理员实名审核、物品审核")
+public class ReviewController {
+
+    @Autowired
+    private ReviewService reviewService;
+
+    @Operation(summary = "待审核用户列表")
+    @GetMapping("/review/users")
+    public Result<PageResult<User>> pendingUsers(@RequestParam(defaultValue = "1") int pageNum,
+                                                  @RequestParam(defaultValue = "10") int pageSize,
+                                                  HttpSession session) {
+        return Result.success(PageResult.of(reviewService.pendingUsers(pageNum, pageSize)));
+    }
+
+    @Operation(summary = "审核用户实名认证")
+    @PostMapping("/review/user/{id}")
+    public Result<Void> reviewUser(@Parameter(description = "用户ID") @PathVariable Long id,
+                                    @Valid @RequestBody ReviewRequest req,
+                                    HttpSession session) {
+        reviewService.reviewUser(id, req, session);
+        return Result.success();
+    }
+
+    @Operation(summary = "待审核物品列表")
+    @GetMapping("/review/items")
+    public Result<PageResult<ItemVO>> pendingItems(@RequestParam(defaultValue = "1") int pageNum,
+                                                    @RequestParam(defaultValue = "10") int pageSize,
+                                                    HttpSession session) {
+        return Result.success(PageResult.of(reviewService.pendingItems(pageNum, pageSize)));
+    }
+
+    @Operation(summary = "审核物品上架")
+    @PostMapping("/review/item/{id}")
+    public Result<Void> reviewItem(@Parameter(description = "物品ID") @PathVariable Long id,
+                                    @Valid @RequestBody ReviewRequest req,
+                                    HttpSession session) {
+        reviewService.reviewItem(id, req, session);
+        return Result.success();
+    }
+}

+ 50 - 0
src/main/java/com/campus/trade/controller/admin/UserManageController.java

@@ -0,0 +1,50 @@
+package com.campus.trade.controller.admin;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.entity.User;
+import com.campus.trade.service.admin.UserManageService;
+import com.campus.trade.vo.PageResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/admin")
+@Tag(name = "UserManagement", description = "管理员管理用户账号")
+public class UserManageController {
+
+    @Autowired
+    private UserManageService userManageService;
+
+    @Operation(summary = "用户列表")
+    @GetMapping("/users")
+    public Result<PageResult<User>> list(@RequestParam(defaultValue = "1") int pageNum,
+                                          @RequestParam(defaultValue = "10") int pageSize,
+                                          HttpSession session) {
+        return Result.success(PageResult.of(userManageService.list(pageNum, pageSize)));
+    }
+
+    @Operation(summary = "封禁用户")
+    @PutMapping("/users/{id}/ban")
+    public Result<Void> ban(@Parameter(description = "用户ID") @PathVariable Long id, HttpSession session) {
+        userManageService.ban(id, session);
+        return Result.success();
+    }
+
+    @Operation(summary = "解封用户")
+    @PutMapping("/users/{id}/unban")
+    public Result<Void> unban(@Parameter(description = "用户ID") @PathVariable Long id, HttpSession session) {
+        userManageService.unban(id, session);
+        return Result.success();
+    }
+
+    @Operation(summary = "重置用户密码", description = "重置为默认密码 123456")
+    @PostMapping("/users/{id}/reset-password")
+    public Result<Void> resetPassword(@Parameter(description = "用户ID") @PathVariable Long id, HttpSession session) {
+        userManageService.resetPassword(id, session);
+        return Result.success();
+    }
+}

+ 19 - 0
src/main/java/com/campus/trade/dto/CategoryRequest.java

@@ -0,0 +1,19 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+
+@Schema(description = "分类请求")
+public class CategoryRequest {
+    @Schema(description = "分类名称", example = "教材", required = true)
+    @NotBlank(message = "分类名称不能为空")
+    private String name;
+
+    @Schema(description = "排序", example = "1")
+    private Integer sortOrder;
+
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public Integer getSortOrder() { return sortOrder; }
+    public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
+}

+ 20 - 0
src/main/java/com/campus/trade/dto/ChangePasswordRequest.java

@@ -0,0 +1,20 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+
+@Schema(description = "修改密码请求")
+public class ChangePasswordRequest {
+    @Schema(description = "旧密码", required = true)
+    @NotBlank(message = "旧密码不能为空")
+    private String oldPassword;
+
+    @Schema(description = "新密码", required = true)
+    @NotBlank(message = "新密码不能为空")
+    private String newPassword;
+
+    public String getOldPassword() { return oldPassword; }
+    public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; }
+    public String getNewPassword() { return newPassword; }
+    public void setNewPassword(String newPassword) { this.newPassword = newPassword; }
+}

+ 14 - 0
src/main/java/com/campus/trade/dto/CreateOrderRequest.java

@@ -0,0 +1,14 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+
+@Schema(description = "创建订单请求")
+public class CreateOrderRequest {
+    @Schema(description = "物品ID", required = true, example = "1")
+    @NotNull(message = "物品ID不能为空")
+    private Long itemId;
+
+    public Long getItemId() { return itemId; }
+    public void setItemId(Long itemId) { this.itemId = itemId; }
+}

+ 46 - 0
src/main/java/com/campus/trade/dto/ItemPublishRequest.java

@@ -0,0 +1,46 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Schema(description = "发布物品请求(multipart/form-data)")
+public class ItemPublishRequest {
+    @Schema(description = "标题", example = "全新高数课本", required = true)
+    @NotBlank(message = "标题不能为空")
+    private String title;
+
+    @Schema(description = "描述", example = "九成新,无涂写")
+    private String description;
+
+    @Schema(description = "价格", example = "25.00", required = true)
+    @NotNull(message = "价格不能为空")
+    private BigDecimal price;
+
+    @Schema(description = "分类ID", example = "1")
+    private Long categoryId;
+
+    @Schema(description = "交易方式", allowableValues = {"PICKUP", "DELIVERY"}, example = "PICKUP", required = true)
+    @NotBlank(message = "交易方式不能为空")
+    private String tradeMethod;
+
+    @Schema(description = "物品图片列表")
+    private List<MultipartFile> images;
+
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
+    public BigDecimal getPrice() { return price; }
+    public void setPrice(BigDecimal price) { this.price = price; }
+    public Long getCategoryId() { return categoryId; }
+    public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
+    public String getTradeMethod() { return tradeMethod; }
+    public void setTradeMethod(String tradeMethod) { this.tradeMethod = tradeMethod; }
+    public List<MultipartFile> getImages() { return images; }
+    public void setImages(List<MultipartFile> images) { this.images = images; }
+}

+ 20 - 0
src/main/java/com/campus/trade/dto/LoginRequest.java

@@ -0,0 +1,20 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+
+@Schema(description = "登录请求")
+public class LoginRequest {
+    @Schema(description = "学号", example = "2021001001", required = true)
+    @NotBlank(message = "学号不能为空")
+    private String studentId;
+
+    @Schema(description = "密码", example = "123456", required = true)
+    @NotBlank(message = "密码不能为空")
+    private String password;
+
+    public String getStudentId() { return studentId; }
+    public void setStudentId(String studentId) { this.studentId = studentId; }
+    public String getPassword() { return password; }
+    public void setPassword(String password) { this.password = password; }
+}

+ 32 - 0
src/main/java/com/campus/trade/dto/RegisterRequest.java

@@ -0,0 +1,32 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import org.springframework.web.multipart.MultipartFile;
+
+@Schema(description = "注册请求(multipart/form-data)")
+public class RegisterRequest {
+    @Schema(description = "学号", example = "2021001001", required = true)
+    @NotBlank(message = "学号不能为空")
+    private String studentId;
+
+    @Schema(description = "姓名", example = "张三", required = true)
+    @NotBlank(message = "姓名不能为空")
+    private String name;
+
+    @Schema(description = "密码", example = "123456", required = true)
+    @NotBlank(message = "密码不能为空")
+    private String password;
+
+    @Schema(description = "校园卡照片文件")
+    private MultipartFile campusCardImage;
+
+    public String getStudentId() { return studentId; }
+    public void setStudentId(String studentId) { this.studentId = studentId; }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public String getPassword() { return password; }
+    public void setPassword(String password) { this.password = password; }
+    public MultipartFile getCampusCardImage() { return campusCardImage; }
+    public void setCampusCardImage(MultipartFile campusCardImage) { this.campusCardImage = campusCardImage; }
+}

+ 19 - 0
src/main/java/com/campus/trade/dto/ReviewRequest.java

@@ -0,0 +1,19 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+
+@Schema(description = "审核请求")
+public class ReviewRequest {
+    @Schema(description = "审核结果", allowableValues = {"APPROVED", "REJECTED"}, required = true)
+    @NotBlank(message = "审核结果不能为空")
+    private String result;
+
+    @Schema(description = "驳回原因(驳回时必填)", example = "照片不清晰")
+    private String reason;
+
+    public String getResult() { return result; }
+    public void setResult(String result) { this.result = result; }
+    public String getReason() { return reason; }
+    public void setReason(String reason) { this.reason = reason; }
+}

+ 29 - 0
src/main/java/com/campus/trade/dto/SendMessageRequest.java

@@ -0,0 +1,29 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+
+@Schema(description = "发送消息请求")
+public class SendMessageRequest {
+    @Schema(description = "接收方用户ID", required = true, example = "2")
+    @NotNull(message = "接收方不能为空")
+    private Long toUserId;
+
+    @Schema(description = "关联物品ID", example = "1")
+    private Long itemId;
+
+    @Schema(description = "文本内容")
+    private String content;
+
+    @Schema(description = "图片URL")
+    private String imageUrl;
+
+    public Long getToUserId() { return toUserId; }
+    public void setToUserId(Long toUserId) { this.toUserId = toUserId; }
+    public Long getItemId() { return itemId; }
+    public void setItemId(Long itemId) { this.itemId = itemId; }
+    public String getContent() { return content; }
+    public void setContent(String content) { this.content = content; }
+    public String getImageUrl() { return imageUrl; }
+    public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
+}

+ 14 - 0
src/main/java/com/campus/trade/dto/StatusRequest.java

@@ -0,0 +1,14 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+
+@Schema(description = "状态切换请求")
+public class StatusRequest {
+    @Schema(description = "状态值", example = "ENABLED", required = true)
+    @NotBlank(message = "状态不能为空")
+    private String status;
+
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+}

+ 23 - 0
src/main/java/com/campus/trade/dto/UserProfileRequest.java

@@ -0,0 +1,23 @@
+package com.campus.trade.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.web.multipart.MultipartFile;
+
+@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;
+
+    public String getNickname() { return nickname; }
+    public void setNickname(String nickname) { this.nickname = nickname; }
+    public String getSignature() { return signature; }
+    public void setSignature(String signature) { this.signature = signature; }
+    public MultipartFile getAvatar() { return avatar; }
+    public void setAvatar(MultipartFile avatar) { this.avatar = avatar; }
+}

+ 24 - 0
src/main/java/com/campus/trade/entity/Category.java

@@ -0,0 +1,24 @@
+package com.campus.trade.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@Schema(description = "分类实体")
+public class Category {
+    private Long id;                    // 分类ID
+    private String name;                // 分类名称
+    private String status;              // 分类状态:ENABLED/DISABLED
+    private Integer sortOrder;          // 排序
+    private LocalDateTime createdAt;    // 创建时间
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public Integer getSortOrder() { return sortOrder; }
+    public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 43 - 0
src/main/java/com/campus/trade/entity/Item.java

@@ -0,0 +1,43 @@
+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;                    // 物品ID
+    private Long sellerId;              // 卖家ID(逻辑外键 → user.id)
+    private Long categoryId;            // 分类ID(逻辑外键 → category.id)
+    private String title;               // 标题
+    private String description;         // 描述
+    private BigDecimal price;           // 价格
+    private String images;              // 图片URL列表(JSON数组字符串)
+    private String tradeMethod;         // 交易方式:PICKUP/DELIVERY
+    private String status;              // 物品状态:PENDING/ON_SALE/OFF_SHELF/SOLD
+    private LocalDateTime createdAt;    // 发布时间
+    private LocalDateTime updatedAt;    // 更新时间
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getSellerId() { return sellerId; }
+    public void setSellerId(Long sellerId) { this.sellerId = sellerId; }
+    public Long getCategoryId() { return categoryId; }
+    public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
+    public BigDecimal getPrice() { return price; }
+    public void setPrice(BigDecimal price) { this.price = price; }
+    public String getImages() { return images; }
+    public void setImages(String images) { this.images = images; }
+    public String getTradeMethod() { return tradeMethod; }
+    public void setTradeMethod(String tradeMethod) { this.tradeMethod = tradeMethod; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 33 - 0
src/main/java/com/campus/trade/entity/Message.java

@@ -0,0 +1,33 @@
+package com.campus.trade.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@Schema(description = "消息实体")
+public class Message {
+    private Long id;                    // 消息ID
+    private Long fromUserId;            // 发送方ID(逻辑外键 → user.id)
+    private Long toUserId;              // 接收方ID(逻辑外键 → user.id)
+    private Long itemId;                // 关联物品ID(逻辑外键 → item.id)
+    private String content;             // 文本内容
+    private String imageUrl;            // 图片URL
+    private Boolean isRead;             // 是否已读
+    private LocalDateTime createdAt;    // 发送时间
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getFromUserId() { return fromUserId; }
+    public void setFromUserId(Long fromUserId) { this.fromUserId = fromUserId; }
+    public Long getToUserId() { return toUserId; }
+    public void setToUserId(Long toUserId) { this.toUserId = toUserId; }
+    public Long getItemId() { return itemId; }
+    public void setItemId(Long itemId) { this.itemId = itemId; }
+    public String getContent() { return content; }
+    public void setContent(String content) { this.content = content; }
+    public String getImageUrl() { return imageUrl; }
+    public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
+    public Boolean getIsRead() { return isRead; }
+    public void setIsRead(Boolean isRead) { this.isRead = isRead; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 33 - 0
src/main/java/com/campus/trade/entity/Order.java

@@ -0,0 +1,33 @@
+package com.campus.trade.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@Schema(description = "订单实体")
+public class Order {
+    private Long id;                    // 订单ID
+    private Long itemId;                // 物品ID(逻辑外键 → item.id)
+    private Long buyerId;               // 买家ID(逻辑外键 → user.id)
+    private Long sellerId;              // 卖家ID(逻辑外键 → user.id)
+    private String tradeMethod;         // 交易方式:PICKUP/DELIVERY
+    private String status;              // 订单状态:PENDING/CONFIRMED/DELIVERED/COMPLETED/CANCELLED
+    private LocalDateTime createdAt;    // 创建时间
+    private LocalDateTime updatedAt;    // 更新时间
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getItemId() { return itemId; }
+    public void setItemId(Long itemId) { this.itemId = itemId; }
+    public Long getBuyerId() { return buyerId; }
+    public void setBuyerId(Long buyerId) { this.buyerId = buyerId; }
+    public Long getSellerId() { return sellerId; }
+    public void setSellerId(Long sellerId) { this.sellerId = sellerId; }
+    public String getTradeMethod() { return tradeMethod; }
+    public void setTradeMethod(String tradeMethod) { this.tradeMethod = tradeMethod; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 30 - 0
src/main/java/com/campus/trade/entity/Review.java

@@ -0,0 +1,30 @@
+package com.campus.trade.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@Schema(description = "审核记录实体")
+public class Review {
+    private Long id;                    // 审核ID
+    private Long adminId;               // 管理员ID(逻辑外键 → user.id)
+    private String reviewType;          // 审核类型:AUTH/ITEM
+    private Long targetId;              // 关联ID(用户ID或物品ID)
+    private String result;              // 审核结果:APPROVED/REJECTED
+    private String reason;              // 驳回原因
+    private LocalDateTime createdAt;    // 审核时间
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getAdminId() { return adminId; }
+    public void setAdminId(Long adminId) { this.adminId = adminId; }
+    public String getReviewType() { return reviewType; }
+    public void setReviewType(String reviewType) { this.reviewType = reviewType; }
+    public Long getTargetId() { return targetId; }
+    public void setTargetId(Long targetId) { this.targetId = targetId; }
+    public String getResult() { return result; }
+    public void setResult(String result) { this.result = result; }
+    public String getReason() { return reason; }
+    public void setReason(String reason) { this.reason = reason; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 48 - 0
src/main/java/com/campus/trade/entity/User.java

@@ -0,0 +1,48 @@
+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;    // 更新时间
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public String getStudentId() { return studentId; }
+    public void setStudentId(String studentId) { this.studentId = studentId; }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public String getPasswordHash() { return passwordHash; }
+    public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
+    public String getNickname() { return nickname; }
+    public void setNickname(String nickname) { this.nickname = nickname; }
+    public String getAvatarUrl() { return avatarUrl; }
+    public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
+    public String getSignature() { return signature; }
+    public void setSignature(String signature) { this.signature = signature; }
+    public String getCampusCardUrl() { return campusCardUrl; }
+    public void setCampusCardUrl(String campusCardUrl) { this.campusCardUrl = campusCardUrl; }
+    public String getAuthStatus() { return authStatus; }
+    public void setAuthStatus(String authStatus) { this.authStatus = authStatus; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public String getRole() { return role; }
+    public void setRole(String role) { this.role = role; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 52 - 0
src/main/java/com/campus/trade/interceptor/AdminInterceptor.java

@@ -0,0 +1,52 @@
+package com.campus.trade.interceptor;
+
+import com.campus.trade.common.Result;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.UserMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@Component
+public class AdminInterceptor implements HandlerInterceptor {
+
+    @Autowired
+    private UserMapper userMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Override
+    public boolean preHandle(HttpServletRequest request,
+                             HttpServletResponse response,
+                             Object handler) throws Exception {
+        HttpSession session = request.getSession(false);
+        if (session == null) {
+            writeForbidden(response, "请先登录");
+            return false;
+        }
+        Long userId = (Long) session.getAttribute("userId");
+        if (userId == null) {
+            writeForbidden(response, "请先登录");
+            return false;
+        }
+        User user = userMapper.selectById(userId);
+        if (user == null || !"ADMIN".equals(user.getRole())) {
+            writeForbidden(response, "无管理员权限");
+            return false;
+        }
+        return true;
+    }
+
+    private void writeForbidden(HttpServletResponse response, String message) throws Exception {
+        response.setContentType("application/json;charset=utf-8");
+        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        response.getWriter().write(
+                objectMapper.writeValueAsString(Result.<Void>error(StatusCode.FORBIDDEN, message))
+        );
+    }
+}

+ 55 - 0
src/main/java/com/campus/trade/interceptor/AuthInterceptor.java

@@ -0,0 +1,55 @@
+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.getServletPath();
+        // GET 请求 /items/{id} 放行(物品详情)
+        if (uri.matches("/items/\\d+") && "GET".equalsIgnoreCase(request.getMethod())) {
+            return true;
+        }
+        for (String white : WHITE_LIST) {
+            if (uri.equals(white) || uri.startsWith(white + "/")) {
+                return true;
+            }
+        }
+        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.<Void>error(StatusCode.UNAUTHORIZED, "请先登录"))
+            );
+            return false;
+        }
+        return true;
+    }
+}

+ 13 - 0
src/main/java/com/campus/trade/mapper/CategoryMapper.java

@@ -0,0 +1,13 @@
+package com.campus.trade.mapper;
+
+import com.campus.trade.entity.Category;
+import java.util.List;
+
+public interface CategoryMapper {
+    List<Category> selectAll();
+    List<Category> selectEnabled();
+    Category selectById(Long id);
+    int insert(Category category);
+    int update(Category category);
+    int updateStatus(Category category);
+}

+ 15 - 0
src/main/java/com/campus/trade/mapper/ItemMapper.java

@@ -0,0 +1,15 @@
+package com.campus.trade.mapper;
+
+import com.campus.trade.entity.Item;
+import java.util.List;
+
+public interface ItemMapper {
+    List<Item> selectByStatus(String status);
+    List<Item> searchByKeyword(String keyword);
+    List<Item> selectByCategoryId(Long categoryId);
+    List<Item> selectBySellerId(Long sellerId);
+    Item selectById(Long id);
+    int insert(Item item);
+    int updateStatus(Item item);
+    int deleteById(Long id);
+}

+ 12 - 0
src/main/java/com/campus/trade/mapper/MessageMapper.java

@@ -0,0 +1,12 @@
+package com.campus.trade.mapper;
+
+import com.campus.trade.entity.Message;
+import com.campus.trade.vo.ConversationVO;
+import java.util.List;
+
+public interface MessageMapper {
+    List<ConversationVO> selectConversations(Long userId);
+    List<Message> selectHistory(Long userId1, Long userId2);
+    int insert(Message message);
+    int markAsRead(Long toUserId, Long fromUserId);
+}

+ 12 - 0
src/main/java/com/campus/trade/mapper/OrderMapper.java

@@ -0,0 +1,12 @@
+package com.campus.trade.mapper;
+
+import com.campus.trade.entity.Order;
+import java.util.List;
+
+public interface OrderMapper {
+    List<Order> selectByBuyerId(Long buyerId);
+    List<Order> selectBySellerId(Long sellerId);
+    Order selectById(Long id);
+    int insert(Order order);
+    int updateStatus(Order order);
+}

+ 9 - 0
src/main/java/com/campus/trade/mapper/ReviewMapper.java

@@ -0,0 +1,9 @@
+package com.campus.trade.mapper;
+
+import com.campus.trade.entity.Review;
+import java.util.List;
+
+public interface ReviewMapper {
+    int insert(Review review);
+    List<Review> selectByTypeAndTarget(String reviewType, Long targetId);
+}

+ 16 - 0
src/main/java/com/campus/trade/mapper/UserMapper.java

@@ -0,0 +1,16 @@
+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);
+    User selectById(Long id);
+    int insert(User user);
+    int updateAuthStatus(User user);
+    int updatePassword(User user);
+    int updateProfile(User user);
+    int updateStatus(User user);
+    List<User> selectAll();
+}

+ 104 - 0
src/main/java/com/campus/trade/service/AuthService.java

@@ -0,0 +1,104 @@
+package com.campus.trade.service;
+
+import com.campus.trade.common.BusinessException;
+import com.campus.trade.common.FileUploadUtil;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.dto.ChangePasswordRequest;
+import com.campus.trade.dto.LoginRequest;
+import com.campus.trade.dto.RegisterRequest;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.UserMapper;
+import com.campus.trade.vo.UserProfileVO;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
+
+import java.nio.charset.StandardCharsets;
+
+@Service
+public class AuthService {
+
+    @Autowired
+    private UserMapper userMapper;
+
+    @Autowired
+    private FileUploadUtil fileUploadUtil;
+
+    // 用户注册
+    public Long register(RegisterRequest req) {
+        // 检查学号是否已注册
+        User existing = userMapper.selectByStudentId(req.getStudentId());
+        if (existing != null) {
+            throw new BusinessException(StatusCode.CONFLICT, "学号已注册");
+        }
+        // 上传校园卡照片
+        String campusCardUrl = null;
+        if (req.getCampusCardImage() != null && !req.getCampusCardImage().isEmpty()) {
+            campusCardUrl = fileUploadUtil.upload(req.getCampusCardImage());
+        }
+        // MD5 加密密码
+        String passwordHash = DigestUtils.md5DigestAsHex(
+                req.getPassword().getBytes(StandardCharsets.UTF_8)
+        );
+        // 构建用户实体
+        User user = new User();
+        user.setStudentId(req.getStudentId());
+        user.setName(req.getName());
+        user.setPasswordHash(passwordHash);
+        user.setCampusCardUrl(campusCardUrl);
+        user.setAuthStatus("PENDING");     // 待审核
+        user.setStatus("NORMAL");
+        user.setRole("USER");
+        // 默认昵称为姓名
+        user.setNickname(req.getName());
+        userMapper.insert(user);
+        return user.getId();
+    }
+
+    // 用户登录
+    public UserProfileVO login(LoginRequest req, HttpSession session) {
+        User user = userMapper.selectByStudentId(req.getStudentId());
+        if (user == null) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "学号或密码错误");
+        }
+        // 检查是否被封禁
+        if ("BANNED".equals(user.getStatus())) {
+            throw new BusinessException(StatusCode.FORBIDDEN, "账号已被封禁");
+        }
+        // MD5 密码比对
+        String inputHash = DigestUtils.md5DigestAsHex(
+                req.getPassword().getBytes(StandardCharsets.UTF_8)
+        );
+        if (!inputHash.equals(user.getPasswordHash())) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "学号或密码错误");
+        }
+        // 创建 Session
+        session.setAttribute("userId", user.getId());
+        session.setAttribute("role", user.getRole());
+
+        UserProfileVO vo = new UserProfileVO();
+        BeanUtils.copyProperties(user, vo);
+        return vo;
+    }
+
+    // 修改密码
+    public void changePassword(ChangePasswordRequest req, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        User user = userMapper.selectById(userId);
+        // 验证旧密码
+        String oldHash = DigestUtils.md5DigestAsHex(
+                req.getOldPassword().getBytes(StandardCharsets.UTF_8)
+        );
+        if (!oldHash.equals(user.getPasswordHash())) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "旧密码错误");
+        }
+        // 更新为新密码
+        String newHash = DigestUtils.md5DigestAsHex(
+                req.getNewPassword().getBytes(StandardCharsets.UTF_8)
+        );
+        user.setPasswordHash(newHash);
+        userMapper.updatePassword(user);
+    }
+}

+ 177 - 0
src/main/java/com/campus/trade/service/ItemService.java

@@ -0,0 +1,177 @@
+package com.campus.trade.service;
+
+import com.campus.trade.common.BusinessException;
+import com.campus.trade.common.FileUploadUtil;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.dto.ItemPublishRequest;
+import com.campus.trade.entity.Category;
+import com.campus.trade.entity.Item;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.CategoryMapper;
+import com.campus.trade.mapper.ItemMapper;
+import com.campus.trade.mapper.UserMapper;
+import com.campus.trade.vo.ItemVO;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class ItemService {
+
+    @Autowired
+    private ItemMapper itemMapper;
+
+    @Autowired
+    private UserMapper userMapper;
+
+    @Autowired
+    private CategoryMapper categoryMapper;
+
+    @Autowired
+    private FileUploadUtil fileUploadUtil;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    // 首页物品列表(分页,按时间倒序)
+    public PageInfo<ItemVO> list(int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<Item> items = itemMapper.selectByStatus("ON_SALE");
+        PageInfo<Item> pageInfo = new PageInfo<>(items);
+        return toVOPageInfo(pageInfo);
+    }
+
+    // 搜索物品
+    public PageInfo<ItemVO> search(String keyword, int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<Item> items = itemMapper.searchByKeyword(keyword);
+        PageInfo<Item> pageInfo = new PageInfo<>(items);
+        return toVOPageInfo(pageInfo);
+    }
+
+    // 按分类筛选
+    public PageInfo<ItemVO> byCategory(Long categoryId, int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<Item> items = itemMapper.selectByCategoryId(categoryId);
+        PageInfo<Item> pageInfo = new PageInfo<>(items);
+        return toVOPageInfo(pageInfo);
+    }
+
+    // 物品详情
+    public ItemVO detail(Long id) {
+        Item item = itemMapper.selectById(id);
+        if (item == null) {
+            throw new BusinessException(StatusCode.NOT_FOUND, "物品不存在");
+        }
+        return toVO(item);
+    }
+
+    // 发布物品
+    public Long publish(ItemPublishRequest req, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        User user = userMapper.selectById(userId);
+        // 校验实名认证
+        if (!"AUTHENTICATED".equals(user.getAuthStatus())) {
+            throw new BusinessException(StatusCode.FORBIDDEN, "请先完成实名认证");
+        }
+        // 批量上传图片
+        List<String> imageUrls = fileUploadUtil.uploadBatch(req.getImages());
+        // 构建物品实体
+        Item item = new Item();
+        item.setSellerId(userId);
+        item.setCategoryId(req.getCategoryId());
+        item.setTitle(req.getTitle());
+        item.setDescription(req.getDescription());
+        item.setPrice(req.getPrice());
+        item.setTradeMethod(req.getTradeMethod());
+        item.setStatus("PENDING");  // 待审核
+        // 图片列表转为 JSON 字符串存储
+        try {
+            item.setImages(objectMapper.writeValueAsString(imageUrls));
+        } catch (JsonProcessingException e) {
+            item.setImages("[]");
+        }
+        itemMapper.insert(item);
+        return item.getId();
+    }
+
+    // 我的发布列表
+    public List<ItemVO> myItems(HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        List<Item> items = itemMapper.selectBySellerId(userId);
+        return items.stream().map(this::toVO).collect(Collectors.toList());
+    }
+
+    // 下架物品
+    public void offline(Long id, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        Item item = itemMapper.selectById(id);
+        if (item == null) {
+            throw new BusinessException(StatusCode.NOT_FOUND, "物品不存在");
+        }
+        if (!item.getSellerId().equals(userId)) {
+            throw new BusinessException(StatusCode.FORBIDDEN, "无权操作该物品");
+        }
+        item.setStatus("OFF_SHELF");
+        itemMapper.updateStatus(item);
+    }
+
+    // 删除物品
+    public void delete(Long id, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        Item item = itemMapper.selectById(id);
+        if (item == null) {
+            throw new BusinessException(StatusCode.NOT_FOUND, "物品不存在");
+        }
+        if (!item.getSellerId().equals(userId)) {
+            throw new BusinessException(StatusCode.FORBIDDEN, "无权操作该物品");
+        }
+        itemMapper.deleteById(id);
+    }
+
+    // --- 私有辅助方法 ---
+
+    private ItemVO toVO(Item item) {
+        ItemVO vo = new ItemVO();
+        BeanUtils.copyProperties(item, vo);
+        // 卖家信息
+        User seller = userMapper.selectById(item.getSellerId());
+        if (seller != null) {
+            vo.setSellerNickname(seller.getNickname());
+            vo.setSellerAvatar(seller.getAvatarUrl());
+        }
+        // 分类名
+        if (item.getCategoryId() != null) {
+            Category category = categoryMapper.selectById(item.getCategoryId());
+            if (category != null) {
+                vo.setCategoryName(category.getName());
+            }
+        }
+        // 图片 JSON 转 List
+        try {
+            String[] urls = objectMapper.readValue(item.getImages(), String[].class);
+            vo.setImages(Arrays.asList(urls));
+        } catch (Exception e) {
+            vo.setImages(Collections.emptyList());
+        }
+        return vo;
+    }
+
+    private PageInfo<ItemVO> toVOPageInfo(PageInfo<Item> pageInfo) {
+        List<ItemVO> voList = pageInfo.getList().stream()
+                .map(this::toVO).collect(Collectors.toList());
+        PageInfo<ItemVO> voPageInfo = new PageInfo<>();
+        BeanUtils.copyProperties(pageInfo, voPageInfo, "list");
+        voPageInfo.setList(voList);
+        return voPageInfo;
+    }
+}

+ 80 - 0
src/main/java/com/campus/trade/service/MessageService.java

@@ -0,0 +1,80 @@
+package com.campus.trade.service;
+
+import com.campus.trade.entity.Message;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.MessageMapper;
+import com.campus.trade.mapper.UserMapper;
+import com.campus.trade.dto.SendMessageRequest;
+import com.campus.trade.vo.ConversationVO;
+import com.campus.trade.vo.MessageVO;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class MessageService {
+
+    @Autowired
+    private MessageMapper messageMapper;
+
+    @Autowired
+    private UserMapper userMapper;
+
+    // 发送消息
+    public Long send(SendMessageRequest req, HttpSession session) {
+        Long fromUserId = (Long) session.getAttribute("userId");
+        Message message = new Message();
+        message.setFromUserId(fromUserId);
+        message.setToUserId(req.getToUserId());
+        message.setItemId(req.getItemId());
+        message.setContent(req.getContent());
+        message.setImageUrl(req.getImageUrl());
+        message.setIsRead(false);
+        messageMapper.insert(message);
+        return message.getId();
+    }
+
+    // 会话列表
+    public List<ConversationVO> conversations(HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        return messageMapper.selectConversations(userId);
+    }
+
+    // 聊天记录(分页)
+    public PageInfo<MessageVO> history(Long targetUserId, int pageNum, int pageSize, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        PageHelper.startPage(pageNum, pageSize);
+        List<Message> messages = messageMapper.selectHistory(userId, targetUserId);
+        PageInfo<Message> pageInfo = new PageInfo<>(messages);
+        // 标记为已读
+        messageMapper.markAsRead(userId, targetUserId);
+        // 转 VO
+        List<MessageVO> voList = messages.stream().map(this::toVO).collect(Collectors.toList());
+        PageInfo<MessageVO> voPageInfo = new PageInfo<>();
+        BeanUtils.copyProperties(pageInfo, voPageInfo, "list");
+        voPageInfo.setList(voList);
+        return voPageInfo;
+    }
+
+    private MessageVO toVO(Message message) {
+        MessageVO vo = new MessageVO();
+        BeanUtils.copyProperties(message, vo);
+        User fromUser = userMapper.selectById(message.getFromUserId());
+        if (fromUser != null) {
+            vo.setFromUserNickname(fromUser.getNickname());
+            vo.setFromUserAvatar(fromUser.getAvatarUrl());
+        }
+        User toUser = userMapper.selectById(message.getToUserId());
+        if (toUser != null) {
+            vo.setToUserNickname(toUser.getNickname());
+            vo.setToUserAvatar(toUser.getAvatarUrl());
+        }
+        return vo;
+    }
+}

+ 170 - 0
src/main/java/com/campus/trade/service/OrderService.java

@@ -0,0 +1,170 @@
+package com.campus.trade.service;
+
+import com.campus.trade.common.BusinessException;
+import com.campus.trade.common.OrderStatusMachine;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.entity.Item;
+import com.campus.trade.entity.Order;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.ItemMapper;
+import com.campus.trade.mapper.OrderMapper;
+import com.campus.trade.mapper.UserMapper;
+import com.campus.trade.vo.OrderVO;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class OrderService {
+
+    @Autowired
+    private OrderMapper orderMapper;
+
+    @Autowired
+    private ItemMapper itemMapper;
+
+    @Autowired
+    private UserMapper userMapper;
+
+    @Autowired
+    private OrderStatusMachine statusMachine;
+
+    // 生成订单
+    @Transactional
+    public Long create(Long itemId, HttpSession session) {
+        Long buyerId = (Long) session.getAttribute("userId");
+        Item item = itemMapper.selectById(itemId);
+        if (item == null) {
+            throw new BusinessException(StatusCode.NOT_FOUND, "物品不存在");
+        }
+        if (!"ON_SALE".equals(item.getStatus())) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "物品已下架或已售出");
+        }
+        // 不能买自己的物品
+        if (item.getSellerId().equals(buyerId)) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "不能购买自己的物品");
+        }
+        // 创建订单
+        Order order = new Order();
+        order.setItemId(itemId);
+        order.setBuyerId(buyerId);
+        order.setSellerId(item.getSellerId());
+        order.setTradeMethod(item.getTradeMethod());
+        order.setStatus("PENDING");
+        orderMapper.insert(order);
+        // 物品标记为已售出
+        item.setStatus("SOLD");
+        itemMapper.updateStatus(item);
+        return order.getId();
+    }
+
+    // 我买到的
+    public PageInfo<OrderVO> bought(int pageNum, int pageSize, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        PageHelper.startPage(pageNum, pageSize);
+        List<Order> orders = orderMapper.selectByBuyerId(userId);
+        return toVOPageInfo(new PageInfo<>(orders));
+    }
+
+    // 我卖出的
+    public PageInfo<OrderVO> sold(int pageNum, int pageSize, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        PageHelper.startPage(pageNum, pageSize);
+        List<Order> orders = orderMapper.selectBySellerId(userId);
+        return toVOPageInfo(new PageInfo<>(orders));
+    }
+
+    // 卖家接单
+    @Transactional
+    public void accept(Long orderId, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        Order order = orderMapper.selectById(orderId);
+        if (order == null) throw new BusinessException(StatusCode.NOT_FOUND, "订单不存在");
+        if (!order.getSellerId().equals(userId)) throw new BusinessException(StatusCode.FORBIDDEN, "无权操作");
+        statusMachine.validateTransition(order.getStatus(), "CONFIRMED");
+        order.setStatus("CONFIRMED");
+        orderMapper.updateStatus(order);
+    }
+
+    // 卖家拒单
+    @Transactional
+    public void reject(Long orderId, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        Order order = orderMapper.selectById(orderId);
+        if (order == null) throw new BusinessException(StatusCode.NOT_FOUND, "订单不存在");
+        if (!order.getSellerId().equals(userId)) throw new BusinessException(StatusCode.FORBIDDEN, "无权操作");
+        statusMachine.validateTransition(order.getStatus(), "CANCELLED");
+        order.setStatus("CANCELLED");
+        orderMapper.updateStatus(order);
+        // 物品恢复在售
+        Item item = itemMapper.selectById(order.getItemId());
+        if (item != null) {
+            item.setStatus("ON_SALE");
+            itemMapper.updateStatus(item);
+        }
+    }
+
+    // 卖家确认交货
+    @Transactional
+    public void deliver(Long orderId, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        Order order = orderMapper.selectById(orderId);
+        if (order == null) throw new BusinessException(StatusCode.NOT_FOUND, "订单不存在");
+        if (!order.getSellerId().equals(userId)) throw new BusinessException(StatusCode.FORBIDDEN, "无权操作");
+        statusMachine.validateTransition(order.getStatus(), "DELIVERED");
+        order.setStatus("DELIVERED");
+        orderMapper.updateStatus(order);
+    }
+
+    // 买家确认收货
+    @Transactional
+    public void confirm(Long orderId, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        Order order = orderMapper.selectById(orderId);
+        if (order == null) throw new BusinessException(StatusCode.NOT_FOUND, "订单不存在");
+        if (!order.getBuyerId().equals(userId)) throw new BusinessException(StatusCode.FORBIDDEN, "无权操作");
+        statusMachine.validateTransition(order.getStatus(), "COMPLETED");
+        order.setStatus("COMPLETED");
+        orderMapper.updateStatus(order);
+    }
+
+    // --- 私有辅助 ---
+
+    private OrderVO toVO(Order order) {
+        OrderVO vo = new OrderVO();
+        BeanUtils.copyProperties(order, vo);
+        Item item = itemMapper.selectById(order.getItemId());
+        if (item != null) {
+            vo.setItemTitle(item.getTitle());
+            vo.setItemPrice(item.getPrice());
+            // 取第一张图作为缩略图
+            if (item.getImages() != null && !item.getImages().isEmpty()) {
+                try {
+                    String[] urls = new com.fasterxml.jackson.databind.ObjectMapper()
+                            .readValue(item.getImages(), String[].class);
+                    if (urls.length > 0) vo.setItemImage(urls[0]);
+                } catch (Exception ignored) {}
+            }
+        }
+        User buyer = userMapper.selectById(order.getBuyerId());
+        if (buyer != null) vo.setBuyerNickname(buyer.getNickname());
+        User seller = userMapper.selectById(order.getSellerId());
+        if (seller != null) vo.setSellerNickname(seller.getNickname());
+        return vo;
+    }
+
+    private PageInfo<OrderVO> toVOPageInfo(PageInfo<Order> pageInfo) {
+        List<OrderVO> voList = pageInfo.getList().stream().map(this::toVO).collect(Collectors.toList());
+        PageInfo<OrderVO> voPageInfo = new PageInfo<>();
+        BeanUtils.copyProperties(pageInfo, voPageInfo, "list");
+        voPageInfo.setList(voList);
+        return voPageInfo;
+    }
+}

+ 62 - 0
src/main/java/com/campus/trade/service/UserService.java

@@ -0,0 +1,62 @@
+package com.campus.trade.service;
+
+import com.campus.trade.common.BusinessException;
+import com.campus.trade.common.FileUploadUtil;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.dto.UserProfileRequest;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.UserMapper;
+import com.campus.trade.vo.UserProfileVO;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
+
+    @Autowired
+    private UserMapper userMapper;
+
+    @Autowired
+    private FileUploadUtil fileUploadUtil;
+
+    // 获取个人资料
+    public UserProfileVO getProfile(HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new BusinessException(StatusCode.NOT_FOUND, "用户不存在");
+        }
+        UserProfileVO vo = new UserProfileVO();
+        BeanUtils.copyProperties(user, vo);
+        return vo;
+    }
+
+    // 修改个人资料
+    public UserProfileVO updateProfile(UserProfileRequest req, HttpSession session) {
+        Long userId = (Long) session.getAttribute("userId");
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new BusinessException(StatusCode.NOT_FOUND, "用户不存在");
+        }
+        // 更新昵称
+        if (req.getNickname() != null && !req.getNickname().isEmpty()) {
+            user.setNickname(req.getNickname());
+        }
+        // 更新签名
+        if (req.getSignature() != null) {
+            user.setSignature(req.getSignature());
+        }
+        // 更新头像
+        if (req.getAvatar() != null && !req.getAvatar().isEmpty()) {
+            String avatarUrl = fileUploadUtil.upload(req.getAvatar());
+            user.setAvatarUrl(avatarUrl);
+        }
+        userMapper.updateProfile(user);
+
+        UserProfileVO vo = new UserProfileVO();
+        BeanUtils.copyProperties(user, vo);
+        return vo;
+    }
+}

+ 50 - 0
src/main/java/com/campus/trade/service/admin/CategoryService.java

@@ -0,0 +1,50 @@
+package com.campus.trade.service.admin;
+
+import com.campus.trade.common.BusinessException;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.dto.CategoryRequest;
+import com.campus.trade.entity.Category;
+import com.campus.trade.mapper.CategoryMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class CategoryService {
+
+    @Autowired
+    private CategoryMapper categoryMapper;
+
+    // 分类列表
+    public List<Category> list() {
+        return categoryMapper.selectAll();
+    }
+
+    // 新增分类
+    public Long create(CategoryRequest req) {
+        Category category = new Category();
+        category.setName(req.getName());
+        category.setSortOrder(req.getSortOrder() != null ? req.getSortOrder() : 0);
+        category.setStatus("ENABLED");
+        categoryMapper.insert(category);
+        return category.getId();
+    }
+
+    // 修改分类
+    public void update(Long id, CategoryRequest req) {
+        Category category = categoryMapper.selectById(id);
+        if (category == null) throw new BusinessException(StatusCode.NOT_FOUND, "分类不存在");
+        category.setName(req.getName());
+        if (req.getSortOrder() != null) category.setSortOrder(req.getSortOrder());
+        categoryMapper.update(category);
+    }
+
+    // 启用/禁用分类
+    public void toggleStatus(Long id, String status) {
+        Category category = categoryMapper.selectById(id);
+        if (category == null) throw new BusinessException(StatusCode.NOT_FOUND, "分类不存在");
+        category.setStatus(status);
+        categoryMapper.updateStatus(category);
+    }
+}

+ 147 - 0
src/main/java/com/campus/trade/service/admin/ReviewService.java

@@ -0,0 +1,147 @@
+package com.campus.trade.service.admin;
+
+import com.campus.trade.common.BusinessException;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.dto.ReviewRequest;
+import com.campus.trade.entity.Category;
+import com.campus.trade.entity.Item;
+import com.campus.trade.entity.Review;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.CategoryMapper;
+import com.campus.trade.mapper.ItemMapper;
+import com.campus.trade.mapper.ReviewMapper;
+import com.campus.trade.mapper.UserMapper;
+import com.campus.trade.vo.ItemVO;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class ReviewService {
+
+    @Autowired
+    private UserMapper userMapper;
+
+    @Autowired
+    private ItemMapper itemMapper;
+
+    @Autowired
+    private ReviewMapper reviewMapper;
+
+    @Autowired
+    private CategoryMapper categoryMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    // 待审核用户列表
+    public PageInfo<User> pendingUsers(int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        return new PageInfo<>(userMapper.selectByAuthStatus("PENDING"));
+    }
+
+    // 审核用户实名认证
+    @Transactional
+    public void reviewUser(Long userId, ReviewRequest req, HttpSession session) {
+        Long adminId = (Long) session.getAttribute("userId");
+        User user = userMapper.selectById(userId);
+        if (user == null) throw new BusinessException(StatusCode.NOT_FOUND, "用户不存在");
+        if (!"PENDING".equals(user.getAuthStatus())) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "该用户不在待审核状态");
+        }
+        // 更新用户认证状态
+        if ("APPROVED".equals(req.getResult())) {
+            user.setAuthStatus("AUTHENTICATED");
+        } else {
+            user.setAuthStatus("UNAUTHENTICATED");
+        }
+        userMapper.updateAuthStatus(user);
+        // 记录审核记录
+        Review review = new Review();
+        review.setAdminId(adminId);
+        review.setReviewType("AUTH");
+        review.setTargetId(userId);
+        review.setResult(req.getResult());
+        review.setReason(req.getReason());
+        reviewMapper.insert(review);
+    }
+
+    // 待审核物品列表
+    public PageInfo<ItemVO> pendingItems(int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        PageInfo<Item> pageInfo = new PageInfo<>(itemMapper.selectByStatus("PENDING"));
+        return toVOPageInfo(pageInfo);
+    }
+
+    // 审核物品上架
+    @Transactional
+    public void reviewItem(Long itemId, ReviewRequest req, HttpSession session) {
+        Long adminId = (Long) session.getAttribute("userId");
+        Item item = itemMapper.selectById(itemId);
+        if (item == null) throw new BusinessException(StatusCode.NOT_FOUND, "物品不存在");
+        if (!"PENDING".equals(item.getStatus())) {
+            throw new BusinessException(StatusCode.BAD_REQUEST, "该物品不在待审核状态");
+        }
+        // 更新物品状态
+        if ("APPROVED".equals(req.getResult())) {
+            item.setStatus("ON_SALE");
+        } else {
+            item.setStatus("OFF_SHELF");
+        }
+        itemMapper.updateStatus(item);
+        // 记录审核记录
+        Review review = new Review();
+        review.setAdminId(adminId);
+        review.setReviewType("ITEM");
+        review.setTargetId(itemId);
+        review.setResult(req.getResult());
+        review.setReason(req.getReason());
+        reviewMapper.insert(review);
+    }
+
+    // --- 私有辅助 ---
+
+    private ItemVO toVO(Item item) {
+        ItemVO vo = new ItemVO();
+        BeanUtils.copyProperties(item, vo);
+        // 卖家信息
+        User seller = userMapper.selectById(item.getSellerId());
+        if (seller != null) {
+            vo.setSellerNickname(seller.getNickname());
+            vo.setSellerAvatar(seller.getAvatarUrl());
+        }
+        // 分类名
+        if (item.getCategoryId() != null) {
+            Category category = categoryMapper.selectById(item.getCategoryId());
+            if (category != null) {
+                vo.setCategoryName(category.getName());
+            }
+        }
+        // 图片 JSON 转 List
+        try {
+            String[] urls = objectMapper.readValue(item.getImages(), String[].class);
+            vo.setImages(Arrays.asList(urls));
+        } catch (Exception e) {
+            vo.setImages(Collections.emptyList());
+        }
+        return vo;
+    }
+
+    private PageInfo<ItemVO> toVOPageInfo(PageInfo<Item> pageInfo) {
+        List<ItemVO> voList = pageInfo.getList().stream()
+                .map(this::toVO).collect(Collectors.toList());
+        PageInfo<ItemVO> voPageInfo = new PageInfo<>();
+        BeanUtils.copyProperties(pageInfo, voPageInfo, "list");
+        voPageInfo.setList(voList);
+        return voPageInfo;
+    }
+}

+ 57 - 0
src/main/java/com/campus/trade/service/admin/UserManageService.java

@@ -0,0 +1,57 @@
+package com.campus.trade.service.admin;
+
+import com.campus.trade.common.BusinessException;
+import com.campus.trade.common.StatusCode;
+import com.campus.trade.entity.User;
+import com.campus.trade.mapper.UserMapper;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
+
+import java.nio.charset.StandardCharsets;
+
+@Service
+public class UserManageService {
+
+    @Autowired
+    private UserMapper userMapper;
+
+    // 用户列表
+    public PageInfo<User> list(int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        return new PageInfo<>(userMapper.selectAll());
+    }
+
+    // 封禁用户
+    public void ban(Long userId, HttpSession session) {
+        User user = userMapper.selectById(userId);
+        if (user == null) throw new BusinessException(StatusCode.NOT_FOUND, "用户不存在");
+        if ("ADMIN".equals(user.getRole())) {
+            throw new BusinessException(StatusCode.FORBIDDEN, "不能封禁管理员");
+        }
+        user.setStatus("BANNED");
+        userMapper.updateStatus(user);
+    }
+
+    // 解封用户
+    public void unban(Long userId, HttpSession session) {
+        User user = userMapper.selectById(userId);
+        if (user == null) throw new BusinessException(StatusCode.NOT_FOUND, "用户不存在");
+        user.setStatus("NORMAL");
+        userMapper.updateStatus(user);
+    }
+
+    // 重置密码(重置为 123456)
+    public void resetPassword(Long userId, HttpSession session) {
+        User user = userMapper.selectById(userId);
+        if (user == null) throw new BusinessException(StatusCode.NOT_FOUND, "用户不存在");
+        String defaultHash = DigestUtils.md5DigestAsHex(
+                "123456".getBytes(StandardCharsets.UTF_8)
+        );
+        user.setPasswordHash(defaultHash);
+        userMapper.updatePassword(user);
+    }
+}

+ 37 - 0
src/main/java/com/campus/trade/vo/ConversationVO.java

@@ -0,0 +1,37 @@
+package com.campus.trade.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@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;
+
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public String getNickname() { return nickname; }
+    public void setNickname(String nickname) { this.nickname = nickname; }
+    public String getAvatarUrl() { return avatarUrl; }
+    public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
+    public String getLastMessage() { return lastMessage; }
+    public void setLastMessage(String lastMessage) { this.lastMessage = lastMessage; }
+    public String getLastMessageImage() { return lastMessageImage; }
+    public void setLastMessageImage(String lastMessageImage) { this.lastMessageImage = lastMessageImage; }
+    public LocalDateTime getLastMessageTime() { return lastMessageTime; }
+    public void setLastMessageTime(LocalDateTime lastMessageTime) { this.lastMessageTime = lastMessageTime; }
+    public Integer getUnreadCount() { return unreadCount; }
+    public void setUnreadCount(Integer unreadCount) { this.unreadCount = unreadCount; }
+}

+ 59 - 0
src/main/java/com/campus/trade/vo/ItemVO.java

@@ -0,0 +1,59 @@
+package com.campus.trade.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@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;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getSellerId() { return sellerId; }
+    public void setSellerId(Long sellerId) { this.sellerId = sellerId; }
+    public String getSellerNickname() { return sellerNickname; }
+    public void setSellerNickname(String sellerNickname) { this.sellerNickname = sellerNickname; }
+    public String getSellerAvatar() { return sellerAvatar; }
+    public void setSellerAvatar(String sellerAvatar) { this.sellerAvatar = sellerAvatar; }
+    public String getCategoryName() { return categoryName; }
+    public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
+    public BigDecimal getPrice() { return price; }
+    public void setPrice(BigDecimal price) { this.price = price; }
+    public List<String> getImages() { return images; }
+    public void setImages(List<String> images) { this.images = images; }
+    public String getTradeMethod() { return tradeMethod; }
+    public void setTradeMethod(String tradeMethod) { this.tradeMethod = tradeMethod; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 53 - 0
src/main/java/com/campus/trade/vo/MessageVO.java

@@ -0,0 +1,53 @@
+package com.campus.trade.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@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 toUserAvatar;
+    @Schema(description = "文本内容")
+    private String content;
+    @Schema(description = "图片URL")
+    private String imageUrl;
+    @Schema(description = "是否已读")
+    private Boolean isRead;
+    @Schema(description = "发送时间")
+    private LocalDateTime createdAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getFromUserId() { return fromUserId; }
+    public void setFromUserId(Long fromUserId) { this.fromUserId = fromUserId; }
+    public String getFromUserNickname() { return fromUserNickname; }
+    public void setFromUserNickname(String fromUserNickname) { this.fromUserNickname = fromUserNickname; }
+    public String getFromUserAvatar() { return fromUserAvatar; }
+    public void setFromUserAvatar(String fromUserAvatar) { this.fromUserAvatar = fromUserAvatar; }
+    public Long getToUserId() { return toUserId; }
+    public void setToUserId(Long toUserId) { this.toUserId = toUserId; }
+    public String getToUserNickname() { return toUserNickname; }
+    public void setToUserNickname(String toUserNickname) { this.toUserNickname = toUserNickname; }
+    public String getToUserAvatar() { return toUserAvatar; }
+    public void setToUserAvatar(String toUserAvatar) { this.toUserAvatar = toUserAvatar; }
+    public String getContent() { return content; }
+    public void setContent(String content) { this.content = content; }
+    public String getImageUrl() { return imageUrl; }
+    public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
+    public Boolean getIsRead() { return isRead; }
+    public void setIsRead(Boolean isRead) { this.isRead = isRead; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 62 - 0
src/main/java/com/campus/trade/vo/OrderVO.java

@@ -0,0 +1,62 @@
+package com.campus.trade.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@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;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getItemId() { return itemId; }
+    public void setItemId(Long itemId) { this.itemId = itemId; }
+    public String getItemTitle() { return itemTitle; }
+    public void setItemTitle(String itemTitle) { this.itemTitle = itemTitle; }
+    public String getItemImage() { return itemImage; }
+    public void setItemImage(String itemImage) { this.itemImage = itemImage; }
+    public BigDecimal getItemPrice() { return itemPrice; }
+    public void setItemPrice(BigDecimal itemPrice) { this.itemPrice = itemPrice; }
+    public Long getBuyerId() { return buyerId; }
+    public void setBuyerId(Long buyerId) { this.buyerId = buyerId; }
+    public String getBuyerNickname() { return buyerNickname; }
+    public void setBuyerNickname(String buyerNickname) { this.buyerNickname = buyerNickname; }
+    public Long getSellerId() { return sellerId; }
+    public void setSellerId(Long sellerId) { this.sellerId = sellerId; }
+    public String getSellerNickname() { return sellerNickname; }
+    public void setSellerNickname(String sellerNickname) { this.sellerNickname = sellerNickname; }
+    public String getTradeMethod() { return tradeMethod; }
+    public void setTradeMethod(String tradeMethod) { this.tradeMethod = tradeMethod; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 40 - 0
src/main/java/com/campus/trade/vo/PageResult.java

@@ -0,0 +1,40 @@
+package com.campus.trade.vo;
+
+import com.github.pagehelper.PageInfo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+
+@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;
+
+    public List<T> getList() { return list; }
+    public void setList(List<T> list) { this.list = list; }
+    public int getPageNum() { return pageNum; }
+    public void setPageNum(int pageNum) { this.pageNum = pageNum; }
+    public int getPageSize() { return pageSize; }
+    public void setPageSize(int pageSize) { this.pageSize = pageSize; }
+    public long getTotal() { return total; }
+    public void setTotal(long total) { this.total = total; }
+    public int getPages() { return pages; }
+    public void setPages(int pages) { this.pages = pages; }
+
+    public static <T> PageResult<T> of(PageInfo<T> pageInfo) {
+        PageResult<T> result = new PageResult<>();
+        result.setList(pageInfo.getList());
+        result.setPageNum(pageInfo.getPageNum());
+        result.setPageSize(pageInfo.getPageSize());
+        result.setTotal(pageInfo.getTotal());
+        result.setPages(pageInfo.getPages());
+        return result;
+    }
+}

+ 49 - 0
src/main/java/com/campus/trade/vo/UserProfileVO.java

@@ -0,0 +1,49 @@
+package com.campus.trade.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+@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;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public String getStudentId() { return studentId; }
+    public void setStudentId(String studentId) { this.studentId = studentId; }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public String getNickname() { return nickname; }
+    public void setNickname(String nickname) { this.nickname = nickname; }
+    public String getAvatarUrl() { return avatarUrl; }
+    public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
+    public String getSignature() { return signature; }
+    public void setSignature(String signature) { this.signature = signature; }
+    public String getAuthStatus() { return authStatus; }
+    public void setAuthStatus(String authStatus) { this.authStatus = authStatus; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public String getRole() { return role; }
+    public void setRole(String role) { this.role = role; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 27 - 3
src/main/resources/application.yaml

@@ -6,20 +6,44 @@ spring:
     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: space.anyi.campusIdleItemTradingPlatform.server.entity
+  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
-  port: 8080
+    context-path: /
+    session:
+      timeout: 3600s
+  port: 8090
+
+upload:
+  dir: ./upload

+ 23 - 0
src/main/resources/mapper/CategoryMapper.xml

@@ -0,0 +1,23 @@
+<?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.CategoryMapper">
+    <select id="selectAll" resultType="Category">
+        SELECT * FROM category ORDER BY sort_order ASC
+    </select>
+    <select id="selectEnabled" resultType="Category">
+        SELECT * FROM category WHERE status = 'ENABLED' ORDER BY sort_order ASC
+    </select>
+    <select id="selectById" resultType="Category">
+        SELECT * FROM category WHERE id = #{id}
+    </select>
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO category (name, status, sort_order) VALUES (#{name}, #{status}, #{sortOrder})
+    </insert>
+    <update id="update">
+        UPDATE category SET name = #{name}, sort_order = #{sortOrder} WHERE id = #{id}
+    </update>
+    <update id="updateStatus">
+        UPDATE category SET status = #{status} WHERE id = #{id}
+    </update>
+</mapper>

+ 35 - 0
src/main/resources/mapper/ItemMapper.xml

@@ -0,0 +1,35 @@
+<?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.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>
+    <select id="selectById" resultType="Item">
+        SELECT * FROM item WHERE id = #{id}
+    </select>
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO item (seller_id, category_id, title, description, price, images, trade_method, status)
+        VALUES (#{sellerId}, #{categoryId}, #{title}, #{description}, #{price}, #{images}, #{tradeMethod}, #{status})
+    </insert>
+    <update id="updateStatus">
+        UPDATE item SET status = #{status} WHERE id = #{id}
+    </update>
+    <delete id="deleteById">
+        DELETE FROM item WHERE id = #{id}
+    </delete>
+</mapper>

+ 35 - 0
src/main/resources/mapper/MessageMapper.xml

@@ -0,0 +1,35 @@
+<?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.MessageMapper">
+    <select id="selectConversations" resultType="com.campus.trade.vo.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>
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO message (from_user_id, to_user_id, item_id, content, image_url, is_read)
+        VALUES (#{fromUserId}, #{toUserId}, #{itemId}, #{content}, #{imageUrl}, #{isRead})
+    </insert>
+    <update id="markAsRead">
+        UPDATE message SET is_read = 1
+        WHERE to_user_id = #{toUserId} AND from_user_id = #{fromUserId}
+    </update>
+</mapper>

+ 21 - 0
src/main/resources/mapper/OrderMapper.xml

@@ -0,0 +1,21 @@
+<?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.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>
+    <select id="selectById" resultType="Order">
+        SELECT * FROM `order` WHERE id = #{id}
+    </select>
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO `order` (item_id, buyer_id, seller_id, trade_method, status)
+        VALUES (#{itemId}, #{buyerId}, #{sellerId}, #{tradeMethod}, #{status})
+    </insert>
+    <update id="updateStatus">
+        UPDATE `order` SET status = #{status} WHERE id = #{id}
+    </update>
+</mapper>

+ 13 - 0
src/main/resources/mapper/ReviewMapper.xml

@@ -0,0 +1,13 @@
+<?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.ReviewMapper">
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO review (admin_id, review_type, target_id, result, reason)
+        VALUES (#{adminId}, #{reviewType}, #{targetId}, #{result}, #{reason})
+    </insert>
+    <select id="selectByTypeAndTarget" resultType="Review">
+        SELECT * FROM review WHERE review_type = #{reviewType} AND target_id = #{targetId}
+        ORDER BY created_at DESC
+    </select>
+</mapper>

+ 36 - 0
src/main/resources/mapper/UserMapper.xml

@@ -0,0 +1,36 @@
+<?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>
+    <select id="selectById" resultType="User">
+        SELECT * FROM user WHERE id = #{id}
+    </select>
+    <select id="selectAll" resultType="User">
+        SELECT * FROM user ORDER BY created_at DESC
+    </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>
+    <update id="updateProfile">
+        UPDATE user SET nickname = #{nickname}, avatar_url = #{avatarUrl}, signature = #{signature}
+        WHERE id = #{id}
+    </update>
+    <update id="updateStatus">
+        UPDATE user SET status = #{status} WHERE id = #{id}
+    </update>
+</mapper>