|
|
@@ -0,0 +1,1144 @@
|
|
|
+# 五、实训结果
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 服务器端
|
|
|
+
|
|
|
+### 1.1 项目结构图
|
|
|
+
|
|
|
+**整体项目模块结构**
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph TD
|
|
|
+ subgraph "CampusUsedBookTradingSystem (父POM)"
|
|
|
+ direction TB
|
|
|
+ common[common 模块<br/>space.anyi.common<br/>实体 / DTO / 枚举]
|
|
|
+ server[server 模块<br/>space.anyi.server<br/>Spring Boot REST API]
|
|
|
+ client[client 模块<br/>space.anyi.client<br/>JavaFX 桌面客户端]
|
|
|
+ end
|
|
|
+
|
|
|
+ common -->|依赖| server
|
|
|
+ common -->|依赖| client
|
|
|
+ server -.->|HTTP JSON| client
|
|
|
+```
|
|
|
+
|
|
|
+**服务器端目录树**
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph TD
|
|
|
+ subgraph "server/src/main"
|
|
|
+ direction TB
|
|
|
+ java["java/space/anyi/server"]
|
|
|
+ resources["resources"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "java 层"
|
|
|
+ direction TB
|
|
|
+ entry["ServerApplication.java"]
|
|
|
+ config["config/"]
|
|
|
+ controller["controller/"]
|
|
|
+ service["service/"]
|
|
|
+ impl["service/impl/"]
|
|
|
+ mapper["mapper/"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "config 配置层"
|
|
|
+ wc["WebConfig.java<br/>CORS跨域"]
|
|
|
+ mpc["MyBatisPlusConfig.java<br/>分页拦截器"]
|
|
|
+ geh["GlobalExceptionHandler.java<br/>全局异常处理"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "controller 控制器层"
|
|
|
+ uc["UserController<br/>用户CRUD"]
|
|
|
+ bc["BookController<br/>图书CRUD+审核"]
|
|
|
+ tc["TransactionController<br/>交易CRUD"]
|
|
|
+ sc["StatisticsController<br/>统计分析"]
|
|
|
+ ac["AiController<br/>AI报告生成"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "service 业务层"
|
|
|
+ us["UserService/UserServiceImpl<br/>登录/注册"]
|
|
|
+ bs["BookService/BookServiceImpl<br/>图书搜索/查询"]
|
|
|
+ ts["TransactionService/TransactionServiceImpl<br/>购买事务"]
|
|
|
+ ss["StatisticsService/StatisticsServiceImpl<br/>聚合统计"]
|
|
|
+ as["AiService/AiServiceImpl<br/>AI分析+流式生成"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "mapper 持久层"
|
|
|
+ um["UserMapper<br/>基础CRUD"]
|
|
|
+ bm["BookMapper<br/>自定义@Select联表"]
|
|
|
+ tm["TransactionRecordMapper<br/>分页+联表"]
|
|
|
+ sm["StatisticsMapper<br/>SUM/GROUP统计"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "resources 资源"
|
|
|
+ yml["application.yml<br/>端口/数据库/AI配置"]
|
|
|
+ xml["mapper/*.xml<br/>空壳XML"]
|
|
|
+ end
|
|
|
+
|
|
|
+ java --> config
|
|
|
+ java --> controller
|
|
|
+ java --> service
|
|
|
+ java --> impl
|
|
|
+ java --> mapper
|
|
|
+
|
|
|
+ config --> wc
|
|
|
+ config --> mpc
|
|
|
+ config --> geh
|
|
|
+
|
|
|
+ controller --> uc
|
|
|
+ controller --> bc
|
|
|
+ controller --> tc
|
|
|
+ controller --> sc
|
|
|
+ controller --> ac
|
|
|
+
|
|
|
+ service --> us
|
|
|
+ service --> bs
|
|
|
+ service --> ts
|
|
|
+ service --> ss
|
|
|
+ service --> as
|
|
|
+
|
|
|
+ impl --> us
|
|
|
+ impl --> bs
|
|
|
+ impl --> ts
|
|
|
+ impl --> ss
|
|
|
+ impl --> as
|
|
|
+
|
|
|
+ mapper --> um
|
|
|
+ mapper --> bm
|
|
|
+ mapper --> tm
|
|
|
+ mapper --> sm
|
|
|
+
|
|
|
+ resources --> yml
|
|
|
+ resources --> xml
|
|
|
+```
|
|
|
+
|
|
|
+### 1.2 核心代码
|
|
|
+
|
|
|
+#### 应用入口
|
|
|
+
|
|
|
+```java
|
|
|
+@MapperScan("space.anyi.server.mapper")
|
|
|
+@SpringBootApplication
|
|
|
+public class ServerApplication {
|
|
|
+ public static void main(String[] args) {
|
|
|
+ SpringApplication.run(ServerApplication.class, args);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 配置层
|
|
|
+
|
|
|
+**WebConfig — CORS跨域配置**
|
|
|
+
|
|
|
+```java
|
|
|
+@Configuration
|
|
|
+public class WebConfig {
|
|
|
+ @Bean
|
|
|
+ public WebMvcConfigurer corsConfigurer() {
|
|
|
+ return new WebMvcConfigurer() {
|
|
|
+ @Override
|
|
|
+ public void addCorsMappings(CorsRegistry registry) {
|
|
|
+ registry.addMapping("/**")
|
|
|
+ .allowedOriginPatterns("*")
|
|
|
+ .allowedMethods("*")
|
|
|
+ .allowedHeaders("*")
|
|
|
+ .allowCredentials(true);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**MyBatisPlusConfig — 分页拦截器**
|
|
|
+
|
|
|
+```java
|
|
|
+@Configuration
|
|
|
+public class MyBatisPlusConfig {
|
|
|
+ @Bean
|
|
|
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
|
|
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
|
|
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
|
|
+ return interceptor;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**GlobalExceptionHandler — 全局异常处理**
|
|
|
+
|
|
|
+```java
|
|
|
+@RestControllerAdvice
|
|
|
+public class GlobalExceptionHandler {
|
|
|
+
|
|
|
+ @ExceptionHandler(DuplicateKeyException.class)
|
|
|
+ public R<Void> handleDuplicateKey(DuplicateKeyException e) {
|
|
|
+ return R.error("用户名已存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ @ExceptionHandler(IllegalArgumentException.class)
|
|
|
+ public R<Void> handleIllegalArgument(IllegalArgumentException e) {
|
|
|
+ return R.error(e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @ExceptionHandler(Exception.class)
|
|
|
+ public R<Void> handleException(Exception e) {
|
|
|
+ return R.error("服务器内部错误: " + e.getMessage());
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 控制器层
|
|
|
+
|
|
|
+**UserController — 登录注册端点**
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/user")
|
|
|
+public class UserController {
|
|
|
+
|
|
|
+ private final UserService userService;
|
|
|
+
|
|
|
+ public UserController(UserService userService) { this.userService = userService; }
|
|
|
+
|
|
|
+ @PostMapping("/login")
|
|
|
+ public R<User> login(@RequestBody LoginRequest req) {
|
|
|
+ User user = userService.login(req.getUsername(), req.getPassword());
|
|
|
+ if (user == null) return R.error("Invalid username or password");
|
|
|
+ return R.ok(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/register")
|
|
|
+ public R<User> register(@RequestBody RegisterRequest req) {
|
|
|
+ User user = new User();
|
|
|
+ user.setUsername(req.getUsername());
|
|
|
+ user.setPassword(req.getPassword());
|
|
|
+ user.setNickname(req.getNickname());
|
|
|
+ user.setEmail(req.getEmail());
|
|
|
+ user.setPhone(req.getPhone());
|
|
|
+ user.setDept(req.getDept());
|
|
|
+ user.setRole(req.getRole());
|
|
|
+ user.setIsSeller(req.getIsSeller());
|
|
|
+ User created = userService.register(user);
|
|
|
+ return R.ok(created);
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/{id}") // 查询单个用户
|
|
|
+ @PutMapping("/{id}") // 更新用户
|
|
|
+ @DeleteMapping("/{id}") // 删除用户
|
|
|
+ @GetMapping // 分页列出所有用户
|
|
|
+ // ...省略
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**BookController — 图书搜索与CRUD**
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/book")
|
|
|
+public class BookController {
|
|
|
+
|
|
|
+ private final BookService bookService;
|
|
|
+
|
|
|
+ public BookController(BookService bookService) { this.bookService = bookService; }
|
|
|
+
|
|
|
+ @GetMapping
|
|
|
+ public R<PageResult<Book>> search(
|
|
|
+ @RequestParam(required = false) String keyword,
|
|
|
+ @RequestParam(required = false) String category,
|
|
|
+ @RequestParam(defaultValue = "1") int page,
|
|
|
+ @RequestParam(defaultValue = "20") int size) {
|
|
|
+ var pageResult = bookService.searchBooks(keyword, category, page, size);
|
|
|
+ PageResult<Book> pr = new PageResult<>();
|
|
|
+ pr.setRecords(pageResult.getRecords());
|
|
|
+ pr.setTotal(pageResult.getTotal());
|
|
|
+ pr.setSize(pageResult.getSize());
|
|
|
+ pr.setCurrent(pageResult.getCurrent());
|
|
|
+ return R.ok(pr);
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/my")
|
|
|
+ public R<List<Book>> getMyBooks(@RequestParam Integer sellerId) {
|
|
|
+ return R.ok(bookService.getBySellerId(sellerId));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping
|
|
|
+ public R<Book> create(@RequestBody Book book) {
|
|
|
+ book.setCreatedAt(LocalDateTime.now());
|
|
|
+ if (book.getStatus() == null) book.setStatus(0);
|
|
|
+ bookService.save(book);
|
|
|
+ return R.ok(book);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PutMapping("/{id}/audit")
|
|
|
+ public R<Book> audit(@PathVariable Integer id, @RequestParam Integer status) {
|
|
|
+ Book book = bookService.getById(id);
|
|
|
+ if (book == null) return R.error("Book not found");
|
|
|
+ book.setStatus(status);
|
|
|
+ bookService.updateById(book);
|
|
|
+ return R.ok(book);
|
|
|
+ }
|
|
|
+ // GET /{id}, PUT /{id}, DELETE /{id}, GET /all, GET /{id}/sales ...省略
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**TransactionController — 购买与交易记录**
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/transaction")
|
|
|
+public class TransactionController {
|
|
|
+
|
|
|
+ private final TransactionService transactionService;
|
|
|
+
|
|
|
+ public TransactionController(TransactionService transactionService) { this.transactionService = transactionService; }
|
|
|
+
|
|
|
+ @PostMapping
|
|
|
+ public R<TransactionRecord> purchase(@RequestBody Map<String, Object> body) {
|
|
|
+ Integer bookId = (Integer) body.get("bookId");
|
|
|
+ Integer buyerId = (Integer) body.get("buyerId");
|
|
|
+ Integer quantity = (Integer) body.getOrDefault("quantity", 1);
|
|
|
+ try {
|
|
|
+ TransactionRecord record = transactionService.purchase(bookId, buyerId, quantity);
|
|
|
+ return R.ok(record);
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ return R.error(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/user/{userId}/detail")
|
|
|
+ public R<List<UserTransactionDTO>> getByUserDetail(@PathVariable Integer userId) {
|
|
|
+ return R.ok(transactionService.getByUserIdWithTitle(userId));
|
|
|
+ }
|
|
|
+ // GET /{id}, GET /user/{userId}, PUT /{id}, DELETE /{id} ...省略
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**StatisticsController — 统计分析**
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/statistics")
|
|
|
+public class StatisticsController {
|
|
|
+
|
|
|
+ private final StatisticsService statisticsService;
|
|
|
+
|
|
|
+ public StatisticsController(StatisticsService statisticsService) { this.statisticsService = statisticsService; }
|
|
|
+
|
|
|
+ @GetMapping("/summary") // {totalAmount, totalCount}
|
|
|
+ public R<StatisticsSummary> getSummary() { return R.ok(statisticsService.getSummary()); }
|
|
|
+
|
|
|
+ @GetMapping("/top-departments") // Top3 院系交易额
|
|
|
+ public R<List<DeptSalesDTO>> getTopDepartments() { return R.ok(statisticsService.getTopDepartments()); }
|
|
|
+
|
|
|
+ @GetMapping("/top-books") // Top5 热门书籍
|
|
|
+ public R<List<HotBookDTO>> getTopBooks() { return R.ok(statisticsService.getTopBooks()); }
|
|
|
+
|
|
|
+ @GetMapping("/discount-rates") // 各院系平均折扣率
|
|
|
+ public R<List<DeptDiscountDTO>> getDiscountRates() { return R.ok(statisticsService.getDeptDiscountRates()); }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**AiController — AI分析报告**
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/ai")
|
|
|
+public class AiController {
|
|
|
+
|
|
|
+ private final AiService aiService;
|
|
|
+
|
|
|
+ public AiController(AiService aiService) { this.aiService = aiService; }
|
|
|
+
|
|
|
+ @GetMapping("/analysis")
|
|
|
+ public R<String> getAnalysis() {
|
|
|
+ String report = aiService.generateAnalysis();
|
|
|
+ return R.ok(report);
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/analysis/stream")
|
|
|
+ public ResponseBodyEmitter streamAnalysis() {
|
|
|
+ ResponseBodyEmitter emitter = new ResponseBodyEmitter(180_000L);
|
|
|
+ Flux<String> flux = aiService.streamAnalysis();
|
|
|
+ flux.subscribe(
|
|
|
+ chunk -> { try { emitter.send(chunk, MediaType.TEXT_PLAIN); }
|
|
|
+ catch (Exception e) { emitter.completeWithError(e); }},
|
|
|
+ emitter::completeWithError,
|
|
|
+ emitter::complete
|
|
|
+ );
|
|
|
+ return emitter;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 业务层
|
|
|
+
|
|
|
+**UserServiceImpl — 登录注册业务**
|
|
|
+
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public User login(String username, String password) {
|
|
|
+ LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(User::getUsername, username).eq(User::getPassword, password);
|
|
|
+ return getOne(wrapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public User register(User user) {
|
|
|
+ user.setCredit(100);
|
|
|
+ if (user.getRole() == null) user.setRole(0);
|
|
|
+ if (user.getIsSeller() == null) user.setIsSeller(0);
|
|
|
+ user.setCreatedAt(LocalDateTime.now());
|
|
|
+ save(user);
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**TransactionServiceImpl — 购买事务(含库存扣减)**
|
|
|
+
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class TransactionServiceImpl
|
|
|
+ extends ServiceImpl<TransactionRecordMapper, TransactionRecord>
|
|
|
+ implements TransactionService {
|
|
|
+
|
|
|
+ private final BookService bookService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional
|
|
|
+ public TransactionRecord purchase(Integer bookId, Integer buyerId, Integer quantity) {
|
|
|
+ Book book = bookService.getById(bookId);
|
|
|
+ if (book == null) throw new RuntimeException("书籍不存在");
|
|
|
+ if (book.getStock() < quantity) throw new RuntimeException("库存不足");
|
|
|
+
|
|
|
+ book.setStock(book.getStock() - quantity);
|
|
|
+ bookService.updateById(book);
|
|
|
+
|
|
|
+ TransactionRecord record = new TransactionRecord();
|
|
|
+ record.setBookId(bookId);
|
|
|
+ record.setBuyerId(buyerId);
|
|
|
+ record.setTransactionNum(quantity);
|
|
|
+ record.setTransactionPrice(book.getSellingPrice()
|
|
|
+ .multiply(BigDecimal.valueOf(quantity)));
|
|
|
+ record.setTransactionTime(LocalDateTime.now());
|
|
|
+ save(record);
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**AiServiceImpl — AI分析报告生成**
|
|
|
+
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class AiServiceImpl implements AiService {
|
|
|
+
|
|
|
+ private final StatisticsService statisticsService;
|
|
|
+ private final ChatClient chatClient;
|
|
|
+
|
|
|
+ public AiServiceImpl(StatisticsService statisticsService, ChatClient.Builder builder) {
|
|
|
+ this.statisticsService = statisticsService;
|
|
|
+ this.chatClient = builder.build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String generateAnalysis() {
|
|
|
+ StatisticsSummary summary = statisticsService.getSummary();
|
|
|
+ List<DeptSalesDTO> topDepts = statisticsService.getTopDepartments();
|
|
|
+ List<HotBookDTO> topBooks = statisticsService.getTopBooks();
|
|
|
+ List<DeptDiscountDTO> discounts = statisticsService.getDeptDiscountRates();
|
|
|
+
|
|
|
+ StringBuilder report = new StringBuilder();
|
|
|
+ report.append("========== 校园二手书交易系统分析报告 ==========\n\n");
|
|
|
+ // ... 格式化输出交易总览、热门院系、热门书籍、折扣率、分析结论
|
|
|
+ return report.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Flux<String> streamAnalysis() {
|
|
|
+ // 构造Prompt,调用ChatClient流式生成
|
|
|
+ return chatClient.prompt().user(buildPrompt(...)).stream().content();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 持久层
|
|
|
+
|
|
|
+**StatisticsMapper — 聚合统计SQL**
|
|
|
+
|
|
|
+```java
|
|
|
+public interface StatisticsMapper {
|
|
|
+
|
|
|
+ @Select("SELECT COALESCE(SUM(transaction_price), 0) AS totalAmount, " +
|
|
|
+ "COUNT(*) AS totalCount FROM transaction_record")
|
|
|
+ StatisticsSummary selectSummary();
|
|
|
+
|
|
|
+ @Select("SELECT u.dept, COALESCE(SUM(t.transaction_price), 0) AS salesAmount " +
|
|
|
+ "FROM transaction_record t JOIN book b ON t.book_id = b.book_id " +
|
|
|
+ "JOIN user u ON b.seller_id = u.userid " +
|
|
|
+ "GROUP BY u.dept ORDER BY salesAmount DESC LIMIT 3")
|
|
|
+ List<DeptSalesDTO> selectTopDepartments();
|
|
|
+
|
|
|
+ @Select("SELECT b.book_id, b.title, b.author, SUM(t.transaction_num) AS totalSold " +
|
|
|
+ "FROM transaction_record t JOIN book b ON t.book_id = b.book_id " +
|
|
|
+ "GROUP BY b.book_id, b.title, b.author ORDER BY totalSold DESC LIMIT 5")
|
|
|
+ List<HotBookDTO> selectTopBooks();
|
|
|
+
|
|
|
+ @Select("SELECT u.dept, AVG((b.original_price - t.transaction_price / t.transaction_num) " +
|
|
|
+ "/ b.original_price * 100) AS avgDiscountRate " +
|
|
|
+ "FROM transaction_record t JOIN book b ON t.book_id = b.book_id " +
|
|
|
+ "JOIN user u ON b.seller_id = u.userid GROUP BY u.dept")
|
|
|
+ List<DeptDiscountDTO> selectDeptDiscountRates();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**BookMapper — 联表查询**
|
|
|
+
|
|
|
+```java
|
|
|
+public interface BookMapper extends BaseMapper<Book> {
|
|
|
+
|
|
|
+ @Select("SELECT b.book_id, u.nickname AS seller_name, b.title, b.author, " +
|
|
|
+ "b.category, b.original_price, b.selling_price, b.stock, b.status " +
|
|
|
+ "FROM book b LEFT JOIN user u ON b.seller_id = u.userid " +
|
|
|
+ "ORDER BY b.created_at DESC")
|
|
|
+ List<BookDetailDTO> selectAllWithSellerName();
|
|
|
+
|
|
|
+ @Select("... WHERE b.status = 0 ...") // 待审核图书
|
|
|
+ List<BookDetailDTO> selectPendingWithSellerName();
|
|
|
+
|
|
|
+ @Select("... JOIN transaction_record ... WHERE b.book_id = #{bookId}") // 销售记录
|
|
|
+ List<SalesRecordDTO> selectSalesByBookId(@Param("bookId") Integer bookId);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**TransactionRecordMapper — 分页联表**
|
|
|
+
|
|
|
+```java
|
|
|
+public interface TransactionRecordMapper extends BaseMapper<TransactionRecord> {
|
|
|
+
|
|
|
+ @Select("SELECT t.trans_id, b.title AS book_title, u1.nickname AS buyer_name, " +
|
|
|
+ "u2.nickname AS seller_name, t.transaction_num, t.transaction_price, " +
|
|
|
+ "t.transaction_time FROM transaction_record t " +
|
|
|
+ "LEFT JOIN book b ON t.book_id = b.book_id " +
|
|
|
+ "LEFT JOIN user u1 ON t.buyer_id = u1.userid " +
|
|
|
+ "LEFT JOIN user u2 ON b.seller_id = u2.userid " +
|
|
|
+ "ORDER BY t.transaction_time DESC")
|
|
|
+ IPage<TransactionDetailDTO> selectPageWithDetail(Page<?> page);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**UserMapper — 基础CRUD**
|
|
|
+
|
|
|
+```java
|
|
|
+public interface UserMapper extends BaseMapper<User> {
|
|
|
+ // 继承 BaseMapper 自带 insert / deleteById / selectById / updateById / selectPage ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 配置文件
|
|
|
+
|
|
|
+**application.yml**
|
|
|
+
|
|
|
+```yaml
|
|
|
+server:
|
|
|
+ port: 8080
|
|
|
+ servlet:
|
|
|
+ context-path: /api
|
|
|
+
|
|
|
+spring:
|
|
|
+ datasource:
|
|
|
+ url: jdbc:mysql://localhost:3306/campus_book_trade?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
|
|
|
+ username: root
|
|
|
+ password: root
|
|
|
+ driver-class-name: com.mysql.cj.jdbc.Driver
|
|
|
+
|
|
|
+ ai:
|
|
|
+ openai:
|
|
|
+ base-url: ${OPENAI_BASE_URL:https://spark-api-open.xf-yun.com}
|
|
|
+ api-key: ${OPENAI_API_KEY:key}
|
|
|
+ chat:
|
|
|
+ options:
|
|
|
+ model: ${OPENAI_MODEL:lite}
|
|
|
+ temperature: 0.3
|
|
|
+
|
|
|
+mybatis-plus:
|
|
|
+ configuration:
|
|
|
+ map-underscore-to-camel-case: true
|
|
|
+ global-config:
|
|
|
+ db-config:
|
|
|
+ id-type: auto
|
|
|
+```
|
|
|
+
|
|
|
+### 1.3 服务器端运行效果图
|
|
|
+
|
|
|
+> 以下为占位符,实际报告中替换为对应截图。
|
|
|
+
|
|
|
+**启动运行**
|
|
|
+
|
|
|
+![服务器启动截图]
|
|
|
+*说明:Spring Boot 启动日志,显示端口 8080,context-path /api,MyBatis-Plus 初始化完成。*
|
|
|
+
|
|
|
+**用户登录接口调用**
|
|
|
+
|
|
|
+![用户登录API截图]
|
|
|
+*说明:通过 Postman / curl 调用 POST /api/user/login 返回 User JSON 数据。*
|
|
|
+
|
|
|
+**用户注册接口调用**
|
|
|
+
|
|
|
+![用户注册API截图]
|
|
|
+*说明:调用 POST /api/user/register 返回注册成功的 User 信息。*
|
|
|
+
|
|
|
+**图书搜索接口调用**
|
|
|
+
|
|
|
+![图书搜索API截图]
|
|
|
+*说明:调用 GET /api/book?keyword=Java&page=1&size=10 返回分页图书列表。*
|
|
|
+
|
|
|
+**购买交易接口调用**
|
|
|
+
|
|
|
+![购买交易API截图]
|
|
|
+*说明:调用 POST /api/transaction 完成购买,返回交易记录。*
|
|
|
+
|
|
|
+**统计接口调用**
|
|
|
+
|
|
|
+![统计API截图]
|
|
|
+*说明:调用 GET /api/statistics/summary 与 /top-books 返回聚合统计数据。*
|
|
|
+
|
|
|
+**AI分析报告接口调用**
|
|
|
+
|
|
|
+![AI分析报告截图]
|
|
|
+*说明:调用 GET /api/ai/analysis 返回 AI 生成的运营分析报告文本。*
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 客户端
|
|
|
+
|
|
|
+### 2.1 项目结构图
|
|
|
+
|
|
|
+**客户端目录树**
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph TD
|
|
|
+ subgraph "client/src/main"
|
|
|
+ direction TB
|
|
|
+ java["java/space/anyi/client"]
|
|
|
+ resources["resources"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "入口层"
|
|
|
+ cl["Client.java<br/>main启动"]
|
|
|
+ ui["UIApplication.java<br/>JavaFX Application<br/>场景管理"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "controller 控制器层"
|
|
|
+ lc["LoginController<br/>登录"]
|
|
|
+ rc["RegisterController<br/>注册"]
|
|
|
+ mc["MainController<br/>1453行<br/>浏览/交易/个人/卖家/管理/统计/AI"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "service HTTP调用层"
|
|
|
+ us["UserService<br/>用户相关API"]
|
|
|
+ bs["BookService<br/>图书相关API"]
|
|
|
+ ts["TransactionService<br/>交易相关API"]
|
|
|
+ ss["StatisticsService<br/>统计API"]
|
|
|
+ as["AiService<br/>AI分析API+流式"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "util 工具层"
|
|
|
+ hcu["HttpClientUtil<br/>JDK HttpClient<br/>+Jackson封装"]
|
|
|
+ luh["LoginUserHolder<br/>当前用户会话<br/>静态持有"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "resources/fxml 视图"
|
|
|
+ login_fxml["login.fxml"]
|
|
|
+ register_fxml["register.fxml"]
|
|
|
+ main_fxml["main.fxml"]
|
|
|
+ browse["browse.fxml"]
|
|
|
+ trans["trans.fxml"]
|
|
|
+ profile["profile.fxml"]
|
|
|
+ seller["seller.fxml"]
|
|
|
+ admin["admin.fxml"]
|
|
|
+ stats["stats.fxml"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "resources/css"
|
|
|
+ css["style.css<br/>436行自定义样式<br/>+BootstrapFX"]
|
|
|
+ end
|
|
|
+
|
|
|
+ java --> cl
|
|
|
+ java --> ui
|
|
|
+ java --> lc
|
|
|
+ java --> rc
|
|
|
+ java --> mc
|
|
|
+ java --> us
|
|
|
+ java --> bs
|
|
|
+ java --> ts
|
|
|
+ java --> ss
|
|
|
+ java --> as
|
|
|
+ java --> hcu
|
|
|
+ java --> luh
|
|
|
+
|
|
|
+ resources --> login_fxml
|
|
|
+ resources --> register_fxml
|
|
|
+ resources --> main_fxml
|
|
|
+ resources --> browse
|
|
|
+ resources --> trans
|
|
|
+ resources --> profile
|
|
|
+ resources --> seller
|
|
|
+ resources --> admin
|
|
|
+ resources --> stats
|
|
|
+ resources --> css
|
|
|
+```
|
|
|
+
|
|
|
+**客户端场景切换流程**
|
|
|
+
|
|
|
+```mermaid
|
|
|
+stateDiagram-v2
|
|
|
+ [*] --> LoginScene: 启动
|
|
|
+
|
|
|
+ LoginScene --> RegisterScene: 点击"注册"
|
|
|
+ RegisterScene --> LoginScene: 点击"返回"
|
|
|
+
|
|
|
+ LoginScene --> MainScene: 登录成功
|
|
|
+
|
|
|
+ state MainScene {
|
|
|
+ [*] --> BrowseTab: 默认
|
|
|
+ BrowseTab --> Purchase: 点击购买
|
|
|
+ BrowseTab --> DetailPanel: 点击图书
|
|
|
+ Purchase --> BrowseTab: 完成/取消
|
|
|
+
|
|
|
+ BrowseTab --> TransactionsTab: 点击导航
|
|
|
+ TransactionsTab --> ProfileTab
|
|
|
+ ProfileTab --> SellerTab
|
|
|
+ SellerTab --> AdminTab
|
|
|
+ AdminTab --> StatisticsTab
|
|
|
+ StatisticsTab --> AIAnalysis
|
|
|
+ }
|
|
|
+
|
|
|
+ MainScene --> LoginScene: 退出登录
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 核心代码
|
|
|
+
|
|
|
+#### 入口启动类
|
|
|
+
|
|
|
+**Client.java — 启动入口**
|
|
|
+
|
|
|
+```java
|
|
|
+public class Client {
|
|
|
+ public static void main(String[] args) {
|
|
|
+ Application.launch(UIApplication.class, args);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**UIApplication.java — JavaFX场景管理**
|
|
|
+
|
|
|
+```java
|
|
|
+public class UIApplication extends Application {
|
|
|
+
|
|
|
+ private static Stage primaryStage;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void start(Stage stage) throws Exception {
|
|
|
+ primaryStage = stage;
|
|
|
+ showLoginScene();
|
|
|
+ stage.setTitle("校园二手书交易系统");
|
|
|
+ stage.getIcons().add(new Image(UIApplication.class.getResourceAsStream("/favicon.png")));
|
|
|
+ stage.show();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void showLoginScene() throws Exception { /* 加载login.fxml */ }
|
|
|
+ public static void showRegisterScene() throws Exception { /* 加载register.fxml */ }
|
|
|
+ public static void showMainScene() throws Exception { /* 加载main.fxml */ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 控制器
|
|
|
+
|
|
|
+**LoginController.java — 登录逻辑**
|
|
|
+
|
|
|
+```java
|
|
|
+public class LoginController {
|
|
|
+
|
|
|
+ @FXML private TextField usernameField;
|
|
|
+ @FXML private PasswordField passwordField;
|
|
|
+ private final UserService userService = new UserService();
|
|
|
+
|
|
|
+ @FXML
|
|
|
+ private void handleLogin() {
|
|
|
+ String username = usernameField.getText().trim();
|
|
|
+ String password = passwordField.getText().trim();
|
|
|
+ if (username.isEmpty() || password.isEmpty()) {
|
|
|
+ showAlert("请填写所有字段");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ R<User> result = userService.login(username, password);
|
|
|
+ if (result.getCode() == 200 && result.getData() != null) {
|
|
|
+ LoginUserHolder.set(result.getData());
|
|
|
+ UIApplication.showMainScene();
|
|
|
+ } else {
|
|
|
+ showAlert(result.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @FXML
|
|
|
+ private void handleRegister() { UIApplication.showRegisterScene(); }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**MainController.java — 核心控制器(核心片段)**
|
|
|
+
|
|
|
+```java
|
|
|
+public class MainController {
|
|
|
+
|
|
|
+ // === 服务初始化 ===
|
|
|
+ private final BookService bookService = new BookService();
|
|
|
+ private final AiService aiService = new AiService();
|
|
|
+ private final TransactionService transactionService = new TransactionService();
|
|
|
+ private final UserService userService = new UserService();
|
|
|
+ private final StatisticsService statisticsService = new StatisticsService();
|
|
|
+
|
|
|
+ // === 导航 ===
|
|
|
+ @FXML private Hyperlink navBrowse, navTrans, navSeller, navAdmin;
|
|
|
+ @FXML private StackPane contentArea;
|
|
|
+
|
|
|
+ @FXML
|
|
|
+ private void initialize() {
|
|
|
+ loadSubPane("/fxml/browse.fxml"); // 默认加载浏览页
|
|
|
+ setupNavigation();
|
|
|
+ initProfile();
|
|
|
+ if (LoginUserHolder.isLoggedIn()) {
|
|
|
+ loadUserTransactions();
|
|
|
+ loadSellerData();
|
|
|
+ loadCharts();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // === 图书浏览与购买 ===
|
|
|
+ @FXML private TextField searchField;
|
|
|
+ @FXML private ComboBox<String> categoryCombo;
|
|
|
+ @FXML private TilePane bookCardGrid;
|
|
|
+
|
|
|
+ private void loadBooks() {
|
|
|
+ R<PageResult<Book>> result = bookService.search(keyword, category, page, size);
|
|
|
+ bookCardGrid.getChildren().clear();
|
|
|
+ for (Book book : result.getData().getRecords()) {
|
|
|
+ VBox card = createBookCard(book);
|
|
|
+ card.setOnMouseClicked(e -> showBookDetail(book));
|
|
|
+ bookCardGrid.getChildren().add(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void purchaseBook(Book book) {
|
|
|
+ boolean confirmed = confirm("确认购买", "购买数量?");
|
|
|
+ if (confirmed) {
|
|
|
+ R<TransactionRecord> result = transactionService.purchase(
|
|
|
+ book.getBookId(), LoginUserHolder.get().getUserid(), quantity);
|
|
|
+ if (result.getCode() == 200) {
|
|
|
+ showInfo("购买成功!");
|
|
|
+ loadBooks();
|
|
|
+ } else {
|
|
|
+ showAlert(result.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // === 统计图表 ===
|
|
|
+ private void loadCharts() {
|
|
|
+ R<StatisticsSummary> summary = statisticsService.getSummary();
|
|
|
+ // 更新KPI卡片:总金额、总笔数
|
|
|
+
|
|
|
+ R<List<DeptSalesDTO>> depts = statisticsService.getTopDepartments();
|
|
|
+ // 绘制饼图 PieChart
|
|
|
+
|
|
|
+ R<List<HotBookDTO>> hotBooks = statisticsService.getTopBooks();
|
|
|
+ // 绘制柱状图 BarChart
|
|
|
+
|
|
|
+ R<List<DeptDiscountDTO>> discounts = statisticsService.getDiscountRates();
|
|
|
+ // 绘制折线图 LineChart
|
|
|
+ }
|
|
|
+
|
|
|
+ // === AI分析 ===
|
|
|
+ @FXML private TextArea analysisArea;
|
|
|
+ @FXML private ProgressBar analysisProgress;
|
|
|
+
|
|
|
+ private void loadAnalysis() {
|
|
|
+ analysisProgress.setVisible(true);
|
|
|
+ aiService.streamAnalysis(
|
|
|
+ token -> Platform.runLater(() -> analysisArea.appendText(token)),
|
|
|
+ () -> Platform.runLater(() -> analysisProgress.setVisible(false)),
|
|
|
+ error -> Platform.runLater(() -> showAlert("AI分析失败: " + error.getMessage()))
|
|
|
+ );
|
|
|
+ }
|
|
|
+ // ... 其余功能(卖家KPI、管理员用户管理/审核、交易管理、个人资料编辑等)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 客户端Service层
|
|
|
+
|
|
|
+**HttpClientUtil — HTTP通信封装**
|
|
|
+
|
|
|
+```java
|
|
|
+public class HttpClientUtil {
|
|
|
+ private static final String BASE_URL = "http://localhost:8080";
|
|
|
+ private static final HttpClient httpClient = HttpClient.newBuilder()
|
|
|
+ .connectTimeout(Duration.ofSeconds(10)).build();
|
|
|
+ private static final ObjectMapper mapper = new ObjectMapper()
|
|
|
+ .registerModule(new JavaTimeModule())
|
|
|
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
|
+
|
|
|
+ public static <T> R<T> get(String path, Class<T> responseType) {
|
|
|
+ return send("GET", path, null, responseType);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> R<T> post(String path, Object body, Class<T> responseType) {
|
|
|
+ return send("POST", path, body, responseType);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> R<T> put(String path, Object body, Class<T> responseType) {
|
|
|
+ return send("PUT", path, body, responseType);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> R<T> delete(String path, Class<T> responseType) {
|
|
|
+ return send("DELETE", path, null, responseType);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static <T> R<T> send(String method, String path, Object body, Class<T> responseType) {
|
|
|
+ // 构建HttpRequest → 发送 → Jackson反序列化为R<T>
|
|
|
+ // 异常处理:ConnectException提示"无法连接到服务器"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**LoginUserHolder — 用户会话**
|
|
|
+
|
|
|
+```java
|
|
|
+public class LoginUserHolder {
|
|
|
+ private static User currentUser;
|
|
|
+
|
|
|
+ public static void set(User user) { currentUser = user; }
|
|
|
+ public static User get() { return currentUser; }
|
|
|
+ public static boolean isLoggedIn() { return currentUser != null; }
|
|
|
+ public static boolean isSeller() { return currentUser != null && currentUser.getIsSeller() == 1; }
|
|
|
+ public static void clear() { currentUser = null; }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**各Service调用示例**
|
|
|
+
|
|
|
+```java
|
|
|
+// UserService
|
|
|
+public R<User> login(String username, String password) {
|
|
|
+ LoginRequest req = new LoginRequest();
|
|
|
+ req.setUsername(username); req.setPassword(password);
|
|
|
+ return HttpClientUtil.post("/api/user/login", req, User.class);
|
|
|
+}
|
|
|
+
|
|
|
+// BookService
|
|
|
+public R<PageResult<Book>> search(String keyword, String category, int page, int size) {
|
|
|
+ StringBuilder path = new StringBuilder("/api/book?page=" + page + "&size=" + size);
|
|
|
+ if (keyword != null && !keyword.isBlank())
|
|
|
+ path.append("&keyword=").append(URLEncoder.encode(keyword, StandardCharsets.UTF_8));
|
|
|
+ if (category != null && !category.isBlank())
|
|
|
+ path.append("&category=").append(URLEncoder.encode(category, StandardCharsets.UTF_8));
|
|
|
+ return HttpClientUtil.get(path.toString(), new TypeReference<R<PageResult<Book>>>() {});
|
|
|
+}
|
|
|
+
|
|
|
+// TransactionService
|
|
|
+public R<TransactionRecord> purchase(Integer bookId, Integer buyerId, Integer quantity) {
|
|
|
+ Map<String, Object> body = Map.of("bookId", bookId, "buyerId", buyerId, "quantity", quantity);
|
|
|
+ return HttpClientUtil.post("/api/transaction", body, TransactionRecord.class);
|
|
|
+}
|
|
|
+
|
|
|
+// StatisticsService
|
|
|
+public R<List<HotBookDTO>> getTopBooks() {
|
|
|
+ return HttpClientUtil.get("/api/statistics/top-books",
|
|
|
+ new TypeReference<R<List<HotBookDTO>>>() {});
|
|
|
+}
|
|
|
+
|
|
|
+// AiService
|
|
|
+public void streamAnalysis(Consumer<String> onToken, Runnable onComplete,
|
|
|
+ Consumer<Throwable> onError) {
|
|
|
+ HttpClient client = HttpClient.newHttpClient();
|
|
|
+ HttpRequest request = HttpRequest.newBuilder()
|
|
|
+ .uri(URI.create("http://localhost:8080/api/ai/analysis/stream"))
|
|
|
+ .GET().build();
|
|
|
+ client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
|
|
|
+ .thenAccept(response -> {
|
|
|
+ response.body().forEach(onToken);
|
|
|
+ onComplete.run();
|
|
|
+ })
|
|
|
+ .exceptionally(e -> { onError.accept(e); return null; });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### FXML视图示例
|
|
|
+
|
|
|
+**login.fxml — 登录界面布局**
|
|
|
+
|
|
|
+```xml
|
|
|
+<VBox alignment="CENTER" spacing="20" styleClass="login-bg">
|
|
|
+ <VBox maxWidth="400" styleClass="card">
|
|
|
+ <Label text="校园二手书交易系统" styleClass="h3" />
|
|
|
+ <TextField fx:id="usernameField" promptText="用户名" />
|
|
|
+ <PasswordField fx:id="passwordField" promptText="密码" />
|
|
|
+ <Button text="登录" onAction="#handleLogin" />
|
|
|
+ <Hyperlink text="没有账号?立即注册" onAction="#handleRegister" />
|
|
|
+ </VBox>
|
|
|
+</VBox>
|
|
|
+```
|
|
|
+
|
|
|
+**admin.fxml — 管理员面板(211行,含5个子面板)**
|
|
|
+
|
|
|
+```xml
|
|
|
+<SplitPane>
|
|
|
+ <VBox styleClass="admin-sidebar">
|
|
|
+ <Button text="用户管理" onAction="#showUserMgmt" />
|
|
|
+ <Button text="图书管理" onAction="#showBookMgmt" />
|
|
|
+ <Button text="待审核" onAction="#showPendingBooks" />
|
|
|
+ <Button text="交易管理" onAction="#showTransMgmt" />
|
|
|
+ <Button text="统计" onAction="#showStats" />
|
|
|
+ </VBox>
|
|
|
+ <StackPane fx:id="adminContent">
|
|
|
+ <!-- 动态切换子面板 -->
|
|
|
+ </StackPane>
|
|
|
+</SplitPane>
|
|
|
+```
|
|
|
+
|
|
|
+**stats.fxml — 统计面板(80行)**
|
|
|
+
|
|
|
+```xml
|
|
|
+<ScrollPane>
|
|
|
+ <VBox spacing="20">
|
|
|
+ <HBox spacing="20">
|
|
|
+ <VBox styleClass="kpi-card"><Label text="交易总金额" /><Label fx:id="totalAmount" /></VBox>
|
|
|
+ <VBox styleClass="kpi-card"><Label text="交易总笔数" /><Label fx:id="totalCount" /></VBox>
|
|
|
+ </HBox>
|
|
|
+ <PieChart fx:id="deptPieChart" title="院系交易额占比" />
|
|
|
+ <BarChart fx:id="topBooksChart" title="热门书籍TOP5" />
|
|
|
+ <LineChart fx:id="discountLineChart" title="各院系折扣率" />
|
|
|
+ </VBox>
|
|
|
+</ScrollPane>
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 客户端运行效果图
|
|
|
+
|
|
|
+> 以下为占位符,实际报告中替换为对应截图。
|
|
|
+
|
|
|
+**登录界面**
|
|
|
+
|
|
|
+![登录界面截图]
|
|
|
+*说明:居中卡片式登录表单,包含用户名/密码输入框和登录按钮,底部有注册跳转链接。*
|
|
|
+
|
|
|
+**注册界面**
|
|
|
+
|
|
|
+![注册界面截图]
|
|
|
+*说明:注册表单包含用户名、密码、昵称、邮箱、手机号、院系六个字段。*
|
|
|
+
|
|
|
+**图书浏览(Browse Tab)**
|
|
|
+
|
|
|
+![图书浏览截图]
|
|
|
+*说明:顶部搜索栏(关键词+分类下拉),中间图书卡片网格(封面占位+书名+作者+价格+库存),右侧详情面板,底部分页。*
|
|
|
+
|
|
|
+**图书购买流程**
|
|
|
+
|
|
|
+![购买弹窗截图]
|
|
|
+*说明:点击图书卡片弹出详情面板,点击"购买"弹出确认对话框输入数量,购买成功后提示。*
|
|
|
+
|
|
|
+**个人交易记录(Transactions Tab)**
|
|
|
+
|
|
|
+![交易记录截图]
|
|
|
+*说明:表格展示该用户的交易记录(交易ID、书名、数量、价格、时间)。*
|
|
|
+
|
|
|
+**个人资料编辑(Profile Tab)**
|
|
|
+
|
|
|
+![个人资料截图]
|
|
|
+*说明:显示当前用户信息(姓名、院系、信用分),可编辑昵称、邮箱、手机号,可切换卖家身份。*
|
|
|
+
|
|
|
+**卖家仪表盘(Seller Tab)**
|
|
|
+
|
|
|
+![卖家仪表盘截图]
|
|
|
+*说明:顶部KPI卡片(在售/库存/已售数量),下方图书管理表格(书名、价格、库存、状态、操作按钮)。*
|
|
|
+
|
|
|
+**管理员用户管理(Admin → User Mgmt)**
|
|
|
+
|
|
|
+![用户管理截图]
|
|
|
+*说明:用户表格(ID、用户名、院系、角色、信用分),可编辑/删除。*
|
|
|
+
|
|
|
+**管理员图书管理(Admin → Book Mgmt)**
|
|
|
+
|
|
|
+![图书管理截图]
|
|
|
+*说明:所有图书列表表格,含卖家名、状态、操作列。*
|
|
|
+
|
|
|
+**管理员待审核(Admin → Pending)**
|
|
|
+
|
|
|
+![待审核截图]
|
|
|
+*说明:待审核图书列表(status=0),含审核通过/拒绝按钮。*
|
|
|
+
|
|
|
+**管理员交易管理(Admin → Trans Mgmt)**
|
|
|
+
|
|
|
+![交易管理截图]
|
|
|
+*说明:所有交易记录表格,含买家名、卖家名、金额、时间。*
|
|
|
+
|
|
|
+**统计面板(Admin → Stats)**
|
|
|
+
|
|
|
+![统计面板截图]
|
|
|
+*说明:KPI卡片卡片(总金额/总笔数),院系交易额饼图,热门书籍TOP5柱状图,各院系折扣率折线图。*
|
|
|
+
|
|
|
+**AI分析报告**
|
|
|
+
|
|
|
+![AI分析报告截图]
|
|
|
+*说明:AI分析页面,展示生成的运营分析报告(交易总览、热门院系、热门书籍、折扣率、分析结论),支持流式输出。*
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 附录:数据库结构
|
|
|
+
|
|
|
+**ER图**
|
|
|
+
|
|
|
+```mermaid
|
|
|
+erDiagram
|
|
|
+ User ||--o{ Book : "发布"
|
|
|
+ User ||--o{ TransactionRecord : "购买"
|
|
|
+ Book ||--o{ TransactionRecord : "被购买"
|
|
|
+
|
|
|
+ User {
|
|
|
+ int userid PK
|
|
|
+ varchar username UK
|
|
|
+ varchar password
|
|
|
+ varchar nickname
|
|
|
+ varchar email
|
|
|
+ varchar phone
|
|
|
+ varchar dept
|
|
|
+ int credit
|
|
|
+ int role "0=学生 1=管理员"
|
|
|
+ int is_seller "0=否 1=是"
|
|
|
+ datetime created_at
|
|
|
+ }
|
|
|
+
|
|
|
+ Book {
|
|
|
+ int book_id PK
|
|
|
+ int seller_id FK
|
|
|
+ varchar title
|
|
|
+ varchar author
|
|
|
+ varchar category
|
|
|
+ decimal original_price
|
|
|
+ decimal selling_price
|
|
|
+ varchar image_url
|
|
|
+ datetime created_at
|
|
|
+ int stock
|
|
|
+ int status "0=待审核 1=上架 2=下架"
|
|
|
+ }
|
|
|
+
|
|
|
+ TransactionRecord {
|
|
|
+ int trans_id PK
|
|
|
+ int book_id FK
|
|
|
+ int buyer_id FK
|
|
|
+ int transaction_num
|
|
|
+ decimal transaction_price
|
|
|
+ datetime transaction_time
|
|
|
+ }
|
|
|
+```
|