feat: 完成 Android 功能并将 MainActivity 迁移为 Java 实现

main
SLMS Development Team 5 months ago
parent 872bb5fe69
commit 20d289918d

@ -44,7 +44,7 @@
android:requestLegacyExternalStorage="false"
android:hardwareAccelerated="true"
android:largeHeap="true"
android:usesCleartextTraffic="false"
android:usesCleartextTraffic="true"
tools:targetApi="34">
<activity
android:name=".android.MainActivity"

@ -1,260 +0,0 @@
package com.smartlibrary.android
import com.smartlibrary.R
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.TextView
import com.smartlibrary.android.ui.UMLViewerActivity
/**
* 智能图书管理系统 - Android 主界面
*/
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var emptyView: TextView
private lateinit var progressBar: ProgressBar
private lateinit var bookAdapter: BookAdapter
private val books = mutableListOf<Book>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupViews()
loadMockData()
}
private fun setupViews() {
recyclerView = findViewById(R.id.recyclerView)
emptyView = findViewById(R.id.emptyView)
progressBar = findViewById(R.id.progressBar)
bookAdapter = BookAdapter(books) { book ->
showBookDetail(book)
}
recyclerView.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = bookAdapter
}
findViewById<Button>(R.id.btnAddBook).setOnClickListener {
showAddBookDialog()
}
findViewById<Button>(R.id.btnAiAssistant).setOnClickListener {
showAiAssistant()
}
}
private fun loadMockData() {
progressBar.visibility = View.VISIBLE
// 模拟数据
books.clear()
books.addAll(listOf(
Book("1", "设计模式", "Gang of Four", "978-0-201-63361-0", "机械工业出版社", "计算机", true),
Book("2", "重构", "Martin Fowler", "978-0-13-475759-9", "人民邮电出版社", "计算机", true),
Book("3", "代码整洁之道", "Robert C. Martin", "978-0-13-235088-4", "人民邮电出版社", "计算机", false),
Book("4", "算法导论", "Thomas H. Cormen", "978-0-262-03384-8", "机械工业出版社", "计算机", true),
Book("5", "深入理解Java虚拟机", "周志明", "978-7-111-64124-1", "机械工业出版社", "计算机", true)
))
progressBar.visibility = View.GONE
updateUI()
}
private fun updateUI() {
if (books.isEmpty()) {
recyclerView.visibility = View.GONE
emptyView.visibility = View.VISIBLE
} else {
recyclerView.visibility = View.VISIBLE
emptyView.visibility = View.GONE
bookAdapter.notifyDataSetChanged()
}
}
private fun showBookDetail(book: Book) {
AlertDialog.Builder(this)
.setTitle(book.title)
.setMessage("""
作者: ${book.author}
ISBN: ${book.isbn}
出版社: ${book.publisher}
分类: ${book.category}
状态: ${if (book.available) "可借阅" else "已借出"}
""".trimIndent())
.setPositiveButton(if (book.available) "借阅" else "归还") { _, _ ->
book.available = !book.available
bookAdapter.notifyDataSetChanged()
Toast.makeText(this, if (book.available) "归还成功" else "借阅成功", Toast.LENGTH_SHORT).show()
}
.setNegativeButton("关闭", null)
.show()
}
private fun showAddBookDialog() {
val dialogView: View = layoutInflater.inflate(R.layout.dialog_add_book, null)
AlertDialog.Builder(this)
.setTitle("添加图书")
.setView(dialogView)
.setPositiveButton("添加") { _: android.content.DialogInterface, _: Int ->
val title = dialogView.findViewById<EditText>(R.id.etTitle).text.toString()
val author = dialogView.findViewById<EditText>(R.id.etAuthor).text.toString()
val isbn = dialogView.findViewById<EditText>(R.id.etIsbn).text.toString()
if (title.isNotBlank() && author.isNotBlank()) {
val newBook = Book(
id = (books.size + 1).toString(),
title = title,
author = author,
isbn = isbn,
publisher = "未知",
category = "未分类",
available = true
)
books.add(0, newBook)
updateUI()
Toast.makeText(this, "添加成功", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton("取消", null)
.show()
}
private fun showAiAssistant() {
val input = EditText(this).apply {
hint = "请输入您的问题..."
}
AlertDialog.Builder(this)
.setTitle("🤖 AI智能助手")
.setView(input)
.setPositiveButton("发送") { _, _ ->
val question = input.text.toString()
if (question.isNotBlank()) {
// 模拟AI回复
val response = when {
question.contains("推荐") -> "根据您的借阅历史,推荐您阅读《设计模式》和《重构》"
question.contains("搜索") || question.contains("查找") -> "已为您找到相关图书,请查看列表"
else -> "感谢您的提问!如需帮助,可以问我:推荐图书、搜索图书等"
}
AlertDialog.Builder(this)
.setTitle("AI回复")
.setMessage(response)
.setPositiveButton("确定", null)
.show()
}
}
.setNegativeButton("取消", null)
.show()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_uml -> {
startActivity(Intent(this, UMLViewerActivity::class.java))
true
}
R.id.action_web -> {
showSwitchToWebDialog()
true
}
R.id.action_about -> {
showAboutDialog()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun showSwitchToWebDialog() {
AlertDialog.Builder(this)
.setTitle("切换到Web端")
.setMessage("Web服务地址:\nhttp://localhost:8082\n\n请确保后端服务已启动")
.setPositiveButton("打开浏览器") { _, _ ->
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("http://localhost:8082"))
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, "无法打开浏览器", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton("取消", null)
.show()
}
private fun showAboutDialog() {
AlertDialog.Builder(this)
.setTitle("关于")
.setMessage("智能图书管理系统 (SLMS)\n版本: 1.0.0\n\n四端统一架构:\n• CLI 命令行\n• GUI 桌面应用\n• WUI Web应用\n• App 移动端")
.setPositiveButton("确定", null)
.show()
}
}
/**
* 图书数据类
*/
data class Book(
val id: String,
val title: String,
val author: String,
val isbn: String,
val publisher: String,
val category: String,
var available: Boolean
)
/**
* 图书列表适配器
*/
class BookAdapter(
private val books: List<Book>,
private val onClick: (Book) -> Unit
) : RecyclerView.Adapter<BookAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val tvTitle: TextView = view.findViewById(R.id.tvTitle)
val tvAuthor: TextView = view.findViewById(R.id.tvAuthor)
val tvStatus: TextView = view.findViewById(R.id.tvStatus)
}
override fun onCreateViewHolder(parent: android.view.ViewGroup, viewType: Int): ViewHolder {
val view = android.view.LayoutInflater.from(parent.context)
.inflate(R.layout.item_book, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val book = books[position]
holder.tvTitle.text = book.title
holder.tvAuthor.text = "作者: ${book.author}"
holder.tvStatus.text = if (book.available) "✅ 可借阅" else "❌ 已借出"
holder.itemView.setOnClickListener { onClick(book) }
}
override fun getItemCount() = books.size
}

@ -12,6 +12,56 @@
android:title="切换到Web"
app:showAsAction="never" />
<item
android:id="@+id/action_reservations"
android:title="我的预约"
app:showAsAction="never" />
<item
android:id="@+id/action_history"
android:title="我的借阅历史"
app:showAsAction="never" />
<item
android:id="@+id/action_notifications"
android:title="通知中心"
app:showAsAction="never" />
<item
android:id="@+id/action_favorites"
android:title="我的收藏"
app:showAsAction="never" />
<item
android:id="@+id/action_notes"
android:title="我的笔记"
app:showAsAction="never" />
<item
android:id="@+id/action_comments"
android:title="图书评论"
app:showAsAction="never" />
<item
android:id="@+id/action_feedback"
android:title="意见反馈"
app:showAsAction="never" />
<item
android:id="@+id/action_stats"
android:title="统计概览"
app:showAsAction="never" />
<item
android:id="@+id/action_ai_analysis"
android:title="AI阅读分析"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:title="系统状态"
app:showAsAction="never" />
<item
android:id="@+id/action_about"
android:title="关于"

@ -2,6 +2,7 @@ package com.smartlibrary.backend.controller;
import com.smartlibrary.model.Book;
import com.smartlibrary.model.Loan;
import com.smartlibrary.model.User;
import com.smartlibrary.service.BookService;
import com.smartlibrary.service.StatisticsService;
import com.smartlibrary.service.NotificationService;
@ -9,6 +10,7 @@ import com.smartlibrary.service.LoanHistoryService;
import com.smartlibrary.service.ReservationService;
import com.smartlibrary.service.ReaderInteractionService;
import com.smartlibrary.service.SystemSettingsService;
import com.smartlibrary.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@ -273,6 +275,37 @@ public class WebController {
return "stats";
}
@GetMapping("/users")
public String users(@RequestParam(required = false) String keyword,
@RequestParam(required = false, defaultValue = "ALL") String role,
@RequestParam(required = false, defaultValue = "ALL") String status,
Model model) {
UserService userService = new UserService();
List<User> allUsers = userService.findAllUsers();
String kw = keyword != null ? keyword.trim() : "";
String roleFilter = role != null ? role.trim().toUpperCase() : "ALL";
String statusFilter = status != null ? status.trim().toUpperCase() : "ALL";
List<User> filtered = allUsers.stream()
.filter(u -> kw.isEmpty() ||
(u.getName() != null && u.getName().contains(kw)) ||
(u.getEmail() != null && u.getEmail().contains(kw)) ||
(u.getPhone() != null && u.getPhone().contains(kw)) ||
(u.getDepartment() != null && u.getDepartment().contains(kw)) ||
(u.getMajor() != null && u.getMajor().contains(kw)))
.filter(u -> "ALL".equals(roleFilter) || (u.getRole() != null && u.getRole().name().equalsIgnoreCase(roleFilter)))
.filter(u -> "ALL".equals(statusFilter) || (u.getStatus() != null && u.getStatus().name().equalsIgnoreCase(statusFilter)))
.toList();
model.addAttribute("users", filtered);
model.addAttribute("keyword", kw);
model.addAttribute("roleFilter", roleFilter);
model.addAttribute("statusFilter", statusFilter);
model.addAttribute("pendingCount", userService.countPendingUsers());
model.addAttribute("pendingUsers", userService.getPendingUsers());
return "users";
}
/**
* API:
@ -486,6 +519,32 @@ public class WebController {
"count", readerService.getLikeCount(targetType, targetId)
);
}
@PostMapping("/users/approve/{id}")
public String approveUser(@PathVariable String id) {
UserService userService = new UserService();
userService.approveUser(id);
return "redirect:/users?approved=true";
}
@PostMapping("/users/reject/{id}")
public String rejectUser(@PathVariable String id) {
UserService userService = new UserService();
userService.rejectUser(id);
return "redirect:/users?rejected=true";
}
@PostMapping("/users/role/{id}")
public String updateUserRole(@PathVariable String id, @RequestParam String role) {
UserService userService = new UserService();
try {
User.Role newRole = User.Role.valueOf(role);
userService.updateUserRole(id, newRole);
return "redirect:/users?roleUpdated=true";
} catch (IllegalArgumentException e) {
return "redirect:/users?roleError=true";
}
}
/**
*
@ -611,6 +670,29 @@ public class WebController {
return bookService.findAllBooks();
}
/**
* API
*/
@PostMapping("/api/login")
@ResponseBody
public Map<String, Object> apiLogin(@RequestBody LoginRequest loginRequest) {
UserService userService = new UserService();
var user = userService.login(loginRequest.getEmail(), loginRequest.getPassword());
if (user == null) {
return Map.of(
"success", false,
"message", "登录失败,邮箱或密码错误,或账户未审核"
);
}
return Map.of(
"success", true,
"userId", user.getId(),
"name", user.getName(),
"email", user.getEmail(),
"role", user.getRole().name()
);
}
/**
* API
*/
@ -671,6 +753,191 @@ public class WebController {
: new ApiResponse(false, "图书归还失败");
}
/**
* API
*/
@PostMapping("/api/reservations")
@ResponseBody
public ReservationApiResponse apiReserveBook(@RequestBody ReservationRequest request) {
ReservationService reservationService = new ReservationService();
var result = reservationService.reserveBook(request.getBookId(), request.getUserId());
return new ReservationApiResponse(result.success(), result.message(), result.reservation());
}
/**
* API
*/
@GetMapping("/api/reservations")
@ResponseBody
public List<ReservationService.Reservation> apiUserReservations(@RequestParam String userId) {
ReservationService reservationService = new ReservationService();
return reservationService.getUserReservations(userId);
}
/**
* API
*/
@DeleteMapping("/api/reservations/{id}")
@ResponseBody
public ApiResponse apiCancelReservation(@PathVariable String id) {
ReservationService reservationService = new ReservationService();
boolean success = reservationService.cancelReservation(id);
return success ? new ApiResponse(true, "取消预约成功")
: new ApiResponse(false, "取消预约失败");
}
/**
* API
*/
@GetMapping("/api/notifications")
@ResponseBody
public Map<String, Object> apiUserNotifications(@RequestParam String userId) {
NotificationService notificationService = new NotificationService();
var notifications = notificationService.getUserNotifications(userId);
int unread = notificationService.getUnreadCount(userId);
return Map.of(
"success", true,
"notifications", notifications,
"unreadCount", unread
);
}
/**
* API
*/
@GetMapping("/api/history")
@ResponseBody
public Map<String, Object> apiUserHistory(@RequestParam String userId) {
LoanHistoryService historyService = new LoanHistoryService();
var history = historyService.getUserHistory(userId);
double totalFine = historyService.getTotalFine(userId);
return Map.of(
"success", true,
"history", history,
"totalFine", totalFine
);
}
@GetMapping("/api/favorites")
@ResponseBody
public Map<String, Object> apiUserFavorites(@RequestParam String userId) {
ReaderInteractionService readerService = new ReaderInteractionService();
var favorites = readerService.getUserFavorites(userId);
return Map.of(
"success", true,
"favorites", favorites
);
}
@PostMapping("/api/favorites")
@ResponseBody
public ApiResponse apiAddFavorite(@RequestBody FavoriteRequest request) {
ReaderInteractionService readerService = new ReaderInteractionService();
boolean success = readerService.addFavorite(request.getUserId(), request.getBookId());
return success ? new ApiResponse(true, "收藏成功")
: new ApiResponse(false, "收藏失败");
}
@DeleteMapping("/api/favorites")
@ResponseBody
public ApiResponse apiRemoveFavorite(@RequestParam String userId, @RequestParam String bookId) {
ReaderInteractionService readerService = new ReaderInteractionService();
boolean success = readerService.removeFavorite(userId, bookId);
return success ? new ApiResponse(true, "取消收藏成功")
: new ApiResponse(false, "取消收藏失败");
}
@GetMapping("/api/notes")
@ResponseBody
public Map<String, Object> apiUserNotes(@RequestParam String userId) {
ReaderInteractionService readerService = new ReaderInteractionService();
var notes = readerService.getUserNotes(userId);
return Map.of(
"success", true,
"notes", notes
);
}
@PostMapping("/api/notes")
@ResponseBody
public ApiResponse apiAddNote(@RequestBody NoteRequest request) {
ReaderInteractionService readerService = new ReaderInteractionService();
String id = readerService.addNote(
request.getUserId(),
request.getBookId(),
request.getTitle(),
request.getContent(),
request.getPageNumber(),
request.getChapter(),
request.isPublicNote()
);
return id != null ? new ApiResponse(true, "添加笔记成功")
: new ApiResponse(false, "添加笔记失败");
}
@DeleteMapping("/api/notes/{id}")
@ResponseBody
public ApiResponse apiDeleteNote(@PathVariable String id) {
ReaderInteractionService readerService = new ReaderInteractionService();
boolean success = readerService.deleteNote(id);
return success ? new ApiResponse(true, "删除笔记成功")
: new ApiResponse(false, "删除笔记失败");
}
@GetMapping("/api/comments")
@ResponseBody
public Map<String, Object> apiBookComments(@RequestParam String bookId) {
ReaderInteractionService readerService = new ReaderInteractionService();
var comments = readerService.getBookComments(bookId);
double avgRating = readerService.getBookAverageRating(bookId);
return Map.of(
"success", true,
"comments", comments,
"avgRating", avgRating
);
}
@PostMapping("/api/comments")
@ResponseBody
public ApiResponse apiAddComment(@RequestBody CommentRequest request) {
ReaderInteractionService readerService = new ReaderInteractionService();
String id = readerService.addComment(
request.getUserId(),
request.getBookId(),
request.getContent(),
request.getRating(),
null
);
return id != null ? new ApiResponse(true, "添加评论成功")
: new ApiResponse(false, "添加评论失败");
}
@GetMapping("/api/feedback")
@ResponseBody
public Map<String, Object> apiUserFeedback(@RequestParam String userId) {
ReaderInteractionService readerService = new ReaderInteractionService();
var feedbacks = readerService.getUserFeedbacks(userId);
return Map.of(
"success", true,
"feedbacks", feedbacks
);
}
@PostMapping("/api/feedback")
@ResponseBody
public ApiResponse apiSubmitFeedback(@RequestBody FeedbackRequest request) {
ReaderInteractionService readerService = new ReaderInteractionService();
String id = readerService.submitFeedback(
request.getUserId(),
request.getType(),
request.getTitle(),
request.getContent(),
request.getContact()
);
return id != null ? new ApiResponse(true, "反馈提交成功")
: new ApiResponse(false, "反馈提交失败");
}
// ========== 请求和响应类 ==========
public static class ApiResponse {
@ -688,6 +955,16 @@ public class WebController {
public void setMessage(String message) { this.message = message; }
}
public static class LoginRequest {
private String email;
private String password;
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
public static class BookRequest {
private String bookType;
private String title;
@ -729,4 +1006,103 @@ public class WebController {
public String getBookId() { return bookId; }
public void setBookId(String bookId) { this.bookId = bookId; }
}
public static class ReservationRequest {
private String bookId;
private String userId;
public String getBookId() { return bookId; }
public void setBookId(String bookId) { this.bookId = bookId; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
}
public static class ReservationApiResponse {
private boolean success;
private String message;
private ReservationService.Reservation reservation;
public ReservationApiResponse(boolean success, String message, ReservationService.Reservation reservation) {
this.success = success;
this.message = message;
this.reservation = reservation;
}
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public ReservationService.Reservation getReservation() { return reservation; }
public void setReservation(ReservationService.Reservation reservation) { this.reservation = reservation; }
}
public static class FavoriteRequest {
private String userId;
private String bookId;
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getBookId() { return bookId; }
public void setBookId(String bookId) { this.bookId = bookId; }
}
public static class NoteRequest {
private String userId;
private String bookId;
private String title;
private String content;
private int pageNumber;
private String chapter;
private boolean publicNote;
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getBookId() { return bookId; }
public void setBookId(String bookId) { this.bookId = bookId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public int getPageNumber() { return pageNumber; }
public void setPageNumber(int pageNumber) { this.pageNumber = pageNumber; }
public String getChapter() { return chapter; }
public void setChapter(String chapter) { this.chapter = chapter; }
public boolean isPublicNote() { return publicNote; }
public void setPublicNote(boolean publicNote) { this.publicNote = publicNote; }
}
public static class CommentRequest {
private String userId;
private String bookId;
private String content;
private int rating;
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getBookId() { return bookId; }
public void setBookId(String bookId) { this.bookId = bookId; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public int getRating() { return rating; }
public void setRating(int rating) { this.rating = rating; }
}
public static class FeedbackRequest {
private String userId;
private String type;
private String title;
private String content;
private String contact;
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getContact() { return contact; }
public void setContact(String contact) { this.contact = contact; }
}
}

@ -12,6 +12,7 @@
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'home'} ? 'active'" th:href="@{/}">首页</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'books'} ? 'active'" th:href="@{/books}">图书</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'loans'} ? 'active'" th:href="@{/loans}">借阅</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'users'} ? 'active'" th:href="@{/users}">用户</a></li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">📚 读者</a>
<ul class="dropdown-menu">

@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - MCSLMS</title>
<th:block th:replace="~{fragments/layout :: styles}"/>
</head>
<body>
<nav th:replace="~{fragments/layout :: navbar('users')}"/>
<main class="container mt-4">
<div th:if="${param.approved}" class="alert alert-success alert-dismissible fade show" role="alert">
用户审核通过成功!<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div th:if="${param.rejected}" class="alert alert-warning alert-dismissible fade show" role="alert">
用户已被标记为拒绝!<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div th:if="${param.roleUpdated}" class="alert alert-success alert-dismissible fade show" role="alert">
用户角色更新成功!<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div th:if="${param.roleError}" class="alert alert-danger alert-dismissible fade show" role="alert">
角色更新失败:无效角色类型。<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>👥 用户管理</h1>
<span class="badge bg-info" th:text="'待审核用户: ' + ${pendingCount}"></span>
</div>
<div class="card mb-4">
<div class="card-body">
<form th:action="@{/users}" method="get" class="row g-3 align-items-end">
<div class="col-md-4">
<label class="form-label">关键字</label>
<input type="text" class="form-control" name="keyword" placeholder="姓名 / 邮箱 / 电话 / 院系 / 专业"
th:value="${keyword}">
</div>
<div class="col-md-3">
<label class="form-label">角色</label>
<select class="form-select" name="role">
<option value="ALL" th:selected="${roleFilter == 'ALL'}">全部角色</option>
<option value="READER" th:selected="${roleFilter == 'READER'}">读者</option>
<option value="LIBRARIAN" th:selected="${roleFilter == 'LIBRARIAN'}">馆员</option>
<option value="ADMIN" th:selected="${roleFilter == 'ADMIN'}">管理员</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">状态</label>
<select class="form-select" name="status">
<option value="ALL" th:selected="${statusFilter == 'ALL'}">全部状态</option>
<option value="PENDING" th:selected="${statusFilter == 'PENDING'}">待审核</option>
<option value="APPROVED" th:selected="${statusFilter == 'APPROVED'}">已通过</option>
<option value="REJECTED" th:selected="${statusFilter == 'REJECTED'}">已拒绝</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">🔍 筛选</button>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">全部用户</h5>
<span class="badge bg-primary" th:text="'共 ' + ${users.size()} + ' 个用户'"></span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>类型</th>
<th>角色</th>
<th>状态</th>
<th>院系/专业</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:if="${users.empty}">
<td colspan="8" class="text-center text-muted py-4">暂无用户记录</td>
</tr>
<tr th:each="u : ${users}">
<td th:text="${u.id}"></td>
<td th:text="${u.name}"></td>
<td th:text="${u.email}"></td>
<td th:text="${u.userType}"></td>
<td>
<span th:if="${u.role.name() == 'ADMIN'}" class="badge bg-danger">管理员</span>
<span th:if="${u.role.name() == 'LIBRARIAN'}" class="badge bg-warning text-dark">馆员</span>
<span th:if="${u.role.name() == 'READER'}" class="badge bg-secondary">读者</span>
</td>
<td>
<span th:if="${u.status.name() == 'PENDING'}" class="badge bg-warning text-dark">待审核</span>
<span th:if="${u.status.name() == 'APPROVED'}" class="badge bg-success">已通过</span>
<span th:if="${u.status.name() == 'REJECTED'}" class="badge bg-secondary">已拒绝</span>
</td>
<td>
<div th:text="${u.department}"></div>
<div class="text-muted small" th:text="${u.major}"></div>
</td>
<td>
<div class="d-flex flex-column gap-1">
<form th:action="@{/users/approve/{id}(id=${u.id})}" method="post" th:if="${u.status.name() == 'PENDING'}">
<button type="submit" class="btn btn-sm btn-success w-100">审核通过</button>
</form>
<form th:action="@{/users/reject/{id}(id=${u.id})}" method="post" th:if="${u.status.name() == 'PENDING'}">
<button type="submit" class="btn btn-sm btn-outline-secondary w-100">拒绝</button>
</form>
<form th:action="@{/users/role/{id}(id=${u.id})}" method="post">
<div class="input-group input-group-sm">
<select class="form-select" name="role">
<option value="READER" th:selected="${u.role.name() == 'READER'}">读者</option>
<option value="LIBRARIAN" th:selected="${u.role.name() == 'LIBRARIAN'}">馆员</option>
<option value="ADMIN" th:selected="${u.role.name() == 'ADMIN'}">管理员</option>
</select>
<button type="submit" class="btn btn-outline-primary">更新</button>
</div>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">待审核用户</h5>
</div>
<div class="card-body">
<div class="list-group" th:if="${!pendingUsers.empty}">
<div class="list-group-item list-group-item-action" th:each="u : ${pendingUsers}">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1" th:text="${u.name}"></h6>
<small th:text="${u.department}"></small>
</div>
<p class="mb-1" th:text="${u.email}"></p>
<small th:text="${u.userType}"></small>
<div class="mt-2 d-flex gap-2">
<form th:action="@{/users/approve/{id}(id=${u.id})}" method="post">
<button type="submit" class="btn btn-sm btn-success">通过</button>
</form>
<form th:action="@{/users/reject/{id}(id=${u.id})}" method="post">
<button type="submit" class="btn btn-sm btn-outline-secondary">拒绝</button>
</form>
</div>
</div>
</div>
<p class="text-muted" th:if="${pendingUsers.empty}">当前没有待审核用户。</p>
</div>
</div>
</div>
</div>
</main>
<footer th:replace="~{fragments/layout :: footer}"/>
<th:block th:replace="~{fragments/layout :: scripts}"/>
</body>
</html>

@ -1 +1,61 @@
所有端的功能点实现状态
## MCSLMS 功能点与四端实现状态
> 基于 `core` 模块中 `SystemSettingsService#getAllFeatures()` 的功能模块定义,结合 CLI / GUI / Web / Android 四端的实际代码实现情况整理。
状态说明:
- ✅ 已实现:该端有完整的功能入口并接入核心服务
- ⚠️ 部分实现:仅实现简化版或演示版,或缺少部分子功能
- ❌ 未实现:当前端没有对应的功能入口或实现
### 1. 核心业务与读者互动功能
| 功能ID | 功能名称 | 描述 | CLI | GUI | Web | Android |
|--------|----------|------|-----|-----|-----|---------|
| books | 图书管理 | 图书的添加、删除、修改、查询 | ✅ | ✅ | ✅ | ⚠️ |
| loans | 借阅管理 | 图书借阅、归还、续借 | ✅ | ✅ | ✅ | ⚠️ |
| users | 用户管理 | 用户注册、信息管理 | ✅ | ✅ | ❌ | ❌ |
| notifications | 通知中心 | 多渠道通知推送 | ✅ | ✅ | ✅ | ❌ |
| reservations | 预约管理 | 图书预约、排队 | ✅ | ✅ | ✅ | ✅ |
| history | 借阅历史 | 借阅记录、罚款 | ✅ | ✅ | ✅ | ❌ |
| notes | 读书笔记 | 记录阅读心得 | ✅ | ✅ | ✅ | ❌ |
| favorites | 我的收藏 | 收藏喜欢的图书 | ✅ | ✅ | ✅ | ❌ |
| comments | 图书评论 | 评价与评分 | ✅ | ✅ | ✅ | ❌ |
| feedback | 意见反馈 | 问题反馈与建议 | ✅ | ❌ | ✅ | ❌ |
| statistics | 数据统计 | 图表分析与报表 | ✅ | ✅ | ✅ | ❌ |
说明:
- **CLI**:通过命令 `notes` / `favorites` / `comments` / `feedback` / `notifications` / `reservations` / `history` / `stats` 等,已接入全部业务与读者互动模块。
- **GUI**:已实现图书管理、借阅归还、预约管理、读者中心(我的借阅 / 借阅历史 / 收藏 / 笔记 / 通知中心)以及基础统计;暂未提供反馈界面。
- **Web**:基于 `WebController` 和 Thymeleaf 页面,已覆盖图书管理、借阅管理、通知中心、预约、历史、笔记、收藏、评论、反馈以及可视化统计。
- **Android**:当前通过 REST API 从后端加载图书列表,并接入统一数据库下的登录与预约管理;借阅/归还和添加图书仍以本地演示逻辑为主,尚未接入通知、历史、笔记、收藏、评论、反馈与统计等业务模块。
### 2. AI 与系统设置功能
| 功能ID | 功能名称 | 描述 | CLI | GUI | Web | Android |
|--------|----------|------|-----|-----|-----|---------|
| ai_recommend | 智能推荐 | AI 图书推荐 | ✅ | ✅ | ❌ | ✅ |
| ai_qa | 智能问答 | AI 助手问答 | ✅ | ✅ | ❌ | ✅ |
| ai_analysis | 智能分析 | 阅读行为分析 / 图书分析 | ❌ | ✅ | ❌ | ❌ |
| settings | 系统设置 | 配置与数据源管理 | ✅ | ❌ | ✅ | ❌ |
说明:
- **CLI**:提供 `ai` / `recommend` 命令进行 AI 问答和推荐,并通过 `settings` 命令查看系统状态;暂未提供独立的 AI 行为分析界面。
- **GUI**:右侧集成 AI 聊天面板,支持聊天问答、智能推荐、图书分析、阅读计划与图书馆报告;当前未接入 `SystemSettingsService` 做统一系统设置界面。
- **Web**:主要通过 `/stats`、`/settings` 访问统计分析与系统配置,尚未集成前端 AI 问答 / 推荐 / 分析界面。
- **Android**:通过 `AIAssistantFragment`、`AIRecommendFragment` 等实现本地/远程 AI 问答与推荐,以及 UML 相关 AI 能力;暂未实现与核心借阅/历史数据联动的阅读行为分析和系统设置功能。
### 3. 四端统一与端切换(补充)
| 功能 | 说明 | CLI | GUI | Web | Android |
|------|------|-----|-----|-----|---------|
| 端切换 | CLI / GUI / Web / App 之间的说明与跳转 | ✅ | ✅ | ✅ | ✅ |
- CLI 通过 `gui` / `web` / `app` 命令提示切换方式。
- GUI 在“切换”菜单中提供 CLI / Web / App 二维码的说明与链接。
- Web 提供 `/switch` 页面说明四端架构与切换方式。
- Android 在菜单中提供“切换到 Web 端”的说明与浏览器跳转。
> 本文档仅标记 **功能是否在各端有入口与实现**,不区分 UI 复杂度和交互细节。后续如果对某端补充了新的功能入口或与核心服务的集成,请同步更新本表。

@ -5,6 +5,7 @@ import com.smartlibrary.service.UserService;
import com.smartlibrary.service.LoanHistoryService;
import com.smartlibrary.service.ReaderInteractionService;
import com.smartlibrary.service.NotificationService;
import com.smartlibrary.service.ReservationService;
import com.smartlibrary.model.Book;
import com.smartlibrary.model.User;
import com.smartlibrary.model.Loan;
@ -31,6 +32,7 @@ public class GUIApplication extends JFrame {
private final LoanHistoryService loanHistoryService;
private final ReaderInteractionService readerService;
private final NotificationService notificationService;
private final ReservationService reservationService;
private final AIService aiService;
private final DesktopSpeechService speechService;
private final PlantUMLService umlService;
@ -53,6 +55,7 @@ public class GUIApplication extends JFrame {
this.loanHistoryService = new LoanHistoryService();
this.readerService = new ReaderInteractionService();
this.notificationService = new NotificationService();
this.reservationService = new ReservationService();
this.aiService = AIModelFactory.getDefaultService();
this.speechService = new DesktopSpeechService();
this.umlService = new PlantUMLService();
@ -220,16 +223,22 @@ public class GUIApplication extends JFrame {
myLoansItem.addActionListener(e -> showMyLoansDialog());
JMenuItem historyItem = new JMenuItem("借阅历史");
historyItem.addActionListener(e -> showHistoryDialog());
JMenuItem reservationsItem = new JMenuItem("我的预约");
reservationsItem.addActionListener(e -> showReservationsDialog());
JMenuItem favoritesItem = new JMenuItem("我的收藏");
favoritesItem.addActionListener(e -> showFavoritesDialog());
JMenuItem notesItem = new JMenuItem("我的笔记");
notesItem.addActionListener(e -> showNotesDialog());
JMenuItem feedbackItem = new JMenuItem("意见反馈");
feedbackItem.addActionListener(e -> showFeedbackDialog());
JMenuItem notificationsItem = new JMenuItem("通知中心");
notificationsItem.addActionListener(e -> showNotificationsDialog());
readerMenu.add(myLoansItem);
readerMenu.add(historyItem);
readerMenu.add(reservationsItem);
readerMenu.add(favoritesItem);
readerMenu.add(notesItem);
readerMenu.add(feedbackItem);
readerMenu.addSeparator();
readerMenu.add(notificationsItem);
menuBar.add(readerMenu);
@ -302,6 +311,10 @@ public class GUIApplication extends JFrame {
returnBtn.addActionListener(e -> returnSelectedBook());
toolBar.add(returnBtn);
JButton reserveBtn = new JButton("预约");
reserveBtn.addActionListener(e -> reserveSelectedBook());
toolBar.add(reserveBtn);
JButton favoriteBtn = new JButton("收藏");
favoriteBtn.addActionListener(e -> toggleFavoriteForSelectedBook());
toolBar.add(favoriteBtn);
@ -709,6 +722,55 @@ public class GUIApplication extends JFrame {
JOptionPane.showMessageDialog(this, scroll, "我的借阅", JOptionPane.INFORMATION_MESSAGE);
}
private void showReservationsDialog() {
if (!ensureLoggedInFor("我的预约")) {
return;
}
List<ReservationService.Reservation> reservations = reservationService.getUserReservations(currentUser.getId());
if (reservations.isEmpty()) {
JOptionPane.showMessageDialog(this, "暂无预约", "我的预约", JOptionPane.INFORMATION_MESSAGE);
return;
}
StringBuilder sb = new StringBuilder();
sb.append("我的预约:\n\n");
for (ReservationService.Reservation r : reservations) {
Book book = bookService.findBookById(r.bookId());
String title = book != null ? book.getTitle() : r.bookId();
String status = r.status() != null ? r.status().getDisplayName() : "";
sb.append(String.format(
"预约ID: %s\n图书: %s (%s)\n状态: %s\n排队位置: %d\n预约日期: %s\n可借日期: %s\n截止日期: %s\n\n",
r.id(),
title,
r.bookId(),
status,
r.queuePosition(),
r.reserveDate(),
r.availableDate(),
r.expireDate()
));
}
JTextArea area = new JTextArea(sb.toString());
area.setEditable(false);
area.setLineWrap(true);
area.setWrapStyleWord(true);
JScrollPane scroll = new JScrollPane(area);
scroll.setPreferredSize(new Dimension(560, 360));
JOptionPane.showMessageDialog(this, scroll, "我的预约", JOptionPane.INFORMATION_MESSAGE);
String reservationId = JOptionPane.showInputDialog(this,
"输入要取消的预约ID留空跳过:",
"取消预约",
JOptionPane.QUESTION_MESSAGE);
if (reservationId == null || reservationId.trim().isEmpty()) {
return;
}
boolean success = reservationService.cancelReservation(reservationId.trim());
JOptionPane.showMessageDialog(this,
success ? "已取消该预约" : "取消预约失败",
"取消预约",
success ? JOptionPane.INFORMATION_MESSAGE : JOptionPane.ERROR_MESSAGE);
}
private void showHistoryDialog() {
if (!ensureLoggedInFor("借阅历史")) {
return;
@ -800,6 +862,51 @@ public class GUIApplication extends JFrame {
JOptionPane.showMessageDialog(this, scroll, "我的笔记", JOptionPane.INFORMATION_MESSAGE);
}
private void showFeedbackDialog() {
if (!ensureLoggedInFor("意见反馈")) {
return;
}
String[] types = {"建议", "问题", "其他"};
JComboBox<String> typeBox = new JComboBox<>(types);
JTextField titleField = new JTextField();
JTextArea contentArea = new JTextArea(5, 20);
JScrollPane contentScroll = new JScrollPane(contentArea);
JTextField contactField = new JTextField(currentUser != null ? currentUser.getEmail() : "");
Object[] fields = {
"反馈类型:", typeBox,
"标题:", titleField,
"内容:", contentScroll,
"联系方式:", contactField
};
int result = JOptionPane.showConfirmDialog(this, fields,
"意见反馈",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
if (result != JOptionPane.OK_OPTION) {
return;
}
String type = (String) typeBox.getSelectedItem();
String title = titleField.getText().trim();
String content = contentArea.getText().trim();
String contact = contactField.getText().trim();
if (title.isEmpty() || content.isEmpty()) {
JOptionPane.showMessageDialog(this, "标题和内容不能为空", "错误", JOptionPane.ERROR_MESSAGE);
return;
}
String id = readerService.submitFeedback(currentUser.getId(), type != null ? type : "其他", title, content, contact);
if (id != null) {
JOptionPane.showMessageDialog(this, "反馈已提交,感谢您的意见!", "意见反馈", JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(this, "提交反馈失败", "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void showNotificationsDialog() {
if (!ensureLoggedInFor("通知中心")) {
return;
@ -899,6 +1006,42 @@ public class GUIApplication extends JFrame {
}
}
private void reserveSelectedBook() {
if (!ensureLoggedInFor("预约图书")) {
return;
}
int row = bookTable.getSelectedRow();
if (row < 0) {
JOptionPane.showMessageDialog(this, "请先选择一本图书");
return;
}
String isbn = (String) tableModel.getValueAt(row, 0);
Book book = bookService.findBookByIsbn(isbn);
if (book == null) {
JOptionPane.showMessageDialog(this, "未找到图书", "错误", JOptionPane.ERROR_MESSAGE);
return;
}
try {
ReservationService.ReservationResult result = reservationService.reserveBook(book.getId(), currentUser.getId());
if (result.success()) {
JOptionPane.showMessageDialog(this,
"预约成功!" + (result.message() != null ? (" " + result.message()) : ""),
"预约图书",
JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(this,
result.message() != null ? result.message() : "预约失败",
"预约图书",
JOptionPane.WARNING_MESSAGE);
}
} catch (Exception e) {
JOptionPane.showMessageDialog(this,
"预约失败: " + e.getMessage(),
"错误",
JOptionPane.ERROR_MESSAGE);
}
}
private void toggleFavoriteForSelectedBook() {
if (!ensureLoggedInFor("收藏图书")) {
return;

Loading…
Cancel
Save