feat(v1.5.0): 通知系统与AI增强

新增服务:
- NotificationService 多渠道通知服务
  - 支持短信/邮件/微信/站内信/电话
  - 借阅成功/归还提醒/逾期通知/罚款通知
  - 预约到书/续借成功/AI推荐等通知类型

- ReservationService 预约管理服务
  - 图书预约/取消预约
  - 预约队列管理
  - 到期自动处理

- LoanHistoryService 借阅历史服务
  - 借阅操作历史记录
  - 续借功能(最多1次,延期14天)
  - 罚款计算(每日0.5元)

AI增强 (SmartAIService):
- 用户借阅行为分析
- 逾期风险预测
- 智能通知内容生成
- 最佳通知时机建议

Web页面:
- /notifications - 通知中心
- /history - 借阅历史
- /reservations - 预约管理
- 续借功能集成

导航栏更新:
- 添加预约/历史/通知入口
main
SLMS Development Team 4 months ago
parent cda70c5acb
commit b777225bd2

@ -4,6 +4,9 @@ import com.smartlibrary.model.Book;
import com.smartlibrary.model.Loan;
import com.smartlibrary.service.BookService;
import com.smartlibrary.service.StatisticsService;
import com.smartlibrary.service.NotificationService;
import com.smartlibrary.service.LoanHistoryService;
import com.smartlibrary.service.ReservationService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@ -296,6 +299,74 @@ public class WebController {
return statisticsService.getMonthlyBorrowTrend(12);
}
// ========== v1.5.0 通知与历史 ==========
/**
*
*/
@GetMapping("/notifications")
public String notifications(@RequestParam(defaultValue = "GUEST") String userId, Model model) {
NotificationService notificationService = new NotificationService();
model.addAttribute("notifications", notificationService.getUserNotifications(userId));
model.addAttribute("unreadCount", notificationService.getUnreadCount(userId));
model.addAttribute("userId", userId);
return "notifications";
}
/**
*
*/
@GetMapping("/history")
public String loanHistory(@RequestParam(defaultValue = "GUEST") String userId, Model model) {
LoanHistoryService historyService = new LoanHistoryService();
model.addAttribute("history", historyService.getUserHistory(userId));
model.addAttribute("totalFine", historyService.getTotalFine(userId));
model.addAttribute("userId", userId);
return "history";
}
/**
*
*/
@GetMapping("/reservations")
public String reservations(@RequestParam(defaultValue = "GUEST") String userId, Model model) {
ReservationService reservationService = new ReservationService();
model.addAttribute("reservations", reservationService.getUserReservations(userId));
model.addAttribute("userId", userId);
model.addAttribute("books", bookService.findAllBooks());
return "reservations";
}
/**
*
*/
@PostMapping("/reservations/add")
public String addReservation(@RequestParam String bookId, @RequestParam String userId) {
ReservationService reservationService = new ReservationService();
reservationService.reserveBook(bookId, userId);
return "redirect:/reservations?userId=" + userId + "&success=true";
}
/**
*
*/
@PostMapping("/reservations/cancel/{id}")
public String cancelReservation(@PathVariable String id, @RequestParam String userId) {
ReservationService reservationService = new ReservationService();
reservationService.cancelReservation(id);
return "redirect:/reservations?userId=" + userId + "&cancelled=true";
}
/**
*
*/
@PostMapping("/loans/renew/{loanId}")
public String renewLoan(@PathVariable String loanId) {
LoanHistoryService historyService = new LoanHistoryService();
historyService.renewLoan(loanId);
return "redirect:/loans?renewed=true";
}
/**
*
*/

@ -12,7 +12,10 @@
<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 == 'stats'} ? 'active'" th:href="@{/stats}">📊 数据统计</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'reservations'} ? 'active'" th:href="@{/reservations}">📅 预约</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'history'} ? 'active'" th:href="@{/history}">📜 历史</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'notifications'} ? 'active'" th:href="@{/notifications}">🔔 通知</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'stats'} ? 'active'" th:href="@{/stats}">📊 统计</a></li>
<li class="nav-item"><a class="nav-link" th:classappend="${active == 'switch'} ? 'active'" th:href="@{/switch}">切换端</a></li>
</ul>
</div>

@ -0,0 +1,149 @@
<!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 v1.5.0</title>
<th:block th:replace="~{fragments/layout :: styles}"/>
<style>
.history-timeline {
position: relative;
padding-left: 30px;
}
.history-timeline::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: #dee2e6;
}
.history-item {
position: relative;
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.history-item::before {
content: '';
position: absolute;
left: -24px;
top: 20px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #007bff;
}
.action-BORROW::before { background: #28a745; }
.action-RETURN::before { background: #17a2b8; }
.action-RENEWAL::before { background: #ffc107; }
.action-OVERDUE::before { background: #dc3545; }
.action-FINE_PAID::before { background: #6c757d; }
.fine-alert {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
color: white;
border-radius: 10px;
padding: 20px;
}
</style>
</head>
<body>
<nav th:replace="~{fragments/layout :: navbar('history')}"/>
<main class="container mt-4">
<h1 class="mb-4">📜 借阅历史</h1>
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span>历史记录</span>
<span class="badge bg-secondary" th:text="${#lists.size(history)} + ' 条记录'">0 条记录</span>
</div>
<div class="card-body">
<div th:if="${#lists.isEmpty(history)}" class="text-center text-muted py-5">
📭 暂无借阅历史记录
</div>
<div class="history-timeline" th:if="${!#lists.isEmpty(history)}">
<div th:each="record : ${history}"
class="history-item"
th:classappend="'action-' + ${record.action().name()}">
<div class="d-flex justify-content-between align-items-start">
<div>
<span class="badge"
th:classappend="${record.action().name() == 'BORROW'} ? 'bg-success' :
(${record.action().name() == 'RETURN'} ? 'bg-info' :
(${record.action().name() == 'RENEWAL'} ? 'bg-warning' :
(${record.action().name() == 'OVERDUE'} ? 'bg-danger' : 'bg-secondary')))"
th:text="${record.action().getDisplayName()}">操作</span>
<strong class="ms-2" th:text="${record.bookTitle() != null} ? '《' + ${record.bookTitle()} + '》' : '图书'">书名</strong>
</div>
<small class="text-muted" th:text="${record.actionDate()}">时间</small>
</div>
<p class="mb-0 mt-2 text-muted small" th:if="${record.details()}"
th:text="${record.details()}">详情</p>
<div class="mt-2" th:if="${record.fineAmount() > 0}">
<span class="badge bg-danger">罚款: ¥<span th:text="${#numbers.formatDecimal(record.fineAmount(), 1, 2)}">0.00</span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<!-- 罚款汇总 -->
<div class="fine-alert mb-4" th:if="${totalFine > 0}">
<h5>⚠️ 待缴罚款</h5>
<div class="display-4">¥<span th:text="${#numbers.formatDecimal(totalFine, 1, 2)}">0.00</span></div>
<p class="mb-0 mt-2 small">请尽快处理,避免影响借阅权限</p>
</div>
<div class="card mb-4" th:if="${totalFine == 0}">
<div class="card-body text-center">
<span style="font-size: 3rem;"></span>
<h5 class="mt-2">无待缴罚款</h5>
<p class="text-muted small">保持良好的借阅习惯!</p>
</div>
</div>
<!-- 操作说明 -->
<div class="card">
<div class="card-header">📌 操作说明</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<span class="badge bg-success">借阅</span>
<small class="text-muted">成功借出图书</small>
</li>
<li class="mb-2">
<span class="badge bg-info">归还</span>
<small class="text-muted">图书已归还</small>
</li>
<li class="mb-2">
<span class="badge bg-warning">续借</span>
<small class="text-muted">延长借阅期限</small>
</li>
<li class="mb-2">
<span class="badge bg-danger">逾期</span>
<small class="text-muted">超过应还日期</small>
</li>
<li>
<span class="badge bg-secondary">缴费</span>
<small class="text-muted">缴纳罚款</small>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<footer th:replace="~{fragments/layout :: footer}"/>
<th:block th:replace="~{fragments/layout :: scripts}"/>
</body>
</html>

@ -0,0 +1,121 @@
<!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 v1.5.0</title>
<th:block th:replace="~{fragments/layout :: styles}"/>
<style>
.notification-card {
border-left: 4px solid #007bff;
margin-bottom: 15px;
transition: all 0.3s;
}
.notification-card.unread {
background-color: #f0f7ff;
border-left-color: #28a745;
}
.notification-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.notification-type {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 10px;
}
.type-BORROW_SUCCESS { background: #d4edda; color: #155724; }
.type-RETURN_REMINDER { background: #fff3cd; color: #856404; }
.type-OVERDUE_NOTICE { background: #f8d7da; color: #721c24; }
.type-FINE_NOTICE { background: #f8d7da; color: #721c24; }
.type-RESERVATION_AVAILABLE { background: #cce5ff; color: #004085; }
.type-RECOMMENDATION { background: #e2d5f1; color: #4a148c; }
.channel-icon { font-size: 1.2rem; margin-right: 5px; }
</style>
</head>
<body>
<nav th:replace="~{fragments/layout :: navbar('notifications')}"/>
<main class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>🔔 通知中心</h1>
<div>
<span class="badge bg-danger me-2" th:if="${unreadCount > 0}" th:text="${unreadCount} + ' 条未读'">0 条未读</span>
<a th:href="@{/notifications(userId=${userId})}" class="btn btn-outline-primary btn-sm">刷新</a>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="card mb-3">
<div class="card-header">通知渠道</div>
<div class="list-group list-group-flush">
<div class="list-group-item">📱 短信通知</div>
<div class="list-group-item">📧 邮件通知</div>
<div class="list-group-item">💬 微信推送</div>
<div class="list-group-item">🔔 站内消息</div>
<div class="list-group-item">📞 电话提醒</div>
</div>
</div>
<div class="card">
<div class="card-header">通知类型</div>
<div class="list-group list-group-flush small">
<div class="list-group-item d-flex justify-content-between">
<span>借阅成功</span>
<span class="notification-type type-BORROW_SUCCESS"></span>
</div>
<div class="list-group-item d-flex justify-content-between">
<span>归还提醒</span>
<span class="notification-type type-RETURN_REMINDER"></span>
</div>
<div class="list-group-item d-flex justify-content-between">
<span>逾期通知</span>
<span class="notification-type type-OVERDUE_NOTICE">⚠️</span>
</div>
<div class="list-group-item d-flex justify-content-between">
<span>预约到书</span>
<span class="notification-type type-RESERVATION_AVAILABLE">📚</span>
</div>
<div class="list-group-item d-flex justify-content-between">
<span>图书推荐</span>
<span class="notification-type type-RECOMMENDATION">🎯</span>
</div>
</div>
</div>
</div>
<div class="col-md-9">
<div th:if="${#lists.isEmpty(notifications)}" class="alert alert-info">
📭 暂无通知消息
</div>
<div th:each="notif : ${notifications}"
class="card notification-card"
th:classappend="${!notif.isRead()} ? 'unread'">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<span class="notification-type" th:classappend="'type-' + ${notif.type()}"
th:text="${notif.type().getDisplayName()}">类型</span>
<span class="channel-icon ms-2" th:switch="${notif.channel().name()}">
<span th:case="'SMS'">📱</span>
<span th:case="'EMAIL'">📧</span>
<span th:case="'WECHAT'">💬</span>
<span th:case="'IN_APP'">🔔</span>
<span th:case="'PHONE'">📞</span>
</span>
</div>
<small class="text-muted" th:text="${notif.sentAt()}">时间</small>
</div>
<h6 class="card-title mt-2" th:text="${notif.title()}">标题</h6>
<p class="card-text small text-muted" style="white-space: pre-line;"
th:text="${notif.content()}">内容</p>
</div>
</div>
</div>
</div>
</main>
<footer th:replace="~{fragments/layout :: footer}"/>
<th:block th:replace="~{fragments/layout :: scripts}"/>
</body>
</html>

@ -0,0 +1,147 @@
<!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 v1.5.0</title>
<th:block th:replace="~{fragments/layout :: styles}"/>
<style>
.reservation-card {
border-left: 4px solid #6c757d;
transition: all 0.3s;
}
.reservation-card.WAITING { border-left-color: #ffc107; }
.reservation-card.AVAILABLE { border-left-color: #28a745; }
.reservation-card.BORROWED { border-left-color: #17a2b8; }
.reservation-card.CANCELLED { border-left-color: #6c757d; }
.reservation-card.EXPIRED { border-left-color: #dc3545; }
.queue-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 50%;
background: #007bff;
color: white;
font-weight: bold;
}
</style>
</head>
<body>
<nav th:replace="~{fragments/layout :: navbar('reservations')}"/>
<main class="container mt-4">
<h1 class="mb-4">📅 预约管理</h1>
<div th:if="${param.success}" class="alert alert-success alert-dismissible fade show">
✅ 预约成功!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div th:if="${param.cancelled}" class="alert alert-info alert-dismissible fade show">
预约已取消
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<span>我的预约</span>
</div>
<div class="card-body">
<div th:if="${#lists.isEmpty(reservations)}" class="text-center text-muted py-5">
📭 暂无预约记录
<p class="mt-2">当图书被借出时,您可以预约等待</p>
</div>
<div th:each="res : ${reservations}"
class="card reservation-card mb-3"
th:classappend="${res.status().name()}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-center">
<span class="queue-badge me-3"
th:if="${res.status().name() == 'WAITING'}"
th:text="${res.queuePosition()}">1</span>
<div>
<h6 class="mb-1">图书ID: <span th:text="${res.bookId()}">ID</span></h6>
<span class="badge"
th:classappend="${res.status().name() == 'WAITING'} ? 'bg-warning' :
(${res.status().name() == 'AVAILABLE'} ? 'bg-success' :
(${res.status().name() == 'BORROWED'} ? 'bg-info' :
(${res.status().name() == 'EXPIRED'} ? 'bg-danger' : 'bg-secondary')))"
th:text="${res.status().getDisplayName()}">状态</span>
</div>
</div>
<div class="text-end">
<small class="text-muted d-block">预约时间</small>
<small th:text="${res.reserveDate()}">日期</small>
</div>
</div>
<div class="mt-3" th:if="${res.status().name() == 'AVAILABLE'}">
<div class="alert alert-success mb-2 py-2">
🎉 图书已到,请在 <strong th:text="${res.expireDate()}">日期</strong> 前来借阅!
</div>
</div>
<div class="mt-3 d-flex gap-2" th:if="${res.status().name() == 'WAITING' or res.status().name() == 'AVAILABLE'}">
<form th:action="@{/reservations/cancel/{id}(id=${res.id()})}" method="post" style="display: inline;">
<input type="hidden" name="userId" th:value="${userId}"/>
<button type="submit" class="btn btn-outline-danger btn-sm"
onclick="return confirm('确定取消预约吗?')">取消预约</button>
</form>
<a th:if="${res.status().name() == 'AVAILABLE'}"
th:href="@{/books}" class="btn btn-success btn-sm">去借阅</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<!-- 新建预约 -->
<div class="card mb-4">
<div class="card-header"> 新建预约</div>
<div class="card-body">
<form th:action="@{/reservations/add}" method="post">
<input type="hidden" name="userId" th:value="${userId}"/>
<div class="mb-3">
<label class="form-label">选择图书</label>
<select name="bookId" class="form-select" required>
<option value="">-- 选择已借出的图书 --</option>
<th:block th:each="book : ${books}">
<option th:if="${!book.isAvailable()}"
th:value="${book.id}"
th:text="${book.title} + ' - ' + ${book.author}">书名</option>
</th:block>
</select>
</div>
<button type="submit" class="btn btn-primary w-100">提交预约</button>
</form>
</div>
</div>
<!-- 预约说明 -->
<div class="card">
<div class="card-header">📌 预约说明</div>
<div class="card-body">
<ol class="ps-3 mb-0">
<li class="mb-2">仅可预约已被借出的图书</li>
<li class="mb-2">预约后进入等待队列</li>
<li class="mb-2">图书归还后,按队列顺序通知</li>
<li class="mb-2">收到通知后3天内须来借阅</li>
<li>逾期未借阅,预约自动取消</li>
</ol>
</div>
</div>
</div>
</div>
</main>
<footer th:replace="~{fragments/layout :: footer}"/>
<th:block th:replace="~{fragments/layout :: scripts}"/>
</body>
</html>

@ -463,4 +463,177 @@ public class SmartAIService implements AIService {
return sb.toString();
}
// ==================== v1.5.0 智能通知与分析 ====================
/**
*
*/
public String analyzeUserBehavior(String userId, List<Book> borrowedBooks) {
if (borrowedBooks.isEmpty()) {
return "📊 暂无借阅记录,无法分析";
}
// 分析偏好分类
Map<String, Long> categoryPreference = borrowedBooks.stream()
.collect(Collectors.groupingBy(Book::getCategory, Collectors.counting()));
String favoriteCategory = categoryPreference.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("综合");
StringBuilder sb = new StringBuilder();
sb.append(String.format("📊 **用户 %s 的阅读分析**\n\n", userId));
sb.append(String.format("📚 总借阅: %d 本\n", borrowedBooks.size()));
sb.append(String.format("❤️ 最爱分类: %s\n\n", favoriteCategory));
sb.append("**分类偏好分布**\n");
categoryPreference.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.forEach(e -> sb.append(String.format("• %s: %d本 (%.0f%%)\n",
e.getKey(), e.getValue(), e.getValue() * 100.0 / borrowedBooks.size())));
sb.append("\n**AI建议**\n");
sb.append(String.format("• 推荐继续探索「%s」类的深度书籍\n", favoriteCategory));
sb.append("• 也可以尝试相关领域拓展阅读\n");
return sb.toString();
}
/**
*
*/
public String predictOverdueRisk(String userId, int currentBorrowCount, int historyOverdueCount) {
double riskScore = 0;
String riskLevel;
String suggestion;
// 计算风险分数
if (historyOverdueCount > 5) riskScore += 40;
else if (historyOverdueCount > 2) riskScore += 20;
else if (historyOverdueCount > 0) riskScore += 10;
if (currentBorrowCount > 5) riskScore += 30;
else if (currentBorrowCount > 3) riskScore += 15;
// 确定风险等级
if (riskScore >= 50) {
riskLevel = "🔴 高风险";
suggestion = "建议优先处理当前借阅,设置归还提醒";
} else if (riskScore >= 25) {
riskLevel = "🟡 中风险";
suggestion = "注意控制借阅数量,按时归还";
} else {
riskLevel = "🟢 低风险";
suggestion = "保持良好的借阅习惯";
}
return String.format("""
📈 ****
: %s
: %s
: %.0f/100
: %d
: %d
💡 : %s
""", userId, riskLevel, riskScore, currentBorrowCount, historyOverdueCount, suggestion);
}
/**
*
*/
public String generateSmartNotification(String notificationType, Map<String, Object> context) {
return switch (notificationType) {
case "RETURN_REMINDER" -> generateReturnReminder(context);
case "RECOMMENDATION" -> generateRecommendationNotice(context);
case "OVERDUE_WARNING" -> generateOverdueWarning(context);
default -> "您有一条新通知,请查看详情。";
};
}
private String generateReturnReminder(Map<String, Object> context) {
String bookTitle = (String) context.getOrDefault("bookTitle", "图书");
int daysLeft = (int) context.getOrDefault("daysLeft", 3);
String urgency = daysLeft <= 1 ? "⚠️ 紧急" : "📅 提醒";
return String.format("""
%s
%s %d
%s
""", urgency, bookTitle, daysLeft,
daysLeft <= 1 ? "请尽快归还,避免产生罚款!" : "请合理安排时间阅读。");
}
private String generateRecommendationNotice(Map<String, Object> context) {
String category = (String) context.getOrDefault("category", "");
return String.format("""
📚
%s
AI
""", category.isEmpty() ? "" : category);
}
private String generateOverdueWarning(Map<String, Object> context) {
String bookTitle = (String) context.getOrDefault("bookTitle", "图书");
int overdueDays = (int) context.getOrDefault("overdueDays", 1);
double fine = (double) context.getOrDefault("fine", 0.5);
return String.format("""
🚨
%s %d
: %.2f
0.5
""", bookTitle, overdueDays, fine);
}
/**
*
*/
public String suggestNotificationTiming(String userId) {
// 模拟分析用户活跃时间
int hour = random.nextInt(24);
String timeSlot;
String reason;
if (hour >= 9 && hour < 12) {
timeSlot = "上午 9:00-12:00";
reason = "用户上午活跃度较高";
} else if (hour >= 14 && hour < 18) {
timeSlot = "下午 14:00-18:00";
reason = "用户下午在线时间长";
} else if (hour >= 19 && hour < 22) {
timeSlot = "晚间 19:00-22:00";
reason = "用户晚间阅读习惯良好";
} else {
timeSlot = "上午 10:00";
reason = "默认推送时段";
}
return String.format("""
****
: %s
: %s
: %s
💡
""", userId, timeSlot, reason);
}
}

@ -0,0 +1,363 @@
package com.smartlibrary.service;
import com.smartlibrary.database.DatabaseConnection;
import com.smartlibrary.model.Book;
import com.smartlibrary.model.Loan;
import java.sql.*;
import java.time.LocalDate;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* - v1.5.0
*
*/
public class LoanHistoryService {
private static final Logger LOGGER = Logger.getLogger(LoanHistoryService.class.getName());
private final DatabaseConnection dbConnection;
private final BookService bookService;
// 罚款率每天0.5元
private static final double DAILY_FINE_RATE = 0.5;
// 最大续借次数
private static final int MAX_RENEWALS = 1;
// 续借延长天数
private static final int RENEWAL_DAYS = 14;
public LoanHistoryService() {
this.dbConnection = DatabaseConnection.getInstance();
this.bookService = new BookService();
initializeHistoryTable();
}
private void initializeHistoryTable() {
// 借阅历史表
String historySql = """
CREATE TABLE IF NOT EXISTS loan_history (
id TEXT PRIMARY KEY,
loan_id TEXT NOT NULL,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
action TEXT NOT NULL,
action_date TEXT DEFAULT (datetime('now')),
details TEXT,
fine_amount REAL DEFAULT 0
)
""";
// 续借记录表
String renewalSql = """
CREATE TABLE IF NOT EXISTS renewals (
id TEXT PRIMARY KEY,
loan_id TEXT NOT NULL,
renewal_date TEXT,
new_due_date TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
""";
try {
Connection conn = dbConnection.getConnection();
try (Statement stmt = conn.createStatement()) {
stmt.execute(historySql);
stmt.execute(renewalSql);
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "初始化历史表失败: {0}", e.getMessage());
}
}
// ==================== 历史记录 ====================
/**
*
*/
public void recordAction(String loanId, String bookId, String userId,
LoanAction action, String details, double fineAmount) {
String id = "H" + System.currentTimeMillis();
String sql = """
INSERT INTO loan_history (id, loan_id, book_id, user_id, action, details, fine_amount)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, id);
pstmt.setString(2, loanId);
pstmt.setString(3, bookId);
pstmt.setString(4, userId);
pstmt.setString(5, action.name());
pstmt.setString(6, details);
pstmt.setDouble(7, fineAmount);
pstmt.executeUpdate();
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "记录操作失败: {0}", e.getMessage());
}
}
/**
*
*/
public List<HistoryRecord> getUserHistory(String userId) {
List<HistoryRecord> history = new ArrayList<>();
String sql = """
SELECT h.*, b.title as book_title
FROM loan_history h
LEFT JOIN books b ON h.book_id = b.id
WHERE h.user_id = ?
ORDER BY h.action_date DESC
""";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, userId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
history.add(new HistoryRecord(
rs.getString("id"),
rs.getString("loan_id"),
rs.getString("book_id"),
rs.getString("book_title"),
rs.getString("user_id"),
LoanAction.valueOf(rs.getString("action")),
rs.getString("action_date"),
rs.getString("details"),
rs.getDouble("fine_amount")
));
}
}
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "查询历史失败: {0}", e.getMessage());
}
return history;
}
/**
*
*/
public List<HistoryRecord> getBookHistory(String bookId) {
List<HistoryRecord> history = new ArrayList<>();
String sql = "SELECT * FROM loan_history WHERE book_id = ? ORDER BY action_date DESC";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, bookId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
history.add(mapResultSetToHistory(rs));
}
}
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "查询历史失败: {0}", e.getMessage());
}
return history;
}
// ==================== 续借功能 ====================
/**
*
*/
public RenewalResult renewLoan(String loanId) {
// 获取借阅记录
Loan loan = findLoanById(loanId);
if (loan == null) {
return new RenewalResult(false, "借阅记录不存在", null);
}
if (loan.isReturned()) {
return new RenewalResult(false, "图书已归还,无法续借", null);
}
// 检查续借次数
int renewalCount = getRenewalCount(loanId);
if (renewalCount >= MAX_RENEWALS) {
return new RenewalResult(false, "已达到最大续借次数(" + MAX_RENEWALS + "次)", null);
}
// 检查是否逾期
if (loan.isOverdue()) {
return new RenewalResult(false, "图书已逾期,请先归还并缴纳罚款", null);
}
// 检查是否有人预约
// TODO: 集成预约服务检查
// 执行续借
LocalDate newDueDate = loan.getDueDate().plusDays(RENEWAL_DAYS);
String renewalId = "RN" + System.currentTimeMillis();
try {
Connection conn = dbConnection.getConnection();
// 更新借阅记录的应还日期
String updateSql = "UPDATE loans SET due_date = ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(updateSql)) {
pstmt.setString(1, newDueDate.toString());
pstmt.setString(2, loanId);
pstmt.executeUpdate();
}
// 记录续借
String insertSql = "INSERT INTO renewals (id, loan_id, renewal_date, new_due_date) VALUES (?, ?, date('now'), ?)";
try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
pstmt.setString(1, renewalId);
pstmt.setString(2, loanId);
pstmt.setString(3, newDueDate.toString());
pstmt.executeUpdate();
}
// 记录历史
recordAction(loanId, loan.getBookId(), loan.getUserId(),
LoanAction.RENEWAL, "续借成功,新应还日期: " + newDueDate, 0);
LOGGER.info("✓ 续借成功: " + loanId + " -> " + newDueDate);
return new RenewalResult(true, "续借成功,新应还日期: " + newDueDate, newDueDate);
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "续借失败: {0}", e.getMessage());
return new RenewalResult(false, "续借失败: " + e.getMessage(), null);
}
}
/**
*
*/
public int getRenewalCount(String loanId) {
String sql = "SELECT COUNT(*) FROM renewals WHERE loan_id = ?";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, loanId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) return rs.getInt(1);
}
}
} catch (SQLException e) {
// ignore
}
return 0;
}
// ==================== 罚款功能 ====================
/**
*
*/
public double calculateFine(Loan loan) {
if (loan.isReturned() || !loan.isOverdue()) {
return 0;
}
return loan.calculateOverdueFine(DAILY_FINE_RATE);
}
/**
*
*/
public Map<String, Double> calculateAllFines(String userId) {
Map<String, Double> fines = new LinkedHashMap<>();
List<Loan> loans = bookService.findAllLoans();
for (Loan loan : loans) {
if (loan.getUserId().equals(userId) && loan.isOverdue() && !loan.isReturned()) {
double fine = calculateFine(loan);
if (fine > 0) {
fines.put(loan.getId(), fine);
}
}
}
return fines;
}
/**
*
*/
public double getTotalFine(String userId) {
return calculateAllFines(userId).values().stream()
.mapToDouble(Double::doubleValue)
.sum();
}
/**
*
*/
public boolean payFine(String loanId, double amount) {
Loan loan = findLoanById(loanId);
if (loan == null) return false;
// 记录缴费历史
recordAction(loanId, loan.getBookId(), loan.getUserId(),
LoanAction.FINE_PAID, "缴纳罚款: " + amount + "元", amount);
LOGGER.info("✓ 罚款缴纳: " + loanId + " -> " + amount + "元");
return true;
}
// ==================== 辅助方法 ====================
private Loan findLoanById(String loanId) {
return bookService.findAllLoans().stream()
.filter(l -> l.getId().equals(loanId))
.findFirst()
.orElse(null);
}
private HistoryRecord mapResultSetToHistory(ResultSet rs) throws SQLException {
return new HistoryRecord(
rs.getString("id"),
rs.getString("loan_id"),
rs.getString("book_id"),
null,
rs.getString("user_id"),
LoanAction.valueOf(rs.getString("action")),
rs.getString("action_date"),
rs.getString("details"),
rs.getDouble("fine_amount")
);
}
/**
*
*/
public enum LoanAction {
BORROW("借阅"),
RETURN("归还"),
RENEWAL("续借"),
OVERDUE("逾期"),
FINE_PAID("缴费"),
RESERVED("预约"),
CANCELLED("取消");
private final String displayName;
LoanAction(String displayName) { this.displayName = displayName; }
public String getDisplayName() { return displayName; }
}
/**
*
*/
public record HistoryRecord(
String id,
String loanId,
String bookId,
String bookTitle,
String userId,
LoanAction action,
String actionDate,
String details,
double fineAmount
) {}
/**
*
*/
public record RenewalResult(boolean success, String message, LocalDate newDueDate) {}
}

@ -0,0 +1,396 @@
package com.smartlibrary.service;
import com.smartlibrary.model.Book;
import com.smartlibrary.model.Loan;
import com.smartlibrary.model.User;
import com.smartlibrary.database.DatabaseConnection;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* - v1.5.0
*
*/
public class NotificationService {
private static final Logger LOGGER = Logger.getLogger(NotificationService.class.getName());
private final DatabaseConnection dbConnection;
// 通知渠道枚举
public enum Channel {
SMS("短信"),
EMAIL("邮件"),
WECHAT("微信"),
IN_APP("站内信"),
PHONE("电话");
private final String displayName;
Channel(String displayName) { this.displayName = displayName; }
public String getDisplayName() { return displayName; }
}
// 通知类型枚举
public enum NotificationType {
BORROW_SUCCESS("借阅成功"),
RETURN_REMINDER("归还提醒"),
OVERDUE_NOTICE("逾期通知"),
FINE_NOTICE("罚款通知"),
RESERVATION_AVAILABLE("预约到书"),
RESERVATION_EXPIRED("预约过期"),
RENEWAL_SUCCESS("续借成功"),
RENEWAL_REJECTED("续借失败"),
RECOMMENDATION("图书推荐"),
SYSTEM_NOTICE("系统通知");
private final String displayName;
NotificationType(String displayName) { this.displayName = displayName; }
public String getDisplayName() { return displayName; }
}
public NotificationService() {
this.dbConnection = DatabaseConnection.getInstance();
initializeNotificationTable();
}
private void initializeNotificationTable() {
String sql = """
CREATE TABLE IF NOT EXISTS notifications (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
channel TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
is_read INTEGER DEFAULT 0,
sent_at TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
""";
try {
Connection conn = dbConnection.getConnection();
try (Statement stmt = conn.createStatement()) {
stmt.execute(sql);
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "初始化通知表失败: {0}", e.getMessage());
}
}
// ==================== 发送通知 ====================
/**
*
*/
public boolean sendNotification(String userId, NotificationType type, String title,
String content, Channel... channels) {
boolean allSuccess = true;
for (Channel channel : channels) {
boolean success = sendSingleNotification(userId, type, channel, title, content);
if (!success) allSuccess = false;
}
return allSuccess;
}
/**
*
*/
private boolean sendSingleNotification(String userId, NotificationType type,
Channel channel, String title, String content) {
String id = "N" + System.currentTimeMillis() + new Random().nextInt(1000);
String sql = """
INSERT INTO notifications (id, user_id, type, channel, title, content, sent_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
""";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, id);
pstmt.setString(2, userId);
pstmt.setString(3, type.name());
pstmt.setString(4, channel.name());
pstmt.setString(5, title);
pstmt.setString(6, content);
pstmt.executeUpdate();
}
// 模拟发送到不同渠道
simulateSendToChannel(channel, userId, title, content);
LOGGER.info(String.format("✓ 通知已发送 [%s] -> %s: %s", channel.getDisplayName(), userId, title));
return true;
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "发送通知失败: {0}", e.getMessage());
return false;
}
}
/**
*
*/
private void simulateSendToChannel(Channel channel, String userId, String title, String content) {
switch (channel) {
case SMS -> System.out.println("📱 [短信] 发送到用户 " + userId + ": " + title);
case EMAIL -> System.out.println("📧 [邮件] 发送到用户 " + userId + ": " + title);
case WECHAT -> System.out.println("💬 [微信] 发送到用户 " + userId + ": " + title);
case IN_APP -> System.out.println("🔔 [站内信] 发送到用户 " + userId + ": " + title);
case PHONE -> System.out.println("📞 [电话] 通知用户 " + userId + ": " + title);
}
}
// ==================== 业务通知方法 ====================
/**
*
*/
public void notifyBorrowSuccess(User user, Book book, Loan loan) {
String title = "借阅成功通知";
String content = String.format("""
%s
%s
%s
%s
""",
user.getName(), book.getTitle(),
loan.getBorrowDate(), loan.getDueDate());
sendNotification(user.getId(), NotificationType.BORROW_SUCCESS, title, content,
Channel.IN_APP, Channel.EMAIL);
}
/**
* 3
*/
public void notifyReturnReminder(User user, Book book, Loan loan) {
String title = "图书归还提醒";
String content = String.format("""
%s
%s %s
"借阅管理"
""",
user.getName(), book.getTitle(), loan.getDueDate());
sendNotification(user.getId(), NotificationType.RETURN_REMINDER, title, content,
Channel.IN_APP, Channel.SMS, Channel.WECHAT);
}
/**
*
*/
public void notifyOverdue(User user, Book book, Loan loan, double fine) {
String title = "图书逾期通知";
String content = String.format("""
%s
%s
%s
%.2f
""",
user.getName(), book.getTitle(), loan.getDueDate(), fine);
sendNotification(user.getId(), NotificationType.OVERDUE_NOTICE, title, content,
Channel.IN_APP, Channel.SMS, Channel.EMAIL, Channel.PHONE);
}
/**
*
*/
public void notifyFine(User user, double fineAmount, String reason) {
String title = "罚款通知";
String content = String.format("""
%s
%.2f
%s
""",
user.getName(), fineAmount, reason);
sendNotification(user.getId(), NotificationType.FINE_NOTICE, title, content,
Channel.IN_APP, Channel.EMAIL);
}
/**
*
*/
public void notifyReservationAvailable(User user, Book book) {
String title = "预约图书已到馆";
String content = String.format("""
%s
%s
3
""",
user.getName(), book.getTitle());
sendNotification(user.getId(), NotificationType.RESERVATION_AVAILABLE, title, content,
Channel.IN_APP, Channel.SMS, Channel.WECHAT);
}
/**
*
*/
public void notifyRenewalSuccess(User user, Book book, Loan loan) {
String title = "续借成功通知";
String content = String.format("""
%s
%s
%s
1
""",
user.getName(), book.getTitle(), loan.getDueDate());
sendNotification(user.getId(), NotificationType.RENEWAL_SUCCESS, title, content,
Channel.IN_APP, Channel.EMAIL);
}
/**
* AI
*/
public void notifyRecommendation(User user, List<Book> recommendations) {
StringBuilder bookList = new StringBuilder();
for (int i = 0; i < Math.min(5, recommendations.size()); i++) {
bookList.append(String.format("%d. 《%s》- %s\n",
i + 1, recommendations.get(i).getTitle(), recommendations.get(i).getAuthor()));
}
String title = "为您推荐的图书";
String content = String.format("""
%s
AI
%s
""",
user.getName(), bookList.toString());
sendNotification(user.getId(), NotificationType.RECOMMENDATION, title, content,
Channel.IN_APP, Channel.EMAIL);
}
// ==================== 查询通知 ====================
/**
*
*/
public List<NotificationRecord> getUserNotifications(String userId) {
List<NotificationRecord> notifications = new ArrayList<>();
String sql = "SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, userId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
notifications.add(mapResultSetToNotification(rs));
}
}
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "查询通知失败: {0}", e.getMessage());
}
return notifications;
}
/**
*
*/
public int getUnreadCount(String userId) {
String sql = "SELECT COUNT(*) FROM notifications WHERE user_id = ? AND is_read = 0";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, userId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) return rs.getInt(1);
}
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "查询未读数量失败: {0}", e.getMessage());
}
return 0;
}
/**
*
*/
public boolean markAsRead(String notificationId) {
String sql = "UPDATE notifications SET is_read = 1 WHERE id = ?";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, notificationId);
return pstmt.executeUpdate() > 0;
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "标记已读失败: {0}", e.getMessage());
return false;
}
}
/**
*
*/
public boolean markAllAsRead(String userId) {
String sql = "UPDATE notifications SET is_read = 1 WHERE user_id = ?";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, userId);
return pstmt.executeUpdate() > 0;
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "标记全部已读失败: {0}", e.getMessage());
return false;
}
}
private NotificationRecord mapResultSetToNotification(ResultSet rs) throws SQLException {
return new NotificationRecord(
rs.getString("id"),
rs.getString("user_id"),
NotificationType.valueOf(rs.getString("type")),
Channel.valueOf(rs.getString("channel")),
rs.getString("title"),
rs.getString("content"),
rs.getInt("is_read") == 1,
rs.getString("sent_at"),
rs.getString("created_at")
);
}
/**
*
*/
public record NotificationRecord(
String id,
String userId,
NotificationType type,
Channel channel,
String title,
String content,
boolean isRead,
String sentAt,
String createdAt
) {}
}

@ -0,0 +1,358 @@
package com.smartlibrary.service;
import com.smartlibrary.database.DatabaseConnection;
import com.smartlibrary.model.Book;
import java.sql.*;
import java.time.LocalDate;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* - v1.5.0
*
*/
public class ReservationService {
private static final Logger LOGGER = Logger.getLogger(ReservationService.class.getName());
private final DatabaseConnection dbConnection;
private final BookService bookService;
// 预约状态
public enum ReservationStatus {
WAITING("等待中"),
AVAILABLE("可借阅"),
BORROWED("已借阅"),
CANCELLED("已取消"),
EXPIRED("已过期");
private final String displayName;
ReservationStatus(String displayName) { this.displayName = displayName; }
public String getDisplayName() { return displayName; }
}
public ReservationService() {
this.dbConnection = DatabaseConnection.getInstance();
this.bookService = new BookService();
initializeReservationTable();
}
private void initializeReservationTable() {
String sql = """
CREATE TABLE IF NOT EXISTS reservations (
id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
status TEXT DEFAULT 'WAITING',
queue_position INTEGER DEFAULT 0,
reserve_date TEXT,
available_date TEXT,
expire_date TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
""";
try {
Connection conn = dbConnection.getConnection();
try (Statement stmt = conn.createStatement()) {
stmt.execute(sql);
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "初始化预约表失败: {0}", e.getMessage());
}
}
/**
*
*/
public ReservationResult reserveBook(String bookId, String userId) {
// 检查图书是否存在
Book book = bookService.findBookById(bookId);
if (book == null) {
return new ReservationResult(false, "图书不存在", null);
}
// 检查是否已预约
if (hasReservation(bookId, userId)) {
return new ReservationResult(false, "您已预约此图书", null);
}
// 检查图书是否可借(如果可借则不需要预约)
if (book.isAvailable()) {
return new ReservationResult(false, "图书当前可借,无需预约", null);
}
// 获取当前队列位置
int position = getQueuePosition(bookId) + 1;
String id = "R" + System.currentTimeMillis();
String sql = """
INSERT INTO reservations (id, book_id, user_id, status, queue_position, reserve_date)
VALUES (?, ?, ?, 'WAITING', ?, date('now'))
""";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, id);
pstmt.setString(2, bookId);
pstmt.setString(3, userId);
pstmt.setInt(4, position);
pstmt.executeUpdate();
LOGGER.info("✓ 预约成功: " + userId + " -> " + book.getTitle() + " (位置: " + position + ")");
return new ReservationResult(true, "预约成功,当前排队位置: " + position,
new Reservation(id, bookId, userId, ReservationStatus.WAITING, position,
LocalDate.now(), null, null));
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "预约失败: {0}", e.getMessage());
return new ReservationResult(false, "预约失败: " + e.getMessage(), null);
}
}
/**
*
*/
public boolean cancelReservation(String reservationId) {
String sql = "UPDATE reservations SET status = 'CANCELLED' WHERE id = ? AND status = 'WAITING'";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, reservationId);
boolean success = pstmt.executeUpdate() > 0;
if (success) {
// 更新队列位置
updateQueuePositions(getBookIdByReservation(reservationId));
}
return success;
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "取消预约失败: {0}", e.getMessage());
return false;
}
}
/**
*
*/
public Reservation processReturnForReservation(String bookId) {
// 获取队列中第一个等待的预约
String sql = """
SELECT * FROM reservations
WHERE book_id = ? AND status = 'WAITING'
ORDER BY queue_position ASC LIMIT 1
""";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, bookId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
String reservationId = rs.getString("id");
// 更新状态为可借阅设置3天有效期
String updateSql = """
UPDATE reservations SET status = 'AVAILABLE',
available_date = date('now'),
expire_date = date('now', '+3 days')
WHERE id = ?
""";
try (PreparedStatement updateStmt = conn.prepareStatement(updateSql)) {
updateStmt.setString(1, reservationId);
updateStmt.executeUpdate();
}
return mapResultSetToReservation(rs);
}
}
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "处理预约队列失败: {0}", e.getMessage());
}
return null;
}
/**
*
*/
public List<Reservation> getUserReservations(String userId) {
List<Reservation> reservations = new ArrayList<>();
String sql = "SELECT * FROM reservations WHERE user_id = ? ORDER BY created_at DESC";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, userId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
reservations.add(mapResultSetToReservation(rs));
}
}
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "查询预约失败: {0}", e.getMessage());
}
return reservations;
}
/**
*
*/
public List<Reservation> getBookReservationQueue(String bookId) {
List<Reservation> queue = new ArrayList<>();
String sql = """
SELECT * FROM reservations
WHERE book_id = ? AND status IN ('WAITING', 'AVAILABLE')
ORDER BY queue_position ASC
""";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, bookId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
queue.add(mapResultSetToReservation(rs));
}
}
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "查询预约队列失败: {0}", e.getMessage());
}
return queue;
}
/**
*
*/
public int processExpiredReservations() {
String sql = """
UPDATE reservations SET status = 'EXPIRED'
WHERE status = 'AVAILABLE' AND expire_date < date('now')
""";
try {
Connection conn = dbConnection.getConnection();
try (Statement stmt = conn.createStatement()) {
int count = stmt.executeUpdate(sql);
if (count > 0) {
LOGGER.info("✓ 处理了 " + count + " 个过期预约");
}
return count;
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "处理过期预约失败: {0}", e.getMessage());
return 0;
}
}
// ==================== 辅助方法 ====================
private boolean hasReservation(String bookId, String userId) {
String sql = "SELECT COUNT(*) FROM reservations WHERE book_id = ? AND user_id = ? AND status IN ('WAITING', 'AVAILABLE')";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, bookId);
pstmt.setString(2, userId);
try (ResultSet rs = pstmt.executeQuery()) {
return rs.next() && rs.getInt(1) > 0;
}
}
} catch (SQLException e) {
return false;
}
}
private int getQueuePosition(String bookId) {
String sql = "SELECT MAX(queue_position) FROM reservations WHERE book_id = ? AND status = 'WAITING'";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, bookId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) return rs.getInt(1);
}
}
} catch (SQLException e) {
// ignore
}
return 0;
}
private void updateQueuePositions(String bookId) {
// 重新计算队列位置
String sql = """
UPDATE reservations SET queue_position = (
SELECT COUNT(*) FROM reservations r2
WHERE r2.book_id = reservations.book_id
AND r2.status = 'WAITING'
AND r2.created_at <= reservations.created_at
) WHERE book_id = ? AND status = 'WAITING'
""";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, bookId);
pstmt.executeUpdate();
}
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "更新队列位置失败: {0}", e.getMessage());
}
}
private String getBookIdByReservation(String reservationId) {
String sql = "SELECT book_id FROM reservations WHERE id = ?";
try {
Connection conn = dbConnection.getConnection();
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, reservationId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) return rs.getString("book_id");
}
}
} catch (SQLException e) {
// ignore
}
return null;
}
private Reservation mapResultSetToReservation(ResultSet rs) throws SQLException {
return new Reservation(
rs.getString("id"),
rs.getString("book_id"),
rs.getString("user_id"),
ReservationStatus.valueOf(rs.getString("status")),
rs.getInt("queue_position"),
parseDate(rs.getString("reserve_date")),
parseDate(rs.getString("available_date")),
parseDate(rs.getString("expire_date"))
);
}
private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isEmpty()) return null;
try {
return LocalDate.parse(dateStr.substring(0, 10));
} catch (Exception e) {
return null;
}
}
/**
*
*/
public record ReservationResult(boolean success, String message, Reservation reservation) {}
/**
*
*/
public record Reservation(
String id,
String bookId,
String userId,
ReservationStatus status,
int queuePosition,
LocalDate reserveDate,
LocalDate availableDate,
LocalDate expireDate
) {}
}
Loading…
Cancel
Save