فهرست منبع

feat:初步实现

yangyi 1 ماه پیش
والد
کامیت
233a6c2d62
56فایلهای تغییر یافته به همراه2177 افزوده شده و 22 حذف شده
  1. 33 7
      client/pom.xml
  2. 43 0
      client/src/main/java/space/anyi/client/App.java
  3. 17 0
      client/src/main/java/space/anyi/client/Client.java
  4. 65 0
      client/src/main/java/space/anyi/client/controller/LoginController.java
  5. 462 0
      client/src/main/java/space/anyi/client/controller/MainController.java
  6. 94 0
      client/src/main/java/space/anyi/client/controller/RegisterController.java
  7. 48 0
      client/src/main/java/space/anyi/client/service/BookService.java
  8. 25 0
      client/src/main/java/space/anyi/client/service/StatisticsService.java
  9. 36 0
      client/src/main/java/space/anyi/client/service/TransactionService.java
  10. 34 0
      client/src/main/java/space/anyi/client/service/UserService.java
  11. 96 0
      client/src/main/java/space/anyi/client/util/HttpClientUtil.java
  12. 27 0
      client/src/main/java/space/anyi/client/util/LoginUserHolder.java
  13. 13 0
      client/src/main/resources/css/style.css
  14. 14 0
      client/src/main/resources/fxml/login.fxml
  15. 181 0
      client/src/main/resources/fxml/main.fxml
  16. 18 0
      client/src/main/resources/fxml/register.fxml
  17. 11 7
      common/pom.xml
  18. 10 0
      common/src/main/java/space/anyi/common/dto/DeptDiscountDTO.java
  19. 10 0
      common/src/main/java/space/anyi/common/dto/DeptSalesDTO.java
  20. 11 0
      common/src/main/java/space/anyi/common/dto/HotBookDTO.java
  21. 9 0
      common/src/main/java/space/anyi/common/dto/LoginRequest.java
  22. 12 0
      common/src/main/java/space/anyi/common/dto/PageResult.java
  23. 33 0
      common/src/main/java/space/anyi/common/dto/R.java
  24. 13 0
      common/src/main/java/space/anyi/common/dto/RegisterRequest.java
  25. 10 0
      common/src/main/java/space/anyi/common/dto/StatisticsSummary.java
  26. 24 0
      common/src/main/java/space/anyi/common/entity/Book.java
  27. 20 0
      common/src/main/java/space/anyi/common/entity/TransactionRecord.java
  28. 26 0
      common/src/main/java/space/anyi/common/entity/User.java
  29. 23 0
      common/src/main/java/space/anyi/common/enums/UserRole.java
  30. 97 1
      pom.xml
  31. 30 7
      server/pom.xml
  32. 12 0
      server/src/main/java/space/anyi/server/ServerApplication.java
  33. 26 0
      server/src/main/java/space/anyi/server/config/GlobalExceptionHandler.java
  34. 17 0
      server/src/main/java/space/anyi/server/config/MyBatisPlusConfig.java
  35. 23 0
      server/src/main/java/space/anyi/server/config/WebConfig.java
  36. 74 0
      server/src/main/java/space/anyi/server/controller/BookController.java
  37. 37 0
      server/src/main/java/space/anyi/server/controller/StatisticsController.java
  38. 67 0
      server/src/main/java/space/anyi/server/controller/TransactionController.java
  39. 71 0
      server/src/main/java/space/anyi/server/controller/UserController.java
  40. 7 0
      server/src/main/java/space/anyi/server/mapper/BookMapper.java
  41. 36 0
      server/src/main/java/space/anyi/server/mapper/StatisticsMapper.java
  42. 7 0
      server/src/main/java/space/anyi/server/mapper/TransactionRecordMapper.java
  43. 7 0
      server/src/main/java/space/anyi/server/mapper/UserMapper.java
  44. 12 0
      server/src/main/java/space/anyi/server/service/BookService.java
  45. 14 0
      server/src/main/java/space/anyi/server/service/StatisticsService.java
  46. 10 0
      server/src/main/java/space/anyi/server/service/TransactionService.java
  47. 9 0
      server/src/main/java/space/anyi/server/service/UserService.java
  48. 42 0
      server/src/main/java/space/anyi/server/service/impl/BookServiceImpl.java
  49. 40 0
      server/src/main/java/space/anyi/server/service/impl/StatisticsServiceImpl.java
  50. 56 0
      server/src/main/java/space/anyi/server/service/impl/TransactionServiceImpl.java
  51. 31 0
      server/src/main/java/space/anyi/server/service/impl/UserServiceImpl.java
  52. 18 0
      server/src/main/resources/application.yml
  53. 4 0
      server/src/main/resources/mapper/BookMapper.xml
  54. 4 0
      server/src/main/resources/mapper/StatisticsMapper.xml
  55. 4 0
      server/src/main/resources/mapper/TransactionRecordMapper.xml
  56. 4 0
      server/src/main/resources/mapper/UserMapper.xml

+ 33 - 7
client/pom.xml

@@ -11,17 +11,43 @@
 
     <artifactId>client</artifactId>
 
-    <properties>
-        <maven.compiler.source>23</maven.compiler.source>
-        <maven.compiler.target>23</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    </properties>
-
     <dependencies>
+        <dependency>
+            <groupId>org.openjfx</groupId>
+            <artifactId>javafx-controls</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.openjfx</groupId>
+            <artifactId>javafx-fxml</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
         <dependency>
             <groupId>space.anyi</groupId>
             <artifactId>common</artifactId>
         </dependency>
     </dependencies>
 
-</project>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.openjfx</groupId>
+                <artifactId>javafx-maven-plugin</artifactId>
+                <version>0.0.8</version>
+                <configuration>
+                    <mainClass>space.anyi.client.App</mainClass>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 43 - 0
client/src/main/java/space/anyi/client/App.java

@@ -0,0 +1,43 @@
+package space.anyi.client;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import space.anyi.client.util.LoginUserHolder;
+
+public class App extends Application {
+
+    private static Stage primaryStage;
+
+    @Override
+    public void start(Stage stage) throws Exception {
+        primaryStage = stage;
+        showLoginScene();
+        stage.setTitle("校园二手书交易系统");
+        stage.show();
+    }
+
+    public static void showLoginScene() throws Exception {
+        FXMLLoader loader = new FXMLLoader(App.class.getResource("/fxml/login.fxml"));
+        Scene scene = new Scene(loader.load());
+        primaryStage.setScene(scene);
+    }
+
+    public static void showRegisterScene() throws Exception {
+        FXMLLoader loader = new FXMLLoader(App.class.getResource("/fxml/register.fxml"));
+        Scene scene = new Scene(loader.load());
+        primaryStage.setScene(scene);
+    }
+
+    public static void showMainScene() throws Exception {
+        FXMLLoader loader = new FXMLLoader(App.class.getResource("/fxml/main.fxml"));
+        Scene scene = new Scene(loader.load());
+        primaryStage.setScene(scene);
+    }
+
+    public static Stage getPrimaryStage() {
+        return primaryStage;
+    }
+
+}

+ 17 - 0
client/src/main/java/space/anyi/client/Client.java

@@ -0,0 +1,17 @@
+package space.anyi.client;
+
+import javafx.application.Application;
+
+/**
+ * @fileName: Client
+ * @projectName: CampusUsedBookTradingSystem
+ * @package: space.anyi.client
+ * @author: yangyi
+ * @date:24/6/2026 3:35 pm
+ * @description:
+ */
+public class Client {
+    public static void main(String[] args) {
+        Application.launch(App.class,args);
+    }
+}

+ 65 - 0
client/src/main/java/space/anyi/client/controller/LoginController.java

@@ -0,0 +1,65 @@
+package space.anyi.client.controller;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.TextField;
+import javafx.scene.control.Alert;
+import javafx.scene.control.TextArea;
+import space.anyi.client.App;
+import space.anyi.client.service.UserService;
+import space.anyi.client.util.LoginUserHolder;
+import space.anyi.common.dto.R;
+import space.anyi.common.entity.User;
+
+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());
+            try {
+                App.showMainScene();
+            } catch (Exception e) {
+                showAlert("加载主界面失败:" + e.getMessage());
+            }
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleRegister() {
+        try {
+            App.showRegisterScene();
+        } catch (Exception e) {
+            showAlert("加载注册界面失败:" + e.getMessage());
+        }
+    }
+
+    private void showAlert(String message) {
+        Alert alert = new Alert(Alert.AlertType.WARNING);
+        alert.setTitle("提示");
+        alert.setHeaderText(null);
+        TextArea textArea = new TextArea(message);
+        textArea.setEditable(false);
+        textArea.setWrapText(true);
+        textArea.setPrefWidth(360);
+        textArea.setPrefHeight(80);
+        alert.getDialogPane().setContent(textArea);
+        alert.showAndWait();
+    }
+}

+ 462 - 0
client/src/main/java/space/anyi/client/controller/MainController.java

