|
|
@ -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; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|