Bladeren bron

feat:更新实验报告result.md文件

yangyi 1 maand geleden
bovenliggende
commit
970fc16c7c
1 gewijzigde bestanden met toevoegingen van 1144 en 0 verwijderingen
  1. 1144 0
      doc/result.md

+ 1144 - 0
doc/result.md

@@ -0,0 +1,1144 @@
+# 五、实训结果
+
+---
+
+## 1. 服务器端
+
+### 1.1 项目结构图
+
+**整体项目模块结构**
+
+```mermaid
+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
+```
+
+**服务器端目录树**
+
+```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<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
+```
+
+### 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<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 — 登录注册端点**
+
+```java
+@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**
+
+```java
+@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 — 购买与交易记录**
+
+```java
+@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 — 统计分析**
+
+```java
+@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分析报告**
+
+```java
+@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 — 登录注册业务**
+
+```java
+@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 — 购买事务(含库存扣减)**
+
+```java
+@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分析报告生成**
+
+```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<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**
+
+```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<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 — 联表查询**
+
+```java
+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 — 分页联表**
+
+```java
+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**
+
+```java
+public interface UserMapper extends BaseMapper<User> {
+    // 继承 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<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
+```
+
+**客户端场景切换流程**
+
+```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<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 — 核心控制器(核心片段)**
+
+```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、管理员用户管理/审核、交易管理、个人资料编辑等)
+}
+```
+
+#### 客户端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 <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 — 用户会话**
+
+```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<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; });
+}
+```
+
+#### FXML视图示例
+
+**login.fxml — 登录界面布局**
+
+```xml
+<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个子面板)**
+
+```xml
+<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行)**
+
+```xml
+<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>
+```
+
+### 2.3 客户端运行效果图
+
+> 以下为占位符,实际报告中替换为对应截图。
+
+**登录界面**
+
+![登录界面截图]
+*说明:居中卡片式登录表单,包含用户名/密码输入框和登录按钮,底部有注册跳转链接。*
+
+**注册界面**
+
+![注册界面截图]
+*说明:注册表单包含用户名、密码、昵称、邮箱、手机号、院系六个字段。*
+
+**图书浏览(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图**
+
+```mermaid
+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
+    }
+```