@@ -0,0 +1,462 @@
+package space.anyi.client.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import javafx.collections.FXCollections;
+import javafx.fxml.FXML;
+import javafx.scene.control.*;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.control.TextArea;
+import space.anyi.client.App;
+import space.anyi.client.service.BookService;
+import space.anyi.client.service.StatisticsService;
+import space.anyi.client.service.TransactionService;
+import space.anyi.client.service.UserService;
+import space.anyi.client.util.HttpClientUtil;
+import space.anyi.client.util.LoginUserHolder;
+import space.anyi.common.dto.PageResult;
+import space.anyi.common.dto.R;
+import space.anyi.common.entity.Book;
+import space.anyi.common.entity.TransactionRecord;
+import space.anyi.common.entity.User;
+import space.anyi.common.dto.DeptSalesDTO;
+import space.anyi.common.dto.HotBookDTO;
+import space.anyi.common.dto.DeptDiscountDTO;
+import space.anyi.common.dto.StatisticsSummary;
+import java.util.List;
+
+public class MainController {
+
+    @FXML private Label welcomeLabel;
+
+    // Browse tab
+    @FXML private TextField searchField;
+    @FXML private ComboBox<String> categoryCombo;
+    @FXML private TableView<Book> bookTable;
+    @FXML private TableColumn<Book, Integer> bookIdCol;
+    @FXML private TableColumn<Book, String> titleCol;
+    @FXML private TableColumn<Book, String> authorCol;
+    @FXML private TableColumn<Book, String> categoryCol;
+    @FXML private TableColumn<Book, java.math.BigDecimal> priceCol;
+    @FXML private TableColumn<Book, Integer> stockCol;
+    @FXML private Label detailTitle;
+    @FXML private Label detailAuthor;
+    @FXML private Label detailCategory;
+    @FXML private Label detailPrice;
+    @FXML private Label detailStock;
+    @FXML private TextField quantityField;
+
+    // My transactions tab
+    @FXML private TableView<TransactionRecord> transTable;
+    @FXML private TableColumn<TransactionRecord, Integer> transIdCol;
+    @FXML private TableColumn<TransactionRecord, Integer> transBookIdCol;
+    @FXML private TableColumn<TransactionRecord, Integer> transNumCol;
+    @FXML private TableColumn<TransactionRecord, java.math.BigDecimal> transPriceCol;
+    @FXML private TableColumn<TransactionRecord, String> transTimeCol;
+
+    // Profile tab
+    @FXML private TextField profileUsername;
+    @FXML private TextField profileNickname;
+    @FXML private TextField profileEmail;
+    @FXML private TextField profilePhone;
+    @FXML private TextField profileDept;
+    @FXML private Label profileCredit;
+
+    // Seller tab
+    @FXML private Tab sellerTab;
+    @FXML private TableView<Book> myBookTable;
+    @FXML private TableColumn<Book, Integer> myBookIdCol;
+    @FXML private TableColumn<Book, String> myBookTitleCol;
+    @FXML private TableColumn<Book, java.math.BigDecimal> myBookPriceCol;
+    @FXML private TableColumn<Book, Integer> myBookStockCol;
+    @FXML private TextField newTitle;
+    @FXML private TextField newAuthor;
+    @FXML private TextField newCategory;
+    @FXML private TextField newOriginalPrice;
+    @FXML private TextField newSellingPrice;
+    @FXML private TextField newStock;
+
+    // Statistics tab
+    @FXML private Label statTotalAmount;
+    @FXML private Label statTotalCount;
+    @FXML private TableView<DeptSalesDTO> deptTable;
+    @FXML private TableColumn<DeptSalesDTO, String> deptNameCol;
+    @FXML private TableColumn<DeptSalesDTO, java.math.BigDecimal> deptSalesCol;
+    @FXML private TableView<HotBookDTO> hotBookTable;
+    @FXML private TableColumn<HotBookDTO, Integer> hotBookIdCol;
+    @FXML private TableColumn<HotBookDTO, String> hotBookTitleCol;
+    @FXML private TableColumn<HotBookDTO, String> hotBookAuthorCol;
+    @FXML private TableColumn<HotBookDTO, Integer> hotBookSoldCol;
+    @FXML private TableView<DeptDiscountDTO> discountTable;
+    @FXML private TableColumn<DeptDiscountDTO, String> discountDeptCol;
+    @FXML private TableColumn<DeptDiscountDTO, java.math.BigDecimal> discountRateCol;
+
+    // Admin tab
+    @FXML private Tab adminTab;
+    @FXML private TableView<User> userTable;
+    @FXML private TableColumn<User, Integer> userIdCol;
+    @FXML private TableColumn<User, String> userNameCol;
+    @FXML private TableColumn<User, String> userNicknameCol;
+    @FXML private TableColumn<User, String> userDeptCol;
+    @FXML private TableColumn<User, Integer> userCreditCol;
+    @FXML private TableColumn<User, Integer> userRoleCol;
+    @FXML private TableColumn<User, Integer> userSellerCol;
+    @FXML private TableView<TransactionRecord> allTransTable;
+    @FXML private TableColumn<TransactionRecord, Integer> allTransIdCol;
+    @FXML private TableColumn<TransactionRecord, Integer> allTransBookIdCol;
+    @FXML private TableColumn<TransactionRecord, Integer> allTransBuyerIdCol;
+    @FXML private TableColumn<TransactionRecord, Integer> allTransNumCol;
+    @FXML private TableColumn<TransactionRecord, java.math.BigDecimal> allTransPriceCol;
+    @FXML private TableColumn<TransactionRecord, String> allTransTimeCol;
+
+    private final BookService bookService = new BookService();
+    private final TransactionService transactionService = new TransactionService();
+    private final UserService userService = new UserService();
+    private final StatisticsService statisticsService = new StatisticsService();
+
+    private Book selectedBook;
+
+    @FXML
+    public void initialize() {
+        User user = LoginUserHolder.get();
+        if (user != null) {
+            welcomeLabel.setText("欢迎," + (user.getNickname() != null ? user.getNickname() : user.getUsername()));
+            setupProfile(user);
+            if (user.getIsSeller() != 1) {
+                sellerTab.setDisable(true);
+            }
+            if (user.getRole() != 1) {
+                adminTab.setDisable(true);
+            }
+        }
+
+        categoryCombo.getItems().addAll("全部", "教材", "小说", "科技", "文学", "考试", "其他");
+        categoryCombo.getSelectionModel().select(0);
+        setupBrowseTable();
+        setupTransTable();
+        setupMyBookTable();
+        setupStatsTables();
+        setupAdminTables();
+        loadBooks();
+    }
+
+    private void setupBrowseTable() {
+        bookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
+        titleCol.setCellValueFactory(new PropertyValueFactory<>("title"));
+        authorCol.setCellValueFactory(new PropertyValueFactory<>("author"));
+        categoryCol.setCellValueFactory(new PropertyValueFactory<>("category"));
+        priceCol.setCellValueFactory(new PropertyValueFactory<>("sellingPrice"));
+        stockCol.setCellValueFactory(new PropertyValueFactory<>("stock"));
+
+        bookTable.getSelectionModel().selectedItemProperty().addListener((obs, old, sel) -> {
+            if (sel != null) {
+                selectedBook = sel;
+                detailTitle.setText(sel.getTitle());
+                detailAuthor.setText(sel.getAuthor());
+                detailCategory.setText(sel.getCategory());
+                detailPrice.setText("¥" + sel.getSellingPrice());
+                detailStock.setText(String.valueOf(sel.getStock()));
+            }
+        });
+    }
+
+    private void setupTransTable() {
+        transIdCol.setCellValueFactory(new PropertyValueFactory<>("transId"));
+        transBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
+        transNumCol.setCellValueFactory(new PropertyValueFactory<>("transactionNum"));
+        transPriceCol.setCellValueFactory(new PropertyValueFactory<>("transactionPrice"));
+        transTimeCol.setCellValueFactory(new PropertyValueFactory<>("transactionTime"));
+    }
+
+    private void setupMyBookTable() {
+        myBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
+        myBookTitleCol.setCellValueFactory(new PropertyValueFactory<>("title"));
+        myBookPriceCol.setCellValueFactory(new PropertyValueFactory<>("sellingPrice"));
+        myBookStockCol.setCellValueFactory(new PropertyValueFactory<>("stock"));
+    }
+
+    private void setupStatsTables() {
+        deptNameCol.setCellValueFactory(new PropertyValueFactory<>("dept"));
+        deptSalesCol.setCellValueFactory(new PropertyValueFactory<>("salesAmount"));
+
+        hotBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
+        hotBookTitleCol.setCellValueFactory(new PropertyValueFactory<>("title"));
+        hotBookAuthorCol.setCellValueFactory(new PropertyValueFactory<>("author"));
+        hotBookSoldCol.setCellValueFactory(new PropertyValueFactory<>("totalSold"));
+
+        discountDeptCol.setCellValueFactory(new PropertyValueFactory<>("dept"));
+        discountRateCol.setCellValueFactory(new PropertyValueFactory<>("avgDiscountRate"));
+    }
+
+    private void setupAdminTables() {
+        userIdCol.setCellValueFactory(new PropertyValueFactory<>("userid"));
+        userNameCol.setCellValueFactory(new PropertyValueFactory<>("username"));
+        userNicknameCol.setCellValueFactory(new PropertyValueFactory<>("nickname"));
+        userDeptCol.setCellValueFactory(new PropertyValueFactory<>("dept"));
+        userCreditCol.setCellValueFactory(new PropertyValueFactory<>("credit"));
+        userRoleCol.setCellValueFactory(new PropertyValueFactory<>("role"));
+        userSellerCol.setCellValueFactory(new PropertyValueFactory<>("isSeller"));
+
+        allTransIdCol.setCellValueFactory(new PropertyValueFactory<>("transId"));
+        allTransBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
+        allTransBuyerIdCol.setCellValueFactory(new PropertyValueFactory<>("buyerId"));
+        allTransNumCol.setCellValueFactory(new PropertyValueFactory<>("transactionNum"));
+        allTransPriceCol.setCellValueFactory(new PropertyValueFactory<>("transactionPrice"));
+        allTransTimeCol.setCellValueFactory(new PropertyValueFactory<>("transactionTime"));
+    }
+
+    @FXML
+    private void handleSearch() {
+        loadBooks();
+    }
+
+    private void loadBooks() {
+        String keyword = searchField.getText().trim();
+        String category = categoryCombo.getValue();
+        if ("全部".equals(category) || category == null || category.isBlank()) {
+            category = null;
+        }
+        var result = bookService.search(keyword, category, 1, 50);
+        if (result.getCode() == 200 && result.getData() != null && result.getData().getRecords() != null) {
+            bookTable.setItems(FXCollections.observableArrayList(result.getData().getRecords()));
+        }
+    }
+
+    @FXML
+    private void handlePurchase() {
+        if (selectedBook == null) {
+            showAlert("请先选择一本书");
+            return;
+        }
+        int qty;
+        try {
+            qty = Integer.parseInt(quantityField.getText().trim());
+        } catch (NumberFormatException e) {
+            showAlert("请输入有效数量");
+            return;
+        }
+        if (qty <= 0) {
+            showAlert("数量必须为正数");
+            return;
+        }
+
+        R<TransactionRecord> result = transactionService.purchase(
+                selectedBook.getBookId(),
+                LoginUserHolder.get().getUserid(),
+                qty);
+        if (result.getCode() == 200) {
+            showInfo("购买成功!");
+            loadBooks();
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleLoadTransactions() {
+        R<List<TransactionRecord>> result = transactionService.getByUser(LoginUserHolder.get().getUserid());
+        if (result.getCode() == 200 && result.getData() != null) {
+            transTable.setItems(FXCollections.observableArrayList(result.getData()));
+        }
+    }
+
+    @FXML
+    private void handleUpdateProfile() {
+        User user = LoginUserHolder.get();
+        user.setNickname(profileNickname.getText().trim());
+        user.setEmail(profileEmail.getText().trim());
+        user.setPhone(profilePhone.getText().trim());
+        user.setDept(profileDept.getText().trim());
+        R<User> result = userService.update(user.getUserid(), user);
+        if (result.getCode() == 200) {
+            showInfo("个人信息已更新");
+            LoginUserHolder.set(result.getData());
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleLoadMyBooks() {
+        R<List<Book>> result = bookService.getMyBooks(LoginUserHolder.get().getUserid());
+        if (result.getCode() == 200 && result.getData() != null) {
+            myBookTable.setItems(FXCollections.observableArrayList(result.getData()));
+        }
+    }
+
+    @FXML
+    private void handleAddBook() {
+        try {
+            Book book = new Book();
+            book.setSellerId(LoginUserHolder.get().getUserid());
+            book.setTitle(newTitle.getText().trim());
+            book.setAuthor(newAuthor.getText().trim());
+            book.setCategory(newCategory.getText().trim());
+            book.setOriginalPrice(new java.math.BigDecimal(newOriginalPrice.getText().trim()));
+            book.setSellingPrice(new java.math.BigDecimal(newSellingPrice.getText().trim()));
+            book.setStock(Integer.parseInt(newStock.getText().trim()));
+
+            R<Book> result = bookService.create(book);
+            if (result.getCode() == 200) {
+                showInfo("书籍已上架!");
+                clearBookForm();
+                handleLoadMyBooks();
+            } else {
+                showAlert(result.getMessage());
+            }
+        } catch (Exception e) {
+            showAlert("输入无效:" + e.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleDeleteBook() {
+        Book sel = myBookTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+                showAlert("请选择要删除的书籍");
+            return;
+        }
+        R<Void> result = bookService.delete(sel.getBookId());
+        if (result.getCode() == 200) {
+            showInfo("书籍已删除");
+            handleLoadMyBooks();
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    // Statistics handlers
+
+    @FXML
+    private void handleLoadStatistics() {
+        // Summary
+        R<StatisticsSummary> summaryResult = statisticsService.getSummary();
+        if (summaryResult.getCode() == 200 && summaryResult.getData() != null) {
+            statTotalAmount.setText("¥" + summaryResult.getData().getTotalAmount());
+            statTotalCount.setText(String.valueOf(summaryResult.getData().getTotalCount()));
+        }
+
+        // Top departments
+        R<List<DeptSalesDTO>> deptResult = statisticsService.getTopDepartments();
+        if (deptResult.getCode() == 200 && deptResult.getData() != null) {
+            deptTable.setItems(FXCollections.observableArrayList(deptResult.getData()));
+        }
+
+        // Top books
+        R<List<HotBookDTO>> bookResult = statisticsService.getTopBooks();
+        if (bookResult.getCode() == 200 && bookResult.getData() != null) {
+            hotBookTable.setItems(FXCollections.observableArrayList(bookResult.getData()));
+        }
+
+        // Discount rates
+        R<List<DeptDiscountDTO>> discountResult = statisticsService.getDiscountRates();
+        if (discountResult.getCode() == 200 && discountResult.getData() != null) {
+            discountTable.setItems(FXCollections.observableArrayList(discountResult.getData()));
+        }
+    }
+
+    // Admin handlers
+
+    @FXML
+    private void handleLoadAllUsers() {
+        R<PageResult<User>> result = HttpClientUtil.get("/api/user?page=1&size=100",
+                new TypeReference<R<PageResult<User>>>() {});
+        if (result.getCode() == 200 && result.getData() != null && result.getData().getRecords() != null) {
+            userTable.setItems(FXCollections.observableArrayList(result.getData().getRecords()));
+        }
+    }
+
+    @FXML
+    private void handleDeleteUser() {
+        User sel = userTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请选择要删除的用户");
+            return;
+        }
+        R<Void> result = userService.delete(sel.getUserid());
+        if (result.getCode() == 200) {
+            showInfo("用户已删除");
+            handleLoadAllUsers();
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleLoadAllTransactions() {
+        R<PageResult<TransactionRecord>> result = HttpClientUtil.get("/api/transaction?page=1&size=100",
+                new TypeReference<R<PageResult<TransactionRecord>>>() {});
+        if (result.getCode() == 200 && result.getData() != null && result.getData().getRecords() != null) {
+            allTransTable.setItems(FXCollections.observableArrayList(result.getData().getRecords()));
+        }
+    }
+
+    @FXML
+    private void handleDeleteTransaction() {
+        TransactionRecord sel = allTransTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请选择要删除的交易");
+            return;
+        }
+        R<Void> result = transactionService.delete(sel.getTransId());
+        if (result.getCode() == 200) {
+            showInfo("交易已删除");
+            handleLoadAllTransactions();
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleLogout() {
+        LoginUserHolder.clear();
+        try {
+            App.showLoginScene();
+        } catch (Exception e) {
+            showAlert("错误:" + e.getMessage());
+        }
+    }
+
+    private void setupProfile(User user) {
+        profileUsername.setText(user.getUsername());
+        profileNickname.setText(user.getNickname());
+        profileEmail.setText(user.getEmail());
+        profilePhone.setText(user.getPhone());
+        profileDept.setText(user.getDept());
+        profileCredit.setText(String.valueOf(user.getCredit()));
+    }
+
+    private void clearBookForm() {
+        newTitle.clear();
+        newAuthor.clear();
+        newCategory.clear();
+        newOriginalPrice.clear();
+        newSellingPrice.clear();
+        newStock.clear();
+    }
+
+    private void showAlert(String message) {
+        Alert alert = new Alert(Alert.AlertType.WARNING);
+        alert.setTitle("提示");
+        alert.setHeaderText(null);
+        TextArea textArea = new TextArea(message);
+        textArea.setEditable(false);
+        textArea.setWrapText(true);
+        textArea.setPrefWidth(360);
+        textArea.setPrefHeight(80);
+        alert.getDialogPane().setContent(textArea);
+        alert.showAndWait();
+    }
+
+    private void showInfo(String message) {
+        Alert alert = new Alert(Alert.AlertType.INFORMATION);
+        alert.setTitle("成功");
+        alert.setHeaderText(null);
+        TextArea textArea = new TextArea(message);
+        textArea.setEditable(false);
+        textArea.setWrapText(true);
+        textArea.setPrefWidth(360);
+        textArea.setPrefHeight(80);
+        alert.getDialogPane().setContent(textArea);
+        alert.showAndWait();
+    }
+}

+ 94 - 0
client/src/main/java/space/anyi/client/controller/RegisterController.java

@@ -0,0 +1,94 @@
+package space.anyi.client.controller;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.TextField;
+import javafx.scene.control.Alert;
+import javafx.scene.control.TextArea;
+import space.anyi.client.App;
+import space.anyi.client.service.UserService;
+import space.anyi.common.dto.R;
+import space.anyi.common.dto.RegisterRequest;
+import space.anyi.common.entity.User;
+
+public class RegisterController {
+
+    @FXML private TextField usernameField;
+    @FXML private PasswordField passwordField;
+    @FXML private TextField nicknameField;
+    @FXML private TextField emailField;
+    @FXML private TextField phoneField;
+    @FXML private TextField deptField;
+
+    private final UserService userService = new UserService();
+
+    @FXML
+    private void handleRegister() {
+        String username = usernameField.getText().trim();
+        String password = passwordField.getText().trim();
+        String nickname = nicknameField.getText().trim();
+        String email = emailField.getText().trim();
+        String phone = phoneField.getText().trim();
+        String dept = deptField.getText().trim();
+
+        if (username.isEmpty() || password.isEmpty()) {
+            showAlert("用户名和密码为必填项");
+            return;
+        }
+
+        RegisterRequest req = new RegisterRequest();
+        req.setUsername(username);
+        req.setPassword(password);
+        req.setNickname(nickname);
+        req.setEmail(email);
+        req.setPhone(phone);
+        req.setDept(dept);
+
+        R<User> result = userService.register(req);
+        if (result.getCode() == 200) {
+            showInfo("注册成功!请登录。");
+            try {
+                App.showLoginScene();
+            } catch (Exception e) {
+                showAlert("错误:" + e.getMessage());
+            }
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleBack() {
+        try {
+            App.showLoginScene();
+        } catch (Exception e) {
+            showAlert("Error: " + e.getMessage());
+        }
+    }
+
+    private void showAlert(String message) {
+        Alert alert = new Alert(Alert.AlertType.WARNING);
+        alert.setTitle("提示");
+        alert.setHeaderText(null);
+        TextArea textArea = new TextArea(message);
+        textArea.setEditable(false);
+        textArea.setWrapText(true);
+        textArea.setPrefWidth(360);
+        textArea.setPrefHeight(80);
+        alert.getDialogPane().setContent(textArea);
+        alert.showAndWait();
+    }
+
+    private void showInfo(String message) {
+        Alert alert = new Alert(Alert.AlertType.INFORMATION);
+        alert.setTitle("成功");
+        alert.setHeaderText(null);
+        TextArea textArea = new TextArea(message);
+        textArea.setEditable(false);
+        textArea.setWrapText(true);
+        textArea.setPrefWidth(360);
+        textArea.setPrefHeight(80);
+        alert.getDialogPane().setContent(textArea);
+        alert.showAndWait();
+    }
+}

+ 48 - 0
client/src/main/java/space/anyi/client/service/BookService.java

@@ -0,0 +1,48 @@
+package space.anyi.client.service;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import space.anyi.common.dto.PageResult;
+import space.anyi.common.dto.R;
+import space.anyi.common.entity.Book;
+import space.anyi.client.util.HttpClientUtil;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+public 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>>>() {});
+    }
+
+    public R<Book> getById(Integer id) {
+        return HttpClientUtil.get("/api/book/" + id, Book.class);
+    }
+
+    public R<List<Book>> getMyBooks(Integer sellerId) {
+        return HttpClientUtil.get("/api/book/my?sellerId=" + sellerId, new TypeReference<R<List<Book>>>() {});
+    }
+
+    public R<List<Book>> listAll() {
+        return HttpClientUtil.get("/api/book/all", new TypeReference<R<List<Book>>>() {});
+    }
+
+    public R<Book> create(Book book) {
+        return HttpClientUtil.post("/api/book", book, Book.class);
+    }
+
+    public R<Book> update(Integer id, Book book) {
+        return HttpClientUtil.put("/api/book/" + id, book, Book.class);
+    }
+
+    public R<Void> delete(Integer id) {
+        return HttpClientUtil.delete("/api/book/" + id, Void.class);
+    }
+}

+ 25 - 0
client/src/main/java/space/anyi/client/service/StatisticsService.java

@@ -0,0 +1,25 @@
+package space.anyi.client.service;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import space.anyi.common.dto.*;
+import space.anyi.client.util.HttpClientUtil;
+import java.util.List;
+
+public class StatisticsService {
+
+    public R<StatisticsSummary> getSummary() {
+        return HttpClientUtil.get("/api/statistics/summary", StatisticsSummary.class);
+    }
+
+    public R<List<DeptSalesDTO>> getTopDepartments() {
+        return HttpClientUtil.get("/api/statistics/top-departments", new TypeReference<R<List<DeptSalesDTO>>>() {});
+    }
+
+    public R<List<HotBookDTO>> getTopBooks() {
+        return HttpClientUtil.get("/api/statistics/top-books", new TypeReference<R<List<HotBookDTO>>>() {});
+    }
+
+    public R<List<DeptDiscountDTO>> getDiscountRates() {
+        return HttpClientUtil.get("/api/statistics/discount-rates", new TypeReference<R<List<DeptDiscountDTO>>>() {});
+    }
+}

+ 36 - 0
client/src/main/java/space/anyi/client/service/TransactionService.java

@@ -0,0 +1,36 @@
+package space.anyi.client.service;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import space.anyi.common.dto.R;
+import space.anyi.common.entity.TransactionRecord;
+import space.anyi.client.util.HttpClientUtil;
+import java.util.List;
+import java.util.Map;
+
+public class 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);
+    }
+
+    public R<TransactionRecord> getById(Integer id) {
+        return HttpClientUtil.get("/api/transaction/" + id, TransactionRecord.class);
+    }
+
+    public R<List<TransactionRecord>> getByUser(Integer userId) {
+        return HttpClientUtil.get("/api/transaction/user/" + userId, new TypeReference<R<List<TransactionRecord>>>() {});
+    }
+
+    public R<TransactionRecord> update(Integer id, TransactionRecord record) {
+        return HttpClientUtil.put("/api/transaction/" + id, record, TransactionRecord.class);
+    }
+
+    public R<Void> delete(Integer id) {
+        return HttpClientUtil.delete("/api/transaction/" + id, Void.class);
+    }
+}

+ 34 - 0
client/src/main/java/space/anyi/client/service/UserService.java

@@ -0,0 +1,34 @@
+package space.anyi.client.service;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import space.anyi.common.dto.R;
+import space.anyi.common.dto.LoginRequest;
+import space.anyi.common.dto.RegisterRequest;
+import space.anyi.common.entity.User;
+import space.anyi.client.util.HttpClientUtil;
+
+public class 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);
+    }
+
+    public R<User> register(RegisterRequest req) {
+        return HttpClientUtil.post("/api/user/register", req, User.class);
+    }
+
+    public R<User> getById(Integer id) {
+        return HttpClientUtil.get("/api/user/" + id, User.class);
+    }
+
+    public R<User> update(Integer id, User user) {
+        return HttpClientUtil.put("/api/user/" + id, user, User.class);
+    }
+
+    public R<Void> delete(Integer id) {
+        return HttpClientUtil.delete("/api/user/" + id, Void.class);
+    }
+}

+ 96 - 0
client/src/main/java/space/anyi/client/util/HttpClientUtil.java

@@ -0,0 +1,96 @@
+package space.anyi.client.util;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import space.anyi.common.dto.R;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+
+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> get(String path, TypeReference<R<T>> typeRef) {
+        return sendWithTypeRef("GET", path, null, typeRef);
+    }
+
+    public static <T> R<T> post(String path, Object body, Class<T> responseType) {
+        return send("POST", path, body, responseType);
+    }
+
+    public static <T> R<T> post(String path, Object body, TypeReference<R<T>> typeRef) {
+        return sendWithTypeRef("POST", path, body, typeRef);
+    }
+
+    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) {
+        try {
+            HttpRequest.Builder builder = HttpRequest.newBuilder()
+                    .uri(URI.create(BASE_URL + path))
+                    .header("Content-Type", "application/json");
+
+            String jsonBody = (body != null) ? mapper.writeValueAsString(body) : "";
+
+            HttpRequest request = switch (method) {
+                case "GET" -> builder.GET().build();
+                case "DELETE" -> builder.method("DELETE", HttpRequest.BodyPublishers.noBody()).build();
+                case "POST" -> builder.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
+                case "PUT" -> builder.PUT(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
+                default -> throw new IllegalArgumentException("Unsupported method: " + method);
+            };
+
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            return mapper.readValue(response.body(), mapper.getTypeFactory().constructParametricType(R.class, responseType));
+        } catch (Exception e) {
+            R<T> r = new R<>();
+            r.setCode(500);
+            r.setMessage("Connection error: " + e.getMessage());
+            return r;
+        }
+    }
+
+    private static <T> R<T> sendWithTypeRef(String method, String path, Object body, TypeReference<R<T>> typeRef) {
+        try {
+            HttpRequest.Builder builder = HttpRequest.newBuilder()
+                    .uri(URI.create(BASE_URL + path))
+                    .header("Content-Type", "application/json");
+
+            String jsonBody = (body != null) ? mapper.writeValueAsString(body) : "";
+
+            HttpRequest request = switch (method) {
+                case "GET" -> builder.GET().build();
+                case "POST" -> builder.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
+                default -> throw new IllegalArgumentException("Unsupported method: " + method);
+            };
+
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            return mapper.readValue(response.body(), typeRef);
+        } catch (Exception e) {
+            R<T> r = new R<>();
+            r.setCode(500);
+            r.setMessage("Connection error: " + e.getMessage());
+            return r;
+        }
+    }
+}

+ 27 - 0
client/src/main/java/space/anyi/client/util/LoginUserHolder.java

@@ -0,0 +1,27 @@
+package space.anyi.client.util;
+
+import space.anyi.common.entity.User;
+
+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;
+    }
+}

