校园二手书交易系统 — 服务端详细设计文档
版本: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<T> 响应 |
| Service 接口 |
业务逻辑抽象,继承 IService 获得通用 CRUD |
| Service 实现 |
核心业务逻辑、事务控制(@Transactional)、组合 Mapper 调用 |
| Mapper |
BaseMapper 扩展提供 CRUD,@Select 自定义 SQL 提供统计查询 |
1.2 依赖关系
Controller → Service → Mapper → MySQL
所有 API 返回统一响应格式 R<T>(定义在 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 客户端直连服务端。
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
2.2 MyBatisPlusConfig.java — 分页插件
注册 PaginationInnerInterceptor(DbType.MYSQL),自动拦截并改写分页 SQL。
配合 application.yml 配置:
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<T> — 统一响应
{"code": 200, "message": "success", "data": ...}
| 静态方法 |
说明 |
R.ok(data) |
成功,code=200 |
R.ok() |
成功无数据 |
R.error(message) |
错误,code=400 |
R.error(code, message) |
自定义错误码 |
PageResult<T> — 分页结果
{"records": [...], "total": 100, "size": 20, "current": 1}
LoginRequest
{"username": "string", "password": "string"}
RegisterRequest
{"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<User> |
登录认证 |
| POST |
/api/user/register |
RegisterRequest JSON |
R<User> |
注册新用户 |
| GET |
/api/user |
?page=&size= |
R<IPage<User>> |
分页获取用户列表(Admin) |
| GET |
/api/user/{id} |
|
R<User> |
获取单个用户 |
| PUT |
/api/user/{id} |
User JSON |
R<User> |
更新用户信息 |
| DELETE |
/api/user/{id} |
|
R<Void> |
删除用户(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<PageResult<Book>> |
搜索书籍(仅返回 status=1) |
| GET |
/api/book/all |
|
R<List<Book>> |
获取所有书籍(不分页) |
| GET |
/api/book/{id} |
|
R<Book> |
获取书籍详情 |
| GET |
/api/book/my |
sellerId |
R<List<Book>> |
卖家查看自己的书籍 |
| GET |
/api/book/{id}/sales |
|
R<List<SalesRecordDTO>> |
查看书籍销售记录(卖家) |
| GET |
/api/book/pending |
|
R<List<Book>> |
查看待审核书籍列表(Admin) |
| POST |
/api/book |
Book JSON |
R<Book> |
创建/上架书籍(默认 status=0) |
| PUT |
/api/book/{id} |
Book JSON |
R<Book> |
更新书籍 |
| PUT |
/api/book/{id}/audit |
?status= |
R<Book> |
审核书籍(Admin:0 待审/1 通过/2 拒绝) |
| DELETE |
/api/book/{id} |
|
R<Void> |
删除书籍 |
Controller: BookController.java
search(): 调用 bookService.searchBooks(),返回 PageResult<Book>
listAll(): 返回所有书籍(按创建时间降序)
getById(): 按主键查询
getMyBooks(): 按 sellerId 查询
getSales(): JOIN 查询销售记录,返回 List<SalesRecordDTO>
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<TransactionRecord> |
购买(需事务) |
| GET |
/api/transaction |
?page=&size= |
R<IPage<TransactionRecord>> |
分页查询所有交易 |
| GET |
/api/transaction/{id} |
|
R<TransactionRecord> |
交易详情 |
| GET |
/api/transaction/user/{userId} |
|
R<List<TransactionRecord>> |
用户交易记录 |
| PUT |
/api/transaction/{id} |
TransactionRecord JSON |
R<TransactionRecord> |
更新交易(预留) |
| DELETE |
/api/transaction/{id} |
|
R<Void> |
删除交易 |
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<StatisticsSummary> |
交易总金额和总笔数 |
| GET |
/api/statistics/top-departments |
R<List<DeptSalesDTO>> |
销售额前三的院系 |
| GET |
/api/statistics/top-books |
R<List<HotBookDTO>> |
销量前五的书籍 |
| GET |
/api/statistics/discount-rates |
R<List<DeptDiscountDTO>> |
各院系平均折扣率 |
Controller: StatisticsController.java — 直接委托 StatisticsService 的四个方法。
4.5 AI 分析模块 — /api/ai
| 方法 |
路径 |
响应 |
说明 |
| GET |
/api/ai/analysis |
R<String> |
触发服务端固定分析流程,返回文本分析报告 |
Controller: AiController.java — 调用 aiService.generateAnalysis()。
Service 实现: AiServiceImpl.java
流程:
- 调用
StatisticsService 获取全部统计指标(summary、topDepartments、topBooks、discountRates)
- 格式化拼接为纯文本分析报告
- 报告包含:交易总览、热门院系 Top 3、热门书籍 Top 5、各院系折扣率、分析结论
5. 核心业务逻辑
5.1 注册流程
- Controller 接收
RegisterRequest
- 构造
User 对象,设置默认值:credit=100, role=0, isSeller=0, createdAt=now
- 调用
userService.register() → save(user)
- 返回
R.ok(user)(含自增 ID)
服务端代码位置: UserServiceImpl.register()
5.2 登录流程
- Controller 接收
LoginRequest
- 调用
userService.login(username, password)
- 构建
LambdaQueryWrapper:eq(username) AND eq(password)
- 匹配则返回
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 搜索书籍流程
- Controller 接收 keyword、category、page、size
- 构建
LambdaQueryWrapper<Book>:
- 固定条件:
eq(Book::getStatus, 1)(仅返回已上架书籍)
- keyword 非空 →
like(title, keyword) OR like(author, keyword)
- category 非空 →
eq(category)
- 排序:
orderByDesc(createdAt)
- 调用
page(new Page<>(page, size), wrapper) 分页查询
- 转换为
PageResult<Book> 返回
服务端代码位置: BookServiceImpl.searchBooks()
6. 数据访问层
6.1 Mapper 结构
| Mapper |
父类 |
自定义方法 |
SQL 类型 |
UserMapper |
BaseMapper<User> |
无 |
— |
BookMapper |
BaseMapper<Book> |
selectSalesByBookId() |
@Select JOIN 查询 |
TransactionRecordMapper |
BaseMapper<TransactionRecord> |
无 |
— |
StatisticsMapper |
无(不是实体 Mapper) |
4 个统计方法 |
全部 @Select 自定义 SQL |
6.2 自定义 SQL
BookMapper.selectSalesByBookId()
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:
{
"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<Book> → 转换为 PageResult<Book>
Client → FXCollections.observableArrayList(records) → TableView
分页查询步骤:
- Controller 接收 page 和 size 参数(默认 page=1, size=20)
- 构建
Page<T> 对象 + LambdaQueryWrapper 条件
- MyBatis-Plus 分页插件自动添加
LIMIT 和 COUNT 子句
- 返回
IPage<T>,包含 records、total、size、current
8. 运行配置
8.1 application.yml
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 构建与运行
# 全量构建
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<User>,书籍模块使用 PageResult<Book>,建议统一
@Transactional 仅应用于购买流程,其他批量操作未加事务
- 服务端未实现权限拦截,所有 API 均可被任意客户端调用