diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index fe74ca6..1569411 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -14,6 +14,8 @@
+
+
@@ -193,7 +195,7 @@
-
+
diff --git a/src/main/java/com/controller/OverduePackageService.java b/src/main/java/com/controller/OverduePackageService.java
new file mode 100644
index 0000000..01fcf63
--- /dev/null
+++ b/src/main/java/com/controller/OverduePackageService.java
@@ -0,0 +1,235 @@
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 滞留件管理服务类
+ * 核心功能:滞留件检测、通知管理、超时处理、统计分析
+ */
+public class OverduePackageService {
+ // 配置参数(可通过配置文件加载)
+ private static final int MAX_FREE_DAYS = 7; // 最大免费存放天数
+ private static final double DAILY_STORAGE_FEE = 1.0; // 每日滞留费用(元)
+ private static final int NOTICE_INTERVAL = 3; // 提醒间隔天数
+
+ // 依赖服务注入
+ private final ExpressService expressService;
+ private final NotificationService notificationService;
+
+ // 内部状态管理
+ private final Map overdueRecords = new HashMap<>();
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+
+ /**
+ * 构造函数(依赖注入模式)
+ */
+ public OverduePackageService(ExpressService expressService,
+ NotificationService notificationService) {
+ this.expressService = expressService;
+ this.notificationService = notificationService;
+ initializeOverdueMonitoring();
+ }
+
+ /**
+ * 初始化滞留件监控线程
+ */
+ private void initializeOverdueMonitoring() {
+ // 创建定时任务(每24小时执行一次检测)
+ Timer timer = new Timer(true);
+ timer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ detectAndProcessOverduePackages();
+ }
+ }, 0, 24 * 60 * 60 * 1000);
+ }
+
+ /**
+ * 滞留件检测与处理主逻辑
+ */
+ public synchronized void detectAndProcessOverduePackages() {
+ // 1. 获取所有待取件包裹
+ List allPackages = expressService.getAllPackages()
+ .stream()
+ .filter(item -> "待取件".equals(item.getStatus()))
+ .collect(Collectors.toList());
+
+ // 2. 计算存放天数并识别滞留件
+ List overduePackages = new ArrayList<>();
+ for (ExpressItem item : allPackages) {
+ int storageDays = calculateStorageDays(item.getArrivalTime());
+ item.setStorageDays(storageDays);
+
+ if (storageDays > MAX_FREE_DAYS) {
+ overduePackages.add(item);
+ recordOverdueHistory(item, storageDays);
+ }
+ }
+
+ // 3. 执行滞留处理流程
+ overduePackages.forEach(this::processOverduePackage);
+ }
+
+ /**
+ * 计算包裹存放天数(精确到小时)
+ */
+ private int calculateStorageDays(Date arrivalTime) {
+ long now = System.currentTimeMillis();
+ long elapsed = now - arrivalTime.getTime();
+ return (int) (elapsed / (24 * 3600 * 1000));
+ }
+
+ /**
+ * 记录滞留历史(防重复处理)
+ */
+ private void recordOverdueHistory(ExpressItem item, int currentDays) {
+ String trackingNumber = item.getTrackingNumber();
+ overdueRecords.putIfAbsent(trackingNumber, new OverdueRecord());
+
+ OverdueRecord record = overdueRecords.get(trackingNumber);
+ if (record.getLastNoticeDay() == 0 ||
+ (currentDays - record.getLastNoticeDay()) >= NOTICE_INTERVAL) {
+
+ record.setLastNoticeDay(currentDays);
+ record.incrementNoticeCount();
+ }
+ }
+
+ /**
+ * 单个滞留件处理流程
+ */
+ private void processOverduePackage(ExpressItem item) {
+ // 1. 费用计算
+ double overdueFee = calculateOverdueFee(item.getStorageDays());
+
+ // 2. 生成通知内容
+ String noticeContent = buildNoticeContent(item, overdueFee);
+
+ // 3. 发送通知(支持多通道)
+ sendMultiChannelNotice(item.getPhone(), noticeContent);
+
+ // 4. 标记为滞留状态(可选扩展)
+ if (item.getStorageDays() > MAX_FREE_DAYS * 2) {
+ expressService.markAsOverdue(item.getTrackingNumber());
+ }
+ }
+
+ /**
+ * 计算滞留费用(简单线性计费)
+ */
+ private double calculateOverdueFee(int storageDays) {
+ int overdueDays = storageDays - MAX_FREE_DAYS;
+ return overdueDays > 0 ? overdueDays * DAILY_STORAGE_FEE : 0;
+ }
+
+ /**
+ * 构建多格式通知内容
+ */
+ private String buildNoticeContent(ExpressItem item, double fee) {
+ return String.format("""
+ 【快递滞留提醒】
+ 运单号:%s
+ 收件人:%s
+ 滞留天数:%d天
+ 当前费用:%.2f元
+ 请尽快取件避免额外费用!
+ """,
+ item.getTrackingNumber(),
+ item.getRecipient(),
+ item.getStorageDays(),
+ fee);
+ }
+
+ /**
+ * 多通道通知发送(短信+邮件+系统消息)
+ */
+ private void sendMultiChannelNotice(String phone, String content) {
+ // 短信通知
+ notificationService.sendSMS(phone, content);
+
+ // 邮件通知(需实现邮件服务)
+ // notificationService.sendEmail(recipientEmail, "快递滞留提醒", content);
+
+ // 系统内消息(需集成消息中心)
+ // messageCenter.pushSystemNotice(userId, content);
+ }
+
+ /**
+ * 滞留件统计报表生成
+ */
+ public String generateOverdueReport() {
+ Map dailyCount = overdueRecords.values().stream()
+ .collect(Collectors.groupingBy(
+ OverdueRecord::getNoticeCount,
+ Collectors.counting()
+ ));
+
+ StringBuilder report = new StringBuilder();
+ report.append("===== 滞留件统计报表 =====\n");
+ report.append("统计时间:").append(dateFormat.format(new Date())).append("\n");
+ report.append("-------------------------\n");
+ report.append("总滞留包裹数:").append(overdueRecords.size()).append("\n");
+
+ dailyCount.forEach((times, count) ->
+ report.append(String.format("被提醒%d次包裹数:%d\n", times, count)));
+
+ return report.toString();
+ }
+
+ /**
+ * 滞留记录实体类
+ */
+ private static class OverdueRecord {
+ private int lastNoticeDay; // 最后一次通知时的存放天数
+ private int noticeCount; // 通知发送次数
+
+ // Getter/Setter
+ public int getLastNoticeDay() { return lastNoticeDay; }
+ public void setLastNoticeDay(int day) { this.lastNoticeDay = day; }
+ public int getNoticeCount() { return noticeCount; }
+ public void incrementNoticeCount() { this.noticeCount++; }
+ }
+
+ /**
+ * 通知服务接口(需具体实现)
+ */
+ public interface NotificationService {
+ void sendSMS(String phone, String message);
+ // void sendEmail(String to, String subject, String content);
+ }
+
+ /**
+ * 滞留件查询服务
+ */
+ public List queryOverduePackages(int minDays, int maxDays) {
+ return expressService.getAllPackages().stream()
+ .filter(item -> {
+ int days = item.getStorageDays();
+ return days >= minDays && days <= maxDays;
+ })
+ .sorted(Comparator.comparingInt(ExpressItem::getStorageDays).reversed())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 批量处理滞留件(自动退回)
+ */
+ public int batchReturnOverduePackages(int maxDays) {
+ List toReturn = queryOverduePackages(maxDays, Integer.MAX_VALUE);
+ toReturn.forEach(item -> expressService.markAsReturned(item.getTrackingNumber()));
+ return toReturn.size();
+ }
+
+ /**
+ * 滞留费用明细查询
+ */
+ public Map getOverdueFees(String trackingNumber) {
+ OverdueRecord record = overdueRecords.get(trackingNumber);
+ if (record == null) return Collections.emptyMap();
+
+ Map feeDetail = new HashMap<>();
+ feeDetail.put("baseFee", (double) (record.getNoticeCount() * NOTICE_INTERVAL));
+ feeDetail.put("totalFee", calculateOverdueFee(record.getNoticeCount() * NOTICE_INTERVAL));
+ return feeDetail;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/controller/PickupCodeService.java b/src/main/java/com/controller/PickupCodeService.java
new file mode 100644
index 0000000..8603ba6
--- /dev/null
+++ b/src/main/java/com/controller/PickupCodeService.java
@@ -0,0 +1,301 @@
+import java.security.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+/**
+ * 取件码管理服务类
+ * 核心功能:生成/验证/查询取件码、安全控制、过期处理、审计日志
+ */
+public class PickupCodeService {
+ // 配置参数(建议通过配置中心动态加载)
+ private static final int CODE_LENGTH = 6; // 取件码长度
+ private static final int MAX_ATTEMPTS = 3; // 最大尝试次数
+ private static final long EXPIRATION_TIME = 24 * 3600; // 有效期(秒)
+ private static final int CLEANUP_INTERVAL = 60 * 60; // 清理线程间隔(秒)
+
+ // 依赖服务注入
+ private final ExpressService expressService;
+ private final SecureRandom secureRandom;
+ private final ScheduledExecutorService cleanupScheduler;
+
+ // 核心数据存储(内存数据库,实际应使用Redis等持久化存储)
+ private final Map codeDatabase = new ConcurrentHashMap<>();
+ private final Map attemptCounter = new ConcurrentHashMap<>();
+
+ /**
+ * 构造函数(依赖注入模式)
+ */
+ public PickupCodeService(ExpressService expressService) {
+ this.expressService = expressService;
+ this.secureRandom = new SecureRandom();
+ this.cleanupScheduler = Executors.newSingleThreadScheduledExecutor();
+ initializeCleanupTask();
+ }
+
+ /**
+ * 初始化自动清理任务
+ */
+ private void initializeCleanupTask() {
+ cleanupScheduler.scheduleAtFixedRate(() -> {
+ long now = System.currentTimeMillis();
+ codeDatabase.entrySet().removeIf(entry ->
+ entry.getValue().getExpirationTime() < now);
+ }, CLEANUP_INTERVAL, CLEANUP_INTERVAL, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 生成取件码(核心算法)
+ * @param trackingNumber 关联运单号
+ * @return 生成的取件码对象
+ * @throws CodeGenerationException 当生成失败时抛出
+ */
+ public PickupCode generatePickupCode(String trackingNumber)
+ throws CodeGenerationException {
+ // 1. 参数验证
+ if (!expressService.packageExists(trackingNumber)) {
+ throw new CodeGenerationException("包裹不存在");
+ }
+
+ // 2. 生成唯一码(时间戳+随机数+包裹哈希)
+ String baseCode = String.format("%d%06d",
+ System.currentTimeMillis() % 1000000,
+ secureRandom.nextInt(999999));
+
+ String hashedCode = hashCode(baseCode + trackingNumber);
+ String formattedCode = String.format("%s-%s",
+ hashedCode.substring(0, 3),
+ hashedCode.substring(3, 6));
+
+ // 3. 创建记录对象
+ PickupCodeRecord record = new PickupCodeRecord(
+ formattedCode,
+ trackingNumber,
+ System.currentTimeMillis() + EXPIRATION_TIME * 1000
+ );
+
+ // 4. 防重存储(CAS操作保证原子性)
+ codeDatabase.putIfAbsent(formattedCode, record);
+ return new PickupCode(formattedCode, record.getExpirationTime());
+ }
+
+ /**
+ * 取件码验证(核心安全流程)
+ * @param inputCode 用户输入的取件码
+ * @param phone 用户手机号(二次验证)
+ * @return 验证结果对象
+ */
+ public ValidationResult validatePickupCode(String inputCode, String phone)
+ throws InvalidCodeException {
+ // 1. 格式预校验
+ if (!inputCode.matches("\\d{3}-\\d{3}")) {
+ throw new InvalidCodeException("格式错误");
+ }
+
+ // 2. 防暴力破解计数器
+ attemptCounter.merge(phone, 1, Integer::sum);
+ if (attemptCounter.get(phone) > MAX_ATTEMPTS) {
+ throw new InvalidCodeException("尝试次数过多,请1小时后重试");
+ }
+
+ // 3. 核心验证逻辑
+ PickupCodeRecord record = codeDatabase.get(inputCode);
+ if (record == null) {
+ throw new InvalidCodeException("无效取件码");
+ }
+
+ if (record.isExpired()) {
+ throw new InvalidCodeException("取件码已过期");
+ }
+
+ if (!record.getTrackingNumber().equals(
+ expressService.getPackageByPhone(phone).getTrackingNumber())) {
+
+ throw new InvalidCodeException("取件码与包裹不匹配");
+ }
+
+ // 4. 验证成功后清理计数器
+ attemptCounter.remove(phone);
+ return new ValidationResult(true, record.getTrackingNumber());
+ }
+
+ /**
+ * 取件码哈希算法(防逆向)
+ */
+ private String hashCode(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = md.digest(input.getBytes());
+ return bytesToHex(hashBytes).substring(0, CODE_LENGTH);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("哈希算法不可用", e);
+ }
+ }
+
+ /**
+ * 字节数组转十六进制字符串
+ */
+ private String bytesToHex(byte[] bytes) {
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : bytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ }
+
+ /**
+ * 查询取件码信息
+ */
+ public Optional queryCodeInfo(String code) {
+ PickupCodeRecord record = codeDatabase.get(code);
+ if (record == null) return Optional.empty();
+
+ ExpressItem item = expressService.getPackage(record.getTrackingNumber());
+ return Optional.of(new PickupCodeInfo(
+ code,
+ item.getRecipient(),
+ item.getPhone(),
+ new Date(record.getExpirationTime()),
+ record.isUsed()
+ ));
+ }
+
+ /**
+ * 标记取件码为已使用
+ */
+ public void markCodeAsUsed(String code) {
+ PickupCodeRecord record = codeDatabase.get(code);
+ if (record != null) {
+ record.setUsed(true);
+ }
+ }
+
+ /**
+ * 批量生成取件码(用于测试)
+ */
+ public List batchGenerateCodes(int quantity)
+ throws CodeGenerationException {
+ return IntStream.range(0, quantity)
+ .mapToObj(i -> {
+ try {
+ return generatePickupCode(generateTrackingNumber());
+ } catch (CodeGenerationException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 模拟生成测试运单号
+ */
+ private String generateTrackingNumber() {
+ return "TEST" + System.currentTimeMillis() % 1000000;
+ }
+
+ /**
+ * 取件码实体类
+ */
+ public static class PickupCode {
+ private final String code;
+ private final long expirationTime;
+
+ PickupCode(String code, long expirationTime) {
+ this.code = code;
+ this.expirationTime = expirationTime;
+ }
+
+ // Getter方法
+ public String getCode() { return code; }
+ public long getExpirationTime() { return expirationTime; }
+ }
+
+ /**
+ * 验证结果封装类
+ */
+ public static class ValidationResult {
+ private final boolean valid;
+ private final String trackingNumber;
+
+ ValidationResult(boolean valid, String trackingNumber) {
+ this.valid = valid;
+ this.trackingNumber = trackingNumber;
+ }
+
+ // Getter方法
+ public boolean isValid() { return valid; }
+ public String getTrackingNumber() { return trackingNumber; }
+ }
+
+ /**
+ * 取件码信息查询结果类
+ */
+ public static class PickupCodeInfo {
+ private final String code;
+ private final String recipient;
+ private final String phone;
+ private final Date expiration;
+ private final boolean used;
+
+ PickupCodeInfo(String code, String recipient, String phone,
+ Date expiration, boolean used) {
+ this.code = code;
+ this.recipient = recipient;
+ this.phone = phone;
+ this.expiration = expiration;
+ this.used = used;
+ }
+
+ // Getter方法
+ public String getCode() { return code; }
+ public String getRecipient() { return recipient; }
+ public String getPhone() { return phone; }
+ public Date getExpiration() { return expiration; }
+ public boolean isUsed() { return used; }
+ }
+
+ /**
+ * 自定义异常类
+ */
+ public static class CodeGenerationException extends Exception {
+ public CodeGenerationException(String message) {
+ super(message);
+ }
+ }
+
+ public static class InvalidCodeException extends Exception {
+ public InvalidCodeException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * 取件码记录实体(内存存储)
+ */
+ private static class PickupCodeRecord {
+ private final String code;
+ private final String trackingNumber;
+ private final long expirationTime;
+ private boolean used = false;
+
+ PickupCodeRecord(String code, String trackingNumber, long expirationTime) {
+ this.code = code;
+ this.trackingNumber = trackingNumber;
+ this.expirationTime = expirationTime;
+ }
+
+ // Getter/Setter
+ public String getCode() { return code; }
+ public String getTrackingNumber() { return trackingNumber; }
+ public long getExpirationTime() { return expirationTime; }
+ public boolean isExpired() {
+ return System.currentTimeMillis() > expirationTime;
+ }
+ public boolean isUsed() { return used; }
+ public void setUsed(boolean used) { this.used = used; }
+ }
+}
\ No newline at end of file