+ 13 - 0
client/src/main/resources/css/style.css

@@ -0,0 +1,13 @@
+.root {
+    -fx-font-family: "System";
+    -fx-font-size: 13px;
+}
+
+.button {
+    -fx-cursor: hand;
+}
+
+.table-view {
+    -fx-border-color: #bdc3c7;
+    -fx-border-radius: 3;
+}

+ 14 - 0
client/src/main/resources/fxml/login.fxml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+
+<VBox alignment="CENTER" spacing="15" xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.client.controller.LoginController" prefWidth="400" prefHeight="350">
+    <padding><Insets top="40" right="40" bottom="40" left="40"/></padding>
+    <Label text="校园书市" style="-fx-font-size: 22px; -fx-font-weight: bold;"/>
+    <TextField fx:id="usernameField" promptText="用户名" maxWidth="300"/>
+    <PasswordField fx:id="passwordField" promptText="密码" maxWidth="300"/>
+    <Button text="登录" onAction="#handleLogin" maxWidth="300" prefWidth="300"/>
+    <Hyperlink text="没有账号?立即注册" onAction="#handleRegister"/>
+</VBox>

+ 181 - 0
client/src/main/resources/fxml/main.fxml

@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+
+<BorderPane xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.client.controller.MainController" prefWidth="1000" prefHeight="700">
+    <top>
+        <HBox alignment="CENTER_LEFT" spacing="10" style="-fx-background-color: #2c3e50; -fx-padding: 10 15;">
+            <Label fx:id="welcomeLabel" text="欢迎" style="-fx-text-fill: white; -fx-font-size: 16px;" HBox.hgrow="ALWAYS"/>
+            <Button text="退出登录" onAction="#handleLogout" style="-fx-background-color: #e74c3c; -fx-text-fill: white;"/>
+        </HBox>
+    </top>
+    <center>
+        <TabPane>
+            <Tab text="浏览书籍" closable="false">
+                <VBox spacing="10" padding="10">
+                    <HBox spacing="10">
+                        <TextField fx:id="searchField" promptText="搜索书名/作者..." HBox.hgrow="ALWAYS"/>
+                        <ComboBox fx:id="categoryCombo" prefWidth="150"/>
+                        <Button text="搜索" onAction="#handleSearch"/>
+                    </HBox>
+                    <SplitPane dividerPositions="0.6" VBox.vgrow="ALWAYS">
+                        <TableView fx:id="bookTable">
+                            <columns>
+                                <TableColumn fx:id="bookIdCol" text="编号" prefWidth="50"/>
+                                <TableColumn fx:id="titleCol" text="书名" prefWidth="200"/>
+                                <TableColumn fx:id="authorCol" text="作者" prefWidth="120"/>
+                                <TableColumn fx:id="categoryCol" text="分类" prefWidth="100"/>
+                                <TableColumn fx:id="priceCol" text="价格" prefWidth="80"/>
+                                <TableColumn fx:id="stockCol" text="库存" prefWidth="60"/>
+                            </columns>
+                        </TableView>
+                        <VBox spacing="10" alignment="TOP_CENTER">
+                            <padding><Insets top="10"/></padding>
+                            <Label text="书籍详情"
+                                   style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                            <Label fx:id="detailTitle" text="-"/>
+                            <Label fx:id="detailAuthor" text="-"/>
+                            <Label fx:id="detailCategory" text="-"/>
+                            <Label fx:id="detailPrice" text="-"/>
+                            <Label fx:id="detailStock" text="-"/>
+                            <TextField fx:id="quantityField" promptText="数量" maxWidth="100"/>
+                            <Button text="购买" onAction="#handlePurchase" style="-fx-background-color: #27ae60; -fx-text-fill: white;"/>
+                        </VBox>
+                    </SplitPane>
+                </VBox>
+            </Tab>
+            <Tab text="我的交易" closable="false">
+                <VBox spacing="10" padding="10">
+                    <Button text="刷新" onAction="#handleLoadTransactions"/>
+                    <TableView fx:id="transTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="transIdCol" text="交易编号" prefWidth="80"/>
+                            <TableColumn fx:id="transBookIdCol" text="书籍编号" prefWidth="80"/>
+                            <TableColumn fx:id="transNumCol" text="数量" prefWidth="80"/>
+                            <TableColumn fx:id="transPriceCol" text="总价" prefWidth="100"/>
+                            <TableColumn fx:id="transTimeCol" text="时间" prefWidth="200"/>
+                        </columns>
+                    </TableView>
+                </VBox>
+            </Tab>
+            <Tab text="个人信息" closable="false">
+                <VBox spacing="12" padding="20" maxWidth="400">
+                    <Label text="个人信息"
+                           style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                    <HBox spacing="10"><Label text="用户名:" prefWidth="100"/><TextField fx:id="profileUsername" editable="false"/></HBox>
+                    <HBox spacing="10"><Label text="昵称:" prefWidth="100"/><TextField fx:id="profileNickname"/></HBox>
+                    <HBox spacing="10"><Label text="邮箱:" prefWidth="100"/><TextField fx:id="profileEmail"/></HBox>
+                    <HBox spacing="10"><Label text="电话:" prefWidth="100"/><TextField fx:id="profilePhone"/></HBox>
+                    <HBox spacing="10"><Label text="院系:" prefWidth="100"/><TextField fx:id="profileDept"/></HBox>
+                    <HBox spacing="10"><Label text="信用分:" prefWidth="100"/><Label fx:id="profileCredit"/></HBox>
+                    <Button text="保存修改" onAction="#handleUpdateProfile"/>
+                </VBox>
+            </Tab>
+            <Tab fx:id="sellerTab" text="卖家中心" closable="false">
+                <VBox spacing="10" padding="10">
+                    <Label text="我的书籍" style="-fx-font-weight: bold;"/>
+                    <Button text="刷新" onAction="#handleLoadMyBooks"/>
+                    <TableView fx:id="myBookTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="myBookIdCol" text="编号" prefWidth="50"/>
+                            <TableColumn fx:id="myBookTitleCol" text="书名" prefWidth="250"/>
+                            <TableColumn fx:id="myBookPriceCol" text="价格" prefWidth="80"/>
+                            <TableColumn fx:id="myBookStockCol" text="库存" prefWidth="60"/>
+                        </columns>
+                    </TableView>
+                    <Button text="删除选中" onAction="#handleDeleteBook"/>
+                    <TitledPane text="添加新书" expanded="false">
+                        <GridPane hgap="10" vgap="8" padding="10">
+                            <Label text="书名" GridPane.rowIndex="0" GridPane.columnIndex="0"/><TextField fx:id="newTitle" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
+                            <Label text="作者" GridPane.rowIndex="1" GridPane.columnIndex="0"/><TextField fx:id="newAuthor" GridPane.rowIndex="1" GridPane.columnIndex="1"/>
+                            <Label text="分类" GridPane.rowIndex="2" GridPane.columnIndex="0"/><TextField fx:id="newCategory" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
+                            <Label text="原价" GridPane.rowIndex="3" GridPane.columnIndex="0"/><TextField fx:id="newOriginalPrice" GridPane.rowIndex="3" GridPane.columnIndex="1"/>
+                            <Label text="售价" GridPane.rowIndex="4" GridPane.columnIndex="0"/><TextField fx:id="newSellingPrice" GridPane.rowIndex="4" GridPane.columnIndex="1"/>
+                            <Label text="库存" GridPane.rowIndex="5" GridPane.columnIndex="0"/><TextField fx:id="newStock" GridPane.rowIndex="5" GridPane.columnIndex="1"/>
+                            <Button text="添加书籍" onAction="#handleAddBook" GridPane.rowIndex="6" GridPane.columnIndex="1"/>
+                        </GridPane>
+                    </TitledPane>
+                </VBox>
+            </Tab>
+            <Tab text="数据统计" closable="false">
+                <ScrollPane fitToWidth="true">
+                    <VBox spacing="15" padding="15">
+                        <Button text="刷新数据" onAction="#handleLoadStatistics" maxWidth="200"/>
+                        <TitledPane text="总览" expanded="true">
+                            <HBox spacing="40" padding="10">
+                                <VBox spacing="5">
+                                    <Label text="交易总金额" style="-fx-font-weight: bold;"/>
+                                    <Label fx:id="statTotalAmount" text="-" style="-fx-font-size: 20px; -fx-text-fill: #27ae60;"/>
+                                </VBox>
+                                <VBox spacing="5">
+                                    <Label text="交易总笔数" style="-fx-font-weight: bold;"/>
+                                    <Label fx:id="statTotalCount" text="-" style="-fx-font-size: 20px;"/>
+                                </VBox>
+                            </HBox>
+                        </TitledPane>
+                        <TitledPane text="热门院系(销量前三)" expanded="true">
+                            <TableView fx:id="deptTable" prefHeight="150">
+                                <columns>
+                                    <TableColumn fx:id="deptNameCol" text="院系" prefWidth="250"/>
+                                    <TableColumn fx:id="deptSalesCol" text="销售额" prefWidth="200"/>
+                                </columns>
+                            </TableView>
+                        </TitledPane>
+                        <TitledPane text="热门书籍(前五)" expanded="true">
+                            <TableView fx:id="hotBookTable" prefHeight="150">
+                                <columns>
+                                    <TableColumn fx:id="hotBookIdCol" text="编号" prefWidth="50"/>
+                                    <TableColumn fx:id="hotBookTitleCol" text="书名" prefWidth="250"/>
+                                    <TableColumn fx:id="hotBookAuthorCol" text="作者" prefWidth="120"/>
+                                    <TableColumn fx:id="hotBookSoldCol" text="销量" prefWidth="80"/>
+                                </columns>
+                            </TableView>
+                        </TitledPane>
+                        <TitledPane text="各院系平均折扣率" expanded="true">
+                            <TableView fx:id="discountTable" prefHeight="150">
+                                <columns>
+                                    <TableColumn fx:id="discountDeptCol" text="院系" prefWidth="250"/>
+                                    <TableColumn fx:id="discountRateCol" text="平均折扣率" prefWidth="200"/>
+                                </columns>
+                            </TableView>
+                        </TitledPane>
+                    </VBox>
+                </ScrollPane>
+            </Tab>
+            <Tab fx:id="adminTab" text="管理" closable="false">
+                <VBox spacing="10" padding="10">
+                    <Label text="用户管理"
+                           style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                    <Button text="刷新用户" onAction="#handleLoadAllUsers"/>
+                    <TableView fx:id="userTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="userIdCol" text="编号" prefWidth="50"/>
+                            <TableColumn fx:id="userNameCol" text="用户名" prefWidth="120"/>
+                            <TableColumn fx:id="userNicknameCol" text="昵称" prefWidth="120"/>
+                            <TableColumn fx:id="userDeptCol" text="院系" prefWidth="150"/>
+                            <TableColumn fx:id="userCreditCol" text="信用分" prefWidth="60"/>
+                            <TableColumn fx:id="userRoleCol" text="角色" prefWidth="60"/>
+                            <TableColumn fx:id="userSellerCol" text="卖家" prefWidth="60"/>
+                        </columns>
+                    </TableView>
+                    <Button text="删除选中用户" onAction="#handleDeleteUser" style="-fx-background-color: #e74c3c; -fx-text-fill: white;"/>
+                    <Label text="全部交易" style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                    <Button text="刷新交易" onAction="#handleLoadAllTransactions"/>
+                    <TableView fx:id="allTransTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="allTransIdCol" text="交易编号" prefWidth="60"/>
+                            <TableColumn fx:id="allTransBookIdCol" text="书籍编号" prefWidth="60"/>
+                            <TableColumn fx:id="allTransBuyerIdCol" text="买家编号" prefWidth="60"/>
+                            <TableColumn fx:id="allTransNumCol" text="数量" prefWidth="40"/>
+                            <TableColumn fx:id="allTransPriceCol" text="价格" prefWidth="100"/>
+                            <TableColumn fx:id="allTransTimeCol" text="时间" prefWidth="200"/>
+                        </columns>
+                    </TableView>
+                    <Button text="删除选中交易" onAction="#handleDeleteTransaction" style="-fx-background-color: #e74c3c; -fx-text-fill: white;"/>
+                </VBox>
+            </Tab>
+        </TabPane>
+    </center>
+</BorderPane>

