From 2f2b39346fb1958759224e791101b1b8ae9f6558 Mon Sep 17 00:00:00 2001 From: wjl <3533384953@qq.com> Date: Tue, 29 Apr 2025 18:49:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8F=96=E4=BB=B6?= =?UTF-8?q?=E7=A0=81=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 4 +- .../com/controller/OverduePackageService.java | 235 ++++++++++++++ .../com/controller/PickupCodeService.java | 301 ++++++++++++++++++ 3 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/controller/OverduePackageService.java create mode 100644 src/main/java/com/controller/PickupCodeService.java 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