init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s

This commit is contained in:
chenxing
2026-08-10 14:50:22 +08:00
commit 4ec19486e0
234 changed files with 15604 additions and 0 deletions
@@ -0,0 +1,155 @@
package com.yunhai.userbase.utils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
/**
* @ Description : 基础工具类,不知道怎么分类放这里就对了
* @ Author : cls
* @ CreateDate : 2020/2/8 16:38
* @ Company : bestsch
*/
@Component
public class BaseUtil {
/**
* 为数字前面自动补充0
* @param number 待补充的数字
* @param length 结果总长度
* 若number为2,length为3,则返回的结果为002
* @return
*/
public static String frontCompWithZore(int number,int length){
String newString = String.format("%0"+length+"d", number);
return newString;
}
/**
* 年级名称转换为年级编号
* @param grade
* @return
*/
public static int gradeConverter(String grade){
if(StringUtils.isNotBlank(grade)){
if(grade.indexOf("一") != -1
&& grade.indexOf("初一") == -1
&& grade.indexOf("高一") == -1){
return 1;
}
if(grade.indexOf("二") != -1
&& grade.indexOf("初二") == -1
&& grade.indexOf("高二") == -1){
return 2;
}
if(grade.indexOf("三") != -1
&& grade.indexOf("初三") == -1
&& grade.indexOf("高三") == -1){
return 3;
}
if(grade.indexOf("四") != -1){
return 4;
}
if(grade.indexOf("五") != -1){
return 5;
}
if(grade.indexOf("六") != -1){
return 6;
}
if(grade.indexOf("七") != -1
|| grade.indexOf("初一") != -1){
return 7;
}
if(grade.indexOf("八") != -1
|| grade.indexOf("初二") != -1){
return 8;
}
if(grade.indexOf("九") != -1
|| grade.indexOf("初三") != -1){
return 9;
}
if(grade.indexOf("十") != -1
|| grade.indexOf("高一") != -1){
return 10;
}
if(grade.indexOf("十一") != -1
|| grade.indexOf("高二") != -1){
return 11;
}
if(grade.indexOf("十二") != -1
|| grade.indexOf("高三") != -1){
return 12;
}
}
return -1;
}
/**
* 学期名称转学期编号
* @param semester
* @return
*/
public static int semesterConverter(String semester){
if(StringUtils.isNotBlank(semester)){
if(semester.indexOf("上") != -1
|| semester.indexOf("一") != -1
|| semester.indexOf("1") != -1){
return 0;
}
if(semester.indexOf("下") != -1
|| semester.indexOf("二") != -1
|| semester.indexOf("2") != -1){
return 1;
}
}
return -1;
}
public static String collectionToString(Collection<?> collection){
String string = "";
if(collection != null && !collection.isEmpty()){
Iterator<?> it = collection.iterator();
while (it.hasNext()){
Object obj = it.next();
if(obj != null && StringUtils.isNotBlank(obj.toString())){
string += obj.toString() + ",";
}
}
if(string.lastIndexOf(",") == string.length()-1){
string = string.substring(0,string.length()-1);
}
}
return string;
}
/** 功能描述: List<String> 转 List<Integer>
* @param list
* @return:
* @Author: cls
* @Date: 2021/10/12 10:06
*/
public static List<Integer> stringListToIntegerList(List<String> list){
return list.stream().map(Integer :: valueOf).collect(Collectors.toList());
}
/**
* 十六进制转字符串
* @param hex
* @return
*/
public final static String hexToString(String hex) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hex.length(); i += 2) {
String hexSubstring = hex.substring(i, i + 2);
int decimalValue = Integer.parseInt(hexSubstring, 16);
sb.append((char) decimalValue);
}
return sb.toString();
}
}
@@ -0,0 +1,57 @@
package com.yunhai.userbase.utils;
import java.util.function.Supplier;
/**
* @author chenx
* @date 2026/1/6 20:54
* @description 通用缓存封装
*/
public final class CacheKitUtils {
private CacheKitUtils() {}
/**
* 泛型模板:先读缓存 → 没有则调用 loader → 回写缓存 → 返回结果
*
* @param redisUtil 你的 RedisUtil
* @param key 缓存键
* @param expireSec 过期秒数
* @param loader 实际业务加载器
* @param <T> 返回值类型
* @return 缓存值或加载值
*/
public static <T> T getOrLoad(RedisUtil redisUtil,
String key,
int expireSec,
Supplier<T> loader) {
// 1. 读缓存
if (redisUtil.hasKey(key)) {
try {
@SuppressWarnings("unchecked")
T v = (T) redisUtil.get(key);
if (v != null) {
return v;
}
} catch (Exception e) {
// 仅打印,继续走 loader
e.printStackTrace();
}
}
// 2. 加载
T value = loader.get();
if (value == null) {
return null;
}
// 3. 回写缓存
try {
redisUtil.set(key, value, expireSec);
} catch (Exception e) {
// 缓存失败不影响主流程
e.printStackTrace();
}
return value;
}
}
@@ -0,0 +1,44 @@
package com.yunhai.userbase.utils;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
public class ConstantUtil {
/**
* redis过期时间
*/
public static final long EXPIRE_TIME=60*60*8;
/**
* 全局缓存时间:5分钟
*/
public static final Duration CACHE_TTL = Duration.ofMinutes(5);
/**
* redis前缀
*/
public static final String PREFIX_KEY="bsch.bschlearn.";//redis前缀
/**
* @Description: 提供AI参数常量
* @Author: chenx
* @Date: 2025/4/28
*/
public static final Map<String, String> PARAMS_CONSTANT;
static {
PARAMS_CONSTANT = new HashMap<>();
/** 默认前两个数据为系统常量 */
PARAMS_CONSTANT.put("score", "分数");
PARAMS_CONSTANT.put("rating", "评级");
PARAMS_CONSTANT.put("polishLevel", "润色幅度");
PARAMS_CONSTANT.put("commentFont", "旁批字体");
PARAMS_CONSTANT.put("correctionStrictness", "批改严格程度");
PARAMS_CONSTANT.put("lineStyle", "句子划线样式");
}
}
@@ -0,0 +1,179 @@
package com.yunhai.userbase.utils;
/**
* @ Description : 存储魔法值
* @ Author : cls
* @ CreateDate : 2020/2/1 9:24
* @ Company : bestsch
*/
public class DicUtil {
/**
* 超管id
*/
public static final Integer SUPER_ADMIN_ID = 100000;
public static final String UNKNOWN = "未知";
/**
* 一般hashMap初始化大小
*/
public static final int initialCapacity = 6;
/**
* 缓存时间,单位毫秒
*/
public static final long ONE_DAY = 1000 * 60 * 60 * 24;
public static final long TEN_MINUTES = 1000 * 60 * 10;
public static final long FIVE_MINUTES = 1000 * 60 * 5;
public static final long ONE_MINUTE = 1000 * 60;
public static final long HALF_MINUTE = 1000 * 30;
public static final long THREE_SECONDS = 1000 * 3;
public static final int SORT_STEP = 10;
/**
* 节点最大层级,索引是9,代表是第10层
*/
public static final int MAX_LEVEL = 9;
/**
* 默认的学科类型 6 代表其他
*/
public static final String DEFAULT_RESOURCE_TYPE = "6";
/**
* 数据状态 1 有效
*/
public static final Integer STATUS_VALID = 1;
/**
* 数据状态 0 无效
*/
public static final Integer STATUS_INVALID = 0;
/**
* 审核状态 0 待审
*/
public static final Integer REVIEW_STATUS_TODO = 0;
/**
* 审核状态 1 通过
*/
public static final Integer REVIEW_STATUS_PASS = 1;
/**
* 审核状态 2 未通过
*/
public static final Integer REVIEW_STATUS_NOPASS = 2;
/**
* 区间参数 不包括
*/
public static final int SECTION_NO_INCLUDE = 0;
/**
* 区间参数 包括
*/
public static final int SECTION_INCLUDE = 1;
/**
* 用户类别 用户中心数据,1 超级管理员
*/
public static final Integer USER_TYPE_SUPER_MANAGER = 1;
/**
* 默认头像
*/
public static final String DEFAULT_PIC = "http://base.bestsch.com/img/defprofilepic.png";
/**
* 排序 降序 0
*/
public static final Integer ORDER_DESC = 0;
/**
* 排序 升序 1
*/
public static final Integer ORDER_ASC = 1;
/**
* 错误信息,上传文件失败
*/
public static final String ERROR_MSG_IMPORT_WRONG = "上传失败,请稍后再试";
/**
* 错误消息,数据不存在
*/
public static final String ERROR_MSG_NOT_FOUND = "数据不存在,可能已被删除";
/**
* 错误消息,无操作权限
*/
public static final String ERROR_MSG_NO_POWER = "您无操作权限";
/**
* 错误消息,无权删除资源
*/
public static final String NO_POWER_TO_DEL_RESOURCE = "您无法删除此文件";
/**
* 错误消息,无权编辑资源
*/
public static final String NO_POWER_TO_MODIFY_RESOURCE = "您无法编辑/移动此文件";
/**
* 错误消息,无权移动到根节点
*/
public static final String NO_MOVE_TO_ROOT = "您无法把文件移至根节点";
/**
* 错误消息,无法删除此节点
*/
public static final String NOT_DEL_NODE = "无法删除,请先移动或删除此节点下的文件";
/**
* 错误消息,没有可查看的节点
*/
public static final String NO_NODE_BY_WATCH = "您暂无可查看的节点";
/**
* 错误消息,未知的教材来源
*/
public static final String UNKNOWN_TEXTBOOK_SOURCE = "未知的教材来源";
/**
* 错误消息,无法对根节点进行操作
*/
public static final String CANT_OPERATE_FOR_ROOT = "无法对根节点进行操作";
public static final String OVER_MAX_LEVEL = "层级超过上限,最多支持" + (MAX_LEVEL + 1) + "层";
/**
* 错误消息,未知的权限类型
*/
public static final String UNKNOWN_AUTHORITY_TYPE = "未知的权限类型";
/**
* 错误消息,未知的根节点类型类型
*/
public static final String UNKNOWN_ROOT_TYPE_TYPE = "未知的根节点类型";
/**
* 错误消息,节点不存在
*/
public static final String NO_NODE = "节点不存在";
/**
* 错误消息,节点不存在
*/
public static final String NO_TEXTBOOK = "教材不存在";
/**
* 错误消息,节点不存在
*/
public static final String NO_AUTHORITY_NODE = "无权限操作该节点";
}
@@ -0,0 +1,56 @@
package com.yunhai.userbase.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.*;
@Slf4j
public class DownloadFileUtil {
public static final String separator = File.separator;
/**
* 下载样表
* @param filePath 文件上级目录
* @param fileName 文件名
* @param newName 下载的展示文件名
* @return 响应
*/
public static ResponseEntity<InputStreamResource> download(String filePath, String fileName, String newName) {
// String route = "static" + separator + "templates" + separator;
// String path = null;
ResponseEntity<InputStreamResource> response = null;
try {
// path = route + filePath + separator + fileName;
File file = new File(filePath);
System.out.println(file.exists());
// ClassPathResource classPathResource = new ClassPathResource(filePath);
InputStream inputStream = new FileInputStream(file);
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Content-Disposition",
"attachment; filename="
+ new String(newName.getBytes("gbk"), "iso8859-1"));
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
response = ResponseEntity.ok().headers(headers)
.contentType(MediaType.parseMediaType("application/octet-stream"))
.body(new InputStreamResource(inputStream));
} catch (FileNotFoundException e1) {
log.error("找不到指定的文件", e1);
} catch (IOException e) {
log.error("获取不到文件流", e);
}
return response;
}
}
@@ -0,0 +1,105 @@
package com.yunhai.userbase.utils;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.word.WordExportUtil;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
public final class EasyPoiUtils {
private EasyPoiUtils() {}
/**
* 只能xls
* @param fileName
* @param response
* @param workbook
*/
public static void downLoadExcel(String fileName, HttpServletResponse response, Workbook workbook) {
try {
response.setCharacterEncoding("UTF-8");
response.setHeader("content-Type", "application/vnd.ms-excel");
fileName = new String(fileName.getBytes("utf-8"), "iso-8859-1") + ".xls";
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
workbook.write(response.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static<T> void defaultExport(List<T> dataList, Class<?> clz, String fileName, HttpServletResponse response, ExportParams exportParams) {
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, clz, dataList);
// workbook.getSheetAt(0).getRow(0).setHeight((short) 0x25622); // 设置行高
if (workbook != null) {
downLoadExcel(fileName, response, workbook);
}
}
public static<T> void exportExcel(List<T> dataList, String title, String sheetName, Class<?> clz, String fileName, boolean isCreateHeader, HttpServletResponse response) {
ExportParams exportParams = new ExportParams(title, sheetName);
exportParams.setCreateHeadRows(isCreateHeader);
defaultExport(dataList, clz, fileName, response, exportParams);
}
public static<T> void exportExcel(List<T> dataList, String title, String sheetName, Class<?> clz, String fileName, HttpServletResponse response) {
defaultExport(dataList, clz, fileName, response, new ExportParams(title, sheetName));
}
private static<T> void changeHeightExport(List<T> dataList, Class<?> clz, String fileName, HttpServletResponse response, ExportParams exportParams, Short height) {
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, clz, dataList);
if (height != null) {
workbook.getSheetAt(0).getRow(0).setHeight(height); // 设置行高
}
if (workbook != null) {
downLoadExcel(fileName, response, workbook);
}
}
public static StringBuffer changeStringTable(String spl,String string) {
StringBuffer buffer = new StringBuffer();
String[] split = string.split(spl);
for (int i = 0; i < split.length; i++) {
if (i < split.length - 1) {
buffer.append(split[i]+(char)11);
} else {
buffer.append(split[i]);
}
}
return buffer;
}
/**
* 导出word
*/
public static void exportWord(String fileName, Map<String, Object> map, HttpServletRequest request, HttpServletResponse response) {
try {
String userAgent = request.getHeader("user-agent").toLowerCase();
if (userAgent.contains("msie") || userAgent.contains("like gecko")) {
fileName = URLEncoder.encode(fileName, "UTF-8");
} else {
fileName = new String(fileName.getBytes("utf-8"), "ISO-8859-1");
}
XWPFDocument doc = WordExportUtil.exportWord07(
"/Users/luona/Downloads/模版.docx", map);
// 设置强制下载不打开
response.setContentType("application/force-download");
// 设置文件名
response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
OutputStream out = response.getOutputStream();
doc.write(out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,134 @@
package com.yunhai.userbase.utils;
import cn.hutool.core.util.StrUtil;
import com.yunhai.userbase.enums.IEnum;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 枚举底层工具实现,适配 Hutool 5.8.36
* 对外统一通过 IEnum.xxx() 静态调用,不直接使用本类
*/
public final class EnumUtils {
private EnumUtils() {
throw new AssertionError("工具类禁止实例化");
}
/**
* 获取全部枚举实例(依赖Hutool5.8.36支持的EnumUtil.toList)
*/
public static <T extends Enum<T> & IEnum<?>> List<T> toList(Class<T> targetEnumClazz) {
return new ArrayList<>(Arrays.asList(targetEnumClazz.getEnumConstants()));
}
/**
* 枚举转完整Map列表(包含enum、name、key、value)
*/
public static <T extends Enum<T> & IEnum<?>> List<Map<String, Object>> toMapList(Class<T> targetEnumClazz) {
List<T> enumList = toList(targetEnumClazz);
List<Map<String, Object>> resultList = new ArrayList<>(enumList.size());
for (T enumItem : enumList) {
Map<String, Object> map = new HashMap<>(4);
map.put("enum", enumItem);
map.put("name", enumItem.name());
map.put("key", enumItem.getKey());
map.put("value", enumItem.getValue());
resultList.add(map);
}
return resultList;
}
/**
* 通过key获取对应value文本
*/
public static <T extends Enum<T> & IEnum<?>> String getValue(Class<T> targetEnumClazz, Object key) {
if (key == null || StrUtil.isBlank(String.valueOf(key))) {
return StrUtil.EMPTY;
}
Optional<T> matchEnum = findByKey(targetEnumClazz, key);
return matchEnum.map(IEnum::getValue).orElse(StrUtil.EMPTY);
}
/**
* 通过value文本获取对应key字符串
*/
public static <T extends Enum<T> & IEnum<?>> String getKey(Class<T> targetEnumClazz, String value) {
if (StrUtil.isBlank(value)) {
return StrUtil.EMPTY;
}
Optional<T> matchEnum = findByValue(targetEnumClazz, value);
return matchEnum.map(item -> String.valueOf(item.getKey())).orElse(StrUtil.EMPTY);
}
/**
* 判断key是否存在
*/
public static <T extends Enum<T> & IEnum<?>> boolean isExistKey(Class<T> targetEnumClazz, Object key) {
return findByKey(targetEnumClazz, key).isPresent();
}
/**
* 判断value是否存在
*/
public static <T extends Enum<T> & IEnum<?>> boolean isExistValue(Class<T> targetEnumClazz, String value) {
return findByValue(targetEnumClazz, value).isPresent();
}
/**
* 根据key获取枚举实例,无匹配返回null
*/
public static <T extends Enum<T> & IEnum<?>> T getEnumByKey(Class<T> targetEnumClazz, Object key) {
return findByKey(targetEnumClazz, key).orElse(null);
}
/**
* 根据key严格获取枚举,不存在抛出异常(fromKey)
*/
public static <T extends Enum<T> & IEnum<?>> T fromKey(Class<T> targetEnumClazz, Object key) {
T enumItem = getEnumByKey(targetEnumClazz, key);
if (enumItem == null) {
throw new IllegalArgumentException(
String.format("枚举不存在,key=%s,枚举类=%s", key, targetEnumClazz.getName())
);
}
return enumItem;
}
/**
* 自定义返回字段名,生成下拉框简易列表
*/
public static <T extends Enum<T> & IEnum<?>> List<Map<String, Object>> toMapList(
Class<T> targetEnumClazz, String keyMapName, String valueMapName
) {
List<T> enumList = toList(targetEnumClazz);
List<Map<String, Object>> result = new ArrayList<>(enumList.size());
for (T enumItem : enumList) {
Map<String, Object> map = new HashMap<>(2);
map.put(keyMapName, enumItem.getKey());
map.put(valueMapName, enumItem.getValue());
result.add(map);
}
return result;
}
// ====================== 私有底层查询方法(基于接口方法,无反射硬编码) ======================
private static <T extends Enum<T> & IEnum<?>> Optional<T> findByKey(Class<T> clazz, Object key) {
String targetKey = String.valueOf(key);
return toList(clazz).stream()
.filter(item -> targetKey.equals(String.valueOf(item.getKey())))
.findFirst();
}
private static <T extends Enum<T> & IEnum<?>> Optional<T> findByValue(Class<T> clazz, String value) {
return toList(clazz).stream()
.filter(item -> value.equals(item.getValue()))
.findFirst();
}
}
@@ -0,0 +1,411 @@
package com.yunhai.userbase.utils;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.bestsch.utils.DException;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author min
* @description File工具类,扩展 hutool 工具包
* @date 2019-07-05
*/
public class FileUtil {
/**
* 定义GB的计算常量(改为long避免int溢出)
*/
private static final long GB = 1024L * 1024 * 1024;
/**
* 定义MB的计算常量
*/
private static final long MB = 1024L * 1024;
/**
* 定义KB的计算常量
*/
private static final long KB = 1024L;
/**
* 格式化小数
*/
private static final DecimalFormat DF = new DecimalFormat("0.00");
/**
* 上传文件存储根目录(由 FileStorageConfig 在启动时注入)。<br>
*/
private static String BASE_STORE_PATH;
/**
* 上传文件对外访问的 Web 根地址(由 FileStorageConfig 在启动时注入)。<br>
*/
private static String BASE_WEB_PATH;
/**
* 危险后缀黑名单:命中则禁止上传,防止恶意文件被执行/访问。<br>
*/
private static final Set<String> DANGEROUS_EXT = new HashSet<>(Arrays.asList(
"jsp", "jspx", "html", "htm", "js", "exe", "sh", "bat",
"php", "asp", "aspx", "jar", "war", "class", "htaccess"
));
/**
* 由 Spring 启动时注入存储根目录与 Web 访问前缀。<br>
*
* @param storePath 磁盘存储根目录
* @param webPath 对外访问的 Web 根地址
*/
public static void init(String storePath, String webPath) {
BASE_STORE_PATH = storePath;
BASE_WEB_PATH = webPath;
}
/**
* 校验文件后缀是否在危险黑名单中。<br>
*
* @param ext 文件后缀(可带或不带点)
* @throws DException 命中黑名单时抛出
*/
private static void validateExt(String ext) {
if (ext == null || ext.isEmpty()) {
return;
}
String lower = ext.toLowerCase().replaceFirst("^\\.", "");
if (DANGEROUS_EXT.contains(lower)) {
throw new DException("不支持的文件类型: " + ext);
}
}
/**
* 净化文件名,防止路径穿越(../ 等)。<br>
*
* @param name 原始文件名
* @return 净化后的安全文件名
*/
private static String sanitizeFileName(String name) {
if (name == null || name.trim().isEmpty()) {
return "file";
}
return name.replaceAll("[\\\\/]", "_")
.replaceAll("\\.{2,}", "_")
.replaceAll("\\s+", "_");
}
/**
* 拼接完整可访问的 Web URL(根地址 + 子目录 + 文件名)。<br>
*
* @param subDir 子目录(如 image/file/icon)
* @param fileName 文件名
* @return 完整访问 URL
*/
private static String buildUrl(String subDir, String fileName) {
String web = (BASE_WEB_PATH == null) ? "" : BASE_WEB_PATH;
web = web.endsWith("/") ? web : web + "/";
return web + subDir + "/" + fileName;
}
/**
* 校验存储配置已注入。<br>
*
* @throws DException 未注入时抛出
*/
private static void ensureStoreBase() {
if (BASE_STORE_PATH == null || BASE_WEB_PATH == null) {
throw new DException("文件存储路径未初始化,请检查 FILE_PATH / FILE_WEB_PATH 配置");
}
}
/**
* MultipartFile转File(临时文件)
* @param multipartFile 上传文件
* @return 临时文件对象,失败返回null
*/
public static File toFile(MultipartFile multipartFile) {
if (multipartFile == null || multipartFile.isEmpty()) {
return null;
}
// 获取文件后缀
String ext = getExtensionName(multipartFile.getOriginalFilename());
String prefix = "." + ext;
File file = null;
try {
// 用uuid作为文件名,防止生成的临时文件重复
file = File.createTempFile(IdUtil.simpleUUID(), prefix);
multipartFile.transferTo(file);
} catch (IOException e) {
e.printStackTrace();
}
return file;
}
/**
* 删除文件/目录
* @param files 待删除文件数组
*/
public static void deleteFile(File... files) {
if (files == null || files.length == 0) {
return;
}
for (File file : files) {
if (file != null && file.exists()) {
cn.hutool.core.io.FileUtil.del(file);
}
}
}
/**
* 获取文件扩展名(不带点)
* @param filename 文件名
* @return 扩展名,无后缀返回原文件名
*/
public static String getExtensionName(String filename) {
return cn.hutool.core.io.FileUtil.extName(filename);
}
/**
* 获取不带扩展名的文件名
* @param filename 文件名
* @return 主文件名
*/
public static String getFileNameNoEx(String filename) {
return cn.hutool.core.io.FileUtil.mainName(filename);
}
/**
* 文件大小转换(修复int溢出问题)
* @param size 文件字节数
* @return 格式化后的大小字符串
*/
public static String getSize(int size) {
long sizeLong = size;
String resultSize;
if (sizeLong / GB >= 1) {
resultSize = DF.format(sizeLong / (double) GB) + "GB ";
} else if (sizeLong / MB >= 1) {
resultSize = DF.format(sizeLong / (double) MB) + "MB ";
} else if (sizeLong / KB >= 1) {
resultSize = DF.format(sizeLong / (double) KB) + "KB ";
} else {
resultSize = sizeLong + "B ";
}
return resultSize;
}
/**
* 字节数组上传图片(固定.png后缀,存 image 子目录)。<br>
*
* @param imgData 图片字节数组
* @return 完整可访问的 Web URL
*/
public static String uploadImg(byte[] imgData) {
return uploadBytes(imgData, "image", ".png");
}
/**
* MultipartFile上传图片(按原后缀,存 image 子目录)。<br>
*
* @param file 上传文件
* @return 完整可访问的 Web URL
*/
public static String uploadImg(MultipartFile file) {
return uploadMultipart(file, "image");
}
/**
* 字节数组上传文件(固定.png后缀,存 file 子目录)。<br>
*
* @param imgData 文件字节数组
* @return 完整可访问的 Web URL
*/
public static String uploadFile(byte[] imgData) {
return uploadBytes(imgData, "file", ".png");
}
/**
* MultipartFile上传文件(按原后缀,存 file 子目录)。<br>
*
* @param file 上传文件
* @return 完整可访问的 Web URL
*/
public static String uploadFile(MultipartFile file) {
return uploadMultipart(file, "file");
}
/**
* 通用上传:按原后缀保存到指定子目录,返回完整可访问的 Web URL。<br>
*
* @param file 上传文件
* @param subDir 子目录(如 image/file/icon/media/word/html)
* @return 完整可访问的 Web URL
*/
public static String upload(MultipartFile file, String subDir) {
return uploadMultipart(file, subDir);
}
/**
* URL文件代理中转下载
* @param response 响应对象
* @param address 远程文件地址
* @param contentType 响应Content-Type,传null自动识别
* @param fileName 下载文件名,传null取远程响应头
* @throws IOException 网络/IO异常
*/
public static void proxyUrlFile(HttpServletResponse response, String address, String contentType, String fileName) throws IOException {
// 原有https转http逻辑保留
if (address != null && address.toLowerCase().startsWith("https:")) {
address = address.replaceFirst("(?i)^https:", "http:")
.replaceAll(":443(/|$)", ":80$1");
}
HttpURLConnection httpURLConnection = null;
InputStream inputStream = null;
ServletOutputStream outputStream = null;
try {
URL url = new URL(address);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(5000);
httpURLConnection.setReadTimeout(30000);
httpURLConnection.connect();
inputStream = httpURLConnection.getInputStream();
outputStream = response.getOutputStream();
// 设置Content-Type
if (contentType != null) {
response.setContentType(contentType);
} else {
response.setContentType("multipart/form-data");
}
// 设置下载文件名
if (fileName != null) {
response.setHeader("Content-disposition", "attachment; filename=\""
+ new String(fileName.getBytes("utf-8"), "ISO8859-1") + "\"");
} else {
String headerFileName = "";
String disposition = httpURLConnection.getHeaderField("Content-Disposition");
if (disposition != null) {
headerFileName = new String(disposition.getBytes("ISO-8859-1"), "UTF-8");
}
if (headerFileName.contains("filename")) {
headerFileName = headerFileName.substring(headerFileName.lastIndexOf("filename") + "filename".length());
// 去除可能的=号和引号
headerFileName = headerFileName.replaceFirst("=", "").replace("\"", "").trim();
}
response.setHeader("Content-disposition", "attachment; filename=" + headerFileName);
}
// 流式拷贝,8KB缓冲区提升性能
IoUtil.copy(inputStream, outputStream, IoUtil.DEFAULT_BUFFER_SIZE);
} catch (Exception e) {
throw e;
} finally {
IoUtil.close(inputStream);
if (outputStream != null) {
outputStream.flush();
IoUtil.close(outputStream);
}
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
}
/**
* 保存上传文件到指定完整路径
* @param mf 上传文件
* @param path 完整保存路径(含文件名)
*/
public static void saveResource(MultipartFile mf, String path) {
if (mf == null || path == null || path.trim().length() == 0) {
throw new DException(DicUtil.ERROR_MSG_IMPORT_WRONG);
}
// 净化文件名,防止路径穿越(如 ../../evil)
File target = new File(path);
String safeName = sanitizeFileName(target.getName());
File safeFile = new File(target.getParentFile(), safeName);
// 自动创建父目录
cn.hutool.core.io.FileUtil.mkParentDirs(safeFile.getPath());
try (InputStream is = mf.getInputStream();
FileOutputStream fos = new FileOutputStream(safeFile)) {
IoUtil.copy(is, fos);
} catch (Exception e) {
throw new DException(DicUtil.ERROR_MSG_IMPORT_WRONG);
}
}
// ==================== 私有通用方法,消除重复代码 ====================
/**
* 字节数组通用上传(流式写入,返回完整 Web URL)。<br>
*
* @param data 字节数组
* @param subDir 子目录
* @param ext 固定后缀(带点)
* @return 完整可访问的 Web URL
*/
private static String uploadBytes(byte[] data, String subDir, String ext) {
if (data == null || data.length == 0 || subDir == null || subDir.trim().length() == 0) {
throw new DException("上传内容为空");
}
// 后缀黑名单校验(固定后缀也走同一规则)
validateExt(ext);
ensureStoreBase();
try {
// 自动创建多级目录
String dirPath = BASE_STORE_PATH + File.separator + subDir;
cn.hutool.core.io.FileUtil.mkdir(dirPath);
String fileName = IdUtil.fastSimpleUUID() + ext;
File targetFile = new File(dirPath, fileName);
// Hutool写入,自动关流
cn.hutool.core.io.FileUtil.writeBytes(data, targetFile);
return buildUrl(subDir, fileName);
} catch (DException e) {
throw e;
} catch (Exception e) {
throw new DException("文件上传失败: " + e.getMessage());
}
}
/**
* MultipartFile通用上传(流式写入,避免大文件OOM,返回完整 Web URL)。<br>
*
* @param file 上传文件
* @param subDir 子目录
* @return 完整可访问的 Web URL
*/
private static String uploadMultipart(MultipartFile file, String subDir) {
if (file == null || file.isEmpty() || subDir == null || subDir.trim().length() == 0) {
throw new DException("上传文件不能为空");
}
// 提取后缀
String originalFilename = file.getOriginalFilename();
String ext = cn.hutool.core.io.FileUtil.extName(originalFilename);
ext = (ext == null || ext.trim().length() == 0) ? "" : "." + ext.toLowerCase();
// 后缀黑名单校验
validateExt(ext);
ensureStoreBase();
try {
String dirPath = BASE_STORE_PATH + File.separator + subDir;
cn.hutool.core.io.FileUtil.mkdir(dirPath);
String fileName = IdUtil.fastSimpleUUID() + ext;
File targetFile = new File(dirPath, fileName);
// 流式写入,不加载全文件到内存
cn.hutool.core.io.FileUtil.writeFromStream(file.getInputStream(), targetFile);
return buildUrl(subDir, fileName);
} catch (DException e) {
throw e;
} catch (Exception e) {
throw new DException("文件上传失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,64 @@
package com.yunhai.userbase.utils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* GZIP 压缩/解压工具。<br>
* 用于操作日志响应结果(response_data)的压缩存储与下载解压。<br>
*
* @author chenx
* @date 2026/7/28
*/
public class GzipUtil {
private static final int BUFFER_SIZE = 4096;
/**
* 将字符串按 UTF-8 压缩为 GZIP 字节数组。
*
* @param data 原始字符串,可为 null
* @return 压缩后字节数组,入参为空返回 null
*/
public static byte[] compress(String data) {
if (data == null) {
return null;
}
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out)) {
gzip.write(data.getBytes(StandardCharsets.UTF_8));
gzip.finish();
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException("GZIP 压缩失败", e);
}
}
/**
* 将 GZIP 字节数组解压为原始字符串。
*
* @param data 压缩字节数组,可为 null 或空
* @return 解压后字符串,入参为空返回 null
*/
public static String decompress(byte[] data) {
if (data == null || data.length == 0) {
return null;
}
try (ByteArrayInputStream in = new ByteArrayInputStream(data);
GZIPInputStream gzip = new GZIPInputStream(in);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = gzip.read(buf)) > 0) {
out.write(buf, 0, n);
}
return out.toString(StandardCharsets.UTF_8.name());
} catch (IOException e) {
throw new RuntimeException("GZIP 解压失败", e);
}
}
}
@@ -0,0 +1,143 @@
package com.yunhai.userbase.utils;
import com.bestsch.utils.DException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
public class HttpUtils {
static {
try {
trustAllHttpsCertificates();
HttpsURLConnection.setDefaultHostnameVerifier
(
(urlHostName, session) -> true
);
} catch (Exception e) {
}
}
/**
* 跳过ssl证书
*
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
*/
private static void trustAllHttpsCertificates()
throws NoSuchAlgorithmException, KeyManagementException {
TrustManager[] trustAllCerts = new TrustManager[1];
trustAllCerts[0] = (TrustManager) new TrustAllManager();
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
private static class TrustAllManager implements X509TrustManager {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
}
public static String get(String aUrl,Integer timeout){
int respCode = 0;
HttpURLConnection conn = null;
String var3;
try {
conn = getHttpURLConnection(aUrl, "GET",timeout);
conn.setDoInput(true);
respCode = conn.getResponseCode();
if (respCode != 301) {
InputStream is = conn.getInputStream();
int contentLen = conn.getContentLength();
byte[] resultData = read(is, contentLen);
is.close();
String var6 = new String(resultData, "utf-8");
return var6;
}
var3 = get(conn.getHeaderField("Location"),timeout);
} catch (Exception var10) {
throw new DException("网络连接失败");
} finally {
close(conn);
}
return var3;
}
private static HttpURLConnection getHttpURLConnection(String uri, String method,int timeout) throws IOException {
URL url = new URL(uri);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(timeout);
conn.setReadTimeout(timeout);
return conn;
}
private static byte[] read(InputStream is, int contentLen) throws IOException {
if (contentLen <= 0) {
int BUF_SIZE = 102400;
byte[] buf = new byte[102400];
ByteBuffer byteBuffer = ByteBuffer.allocate(102400);
int readed;
do {
if (byteBuffer.position() + 102400 > byteBuffer.capacity()) {
byteBuffer = reallocByteBuffer(byteBuffer, byteBuffer.capacity() + 102400);
}
readed = is.read(buf, 0, 102400);
if (readed > 0) {
byteBuffer.put(buf, 0, readed);
}
} while(readed != -1);
byte[] data = new byte[byteBuffer.position()];
System.arraycopy(byteBuffer.array(), 0, data, 0, byteBuffer.position());
return data;
} else {
byte[] data = new byte[contentLen];
int readed;
for(int remain = contentLen; remain > 0; remain -= readed) {
readed = is.read(data, contentLen - remain, remain);
if (readed == -1) {
break;
}
}
return data;
}
}
private static ByteBuffer reallocByteBuffer(ByteBuffer buffer, int size) {
ByteBuffer newBuf = ByteBuffer.allocate(size);
newBuf.put(buffer.array(), 0, buffer.position());
return newBuf;
}
private static void close(HttpURLConnection conn) {
try {
if (conn == null) {
return;
}
conn.disconnect();
} catch (Exception var2) {
var2.printStackTrace();
}
}
}
@@ -0,0 +1,53 @@
package com.yunhai.userbase.utils;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 客户端 IP 获取工具。<br>
* 兼容反向代理场景:优先取 X-Forwarded-For 等代理头中的第一个真实 IP。<br>
*
* @author chenx
* @date 2026/7/28
*/
public class IpUtils {
/** 常见的真实 IP 代理头。 */
private static final String[] PROXY_HEADERS = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR",
"X-Real-IP"
};
private static final String UNKNOWN = "unknown";
/**
* 获取客户端真实 IP。<br>
* X-Forwarded-For 可能为逗号分隔的多级代理,取第一段。<br>
*
* @param request HTTP 请求
* @return 客户端 IP,获取失败返回 "unknown"
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return UNKNOWN;
}
String ip = null;
for (String header : PROXY_HEADERS) {
ip = request.getHeader(header);
if (StringUtils.isNotBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
int comma = ip.indexOf(',');
if (comma > 0) {
ip = ip.substring(0, comma).trim();
}
return ip;
}
}
ip = request.getRemoteAddr();
return StringUtils.isBlank(ip) ? UNKNOWN : ip;
}
}
@@ -0,0 +1,86 @@
package com.yunhai.userbase.utils;
import java.util.HashMap;
import java.util.Map;
/**
* 课程节点 64 bit 复合 ID 工具类(无实体、纯静态)
* rootType 根节点类别【资源,辅导,题库,试卷,专项】
* nodeType 虚拟节点类型【根节点,教材(辅)目录节点,年级节点,教材知识树节点,真实节点】
* stage 学段【2小学 3初中 4高中】
* grade 年级 1-12
*/
public final class NodeIdUtil {
/* ===================== 位宽 ===================== */
private static final int ROOT_BITS = 4;
private static final int TYPE_BITS = 4; // 原 biz
private static final int SUBJ_BITS = 20;
private static final int STAGE_BITS = 4;
private static final int GRADE_BITS = 4;
private static final int NODE_BITS = 28;
private static final long ROOT_MASK = mask(ROOT_BITS);
private static final long TYPE_MASK = mask(TYPE_BITS);
private static final long SUBJ_MASK = mask(SUBJ_BITS);
private static final long STAGE_MASK = mask(STAGE_BITS);
private static final long GRADE_MASK = mask(GRADE_BITS);
private static final long NODE_MASK = mask(NODE_BITS);
/* ===================== 编码 ===================== */
public static long encode(Integer rootType,
String subject,
Integer stage,
Integer nodeType,
Integer grade,
Integer nodeId) {
int subNum = subjectToInt(subject);
long root = (rootType == null ? 0 : rootType) & ROOT_MASK;
long type = (nodeType == null ? 0 : nodeType) & TYPE_MASK;
long stageVal = (stage == null ? 0 : stage) & STAGE_MASK;
long gradeVal = (grade == null ? 0 : grade) & GRADE_MASK;
long nodeVal = (nodeId == null ? 0 : nodeId) & NODE_MASK;
return root << (TYPE_BITS + SUBJ_BITS + STAGE_BITS + GRADE_BITS + NODE_BITS)
| type << (SUBJ_BITS + STAGE_BITS + GRADE_BITS + NODE_BITS)
| (long) subNum << (STAGE_BITS + GRADE_BITS + NODE_BITS)
| stageVal << (GRADE_BITS + NODE_BITS)
| gradeVal << NODE_BITS
| nodeVal;
}
/* ===================== 解码 ===================== */
public static Map<String, Object> decode(long compositeId) {
Map<String, Object> map = new HashMap<>(6);
int root = (int) ((compositeId >>> (TYPE_BITS + SUBJ_BITS + STAGE_BITS + GRADE_BITS + NODE_BITS)) & ROOT_MASK);
int type = (int) ((compositeId >>> (SUBJ_BITS + STAGE_BITS + GRADE_BITS + NODE_BITS)) & TYPE_MASK);
int subNum = (int) ((compositeId >>> (STAGE_BITS + GRADE_BITS + NODE_BITS)) & SUBJ_MASK);
int stage = (int) ((compositeId >>> (GRADE_BITS + NODE_BITS)) & STAGE_MASK);
int grade = (int) ((compositeId >>> (NODE_BITS)) & GRADE_MASK);
int nodeId = (int) (compositeId & NODE_MASK);
map.put("rootType", root == 0 ? null : root);
map.put("nodeType", type == 0 ? null : type);
map.put("subject", intToSubject(subNum));
map.put("stage", stage == 0 ? null : stage);
map.put("grade", grade == 0 ? null : grade);
map.put("nodeId", nodeId == 0 ? null : nodeId);
return map;
}
/* ===================== subject 工具 ===================== */
private static int subjectToInt(String subject) {
if (subject == null || !subject.startsWith("SUB")) return 0;
return Integer.parseUnsignedInt(subject.substring(3));
}
private static String intToSubject(int n) {
if (n == 0) return null;
return (n <= 6) ? "SUB0" + n : "SUB" + n;
}
private static long mask(int bits) { return (1L << bits) - 1; }
private NodeIdUtil() {}
}
@@ -0,0 +1,85 @@
package com.yunhai.userbase.utils;
import com.yunhai.userbase.enums.LogLevel;
import org.apache.commons.lang3.StringUtils;
/**
* 操作日志级别与操作类型的自动判定。<br>
* 规则(贴合本项目的接口风格):<br>
* - GET 一律记为「一般(查询)」、操作类型 QUERY;<br>
* - HTTP DELETE 方法或路径/方法名含 delete/del/remove 记为「重要(删除)」、操作类型 DELETE;<br>
* - 含 add/create/insert 记为操作类型 CREATE;<br>
* - 其余 POST/PUT 记为「次要(新增/编辑)」、操作类型 UPDATE。<br>
*
* @author chenx
* @date 2026/7/28
*/
public class OperationLogResolver {
private static final String[] DELETE_KEYWORDS = {"delete", "del", "remove"};
private static final String[] CREATE_KEYWORDS = {"add", "create", "insert"};
/**
* 判定日志级别。
*
* @param httpMethod HTTP 方法(GET/POST/PUT/DELETE...)
* @param uri 请求路径
* @param methodName 被拦截的方法名
* @return 日志级别
*/
public static LogLevel resolveLevel(String httpMethod, String uri, String methodName) {
if (StringUtils.isBlank(httpMethod)) {
return LogLevel.SECONDARY;
}
String method = httpMethod.toUpperCase();
if ("GET".equals(method)) {
return LogLevel.GENERAL;
}
if ("DELETE".equals(method)) {
return LogLevel.IMPORTANT;
}
if (containsAny(uri, methodName, DELETE_KEYWORDS)) {
return LogLevel.IMPORTANT;
}
return LogLevel.SECONDARY;
}
/**
* 判定操作类型(DELETE/CREATE/UPDATE/QUERY)。
*
* @param httpMethod HTTP 方法
* @param uri 请求路径
* @param methodName 被拦截的方法名
* @return 操作类型字符串
*/
public static String resolveOperationType(String httpMethod, String uri, String methodName) {
if (StringUtils.isBlank(httpMethod)) {
return "UPDATE";
}
String method = httpMethod.toUpperCase();
if ("GET".equals(method)) {
return "QUERY";
}
if ("DELETE".equals(method)) {
return "DELETE";
}
if (containsAny(uri, methodName, DELETE_KEYWORDS)) {
return "DELETE";
}
if (containsAny(uri, methodName, CREATE_KEYWORDS)) {
return "CREATE";
}
return "UPDATE";
}
private static boolean containsAny(String uri, String methodName, String[] keywords) {
String text = (StringUtils.lowerCase(uri) + " " + StringUtils.lowerCase(methodName));
for (String keyword : keywords) {
if (StringUtils.contains(text, keyword)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,139 @@
package com.yunhai.userbase.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.bestsch.utils.DException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/*
* 杂项公用方法集合
* */
public class OtherUtil {
/**
* 十六进制转字符串
* @param hex
* @return
*/
public final static String hexToString(String hex) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hex.length(); i += 2) {
String hexSubstring = hex.substring(i, i + 2);
int decimalValue = Integer.parseInt(hexSubstring, 16);
sb.append((char) decimalValue);
}
return sb.toString();
}
/**
* @Description: 判断字符串能否序列化
* @Author: chenx
* @Date: 2023/4/20
*/
public final static boolean isJSONValid(String str) {
try {
JSONObject.parse(str);
} catch (JSONException ex) {
return false;
}
return true;
}
/**
* @Description: 判断字符串能否序列化为json数组
* @Author: chenx
* @Date: 2023/4/20
*/
public final static boolean isJsonArray(String str) {
if (!isJSONValid(str)) return false;
try {
Object object = JSON.parse(str);
return object instanceof JSONArray;
} catch (JSONException ex) {
return false;
}
}
/**
* @Description: 数组深拷贝
* @Author: chenx
* @Date: 2023/4/20
*/
public final static <T> List<T> deepCopy(List<T> src, Class<T> clazz) {
String jsonStr=JSON.toJSONString(src);
if (!isJsonArray(jsonStr)) return src;
return JSONArray.parseArray(jsonStr,clazz);
}
/**
* @Description: 逗号分隔字符串转List数组
* @Author: chenx
* @Date: 2023/4/20
*/
public final static List<Integer> SplitToList(String idsStr) {
return Arrays.stream(idsStr.split(",")).map(s -> Integer.parseInt(s.trim())).collect(Collectors.toList());
}
/**
* 判断是否为校领导
*/
public final static Boolean isAdminById(Integer roleId){
return Arrays.asList(2,5,7).contains(roleId);
}
public static JSONObject getDataOrThrow(String result) {
JSONObject root = JSON.parseObject(result);
if (root.getIntValue("code") != 0) {
throw new DException("接口异常: " + root.getString("msg"));
}
return root.getJSONObject("data");
}
/**
* result 里 data 直接就是数组 [],返回 List<JSONObject>
*/
public static List<JSONObject> getDataArray(String result) {
JSONObject root = JSON.parseObject(result);
if (root.getIntValue("code") != 0) {
throw new DException("接口异常: " + root.getString("msg"));
}
// data 本身就是 JSONArray
return root.getJSONArray("data").toJavaList(JSONObject.class);
}
/**
* 根据 code 最大值返回学段
* 1~6 → 2
* 7~9 → 3
* 10~12→ 4
*/
public static int calcStage(List<Integer> codes) {
if (codes == null || codes.isEmpty()) {
throw new IllegalArgumentException("codes 不能为空");
}
int max = codes.stream().max(Integer::compareTo).get();
if (max <= 6) return 2;
if (max <= 9) return 3;
return 4;
}
public static String getUUID(){
return UUID.randomUUID().toString().replace("-","");
}
public static Integer parseIntNullable(Object val) {
return val == null ? null : Integer.parseInt(val.toString());
}
}
@@ -0,0 +1,53 @@
package com.yunhai.userbase.utils;
import com.yunhai.userbase.model.BasePageQuery;
import com.yunhai.userbase.model.BasePageResult;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
/**
* @author chenx
* @date 2026/7/21 21:54
* @description 分页辅助类
*/
public class PageUtils {
private PageUtils() {
}
/**
* 将 BasePageQuery 转换为 Spring Data JPA 的 Pageable。<br>
*
* @param query 前端分页参数(pageIndex 从 1 开始)
* @param sort 排序规则
* @return Pageable
*/
public static Pageable toPageable(BasePageQuery query, Sort sort) {
int page = Math.max(query.getPageIndex() - 1, 0);
int size = Math.max(query.getPageSize(), 1);
if(sort==null)sort=Sort.unsorted();
return PageRequest.of(page, size, sort);
}
/**
* 将 Spring Data 的 Page 装配为统一分页响应外壳 BasePageResult。<br>
* 页码约定:Spring Data 的 page 从 0 起,对外暴露的 pageIndex 从 1 起。<br>
*
* @param page 仓储/Service 返回的分页结果
* @param <T> 数据元素类型
* @return 统一分页响应
*/
public static <T> BasePageResult<T> toResult(Page<T> page) {
return new BasePageResult<T>(
page.getContent(),
(int) page.getTotalElements(),
page.getNumber() + 1,
page.getSize(),
page.getTotalPages(),
page.hasNext()
);
}
}
@@ -0,0 +1,43 @@
package com.yunhai.userbase.utils;
/**
* @author chenx
* @date 2025/12/29 9:47
* @description 权限bit类型许可帮助类
*/
public final class PermissionBits {
// 位掩码
public static final int CREATE = 1; // 0001
public static final int UPDATE = 2; // 0010
public static final int MOVE = 4; // 0100
public static final int DELETE = 8; // 1000
/** 布尔数组→二进制 */
public static byte pack(boolean canCreate, boolean canUpdate,
boolean canMove, boolean canDelete) {
int value = 0;
if (canCreate) value |= CREATE;
if (canUpdate) value |= UPDATE;
if (canMove) value |= MOVE;
if (canDelete) value |= DELETE;
return (byte) value; // 范围 0~15,够用
}
/** 二进制→布尔数组,返回顺序与上面一致 */
public static boolean[] unpack(byte packed) {
boolean[] arr = new boolean[4];
arr[0] = (packed & CREATE) != 0; // 新增
arr[1] = (packed & UPDATE) != 0; // 编辑
arr[2] = (packed & MOVE) != 0; // 移动
arr[3] = (packed & DELETE) != 0; // 删除
return arr;
}
/* 可选:校验是否拥有某权限 */
public static boolean has(int packed, int permission) {
return (packed & permission) == permission;
}
private PermissionBits() {}
}
@@ -0,0 +1,10 @@
package com.yunhai.userbase.utils;
public class RedisConstants {
private static final String CachePrefix = "yunhai.userbase.";
//学校目录树
public static final String SCH_ROOT_TREE =CachePrefix + "sch_root_tree";
}
@@ -0,0 +1,868 @@
package com.yunhai.userbase.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
*/
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* * @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
@@ -0,0 +1,78 @@
package com.yunhai.userbase.utils;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author chenx
* @date 2025/12/23 9:26
* @description 学科Code映射表 静态字典
*/
public final class SubjectDict {
/* ---------- 只读缓存 ---------- */
private static final Map<String, String> NAME_TO_CODE;
private static final Map<String, String> CODE_TO_NAME;
/* ---------- 初始化 ---------- */
static {
Map<String, String> tmp =new LinkedHashMap<>(32);
tmp.put("语文", "SUB01");
tmp.put("数学", "SUB02");
tmp.put("英语", "SUB03");
tmp.put("物理", "SUB04");
tmp.put("化学", "SUB05");
tmp.put("生物", "SUB06");
tmp.put("信息", "SUB7");
tmp.put("品德", "SUB8");
tmp.put("体育", "SUB9");
tmp.put("美术", "SUB10");
tmp.put("科学", "SUB11");
tmp.put("劳技", "SUB12");
tmp.put("历史", "SUB13");
tmp.put("地理", "SUB14");
tmp.put("政治", "SUB15");
tmp.put("音乐", "SUB604741");
tmp.put("心理", "SUB605021");
tmp.put("艺术", "SUB605101");
tmp.put("道法", "SUB605341");
tmp.put("社会", "SUB605371");
NAME_TO_CODE = Collections.unmodifiableMap(tmp);
CODE_TO_NAME = Collections.unmodifiableMap(invert(tmp));
}
private static Map<String, String> invert(Map<String, String> source) {
Map<String, String> target = new HashMap<>(source.size());
source.forEach((k, v) -> target.put(v, k));
return target;
}
/* ---------- 工具方法 ---------- */
/** 中文名称 -> code */
public static String toCode(String name) {
return NAME_TO_CODE.get(name);
}
/** code -> 中文名称 */
public static String toName(String code) {
return CODE_TO_NAME.get(code);
}
public static boolean containsName(String name) {
return NAME_TO_CODE.containsKey(name);
}
/** 只读视图 */
public static Map<String, String> allName2Code() {
return NAME_TO_CODE;
}
private SubjectDict() {
// 禁止实例化
}
}
@@ -0,0 +1,273 @@
package com.yunhai.userbase.utils;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.yunhai.userbase.model.StartEndDate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
public class TimeUtils {
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
private static SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static SimpleDateFormat format2 = new SimpleDateFormat("yyyy/MM/dd");
/*
* 毫秒转化时分秒毫秒
*/
public static String formatTime(Long ms) {
Integer ss = 1000;
Integer mi = ss * 60;
Integer hh = mi * 60;
Integer dd = hh * 24;
Long day = ms / dd;
Long hour = (ms - day * dd) / hh;
Long minute = (ms - day * dd - hour * hh) / mi;
Long second = (ms - day * dd - hour * hh - minute * mi) / ss;
Long milliSecond = ms - day * dd - hour * hh - minute * mi - second * ss;
StringBuffer sb = new StringBuffer();
if(day > 0) {
sb.append(day+"天");
}
if(hour > 0) {
sb.append(hour+"小时");
}
if(minute > 0) {
sb.append(minute+"分");
}
if(second > 0) {
sb.append(second+"秒");
}
/*if(milliSecond > 0) {
sb.append(milliSecond+"毫秒");
}*/
return sb.toString();
}
/**
* 获取过去几天内的日期数组
* @param intervals intervals天内
* @return 日期数组
*/
public static ArrayList<String> getDays(int intervals) {
ArrayList<String> pastDaysList = new ArrayList<>();
for (int i = intervals -1; i >= 0; i--) {
pastDaysList.add(getPastDate(i));
}
return pastDaysList;
}
/**
* 获取过去第几天的日期
* @param past
* @return
*/
public static String getPastDate(int past) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) - past);
Date today = calendar.getTime();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
String result = format.format(today);
return result;
}
/**
* 获取两个日期之间的天数
* @param start
* @param end
* @return
*/
public static List<String> getBetweenDates(Date start, Date end) {
List<String> days = new ArrayList<String>();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
Calendar tempStart = Calendar.getInstance();
tempStart.setTime(start);
Calendar tempEnd = Calendar.getInstance();
tempEnd.setTime(end);
tempEnd.add(Calendar.DATE, 0);// 日期加1(包含结束)
while (tempStart.before(tempEnd)) {
days.add(dateFormat.format(tempStart.getTime()));
tempStart.add(Calendar.DAY_OF_YEAR, 1);
}
} catch (Exception e) {
e.printStackTrace();
}
return days;
}
/**
* 根据俩日期,获取中间存在某周的周一、周日
*/
public static List<StartEndDate> getMonAndSunWeek(Date startDate, Date endDate) {
List<StartEndDate> list = new ArrayList<>();
Date tmp = startDate;
int ii = 0;
try {
while (tmp.before(endDate)) {
StartEndDate pram = new StartEndDate();
Calendar calendar = Calendar.getInstance();
if (ii == 0) {
String dateStr = format.format(startDate);
Date date = format.parse(dateStr);
calendar.setTime(date);
if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)
calendar.add(Calendar.DAY_OF_YEAR, -1);
} else {
calendar.setTime(tmp);
calendar.add(Calendar.DATE, 1);
}
int i = calendar.get(Calendar.DAY_OF_WEEK) - 1;
calendar.add(Calendar.DATE, -i + 1);
pram.setStartDate(format1.parse(format.format(calendar.getTime()) + " 00:00:00"));//周一
calendar.add(Calendar.DATE, 6);
Date dd = format1.parse(format.format(calendar.getTime()) + " 23:59:59");
pram.setEndDate(dd);//周日
tmp = dd;
// System.out.println("ii===========" + ii);
ii++;
list.add(pram);
}
} catch (Exception e) {
}
return list;
}
/**
* 根据俩日期,获取中间存在某月的月初、月末
*/
public static List<StartEndDate> getBeginAndLastMonth(Date startDate,Date endDate) {
List<StartEndDate> list = new ArrayList<>();
Date tmp = startDate;
int ii = 0;
try {
while (tmp.before(endDate)) {
StartEndDate pram = new StartEndDate();
Calendar calendar = Calendar.getInstance();
if (ii == 0) {
String dateStr = format.format(startDate);
Date date = format.parse(dateStr);
calendar.setTime(date);
} else {
calendar.setTime(tmp);
calendar.add(Calendar.DATE, 1);
}
calendar.add(Calendar.MONTH, 0);
calendar.set(Calendar.DAY_OF_MONTH,1);//设置为1号,当前日期既为本月第一天
pram.setStartDate(format1.parse(format.format(calendar.getTime()) + " 00:00:00"));//周一
//获取当前月最后一天
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
Date dd = format1.parse(format.format(calendar.getTime()) + " 23:59:59");
pram.setEndDate(dd);//月末
tmp = dd;
ii++;
list.add(pram);
}
} catch (Exception e) {
}
return list;
}
/**
* 根据俩日期,获取中间存
*/
public static List<StartEndDate> getBeiginAndLastTerm(Date startDate,Date endDate) {
List<StartEndDate> list = new ArrayList<>();
StartEndDate _pram = new StartEndDate();
_pram.setStartDate(startDate);
_pram.setEndDate(endDate);
list.add(_pram);
return list;
}
/**
* 指定某日期,在某段时间内是第几周
*/
public static Integer getWeek(String sDate1,String eDate1,String date1) {
DateTime sDate = DateUtil.parseDate(sDate1);
DateTime eDate = DateUtil.parseDate(eDate1);
DateTime date = DateUtil.parseDate(date1);
int week = 1;
if (DateUtil.isIn(date, sDate, eDate)) {
//获取某周的结束时间(周日)
Date endDateOfWeek = DateUtil.endOfWeek(sDate);
while (!DateUtil.isIn(date, sDate, endDateOfWeek)) {
++week;
//获取某周的结束时间(周日)
endDateOfWeek = DateUtil.endOfWeek(DateUtil.offsetDay(endDateOfWeek, 1));
}
return week;
}
return null;
}
/**
* 指定某日期,在某段时间内是第几月
*/
public static Integer getMonth(String sDate1,String eDate1,String date1) {
DateTime sDate = DateUtil.parseDate(sDate1);
DateTime eDate = DateUtil.parseDate(eDate1);
DateTime date = DateUtil.parseDate(date1);
int month = 1;
if (DateUtil.isIn(date, sDate, eDate)) {
//获取某月的结束时间(月末)
Date endDateOfMonth = DateUtil.endOfMonth(sDate);
while (!DateUtil.isIn(date, sDate, endDateOfMonth)) {
++month;
endDateOfMonth = DateUtil.endOfMonth(DateUtil.offsetDay(endDateOfMonth, 1));
}
return month;
}
return null;
}
/**
* 获取学年日期区间
*/
public static String getSchoolYear(Integer year) {
Calendar tempStart = Calendar.getInstance();
tempStart.set(year, 8, 1, 0, 0, 0);
Calendar tempStart2 =(Calendar)tempStart.clone();
tempStart2.add(Calendar.YEAR,1);
tempStart2.add(Calendar.DAY_OF_MONTH,-1);
return format2.format(tempStart.getTime()) + "-" + format2.format(tempStart2.getTime());
}
/**
* 获取自然年日期区间
*/
public static String getNatureYear(Integer year) {
Calendar tempStart = Calendar.getInstance();
tempStart.set(year, 0, 1, 0, 0, 0);
Calendar tempStart2 =(Calendar)tempStart.clone();
tempStart2.add(Calendar.YEAR,1);
tempStart2.add(Calendar.DATE,-1);
return format2.format(tempStart.getTime()) + "-" + format2.format(tempStart2.getTime());
}
}
@@ -0,0 +1,134 @@
package com.yunhai.userbase.utils;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
/**
* 统一校验工具:手机号、身份证、密码强度等基于正则的规则集中在此类维护。<br>
* 所有正则常量私有,只暴露语义化的校验方法,便于复用与统一调整。<br>
*
* @author chenx
* @date 2026/7/23
* @description 统一正则校验工具
*/
public final class ValidatorUtil {
/** 中国大陆手机号:1 开头,第二位 3-9,共 11 位。 */
private static final Pattern PHONE = Pattern.compile("^1[3-9]\\d{9}$");
/** 18 位居民身份证结构:6 位地址码 + 8 位出生日期(19/20xx) + 3 位顺序码 + 1 位校验位(数字或 X)。 */
private static final Pattern ID_CARD_18 = Pattern.compile(
"^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$");
/** 是否包含字母。 */
private static final Pattern HAS_LETTER = Pattern.compile("[a-zA-Z]");
/** 是否包含数字。 */
private static final Pattern HAS_DIGIT = Pattern.compile("\\d");
/** 是否包含符号(非字母、非数字、非空白的可见字符)。 */
private static final Pattern HAS_SYMBOL = Pattern.compile("[^a-zA-Z0-9\\s]");
/** 身份证 18 位加权因子。 */
private static final int[] ID_CARD_WEIGHT = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
/** 身份证校验位对照表(余数 0-10 对应的校验码)。 */
private static final char[] ID_CARD_CHECK = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};
/** 密码最小长度。 */
private static final int PASSWORD_MIN_LENGTH = 8;
/** 密码最大长度。 */
private static final int PASSWORD_MAX_LENGTH = 32;
private ValidatorUtil() {
}
/**
* 校验手机号是否合规。<br>
*
* @param phone 手机号
* @return true=合规
*/
public static boolean isPhone(String phone) {
return StringUtils.isNotBlank(phone) && PHONE.matcher(phone).matches();
}
/**
* 基础校验身份证是否合法:18 位结构正则 + 末位校验码。<br>
*
* @param idCard 身份证号
* @return true=合法
*/
public static boolean isIdCard(String idCard) {
if (StringUtils.isBlank(idCard) || !ID_CARD_18.matcher(idCard).matches()) {
return false;
}
int sum = 0;
for (int i = 0; i < ID_CARD_WEIGHT.length; i++) {
sum += (idCard.charAt(i) - '0') * ID_CARD_WEIGHT[i];
}
char expected = ID_CARD_CHECK[sum % 11];
char actual = Character.toUpperCase(idCard.charAt(17));
return expected == actual;
}
/**
* 校验密码强度,不满足时返回具体原因;满足时返回 null。<br>
* 规则:长度 8-32;必须同时包含字母、数字、符号;不允许出现连续自然数序列(如 1234、6789)。<br>
*
* @param password 明文密码
* @return 不合规原因,合规返回 null
*/
public static String checkPasswordStrength(String password) {
if (StringUtils.isBlank(password)) {
return "密码不能为空";
}
int length = password.length();
if (length < PASSWORD_MIN_LENGTH || length > PASSWORD_MAX_LENGTH) {
return "密码长度需为 " + PASSWORD_MIN_LENGTH + "-" + PASSWORD_MAX_LENGTH + " 位";
}
if (!HAS_LETTER.matcher(password).find()) {
return "密码必须包含字母";
}
if (!HAS_DIGIT.matcher(password).find()) {
return "密码必须包含数字";
}
if (!HAS_SYMBOL.matcher(password).find()) {
return "密码必须包含符号";
}
if (containsConsecutiveSequence(password)) {
return "密码不允许出现连续自然数序列(如 1234)";
}
return null;
}
/**
* 是否包含「连续自然数」序列:相邻数字逐位递增 1,且连续 4 位及以上(如 1234、6789)。<br>
* 仅拦截严格等差 +1 的序列;重复的(0000、8888)或非等差的(1121)均不被判定为连续自然数。
*
* @param s 待检测字符串(一般为密码)
* @return true=包含连续自然数序列
*/
private static boolean containsConsecutiveSequence(String s) {
int run = 1;
for (int i = 1; i < s.length(); i++) {
char c = s.charAt(i);
if (c < '0' || c > '9') {
run = 1;
continue;
}
char prev = s.charAt(i - 1);
if (prev >= '0' && prev <= '9' && (c - prev) == 1) {
run++;
if (run >= 4) {
return true;
}
} else {
run = 1;
}
}
return false;
}
}
@@ -0,0 +1,8 @@
package com.yunhai.userbase.utils;
public class WarnInfoUtil {
public static String MORE_RATIO_CONDITIONS(Integer key){
return "占比设置已达" + key + "%,已超100%,请重新输入!";
}
}