+ 18 - 0
client/src/main/resources/fxml/register.fxml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+
+<VBox alignment="CENTER" spacing="12" xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.client.controller.RegisterController" prefWidth="400" prefHeight="500">
+    <padding><Insets top="30" right="40" bottom="30" left="40"/></padding>
+    <Label text="创建账号" style="-fx-font-size: 20px; -fx-font-weight: bold;"/>
+    <TextField fx:id="usernameField" promptText="用户名 *" maxWidth="300"/>
+    <PasswordField fx:id="passwordField" promptText="密码 *" maxWidth="300"/>
+    <TextField fx:id="nicknameField" promptText="昵称" maxWidth="300"/>
+    <TextField fx:id="emailField" promptText="邮箱" maxWidth="300"/>
+    <TextField fx:id="phoneField" promptText="电话" maxWidth="300"/>
+    <TextField fx:id="deptField" promptText="院系" maxWidth="300"/>
+    <Button text="注册" onAction="#handleRegister" maxWidth="300" prefWidth="300"/>
+    <Hyperlink text="已有账号?去登录" onAction="#handleBack"/>
+</VBox>

+ 11 - 7
common/pom.xml

@@ -11,10 +11,14 @@
 
     <artifactId>common</artifactId>
 
-    <properties>
-        <maven.compiler.source>23</maven.compiler.source>
-        <maven.compiler.target>23</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    </properties>
-
-</project>
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 10 - 0
common/src/main/java/space/anyi/common/dto/DeptDiscountDTO.java

