master
parent
014187cf99
commit
dbf2112ca0
31 changed files with 960 additions and 295 deletions
@ -0,0 +1,42 @@ |
||||
package com.nov.KgLowDurable.config; |
||||
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.stereotype.Component; |
||||
/** |
||||
* @author: liweidong |
||||
* @create: 2025-12-31 |
||||
*/ |
||||
@Component |
||||
public class FileUploadConfig { |
||||
|
||||
// 本地服务器地址
|
||||
@Value("${file.upload.server:http://10.90.100.231:8132}") |
||||
private String serverUrl; |
||||
|
||||
// 基础存储路径
|
||||
@Value("${file.upload.base-path:/opt/SmartParkFilesTest}") |
||||
private String basePath; |
||||
|
||||
// 允许的文件类型
|
||||
@Value("${file.upload.allowed-types:jpg,jpeg,png,gif,bmp}") |
||||
private String allowedTypes; |
||||
|
||||
// 最大文件大小(MB)
|
||||
@Value("${file.upload.max-size:10}") |
||||
private long maxFileSize; |
||||
|
||||
public String getServerUrl() { |
||||
return serverUrl; |
||||
} |
||||
|
||||
public String getBasePath() { |
||||
return basePath; |
||||
} |
||||
|
||||
public String[] getAllowedTypes() { |
||||
return allowedTypes.split(","); |
||||
} |
||||
|
||||
public long getMaxFileSize() { |
||||
return maxFileSize * 1024 * 1024; // 转换为字节
|
||||
} |
||||
} |
||||
@ -1,31 +1,28 @@ |
||||
//package com.nov.KgLowDurable.config;
|
||||
//import org.springframework.context.annotation.Bean;
|
||||
//import org.springframework.context.annotation.Configuration;
|
||||
//import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
//import org.springframework.data.redis.core.RedisTemplate;
|
||||
//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
//import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
//
|
||||
///**
|
||||
// * @author: liweidong
|
||||
// * @create: 2025-12-17
|
||||
// */
|
||||
//@Configuration
|
||||
//public class RedisConfig {
|
||||
// @Bean
|
||||
// public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
// RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
// template.setConnectionFactory(connectionFactory);
|
||||
//
|
||||
// // 使用 String 序列化器作为 key 的序列化器
|
||||
// template.setKeySerializer(new StringRedisSerializer());
|
||||
// template.setHashKeySerializer(new StringRedisSerializer());
|
||||
//
|
||||
// // 使用 JSON 序列化器作为 value 的序列化器(可以存储复杂对象)
|
||||
// template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
// template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
//
|
||||
// template.afterPropertiesSet();
|
||||
// return template;
|
||||
// }
|
||||
//}
|
||||
package com.nov.KgLowDurable.config; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.data.redis.serializer.StringRedisSerializer; |
||||
/** |
||||
* @author: liweidong |
||||
* @create: 2025-12-17 |
||||
*/ |
||||
@Configuration |
||||
public class RedisConfig { |
||||
|
||||
@Bean |
||||
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { |
||||
RedisTemplate<String, String> template = new RedisTemplate<>(); |
||||
template.setConnectionFactory(factory); |
||||
|
||||
// 使用String序列化
|
||||
template.setKeySerializer(new StringRedisSerializer()); |
||||
template.setValueSerializer(new StringRedisSerializer()); |
||||
template.setHashKeySerializer(new StringRedisSerializer()); |
||||
template.setHashValueSerializer(new StringRedisSerializer()); |
||||
|
||||
template.afterPropertiesSet(); |
||||
return template; |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,48 @@ |
||||
package com.nov.KgLowDurable.pojo.dto; |
||||
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
import lombok.Data; |
||||
import lombok.EqualsAndHashCode; |
||||
import lombok.experimental.Accessors; |
||||
import java.io.Serializable; |
||||
import java.math.BigDecimal; |
||||
import java.util.Date; |
||||
|
||||
/** |
||||
* 低值耐用品二级库库存表VO |
||||
* @author: liweidong |
||||
* @create: 2025-12-24 |
||||
*/ |
||||
@Data |
||||
@EqualsAndHashCode(callSuper = false) |
||||
@Accessors(chain = true) |
||||
public class LdDurableFormDto implements Serializable { |
||||
|
||||
/** |
||||
* 耐用品Id |
||||
*/ |
||||
private Long durableFormId; |
||||
|
||||
/** |
||||
* 数量 |
||||
*/ |
||||
private BigDecimal num; |
||||
|
||||
/** |
||||
* 归还时间 |
||||
*/ |
||||
@JsonFormat(pattern = "yyyy-MM-dd") |
||||
private Date returnTime; |
||||
|
||||
/** |
||||
* 归还理由 |
||||
*/ |
||||
private String returnReason; |
||||
|
||||
/** |
||||
* 1.归还 2.报废 |
||||
*/ |
||||
private String type; |
||||
|
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,224 @@ |
||||
package com.nov.KgLowDurable.util; |
||||
import com.nov.KgLowDurable.config.FileUploadConfig; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.Paths; |
||||
import java.text.SimpleDateFormat; |
||||
import java.util.Date; |
||||
import java.util.UUID; |
||||
/** |
||||
* @author: liweidong |
||||
* @create: 2025-12-31 |
||||
*/ |
||||
@Component |
||||
public class ImageUploadUtil { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ImageUploadUtil.class); |
||||
|
||||
@Autowired |
||||
private FileUploadConfig config; |
||||
|
||||
/** |
||||
* 上传图片文件 |
||||
* @param file 上传的文件 |
||||
* @param module 业务模块名称 |
||||
* @return 上传后的文件访问URL |
||||
*/ |
||||
public String uploadImage(MultipartFile file, String module) throws IOException { |
||||
// 1. 参数校验
|
||||
validateFile(file); |
||||
|
||||
// 2. 验证文件类型
|
||||
validateFileType(file); |
||||
|
||||
// 3. 生成存储路径
|
||||
String storagePath = generateStoragePath(module); |
||||
|
||||
// 4. 生成文件名
|
||||
String fileName = generateFileName(file.getOriginalFilename()); |
||||
|
||||
// 5. 创建目录(如果不存在)
|
||||
createDirectoryIfNotExists(storagePath); |
||||
|
||||
// 6. 保存文件
|
||||
String fullPath = saveFile(file, storagePath, fileName); |
||||
|
||||
// 7. 生成访问URL
|
||||
return generateAccessUrl(module, fileName); |
||||
} |
||||
|
||||
/** |
||||
* 批量上传图片 |
||||
* @param files 文件数组 |
||||
* @param module 业务模块 |
||||
* @return 上传成功的文件URL数组 |
||||
*/ |
||||
public String[] uploadImages(MultipartFile[] files, String module) throws IOException { |
||||
if (files == null || files.length == 0) { |
||||
throw new IllegalArgumentException("上传文件不能为空"); |
||||
} |
||||
|
||||
String[] urls = new String[files.length]; |
||||
for (int i = 0; i < files.length; i++) { |
||||
urls[i] = uploadImage(files[i], module); |
||||
} |
||||
return urls; |
||||
} |
||||
|
||||
/** |
||||
* 删除文件 |
||||
* @param fileUrl 文件URL |
||||
* @return 是否删除成功 |
||||
*/ |
||||
public boolean deleteFile(String fileUrl) { |
||||
try { |
||||
// 从URL中提取文件路径
|
||||
String relativePath = extractRelativePathFromUrl(fileUrl); |
||||
String fullPath = config.getBasePath() + relativePath; |
||||
|
||||
File file = new File(fullPath); |
||||
if (file.exists()) { |
||||
return file.delete(); |
||||
} |
||||
return false; |
||||
} catch (Exception e) { |
||||
logger.error("删除文件失败: {}", fileUrl, e); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取文件的本地存储路径 |
||||
* @param fileUrl 文件URL |
||||
* @return 本地文件路径 |
||||
*/ |
||||
public String getLocalFilePath(String fileUrl) { |
||||
String relativePath = extractRelativePathFromUrl(fileUrl); |
||||
return config.getBasePath() + relativePath; |
||||
} |
||||
|
||||
|
||||
private void validateFile(MultipartFile file) { |
||||
if (file == null || file.isEmpty()) { |
||||
throw new IllegalArgumentException("上传文件不能为空"); |
||||
} |
||||
|
||||
if (file.getSize() > config.getMaxFileSize()) { |
||||
throw new IllegalArgumentException("文件大小不能超过 " + (config.getMaxFileSize() / 1024 / 1024) + "MB"); |
||||
} |
||||
} |
||||
|
||||
private void validateFileType(MultipartFile file) throws IOException { |
||||
String originalFilename = file.getOriginalFilename(); |
||||
if (originalFilename == null) { |
||||
throw new IllegalArgumentException("文件名不能为空"); |
||||
} |
||||
|
||||
String fileExtension = getFileExtension(originalFilename).toLowerCase(); |
||||
boolean allowed = false; |
||||
|
||||
for (String allowedType : config.getAllowedTypes()) { |
||||
if (allowedType.trim().toLowerCase().equals(fileExtension)) { |
||||
allowed = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (!allowed) { |
||||
throw new IllegalArgumentException("不支持的文件类型,仅支持: " + |
||||
String.join(", ", config.getAllowedTypes())); |
||||
} |
||||
|
||||
// 通过MIME类型再次验证
|
||||
String contentType = file.getContentType(); |
||||
if (contentType == null || !contentType.startsWith("image/")) { |
||||
throw new IllegalArgumentException("请上传图片文件"); |
||||
} |
||||
} |
||||
|
||||
private String generateStoragePath(String module) { |
||||
// 按天创建文件夹:yyyy-MM-dd
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); |
||||
String dateFolder = dateFormat.format(new Date()); |
||||
|
||||
// 生成完整路径:/opt/SmartParkFilesTest/模块名/yyyy-MM-dd/
|
||||
return config.getBasePath() + File.separator + |
||||
module + File.separator + |
||||
dateFolder + File.separator; |
||||
} |
||||
|
||||
private String generateFileName(String originalFilename) { |
||||
// 生成UUID作为文件名,避免重复
|
||||
String uuid = UUID.randomUUID().toString().replace("-", ""); |
||||
|
||||
// 保留原文件扩展名
|
||||
String extension = getFileExtension(originalFilename); |
||||
|
||||
return uuid + "." + extension; |
||||
} |
||||
|
||||
private void createDirectoryIfNotExists(String path) { |
||||
File directory = new File(path); |
||||
if (!directory.exists()) { |
||||
boolean created = directory.mkdirs(); |
||||
if (!created) { |
||||
throw new RuntimeException("创建目录失败: " + path); |
||||
} |
||||
logger.info("创建目录: {}", path); |
||||
} |
||||
} |
||||
|
||||
private String saveFile(MultipartFile file, String storagePath, String fileName) throws IOException { |
||||
String fullPath = storagePath + fileName; |
||||
Path path = Paths.get(fullPath); |
||||
|
||||
// 保存文件
|
||||
Files.copy(file.getInputStream(), path); |
||||
|
||||
logger.info("文件保存成功: {}", fullPath); |
||||
return fullPath; |
||||
} |
||||
|
||||
private String generateAccessUrl(String module, String fileName) { |
||||
// 按天创建文件夹:yyyy-MM-dd
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); |
||||
String dateFolder = dateFormat.format(new Date()); |
||||
|
||||
// 生成访问URL:http://10.90.100.231:8132/opt/SmartParkFilesTest/模块名/yyyy-MM-dd/文件名
|
||||
return String.format("%s/%s/%s/%s", |
||||
config.getServerUrl().endsWith("/") ? |
||||
config.getServerUrl().substring(0, config.getServerUrl().length() - 1) : |
||||
config.getServerUrl(), |
||||
module, |
||||
dateFolder, |
||||
fileName); |
||||
} |
||||
|
||||
private String getFileExtension(String filename) { |
||||
int lastDotIndex = filename.lastIndexOf("."); |
||||
if (lastDotIndex == -1) { |
||||
throw new IllegalArgumentException("文件缺少扩展名"); |
||||
} |
||||
return filename.substring(lastDotIndex + 1); |
||||
} |
||||
|
||||
private String extractRelativePathFromUrl(String fileUrl) { |
||||
// 从URL中提取相对路径
|
||||
// 例如: http://10.90.100.231:8132/opt/SmartParkFilesTest/user/2025-12-31/uuid.jpg
|
||||
// 提取为: /user/2023-10-25/uuid.jpg
|
||||
|
||||
String baseUrl = config.getServerUrl() + config.getBasePath(); |
||||
if (!fileUrl.startsWith(baseUrl)) { |
||||
throw new IllegalArgumentException("无效的文件URL"); |
||||
} |
||||
|
||||
return fileUrl.substring(baseUrl.length()); |
||||
} |
||||
} |
||||
@ -1,159 +0,0 @@ |
||||
package com.nov.KgLowDurable.util; |
||||
import com.nov.KgLowDurable.mapper.LdOnePutStorageDetailMapper; |
||||
import com.nov.KgLowDurable.mapper.LdOnePutStorageMapper; |
||||
import com.nov.KgLowDurable.service.ILdOneFormService; |
||||
import com.nov.KgLowDurable.service.ILdOnePutStorageService; |
||||
import com.nov.KgLowDurable.service.Impl.LdOnePutStorageServiceImpl; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
import java.text.SimpleDateFormat; |
||||
import java.util.*; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.concurrent.atomic.AtomicInteger; |
||||
import java.text.SimpleDateFormat; |
||||
import java.util.Date; |
||||
|
||||
/** |
||||
* 单号生成方法 |
||||
* 传入前缀:FI, FO, SI, SO, FID |
||||
* 返回:前缀 + yyyyMMdd + 3位序号 |
||||
* @author liweidong |
||||
*/ |
||||
@Component |
||||
public class OrderNoGen { |
||||
|
||||
@Autowired |
||||
private LdOnePutStorageMapper onePutStorageMapper; |
||||
|
||||
@Autowired |
||||
private LdOnePutStorageDetailMapper onePutStorageDetailMapper; |
||||
|
||||
// 添加静态变量保存状态
|
||||
private static String lastPrefix = null; |
||||
private static String lastDate = null; |
||||
private static int currentSeq = 0; |
||||
private static boolean initialized = false; |
||||
|
||||
// 私有构造,防止直接new
|
||||
private OrderNoGen() {} |
||||
|
||||
/** |
||||
* 生成单号 |
||||
*/ |
||||
public synchronized String generate(String prefix) { |
||||
validatePrefix(prefix); |
||||
|
||||
// 获取当天日期
|
||||
String today = new SimpleDateFormat("yyyyMMdd").format(new Date()); |
||||
|
||||
// 如果是新的一天或新类型,需要重新初始化
|
||||
if (!today.equals(lastDate) || !prefix.equals(lastPrefix)) { |
||||
// 重置状态
|
||||
lastPrefix = prefix; |
||||
lastDate = today; |
||||
currentSeq = getTodayMaxSeqFromDB(prefix, today); |
||||
initialized = true; |
||||
} else if (!initialized) { |
||||
// 第一次调用,需要初始化
|
||||
currentSeq = getTodayMaxSeqFromDB(prefix, today); |
||||
initialized = true; |
||||
} |
||||
|
||||
// 递增序号
|
||||
currentSeq++; |
||||
if (currentSeq > 999) { |
||||
throw new RuntimeException("今天" + prefix + "类型的单号已超过999个"); |
||||
} |
||||
|
||||
return String.format("%s%s%03d", prefix, today, currentSeq); |
||||
} |
||||
|
||||
/** |
||||
* 重置状态(可选,比如在每天开始时调用) |
||||
*/ |
||||
public synchronized void reset() { |
||||
lastPrefix = null; |
||||
lastDate = null; |
||||
currentSeq = 0; |
||||
initialized = false; |
||||
} |
||||
|
||||
/** |
||||
* 批量生成 |
||||
*/ |
||||
public synchronized List<String> generateBatch(String prefix, int count) { |
||||
validatePrefix(prefix); |
||||
String today = new SimpleDateFormat("yyyyMMdd").format(new Date()); |
||||
|
||||
// 获取当前最大序号
|
||||
int maxSeq; |
||||
if (today.equals(lastDate) && prefix.equals(lastPrefix) && initialized) { |
||||
maxSeq = currentSeq; |
||||
} else { |
||||
maxSeq = getTodayMaxSeqFromDB(prefix, today); |
||||
lastPrefix = prefix; |
||||
lastDate = today; |
||||
currentSeq = maxSeq; |
||||
initialized = true; |
||||
} |
||||
|
||||
// 检查是否超限
|
||||
if (maxSeq + count > 999) { |
||||
throw new RuntimeException("今天" + prefix + "类型的单号已超过999个"); |
||||
} |
||||
|
||||
// 生成批量单号
|
||||
List<String> orderNos = new ArrayList<>(); |
||||
for (int i = 1; i <= count; i++) { |
||||
int seq = maxSeq + i; |
||||
orderNos.add(String.format("%s%s%03d", prefix, today, seq)); |
||||
} |
||||
|
||||
// 更新当前序号
|
||||
currentSeq = maxSeq + count; |
||||
|
||||
return orderNos; |
||||
} |
||||
|
||||
/** |
||||
* 从数据库查询当天最大序号 |
||||
*/ |
||||
private int getTodayMaxSeqFromDB(String prefix, String date) { |
||||
try { |
||||
// 查询数据库中该类型和日期的最大单号
|
||||
String maxOrderNo = null; |
||||
if("FI".equals(prefix)){ |
||||
maxOrderNo = onePutStorageMapper.selectMaxOrderNo(prefix, date); |
||||
} |
||||
if("FID".equals(prefix)){ |
||||
maxOrderNo = onePutStorageDetailMapper.selectMaxOrderNo(prefix, date); |
||||
} |
||||
|
||||
if (maxOrderNo != null && maxOrderNo.length() == 13) { |
||||
String seqStr = maxOrderNo.substring(10); |
||||
return Integer.parseInt(seqStr); |
||||
} |
||||
|
||||
return 0; |
||||
|
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
return 0; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 校验前缀是否合法 |
||||
*/ |
||||
private void validatePrefix(String prefix) { |
||||
if (prefix == null) { |
||||
throw new IllegalArgumentException("前缀不能为null"); |
||||
} |
||||
// 可随时增加
|
||||
if (!prefix.matches("^(FI|FID|FO|SI|SO|SOD)$")) { |
||||
throw new IllegalArgumentException( |
||||
"前缀必须为以下值之一:FI(一级库入库),FID(一级入库明细), FO(一级库出库), SI(二级库入库), SO(二级库出库)" |
||||
); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,313 @@ |
||||
package com.nov.KgLowDurable.util; |
||||
import org.springframework.dao.DataAccessException; |
||||
import org.springframework.data.redis.connection.RedisConnection; |
||||
import org.springframework.data.redis.core.RedisCallback; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.data.redis.core.script.DefaultRedisScript; |
||||
import org.springframework.stereotype.Component; |
||||
import org.springframework.util.StringUtils; |
||||
import javax.annotation.PostConstruct; |
||||
import java.time.LocalDate; |
||||
import java.time.LocalDateTime; |
||||
import java.time.ZoneId; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.*; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.data.redis.core.script.DefaultRedisScript; |
||||
import org.springframework.stereotype.Component; |
||||
import org.springframework.util.StringUtils; |
||||
import javax.annotation.PostConstruct; |
||||
import java.time.LocalDate; |
||||
import java.time.ZoneId; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.*; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.StringRedisTemplate; |
||||
import org.springframework.stereotype.Component; |
||||
import java.time.LocalDate; |
||||
import java.time.ZoneId; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* 精简版单号生成工具类 |
||||
* 功能:前缀 + yyyyMMdd + 3位序号(001-999) |
||||
* 存储于Redis,每天从001重新开始 |
||||
*/ |
||||
@Component |
||||
public class SerialNumberUtil { |
||||
|
||||
@Autowired |
||||
private StringRedisTemplate redisTemplate; |
||||
|
||||
// Redis键前缀
|
||||
private static final String REDIS_KEY_PREFIX = "SN"; |
||||
// 日期格式
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); |
||||
// 最大序列号
|
||||
private static final int MAX_SEQUENCE = 999; |
||||
// 默认前缀集合
|
||||
private static final String[] DEFAULT_PREFIXES = {"FI","FID","FO","FOD","SI","SID", "SO", "SOD"}; |
||||
|
||||
/** |
||||
* 生成单个单号 |
||||
* @param prefix 前缀(如:FI, FO, SI, SO, FID) |
||||
* @return 完整单号(如:FI20251231001) |
||||
*/ |
||||
public String generateSimple(String prefix) { |
||||
// 验证前缀
|
||||
validatePrefix(prefix); |
||||
|
||||
// 获取当前日期
|
||||
String dateStr = getCurrentDate(); |
||||
|
||||
// 构建Redis键
|
||||
String redisKey = buildRedisKey(prefix, dateStr); |
||||
|
||||
try { |
||||
// 使用Redis的原子递增操作
|
||||
Long sequence = redisTemplate.opsForValue().increment(redisKey); |
||||
|
||||
// 如果是第一次设置,设置过期时间(到明天凌晨)
|
||||
if (sequence == 1) { |
||||
setExpireAtMidnight(redisKey); |
||||
} |
||||
|
||||
// 检查是否超过最大值
|
||||
if (sequence > MAX_SEQUENCE) { |
||||
// 递减回去
|
||||
redisTemplate.opsForValue().decrement(redisKey); |
||||
throw new RuntimeException(String.format( |
||||
"序列号已用完,前缀:%s,日期:%s,最大:%d", |
||||
prefix, dateStr, MAX_SEQUENCE |
||||
)); |
||||
} |
||||
|
||||
// 生成并返回单号
|
||||
String sequenceStr = String.format("%03d", sequence); |
||||
return prefix + dateStr + sequenceStr; |
||||
|
||||
} catch (Exception e) { |
||||
throw new RuntimeException("生成序列号失败:" + e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 批量生成单号 |
||||
* @param prefix 前缀 |
||||
* @param count 生成数量(1-100) |
||||
* @return 单号列表 |
||||
*/ |
||||
public List<String> generateBatchSimple(String prefix, int count) { |
||||
// 验证参数
|
||||
validatePrefix(prefix); |
||||
if (count <= 0 || count > 100) { |
||||
throw new IllegalArgumentException("生成数量必须在1-100之间"); |
||||
} |
||||
|
||||
// 获取当前日期
|
||||
String dateStr = getCurrentDate(); |
||||
|
||||
// 构建Redis键
|
||||
String redisKey = buildRedisKey(prefix, dateStr); |
||||
|
||||
try { |
||||
// 批量递增
|
||||
Long sequence = redisTemplate.opsForValue().increment(redisKey, count); |
||||
|
||||
// 如果是第一次设置,设置过期时间
|
||||
if (sequence == count) { |
||||
setExpireAtMidnight(redisKey); |
||||
} |
||||
|
||||
// 检查是否超过最大值
|
||||
if (sequence > MAX_SEQUENCE) { |
||||
// 计算超出的数量并递减回去
|
||||
long overCount = sequence - MAX_SEQUENCE; |
||||
redisTemplate.opsForValue().decrement(redisKey, overCount); |
||||
throw new RuntimeException(String.format( |
||||
"序列号已用完,前缀:%s,日期:%s,需要:%d,最大:%d", |
||||
prefix, dateStr, count, MAX_SEQUENCE |
||||
)); |
||||
} |
||||
|
||||
// 生成序列号列表
|
||||
return generateNumbers(prefix, dateStr, sequence - count + 1, count); |
||||
|
||||
} catch (Exception e) { |
||||
throw new RuntimeException("批量生成序列号失败:" + e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取当前序列号(不递增) |
||||
* @param prefix 前缀 |
||||
* @return 当前序列号(0-999) |
||||
*/ |
||||
public int getCurrentSequence(String prefix) { |
||||
validatePrefix(prefix); |
||||
|
||||
String dateStr = getCurrentDate(); |
||||
String redisKey = buildRedisKey(prefix, dateStr); |
||||
|
||||
String value = redisTemplate.opsForValue().get(redisKey); |
||||
return value == null ? 0 : Integer.parseInt(value); |
||||
} |
||||
|
||||
/** |
||||
* 注册新前缀(支持扩展) |
||||
* @param prefix 新前缀 |
||||
*/ |
||||
public void registerPrefix(String prefix) { |
||||
validatePrefix(prefix); |
||||
|
||||
String dateStr = getCurrentDate(); |
||||
String redisKey = buildRedisKey(prefix, dateStr); |
||||
|
||||
// 如果key不存在,初始化
|
||||
Boolean exists = redisTemplate.hasKey(redisKey); |
||||
if (exists == null || !exists) { |
||||
redisTemplate.opsForValue().setIfAbsent(redisKey, "0", getExpireSeconds(), TimeUnit.SECONDS); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 预测下一个单号(不实际生成) |
||||
* @param prefix 前缀 |
||||
* @param count 预测数量 |
||||
* @return 预测的单号列表 |
||||
*/ |
||||
public List<String> predictNext(String prefix, int count) { |
||||
int current = getCurrentSequence(prefix); |
||||
|
||||
if (current + count > MAX_SEQUENCE) { |
||||
throw new RuntimeException(String.format( |
||||
"预测数量将超过最大值,当前:%d,需要:%d,最大:%d", |
||||
current, count, MAX_SEQUENCE |
||||
)); |
||||
} |
||||
|
||||
String dateStr = getCurrentDate(); |
||||
return generateNumbers(prefix, dateStr, current + 1, count); |
||||
} |
||||
|
||||
/** |
||||
* 重置序列号(主要用于测试) |
||||
* @param prefix 前缀 |
||||
* @param date 日期(yyyyMMdd格式) |
||||
* @param sequence 序列号(0-999) |
||||
*/ |
||||
public void resetSequence(String prefix, String date, int sequence) { |
||||
validatePrefix(prefix); |
||||
|
||||
if (sequence < 0 || sequence > MAX_SEQUENCE) { |
||||
throw new IllegalArgumentException("序列号必须在0-" + MAX_SEQUENCE + "之间"); |
||||
} |
||||
|
||||
String redisKey = buildRedisKey(prefix, date); |
||||
redisTemplate.opsForValue().set( |
||||
redisKey, |
||||
String.valueOf(sequence), |
||||
getExpireSeconds(date), |
||||
TimeUnit.SECONDS |
||||
); |
||||
} |
||||
|
||||
// ============ 私有辅助方法 ============
|
||||
|
||||
/** |
||||
* 验证前缀格式 |
||||
*/ |
||||
private void validatePrefix(String prefix) { |
||||
if (prefix == null || prefix.trim().isEmpty()) { |
||||
throw new IllegalArgumentException("前缀不能为空"); |
||||
} |
||||
|
||||
// 前缀格式:字母数字,长度1-10
|
||||
if (!prefix.matches("[A-Za-z0-9]{1,10}")) { |
||||
throw new IllegalArgumentException("前缀必须是1-10位字母数字"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取当前日期字符串 |
||||
*/ |
||||
private String getCurrentDate() { |
||||
return LocalDate.now().format(DATE_FORMATTER); |
||||
} |
||||
|
||||
/** |
||||
* 构建Redis键 |
||||
*/ |
||||
private String buildRedisKey(String prefix, String date) { |
||||
return String.format("%s:%s:%s", REDIS_KEY_PREFIX, prefix, date); |
||||
} |
||||
|
||||
/** |
||||
* 设置key在明天凌晨过期 |
||||
*/ |
||||
private void setExpireAtMidnight(String redisKey) { |
||||
long expireSeconds = getExpireSeconds(); |
||||
if (expireSeconds > 0) { |
||||
redisTemplate.expire(redisKey, expireSeconds, TimeUnit.SECONDS); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取过期时间(秒) |
||||
*/ |
||||
private long getExpireSeconds() { |
||||
return getExpireSeconds(getCurrentDate()); |
||||
} |
||||
|
||||
/** |
||||
* 获取指定日期的过期时间(秒) |
||||
*/ |
||||
private long getExpireSeconds(String dateStr) { |
||||
try { |
||||
LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER); |
||||
LocalDate tomorrow = date.plusDays(1); |
||||
|
||||
long expireAt = tomorrow.atStartOfDay() |
||||
.atZone(ZoneId.systemDefault()) |
||||
.toEpochSecond(); |
||||
|
||||
long now = System.currentTimeMillis() / 1000; |
||||
|
||||
// 返回距离明天凌晨的秒数,至少60秒
|
||||
return Math.max(expireAt - now, 60); |
||||
|
||||
} catch (Exception e) { |
||||
// 解析失败,默认24小时
|
||||
return 24 * 60 * 60; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 生成单号列表 |
||||
*/ |
||||
private List<String> generateNumbers(String prefix, String date, long startSeq, int count) { |
||||
List<String> result = new ArrayList<>(count); |
||||
for (int i = 0; i < count; i++) { |
||||
String sequenceStr = String.format("%03d", startSeq + i); |
||||
result.add(prefix + date + sequenceStr); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* 初始化默认前缀(可在应用启动时调用) |
||||
*/ |
||||
public void initDefaultPrefixes() { |
||||
for (String prefix : DEFAULT_PREFIXES) { |
||||
try { |
||||
registerPrefix(prefix); |
||||
} catch (Exception e) { |
||||
// 初始化失败,记录日志但不中断
|
||||
System.err.println("初始化前缀失败:" + prefix + ", " + e.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue