Browse Source

feat:完成所有功能的页面编写和服务端API编写

yangyi 1 month ago
parent
commit
7bf0ba27b5

+ 336 - 15
client/src/main/java/space/anyi/client/controller/MainController.java

@@ -6,7 +6,9 @@ import javafx.fxml.FXML;
 import javafx.scene.control.*;
 import javafx.scene.control.cell.PropertyValueFactory;
 import javafx.scene.control.TextArea;
+import javafx.scene.layout.HBox;
 import space.anyi.client.App;
+import space.anyi.client.service.AiService;
 import space.anyi.client.service.BookService;
 import space.anyi.client.service.StatisticsService;
 import space.anyi.client.service.TransactionService;
@@ -19,9 +21,12 @@ 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.RegisterRequest;
 import space.anyi.common.dto.HotBookDTO;
 import space.anyi.common.dto.DeptDiscountDTO;
+import space.anyi.common.dto.SalesRecordDTO;
 import space.anyi.common.dto.StatisticsSummary;
+import java.math.BigDecimal;
 import java.util.List;
 
 public class MainController {
@@ -68,6 +73,8 @@ public class MainController {
     @FXML private TableColumn<Book, String> myBookTitleCol;
     @FXML private TableColumn<Book, java.math.BigDecimal> myBookPriceCol;
     @FXML private TableColumn<Book, Integer> myBookStockCol;
+    @FXML private TitledPane bookFormPane;
+    @FXML private Button saveBookBtn;
     @FXML private TextField newTitle;
     @FXML private TextField newAuthor;
     @FXML private TextField newCategory;
@@ -93,6 +100,14 @@ public class MainController {
     // Admin tab
     @FXML private Tab adminTab;
     @FXML private TableView<User> userTable;
+    @FXML private TitledPane userFormPane;
+    @FXML private Button saveUserBtn;
+    @FXML private TextField adminUsername;
+    @FXML private PasswordField adminPassword;
+    @FXML private TextField adminNickname;
+    @FXML private TextField adminEmail;
+    @FXML private TextField adminPhone;
+    @FXML private TextField adminDept;
     @FXML private TableColumn<User, Integer> userIdCol;
     @FXML private TableColumn<User, String> userNameCol;
     @FXML private TableColumn<User, String> userNicknameCol;
@@ -100,6 +115,20 @@ public class MainController {
     @FXML private TableColumn<User, Integer> userCreditCol;
     @FXML private TableColumn<User, Integer> userRoleCol;
     @FXML private TableColumn<User, Integer> userSellerCol;
+    @FXML private TableView<Book> adminBookTable;
+    @FXML private TableColumn<Book, Integer> adminBookIdCol;
+    @FXML private TableColumn<Book, String> adminBookTitleCol;
+    @FXML private TableColumn<Book, String> adminBookAuthorCol;
+    @FXML private TableColumn<Book, String> adminBookCategoryCol;
+    @FXML private TableColumn<Book, java.math.BigDecimal> adminBookPriceCol;
+    @FXML private TableColumn<Book, Integer> adminBookStockCol;
+    @FXML private TableColumn<Book, Integer> adminBookSellerCol;
+    @FXML private TableView<Book> pendingBookTable;
+    @FXML private TableColumn<Book, Integer> pendingBookIdCol;
+    @FXML private TableColumn<Book, String> pendingBookTitleCol;
+    @FXML private TableColumn<Book, String> pendingBookAuthorCol;
+    @FXML private TableColumn<Book, Integer> pendingBookSellerCol;
+    @FXML private TableColumn<Book, java.math.BigDecimal> pendingBookPriceCol;
     @FXML private TableView<TransactionRecord> allTransTable;
     @FXML private TableColumn<TransactionRecord, Integer> allTransIdCol;
     @FXML private TableColumn<TransactionRecord, Integer> allTransBookIdCol;
@@ -107,13 +136,17 @@ public class MainController {
     @FXML private TableColumn<TransactionRecord, Integer> allTransNumCol;
     @FXML private TableColumn<TransactionRecord, java.math.BigDecimal> allTransPriceCol;
     @FXML private TableColumn<TransactionRecord, String> allTransTimeCol;
+    @FXML private TextArea analysisResult;
 
     private final BookService bookService = new BookService();
+    private final AiService aiService = new AiService();
     private final TransactionService transactionService = new TransactionService();
     private final UserService userService = new UserService();
     private final StatisticsService statisticsService = new StatisticsService();
 
     private Book selectedBook;
+    private Book editingBook;
+    private User editingUser;
 
     @FXML
     public void initialize() {
@@ -196,6 +229,20 @@ public class MainController {
         userRoleCol.setCellValueFactory(new PropertyValueFactory<>("role"));
         userSellerCol.setCellValueFactory(new PropertyValueFactory<>("isSeller"));
 
+        adminBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
+        adminBookTitleCol.setCellValueFactory(new PropertyValueFactory<>("title"));
+        adminBookAuthorCol.setCellValueFactory(new PropertyValueFactory<>("author"));
+        adminBookCategoryCol.setCellValueFactory(new PropertyValueFactory<>("category"));
+        adminBookPriceCol.setCellValueFactory(new PropertyValueFactory<>("sellingPrice"));
+        adminBookStockCol.setCellValueFactory(new PropertyValueFactory<>("stock"));
+        adminBookSellerCol.setCellValueFactory(new PropertyValueFactory<>("sellerId"));
+
+        pendingBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
+        pendingBookTitleCol.setCellValueFactory(new PropertyValueFactory<>("title"));
+        pendingBookAuthorCol.setCellValueFactory(new PropertyValueFactory<>("author"));
+        pendingBookSellerCol.setCellValueFactory(new PropertyValueFactory<>("sellerId"));
+        pendingBookPriceCol.setCellValueFactory(new PropertyValueFactory<>("sellingPrice"));
+
         allTransIdCol.setCellValueFactory(new PropertyValueFactory<>("transId"));
         allTransBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
         allTransBuyerIdCol.setCellValueFactory(new PropertyValueFactory<>("buyerId"));
@@ -283,25 +330,67 @@ public class MainController {
         }
     }
 
+    @FXML
+    private void handleEditBook() {
+        Book sel = myBookTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请先选择要编辑的书籍");
+            return;
+        }
+        editingBook = sel;
+        newTitle.setText(sel.getTitle());
+        newAuthor.setText(sel.getAuthor());
+        newCategory.setText(sel.getCategory());
+        newOriginalPrice.setText(sel.getOriginalPrice().toString());
+        newSellingPrice.setText(sel.getSellingPrice().toString());
+        newStock.setText(String.valueOf(sel.getStock()));
+        bookFormPane.setText("编辑书籍");
+        saveBookBtn.setText("保存修改");
+        bookFormPane.setExpanded(true);
+    }
+
     @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();
+            String title = newTitle.getText().trim();
+            String author = newAuthor.getText().trim();
+            String category = newCategory.getText().trim();
+            BigDecimal originalPrice = new BigDecimal(newOriginalPrice.getText().trim());
+            BigDecimal sellingPrice = new BigDecimal(newSellingPrice.getText().trim());
+            int stock = Integer.parseInt(newStock.getText().trim());
+
+            if (editingBook != null) {
+                editingBook.setTitle(title);
+                editingBook.setAuthor(author);
+                editingBook.setCategory(category);
+                editingBook.setOriginalPrice(originalPrice);
+                editingBook.setSellingPrice(sellingPrice);
+                editingBook.setStock(stock);
+                R<Book> result = bookService.update(editingBook.getBookId(), editingBook);
+                if (result.getCode() == 200) {
+                    showInfo("书籍已更新!");
+                    clearBookForm();
+                    handleLoadMyBooks();
+                } else {
+                    showAlert(result.getMessage());
+                }
             } else {
-                showAlert(result.getMessage());
+                Book book = new Book();
+                book.setSellerId(LoginUserHolder.get().getUserid());
+                book.setTitle(title);
+                book.setAuthor(author);
+                book.setCategory(category);
+                book.setOriginalPrice(originalPrice);
+                book.setSellingPrice(sellingPrice);
+                book.setStock(stock);
+                R<Book> result = bookService.create(book);
+                if (result.getCode() == 200) {
+                    showInfo("书籍已上架!");
+                    clearBookForm();
+                    handleLoadMyBooks();
+                } else {
+                    showAlert(result.getMessage());
+                }
             }
         } catch (Exception e) {
             showAlert("输入无效:" + e.getMessage());
@@ -324,6 +413,38 @@ public class MainController {
         }
     }
 
+    @FXML
+    private void handleViewSales() {
+        Book sel = myBookTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请先选择要查看的书籍");
+            return;
+        }
+        R<List<SalesRecordDTO>> result = bookService.getSales(sel.getBookId());
+        if (result.getCode() != 200 || result.getData() == null || result.getData().isEmpty()) {
+            showInfo("暂无销售记录");
+            return;
+        }
+        Dialog<Void> dialog = new Dialog<>();
+        dialog.setTitle("销售记录 - " + sel.getTitle());
+        dialog.getDialogPane().setPrefSize(600, 400);
+        TableView<SalesRecordDTO> table = new TableView<>();
+        TableColumn<SalesRecordDTO, String> buyerCol = new TableColumn<>("买家");
+        buyerCol.setCellValueFactory(new PropertyValueFactory<>("buyerName"));
+        TableColumn<SalesRecordDTO, Integer> qtyCol = new TableColumn<>("数量");
+        qtyCol.setCellValueFactory(new PropertyValueFactory<>("quantity"));
+        TableColumn<SalesRecordDTO, BigDecimal> priceCol = new TableColumn<>("总价");
+        priceCol.setCellValueFactory(new PropertyValueFactory<>("totalPrice"));
+        TableColumn<SalesRecordDTO, String> timeCol = new TableColumn<>("时间");
+        timeCol.setCellValueFactory(new PropertyValueFactory<>("transactionTime"));
+        timeCol.setPrefWidth(200);
+        table.getColumns().addAll(buyerCol, qtyCol, priceCol, timeCol);
+        table.setItems(FXCollections.observableArrayList(result.getData()));
+        dialog.getDialogPane().setContent(table);
+        dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
+        dialog.showAndWait();
+    }
+
     // Statistics handlers
 
     @FXML
@@ -365,6 +486,115 @@ public class MainController {
         }
     }
 
+    @FXML
+    private void handleEditUser() {
+        User sel = userTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请先选择要编辑的用户");
+            return;
+        }
+        editingUser = sel;
+        adminUsername.setText(sel.getUsername());
+        adminPassword.setText(sel.getPassword());
+        adminNickname.setText(sel.getNickname());
+        adminEmail.setText(sel.getEmail());
+        adminPhone.setText(sel.getPhone());
+        adminDept.setText(sel.getDept());
+        userFormPane.setText("编辑用户");
+        saveUserBtn.setText("保存修改");
+        userFormPane.setExpanded(true);
+    }
+
+    @FXML
+    private void handleResetPassword() {
+        User sel = userTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请先选择要重置密码的用户");
+            return;
+        }
+        sel.setPassword("123456");
+        R<User> result = userService.update(sel.getUserid(), sel);
+        if (result.getCode() == 200) {
+            showInfo("密码已重置为 123456");
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleEditCredit() {
+        User sel = userTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请先选择要修改信用分的用户");
+            return;
+        }
+        TextInputDialog dialog = new TextInputDialog(String.valueOf(sel.getCredit()));
+        dialog.setTitle("修改信用分");
+        dialog.setHeaderText("用户:" + sel.getUsername());
+        dialog.setContentText("信用分:");
+        dialog.showAndWait().ifPresent(input -> {
+            try {
+                int credit = Integer.parseInt(input.trim());
+                sel.setCredit(credit);
+                R<User> result = userService.update(sel.getUserid(), sel);
+                if (result.getCode() == 200) {
+                    showInfo("信用分已更新");
+                    handleLoadAllUsers();
+                } else {
+                    showAlert(result.getMessage());
+                }
+            } catch (NumberFormatException e) {
+                showAlert("请输入有效数字");
+            }
+        });
+    }
+
+    @FXML
+    private void handleSaveUser() {
+        try {
+            String username = adminUsername.getText().trim();
+            String password = adminPassword.getText().trim();
+            if (username.isEmpty() || password.isEmpty()) {
+                showAlert("用户名和密码为必填项");
+                return;
+            }
+            if (editingUser != null) {
+                editingUser.setUsername(username);
+                editingUser.setPassword(password);
+                editingUser.setNickname(adminNickname.getText().trim());
+                editingUser.setEmail(adminEmail.getText().trim());
+                editingUser.setPhone(adminPhone.getText().trim());
+                editingUser.setDept(adminDept.getText().trim());
+                R<User> result = userService.update(editingUser.getUserid(), editingUser);
+                if (result.getCode() == 200) {
+                    showInfo("用户信息已更新");
+                    clearUserForm();
+                    handleLoadAllUsers();
+                } else {
+                    showAlert(result.getMessage());
+                }
+            } else {
+                RegisterRequest req = new RegisterRequest();
+                req.setUsername(username);
+                req.setPassword(password);
+                req.setNickname(adminNickname.getText().trim());
+                req.setEmail(adminEmail.getText().trim());
+                req.setPhone(adminPhone.getText().trim());
+                req.setDept(adminDept.getText().trim());
+                R<User> result = userService.register(req);
+                if (result.getCode() == 200) {
+                    showInfo("用户已添加");
+                    clearUserForm();
+                    handleLoadAllUsers();
+                } else {
+                    showAlert(result.getMessage());
+                }
+            }
+        } catch (Exception e) {
+            showAlert("输入无效:" + e.getMessage());
+        }
+    }
+
     @FXML
     private void handleDeleteUser() {
         User sel = userTable.getSelectionModel().getSelectedItem();
@@ -381,6 +611,70 @@ public class MainController {
         }
     }
 
+    @FXML
+    private void handleLoadAllBooksAdmin() {
+        R<List<Book>> result = bookService.listAll();
+        if (result.getCode() == 200 && result.getData() != null) {
+            adminBookTable.setItems(FXCollections.observableArrayList(result.getData()));
+        }
+    }
+
+    @FXML
+    private void handleDeleteBookAdmin() {
+        Book sel = adminBookTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请选择要删除的书籍");
+            return;
+        }
+        R<Void> result = bookService.delete(sel.getBookId());
+        if (result.getCode() == 200) {
+            showInfo("书籍已删除");
+            handleLoadAllBooksAdmin();
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleLoadPendingBooks() {
+        R<List<Book>> result = bookService.getPendingBooks();
+        if (result.getCode() == 200 && result.getData() != null) {
+            pendingBookTable.setItems(FXCollections.observableArrayList(result.getData()));
+        }
+    }
+
+    @FXML
+    private void handleApproveBook() {
+        Book sel = pendingBookTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请先选择待审核的书籍");
+            return;
+        }
+        R<Book> result = bookService.audit(sel.getBookId(), 1);
+        if (result.getCode() == 200) {
+            showInfo("书籍已审核通过");
+            handleLoadPendingBooks();
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
+    @FXML
+    private void handleRejectBook() {
+        Book sel = pendingBookTable.getSelectionModel().getSelectedItem();
+        if (sel == null) {
+            showAlert("请先选择待审核的书籍");
+            return;
+        }
+        R<Book> result = bookService.audit(sel.getBookId(), 2);
+        if (result.getCode() == 200) {
+            showInfo("书籍已拒绝");
+            handleLoadPendingBooks();
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
     @FXML
     private void handleLoadAllTransactions() {
         R<PageResult<TransactionRecord>> result = HttpClientUtil.get("/api/transaction?page=1&size=100",
@@ -406,6 +700,16 @@ public class MainController {
         }
     }
 
+    @FXML
+    private void handleGenerateAnalysis() {
+        R<String> result = aiService.getAnalysis();
+        if (result.getCode() == 200 && result.getData() != null) {
+            analysisResult.setText(result.getData());
+        } else {
+            showAlert(result.getMessage());
+        }
+    }
+
     @FXML
     private void handleLogout() {
         LoginUserHolder.clear();
@@ -426,12 +730,29 @@ public class MainController {
     }
 
     private void clearBookForm() {
+        editingBook = null;
         newTitle.clear();
         newAuthor.clear();
         newCategory.clear();
         newOriginalPrice.clear();
         newSellingPrice.clear();
         newStock.clear();
+        bookFormPane.setText("添加新书");
+        saveBookBtn.setText("添加书籍");
+        bookFormPane.setExpanded(false);
+    }
+
+    private void clearUserForm() {
+        editingUser = null;
+        adminUsername.clear();
+        adminPassword.clear();
+        adminNickname.clear();
+        adminEmail.clear();
+        adminPhone.clear();
+        adminDept.clear();
+        userFormPane.setText("添加用户");
+        saveUserBtn.setText("保存");
+        userFormPane.setExpanded(false);
     }
 
     private void showAlert(String message) {

+ 11 - 0
client/src/main/java/space/anyi/client/service/AiService.java

@@ -0,0 +1,11 @@
+package space.anyi.client.service;
+
+import space.anyi.client.util.HttpClientUtil;
+import space.anyi.common.dto.R;
+
+public class AiService {
+
+    public R<String> getAnalysis() {
+        return HttpClientUtil.get("/api/ai/analysis", String.class);
+    }
+}

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

@@ -3,6 +3,7 @@ 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.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
 import space.anyi.client.util.HttpClientUtil;
 import java.net.URLEncoder;
@@ -45,4 +46,16 @@ public class BookService {
     public R<Void> delete(Integer id) {
         return HttpClientUtil.delete("/api/book/" + id, Void.class);
     }
+
+    public R<List<SalesRecordDTO>> getSales(Integer bookId) {
+        return HttpClientUtil.get("/api/book/" + bookId + "/sales", new TypeReference<R<List<SalesRecordDTO>>>() {});
+    }
+
+    public R<List<Book>> getPendingBooks() {
+        return HttpClientUtil.get("/api/book/pending", new TypeReference<R<List<Book>>>() {});
+    }
+
+    public R<Book> audit(Integer bookId, Integer status) {
+        return HttpClientUtil.put("/api/book/" + bookId + "/audit?status=" + status, null, Book.class);
+    }
 }

+ 88 - 34
client/src/main/resources/fxml/main.fxml

@@ -89,8 +89,12 @@
                             <TableColumn fx:id="myBookStockCol" text="库存" prefWidth="60"/>
                         </columns>
                     </TableView>
-                    <Button text="删除选中" onAction="#handleDeleteBook" styleClass="btn,btn-danger"/>
-                    <TitledPane text="添加新书" expanded="false">
+                    <HBox spacing="10">
+                        <Button text="编辑选中" onAction="#handleEditBook" styleClass="btn,btn-warning"/>
+                        <Button text="查看销售记录" onAction="#handleViewSales" styleClass="btn,btn-info"/>
+                        <Button text="删除选中" onAction="#handleDeleteBook" styleClass="btn,btn-danger"/>
+                    </HBox>
+                    <TitledPane fx:id="bookFormPane" text="添加新书" expanded="false">
                         <GridPane hgap="10" vgap="8">
                             <padding><Insets top="10" right="10" bottom="10" left="10"/></padding>
                             <Label text="书名" GridPane.rowIndex="0" GridPane.columnIndex="0"/><TextField fx:id="newTitle" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
@@ -99,7 +103,7 @@
                             <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" styleClass="btn,btn-success"/>
+                            <Button fx:id="saveBookBtn" text="添加书籍" onAction="#handleAddBook" GridPane.rowIndex="6" GridPane.columnIndex="1" styleClass="btn,btn-success"/>
                         </GridPane>
                     </TitledPane>
                 </VBox>
@@ -152,37 +156,87 @@
                 </ScrollPane>
             </Tab>
             <Tab fx:id="adminTab" text="管理" closable="false">
-                <VBox spacing="10">
-                    <padding><Insets top="10" right="10" bottom="10" left="10"/></padding>
-                    <Label text="用户管理"
-                           style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
-                    <Button text="刷新用户" onAction="#handleLoadAllUsers" styleClass="btn,btn-secondary"/>
-                    <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" styleClass="btn,btn-danger"/>
-                    <Label text="全部交易" style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
-                    <Button text="刷新交易" onAction="#handleLoadAllTransactions" styleClass="btn,btn-secondary"/>
-                    <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" styleClass="btn,btn-danger"/>
-                </VBox>
+                <ScrollPane fitToWidth="true">
+                    <VBox spacing="10">
+                        <padding><Insets top="10" right="10" bottom="10" left="10"/></padding>
+                        <Label text="用户管理" style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                        <Button text="刷新用户" onAction="#handleLoadAllUsers" styleClass="btn,btn-secondary"/>
+                        <TableView fx:id="userTable" prefHeight="150">
+                            <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>
+                        <HBox spacing="10">
+                            <Button text="编辑选中" onAction="#handleEditUser" styleClass="btn,btn-warning"/>
+                            <Button text="重置密码" onAction="#handleResetPassword" styleClass="btn,btn-secondary"/>
+                            <Button text="修改信用分" onAction="#handleEditCredit" styleClass="btn,btn-info"/>
+                            <Button text="删除选中用户" onAction="#handleDeleteUser" styleClass="btn,btn-danger"/>
+                        </HBox>
+                        <TitledPane fx:id="userFormPane" text="添加用户" expanded="false">
+                            <GridPane hgap="10" vgap="8">
+                                <padding><Insets top="10" right="10" bottom="10" left="10"/></padding>
+                                <Label text="用户名" GridPane.rowIndex="0" GridPane.columnIndex="0"/><TextField fx:id="adminUsername" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
+                                <Label text="密码" GridPane.rowIndex="1" GridPane.columnIndex="0"/><PasswordField fx:id="adminPassword" GridPane.rowIndex="1" GridPane.columnIndex="1"/>
+                                <Label text="昵称" GridPane.rowIndex="2" GridPane.columnIndex="0"/><TextField fx:id="adminNickname" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
+                                <Label text="邮箱" GridPane.rowIndex="3" GridPane.columnIndex="0"/><TextField fx:id="adminEmail" GridPane.rowIndex="3" GridPane.columnIndex="1"/>
+                                <Label text="电话" GridPane.rowIndex="4" GridPane.columnIndex="0"/><TextField fx:id="adminPhone" GridPane.rowIndex="4" GridPane.columnIndex="1"/>
+                                <Label text="院系" GridPane.rowIndex="5" GridPane.columnIndex="0"/><TextField fx:id="adminDept" GridPane.rowIndex="5" GridPane.columnIndex="1"/>
+                                <Button fx:id="saveUserBtn" text="保存" onAction="#handleSaveUser" GridPane.rowIndex="6" GridPane.columnIndex="1" styleClass="btn,btn-success"/>
+                            </GridPane>
+                        </TitledPane>
+                        <Label text="书籍管理" style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                        <Button text="刷新书籍" onAction="#handleLoadAllBooksAdmin" styleClass="btn,btn-secondary"/>
+                        <TableView fx:id="adminBookTable" prefHeight="150">
+                            <columns>
+                                <TableColumn fx:id="adminBookIdCol" text="编号" prefWidth="50"/>
+                                <TableColumn fx:id="adminBookTitleCol" text="书名" prefWidth="200"/>
+                                <TableColumn fx:id="adminBookAuthorCol" text="作者" prefWidth="120"/>
+                                <TableColumn fx:id="adminBookCategoryCol" text="分类" prefWidth="80"/>
+                                <TableColumn fx:id="adminBookPriceCol" text="售价" prefWidth="80"/>
+                                <TableColumn fx:id="adminBookStockCol" text="库存" prefWidth="60"/>
+                                <TableColumn fx:id="adminBookSellerCol" text="卖家ID" prefWidth="60"/>
+                            </columns>
+                        </TableView>
+                        <Button text="删除选中书籍" onAction="#handleDeleteBookAdmin" styleClass="btn,btn-danger"/>
+                        <Label text="待审核书籍" style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                        <Button text="刷新" onAction="#handleLoadPendingBooks" styleClass="btn,btn-secondary"/>
+                        <TableView fx:id="pendingBookTable" prefHeight="150">
+                            <columns>
+                                <TableColumn fx:id="pendingBookIdCol" text="编号" prefWidth="50"/>
+                                <TableColumn fx:id="pendingBookTitleCol" text="书名" prefWidth="200"/>
+                                <TableColumn fx:id="pendingBookAuthorCol" text="作者" prefWidth="120"/>
+                                <TableColumn fx:id="pendingBookSellerCol" text="卖家ID" prefWidth="60"/>
+                                <TableColumn fx:id="pendingBookPriceCol" text="售价" prefWidth="80"/>
+                            </columns>
+                        </TableView>
+                        <HBox spacing="10">
+                            <Button text="通过" onAction="#handleApproveBook" styleClass="btn,btn-success"/>
+                            <Button text="拒绝" onAction="#handleRejectBook" styleClass="btn,btn-danger"/>
+                        </HBox>
+                        <Label text="全部交易" style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                        <Button text="刷新交易" onAction="#handleLoadAllTransactions" styleClass="btn,btn-secondary"/>
+                        <TableView fx:id="allTransTable" prefHeight="150">
+                            <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" styleClass="btn,btn-danger"/>
+                        <Label text="AI 分析" style="-fx-font-weight: bold; -fx-font-size: 16px;"/>
+                        <Button text="生成分析报告" onAction="#handleGenerateAnalysis" styleClass="btn,btn-primary"/>
+                        <TextArea fx:id="analysisResult" editable="false" wrapText="true" prefHeight="250"/>
+                    </VBox>
+                </ScrollPane>
             </Tab>
         </TabPane>
     </center>

+ 16 - 0
common/src/main/java/space/anyi/common/dto/SalesRecordDTO.java

@@ -0,0 +1,16 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class SalesRecordDTO {
+    private Integer bookId;
+    private String title;
+    private Integer buyerId;
+    private String buyerName;
+    private Integer quantity;
+    private BigDecimal totalPrice;
+    private LocalDateTime transactionTime;
+}

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

@@ -1,6 +1,7 @@
 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;
@@ -21,4 +22,6 @@ public class Book {
     private String imageUrl;
     private LocalDateTime createdAt;
     private Integer stock;
+    @TableField("status")
+    private Integer status;
 }

+ 24 - 0
server/src/main/java/space/anyi/server/controller/AiController.java

@@ -0,0 +1,24 @@
+package space.anyi.server.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import space.anyi.common.dto.R;
+import space.anyi.server.service.AiService;
+
+@RestController
+@RequestMapping("/ai")
+public class AiController {
+
+    private final AiService aiService;
+
+    public AiController(AiService aiService) {
+        this.aiService = aiService;
+    }
+
+    @GetMapping("/analysis")
+    public R<String> getAnalysis() {
+        String report = aiService.generateAnalysis();
+        return R.ok(report);
+    }
+}

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

@@ -3,6 +3,7 @@ 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.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
 import space.anyi.server.service.BookService;
 import java.time.LocalDateTime;
@@ -52,9 +53,33 @@ public class BookController {
         return R.ok(bookService.getBySellerId(sellerId));
     }
 
+    @GetMapping("/{id}/sales")
+    public R<List<SalesRecordDTO>> getSales(@PathVariable Integer id) {
+        return R.ok(bookService.getSalesByBookId(id));
+    }
+
+    @GetMapping("/pending")
+    public R<List<Book>> getPendingBooks() {
+        return R.ok(bookService.getPendingBooks());
+    }
+
+    @PutMapping("/{id}/audit")
+    public R<Book> audit(@PathVariable Integer id, @RequestParam Integer status) {
+        Book book = bookService.getById(id);
+        if (book == null) {
+            return R.error("Book not found");
+        }
+        book.setStatus(status);
+        bookService.updateById(book);
+        return R.ok(book);
+    }
+
     @PostMapping
     public R<Book> create(@RequestBody Book book) {
         book.setCreatedAt(LocalDateTime.now());
+        if (book.getStatus() == null) {
+            book.setStatus(0);
+        }
         bookService.save(book);
         return R.ok(book);
     }

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

@@ -1,7 +1,18 @@
 package space.anyi.server.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import space.anyi.common.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
+import java.util.List;
 
 public interface BookMapper extends BaseMapper<Book> {
+    @Select("SELECT b.book_id, b.title, t.buyer_id, u.nickname AS buyerName, t.transaction_num AS quantity, t.transaction_price AS totalPrice, t.transaction_time " +
+            "FROM transaction_record t " +
+            "JOIN book b ON t.book_id = b.book_id " +
+            "JOIN user u ON t.buyer_id = u.userid " +
+            "WHERE b.book_id = #{bookId} " +
+            "ORDER BY t.transaction_time DESC")
+    List<SalesRecordDTO> selectSalesByBookId(@Param("bookId") Integer bookId);
 }

+ 5 - 0
server/src/main/java/space/anyi/server/service/AiService.java

@@ -0,0 +1,5 @@
+package space.anyi.server.service;
+
+public interface AiService {
+    String generateAnalysis();
+}

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

@@ -2,6 +2,7 @@ package space.anyi.server.service;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.common.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
 import java.util.List;
 
@@ -9,4 +10,6 @@ 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();
+    List<SalesRecordDTO> getSalesByBookId(Integer bookId);
+    List<Book> getPendingBooks();
 }

+ 86 - 0
server/src/main/java/space/anyi/server/service/impl/AiServiceImpl.java

@@ -0,0 +1,86 @@
+package space.anyi.server.service.impl;
+
+import org.springframework.stereotype.Service;
+import space.anyi.common.dto.*;
+import space.anyi.server.service.AiService;
+import space.anyi.server.service.StatisticsService;
+import java.util.List;
+
+@Service
+public class AiServiceImpl implements AiService {
+
+    private final StatisticsService statisticsService;
+
+    public AiServiceImpl(StatisticsService statisticsService) {
+        this.statisticsService = statisticsService;
+    }
+
+    @Override
+    public String generateAnalysis() {
+        StatisticsSummary summary = statisticsService.getSummary();
+        List<DeptSalesDTO> topDepts = statisticsService.getTopDepartments();
+        List<HotBookDTO> topBooks = statisticsService.getTopBooks();
+        List<DeptDiscountDTO> discounts = statisticsService.getDeptDiscountRates();
+
+        StringBuilder report = new StringBuilder();
+        report.append("========== 校园二手书交易系统分析报告 ==========\n\n");
+        report.append("【交易总览】\n");
+        report.append("  交易总金额:¥").append(summary.getTotalAmount()).append("\n");
+        report.append("  交易总笔数:").append(summary.getTotalCount()).append(" 笔\n\n");
+
+        report.append("【热门院系 TOP 3】\n");
+        if (topDepts.isEmpty()) {
+            report.append("  暂无数据\n");
+        } else {
+            for (int i = 0; i < topDepts.size(); i++) {
+                DeptSalesDTO d = topDepts.get(i);
+                report.append("  ").append(i + 1).append(". ").append(d.getDept())
+                       .append(" - ¥").append(d.getSalesAmount()).append("\n");
+            }
+        }
+        report.append("\n");
+
+        report.append("【热门书籍 TOP 5】\n");
+        if (topBooks.isEmpty()) {
+            report.append("  暂无数据\n");
+        } else {
+            for (int i = 0; i < topBooks.size(); i++) {
+                HotBookDTO b = topBooks.get(i);
+                report.append("  ").append(i + 1).append(". 《").append(b.getTitle())
+                       .append("》").append(b.getAuthor())
+                       .append(" - 销量 ").append(b.getTotalSold()).append(" 本\n");
+            }
+        }
+        report.append("\n");
+
+        report.append("【各院系折扣率】\n");
+        if (discounts.isEmpty()) {
+            report.append("  暂无数据\n");
+        } else {
+            for (DeptDiscountDTO d : discounts) {
+                report.append("  ").append(d.getDept())
+                       .append(":平均折扣率 ")
+                       .append(String.format("%.1f", d.getAvgDiscountRate())).append("%\n");
+            }
+        }
+        report.append("\n");
+        report.append("【分析结论】\n");
+
+        long totalCount = summary.getTotalCount();
+        if (totalCount == 0) {
+            report.append("  平台暂无交易数据,建议鼓励学生积极发布和购买二手教材。\n");
+        } else {
+            report.append("  平台运营良好,共完成 ").append(totalCount).append(" 笔交易");
+            report.append(",总金额 ¥").append(summary.getTotalAmount()).append("。\n");
+            if (!topDepts.isEmpty()) {
+                report.append("  销量最高的院系是 ").append(topDepts.get(0).getDept()).append(",可重点推广。\n");
+            }
+            if (!topBooks.isEmpty()) {
+                report.append("  最受欢迎的书籍是《").append(topBooks.get(0).getTitle())
+                       .append("》,建议增加库存。\n");
+            }
+        }
+        report.append("\n================================================");
+        return report.toString();
+    }
+}

+ 23 - 2
server/src/main/java/space/anyi/server/service/impl/BookServiceImpl.java

@@ -5,6 +5,7 @@ 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.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
 import space.anyi.server.mapper.BookMapper;
 import space.anyi.server.service.BookService;
@@ -13,12 +14,19 @@ import java.util.List;
 @Service
 public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements BookService {
 
+    private final BookMapper bookMapper;
+
+    public BookServiceImpl(BookMapper bookMapper) {
+        this.bookMapper = bookMapper;
+    }
+
     @Override
     public IPage<Book> searchBooks(String keyword, String category, int page, int size) {
         LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Book::getStatus, 1);
         if (keyword != null && !keyword.isBlank()) {
-            wrapper.like(Book::getTitle, keyword)
-                   .or().like(Book::getAuthor, keyword);
+            wrapper.and(w -> w.like(Book::getTitle, keyword)
+                               .or().like(Book::getAuthor, keyword));
         }
         if (category != null && !category.isBlank()) {
             wrapper.eq(Book::getCategory, category);
@@ -39,4 +47,17 @@ public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements Bo
     public List<Book> listAll() {
         return lambdaQuery().orderByDesc(Book::getCreatedAt).list();
     }
+
+    @Override
+    public List<SalesRecordDTO> getSalesByBookId(Integer bookId) {
+        return bookMapper.selectSalesByBookId(bookId);
+    }
+
+    @Override
+    public List<Book> getPendingBooks() {
+        LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Book::getStatus, 0)
+               .orderByDesc(Book::getCreatedAt);
+        return list(wrapper);
+    }
 }

+ 9 - 4
sql/campus_book_trade.sql

@@ -32,16 +32,17 @@ CREATE TABLE `book`  (
   `image_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
   `created_at` datetime(0) NULL DEFAULT NULL,
   `stock` int(0) NULL DEFAULT NULL,
+  `status` int(0) NULL DEFAULT 0 COMMENT '0=待审核, 1=已上架, 2=已下架',
   PRIMARY KEY (`book_id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
 
 -- ----------------------------
 -- Records of book
 -- ----------------------------
-INSERT INTO `book` VALUES (1, 3, 'Java程序设计', '刘晓明', '教材', 49.90, 48.00, NULL, '2026-06-15 12:07:36', 90);
-INSERT INTO `book` VALUES (2, 5, 'photoshop图形图像处理', '王艳梅', '教材', 59.80, 56.00, NULL, '2026-06-15 12:08:47', 50);
-INSERT INTO `book` VALUES (3, 2, '影视栏目包装', '李大山', '教材', 46.80, 42.00, NULL, '2026-06-16 21:32:13', 50);
-INSERT INTO `book` VALUES (4, 6, '数据库原理与应用', '肖时光', '教材', 55.20, 55.00, NULL, '2026-06-16 21:34:29', 4);
+INSERT INTO `book` VALUES (1, 3, 'Java程序设计', '刘晓明', '教材', 49.90, 48.00, NULL, '2026-06-15 12:07:36', 90, 1);
+INSERT INTO `book` VALUES (2, 5, 'photoshop图形图像处理', '王艳梅', '教材', 59.80, 56.00, NULL, '2026-06-15 12:08:47', 50, 1);
+INSERT INTO `book` VALUES (3, 2, '影视栏目包装', '李大山', '教材', 46.80, 42.00, NULL, '2026-06-16 21:32:13', 50, 1);
+INSERT INTO `book` VALUES (4, 6, '数据库原理与应用', '肖时光', '教材', 55.20, 55.00, NULL, '2026-06-16 21:34:29', 4, 1);
 
 -- ----------------------------
 -- Table structure for transaction_record
@@ -96,3 +97,7 @@ INSERT INTO `user` VALUES (5, '谢东格', '123456', '东格', '123@qq.com', '12
 INSERT INTO `user` VALUES (6, '李大海', '123456', '大海', '123@qq.com', '1234567890', '计算机学院', 100, 0, 1, '2026-06-16 21:33:46');
 
 SET FOREIGN_KEY_CHECKS = 1;
+
+-- Migration for existing databases:
+-- ALTER TABLE `book` ADD COLUMN `status` int DEFAULT 0 COMMENT '0=待审核, 1=已上架, 2=已下架';
+-- UPDATE `book` SET `status` = 1;