# 五、实训结果 --- ## 1. 服务器端 ### 1.1 项目结构图 **整体项目模块结构** ```mermaid graph TD subgraph "CampusUsedBookTradingSystem (父POM)" direction TB common[common 模块
space.anyi.common
实体 / DTO / 枚举] server[server 模块
space.anyi.server
Spring Boot REST API] client[client 模块
space.anyi.client
JavaFX 桌面客户端] end common -->|依赖| server common -->|依赖| client server -.->|HTTP JSON| client ``` **服务器端目录树** ```mermaid 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
CORS跨域"] mpc["MyBatisPlusConfig.java
分页拦截器"] geh["GlobalExceptionHandler.java
全局异常处理"] end subgraph "controller 控制器层" uc["UserController
用户CRUD"] bc["BookController
图书CRUD+审核"] tc["TransactionController
交易CRUD"] sc["StatisticsController
统计分析"] ac["AiController
AI报告生成"] end subgraph "service 业务层" us["UserService/UserServiceImpl
登录/注册"] bs["BookService/BookServiceImpl
图书搜索/查询"] ts["TransactionService/TransactionServiceImpl
购买事务"] ss["StatisticsService/StatisticsServiceImpl
聚合统计"] as["AiService/AiServiceImpl
AI分析+流式生成"] end subgraph "mapper 持久层" um["UserMapper
基础CRUD"] bm["BookMapper
自定义@Select联表"] tm["TransactionRecordMapper
分页+联表"] sm["StatisticsMapper
SUM/GROUP统计"] end subgraph "resources 资源" yml["application.yml
端口/数据库/AI配置"] xml["mapper/*.xml
空壳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 ``` ### 1.2 核心代码 #### 应用入口 ```java @MapperScan("space.anyi.server.mapper") @SpringBootApplication public class ServerApplication { public static void main(String[] args) { SpringApplication.run(ServerApplication.class, args); } } ``` #### 配置层 **WebConfig — CORS跨域配置** ```java @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 — 分页拦截器** ```java @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } ``` **GlobalExceptionHandler — 全局异常处理** ```java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(DuplicateKeyException.class) public R handleDuplicateKey(DuplicateKeyException e) { return R.error("用户名已存在"); } @ExceptionHandler(IllegalArgumentException.class) public R handleIllegalArgument(IllegalArgumentException e) { return R.error(e.getMessage()); } @ExceptionHandler(Exception.class) public R handleException(Exception e) { return R.error("服务器内部错误: " + e.getMessage()); } } ``` #### 控制器层 **UserController — 登录注册端点** ```java @RestController @RequestMapping("/user") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping("/login") public R 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 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** ```java @RestController @RequestMapping("/book") public class BookController { private final BookService bookService; public BookController(BookService bookService) { this.bookService = bookService; } @GetMapping public R> 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 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> getMyBooks(@RequestParam Integer sellerId) { return R.ok(bookService.getBySellerId(sellerId)); } @PostMapping public R 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 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 — 购买与交易记录** ```java @RestController @RequestMapping("/transaction") public class TransactionController { private final TransactionService transactionService; public TransactionController(TransactionService transactionService) { this.transactionService = transactionService; } @PostMapping public R purchase(@RequestBody Map 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> getByUserDetail(@PathVariable Integer userId) { return R.ok(transactionService.getByUserIdWithTitle(userId)); } // GET /{id}, GET /user/{userId}, PUT /{id}, DELETE /{id} ...省略 } ``` **StatisticsController — 统计分析** ```java @RestController @RequestMapping("/statistics") public class StatisticsController { private final StatisticsService statisticsService; public StatisticsController(StatisticsService statisticsService) { this.statisticsService = statisticsService; } @GetMapping("/summary") // {totalAmount, totalCount} public R getSummary() { return R.ok(statisticsService.getSummary()); } @GetMapping("/top-departments") // Top3 院系交易额 public R> getTopDepartments() { return R.ok(statisticsService.getTopDepartments()); } @GetMapping("/top-books") // Top5 热门书籍 public R> getTopBooks() { return R.ok(statisticsService.getTopBooks()); } @GetMapping("/discount-rates") // 各院系平均折扣率 public R> getDiscountRates() { return R.ok(statisticsService.getDeptDiscountRates()); } } ``` **AiController — AI分析报告** ```java @RestController @RequestMapping("/ai") public class AiController { private final AiService aiService; public AiController(AiService aiService) { this.aiService = aiService; } @GetMapping("/analysis") public R getAnalysis() { String report = aiService.generateAnalysis(); return R.ok(report); } @GetMapping("/analysis/stream") public ResponseBodyEmitter streamAnalysis() { ResponseBodyEmitter emitter = new ResponseBodyEmitter(180_000L); Flux 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 — 登录注册业务** ```java @Service public class UserServiceImpl extends ServiceImpl implements UserService { @Override public User login(String username, String password) { LambdaQueryWrapper 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 — 购买事务(含库存扣减)** ```java @Service public class TransactionServiceImpl extends ServiceImpl 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分析报告生成** ```java @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 topDepts = statisticsService.getTopDepartments(); List topBooks = statisticsService.getTopBooks(); List discounts = statisticsService.getDeptDiscountRates(); StringBuilder report = new StringBuilder(); report.append("========== 校园二手书交易系统分析报告 ==========\n\n"); // ... 格式化输出交易总览、热门院系、热门书籍、折扣率、分析结论 return report.toString(); } @Override public Flux streamAnalysis() { // 构造Prompt,调用ChatClient流式生成 return chatClient.prompt().user(buildPrompt(...)).stream().content(); } } ``` #### 持久层 **StatisticsMapper — 聚合统计SQL** ```java 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 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 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 selectDeptDiscountRates(); } ``` **BookMapper — 联表查询** ```java public interface BookMapper extends BaseMapper { @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 selectAllWithSellerName(); @Select("... WHERE b.status = 0 ...") // 待审核图书 List selectPendingWithSellerName(); @Select("... JOIN transaction_record ... WHERE b.book_id = #{bookId}") // 销售记录 List selectSalesByBookId(@Param("bookId") Integer bookId); } ``` **TransactionRecordMapper — 分页联表** ```java public interface TransactionRecordMapper extends BaseMapper { @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 selectPageWithDetail(Page page); } ``` **UserMapper — 基础CRUD** ```java public interface UserMapper extends BaseMapper { // 继承 BaseMapper 自带 insert / deleteById / selectById / updateById / selectPage ... } ``` #### 配置文件 **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 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 ``` ### 1.3 服务器端运行效果图 > 以下为占位符,实际报告中替换为对应截图。 **启动运行** ![服务器启动截图] *说明: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 生成的运营分析报告文本。* --- ## 2. 客户端 ### 2.1 项目结构图 **客户端目录树** ```mermaid graph TD subgraph "client/src/main" direction TB java["java/space/anyi/client"] resources["resources"] end subgraph "入口层" cl["Client.java
main启动"] ui["UIApplication.java
JavaFX Application
场景管理"] end subgraph "controller 控制器层" lc["LoginController
登录"] rc["RegisterController
注册"] mc["MainController
1453行
浏览/交易/个人/卖家/管理/统计/AI"] end subgraph "service HTTP调用层" us["UserService
用户相关API"] bs["BookService
图书相关API"] ts["TransactionService
交易相关API"] ss["StatisticsService
统计API"] as["AiService
AI分析API+流式"] end subgraph "util 工具层" hcu["HttpClientUtil
JDK HttpClient
+Jackson封装"] luh["LoginUserHolder
当前用户会话
静态持有"] 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
436行自定义样式
+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 ``` **客户端场景切换流程** ```mermaid 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: 退出登录 ``` ### 2.2 核心代码 #### 入口启动类 **Client.java — 启动入口** ```java public class Client { public static void main(String[] args) { Application.launch(UIApplication.class, args); } } ``` **UIApplication.java — JavaFX场景管理** ```java 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 — 登录逻辑** ```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 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 — 核心控制器(核心片段)** ```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 categoryCombo; @FXML private TilePane bookCardGrid; private void loadBooks() { R> 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 result = transactionService.purchase( book.getBookId(), LoginUserHolder.get().getUserid(), quantity); if (result.getCode() == 200) { showInfo("购买成功!"); loadBooks(); } else { showAlert(result.getMessage()); } } } // === 统计图表 === private void loadCharts() { R summary = statisticsService.getSummary(); // 更新KPI卡片:总金额、总笔数 R> depts = statisticsService.getTopDepartments(); // 绘制饼图 PieChart R> hotBooks = statisticsService.getTopBooks(); // 绘制柱状图 BarChart R> 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、管理员用户管理/审核、交易管理、个人资料编辑等) } ``` #### 客户端Service层 **HttpClientUtil — HTTP通信封装** ```java 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 R get(String path, Class responseType) { return send("GET", path, null, responseType); } public static R post(String path, Object body, Class responseType) { return send("POST", path, body, responseType); } public static R put(String path, Object body, Class responseType) { return send("PUT", path, body, responseType); } public static R delete(String path, Class responseType) { return send("DELETE", path, null, responseType); } private static R send(String method, String path, Object body, Class responseType) { // 构建HttpRequest → 发送 → Jackson反序列化为R // 异常处理:ConnectException提示"无法连接到服务器" } } ``` **LoginUserHolder — 用户会话** ```java 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调用示例** ```java // UserService public R 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> 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>>() {}); } // TransactionService public R purchase(Integer bookId, Integer buyerId, Integer quantity) { Map body = Map.of("bookId", bookId, "buyerId", buyerId, "quantity", quantity); return HttpClientUtil.post("/api/transaction", body, TransactionRecord.class); } // StatisticsService public R> getTopBooks() { return HttpClientUtil.get("/api/statistics/top-books", new TypeReference>>() {}); } // AiService public void streamAnalysis(Consumer onToken, Runnable onComplete, Consumer 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; }); } ``` #### FXML视图示例 **login.fxml — 登录界面布局** ```xml