整体项目模块结构
graph TD
subgraph "CampusUsedBookTradingSystem (父POM)"
direction TB
common[common 模块<br/>space.anyi.common<br/>实体 / DTO / 枚举]
server[server 模块<br/>space.anyi.server<br/>Spring Boot REST API]
client[client 模块<br/>space.anyi.client<br/>JavaFX 桌面客户端]
end
common -->|依赖| server
common -->|依赖| client
server -.->|HTTP JSON| client
服务器端目录树
graph TD
subgraph "server/src/main"
direction TB
java["java/space/anyi/server"]
resources["resources"]
end
subgraph "java 层"
direction TB
entry["ServerApplication.java"]
config["config/"]
controller["controller/"]
service["service/"]
impl["service/impl/"]
mapper["mapper/"]
end
subgraph "config 配置层"
wc["WebConfig.java<br/>CORS跨域"]
mpc["MyBatisPlusConfig.java<br/>分页拦截器"]
geh["GlobalExceptionHandler.java<br/>全局异常处理"]
end
subgraph "controller 控制器层"
uc["UserController<br/>用户CRUD"]
bc["BookController<br/>图书CRUD+审核"]
tc["TransactionController<br/>交易CRUD"]
sc["StatisticsController<br/>统计分析"]
ac["AiController<br/>AI报告生成"]
end
subgraph "service 业务层"
us["UserService/UserServiceImpl<br/>登录/注册"]
bs["BookService/BookServiceImpl<br/>图书搜索/查询"]
ts["TransactionService/TransactionServiceImpl<br/>购买事务"]
ss["StatisticsService/StatisticsServiceImpl<br/>聚合统计"]
as["AiService/AiServiceImpl<br/>AI分析+流式生成"]
end
subgraph "mapper 持久层"
um["UserMapper<br/>基础CRUD"]
bm["BookMapper<br/>自定义@Select联表"]
tm["TransactionRecordMapper<br/>分页+联表"]
sm["StatisticsMapper<br/>SUM/GROUP统计"]
end
subgraph "resources 资源"
yml["application.yml<br/>端口/数据库/AI配置"]
xml["mapper/*.xml<br/>空壳XML"]
end
java --> config
java --> controller
java --> service
java --> impl
java --> mapper
config --> wc
config --> mpc
config --> geh
controller --> uc
controller --> bc
controller --> tc
controller --> sc
controller --> ac
service --> us
service --> bs
service --> ts
service --> ss
service --> as
impl --> us
impl --> bs
impl --> ts
impl --> ss
impl --> as
mapper --> um
mapper --> bm
mapper --> tm
mapper --> sm
resources --> yml
resources --> xml
@MapperScan("space.anyi.server.mapper")
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
WebConfig — CORS跨域配置
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}
MyBatisPlusConfig — 分页拦截器
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
GlobalExceptionHandler — 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateKeyException.class)
public R<Void> handleDuplicateKey(DuplicateKeyException e) {
return R.error("用户名已存在");
}
@ExceptionHandler(IllegalArgumentException.class)
public R<Void> handleIllegalArgument(IllegalArgumentException e) {
return R.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) {
return R.error("服务器内部错误: " + e.getMessage());
}
}
UserController — 登录注册端点
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) { this.userService = userService; }
@PostMapping("/login")
public R<User> login(@RequestBody LoginRequest req) {
User user = userService.login(req.getUsername(), req.getPassword());
if (user == null) return R.error("Invalid username or password");
return R.ok(user);
}
@PostMapping("/register")
public R<User> register(@RequestBody RegisterRequest req) {
User user = new User();
user.setUsername(req.getUsername());
user.setPassword(req.getPassword());
user.setNickname(req.getNickname());
user.setEmail(req.getEmail());
user.setPhone(req.getPhone());
user.setDept(req.getDept());
user.setRole(req.getRole());
user.setIsSeller(req.getIsSeller());
User created = userService.register(user);
return R.ok(created);
}
@GetMapping("/{id}") // 查询单个用户
@PutMapping("/{id}") // 更新用户
@DeleteMapping("/{id}") // 删除用户
@GetMapping // 分页列出所有用户
// ...省略
}
BookController — 图书搜索与CRUD
@RestController
@RequestMapping("/book")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) { this.bookService = bookService; }
@GetMapping
public R<PageResult<Book>> search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
var pageResult = bookService.searchBooks(keyword, category, page, size);
PageResult<Book> pr = new PageResult<>();
pr.setRecords(pageResult.getRecords());
pr.setTotal(pageResult.getTotal());
pr.setSize(pageResult.getSize());
pr.setCurrent(pageResult.getCurrent());
return R.ok(pr);
}
@GetMapping("/my")
public R<List<Book>> getMyBooks(@RequestParam Integer sellerId) {
return R.ok(bookService.getBySellerId(sellerId));
}
@PostMapping
public R<Book> create(@RequestBody Book book) {
book.setCreatedAt(LocalDateTime.now());
if (book.getStatus() == null) book.setStatus(0);
bookService.save(book);
return R.ok(book);
}
@PutMapping("/{id}/audit")
public R<Book> audit(@PathVariable Integer id, @RequestParam Integer status) {
Book book = bookService.getById(id);
if (book == null) return R.error("Book not found");
book.setStatus(status);
bookService.updateById(book);
return R.ok(book);
}
// GET /{id}, PUT /{id}, DELETE /{id}, GET /all, GET /{id}/sales ...省略
}
TransactionController — 购买与交易记录
@RestController
@RequestMapping("/transaction")
public class TransactionController {
private final TransactionService transactionService;
public TransactionController(TransactionService transactionService) { this.transactionService = transactionService; }
@PostMapping
public R<TransactionRecord> purchase(@RequestBody Map<String, Object> body) {
Integer bookId = (Integer) body.get("bookId");
Integer buyerId = (Integer) body.get("buyerId");
Integer quantity = (Integer) body.getOrDefault("quantity", 1);
try {
TransactionRecord record = transactionService.purchase(bookId, buyerId, quantity);
return R.ok(record);
} catch (RuntimeException e) {
return R.error(e.getMessage());
}
}
@GetMapping("/user/{userId}/detail")
public R<List<UserTransactionDTO>> getByUserDetail(@PathVariable Integer userId) {
return R.ok(transactionService.getByUserIdWithTitle(userId));
}
// GET /{id}, GET /user/{userId}, PUT /{id}, DELETE /{id} ...省略
}
StatisticsController — 统计分析
@RestController
@RequestMapping("/statistics")
public class StatisticsController {
private final StatisticsService statisticsService;
public StatisticsController(StatisticsService statisticsService) { this.statisticsService = statisticsService; }
@GetMapping("/summary") // {totalAmount, totalCount}
public R<StatisticsSummary> getSummary() { return R.ok(statisticsService.getSummary()); }
@GetMapping("/top-departments") // Top3 院系交易额
public R<List<DeptSalesDTO>> getTopDepartments() { return R.ok(statisticsService.getTopDepartments()); }
@GetMapping("/top-books") // Top5 热门书籍
public R<List<HotBookDTO>> getTopBooks() { return R.ok(statisticsService.getTopBooks()); }
@GetMapping("/discount-rates") // 各院系平均折扣率
public R<List<DeptDiscountDTO>> getDiscountRates() { return R.ok(statisticsService.getDeptDiscountRates()); }
}
AiController — AI分析报告
@RestController
@RequestMapping("/ai")
public class AiController {
private final AiService aiService;
public AiController(AiService aiService) { this.aiService = aiService; }
@GetMapping("/analysis")
public R<String> getAnalysis() {
String report = aiService.generateAnalysis();
return R.ok(report);
}
@GetMapping("/analysis/stream")
public ResponseBodyEmitter streamAnalysis() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter(180_000L);
Flux<String> flux = aiService.streamAnalysis();
flux.subscribe(
chunk -> { try { emitter.send(chunk, MediaType.TEXT_PLAIN); }
catch (Exception e) { emitter.completeWithError(e); }},
emitter::completeWithError,
emitter::complete
);
return emitter;
}
}
UserServiceImpl — 登录注册业务
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User login(String username, String password) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username).eq(User::getPassword, password);
return getOne(wrapper);
}
@Override
public User register(User user) {
user.setCredit(100);
if (user.getRole() == null) user.setRole(0);
if (user.getIsSeller() == null) user.setIsSeller(0);
user.setCreatedAt(LocalDateTime.now());
save(user);
return user;
}
}
TransactionServiceImpl — 购买事务(含库存扣减)
@Service
public class TransactionServiceImpl
extends ServiceImpl<TransactionRecordMapper, TransactionRecord>
implements TransactionService {
private final BookService bookService;
@Override
@Transactional
public TransactionRecord purchase(Integer bookId, Integer buyerId, Integer quantity) {
Book book = bookService.getById(bookId);
if (book == null) throw new RuntimeException("书籍不存在");
if (book.getStock() < quantity) throw new RuntimeException("库存不足");
book.setStock(book.getStock() - quantity);
bookService.updateById(book);
TransactionRecord record = new TransactionRecord();
record.setBookId(bookId);
record.setBuyerId(buyerId);
record.setTransactionNum(quantity);
record.setTransactionPrice(book.getSellingPrice()
.multiply(BigDecimal.valueOf(quantity)));
record.setTransactionTime(LocalDateTime.now());
save(record);
return record;
}
}
AiServiceImpl — AI分析报告生成
@Service
public class AiServiceImpl implements AiService {
private final StatisticsService statisticsService;
private final ChatClient chatClient;
public AiServiceImpl(StatisticsService statisticsService, ChatClient.Builder builder) {
this.statisticsService = statisticsService;
this.chatClient = builder.build();
}
@Override
public String generateAnalysis() {
StatisticsSummary summary = statisticsService.getSummary();
List<DeptSalesDTO> topDepts = statisticsService.getTopDepartments();
List<HotBookDTO> topBooks = statisticsService.getTopBooks();
List<DeptDiscountDTO> discounts = statisticsService.getDeptDiscountRates();
StringBuilder report = new StringBuilder();
report.append("========== 校园二手书交易系统分析报告 ==========\n\n");
// ... 格式化输出交易总览、热门院系、热门书籍、折扣率、分析结论
return report.toString();
}
@Override
public Flux<String> streamAnalysis() {
// 构造Prompt,调用ChatClient流式生成
return chatClient.prompt().user(buildPrompt(...)).stream().content();
}
}
StatisticsMapper — 聚合统计SQL
public interface StatisticsMapper {
@Select("SELECT COALESCE(SUM(transaction_price), 0) AS totalAmount, " +
"COUNT(*) AS totalCount FROM transaction_record")
StatisticsSummary selectSummary();
@Select("SELECT u.dept, COALESCE(SUM(t.transaction_price), 0) AS salesAmount " +
"FROM transaction_record t JOIN book b ON t.book_id = b.book_id " +
"JOIN user u ON b.seller_id = u.userid " +
"GROUP BY u.dept ORDER BY salesAmount DESC LIMIT 3")
List<DeptSalesDTO> selectTopDepartments();
@Select("SELECT b.book_id, b.title, b.author, SUM(t.transaction_num) AS totalSold " +
"FROM transaction_record t JOIN book b ON t.book_id = b.book_id " +
"GROUP BY b.book_id, b.title, b.author ORDER BY totalSold DESC LIMIT 5")
List<HotBookDTO> selectTopBooks();
@Select("SELECT u.dept, AVG((b.original_price - t.transaction_price / t.transaction_num) " +
"/ b.original_price * 100) AS avgDiscountRate " +
"FROM transaction_record t JOIN book b ON t.book_id = b.book_id " +
"JOIN user u ON b.seller_id = u.userid GROUP BY u.dept")
List<DeptDiscountDTO> selectDeptDiscountRates();
}
BookMapper — 联表查询
public interface BookMapper extends BaseMapper<Book> {
@Select("SELECT b.book_id, u.nickname AS seller_name, b.title, b.author, " +
"b.category, b.original_price, b.selling_price, b.stock, b.status " +
"FROM book b LEFT JOIN user u ON b.seller_id = u.userid " +
"ORDER BY b.created_at DESC")
List<BookDetailDTO> selectAllWithSellerName();
@Select("... WHERE b.status = 0 ...") // 待审核图书
List<BookDetailDTO> selectPendingWithSellerName();
@Select("... JOIN transaction_record ... WHERE b.book_id = #{bookId}") // 销售记录
List<SalesRecordDTO> selectSalesByBookId(@Param("bookId") Integer bookId);
}
TransactionRecordMapper — 分页联表
public interface TransactionRecordMapper extends BaseMapper<TransactionRecord> {
@Select("SELECT t.trans_id, b.title AS book_title, u1.nickname AS buyer_name, " +
"u2.nickname AS seller_name, t.transaction_num, t.transaction_price, " +
"t.transaction_time FROM transaction_record t " +
"LEFT JOIN book b ON t.book_id = b.book_id " +
"LEFT JOIN user u1 ON t.buyer_id = u1.userid " +
"LEFT JOIN user u2 ON b.seller_id = u2.userid " +
"ORDER BY t.transaction_time DESC")
IPage<TransactionDetailDTO> selectPageWithDetail(Page<?> page);
}
UserMapper — 基础CRUD
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper 自带 insert / deleteById / selectById / updateById / selectPage ...
}
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
ai:
openai:
base-url: ${OPENAI_BASE_URL:https://spark-api-open.xf-yun.com}
api-key: ${OPENAI_API_KEY:key}
chat:
options:
model: ${OPENAI_MODEL:lite}
temperature: 0.3
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
以下为占位符,实际报告中替换为对应截图。
启动运行
![服务器启动截图] 说明:Spring Boot 启动日志,显示端口 8080,context-path /api,MyBatis-Plus 初始化完成。
用户登录接口调用
![用户登录API截图] 说明:通过 Postman / curl 调用 POST /api/user/login 返回 User JSON 数据。
用户注册接口调用
![用户注册API截图] 说明:调用 POST /api/user/register 返回注册成功的 User 信息。
图书搜索接口调用
![图书搜索API截图] 说明:调用 GET /api/book?keyword=Java&page=1&size=10 返回分页图书列表。
购买交易接口调用
![购买交易API截图] 说明:调用 POST /api/transaction 完成购买,返回交易记录。
统计接口调用
![统计API截图] 说明:调用 GET /api/statistics/summary 与 /top-books 返回聚合统计数据。
AI分析报告接口调用
![AI分析报告截图] 说明:调用 GET /api/ai/analysis 返回 AI 生成的运营分析报告文本。
客户端目录树
graph TD
subgraph "client/src/main"
direction TB
java["java/space/anyi/client"]
resources["resources"]
end
subgraph "入口层"
cl["Client.java<br/>main启动"]
ui["UIApplication.java<br/>JavaFX Application<br/>场景管理"]
end
subgraph "controller 控制器层"
lc["LoginController<br/>登录"]
rc["RegisterController<br/>注册"]
mc["MainController<br/>1453行<br/>浏览/交易/个人/卖家/管理/统计/AI"]
end
subgraph "service HTTP调用层"
us["UserService<br/>用户相关API"]
bs["BookService<br/>图书相关API"]
ts["TransactionService<br/>交易相关API"]
ss["StatisticsService<br/>统计API"]
as["AiService<br/>AI分析API+流式"]
end
subgraph "util 工具层"
hcu["HttpClientUtil<br/>JDK HttpClient<br/>+Jackson封装"]
luh["LoginUserHolder<br/>当前用户会话<br/>静态持有"]
end
subgraph "resources/fxml 视图"
login_fxml["login.fxml"]
register_fxml["register.fxml"]
main_fxml["main.fxml"]
browse["browse.fxml"]
trans["trans.fxml"]
profile["profile.fxml"]
seller["seller.fxml"]
admin["admin.fxml"]
stats["stats.fxml"]
end
subgraph "resources/css"
css["style.css<br/>436行自定义样式<br/>+BootstrapFX"]
end
java --> cl
java --> ui
java --> lc
java --> rc
java --> mc
java --> us
java --> bs
java --> ts
java --> ss
java --> as
java --> hcu
java --> luh
resources --> login_fxml
resources --> register_fxml
resources --> main_fxml
resources --> browse
resources --> trans
resources --> profile
resources --> seller
resources --> admin
resources --> stats
resources --> css
客户端场景切换流程
stateDiagram-v2
[*] --> LoginScene: 启动
LoginScene --> RegisterScene: 点击"注册"
RegisterScene --> LoginScene: 点击"返回"
LoginScene --> MainScene: 登录成功
state MainScene {
[*] --> BrowseTab: 默认
BrowseTab --> Purchase: 点击购买
BrowseTab --> DetailPanel: 点击图书
Purchase --> BrowseTab: 完成/取消
BrowseTab --> TransactionsTab: 点击导航
TransactionsTab --> ProfileTab
ProfileTab --> SellerTab
SellerTab --> AdminTab
AdminTab --> StatisticsTab
StatisticsTab --> AIAnalysis
}
MainScene --> LoginScene: 退出登录
Client.java — 启动入口
public class Client {
public static void main(String[] args) {
Application.launch(UIApplication.class, args);
}
}
UIApplication.java — JavaFX场景管理
public class UIApplication extends Application {
private static Stage primaryStage;
@Override
public void start(Stage stage) throws Exception {
primaryStage = stage;
showLoginScene();
stage.setTitle("校园二手书交易系统");
stage.getIcons().add(new Image(UIApplication.class.getResourceAsStream("/favicon.png")));
stage.show();
}
public static void showLoginScene() throws Exception { /* 加载login.fxml */ }
public static void showRegisterScene() throws Exception { /* 加载register.fxml */ }
public static void showMainScene() throws Exception { /* 加载main.fxml */ }
}
LoginController.java — 登录逻辑
public class LoginController {
@FXML private TextField usernameField;
@FXML private PasswordField passwordField;
private final UserService userService = new UserService();
@FXML
private void handleLogin() {
String username = usernameField.getText().trim();
String password = passwordField.getText().trim();
if (username.isEmpty() || password.isEmpty()) {
showAlert("请填写所有字段");
return;
}
R<User> result = userService.login(username, password);
if (result.getCode() == 200 && result.getData() != null) {
LoginUserHolder.set(result.getData());
UIApplication.showMainScene();
} else {
showAlert(result.getMessage());
}
}
@FXML
private void handleRegister() { UIApplication.showRegisterScene(); }
}
MainController.java — 核心控制器(核心片段)
public class MainController {
// === 服务初始化 ===
private final BookService bookService = new BookService();
private final AiService aiService = new AiService();
private final TransactionService transactionService = new TransactionService();
private final UserService userService = new UserService();
private final StatisticsService statisticsService = new StatisticsService();
// === 导航 ===
@FXML private Hyperlink navBrowse, navTrans, navSeller, navAdmin;
@FXML private StackPane contentArea;
@FXML
private void initialize() {
loadSubPane("/fxml/browse.fxml"); // 默认加载浏览页
setupNavigation();
initProfile();
if (LoginUserHolder.isLoggedIn()) {
loadUserTransactions();
loadSellerData();
loadCharts();
}
}
// === 图书浏览与购买 ===
@FXML private TextField searchField;
@FXML private ComboBox<String> categoryCombo;
@FXML private TilePane bookCardGrid;
private void loadBooks() {
R<PageResult<Book>> result = bookService.search(keyword, category, page, size);
bookCardGrid.getChildren().clear();
for (Book book : result.getData().getRecords()) {
VBox card = createBookCard(book);
card.setOnMouseClicked(e -> showBookDetail(book));
bookCardGrid.getChildren().add(card);
}
}
private void purchaseBook(Book book) {
boolean confirmed = confirm("确认购买", "购买数量?");
if (confirmed) {
R<TransactionRecord> result = transactionService.purchase(
book.getBookId(), LoginUserHolder.get().getUserid(), quantity);
if (result.getCode() == 200) {
showInfo("购买成功!");
loadBooks();
} else {
showAlert(result.getMessage());
}
}
}
// === 统计图表 ===
private void loadCharts() {
R<StatisticsSummary> summary = statisticsService.getSummary();
// 更新KPI卡片:总金额、总笔数
R<List<DeptSalesDTO>> depts = statisticsService.getTopDepartments();
// 绘制饼图 PieChart
R<List<HotBookDTO>> hotBooks = statisticsService.getTopBooks();
// 绘制柱状图 BarChart
R<List<DeptDiscountDTO>> discounts = statisticsService.getDiscountRates();
// 绘制折线图 LineChart
}
// === AI分析 ===
@FXML private TextArea analysisArea;
@FXML private ProgressBar analysisProgress;
private void loadAnalysis() {
analysisProgress.setVisible(true);
aiService.streamAnalysis(
token -> Platform.runLater(() -> analysisArea.appendText(token)),
() -> Platform.runLater(() -> analysisProgress.setVisible(false)),
error -> Platform.runLater(() -> showAlert("AI分析失败: " + error.getMessage()))
);
}
// ... 其余功能(卖家KPI、管理员用户管理/审核、交易管理、个人资料编辑等)
}
HttpClientUtil — HTTP通信封装
public class HttpClientUtil {
private static final String BASE_URL = "http://localhost:8080";
private static final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)).build();
private static final ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
public static <T> R<T> get(String path, Class<T> responseType) {
return send("GET", path, null, responseType);
}
public static <T> R<T> post(String path, Object body, Class<T> responseType) {
return send("POST", path, body, responseType);
}
public static <T> R<T> put(String path, Object body, Class<T> responseType) {
return send("PUT", path, body, responseType);
}
public static <T> R<T> delete(String path, Class<T> responseType) {
return send("DELETE", path, null, responseType);
}
private static <T> R<T> send(String method, String path, Object body, Class<T> responseType) {
// 构建HttpRequest → 发送 → Jackson反序列化为R<T>
// 异常处理:ConnectException提示"无法连接到服务器"
}
}
LoginUserHolder — 用户会话
public class LoginUserHolder {
private static User currentUser;
public static void set(User user) { currentUser = user; }
public static User get() { return currentUser; }
public static boolean isLoggedIn() { return currentUser != null; }
public static boolean isSeller() { return currentUser != null && currentUser.getIsSeller() == 1; }
public static void clear() { currentUser = null; }
}
各Service调用示例
// UserService
public R<User> login(String username, String password) {
LoginRequest req = new LoginRequest();
req.setUsername(username); req.setPassword(password);
return HttpClientUtil.post("/api/user/login", req, User.class);
}
// BookService
public R<PageResult<Book>> search(String keyword, String category, int page, int size) {
StringBuilder path = new StringBuilder("/api/book?page=" + page + "&size=" + size);
if (keyword != null && !keyword.isBlank())
path.append("&keyword=").append(URLEncoder.encode(keyword, StandardCharsets.UTF_8));
if (category != null && !category.isBlank())
path.append("&category=").append(URLEncoder.encode(category, StandardCharsets.UTF_8));
return HttpClientUtil.get(path.toString(), new TypeReference<R<PageResult<Book>>>() {});
}
// TransactionService
public R<TransactionRecord> purchase(Integer bookId, Integer buyerId, Integer quantity) {
Map<String, Object> body = Map.of("bookId", bookId, "buyerId", buyerId, "quantity", quantity);
return HttpClientUtil.post("/api/transaction", body, TransactionRecord.class);
}
// StatisticsService
public R<List<HotBookDTO>> getTopBooks() {
return HttpClientUtil.get("/api/statistics/top-books",
new TypeReference<R<List<HotBookDTO>>>() {});
}
// AiService
public void streamAnalysis(Consumer<String> onToken, Runnable onComplete,
Consumer<Throwable> onError) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/ai/analysis/stream"))
.GET().build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
.thenAccept(response -> {
response.body().forEach(onToken);
onComplete.run();
})
.exceptionally(e -> { onError.accept(e); return null; });
}
login.fxml — 登录界面布局
<VBox alignment="CENTER" spacing="20" styleClass="login-bg">
<VBox maxWidth="400" styleClass="card">
<Label text="校园二手书交易系统" styleClass="h3" />
<TextField fx:id="usernameField" promptText="用户名" />
<PasswordField fx:id="passwordField" promptText="密码" />
<Button text="登录" onAction="#handleLogin" />
<Hyperlink text="没有账号?立即注册" onAction="#handleRegister" />
</VBox>
</VBox>
admin.fxml — 管理员面板(211行,含5个子面板)
<SplitPane>
<VBox styleClass="admin-sidebar">
<Button text="用户管理" onAction="#showUserMgmt" />
<Button text="图书管理" onAction="#showBookMgmt" />
<Button text="待审核" onAction="#showPendingBooks" />
<Button text="交易管理" onAction="#showTransMgmt" />
<Button text="统计" onAction="#showStats" />
</VBox>
<StackPane fx:id="adminContent">
<!-- 动态切换子面板 -->
</StackPane>
</SplitPane>
stats.fxml — 统计面板(80行)
<ScrollPane>
<VBox spacing="20">
<HBox spacing="20">
<VBox styleClass="kpi-card"><Label text="交易总金额" /><Label fx:id="totalAmount" /></VBox>
<VBox styleClass="kpi-card"><Label text="交易总笔数" /><Label fx:id="totalCount" /></VBox>
</HBox>
<PieChart fx:id="deptPieChart" title="院系交易额占比" />
<BarChart fx:id="topBooksChart" title="热门书籍TOP5" />
<LineChart fx:id="discountLineChart" title="各院系折扣率" />
</VBox>
</ScrollPane>
以下为占位符,实际报告中替换为对应截图。
登录界面
![登录界面截图] 说明:居中卡片式登录表单,包含用户名/密码输入框和登录按钮,底部有注册跳转链接。
注册界面
![注册界面截图] 说明:注册表单包含用户名、密码、昵称、邮箱、手机号、院系六个字段。
图书浏览(Browse Tab)
![图书浏览截图] 说明:顶部搜索栏(关键词+分类下拉),中间图书卡片网格(封面占位+书名+作者+价格+库存),右侧详情面板,底部分页。
图书购买流程
![购买弹窗截图] 说明:点击图书卡片弹出详情面板,点击"购买"弹出确认对话框输入数量,购买成功后提示。
个人交易记录(Transactions Tab)
![交易记录截图] 说明:表格展示该用户的交易记录(交易ID、书名、数量、价格、时间)。
个人资料编辑(Profile Tab)
![个人资料截图] 说明:显示当前用户信息(姓名、院系、信用分),可编辑昵称、邮箱、手机号,可切换卖家身份。
卖家仪表盘(Seller Tab)
![卖家仪表盘截图] 说明:顶部KPI卡片(在售/库存/已售数量),下方图书管理表格(书名、价格、库存、状态、操作按钮)。
管理员用户管理(Admin → User Mgmt)
![用户管理截图] 说明:用户表格(ID、用户名、院系、角色、信用分),可编辑/删除。
管理员图书管理(Admin → Book Mgmt)
![图书管理截图] 说明:所有图书列表表格,含卖家名、状态、操作列。
管理员待审核(Admin → Pending)
![待审核截图] 说明:待审核图书列表(status=0),含审核通过/拒绝按钮。
管理员交易管理(Admin → Trans Mgmt)
![交易管理截图] 说明:所有交易记录表格,含买家名、卖家名、金额、时间。
统计面板(Admin → Stats)
![统计面板截图] 说明:KPI卡片卡片(总金额/总笔数),院系交易额饼图,热门书籍TOP5柱状图,各院系折扣率折线图。
AI分析报告
![AI分析报告截图] 说明:AI分析页面,展示生成的运营分析报告(交易总览、热门院系、热门书籍、折扣率、分析结论),支持流式输出。
ER图
erDiagram
User ||--o{ Book : "发布"
User ||--o{ TransactionRecord : "购买"
Book ||--o{ TransactionRecord : "被购买"
User {
int userid PK
varchar username UK
varchar password
varchar nickname
varchar email
varchar phone
varchar dept
int credit
int role "0=学生 1=管理员"
int is_seller "0=否 1=是"
datetime created_at
}
Book {
int book_id PK
int seller_id FK
varchar title
varchar author
varchar category
decimal original_price
decimal selling_price
varchar image_url
datetime created_at
int stock
int status "0=待审核 1=上架 2=下架"
}
TransactionRecord {
int trans_id PK
int book_id FK
int buyer_id FK
int transaction_num
decimal transaction_price
datetime transaction_time
}