init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s
ci-userbase-service / build-and-push (push) Successful in 40s
This commit is contained in:
@@ -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%,请重新输入!";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user