feat: 实现取件码管理功能

master
wjl 4 months ago
parent 2746814257
commit 2f2b39346f

@ -14,6 +14,8 @@
<change afterPath="$PROJECT_DIR$/src/main/java/com/controller/ExpressItem.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/controller/ExpressIteml.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/controller/IgnoreAuth.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/controller/OverduePackageService.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/controller/PickupCodeService.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/controller/UserManagementService.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/model/enums/TypeEnum.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
@ -193,7 +195,7 @@
<workItem from="1744280672775" duration="39000" />
<workItem from="1744280743010" duration="312000" />
<workItem from="1745840073782" duration="4327000" />
<workItem from="1745911770182" duration="8739000" />
<workItem from="1745911770182" duration="9658000" />
</task>
<servers />
</component>

@ -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<String, OverdueRecord> 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<ExpressItem> allPackages = expressService.getAllPackages()
.stream()
.filter(item -> "待取件".equals(item.getStatus()))
.collect(Collectors.toList());
// 2. 计算存放天数并识别滞留件
List<ExpressItem> 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<Integer, Long> 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<ExpressItem> 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<ExpressItem> toReturn = queryOverduePackages(maxDays, Integer.MAX_VALUE);
toReturn.forEach(item -> expressService.markAsReturned(item.getTrackingNumber()));
return toReturn.size();
}
/**
*
*/
public Map<String, Double> getOverdueFees(String trackingNumber) {
OverdueRecord record = overdueRecords.get(trackingNumber);
if (record == null) return Collections.emptyMap();
Map<String, Double> feeDetail = new HashMap<>();
feeDetail.put("baseFee", (double) (record.getNoticeCount() * NOTICE_INTERVAL));
feeDetail.put("totalFee", calculateOverdueFee(record.getNoticeCount() * NOTICE_INTERVAL));
return feeDetail;
}
}

@ -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<String, PickupCodeRecord> codeDatabase = new ConcurrentHashMap<>();
private final Map<String, Integer> 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<PickupCodeInfo> 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<PickupCode> 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; }
}
}
Loading…
Cancel
Save