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