result.md 34 KB

五、实训结果


1. 服务器端

1.1 项目结构图

整体项目模块结构

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

1.2 核心代码

应用入口

@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

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 项目结构图

客户端目录树

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: 退出登录

2.2 核心代码

入口启动类

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、管理员用户管理/审核、交易管理、个人资料编辑等)
}

客户端Service层

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; });
}

FXML视图示例

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>

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图

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
    }