@@ -0,0 +1,10 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class DeptDiscountDTO {
+    private String dept;
+    private BigDecimal avgDiscountRate;
+}

+ 10 - 0
common/src/main/java/space/anyi/common/dto/DeptSalesDTO.java

@@ -0,0 +1,10 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class DeptSalesDTO {
+    private String dept;
+    private BigDecimal salesAmount;
+}

+ 11 - 0
common/src/main/java/space/anyi/common/dto/HotBookDTO.java

@@ -0,0 +1,11 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+
+@Data
+public class HotBookDTO {
+    private Integer bookId;
+    private String title;
+    private String author;
+    private int totalSold;
+}

+ 9 - 0
common/src/main/java/space/anyi/common/dto/LoginRequest.java

@@ -0,0 +1,9 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+
+@Data
+public class LoginRequest {
+    private String username;
+    private String password;
+}

+ 12 - 0
common/src/main/java/space/anyi/common/dto/PageResult.java

@@ -0,0 +1,12 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class PageResult<T> {
+    private List<T> records;
+    private long total;
+    private long size;
+    private long current;
+}

+ 33 - 0
common/src/main/java/space/anyi/common/dto/R.java

@@ -0,0 +1,33 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+
+@Data
+public class R<T> {
+    private int code;
+    private String message;
+    private T data;
+
+    public static <T> R<T> ok(T data) {
+        R<T> r = new R<>();
+        r.code = 200;
+        r.message = "success";
+        r.data = data;
+        return r;
+    }
+
+    public static <T> R<T> ok() {
+        return ok(null);
+    }
+
+    public static <T> R<T> error(int code, String message) {
+        R<T> r = new R<>();
+        r.code = code;
+        r.message = message;
+        return r;
+    }
+
+    public static <T> R<T> error(String message) {
+        return error(400, message);
+    }
+}

