# 校园二手书交易系统 — 服务端详细设计文档 > 版本:1.0 > 对应设计:`design.md` 第 4 章 --- ## 1. 分层架构 ``` space.anyi.server ├── ServerApplication.java — Spring Boot 入口 ├── config/ — 配置层 │ ├── WebConfig.java — CORS 跨域配置 │ ├── MyBatisPlusConfig.java — MyBatis-Plus 分页插件 │ └── GlobalExceptionHandler.java — 全局异常处理 ├── controller/ — Controller 层(REST 接口) │ ├── UserController.java │ ├── BookController.java │ ├── TransactionController.java │ ├── StatisticsController.java │ └── AiController.java ├── service/ — Service 接口层 │ ├── UserService.java │ ├── BookService.java │ ├── TransactionService.java │ ├── StatisticsService.java │ └── AiService.java ├── service/impl/ — Service 实现层 │ ├── UserServiceImpl.java │ ├── BookServiceImpl.java │ ├── TransactionServiceImpl.java │ ├── StatisticsServiceImpl.java │ └── AiServiceImpl.java └── mapper/ — Mapper 层(数据访问) ├── UserMapper.java ├── BookMapper.java ├── TransactionRecordMapper.java └── StatisticsMapper.java ``` ### 1.1 各层职责 | 层次 | 职责 | |------|------| | **Config** | CORS 跨域、MyBatis-Plus 分页拦截器、全局异常处理 | | **Controller** | `@RestController`,请求映射、参数解析、调用 Service 并封装 `R` 响应 | | **Service 接口** | 业务逻辑抽象,继承 `IService` 获得通用 CRUD | | **Service 实现** | 核心业务逻辑、事务控制(`@Transactional`)、组合 Mapper 调用 | | **Mapper** | `BaseMapper` 扩展提供 CRUD,`@Select` 自定义 SQL 提供统计查询 | ### 1.2 依赖关系 ``` Controller → Service → Mapper → MySQL ``` 所有 API 返回统一响应格式 `R`(定义在 `common` 模块)。 ### 1.3 Maven 依赖(server/pom.xml) | 依赖 | 说明 | |------|------| | `spring-boot-starter-web` | Spring Boot Web 容器 | | `mybatis-plus-spring-boot3-starter` | MyBatis-Plus ORM(适配 Spring Boot 3) | | `mybatis-plus-jsqlparser` | MyBatis-Plus SQL 解析器(分页插件依赖) | | `mysql-connector-j` | MySQL JDBC 驱动 | | `lombok` | 编译期注解生成 getter/setter | | `common` | 项目共享模块(实体、DTO、枚举) | --- ## 2. 配置层 ### 2.1 WebConfig.java — CORS 跨域 允许所有来源的跨域请求,用于开发环境 JavaFX 客户端直连服务端。 ```java registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true); ``` ### 2.2 MyBatisPlusConfig.java — 分页插件 注册 `PaginationInnerInterceptor(DbType.MYSQL)`,自动拦截并改写分页 SQL。 配合 `application.yml` 配置: ```yaml mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto ``` ### 2.3 GlobalExceptionHandler.java — 全局异常处理 | 异常 | 响应 | |------|------| | `DuplicateKeyException` | `R.error("用户名已存在")` → code=400 | | `IllegalArgumentException` | `R.error(e.getMessage())` → code=400 | | `Exception`(兜底) | `R.error("服务器内部错误: " + e.getMessage())` → code=500 | --- ## 3. 公共模块(common) ### 3.1 实体类(`space.anyi.common.entity`) #### User | 字段 | 类型 | 说明 | |------|------|------| | `userid` | Integer | PK,自增 | | `username` | String | 用户名 | | `password` | String | 密码 | | `nickname` | String | 昵称 | | `email` | String | 邮箱 | | `phone` | String | 电话 | | `dept` | String | 院系 | | `credit` | Integer | 信用分,默认 100 | | `role` | Integer | 角色(0=学生, 1=管理员) | | `isSeller` | Integer | `@TableField("is_seller")`,是否为卖家(0=否, 1=是) | | `createdAt` | LocalDateTime | 注册时间 | #### Book | 字段 | 类型 | 说明 | |------|------|------| | `bookId` | Integer | PK,自增 | | `sellerId` | Integer | 卖家 ID(逻辑外键 → user.userid) | | `title` | String | 书名 | | `author` | String | 作者 | | `category` | String | 分类 | | `originalPrice` | BigDecimal | 原价 | | `sellingPrice` | BigDecimal | 售价 | | `imageUrl` | String | 图片 URL(预留) | | `createdAt` | LocalDateTime | 上架时间 | | `stock` | Integer | 库存数量 | | `status` | Integer | `@TableField("status")`,0=待审核, 1=已上架, 2=已下架 | #### TransactionRecord | 字段 | 类型 | 说明 | |------|------|------| | `transId` | Integer | PK,自增 | | `bookId` | Integer | 书籍 ID(逻辑外键 → book.book_id) | | `buyerId` | Integer | 买家 ID(逻辑外键 → user.userid) | | `transactionNum` | Integer | 购买数量 | | `transactionPrice` | BigDecimal | 交易总价(= sellingPrice × quantity) | | `transactionTime` | LocalDateTime | 交易时间 | ### 3.2 DTO 类(`space.anyi.common.dto`) #### R\ — 统一响应 ```json {"code": 200, "message": "success", "data": ...} ``` | 静态方法 | 说明 | |----------|------| | `R.ok(data)` | 成功,code=200 | | `R.ok()` | 成功无数据 | | `R.error(message)` | 错误,code=400 | | `R.error(code, message)` | 自定义错误码 | #### PageResult\ — 分页结果 ```json {"records": [...], "total": 100, "size": 20, "current": 1} ``` #### LoginRequest ```json {"username": "string", "password": "string"} ``` #### RegisterRequest ```json {"username": "string", "password": "string", "nickname": "string", "email": "string", "phone": "string", "dept": "string"} ``` #### SalesRecordDTO | 字段 | 类型 | 说明 | |------|------|------| | `bookId` | Integer | 书籍ID | | `title` | String | 书名 | | `buyerId` | Integer | 买家ID | | `buyerName` | String | 买家昵称(JOIN user 表) | | `quantity` | Integer | 购买数量 | | `totalPrice` | BigDecimal | 成交金额 | | `transactionTime` | LocalDateTime | 交易时间 | #### StatisticsSummary | 字段 | 类型 | 说明 | |------|------|------| | `totalAmount` | BigDecimal | 交易总金额 | | `totalCount` | long | 交易总笔数 | #### DeptSalesDTO | 字段 | 类型 | 说明 | |------|------|------| | `dept` | String | 院系名称 | | `salesAmount` | BigDecimal | 销售额 | #### HotBookDTO | 字段 | 类型 | 说明 | |------|------|------| | `bookId` | Integer | 书籍ID | | `title` | String | 书名 | | `author` | String | 作者 | | `totalSold` | int | 总销量 | #### DeptDiscountDTO | 字段 | 类型 | 说明 | |------|------|------| | `dept` | String | 院系名称 | | `avgDiscountRate` | BigDecimal | 平均折扣率(百分比) | ### 3.3 枚举类(`space.anyi.common.enums`) #### UserRole | 常量 | 值 | 说明 | |------|:----:|------| | `STUDENT` | 0 | 学生 | | `ADMIN` | 1 | 管理员 | 提供 `fromValue(int)` 工厂方法。 --- ## 4. RESTful API 接口定义 所有接口基路径:`/api`(通过 `server.servlet.context-path` 配置) ### 4.1 用户模块 — `/api/user` | 方法 | 路径 | 请求 | 响应 | 说明 | |------|------|------|------|------| | POST | `/api/user/login` | `LoginRequest` JSON | `R` | 登录认证 | | POST | `/api/user/register` | `RegisterRequest` JSON | `R` | 注册新用户 | | GET | `/api/user` | `?page=&size=` | `R>` | 分页获取用户列表(Admin) | | GET | `/api/user/{id}` | | `R` | 获取单个用户 | | PUT | `/api/user/{id}` | `User` JSON | `R` | 更新用户信息 | | DELETE | `/api/user/{id}` | | `R` | 删除用户(Admin) | **Controller:** `UserController.java` - `login()`: 调用 `userService.login(username, password)`,匹配则返回 User,否则 `R.error` - `register()`: 创建 User 对象,调用 `userService.register(user)` - `listAll()`: MyBatis-Plus `page()` 分页查询 - `getById()`: 按主键查询,不存在则返回错误 - `update()`: 设置 userId 后 `updateById()`,再返回最新数据 - `delete()`: `removeById()` 物理删除 ### 4.2 书籍模块 — `/api/book` | 方法 | 路径 | 参数 | 响应 | 说明 | |------|------|------|------|------| | GET | `/api/book` | keyword, category, page, size | `R>` | 搜索书籍(仅返回 status=1) | | GET | `/api/book/all` | | `R>` | 获取所有书籍(不分页) | | GET | `/api/book/{id}` | | `R` | 获取书籍详情 | | GET | `/api/book/my` | sellerId | `R>` | 卖家查看自己的书籍 | | GET | `/api/book/{id}/sales` | | `R>` | 查看书籍销售记录(卖家) | | GET | `/api/book/pending` | | `R>` | 查看待审核书籍列表(Admin) | | POST | `/api/book` | `Book` JSON | `R` | 创建/上架书籍(默认 status=0) | | PUT | `/api/book/{id}` | `Book` JSON | `R` | 更新书籍 | | PUT | `/api/book/{id}/audit` | `?status=` | `R` | 审核书籍(Admin:0 待审/1 通过/2 拒绝) | | DELETE | `/api/book/{id}` | | `R` | 删除书籍 | **Controller:** `BookController.java` - `search()`: 调用 `bookService.searchBooks()`,返回 `PageResult` - `listAll()`: 返回所有书籍(按创建时间降序) - `getById()`: 按主键查询 - `getMyBooks()`: 按 sellerId 查询 - `getSales()`: JOIN 查询销售记录,返回 `List` - `getPendingBooks()`: 查询 `status=0` 的书籍 - `create()`: 设置 `createdAt=now`, `status=0`,保存后返回 - `update()`: 设置 bookId 后 `updateById()` - `audit()`: 查询书籍 → 设置新 status → 更新 - `delete()`: `removeById()` 物理删除 **Service 方法:** `BookServiceImpl.java` | 方法 | 说明 | |------|------| | `searchBooks(keyword, category, page, size)` | 动态条件:keyword 模糊匹配 title 或 author;category 精确匹配;仅返回 status=1;按 created_at DESC 排序 | | `getBySellerId(sellerId)` | 按卖家 ID 查询,按创建时间降序 | | `listAll()` | 返回所有书籍,按创建时间降序 | | `getSalesByBookId(bookId)` | 委托 `bookMapper.selectSalesByBookId()` JOIN 查询 | | `getPendingBooks()` | 查询 `status=0` 的书籍 | ### 4.3 交易模块 — `/api/transaction` | 方法 | 路径 | 请求 | 响应 | 说明 | |------|------|------|------|------| | POST | `/api/transaction` | `{bookId, buyerId, quantity}` | `R` | 购买(需事务) | | GET | `/api/transaction` | `?page=&size=` | `R>` | 分页查询所有交易 | | GET | `/api/transaction/{id}` | | `R` | 交易详情 | | GET | `/api/transaction/user/{userId}` | | `R>` | 用户交易记录 | | PUT | `/api/transaction/{id}` | `TransactionRecord` JSON | `R` | 更新交易(预留) | | DELETE | `/api/transaction/{id}` | | `R` | 删除交易 | **Controller:** `TransactionController.java` - `purchase()`: 从 Map 中提取 bookId/buyerId/quantity,调用 `transactionService.purchase()`,捕获运行时异常返回错误 - `listAll()`: MyBatis-Plus 分页查询 - `getById()`: 按主键查询 - `getByUser()`: 调用 `transactionService.getByUserId()` - `update()`/`delete()`: MyBatis-Plus 通用操作 ### 4.4 统计模块 — `/api/statistics` | 方法 | 路径 | 响应 | 说明 | |------|------|------|------| | GET | `/api/statistics/summary` | `R` | 交易总金额和总笔数 | | GET | `/api/statistics/top-departments` | `R>` | 销售额前三的院系 | | GET | `/api/statistics/top-books` | `R>` | 销量前五的书籍 | | GET | `/api/statistics/discount-rates` | `R>` | 各院系平均折扣率 | **Controller:** `StatisticsController.java` — 直接委托 `StatisticsService` 的四个方法。 ### 4.5 AI 分析模块 — `/api/ai` | 方法 | 路径 | 响应 | 说明 | |------|------|------|------| | GET | `/api/ai/analysis` | `R` | 触发服务端固定分析流程,返回文本分析报告 | **Controller:** `AiController.java` — 调用 `aiService.generateAnalysis()`。 **Service 实现:** `AiServiceImpl.java` 流程: 1. 调用 `StatisticsService` 获取全部统计指标(summary、topDepartments、topBooks、discountRates) 2. 格式化拼接为纯文本分析报告 3. 报告包含:交易总览、热门院系 Top 3、热门书籍 Top 5、各院系折扣率、分析结论 --- ## 5. 核心业务逻辑 ### 5.1 注册流程 1. Controller 接收 `RegisterRequest` 2. 构造 `User` 对象,设置默认值:`credit=100`, `role=0`, `isSeller=0`, `createdAt=now` 3. 调用 `userService.register()` → `save(user)` 4. 返回 `R.ok(user)`(含自增 ID) **服务端代码位置:** `UserServiceImpl.register()` ### 5.2 登录流程 1. Controller 接收 `LoginRequest` 2. 调用 `userService.login(username, password)` 3. 构建 `LambdaQueryWrapper`:`eq(username) AND eq(password)` 4. 匹配则返回 `R.ok(user)`,否则返回 `R.error("Invalid username or password")` **服务端代码位置:** `UserServiceImpl.login()` ### 5.3 购买流程(事务) ``` @Transactional 开启事务 1. bookService.getById(bookId) → 查询书籍 2. 校验 stock >= quantity,否则抛异常回滚 3. book.setStock(stock - quantity) → bookService.updateById() 4. 创建 TransactionRecord: - bookId, buyerId, transactionNum - transactionPrice = sellingPrice × quantity - transactionTime = now 5. save(record) 提交事务 ``` **事务边界:** `TransactionServiceImpl.purchase()` 方法标注 `@Transactional` ### 5.4 搜索书籍流程 1. Controller 接收 keyword、category、page、size 2. 构建 `LambdaQueryWrapper`: - 固定条件:`eq(Book::getStatus, 1)`(仅返回已上架书籍) - keyword 非空 → `like(title, keyword) OR like(author, keyword)` - category 非空 → `eq(category)` - 排序:`orderByDesc(createdAt)` 3. 调用 `page(new Page<>(page, size), wrapper)` 分页查询 4. 转换为 `PageResult` 返回 **服务端代码位置:** `BookServiceImpl.searchBooks()` --- ## 6. 数据访问层 ### 6.1 Mapper 结构 | Mapper | 父类 | 自定义方法 | SQL 类型 | |--------|------|-----------|----------| | `UserMapper` | `BaseMapper` | 无 | — | | `BookMapper` | `BaseMapper` | `selectSalesByBookId()` | `@Select` JOIN 查询 | | `TransactionRecordMapper` | `BaseMapper` | 无 | — | | `StatisticsMapper` | 无(不是实体 Mapper) | 4 个统计方法 | 全部 `@Select` 自定义 SQL | ### 6.2 自定义 SQL #### BookMapper.selectSalesByBookId() ```sql SELECT b.book_id, b.title, t.buyer_id, u.nickname AS buyerName, t.transaction_num AS quantity, t.transaction_price AS totalPrice, t.transaction_time FROM transaction_record t JOIN book b ON t.book_id = b.book_id JOIN user u ON t.buyer_id = u.userid WHERE b.book_id = #{bookId} ORDER BY t.transaction_time DESC ``` #### StatisticsMapper | 方法 | SQL | |------|-----| | `selectSummary()` | `SELECT COALESCE(SUM(transaction_price), 0), COUNT(*) FROM transaction_record` | | `selectTopDepartments()` | JOIN `transaction_record` + `book` + `user`,按 `user.dept` 分组,销售额前 3 | | `selectTopBooks()` | JOIN `transaction_record` + `book`,按 `book` 分组,销量前 5 | | `selectDeptDiscountRates()` | `AVG((original_price - unit_price) / original_price * 100)`,按 `dept` 分组 | --- ## 7. 统一响应与分页 ### 7.1 响应格式 所有 API 返回统一 JSON: ```json { "code": 200, "message": "success", "data": { ... } } ``` | code | 含义 | |:----:|------| | 200 | 成功 | | 400 | 业务错误(参数错误、库存不足、用户名已存在等) | | 500 | 服务器内部错误 | ### 7.2 分页机制 ``` Client → GET /api/book?keyword=&category=&page=1&size=20 Server → MyBatis-Plus Page + LambdaQueryWrapper → PaginationInnerInterceptor 自动拦截改写 SQL → 返回 IPage → 转换为 PageResult Client → FXCollections.observableArrayList(records) → TableView ``` 分页查询步骤: 1. Controller 接收 page 和 size 参数(默认 page=1, size=20) 2. 构建 `Page` 对象 + `LambdaQueryWrapper` 条件 3. MyBatis-Plus 分页插件自动添加 `LIMIT` 和 `COUNT` 子句 4. 返回 `IPage`,包含 records、total、size、current --- ## 8. 运行配置 ### 8.1 application.yml ```yaml server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/campus_book_trade?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto ``` ### 8.2 启动入口 `ServerApplication.java`: - `@SpringBootApplication` + `@MapperScan("space.anyi.server.mapper")` - 运行 `main()` 监听 8080 端口 ### 8.3 构建与运行 ```sh # 全量构建 JAVA_HOME=/home/yangyi/.jdks/corretto-23.0.2 mvn clean install # 单独启动服务端 mvn spring-boot:run -pl server ``` --- ## 9. 已实现功能清单与代码位置 ### 9.1 基础功能 | 功能 | Controller | Service 实现 | Mapper | |------|-----------|-------------|--------| | 登录 | `UserController.java:22` | `UserServiceImpl.java:15` | `UserMapper` (BaseMapper) | | 注册 | `UserController.java:31` | `UserServiceImpl.java:22` | `UserMapper` (BaseMapper) | | 用户 CRUD | `UserController.java:44-70` | `ServiceImpl` 继承 | `UserMapper` (BaseMapper) | | 书籍搜索 | `BookController.java:22` | `BookServiceImpl.java:24` | `BookMapper` (BaseMapper) | | 书籍 CRUD | `BookController.java:38-98` | `ServiceImpl` 继承 | `BookMapper` (BaseMapper) | | 购买交易 | `TransactionController.java:22` | `TransactionServiceImpl.java:26` | `TransactionRecordMapper` + `BookMapper` | | 交易 CRUD | `TransactionController.java:35-66` | `ServiceImpl` 继承 | `TransactionRecordMapper` (BaseMapper) | | 统计数据 | `StatisticsController.java` | `StatisticsServiceImpl.java` | `StatisticsMapper.java`(4 个 @Select) | ### 9.2 扩展功能 | 功能 | Controller | Service 实现 | Mapper | |------|-----------|-------------|--------| | 销售记录 | `BookController.java:56` | `BookServiceImpl.java:52` | `BookMapper.selectSalesByBookId()` | | 待审核书籍 | `BookController.java:61` | `BookServiceImpl.java:57` | `BookMapper` (BaseMapper) | | 审核书籍 | `BookController.java:66` | —(直接在 Controller 中操作) | `BookMapper` (BaseMapper) | | AI 分析 | `AiController.java` | `AiServiceImpl.java` | —(依赖 StatisticsService) | --- ## 10. 设计补充说明 ### 10.1 数据库约束 - 未显式声明外键约束,由应用层保证数据一致性 - `transaction_record` 和 `book` 的 `price` 字段使用 `decimal(10,2)` 确保精度 ### 10.2 安全性 - 当前无 Token/JWT 机制,userId 通过请求体或参数明文传输 - 密码明文存储,后续建议引入加密和 Spring Security ### 10.3 待扩展 - 分页返回类型不一致:用户模块使用 `IPage`,书籍模块使用 `PageResult`,建议统一 - `@Transactional` 仅应用于购买流程,其他批量操作未加事务 - 服务端未实现权限拦截,所有 API 均可被任意客户端调用