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;
|
package com.nov.KgLowDurable.config; |
||||||
//import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean; |
||||||
//import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration; |
||||||
//import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||||
//import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate; |
||||||
//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer; |
||||||
//import org.springframework.data.redis.serializer.StringRedisSerializer;
|
/** |
||||||
//
|
* @author: liweidong |
||||||
///**
|
* @create: 2025-12-17 |
||||||
// * @author: liweidong
|
*/ |
||||||
// * @create: 2025-12-17
|
@Configuration |
||||||
// */
|
public class RedisConfig { |
||||||
//@Configuration
|
|
||||||
//public class RedisConfig {
|
@Bean |
||||||
// @Bean
|
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { |
||||||
// public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
RedisTemplate<String, String> template = new RedisTemplate<>(); |
||||||
// RedisTemplate<String, Object> template = new RedisTemplate<>();
|
template.setConnectionFactory(factory); |
||||||
// template.setConnectionFactory(connectionFactory);
|
|
||||||
//
|
// 使用String序列化
|
||||||
// // 使用 String 序列化器作为 key 的序列化器
|
template.setKeySerializer(new StringRedisSerializer()); |
||||||
// template.setKeySerializer(new StringRedisSerializer());
|
template.setValueSerializer(new StringRedisSerializer()); |
||||||
// template.setHashKeySerializer(new StringRedisSerializer());
|
template.setHashKeySerializer(new StringRedisSerializer()); |
||||||
//
|
template.setHashValueSerializer(new StringRedisSerializer()); |
||||||
// // 使用 JSON 序列化器作为 value 的序列化器(可以存储复杂对象)
|
|
||||||
// template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
template.afterPropertiesSet(); |
||||||
// template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
|
return template; |
||||||
//
|
} |
||||||
// 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