+ 13 - 0
common/src/main/java/space/anyi/common/dto/RegisterRequest.java

@@ -0,0 +1,13 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+
+@Data
+public class RegisterRequest {
+    private String username;
+    private String password;
+    private String nickname;
+    private String email;
+    private String phone;
+    private String dept;
+}

+ 10 - 0
common/src/main/java/space/anyi/common/dto/StatisticsSummary.java

@@ -0,0 +1,10 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class StatisticsSummary {
+    private BigDecimal totalAmount;
+    private long totalCount;
+}

+ 24 - 0
common/src/main/java/space/anyi/common/entity/Book.java

@@ -0,0 +1,24 @@
+package space.anyi.common.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("book")
+public class Book {
+    @TableId(value = "book_id", type = IdType.AUTO)
+    private Integer bookId;
+    private Integer sellerId;
+    private String title;
+    private String author;
+    private String category;
+    private BigDecimal originalPrice;
+    private BigDecimal sellingPrice;
+    private String imageUrl;
+    private LocalDateTime createdAt;
+    private Integer stock;
+}

+ 20 - 0
common/src/main/java/space/anyi/common/entity/TransactionRecord.java

@@ -0,0 +1,20 @@
+package space.anyi.common.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("transaction_record")
+public class TransactionRecord {
+    @TableId(value = "trans_id", type = IdType.AUTO)
+    private Integer transId;
+    private Integer bookId;
+    private Integer buyerId;
+    private Integer transactionNum;
+    private BigDecimal transactionPrice;
+    private LocalDateTime transactionTime;
+}

+ 26 - 0
common/src/main/java/space/anyi/common/entity/User.java

@@ -0,0 +1,26 @@
+package space.anyi.common.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("user")
+public class User {
+    @TableId(type = IdType.AUTO)
+    private Integer userid;
+    private String username;
+    private String password;
+    private String nickname;
+    private String email;
+    private String phone;
+    private String dept;
+    private Integer credit;
+    private Integer role;
+    @TableField("is_seller")
+    private Integer isSeller;
+    private LocalDateTime createdAt;
+}

+ 23 - 0
common/src/main/java/space/anyi/common/enums/UserRole.java

@@ -0,0 +1,23 @@
+package space.anyi.common.enums;
+
+public enum UserRole {
+    STUDENT(0),
+    ADMIN(1);
+
+    private final int value;
+
+    UserRole(int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    public static UserRole fromValue(int value) {
+        for (UserRole role : values()) {
+            if (role.value == value) return role;
+        }
+        return STUDENT;
+    }
+}

+ 97 - 1
pom.xml

@@ -18,6 +18,102 @@
         <maven.compiler.source>23</maven.compiler.source>
         <maven.compiler.target>23</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-boot.version>3.4.4</spring-boot.version>
+        <mybatis-plus.version>3.5.9</mybatis-plus.version>
+        <mysql-connector.version>8.4.0</mysql-connector.version>
+        <javafx.version>23.0.2</javafx.version>
+        <jackson.version>2.17.3</jackson.version>
+        <lombok.version>1.18.36</lombok.version>
     </properties>
 
-</project>
+    <dependencyManagement>
+        <dependencies>
+            <!-- Spring Boot -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- MyBatis-Plus -->
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-jsqlparser</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+            <!-- MySQL -->
+            <dependency>
+                <groupId>com.mysql</groupId>
+                <artifactId>mysql-connector-j</artifactId>
+                <version>${mysql-connector.version}</version>
+            </dependency>
+
+            <!-- JavaFX -->
+            <dependency>
+                <groupId>org.openjfx</groupId>
+                <artifactId>javafx-controls</artifactId>
+                <version>${javafx.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.openjfx</groupId>
+                <artifactId>javafx-fxml</artifactId>
+                <version>${javafx.version}</version>
+            </dependency>
+
+            <!-- Jackson -->
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-databind</artifactId>
+                <version>${jackson.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson.datatype</groupId>
+                <artifactId>jackson-datatype-jsr310</artifactId>
+                <version>${jackson.version}</version>
+            </dependency>
+
+            <!-- Lombok -->
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+                <scope>provided</scope>
+            </dependency>
+
+            <!-- Intra-project -->
+            <dependency>
+                <groupId>space.anyi</groupId>
+                <artifactId>common</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <configuration>
+                        <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok</artifactId>
+                                <version>${lombok.version}</version>
+                            </path>
+                        </annotationProcessorPaths>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+</project>

+ 30 - 7
server/pom.xml

@@ -11,17 +11,40 @@
 
     <artifactId>server</artifactId>
 
-    <properties>
-        <maven.compiler.source>23</maven.compiler.source>
-        <maven.compiler.target>23</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    </properties>
-
     <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
         <dependency>
             <groupId>space.anyi</groupId>
             <artifactId>common</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-jsqlparser</artifactId>
+        </dependency>
     </dependencies>
 
-</project>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 12 - 0
server/src/main/java/space/anyi/server/ServerApplication.java

@@ -0,0 +1,12 @@
+package space.anyi.server;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+@MapperScan("space.anyi.server.mapper")
+@SpringBootApplication
+public class ServerApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ServerApplication.class, args);
+    }
+}

+ 26 - 0
server/src/main/java/space/anyi/server/config/GlobalExceptionHandler.java

@@ -0,0 +1,26 @@
+package space.anyi.server.config;
+
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import space.anyi.common.dto.R;
+
+@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());
+    }
+}

+ 17 - 0
server/src/main/java/space/anyi/server/config/MyBatisPlusConfig.java

@@ -0,0 +1,17 @@
+package space.anyi.server.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MyBatisPlusConfig {
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}

+ 23 - 0
server/src/main/java/space/anyi/server/config/WebConfig.java

@@ -0,0 +1,23 @@
+package space.anyi.server.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig {
+    @Bean
+    public WebMvcConfigurer corsConfigurer() {
+        return new WebMvcConfigurer() {
+            @Override
+            public void addCorsMappings(CorsRegistry registry) {
+                registry.addMapping("/**")
+                        .allowedOriginPatterns("*")
+                        .allowedMethods("*")
+                        .allowedHeaders("*")
+                        .allowCredentials(true);
+            }
+        };
+    }
+}

+ 74 - 0
server/src/main/java/space/anyi/server/controller/BookController.java

@@ -0,0 +1,74 @@
+package space.anyi.server.controller;
+
+import org.springframework.web.bind.annotation.*;
+import space.anyi.common.dto.PageResult;
+import space.anyi.common.dto.R;
+import space.anyi.common.entity.Book;
+import space.anyi.server.service.BookService;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@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("/all")
+    public R<List<Book>> listAll() {
+        return R.ok(bookService.listAll());
+    }
+
+    @GetMapping("/{id}")
+    public R<Book> getById(@PathVariable Integer id) {
+        Book book = bookService.getById(id);
+        if (book == null) {
+            return R.error("Book not found");
+        }
+        return R.ok(book);
+    }
+
+    @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());
+        bookService.save(book);
+        return R.ok(book);
+    }
+
+    @PutMapping("/{id}")
+    public R<Book> update(@PathVariable Integer id, @RequestBody Book book) {
+        book.setBookId(id);
+        bookService.updateById(book);
+        return R.ok(bookService.getById(id));
+    }
+
+    @DeleteMapping("/{id}")
+    public R<Void> delete(@PathVariable Integer id) {
+        bookService.removeById(id);
+        return R.ok();
+    }
+}

+ 37 - 0
server/src/main/java/space/anyi/server/controller/StatisticsController.java

@@ -0,0 +1,37 @@
+package space.anyi.server.controller;
+
+import org.springframework.web.bind.annotation.*;
+import space.anyi.common.dto.*;
+import space.anyi.server.service.StatisticsService;
+import java.util.List;
+
+@RestController
+@RequestMapping("/statistics")
+public class StatisticsController {
+
+    private final StatisticsService statisticsService;
+
+    public StatisticsController(StatisticsService statisticsService) {
+        this.statisticsService = statisticsService;
+    }
+
+    @GetMapping("/summary")
+    public R<StatisticsSummary> getSummary() {
+        return R.ok(statisticsService.getSummary());
+    }
+
+    @GetMapping("/top-departments")
+    public R<List<DeptSalesDTO>> getTopDepartments() {
+        return R.ok(statisticsService.getTopDepartments());
+    }
+
+    @GetMapping("/top-books")
+    public R<List<HotBookDTO>> getTopBooks() {
+        return R.ok(statisticsService.getTopBooks());
+    }
+
+    @GetMapping("/discount-rates")
+    public R<List<DeptDiscountDTO>> getDiscountRates() {
+        return R.ok(statisticsService.getDeptDiscountRates());
+    }
+}

+ 67 - 0
server/src/main/java/space/anyi/server/controller/TransactionController.java

@@ -0,0 +1,67 @@
+package space.anyi.server.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.common.dto.R;
+import space.anyi.common.entity.TransactionRecord;
+import space.anyi.server.service.TransactionService;
+import java.util.List;
+import java.util.Map;
+
+@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
+    public R<IPage<TransactionRecord>> listAll(@RequestParam(defaultValue = "1") int page,
+                                                @RequestParam(defaultValue = "20") int size) {
+        return R.ok(transactionService.page(new Page<>(page, size)));
+    }
+
+    @GetMapping("/{id}")
+    public R<TransactionRecord> getById(@PathVariable Integer id) {
+        TransactionRecord record = transactionService.getById(id);
+        if (record == null) {
+            return R.error("Transaction not found");
+        }
+        return R.ok(record);
+    }
+
+    @GetMapping("/user/{userId}")
+    public R<List<TransactionRecord>> getByUser(@PathVariable Integer userId) {
+        return R.ok(transactionService.getByUserId(userId));
+    }
+
+    @PutMapping("/{id}")
+    public R<TransactionRecord> update(@PathVariable Integer id, @RequestBody TransactionRecord record) {
+        record.setTransId(id);
+        transactionService.updateById(record);
+        return R.ok(transactionService.getById(id));
+    }
+
+    @DeleteMapping("/{id}")
+    public R<Void> delete(@PathVariable Integer id) {
+        transactionService.removeById(id);
+        return R.ok();
+    }
+}

+ 71 - 0
server/src/main/java/space/anyi/server/controller/UserController.java

@@ -0,0 +1,71 @@
+package space.anyi.server.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.common.dto.LoginRequest;
+import space.anyi.common.dto.R;
+import space.anyi.common.dto.RegisterRequest;
+import space.anyi.common.entity.User;
+import space.anyi.server.service.UserService;
+
+@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 created = userService.register(user);
+        return R.ok(created);
+    }
+
+    @GetMapping
+    public R<IPage<User>> listAll(@RequestParam(defaultValue = "1") int page,
+                                   @RequestParam(defaultValue = "20") int size) {
+        return R.ok(userService.page(new Page<>(page, size)));
+    }
+
+    @GetMapping("/{id}")
+    public R<User> getById(@PathVariable Integer id) {
+        User user = userService.getById(id);
+        if (user == null) {
+            return R.error("User not found");
+        }
+        return R.ok(user);
+    }
+
+    @PutMapping("/{id}")
+    public R<User> update(@PathVariable Integer id, @RequestBody User user) {
+        user.setUserid(id);
+        userService.updateById(user);
+        return R.ok(userService.getById(id));
+    }
+
+    @DeleteMapping("/{id}")
+    public R<Void> delete(@PathVariable Integer id) {
+        userService.removeById(id);
+        return R.ok();
+    }
+}

