BorrowReminderService.java
package org.darkroomlibrary.service.impl;
import org.darkroomlibrary.mapper.BorrowRecordMapper;
import org.darkroomlibrary.service.NotificationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import jakarta.annotation.Resource;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 还书到期提醒服务
* 每天 8:00 检查 3 天后到期的借阅,发送一次温柔提醒
*/
@Slf4j
@Component
public class BorrowReminderService {
private static final int REMINDER_BATCH_SIZE = 200;
@Resource
private BorrowRecordMapper borrowRecordMapper;
@Resource
private NotificationService notificationService;
@Resource
private PlatformTransactionManager transactionManager;
@Value("${borrow.renew.window-days-before-due:3}")
private int reminderDays;
@Scheduled(cron = "0 0 8 * * ?")
public void sendDueReminders() {
LocalDate today = LocalDate.now();
LocalDateTime startTime = today.atStartOfDay();
LocalDateTime endTime = today.plusDays(reminderDays + 1L).atStartOfDay();
int scanned = 0;
int afterId = 0;
while (true) {
List<Map<String, Object>> reminders = borrowRecordMapper.findDueReminders(
startTime, endTime, afterId, REMINDER_BATCH_SIZE);
if (reminders.isEmpty()) {
break;
}
int nextAfterId = afterId;
for (Map<String, Object> r : reminders) {
Integer recordId = toInteger(getValue(r, "recordId"));
if (recordId != null) {
nextAfterId = Math.max(nextAfterId, recordId);
}
String email = toString(getValue(r, "userEmail"));
String userName = toString(getValue(r, "userName"));
String bookName = toString(getValue(r, "bookName"));
LocalDateTime dueDate = toLocalDateTime(getValue(r, "dueDate"));
if (recordId == null || email == null || email.isEmpty()) {
continue;
}
try {
enqueueReminderOnce(recordId, email, userName, bookName, dueDate);
} catch (Exception e) {
log.warn("还书提醒入队失败: recordId={}, email={}, book={}, error={}",
recordId, email, bookName, e.getMessage());
}
}
scanned += reminders.size();
if (reminders.size() < REMINDER_BATCH_SIZE || nextAfterId <= afterId) {
break;
}
afterId = nextAfterId;
}
if (scanned > 0) {
log.info("已扫描 {} 条即将到期的借阅提醒", scanned);
}
}
private void enqueueReminderOnce(Integer recordId,
String email,
String userName,
String bookName,
LocalDateTime dueDate) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.executeWithoutResult(status -> {
if (borrowRecordMapper.markDueReminderSent(recordId, LocalDateTime.now()) == 0) {
return;
}
String dueDateText = dueDate == null ? "近期" : dueDate.toLocalDate().toString();
notificationService.enqueueEmail(
email,
"【暗室藏书】还书提醒",
userName + ",你好。\n\n"
+ "你借阅的《" + bookName + "》将于 " + dueDateText + " 到期。\n"
+ "到期前 " + reminderDays + " 天内可以续借,请及时处理。\n\n"
+ "—— 暗室藏书"
);
});
}
private Object getValue(Map<String, Object> row, String key) {
Object value = row.get(key);
if (value != null) {
return value;
}
for (Map.Entry<String, Object> entry : row.entrySet()) {
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue();
}
}
return null;
}
private String toString(Object value) {
return value == null ? null : String.valueOf(value);
}
private Integer toInteger(Object value) {
if (value instanceof Number) {
return ((Number) value).intValue();
}
if (value instanceof String) {
return Integer.valueOf((String) value);
}
return null;
}
private LocalDateTime toLocalDateTime(Object value) {
if (value instanceof LocalDateTime) {
return (LocalDateTime) value;
}
if (value instanceof Timestamp) {
return ((Timestamp) value).toLocalDateTime();
}
if (value instanceof LocalDate) {
return ((LocalDate) value).atStartOfDay();
}
return null;
}
}