+ 7 - 0
server/src/main/java/space/anyi/server/mapper/BookMapper.java

@@ -0,0 +1,7 @@
+package space.anyi.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import space.anyi.common.entity.Book;
+
+public interface BookMapper extends BaseMapper<Book> {
+}

+ 36 - 0
server/src/main/java/space/anyi/server/mapper/StatisticsMapper.java

@@ -0,0 +1,36 @@
+package space.anyi.server.mapper;
+
+import org.apache.ibatis.annotations.Select;
+import space.anyi.common.dto.DeptDiscountDTO;
+import space.anyi.common.dto.DeptSalesDTO;
+import space.anyi.common.dto.HotBookDTO;
+import space.anyi.common.dto.StatisticsSummary;
+import java.util.List;
+
+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();
+}

+ 7 - 0
server/src/main/java/space/anyi/server/mapper/TransactionRecordMapper.java

@@ -0,0 +1,7 @@
+package space.anyi.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import space.anyi.common.entity.TransactionRecord;
+
+public interface TransactionRecordMapper extends BaseMapper<TransactionRecord> {
+}

+ 7 - 0
server/src/main/java/space/anyi/server/mapper/UserMapper.java

@@ -0,0 +1,7 @@
+package space.anyi.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import space.anyi.common.entity.User;
+
+public interface UserMapper extends BaseMapper<User> {
+}

+ 12 - 0
server/src/main/java/space/anyi/server/service/BookService.java

@@ -0,0 +1,12 @@
+package space.anyi.server.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.common.entity.Book;
+import java.util.List;
+
+public interface BookService extends IService<Book> {
+    IPage<Book> searchBooks(String keyword, String category, int page, int size);
+    List<Book> getBySellerId(Integer sellerId);
+    List<Book> listAll();
+}

+ 14 - 0
server/src/main/java/space/anyi/server/service/StatisticsService.java

@@ -0,0 +1,14 @@
+package space.anyi.server.service;
+
+import space.anyi.common.dto.DeptDiscountDTO;
+import space.anyi.common.dto.DeptSalesDTO;
+import space.anyi.common.dto.HotBookDTO;
+import space.anyi.common.dto.StatisticsSummary;
+import java.util.List;
+
+public interface StatisticsService {
+    StatisticsSummary getSummary();
+    List<DeptSalesDTO> getTopDepartments();
+    List<HotBookDTO> getTopBooks();
+    List<DeptDiscountDTO> getDeptDiscountRates();
+}

+ 10 - 0
server/src/main/java/space/anyi/server/service/TransactionService.java

@@ -0,0 +1,10 @@
+package space.anyi.server.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.common.entity.TransactionRecord;
+import java.util.List;
+
+public interface TransactionService extends IService<TransactionRecord> {
+    TransactionRecord purchase(Integer bookId, Integer buyerId, Integer quantity);
+    List<TransactionRecord> getByUserId(Integer userId);
+}

+ 9 - 0
server/src/main/java/space/anyi/server/service/UserService.java

@@ -0,0 +1,9 @@
+package space.anyi.server.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.common.entity.User;
+
+public interface UserService extends IService<User> {
+    User login(String username, String password);
+    User register(User user);
+}

+ 42 - 0
server/src/main/java/space/anyi/server/service/impl/BookServiceImpl.java

@@ -0,0 +1,42 @@
+package space.anyi.server.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import space.anyi.common.entity.Book;
+import space.anyi.server.mapper.BookMapper;
+import space.anyi.server.service.BookService;
+import java.util.List;
+
+@Service
+public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements BookService {
+
+    @Override
+    public IPage<Book> searchBooks(String keyword, String category, int page, int size) {
+        LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
+        if (keyword != null && !keyword.isBlank()) {
+            wrapper.like(Book::getTitle, keyword)
+                   .or().like(Book::getAuthor, keyword);
+        }
+        if (category != null && !category.isBlank()) {
+            wrapper.eq(Book::getCategory, category);
+        }
+        wrapper.orderByDesc(Book::getCreatedAt);
+        return page(new Page<>(page, size), wrapper);
+    }
+
+    @Override
+    public List<Book> getBySellerId(Integer sellerId) {
+        LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Book::getSellerId, sellerId)
+               .orderByDesc(Book::getCreatedAt);
+        return list(wrapper);
+    }
+
+    @Override
+    public List<Book> listAll() {
+        return lambdaQuery().orderByDesc(Book::getCreatedAt).list();
+    }
+}

+ 40 - 0
server/src/main/java/space/anyi/server/service/impl/StatisticsServiceImpl.java

@@ -0,0 +1,40 @@
+package space.anyi.server.service.impl;
+
+import org.springframework.stereotype.Service;
+import space.anyi.common.dto.DeptDiscountDTO;
+import space.anyi.common.dto.DeptSalesDTO;
+import space.anyi.common.dto.HotBookDTO;
+import space.anyi.common.dto.StatisticsSummary;
+import space.anyi.server.mapper.StatisticsMapper;
+import space.anyi.server.service.StatisticsService;
+import java.util.List;
+
+@Service
+public class StatisticsServiceImpl implements StatisticsService {
+
+    private final StatisticsMapper statisticsMapper;
+
+    public StatisticsServiceImpl(StatisticsMapper statisticsMapper) {
+        this.statisticsMapper = statisticsMapper;
+    }
+
+    @Override
+    public StatisticsSummary getSummary() {
+        return statisticsMapper.selectSummary();
+    }
+
+    @Override
+    public List<DeptSalesDTO> getTopDepartments() {
+        return statisticsMapper.selectTopDepartments();
+    }
+
+    @Override
+    public List<HotBookDTO> getTopBooks() {
+        return statisticsMapper.selectTopBooks();
+    }
+
+    @Override
+    public List<DeptDiscountDTO> getDeptDiscountRates() {
+        return statisticsMapper.selectDeptDiscountRates();
+    }
+}

+ 56 - 0
server/src/main/java/space/anyi/server/service/impl/TransactionServiceImpl.java

@@ -0,0 +1,56 @@
+package space.anyi.server.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import space.anyi.common.entity.Book;
+import space.anyi.common.entity.TransactionRecord;
+import space.anyi.server.mapper.TransactionRecordMapper;
+import space.anyi.server.service.BookService;
+import space.anyi.server.service.TransactionService;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class TransactionServiceImpl extends ServiceImpl<TransactionRecordMapper, TransactionRecord> implements TransactionService {
+
+    private final BookService bookService;
+
+    public TransactionServiceImpl(BookService bookService) {
+        this.bookService = bookService;
+    }
+
+    @Override
+    @Transactional
+    public TransactionRecord purchase(Integer bookId, Integer buyerId, Integer quantity) {
+        Book book = bookService.getById(bookId);
+        if (book == null) {
+            throw new RuntimeException("Book not found");
+        }
+        if (book.getStock() < quantity) {
+            throw new RuntimeException("Insufficient stock");
+        }
+
+        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;
+    }
+
+    @Override
+    public List<TransactionRecord> getByUserId(Integer userId) {
+        LambdaQueryWrapper<TransactionRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TransactionRecord::getBuyerId, userId)
+               .orderByDesc(TransactionRecord::getTransactionTime);
+        return list(wrapper);
+    }
+}

+ 31 - 0
server/src/main/java/space/anyi/server/service/impl/UserServiceImpl.java

@@ -0,0 +1,31 @@
+package space.anyi.server.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import space.anyi.common.entity.User;
+import space.anyi.server.mapper.UserMapper;
+import space.anyi.server.service.UserService;
+import java.time.LocalDateTime;
+
+@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);
+        user.setRole(0);
+        user.setIsSeller(0);
+        user.setCreatedAt(LocalDateTime.now());
+        save(user);
+        return user;
+    }
+}

+ 18 - 0
server/src/main/resources/application.yml

@@ -0,0 +1,18 @@
+server:
+  port: 8080
+  servlet:
+    context-path: /api
+
+spring:
+  datasource:
+    url: jdbc:mysql://localhost:3306/campus_book_trade?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
+    username: root
+    password: root
+    driver-class-name: com.mysql.cj.jdbc.Driver
+
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true
+  global-config:
+    db-config:
+      id-type: auto

+ 4 - 0
server/src/main/resources/mapper/BookMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.server.mapper.BookMapper">
+</mapper>

+ 4 - 0
server/src/main/resources/mapper/StatisticsMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.server.mapper.StatisticsMapper">
+</mapper>

+ 4 - 0
server/src/main/resources/mapper/TransactionRecordMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.server.mapper.TransactionRecordMapper">
+</mapper>

+ 4 - 0
server/src/main/resources/mapper/UserMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.server.mapper.UserMapper">
+</mapper>