This commit is contained in:
eibons
2025-08-15 20:58:57 +08:00
parent 694842a4a0
commit 49ac45ca05
730 changed files with 68015 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
package com.cool;
import com.cool.core.util.PathUtils;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.dromara.autotable.springboot.EnableAutoTable;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* CoolApplication - 应用程序的主类
* 该类配置并运行应用程序。
*/
@Slf4j
@EnableAutoTable // 开启自动建表
@EnableAsync // 开启异步处理
@EnableCaching // 开启缓存
@SpringBootApplication
@MapperScan("com.cool.**.mapper") // 扫描指定包中的MyBatis映射器
public class CoolApplication {
private static volatile ConfigurableApplicationContext context;
private static ClassLoader mainThreadClassLoader;
public static void main(String[] args) {
mainThreadClassLoader = Thread.currentThread().getContextClassLoader();
context = SpringApplication.run(CoolApplication.class, args);
}
/**
* 通过关闭当前上下文并启动新上下文来重启应用程序。
*/
public static void restart(List<String> javaPathList) {
// 从当前上下文获取应用程序参数
ApplicationArguments args = context.getBean(ApplicationArguments.class);
// 创建新线程来重启应用程序
Thread thread = new Thread(() -> {
try {
// 关闭当前应用程序上下文
context.close();
// 等待上下文完全关闭
while (context.isActive()) {
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 加载动态生成的代码
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
javaPathList.forEach(javaPath -> {
try {
classLoader.loadClass(PathUtils.getClassName(javaPath));
} catch (ClassNotFoundException e) {
log.error("loadClassErr {}", javaPath, e);
}
});
// 使用相同的参数运行Spring Boot应用程序并设置上下文
context = SpringApplication.run(CoolApplication.class, args.getSourceArgs());
});
// 设置线程的上下文类加载器
thread.setContextClassLoader(mainThreadClassLoader);
// 确保线程不是守护线程
thread.setDaemon(false);
// 启动线程
thread.start();
}
}

View File

@@ -0,0 +1,17 @@
package com.cool;
import com.cool.core.annotation.TokenIgnore;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequiredArgsConstructor
public class Welcome {
@RequestMapping("/")
@TokenIgnore
public String welcome() {
return "welcome";
}
}

View File

@@ -0,0 +1,12 @@
package com.cool.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CoolPlugin {
String value() default "";
}

View File

@@ -0,0 +1,34 @@
package com.cool.core.annotation;
import java.lang.annotation.*;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 自定义路由注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping
public @interface CoolRestController {
@AliasFor(annotation = RequestMapping.class)
String name() default "";
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
String[] api() default {};
/**
* 如前缀: /admin/goods/searchKeyword
* 没指定该字段 cname="searchKeyword",
* 按规则是解析为: /admin/goods/search/keyword
* 前端和node版本已经定义为 searchKeyword,没按规则解析,使用该字段自定义规则 进行兼容
* com.cool.core.request.prefix.AutoPrefixUrlMapping#getCName(java.lang.Class, java.lang.String)
*/
String cname() default "";
}

View File

@@ -0,0 +1,14 @@
package com.cool.core.annotation;
import com.cool.core.enums.AdminComponentsEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EpsField {
String component() default AdminComponentsEnum.INPUT;
}

View File

@@ -0,0 +1,12 @@
package com.cool.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreRecycleData {
String value() default "";
}

View File

@@ -0,0 +1,12 @@
package com.cool.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NoRepeatSubmit {
long expireTime() default 2000; // 默认2秒过期时间,单位毫秒
}

View File

@@ -0,0 +1,15 @@
package com.cool.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 忽略Token验证
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenIgnore {
String[] value() default {};
}

View File

@@ -0,0 +1,39 @@
package com.cool.core.aop;
import com.cool.core.annotation.NoRepeatSubmit;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.lock.CoolLock;
import com.cool.core.util.CoolSecurityUtil;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@Component
@RequiredArgsConstructor
public class NoRepeatSubmitAspect {
private final CoolLock coolLock;
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest();
String key = request.getRequestURI() + ":" + CoolSecurityUtil.getCurrentUserId();
// 加锁
CoolPreconditions.check(!coolLock.tryLock(key, Duration.ofMillis(noRepeatSubmit.expireTime())), "请勿重复操作");
try {
return joinPoint.proceed();
} finally {
// 移除锁
coolLock.unlock(key);
}
}
}

View File

@@ -0,0 +1,277 @@
package com.cool.core.base;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.lang.Editor;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.core.enums.QueryModeEnum;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.request.CrudOption;
import com.cool.core.request.PageResult;
import com.cool.core.request.R;
import com.mybatisflex.core.paginate.Page;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 控制层基类
*
* @param <S>
* @param <T>
*/
public abstract class BaseController<S extends BaseService<T>, T extends BaseEntity<T>> {
@Getter
@Autowired
protected S service;
protected Class<T> entityClass;
protected final String COOL_PAGE_OP = "COOL_PAGE_OP";
protected final String COOL_LIST_OP = "COOL_LIST_OP";
protected final String COOL_INFO_OP = "COOL_INFO_OP";
private final ThreadLocal<CrudOption<T>> pageOption = new ThreadLocal<>();
private final ThreadLocal<CrudOption<T>> listOption = new ThreadLocal<>();
private final ThreadLocal<CrudOption<T>> infoOption = new ThreadLocal<>();
private final ThreadLocal<JSONObject> requestParams = new ThreadLocal<>();
@ModelAttribute
protected void preHandle(HttpServletRequest request,
@RequestAttribute JSONObject requestParams) {
String requestPath = ((ServletRequestAttributes) Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest().getRequestURI();
if (!requestPath.endsWith("/page") && !requestPath.endsWith("/list")
&& !requestPath.endsWith("/info")) {
// 非page或list不执行
return;
}
this.pageOption.set(new CrudOption<>(requestParams));
this.listOption.set(new CrudOption<>(requestParams));
this.infoOption.set(new CrudOption<>(requestParams));
this.requestParams.set(requestParams);
init(request, requestParams);
request.setAttribute(COOL_PAGE_OP, this.pageOption.get());
request.setAttribute(COOL_LIST_OP, this.listOption.get());
request.setAttribute(COOL_INFO_OP, this.infoOption.get());
removeThreadLocal();
}
/**
* 手动移除变量
*/
private void removeThreadLocal() {
this.listOption.remove();
this.pageOption.remove();
this.requestParams.remove();
}
public CrudOption<T> createOp() {
return new CrudOption<>(this.requestParams.get());
}
public void setInfoOption(CrudOption<T> infoOption) {
this.infoOption.set(infoOption);
}
public void setListOption(CrudOption<T> listOption) {
this.listOption.set(listOption);
}
public void setPageOption(CrudOption<T> pageOption) {
this.pageOption.set(pageOption);
}
protected abstract void init(HttpServletRequest request, JSONObject requestParams);
/**
* 新增
* <p>
* // * @param t 实体对象
*/
@Operation(summary = "新增", description = "新增信息,对应后端的实体类")
@PostMapping("/add")
protected R add(@RequestAttribute() JSONObject requestParams) {
String body = requestParams.getStr("body");
if (JSONUtil.isTypeJSONArray(body)) {
JSONArray array = JSONUtil.parseArray(body);
return R.ok(Dict.create()
.set("id", service.addBatch(requestParams, array.toList(currentEntityClass()))));
} else {
return R.ok(Dict.create().set("id",
service.add(requestParams, requestParams.toBean(currentEntityClass()))));
}
}
/**
* 删除
*
* @param params 请求参数 ids 数组 或者按","隔开
*/
@Operation(summary = "删除", description = "支持批量删除 请求参数 ids 数组 或者按\",\"隔开")
@PostMapping("/delete")
protected R delete(HttpServletRequest request, @RequestBody Map<String, Object> params,
@RequestAttribute() JSONObject requestParams) {
service.delete(requestParams, Convert.toLongArray(getIds(params)));
return R.ok();
}
/**
* 修改
*
* @param t 修改对象
*/
@Operation(summary = "修改", description = "根据ID修改")
@PostMapping("/update")
protected R update(@RequestBody T t, @RequestAttribute() JSONObject requestParams) {
Long id = t.getId();
JSONObject info = JSONUtil.parseObj(JSONUtil.toJsonStr(service.getById(id)));
requestParams.forEach(info::set);
info.set("updateTime", new Date());
service.update(requestParams, JSONUtil.toBean(info, currentEntityClass()));
return R.ok();
}
/**
* 信息
*
* @param id ID
*/
@Operation(summary = "信息", description = "根据ID查询单个信息")
@GetMapping("/info")
protected R<T> info(@RequestAttribute() JSONObject requestParams,
@RequestParam() Long id,
@RequestAttribute(COOL_INFO_OP) CrudOption<T> option) {
T info = (T) service.info(requestParams, id);
invokerTransform(option, info);
return R.ok(info);
}
/**
* 列表查询
*
* @param requestParams 请求参数
*/
@Operation(summary = "查询", description = "查询多个信息")
@PostMapping("/list")
protected R<List<T>> list(@RequestAttribute() JSONObject requestParams,
@RequestAttribute(COOL_LIST_OP) CrudOption<T> option) {
QueryModeEnum queryModeEnum = option.getQueryModeEnum();
List list = (List) switch (queryModeEnum) {
case ENTITY_WITH_RELATIONS -> service.listWithRelations(requestParams, option.getQueryWrapper(currentEntityClass()));
case CUSTOM -> transformList(service.list(requestParams, option.getQueryWrapper(currentEntityClass()), option.getAsType()), option.getAsType());
default -> service.list(requestParams, option.getQueryWrapper(currentEntityClass()));
};
invokerTransform(option, list);
return R.ok(list);
}
/**
* 分页查询
*
* @param requestParams 请求参数
*/
@Operation(summary = "分页", description = "分页查询多个信息")
@PostMapping("/page")
protected R<PageResult<T>> page(@RequestAttribute() JSONObject requestParams,
@RequestAttribute(COOL_PAGE_OP) CrudOption<T> option) {
Integer page = requestParams.getInt("page", 1);
Integer size = requestParams.getInt("size", 20);
QueryModeEnum queryModeEnum = option.getQueryModeEnum();
Object obj = switch (queryModeEnum) {
case ENTITY_WITH_RELATIONS -> service.pageWithRelations(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()));
case CUSTOM -> transformPage(service.page(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()), option.getAsType()), option.getAsType());
default -> service.page(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()));
};
Page pageResult = (Page) obj;
invokerTransform(option, pageResult.getRecords());
return R.ok(pageResult(pageResult));
}
/**
* 转换参数,组装数据
*/
private void invokerTransform(CrudOption<T> option, Object obj) {
if (ObjUtil.isNotEmpty(option.getTransform())) {
if (obj instanceof List) {
((List)obj).forEach(o -> {
option.getTransform().apply(o);
});
} else {
option.getTransform().apply(obj);
}
}
}
/**
* 分页结果
*
* @param page 分页返回数据
*/
protected PageResult<T> pageResult(Page<T> page) {
return PageResult.of(page);
}
public Class<T> currentEntityClass() {
if (entityClass != null) {
return this.entityClass;
}
// 使用 获取泛型参数类型
Type type = TypeUtil.getTypeArgument(this.getClass(), 1); // 获取第二个泛型参数
if (type instanceof Class<?>) {
entityClass = (Class<T>) type;
return entityClass;
}
throw new IllegalStateException("Unable to determine entity class type");
}
protected List<Long> getIds(Map<String, Object> params) {
Object ids = params.get("ids");
CoolPreconditions.checkEmpty(ids, "ids 参数错误");
if (!(ids instanceof ArrayList)) {
ids = ids.toString().split(",");
}
return Convert.toList(Long.class, ids);
}
/**
* 适用于自定义返回值为 mapmap 的key为数据库字段转驼峰命名
*/
protected List transformList(List records, Class<?> asType) {
if (ObjUtil.isEmpty(asType) || !Map.class.isAssignableFrom(asType)) {
return records;
}
List<Map> list = new ArrayList<>();
Editor<String> keyEditor = property -> StrUtil.toCamelCase(property);
records.forEach(o ->
list.add(BeanUtil.beanToMap(o, new HashMap(), false, keyEditor)));
return list;
}
protected Page transformPage(Page page, Class<?> asType) {
page.setRecords(transformList(page.getRecords(), asType));
return page;
}
}

View File

@@ -0,0 +1,38 @@
package com.cool.core.base;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.core.activerecord.Model;
import com.mybatisflex.core.query.QueryWrapper;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import java.io.Serializable;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Ignore;
/**
* 基础实体类
*/
@Getter
@Setter
public abstract class BaseEntity<T extends Model<T>> extends Model<T> implements Serializable {
@Id(keyType = KeyType.Auto, comment = "ID")
protected Long id;
@Column(onInsertValue = "now()")
@ColumnDefine(comment = "创建时间")
protected Date createTime;
@Column(onInsertValue = "now()", onUpdateValue = "now()")
@ColumnDefine(comment = "更新时间")
protected Date updateTime;
@Ignore
@Column(ignore = true)
@JsonIgnore
private QueryWrapper queryWrapper;
}

View File

@@ -0,0 +1,175 @@
package com.cool.core.base;
import cn.hutool.json.JSONObject;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.service.IService;
import java.util.List;
/**
* 基础service类
*
* @param <T> 实体
*/
public interface BaseService<T> extends IService<T> {
/**
* 新增
*
* @param entity 对应的实体
*/
Long add(T entity);
/**
* 新增
*
* @param requestParams 请求参数
* @param entity 对应的实体
* @return ID
*/
Object add(JSONObject requestParams, T entity);
/**
* 批量添加
*
* @param requestParams 请求参数
* @param entitys 请求参数
* @return ID 集合
*/
Object addBatch(JSONObject requestParams, List<T> entitys);
/**
* 删除, 支持单个或者批量删除
*
* @param ids ID数组
*/
boolean delete(Long... ids);
/**
* 多个删除,带请求参数
*
* @param requestParams 请求参数
* @param ids ID数组
*/
boolean delete(JSONObject requestParams, Long... ids);
/**
* 更新
*
* @param entity 实体
*/
boolean update(T entity);
/**
* 更新
*
* @param requestParams 请求参数
* @param entity 实体
*/
boolean update(JSONObject requestParams, T entity);
/**
* 查询所有
*
* @param requestParams 请求参数
* @param queryWrapper 查询条件
* @return 列表信息
*/
Object list(JSONObject requestParams, QueryWrapper queryWrapper);
/**
* 查询所有
*
* @param requestParams 请求参数
* @param queryWrapper 查询条件
* @return 列表信息
*/
<R> List<R> list(JSONObject requestParams, QueryWrapper queryWrapper, Class<R> asType);
/**
* 查询所有
* 带关联查询
* @param requestParams 请求参数
* @param queryWrapper 查询条件
* @return 列表信息
*/
Object listWithRelations(JSONObject requestParams, QueryWrapper queryWrapper);
/**
* 分页查询
*
* @param requestParams 请求参数
* @param page 分页信息
* @param queryWrapper 查询条件
* @return 分页信息
*/
Object page(JSONObject requestParams, Page<T> page, QueryWrapper queryWrapper);
/**
* 分页查询
*
* @param requestParams 请求参数
* @param page 分页信息
* @param queryWrapper 查询条件
* @return 分页信息
*/
<R> Page<R> page(JSONObject requestParams, Page page, QueryWrapper queryWrapper, Class<R> asType);
/**
* 分页查询
* 带关联查询
* @param requestParams 请求参数
* @param page 分页信息
* @param queryWrapper 查询条件
* @return 分页信息
*/
Object pageWithRelations(JSONObject requestParams, Page<T> page, QueryWrapper queryWrapper);
/**
* 查询信息
*
* @param id ID
*/
Object info(Long id);
/**
* 查询信息
*
* @param requestParams 请求参数
* @param id ID
*/
Object info(JSONObject requestParams, Long id);
/**
* 修改之后
*
* @param requestParams 请求参数
* @param t 对应实体
*/
void modifyAfter(JSONObject requestParams, T t);
/**
* 修改之后
*
* @param requestParams 请求参数
* @param t 对应实体
* @param type 修改类型
*/
void modifyAfter(JSONObject requestParams, T t, ModifyEnum type);
/**
* 修改之前
*
* @param requestParams 请求参数
* @param t 对应实体
*/
void modifyBefore(JSONObject requestParams, T t);
/**
* 修改之前
*
* @param requestParams 请求参数
* @param t 对应实体
* @param type 修改类型
*/
void modifyBefore(JSONObject requestParams, T t, ModifyEnum type);
}

View File

@@ -0,0 +1,137 @@
package com.cool.core.base;
import cn.hutool.json.JSONObject;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 基础service实现类
*
* @param <M> Mapper 类
* @param <T> 实体
*/
public class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseEntity<T>> extends
ServiceImpl<M, T>
implements BaseService<T> {
@Override
public Long add(T entity) {
mapper.insertSelective(entity);
return entity.getId();
}
@Override
public Object add(JSONObject requestParams, T entity) {
this.modifyBefore(requestParams, entity, ModifyEnum.ADD);
this.add(entity);
this.modifyAfter(requestParams, entity, ModifyEnum.ADD);
return entity.getId();
}
@Override
public Object addBatch(JSONObject requestParams, List<T> entitys) {
this.modifyBefore(requestParams, null, ModifyEnum.ADD);
List<Long> ids = new ArrayList<>();
entitys.forEach(e -> ids.add(this.add(e)));
requestParams.set("ids", ids);
this.modifyAfter(requestParams, null, ModifyEnum.ADD);
return ids;
}
@Override
public boolean delete(Long... ids) {
return mapper.deleteBatchByIds(Arrays.asList(ids)) > 0;
}
@Override
public boolean delete(JSONObject requestParams, Long... ids) {
this.modifyBefore(requestParams, null, ModifyEnum.DELETE);
boolean flag = this.delete(ids);
if (flag) {
this.modifyAfter(requestParams, null, ModifyEnum.DELETE);
}
return flag;
}
@Override
public boolean update(T entity) {
return mapper.update(entity) > 0;
}
@Override
public boolean update(JSONObject requestParams, T entity) {
this.modifyBefore(requestParams, entity, ModifyEnum.UPDATE);
boolean flag = this.update(entity);
if (flag) {
this.modifyAfter(requestParams, entity, ModifyEnum.UPDATE);
}
return flag;
}
@Override
public Object list(JSONObject requestParams, QueryWrapper queryWrapper) {
return this.list(queryWrapper);
}
@Override
public <R> List<R> list(JSONObject requestParams, QueryWrapper queryWrapper, Class<R> asType) {
return mapper.selectListByQueryAs(queryWrapper, asType);
}
@Override
public Object listWithRelations(JSONObject requestParams, QueryWrapper queryWrapper) {
return mapper.selectListWithRelationsByQuery(queryWrapper);
}
@Override
public Object page(JSONObject requestParams, Page<T> page, QueryWrapper queryWrapper) {
return this.page(page, queryWrapper);
}
@Override
public <R> Page<R> page(JSONObject requestParams, Page page, QueryWrapper queryWrapper,
Class<R> asType) {
return mapper.paginateAs(page, queryWrapper, asType);
}
@Override
public Object pageWithRelations(JSONObject requestParams, Page<T> page,
QueryWrapper queryWrapper) {
return mapper.paginateWithRelations(page, queryWrapper);
}
@Override
public Object info(JSONObject requestParams, Long id) {
return info(id);
}
@Override
public Object info(Long id) {
return mapper.selectOneById(id);
}
@Override
public void modifyAfter(JSONObject requestParams, T t) {
}
@Override
public void modifyAfter(JSONObject requestParams, T t, ModifyEnum type) {
modifyAfter(requestParams, t);
}
@Override
public void modifyBefore(JSONObject requestParams, T t) {
}
@Override
public void modifyBefore(JSONObject requestParams, T t, ModifyEnum type) {
}
}

View File

@@ -0,0 +1,13 @@
package com.cool.core.base;
/**
* 修改枚举
*/
public enum ModifyEnum {
// 新增
ADD,
// 修改
UPDATE,
// 删除
DELETE
}

View File

@@ -0,0 +1,16 @@
package com.cool.core.base;
import com.mybatisflex.core.activerecord.Model;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
/** 租户ID实体类 */
@Getter
@Setter
public class TenantEntity<T extends Model<T>> extends BaseEntity<T> {
@Index
@ColumnDefine(comment = "租户id")
protected Long tenantId;
}

View File

@@ -0,0 +1,52 @@
package com.cool.core.base.service;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.TypeUtil;
import com.cool.core.util.SpringContextUtils;
import com.mybatisflex.core.BaseMapper;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class MapperProviderService {
private Map<Class<?>, BaseMapper<?>> mapperMap;
/**
* 初始化mapperMapkey 为entityClassvalue 为 mapper
*/
private void init() {
// 获取所有BaseMapper类型的Bean
Map<String, BaseMapper> beansOfType = SpringContextUtils.getBeansOfType(BaseMapper.class);
mapperMap = new HashMap<>();
for (BaseMapper mapper : beansOfType.values()) {
// 通过反射获取泛型参数,即实体类
Class<?> entityClass = getGenericType(mapper);
if (entityClass != null) {
mapperMap.put(entityClass, mapper);
}
}
}
/**
* 通过entity类获取 对应的mapper接口
*/
public <T> BaseMapper<T> getMapperByEntityClass(Class<T> entityClass) {
if (ObjUtil.isEmpty(mapperMap)) {
init();
}
return (BaseMapper<T>) mapperMap.get(entityClass);
}
/**
* 获取mapper对应的entity对象
*/
private Class<?> getGenericType(BaseMapper<?> mapper) {
// 使用 获取泛型参数类型
Type[] types = mapper.getClass().getGenericInterfaces();
Type typeArgument = TypeUtil.getTypeArgument(types[0], 0);
return ObjUtil.isEmpty(typeArgument) ? null : (Class<?>) typeArgument;
}
}

View File

@@ -0,0 +1,180 @@
package com.cool.core.cache;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.cool.core.util.ConvertUtil;
import jakarta.annotation.PostConstruct;
import java.time.Duration;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.cache.CacheType;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.stereotype.Component;
/**
* 缓存工具类
*/
@EnableCaching
@Configuration
@Component
@RequiredArgsConstructor
public class CoolCache {
// 缓存类型
@Value("${spring.cache.type}")
private String type;
// redis
public RedisCacheWriter redisCache;
private Cache cache;
@Value("${cool.cacheName}")
private String cacheName;
private final static String NULL_VALUE = "@_NULL_VALUE$@";
final private CacheManager cacheManager;
@PostConstruct
private void init() {
cache = cacheManager.getCache(cacheName);
this.type = type.toLowerCase();
assert cache != null : "Cache not found: " + cacheName; // Ensure cache is not null
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
redisCache = (RedisCacheWriter) cache.getNativeCache();
}
}
/**
* 数据来源
*/
public interface ToCacheData {
Object apply();
}
/**
* 删除缓存
*
* @param keys 一个或多个key
*/
public void del(String... keys) {
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
Arrays.stream(keys).forEach(o -> cache.evict(o));
}
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
Arrays.stream(keys).forEach(key -> redisCache.remove(cacheName, key.getBytes()));
}
}
/**
* 普通缓存获取
*
* @param key 键
*/
public Object get(String key) {
Object ifNullValue = getIfNullValue(key);
if (ObjUtil.equals(ifNullValue, NULL_VALUE)) {
return null;
}
return ifNullValue;
}
/**
* 普通缓存获取
*
* @param key 键
*/
public Object get(String key, Duration duration, ToCacheData toCacheData) {
Object ifNullValue = getIfNullValue(key);
if (ObjUtil.equals(ifNullValue, NULL_VALUE)) {
return null;
}
if (ObjUtil.isEmpty(ifNullValue)) {
Object obj = toCacheData.apply();
set(key, obj, duration.toSeconds());
return obj;
}
return ifNullValue;
}
private Object getIfNullValue(String key) {
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
Cache.ValueWrapper valueWrapper = cache.get(key);
if (valueWrapper != null) {
return valueWrapper.get(); // 获取实际的缓存值
}
}
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
byte[] bytes = redisCache.get(cacheName, key.getBytes());
if (bytes != null && bytes.length > 0) {
return ConvertUtil.toObject(bytes);
}
}
return null;
}
/**
* 获得对象
*
* @param key 键
* @param valueType 值类型
*/
public <T> T get(String key, Class<T> valueType) {
Object result = get(key);
if (result != null && JSONUtil.isTypeJSONObject(result.toString())) {
return JSONUtil.parseObj(result).toBean(valueType);
}
return result != null ? (T) result : null;
}
/**
* 获得缓存类型
*/
public String getMode() {
return this.type;
}
/**
* 获得原生缓存实例
*/
public Object getMetaCache() {
return this.cache;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
*/
public void set(String key, Object value) {
set(key, value, 0);
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param ttl 时间(秒) time要大于0 如果time小于等于0 将设置无限期
*/
public void set(String key, Object value, long ttl) {
if (ObjUtil.isNull(value)) {
value = NULL_VALUE;
}
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
// 放入缓存
cache.put(key, value);
} else if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
redisCache.put(cacheName, key.getBytes(), ObjectUtil.serialize(value),
java.time.Duration.ofSeconds(ttl));
}
}
}

View File

@@ -0,0 +1,108 @@
package com.cool.core.code;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
/**
* 代码生成器
*/
@Component
public class CodeGenerator {
private TemplateEngine templateEngine;
private String baseSrcPath;
private String baseResPath;
@PostConstruct
public void init() {
templateEngine = coolTemplateEngine();
baseSrcPath = System.getProperty("user.dir") + "/src/main/java/com/cool/modules/";
baseResPath = System.getProperty("user.dir") + "/src/main/resources/";
}
public TemplateEngine coolTemplateEngine() {
return TemplateUtil.createEngine(
new TemplateConfig("cool/code", TemplateConfig.ResourceMode.CLASSPATH));
}
private String filePath(CodeModel codeModel, String type) {
if (type.equals("controller")) {
return StrUtil.isEmpty(codeModel.getSubModule())
? baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getType()
.value()
: baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getType()
.value() + "/"
+ codeModel.getSubModule();
}
if (type.equals("xmlMapper")) {
return StrUtil.isEmpty(codeModel.getSubModule()) ? baseResPath + "mapper/"
+ codeModel.getModule()
: baseResPath + "mapper/" + codeModel.getModule() + "/" + codeModel.getSubModule();
}
return StrUtil.isEmpty(codeModel.getSubModule()) ? baseSrcPath + codeModel.getModule() + "/"
+ type
: baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getSubModule();
}
/**
* 生成Mapper
*
* @param codeModel 代码模型
*/
public void mapper(CodeModel codeModel) {
Template template = templateEngine.getTemplate("/mapper/interface.th");
String result = template.render(Dict.parse(codeModel));
FileWriter writer = new FileWriter(
filePath(codeModel, "mapper") + "/" + codeModel.getEntity() + "Mapper.java");
writer.write(result);
}
/**
* 生成Service
*
* @param codeModel 代码模型
*/
public void service(CodeModel codeModel) {
Template interfaceTemplate = templateEngine.getTemplate("/service/interface.th");
String interfaceResult = interfaceTemplate.render(Dict.parse(codeModel));
FileWriter interfaceWriter = new FileWriter(
filePath(codeModel, "service") + "/" + codeModel.getEntity() + "Service.java");
interfaceWriter.write(interfaceResult);
Template template = templateEngine.getTemplate("/service/impl.th");
String result = template.render(Dict.parse(codeModel));
FileWriter writer = new FileWriter(
filePath(codeModel, "service") + "/impl/" + codeModel.getEntity() + "ServiceImpl.java");
writer.write(result);
}
/**
* 生成Controller
*
* @param codeModel 代码模型
*/
public void controller(CodeModel codeModel) {
Template template = templateEngine.getTemplate("controller.th");
System.out.println(codeModel.getType().value());
Dict data = Dict.create().set("upperType", StrUtil.upperFirst(codeModel.getType().value()))
.set("url",
"/" + codeModel.getType() + "/" + StrUtil.toUnderlineCase(codeModel.getEntity())
.replace("_", "/"));
data.putAll(Dict.parse(codeModel));
data.set("type", codeModel.getType().value());
String result = template.render(data);
FileWriter writer = new FileWriter(filePath(codeModel, "controller") + "/"
+ StrUtil.upperFirst(codeModel.getType().value()) + codeModel.getEntity()
+ "Controller.java");
writer.write(result);
}
}

View File

@@ -0,0 +1,36 @@
package com.cool.core.code;
import lombok.Data;
/**
* 代码模型
*/
@Data
public class CodeModel {
/**
* 类型 后台还是对外的接口 admin app
*/
private CodeTypeEnum type;
/**
* 名称
*/
private String name;
/**
* 模块
*/
private String module;
/**
* 子模块
*/
private String subModule;
/**
* 实体类
*/
private String entity;
public void setEntity(Class entity) {
this.entity = entity.getSimpleName().replace("Entity", "");
}
}

View File

@@ -0,0 +1,21 @@
package com.cool.core.code;
/**
* 代码类型
*/
public enum CodeTypeEnum {
ADMIN("admin", "后端接口"), APP("app", "对外接口");
private String value;
private String des;
CodeTypeEnum(String value, String des) {
this.value = value;
this.des = des;
}
public String value() {
return this.value;
}
}

View File

@@ -0,0 +1,23 @@
package com.cool.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.context.annotation.Configuration;
/**
* cool的配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "cool")
public class CoolProperties {
// 是否自动导入数据
private Boolean initData = false;
// token配置
@NestedConfigurationProperty
private TokenProperties token;
// 文件配置
@NestedConfigurationProperty
private FileProperties file;
}

View File

@@ -0,0 +1,40 @@
package com.cool.core.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Locale;
import org.springdoc.core.customizers.SpringDocCustomizers;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.SpringDocProviders;
import org.springdoc.core.service.AbstractRequestService;
import org.springdoc.core.service.GenericResponseService;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.service.OperationService;
import org.springdoc.webmvc.api.OpenApiResource;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* 自定义 OpenApiResource
*/
@Component
@ConditionalOnProperty(
name = "springdoc.api-docs.enabled",
havingValue = "true"
)
public class CustomOpenApiResource extends OpenApiResource {
public CustomOpenApiResource(ObjectFactory<OpenAPIService> openAPIBuilderObjectFactory, AbstractRequestService requestBuilder, GenericResponseService responseBuilder, OperationService operationParser, SpringDocConfigProperties springDocConfigProperties, SpringDocProviders springDocProviders, SpringDocCustomizers springDocCustomizers) {
super("springdocDefault", openAPIBuilderObjectFactory, requestBuilder, responseBuilder, operationParser, springDocConfigProperties, springDocProviders, springDocCustomizers);
}
@Override
protected String getServerUrl(HttpServletRequest request, String apiDocsUrl) {
return "";
}
public byte[] getOpenApiJson() throws JsonProcessingException {
return writeJsonValue(getOpenApi(Locale.getDefault()));
}
}

View File

@@ -0,0 +1,28 @@
package com.cool.core.config;
/**
* 文件模式
*/
public enum FileModeEnum {
LOCAL("local", "local", "本地"), CLOUD("cloud", "oss", "云存储"), OTHER("other", "other", "其他");
private String value;
private String type;
private String des;
FileModeEnum(String value, String type, String des) {
this.value = value;
this.type = type;
this.des = des;
}
public String value() {
return this.value;
}
public String type() {
return this.type;
}
}

View File

@@ -0,0 +1,22 @@
package com.cool.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.context.annotation.Configuration;
/**
* 文件
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "cool.file")
public class FileProperties {
// 上传模式
private FileModeEnum mode;
// 上传类型
private String type;
// 本地文件上传
@NestedConfigurationProperty
private LocalFileProperties local;
}

View File

@@ -0,0 +1,67 @@
package com.cool.core.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import java.io.IOException;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Configuration
public class JacksonConfig {
@Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
final Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
final ObjectMapper objectMapper = builder.build();
SimpleModule simpleModule = new SimpleModule();
// Long,BigInteger 转为 String 防止 js 丢失精度
simpleModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
simpleModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
simpleModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
objectMapper.registerModule(simpleModule);
// 配置日期格式为 yyyy-MM-dd HH:mm:ss
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
objectMapper.setDateFormat(dateFormat);
return new MappingJackson2HttpMessageConverter(objectMapper);
}
/**
* 超出 JS 最大最小值 处理
*/
@JacksonStdImpl
public static class BigNumberSerializer extends NumberSerializer {
/**
* 根据 JS Number.MAX_SAFE_INTEGER 与 Number.MIN_SAFE_INTEGER 得来
*/
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
/**
* 提供实例
*/
public static final BigNumberSerializer INSTANCE = new BigNumberSerializer(Number.class);
public BigNumberSerializer(Class<? extends Number> rawType) {
super(rawType);
}
@Override
public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 超出范围 序列化位字符串
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
super.serialize(value, gen, provider);
} else {
gen.writeString(value.toString());
}
}
}
}

View File

@@ -0,0 +1,30 @@
package com.cool.core.config;
import com.cool.core.util.PathUtils;
import java.io.File;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 文件
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "cool.file.local")
public class LocalFileProperties {
// 跟域名
private String baseUrl;
private String uploadPath = "assets/public/upload";
public String getAbsoluteUploadFolder() {
if (!PathUtils.isAbsolutePath(uploadPath)) {
// 相对路径
return System.getProperty("user.dir") + File.separator + uploadPath;
}
// 绝对路径
return uploadPath;
}
}

View File

@@ -0,0 +1,13 @@
package com.cool.core.config;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LogDiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.warn("logTaskExecutor 当前已超过线程池最大队列容量,拒绝策略为丢弃该线程 {}", r.toString());
}
}

View File

@@ -0,0 +1,28 @@
package com.cool.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "cool.log")
public class LogProperties {
/**
* 请求参数最大字节,超过请求参数不记录
*/
private int maxByteLength;
/**
* 核心线程数的倍数
*/
private int corePoolSizeMultiplier;
/**
* 最大线程数的倍数
*/
private int maxPoolSizeMultiplier;
/**
* 队列容量的倍数
*/
private int queueCapacityMultiplier;
}

View File

@@ -0,0 +1,27 @@
package com.cool.core.config;
import com.cool.core.tenant.CoolTenantFactory;
import com.mybatisflex.core.FlexGlobalConfig;
import com.mybatisflex.core.tenant.TenantFactory;
import com.mybatisflex.spring.boot.MyBatisFlexCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisFlexConfiguration implements MyBatisFlexCustomizer {
@Override
public void customize(FlexGlobalConfig globalConfig) {
// 我们可以在这里进行一些列的初始化配置
// 指定多租户列的列名
FlexGlobalConfig.getDefaultConfig().setTenantColumn("tenant_id");
}
@Bean
@ConditionalOnProperty(name = "cool.multi-tenant.enable", havingValue = "true")
public TenantFactory tenantFactory(){
return new CoolTenantFactory();
}
}

View File

@@ -0,0 +1,24 @@
package com.cool.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 文件
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "cool.file.oss")
public class OssFileProperties {
// accessKeyId
private String accessKeyId;
// accessKeySecret
private String accessKeySecret;
// 文件空间
private String bucket;
// 地址
private String endpoint;
// 超时时间
private Long timeout;
}

View File

@@ -0,0 +1,57 @@
package com.cool.core.config;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.util.Map;
@Data
public class PluginJson {
/**
* 插件名称
*/
private String name;
/**
* 插件标识
*/
private String key;
/**
* 插件钩子比如替换系统的上传组件upload
*/
private String hook;
/**
* 版本号
*/
private String version;
/**
* 插件描述
*/
private String description;
/**
* 作者
*/
private String author;
/**
* 插件 logo建议尺寸 256x256
*/
private String logo;
/**
* 插件介绍,会展示在插件的详情中
*/
private String readme;
/**
* 插件配置, 每个插件的配置各不相同
*/
private Map<String, Object> config;
/**
* jar包存放路径
*/
private String jarPath;
/**
* 同名hook id
*/
@JsonIgnore
private Long sameHookId;
}

View File

@@ -0,0 +1,16 @@
package com.cool.core.config;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
@OpenAPIDefinition(info = @Info(title = "COOL-ADMIN", version = "4.0", description = "一个很酷的后台权限管理系统开发框架", contact = @Contact(name = "闪酷科技")), security = @SecurityRequirement(name = "Authorization"), externalDocs = @ExternalDocumentation(description = "参考文档", url = "https://cool-js.com"))
@SecurityScheme(type = SecuritySchemeType.APIKEY, name = "Authorization", in = SecuritySchemeIn.HEADER)
public class SwaggerConfig {
}

View File

@@ -0,0 +1,41 @@
package com.cool.core.config;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@RequiredArgsConstructor
public class ThreadPoolConfig {
private final LogProperties logProperties;
@Bean(name = "logTaskExecutor")
public Executor loggingTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int corePoolSize = Runtime.getRuntime().availableProcessors() * logProperties.getCorePoolSizeMultiplier();
int maxPoolSize = corePoolSize * logProperties.getMaxPoolSizeMultiplier();
int queueCapacity = maxPoolSize * logProperties.getQueueCapacityMultiplier();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix("logTask-");
// 自定义拒绝策略
executor.setRejectedExecutionHandler(new LogDiscardPolicy());
executor.initialize();
return executor;
}
@Bean(name = "cachedThreadPool")
public ExecutorService cachedThreadPool() {
// 创建一个虚拟线程池,每个任务使用一个虚拟线程执行
return Executors.newCachedThreadPool();
}
}

View File

@@ -0,0 +1,18 @@
package com.cool.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* token配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "cool.token")
public class TokenProperties {
// token 过期时间
private Long expire;
// refreshToken 过期时间
private Long refreshExpire;
}

View File

@@ -0,0 +1,122 @@
package com.cool.core.config.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
@Slf4j
@Configuration
@EnableCaching
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "CAFFEINE")
public class CaffeineConfig {
@Value("${spring.cache.file}")
private String cacheFile;
@Value("${cool.cacheName}")
private String cacheName;
@Bean
public Caffeine<Object, Object> caffeine() {
return Caffeine.newBuilder().maximumSize(10000);
}
@Bean
public CaffeineCacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
loadCache(cacheManager);
return cacheManager;
}
@PostConstruct
public void init() {
File cacheDir = new File(cacheFile).getParentFile();
if (!cacheDir.exists()) {
if (cacheDir.mkdirs()) {
log.info("Created directory: " + cacheDir.getAbsolutePath());
} else {
log.error("Failed to create directory: " + cacheDir.getAbsolutePath());
}
}
}
private void loadCache(CaffeineCacheManager cacheManager) {
if (cacheManager == null) {
log.error("CacheManager is null");
return;
}
if (cacheFile == null || cacheFile.isEmpty()) {
log.error("Cache file path is null or empty");
return;
}
File file = new File(cacheFile);
if (!file.exists()) {
log.warn("Cache file does not exist: " + cacheFile);
return;
}
try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) {
Map<Object, Object> cacheMap = (Map<Object, Object>) inputStream.readObject();
com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache = Caffeine.newBuilder()
.build();
caffeineCache.putAll(cacheMap);
cacheManager.registerCustomCache(cacheName, caffeineCache);
} catch (IOException | ClassNotFoundException e) {
log.error("loadCacheErr", e);
}
}
@Bean
public CacheLoader cacheLoader(CaffeineCacheManager cacheManager) {
return new CacheLoader(cacheManager, cacheFile);
}
class CacheLoader {
private final CaffeineCacheManager cacheManager;
private final String cacheFile;
public CacheLoader(CaffeineCacheManager cacheManager, String cacheFile) {
this.cacheManager = cacheManager;
this.cacheFile = cacheFile;
}
@EventListener(ContextClosedEvent.class)
@Scheduled(fixedRate = 10000)
public void persistCache() {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null
&& cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
Map<Object, Object> cacheMap = ((com.github.benmanes.caffeine.cache.Cache<Object, Object>) cache
.getNativeCache()).asMap();
try (ObjectOutputStream outputStream = new ObjectOutputStream(
new FileOutputStream(cacheFile))) {
outputStream.writeObject(new HashMap<>(cacheMap));
} catch (IOException e) {
log.error("persistCacheErr", e);
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
package com.cool.core.config.cache;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
@EnableCaching
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.create(redisConnectionFactory);
}
}

View File

@@ -0,0 +1,105 @@
package com.cool.core.enums;
public class AdminComponentsEnum {
/**
* 省市区选择器 - 用户选择省市区
*/
public static final String PCA = "pca";
/**
* 文本输入 - 文本编辑框
*/
public static final String INPUT = "input";
/**
* 文本域 - 多行文本编辑框
*/
public static final String TEXTAREA = "textarea";
/**
* 富文本编辑器 - 用于文章,商品详情的编辑
*/
public static final String EDITOR_RICH = "editor-rich";
/**
* 代码编辑器 - 用于开发人员编写代码,支持多种语言,支持代码高亮,支持代码格式化
*/
public static final String CODING = "coding";
/**
* 数字输入 - 数字输入编辑框
*/
public static final String INPUT_NUMBER = "input-number";
/**
* 日期选择器 - 用户选择 年-月-日
*/
public static final String DATE = "date";
/**
* 日期范围选择器 - 用户选择起始 年-月-日
*/
public static final String DATERANGE = "daterange";
/**
* 时间选择器 - 用户选择 时:分:秒
*/
public static final String DATETIME = "datetime";
/**
* 时间范围选择器 - 用户选择起始 年-月-日 时:分:秒
*/
public static final String DATETIMERANGE = "datetimerange";
/**
* 单图上传 - 用户上传单张图片头像、logo、封面
*/
public static final String UPLOAD_IMG = "upload-img";
/**
* 多图上传 - 用户上传多张图片, 如:照片、图片
*/
public static final String UPLOAD_IMG_MULTIPLE = "upload-img-multiple";
/**
* 单个文件上传 - 用户上传单个文件
*/
public static final String UPLOAD_FILE = "upload-file";
/**
* 多个文件上传 - 用户上传多个文件
*/
public static final String UPLOAD_FILE_MULTIPLE = "upload-file-multiple";
/**
* 状态选择器 - 用户开启或者关闭操作,如:是否启用、是否推荐、是否默认、置顶、启用禁用等
*/
public static final String SWITCH = "switch";
/**
* 评分选择器 - 用户评分
*/
public static final String RATE = "rate";
/**
* 滑块选择器 - 在一个固定区间内进行选择, 如:进度
*/
public static final String PROGRESS = "progress";
/**
* 单选框 - 在一组备选项中进行单选,如:审批状态
*/
public static final String RADIO = "radio";
/**
* 多选框 - 适用于选项比较少的情况,在一组备选项中进行多选, 如:学历、爱好等
*/
public static final String CHECKBOX = "checkbox";
/**
* 下拉框 - 适用于当选项过多时,使用下拉菜单展示并选择内容,如:分类、标签等
*/
public static final String SELECT = "select";
}

View File

@@ -0,0 +1,13 @@
package com.cool.core.enums;
public class Apis {
public static final String ADD = "add";
public static final String DELETE = "delete";
public static final String UPDATE = "update";
public static final String PAGE = "page";
public static final String LIST = "list";
public static final String INFO = "info";
public static final String[] ALL_API = new String[]{ ADD, DELETE, UPDATE, PAGE, LIST, INFO };
}

View File

@@ -0,0 +1,10 @@
package com.cool.core.enums;
/**
* 查询模式决定返回值
*/
public enum QueryModeEnum {
ENTITY, // 实体(默认)
ENTITY_WITH_RELATIONS, // 实体关联查询(如实体字段上加 @RelationOneToMany 等注解)
CUSTOM , // 自定义默认为Map
}

View File

@@ -0,0 +1,10 @@
package com.cool.core.enums;
/**
* 用户类型
*/
public enum UserTypeEnum {
ADMIN, // 后台
APP, // app
UNKNOWN, // 未知
}

View File

@@ -0,0 +1,410 @@
package com.cool.core.eps;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.core.annotation.EpsField;
import com.cool.core.annotation.TokenIgnore;
import com.cool.core.config.CustomOpenApiResource;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* 实体信息与路径
*/
@Getter
@Component
@Slf4j
@RequiredArgsConstructor
public class CoolEps {
@Value("${server.port}")
private int serverPort;
private Dict entityInfo;
private Dict menuInfo;
private JSONObject swaggerInfo;
@Value("${springdoc.api-docs.enabled:false}")
private boolean apiDocsEnabled;
public Dict admin;
public Dict app;
final private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Async
public void init() {
if (!apiDocsEnabled) {
log.info("服务启动成功,端口:{}", serverPort);
return;
}
entityInfo = Dict.create();
menuInfo = Dict.create();
swaggerInfo = swaggerInfo();
Runnable task = () -> {
entity();
urls();
log.info("初始化eps完成服务启动成功端口{}", serverPort);
};
// ThreadUtil.safeSleep(3000);
ThreadUtil.execute(task);
}
/**
* 清空所有的数据
*/
public void clear() {
admin = Dict.create();
app = Dict.create();
}
/**
* 构建所有的url
*/
private void urls() {
Dict admin = Dict.create();
Dict app = Dict.create();
ArrayList<Object> emptyList = new ArrayList<>();
Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> methodEntry : map.entrySet()) {
RequestMappingInfo info = methodEntry.getKey();
HandlerMethod method = methodEntry.getValue();
TokenIgnore tokenIgnore = method.getMethodAnnotation(TokenIgnore.class);
String module = getModule(method);
if (StrUtil.isNotEmpty(module)) {
String entityName = getEntity(method.getBeanType());
String methodPath = getMethodUrl(method);
String escapedMethodPath = methodPath.replace("{", "\\{").replace("}", "\\}");
String prefix = Objects.requireNonNull(getUrl(info))
.replaceFirst("(?s)(.*)" + escapedMethodPath, "$1");
Dict result;
int type = 0;
if (prefix.startsWith("/admin")) {
result = admin;
} else if (prefix.startsWith("/app")) {
result = app;
type = 1;
} else {
continue;
}
if (result.get(module) == null) {
result.set(module, new ArrayList<Dict>());
}
List<Dict> urls = result.getBean(module);
Dict item = CollUtil.findOne(urls, dict -> {
if (dict != null) {
return dict.getStr("module").equals(module)
&& dict.getStr("controller")
.equals(method.getBeanType().getSimpleName());
} else {
return false;
}
});
if (item != null) {
item.set("api", apis(prefix, methodPath, item.getBean("api"), tokenIgnore));
} else {
item = Dict.create();
item.set("controller", method.getBeanType().getSimpleName());
item.set("module", module);
item.set("info", Dict.create().set("type",
Dict.create()
.set("name", getLastPathSegment(prefix))
.set("description", "")
));
item.set("api", apis(prefix, methodPath, item.getBean("api"), tokenIgnore));
item.set("name", entityName);
item.set("columns", entityInfo.get(entityName));
item.set("pageQueryOp", Dict.create().set("keyWordLikeFields", emptyList)
.set("fieldEq", emptyList)
.set("fieldLike", emptyList));
item.set("prefix", prefix);
item.set("menu", menuInfo.get(entityName));
urls.add(item);
}
if (type == 0) {
admin.set(module, urls);
}
if (type == 1) {
app.set(module, urls);
}
}
}
this.admin = admin;
this.app = app;
}
/**
* 提取URL路径中的最后一个路径段
* 示例:输入 "/api/getData" 返回 "getData"
*/
private String getLastPathSegment(String url) {
if (StrUtil.isBlank(url)) {
return "";
}
int queryIndex = url.indexOf('?');
if (queryIndex != -1) {
url = url.substring(0, queryIndex);
}
int slashIndex = url.lastIndexOf('/');
if (slashIndex != -1 && slashIndex < url.length() - 1) {
return url.substring(slashIndex + 1);
} else {
return url;
}
}
/**
* 设置所有的api
*
* @param prefix 路由前缀
* @param methodPath 方法路由
* @param list api列表
* @return api列表
*/
private List<Dict> apis(String prefix, String methodPath, List<Dict> list, TokenIgnore tokenIgnore) {
if (ObjUtil.isNull(list)) {
list = new ArrayList<>();
}
Dict item = Dict.create();
item.set("path", methodPath);
item.set("tag", "");
item.set("dts", Dict.create());
item.set("ignoreToken", false);
if (tokenIgnore != null) {
item.set("ignoreToken", true);
}
setSwaggerInfo(item, prefix + methodPath);
list.add(item);
return list;
}
/**
* 设置swagger相关信息
*
* @param item 信息载体
* @param url url地址
*/
private void setSwaggerInfo(Dict item, String url) {
JSONObject paths = swaggerInfo.getJSONObject("paths");
JSONObject urlInfo = paths.getJSONObject(url);
String method = urlInfo.keySet().iterator().next();
JSONObject methodInfo = urlInfo.getJSONObject(method);
item.set("method", method);
item.set("summary", methodInfo.getStr("summary"));
}
/**
* 获得方法的url地址
*
* @param handlerMethod 方法
* @return 方法url地址
*/
private String getMethodUrl(HandlerMethod handlerMethod) {
String url = "";
Method method = handlerMethod.getMethod();
Annotation[] annotations = method.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType.getName().contains("org.springframework.web.bind.annotation")) {
Map<String, Object> attributes = Arrays.stream(annotationType.getDeclaredMethods())
.collect(Collectors.toMap(Method::getName, m -> {
try {
return m.invoke(annotation);
} catch (Exception e) {
throw new IllegalStateException("Failed to access annotation attribute",
e);
}
}));
if (attributes.containsKey("value") && ObjUtil.isNotEmpty(attributes.get("value"))) {
url = ((String[]) attributes.get("value"))[0];
}
break;
}
}
return url;
}
/**
* 获得url地址
*
* @param info 路由信息
* @return url地址
*/
private String getUrl(RequestMappingInfo info) {
if (info.getPathPatternsCondition() == null) {
return null;
}
Set<String> paths = info.getPathPatternsCondition().getPatternValues();
return paths.iterator().next();
}
/**
* 获得模块
*
* @param method 方法
* @return 模块
*/
private String getModule(HandlerMethod method) {
String beanName = method.getBeanType().getName();
String[] beanNames = beanName.split("[.]");
int index = ArrayUtil.indexOf(beanNames, "modules");
if (index > 0) {
return beanNames[index + 1];
}
return null;
}
/**
* 获得swagger的json信息
*/
private JSONObject swaggerInfo() {
try {
byte[] bytes = SpringUtil.getBean(CustomOpenApiResource.class).getOpenApiJson();
return JSONUtil.parseObj(new String(bytes));
} catch (Exception e) {
return new JSONObject();
}
}
/**
* 获得Controller上的实体类型
*
* @param controller Controller类
* @return 实体名称
*/
private String getEntity(Class<?> controller) {
try {
ParameterizedType parameterizedType = (ParameterizedType) controller.getGenericSuperclass();
Class<?> entityClass = (Class<?>) parameterizedType.getActualTypeArguments()[1];
return entityClass.getSimpleName();
} catch (Exception e) {
return "";
}
}
private void entity() {
// 扫描所有的实体类
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation("", Table.class);
classes.forEach(e -> {
// 获得属性
Field[] fields = getAllDeclaredFields(e);
List<Dict> columns = columns(fields);
entityInfo.set(e.getSimpleName(), columns);
Table mergedAnnotation = AnnotatedElementUtils.findMergedAnnotation(e, Table.class);
menuInfo.set(e.getSimpleName(), mergedAnnotation.comment());
});
}
/**
* 获取类及其所有父类中声明的字段
*
* @param clazz 要检查的类
* @return 包含类及其所有父类中声明的所有字段的数组
*/
public static Field[] getAllDeclaredFields(Class<?> clazz) {
// 参数校验
if (clazz == null) {
throw new IllegalArgumentException("Class cannot be null");
}
List<Field> allFields = new ArrayList<>();
Class<?> currentClass = clazz;
// 循环遍历类及其父类
while (currentClass != null) {
Field[] declaredFields = currentClass.getDeclaredFields();
allFields.addAll(Arrays.asList(declaredFields));
currentClass = currentClass.getSuperclass();
}
// 将列表转换为数组返回
return allFields.toArray(new Field[0]);
}
/**
* 获得所有的列
*
* @param fields 字段名
* @return 所有的列
*/
private List<Dict> columns(Field[] fields) {
List<Dict> dictList = new ArrayList<>();
for (Field field : fields) {
Dict dict = Dict.create();
EpsField epsField = AnnotatedElementUtils.findMergedAnnotation(field, EpsField.class);
if (epsField != null) {
dict.set("component", epsField.component());
}
ColumnDefine columnInfo = AnnotatedElementUtils.findMergedAnnotation(field, ColumnDefine.class);
if (columnInfo == null) {
continue;
}
dict.set("comment", columnInfo.comment());
dict.set("length", columnInfo.length());
dict.set("propertyName", field.getName());
dict.set("type", matchType(field.getType().getName()));
dict.set("nullable", !columnInfo.notNull());
dict.set("source", "a." + field.getName());
dictList.add(dict);
}
return dictList;
}
/**
* java类型转换成JavaScript对应的类型
*
* @param type 类型
* @return JavaScript类型
*/
private String matchType(String type) {
return switch (type) {
case "java.lang.Boolean" -> "boolean";
case "java.lang.Long", "java.lang.Integer", "java.lang.Short", "java.lang.Float",
"java.lang.Double" -> "number";
case "java.util.Date" -> "date";
default -> "string";
};
}
}

View File

@@ -0,0 +1,26 @@
package com.cool.core.eps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 事件监听
*/
@Slf4j
@Component
@Profile({"local"})
@RequiredArgsConstructor
public class EpsEvent {
final private CoolEps coolEps;
@EventListener
public void onApplicationEvent(ApplicationReadyEvent event) {
coolEps.init();
log.info("构建eps信息");
}
}

View File

@@ -0,0 +1,43 @@
package com.cool.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 自定义异常处理
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class CoolException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
private Object data;
public CoolException(String msg) {
super(msg);
this.msg = msg;
}
public CoolException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public CoolException(String msg, int code) {
super(msg);
this.msg = msg;
this.code = code;
}
public CoolException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public CoolException(Object data) {
this.data = data;
}
}

View File

@@ -0,0 +1,71 @@
package com.cool.core.exception;
import cn.hutool.core.util.ObjUtil;
import com.cool.core.request.R;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 异常处理器
*/
@RestControllerAdvice
@Slf4j
public class CoolExceptionHandler {
@ExceptionHandler(CoolException.class)
public R handleRRException(CoolException e) {
R r = new R();
if (ObjUtil.isNotEmpty(e.getData())) {
r.setData( e.getData() );
} else {
r.setCode( e.getCode() );
r.setMessage( e.getMessage() );
}
if (ObjUtil.isNotEmpty(e.getCause())) {
log.error(e.getCause().getMessage(), e.getCause());
}
return r;
}
@ExceptionHandler(DuplicateKeyException.class)
public R handleDuplicateKeyException(DuplicateKeyException e) {
log.error(e.getMessage(), e);
return R.error("已存在该记录或值不能重复");
}
@ExceptionHandler(BadCredentialsException.class)
public R handleBadCredentialsException(BadCredentialsException e) {
log.error(e.getMessage(), e);
return R.error("账户密码不正确");
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R handleHttpRequestMethodNotSupportedException(
HttpRequestMethodNotSupportedException e) {
log.error(e.getMessage(), e);
return R.error("不支持该请求方式请区分POST、GET等请求方式是否正确");
}
@ExceptionHandler(IllegalArgumentException.class)
public R handleIllegalArgumentException(IllegalArgumentException e) {
log.error(e.getMessage(), e);
return R.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public R handleException(Exception e) {
log.error(e.getMessage(), e);
return R.error();
}
@ExceptionHandler(WxErrorException.class)
public R handleException(WxErrorException e) {
log.error(e.getMessage(), e);
return R.error(e.getMessage());
}
}

View File

@@ -0,0 +1,102 @@
package com.cool.core.exception;
import cn.hutool.core.util.ObjectUtil;
import com.cool.core.util.I18nUtil;
import java.util.Arrays;
import java.util.Optional;
import lombok.Getter;
import lombok.Setter;
/**
* 校验处理
*/
public class CoolPreconditions {
/**
* 条件如果为真 就抛异常 如 CoolPreconditions.check(StrUtil.isEmptyIfStr(name), 500,
* "名称不能为空"); name 字段如果为 null或空字符串就抛异常
*/
public static void check(boolean flag, int code, String message, Object... arguments) {
if (flag) {
throw getCoolException(message, code, arguments);
}
}
public static void check(boolean flag, String message, Object... arguments) {
if (flag) {
throw getCoolException(message, arguments);
}
}
public static void alwaysThrow(String message, Object... arguments) {
throw getCoolException(message, arguments);
}
private static CoolException getCoolException(String message, Object... arguments) {
Optional<Object> first = Arrays.stream(arguments).filter(o -> o instanceof Throwable)
.findFirst();
return new CoolException(formatMessage(message, arguments), (Throwable) first.orElse(null));
}
private static CoolException getCoolException(String message, int code, Object... arguments) {
Optional<Object> first = Arrays.stream(arguments).filter(o -> o instanceof Throwable)
.findFirst();
return new CoolException(formatMessage(message, arguments), code, (Throwable) first.orElse(null));
}
/**
* 返回data
*/
public static void returnData(boolean flag, Object data) {
if (flag) {
throw new CoolException(data);
}
}
public static void returnData(Object data) {
returnData(true, data);
}
/**
* 对象如果为空 就抛异常
*/
public static void checkEmpty(Object object, String message, Object... arguments) {
check(ObjectUtil.isEmpty(object), formatMessage(message, arguments));
}
public static void checkEmpty(Object object) {
check(ObjectUtil.isEmpty(object), "参数不能为空");
}
private static String formatMessage(String messagePattern, Object... arguments) {
messagePattern = I18nUtil.getI18nMsg(messagePattern);
StringBuilder sb = new StringBuilder();
int argumentIndex = 0;
int placeholderIndex = messagePattern.indexOf("{}");
while (placeholderIndex != -1) {
sb.append(messagePattern, 0, placeholderIndex);
if (argumentIndex < arguments.length) {
sb.append(arguments[argumentIndex++]);
} else {
sb.append("{}"); // 如果参数不足,保留原样
}
messagePattern = messagePattern.substring(placeholderIndex + 2);
placeholderIndex = messagePattern.indexOf("{}");
}
sb.append(messagePattern); // 添加剩余部分
return sb.toString();
}
@Setter
@Getter
public static class ReturnData {
private Integer type;
private String message;
public ReturnData(Integer type, String message, Object... arguments) {
this.type = type;
this.message = formatMessage(message, arguments);
}
}
}

View File

@@ -0,0 +1,53 @@
package com.cool.core.file;
import static com.cool.core.plugin.consts.PluginConsts.uploadHook;
import cn.hutool.core.util.ObjUtil;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.file.strategy.FileUploadStrategy;
import com.cool.core.plugin.service.CoolPluginService;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Component
@RequiredArgsConstructor
public class FileUploadStrategyFactory {
final private ApplicationContext applicationContext;
final private CoolPluginService coolPluginService;
private FileUploadStrategy getStrategy(PluginInfoEntity pluginInfoEntity) {
if (ObjUtil.isEmpty(pluginInfoEntity)) {
return applicationContext.getBean("localFileUploadStrategy", FileUploadStrategy.class);
}
return applicationContext.getBean("cloudFileUploadStrategy", FileUploadStrategy.class);
}
public Object upload(MultipartFile[] files, HttpServletRequest request) {
PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook(uploadHook);
try {
return getStrategy(pluginInfoEntity).upload(files, request, pluginInfoEntity);
} catch (IOException e) {
log.error("上传文件失败", e);
CoolPreconditions.alwaysThrow("上传文件失败 {}", e.getMessage());
}
return null;
}
public Object getMode() {
PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook(uploadHook);
String key = null;
if (ObjUtil.isNotEmpty(pluginInfoEntity)) {
key = pluginInfoEntity.getKey();
}
return getStrategy(pluginInfoEntity).getMode(key);
}
}

View File

@@ -0,0 +1,26 @@
package com.cool.core.file;
import com.cool.core.config.FileModeEnum;
import lombok.Data;
/**
* 上传模式类型
*/
@Data
public class UpLoadModeType {
/**
* 模式
*/
private FileModeEnum mode;
/**
* 类型
*/
private String type;
public UpLoadModeType(FileModeEnum mode) {
this.mode = mode;
this.type = mode.type();
}
}

View File

@@ -0,0 +1,33 @@
package com.cool.core.file.strategy;
import com.cool.core.config.FileModeEnum;
import com.cool.core.util.CoolPluginInvokers;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Component("cloudFileUploadStrategy")
public class CloudFileUploadStrategy implements FileUploadStrategy {
@Override
public Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity)
throws IOException {
return CoolPluginInvokers.invokePlugin(pluginInfoEntity.getKey());
}
@Override
public Map<String, String> getMode(String key) {
try{
Object mode = CoolPluginInvokers.invoke(key, "getMode");
if (Objects.nonNull(mode)) {
return (Map) mode;
}
} catch (Exception ignore){}
return Map.of("mode", FileModeEnum.CLOUD.value(),
"type", FileModeEnum.CLOUD.type());
}
}

View File

@@ -0,0 +1,38 @@
package com.cool.core.file.strategy;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
public interface FileUploadStrategy {
/**
* 文件上传
*/
Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity)
throws IOException;
/**
* 文件上传模式
*
* @return 上传模式
*/
Map<String, String> getMode(String key);
default boolean isAbsolutePath(String pathStr) {
Path path = Paths.get(pathStr);
return path.isAbsolute();
}
default String getExtensionName(String fileName) {
if (fileName.contains(".")) {
String[] names = fileName.split("[.]");
return "." + names[names.length - 1];
}
return "";
}
}

View File

@@ -0,0 +1,74 @@
package com.cool.core.file.strategy;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.cool.core.config.FileModeEnum;
import com.cool.core.config.LocalFileProperties;
import com.cool.core.exception.CoolException;
import com.cool.core.exception.CoolPreconditions;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import jakarta.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Component("localFileUploadStrategy")
@RequiredArgsConstructor
public class LocalFileUploadStrategy implements FileUploadStrategy {
final private LocalFileProperties localFileProperties;
/**
* 上传文件
*
* @param files 上传的文件
* @return 文件路径
*/
@Override
public Object upload(MultipartFile[] files, HttpServletRequest request,
PluginInfoEntity pluginInfoEntity) {
CoolPreconditions.check(StrUtil.isEmpty(localFileProperties.getBaseUrl()),
"filePath 或 baseUrl 未配置");
try {
List<String> fileUrls = new ArrayList<>();
String baseUrl = localFileProperties.getBaseUrl();
String date = DateUtil.format(new Date(),
DatePattern.PURE_DATE_PATTERN);
String absoluteUploadFolder = localFileProperties.getAbsoluteUploadFolder();
String fullPath = absoluteUploadFolder + "/" + date;
FileUtil.mkdir(fullPath);
for (MultipartFile file : files) {
// 保存文件
String fileName = StrUtil.uuid().replaceAll("-", "") + getExtensionName(
Objects.requireNonNull(file.getOriginalFilename()));
file.transferTo(new File(fullPath
+ "/" + fileName));
fileUrls.add(baseUrl + "/" + date + "/" + fileName);
}
if (fileUrls.size() == 1) {
return fileUrls.get(0);
}
return fileUrls;
} catch (Exception e) {
throw new CoolException("文件上传失败", e);
}
}
/**
* 文件上传模式
*
* @return 上传模式
*/
public Map<String, String> getMode(String key) {
return Map.of("mode", FileModeEnum.LOCAL.value(),
"type", FileModeEnum.LOCAL.type());
}
}

View File

@@ -0,0 +1,219 @@
package com.cool.core.i18n;
import static com.cool.core.util.I18nUtil.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.core.lock.CoolLock;
import com.cool.core.util.CoolPluginInvokers;
import com.cool.core.util.I18nUtil;
import com.cool.core.util.PathUtils;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.service.sys.BaseSysMenuService;
import com.cool.modules.dict.entity.DictInfoEntity;
import com.cool.modules.dict.entity.DictTypeEntity;
import com.cool.modules.dict.service.DictInfoService;
import com.cool.modules.dict.service.DictTypeService;
import com.mybatisflex.core.query.QueryWrapper;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class I18nGenerator {
private final BaseSysMenuService baseSysMenuService;
private final DictTypeService dictTypeService;
private final DictInfoService dictInfoService;
private final CoolLock coolLock;
private final I18nUtil i18nUtil;
private List<String> languages;
private static final Duration DURATION = Duration.ofSeconds(30);
public void run(Map<String, Object> map) {
log.info("国际化 翻译...");
languages = (List<String>) map.getOrDefault("languages", List.of("zh-cn", "zh-tw", "en"));
path = (String) map.getOrDefault("path", "assets/i18n");
init();
log.info("✅国际化 翻译 成功!!!");
enable = true;
}
public void init() {
// 四个任务并发执行
CompletableFuture<Void> futureMsg = CompletableFuture.runAsync(this::genBaseMsg);
CompletableFuture<Void> futureMenu = CompletableFuture.runAsync(this::genBaseMenu);
CompletableFuture<Void> futureDictInfo = CompletableFuture.runAsync(this::genBaseDictInfo);
CompletableFuture<Void> futureDictType = CompletableFuture.runAsync(this::genBaseDictType);
// 等待全部执行完成
CompletableFuture.allOf(futureMsg, futureMenu, futureDictInfo, futureDictType).join();
}
private void genBaseMsg() {
try {
Map<String, String> msgMap = new HashMap<>();
// 从idea本地启动时从项目目录中读取
Files.walk(Paths.get(System.getProperty("user.dir")))
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".java"))
.filter(path -> !path.toString().contains("/target/") && !path.toString().contains("/.git/"))
.forEach(path -> msgMap.putAll(processFile(path)));
if (ObjUtil.isNotEmpty(msgMap)) {
// 系统异常信息,输出到resources/i18n 文件夹下,只有本地运行会生成
File msgfile = FileUtil.file(PathUtils.getUserDir(),
"src", "main", "resources", "cool", "i18n", "msg", "template.json");
// 确保父目录存在
FileUtil.mkParentDirs(msgfile);
// 写入内容
FileUtil.writeUtf8String(JSONUtil.toJsonStr(msgMap), msgfile);
} else {
try {
// jar启动时从jar包中读取
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource resource = resolver.getResource("classpath:cool/i18n/msg/template.json");
String content = FileUtil.readUtf8String(resource.getFile());
msgMap.putAll(JSONUtil.toBean(content, Map.class));
} catch (Exception e) {
log.error("获取系统异常信息失败", e);
}
}
extracted(MSG_PREFIX, msgMap);
} catch (Exception e) {
log.error("国际化系统异常信息失败", e);
}
}
/**
* 生成菜单信息国际化
*/
@Async
public void asyncGenBaseMenu() {
if (coolLock.tryLock(MENU_PREFIX, DURATION)) {
genBaseMenu();
coolLock.unlock(MENU_PREFIX);
}
}
private void genBaseMenu() {
try {
Map<String, String> menuMap = baseSysMenuService.list(QueryWrapper.create().select(BaseSysMenuEntity::getName))
.stream()
.collect(Collectors.toMap(
BaseSysMenuEntity::getName,
BaseSysMenuEntity::getName,
(oldValue, newValue) -> oldValue
));
extracted(MENU_PREFIX, menuMap);
} catch (Exception e) {
log.error("国际化菜单信息失败", e);
}
}
@Async
public void asyncGenBaseDictType() {
if (coolLock.tryLock(DICT_TYPE_PREFIX, DURATION)) {
genBaseDictType();
coolLock.unlock(DICT_TYPE_PREFIX);
}
}
private void genBaseDictType() {
try {
Map<String, String> dataMap = dictTypeService.list(QueryWrapper.create().select(DictTypeEntity::getName))
.stream()
.collect(Collectors.toMap(
DictTypeEntity::getName,
DictTypeEntity::getName,
(oldValue, newValue) -> oldValue
));
extracted(DICT_TYPE_PREFIX, dataMap);
} catch (Exception e) {
log.error("国际化字段类型信息失败", e);
}
}
@Async
public void asyncGenBaseDictInfo() {
if (coolLock.tryLock(DICT_INFO_PREFIX, DURATION)) {
genBaseDictInfo();
coolLock.unlock(DICT_INFO_PREFIX);
}
}
private void genBaseDictInfo() {
try {
Map<String, String> dataMap = dictInfoService.list(QueryWrapper.create().select(DictInfoEntity::getName))
.stream()
.collect(Collectors.toMap(
DictInfoEntity::getName,
DictInfoEntity::getName,
(oldValue, newValue) -> oldValue
));
extracted(DICT_INFO_PREFIX, dataMap);
} catch (Exception e) {
log.error("国际化字段类型信息失败", e);
}
}
private void extracted(String prefix, Map<String, String> dataMap) {
languages.forEach(language -> {
String key = prefix + language;
if (!i18nUtil.exist(key)) {
JSONObject jsonObject = invokeTranslate(dataMap, language);
if (ObjUtil.isNotNull(jsonObject)) {
i18nUtil.update(key, jsonObject);
}
}
});
}
private JSONObject invokeTranslate(Map<String, String> map, String language) {
return (JSONObject) CoolPluginInvokers.invoke("i18n", "invokeTranslate", map, language);
}
// 匹配 CoolPreconditions 抛异常语句中的中文字符串
private static final Pattern EXCEPTION_PATTERN = Pattern.compile(
"CoolPreconditions\\.(\\w+)\\s*\\([^;]*?\"([^\"]*[\u4e00-\u9fa5]+[^\"]*)\"", Pattern.MULTILINE
);
private static Map<String, String> processFile(Path path) {
Map<String, String> map = new HashMap<>();
try {
String content = Files.readString(path, StandardCharsets.UTF_8);
// 去掉注释
content = removeComments(content);
// 仅查找方法体内的 CoolPreconditions 调用
Matcher matcher = EXCEPTION_PATTERN.matcher(content);
while (matcher.find()) {
String chineseText = matcher.group(2).trim();
map.put(chineseText, chineseText);
}
} catch (IOException e) {
e.printStackTrace();
}
return map;
}
// 移除注释(单行与多行)
private static String removeComments(String code) {
String noMultiLine = code.replaceAll("/\\*.*?\\*/", ""); // 多行注释
return noMultiLine.replaceAll("//.*", ""); // 单行注释
}
}

View File

@@ -0,0 +1,24 @@
package com.cool.core.init;
import com.cool.core.plugin.service.CoolPluginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 历史安装过的插件执行初始化
**/
@Slf4j
@Component
@RequiredArgsConstructor
public class CoolPluginInit {
final private CoolPluginService coolPluginService;
@EventListener(ApplicationReadyEvent.class)
public void run() {
coolPluginService.init();
}
}

View File

@@ -0,0 +1,252 @@
package com.cool.core.init;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.core.base.service.MapperProviderService;
import com.cool.core.mybatis.pg.PostgresSequenceSyncService;
import com.cool.core.util.DatabaseDialectUtils;
import com.cool.core.util.EntityUtils;
import com.cool.modules.base.entity.sys.BaseSysConfEntity;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.service.sys.BaseSysConfService;
import com.cool.modules.base.service.sys.BaseSysMenuService;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.query.QueryWrapper;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
/**
* 数据库初始数据初始化 在 classpath:cool/data/db 目录下创建.json文件 并定义表数据, 由该类统一执行初始化
**/
@Slf4j
@Component
@RequiredArgsConstructor
public class DBFromJsonInit {
final private BaseSysConfService baseSysConfService;
final private BaseSysMenuService baseSysMenuService;
final private MapperProviderService mapperProviderService;
final private ApplicationEventPublisher eventPublisher;
final private PostgresSequenceSyncService postgresSequenceSyncService;
@Value("${cool.initData}")
private boolean initData;
@EventListener(ApplicationReadyEvent.class)
public void run() {
if (!initData) {
return;
}
// 初始化自定义的数据
boolean initFlag = extractedDb();
// 初始化菜单数据
initFlag = extractedMenu() || initFlag;
// 发送数据库初始化完成事件
eventPublisher.publishEvent(new DbInitCompleteEvent(this));
if (initFlag) {
// 如果是postgresql同步序列
syncIdentitySequences();
}
log.info("数据初始化完成!");
}
private void syncIdentitySequences() {
if (DatabaseDialectUtils.isPostgresql()) {
postgresSequenceSyncService.syncIdentitySequences();
}
}
@Getter
public static class DbInitCompleteEvent {
private final Object source;
public DbInitCompleteEvent(Object source) {
this.source = source;
}
}
/**
* 解析插入业务数据
*/
private boolean extractedDb() {
try {
// 加载 JSON 文件
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:cool/data/db/*.json");
// 遍历所有.json文件
return analysisResources(resources);
} catch (Exception e) {
log.error("Failed to initialize data", e);
}
return false;
}
private boolean analysisResources(Resource[] resources)
throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
String prefix = "db_";
boolean isInit = false;
for (Resource resource : resources) {
File resourceFile = new File(resource.getURL().getFile());
String fileName = prefix + resourceFile.getName();
String value = baseSysConfService.getValue(fileName);
if (StrUtil.isNotEmpty(value)) {
log.info("{} 业务数据已初始化过...", fileName);
continue;
}
String jsonStr = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
JSONObject jsonObject = JSONUtil.parseObj(jsonStr);
// 遍历 JSON 文件中的数据
analysisJson(jsonObject);
BaseSysConfEntity baseSysUserEntity = new BaseSysConfEntity();
baseSysUserEntity.setCKey(fileName);
baseSysUserEntity.setCValue("success");
// 当前文件已加载
baseSysConfService.add(baseSysUserEntity);
isInit = true;
log.info("{} 业务数据初始化成功...", fileName);
}
return isInit;
}
private void analysisJson(JSONObject jsonObject)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Map<String, Class<?>> tableMap = EntityUtils.findTableMap();
for (String tableName : jsonObject.keySet()) {
JSONArray records = jsonObject.getJSONArray(tableName);
// 根据表名生成实体类名和 Mapper 接口名
Class<?> entityClass = tableMap.get(tableName);
BaseMapper<?> baseMapper = mapperProviderService.getMapperByEntityClass(entityClass);
// 插入
insertList(baseMapper, entityClass, records);
}
}
/**
* 插入列表数据
*/
private void insertList(BaseMapper baseMapper, Class<?> entityClass,
JSONArray records)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
// 插入数据
for (int i = 0; i < records.size(); i++) {
JSONObject record = records.getJSONObject(i);
Object entity = JSONUtil.toBean(record, entityClass);
Method getIdMethod = entityClass.getMethod("getId");
Object id = getIdMethod.invoke(entity);
if (ObjUtil.isNotEmpty(id) && ObjUtil.isNotEmpty(
baseMapper.selectOneById((Long) id))) {
// 数据库已经有值了
continue;
}
if (ObjUtil.isNotEmpty(id)) {
// 带id插入
baseMapper.insertSelectiveWithPk(entity);
} else {
baseMapper.insert(entity);
}
}
}
/**
* 解析插入菜单数据
*/
public boolean extractedMenu() {
boolean initFlag = false;
try {
String prefix = "menu_";
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:cool/data/menu/*.json");
// 遍历所有.json文件
for (Resource resource : resources) {
File resourceFile = new File(resource.getURL().getFile());
String fileName = prefix + resourceFile.getName();
String value = baseSysConfService.getValue(fileName);
if (StrUtil.isNotEmpty(value)) {
log.info("{} 菜单数据已初始化过...", fileName);
continue;
}
analysisResources(resource, fileName);
initFlag = true;
}
} catch (Exception e) {
log.error("Failed to initialize data", e);
}
return initFlag;
}
private void analysisResources(Resource resource, String fileName) throws IOException {
String jsonStr = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
// 使用 解析 JSON 字符串
JSONArray jsonArray = JSONUtil.parseArray(jsonStr);
// 遍历 JSON 数组
for (Object obj : jsonArray) {
JSONObject jsonObj = (JSONObject) obj;
// 将 JSON 对象转换为 Menu 对象
parseMenu(jsonObj, null);
}
BaseSysConfEntity baseSysUserEntity = new BaseSysConfEntity();
baseSysUserEntity.setCKey(fileName);
baseSysUserEntity.setCValue("success");
// 当前文件已加载
baseSysConfService.add(baseSysUserEntity);
log.info("{} 菜单数据初始化成功...", fileName);
}
// 递归解析 JSON 对象为 Menu 对象
private void parseMenu(JSONObject jsonObj, BaseSysMenuEntity parentMenuEntity) {
BaseSysMenuEntity menuEntity = BeanUtil.copyProperties(jsonObj, BaseSysMenuEntity.class);
if (ObjUtil.isNotEmpty(parentMenuEntity)) {
menuEntity.setParentName(parentMenuEntity.getName());
menuEntity.setParentId(parentMenuEntity.getId());
}
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(BaseSysMenuEntity::getName, menuEntity.getName());
if (ObjUtil.isNull(menuEntity.getParentId())) {
queryWrapper.isNull(BaseSysMenuEntity::getParentId);
} else {
queryWrapper.eq(BaseSysMenuEntity::getParentId, menuEntity.getParentId());
}
BaseSysMenuEntity dbBaseSysMenuEntity = baseSysMenuService.getOne(queryWrapper);
if (ObjUtil.isNull(dbBaseSysMenuEntity)) {
baseSysMenuService.add(menuEntity);
} else {
menuEntity = dbBaseSysMenuEntity;
}
// 递归处理子菜单
JSONArray childMenus = jsonObj.getJSONArray("childMenus");
if (childMenus != null) {
for (Object obj : childMenus) {
JSONObject childObj = (JSONObject) obj;
parseMenu(childObj, menuEntity);
}
}
}
}

View File

@@ -0,0 +1,24 @@
package com.cool.core.init;
import com.cool.core.leaf.IDGenService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 唯一ID 组件初始化
**/
@Slf4j
@Component
@RequiredArgsConstructor
public class IDGenInit {
final private IDGenService idGenService;
@EventListener(ApplicationReadyEvent.class)
public void run() {
idGenService.init();
}
}

View File

@@ -0,0 +1,6 @@
package com.cool.core.leaf;
public interface IDGenService {
long next(String key);
void init();
}

View File

@@ -0,0 +1,27 @@
package com.cool.core.leaf.common;
public class CheckVO {
private long timestamp;
private int workID;
public CheckVO(long timestamp, int workID) {
this.timestamp = timestamp;
this.workID = workID;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public int getWorkID() {
return workID;
}
public void setWorkID(int workID) {
this.workID = workID;
}
}

View File

@@ -0,0 +1,39 @@
package com.cool.core.leaf.common;
public class Result {
private long id;
private Status status;
public Result() {
}
public Result(long id, Status status) {
this.id = id;
this.status = status;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Result{");
sb.append("id=").append(id);
sb.append(", status=").append(status);
sb.append('}');
return sb.toString();
}
}

View File

@@ -0,0 +1,6 @@
package com.cool.core.leaf.common;
public enum Status {
SUCCESS,
EXCEPTION
}

View File

@@ -0,0 +1,5 @@
/**
* 全局唯一id生成
* 来源美团https://github.com/Meituan-Dianping/Leaf
*/
package com.cool.core.leaf;

View File

@@ -0,0 +1,310 @@
package com.cool.core.leaf.segment;
import static com.cool.core.leaf.segment.entity.table.LeafAllocEntityTableDef.LEAF_ALLOC_ENTITY;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.leaf.IDGenService;
import com.cool.core.leaf.common.Result;
import com.cool.core.leaf.common.Status;
import com.cool.core.leaf.segment.entity.LeafAllocEntity;
import com.cool.core.leaf.segment.mapper.LeafAllocMapper;
import com.cool.core.leaf.segment.model.Segment;
import com.cool.core.leaf.segment.model.SegmentBuffer;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import lombok.RequiredArgsConstructor;
import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class SegmentIDGenImpl implements IDGenService, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(SegmentIDGenImpl.class);
@Value("${leaf.segment.enable:false}")
private boolean enable;
/**
* IDCache未初始化成功时的异常码
*/
private static final long EXCEPTION_ID_IDCACHE_INIT_FALSE = -1;
/**
* key不存在时的异常码
*/
private static final long EXCEPTION_ID_KEY_NOT_EXISTS = -2;
/**
* SegmentBuffer中的两个Segment均未从DB中装载时的异常码
*/
private static final long EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL = -3;
/**
* 最大步长不超过100,0000
*/
private static final int MAX_STEP = 1000000;
/**
* 一个Segment维持时间为15分钟
*/
private static final long SEGMENT_DURATION = 15 * 60 * 1000L;
private final ExecutorService executorService = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new UpdateThreadFactory());
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r);
t.setName("check-idCache-thread");
t.setDaemon(true);
return t;
});
private volatile boolean initOK = false;
private final Map<String, SegmentBuffer> cache = new ConcurrentHashMap<>();
private final LeafAllocMapper leafAllocMapper;
public static class UpdateThreadFactory implements ThreadFactory {
private static int threadInitNumber = 0;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Thread-Segment-Update-" + nextThreadNum());
}
}
@Override
public long next(String key) {
Result result = get(key);
CoolPreconditions.check(result.getId() < 0, "获取失败code值: {}", result.getId());
return result.getId();
}
@Override
public void init() {
if (enable) {
// 确保加载到kv后才初始化成功
updateCacheFromDb();
initOK = true;
updateCacheFromDbAtEveryMinute();
logger.info("唯一ID组件初始化成功 ...");
}
}
private void updateCacheFromDbAtEveryMinute() {
scheduledExecutorService.scheduleWithFixedDelay(this::updateCacheFromDb, 60, 60, TimeUnit.SECONDS);
}
private void updateCacheFromDb() {
logger.info("update cache from db");
StopWatch sw = new Slf4JStopWatch();
try {
List<String> dbTags = leafAllocMapper.selectListByQuery(QueryWrapper.create().select(
LeafAllocEntity::getKey)).stream().map(LeafAllocEntity::getKey).toList();
if (dbTags.isEmpty()) {
return;
}
List<String> cacheTags = new ArrayList<String>(cache.keySet());
Set<String> insertTagsSet = new HashSet<>(dbTags);
Set<String> removeTagsSet = new HashSet<>(cacheTags);
//db中新加的tags灌进cache
for (String tmp : cacheTags) {
insertTagsSet.remove(tmp);
}
for (String tag : insertTagsSet) {
SegmentBuffer buffer = new SegmentBuffer();
buffer.setKey(tag);
Segment segment = buffer.getCurrent();
segment.setValue(new AtomicLong(0));
segment.setMax(0);
segment.setStep(0);
cache.put(tag, buffer);
logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);
}
//cache中已失效的tags从cache删除
for (String tmp : dbTags) {
removeTagsSet.remove(tmp);
}
for (String tag : removeTagsSet) {
cache.remove(tag);
logger.info("Remove tag {} from IdCache", tag);
}
} catch (Exception e) {
logger.warn("update cache from db exception", e);
} finally {
sw.stop("updateCacheFromDb");
}
}
private Result get(final String key) {
if (!initOK) {
return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION);
}
CoolPreconditions.check(!initOK, "IDCache未初始化成功");
if (cache.containsKey(key)) {
SegmentBuffer buffer = cache.get(key);
if (!buffer.isInitOk()) {
synchronized (buffer) {
if (!buffer.isInitOk()) {
try {
updateSegmentFromDb(key, buffer.getCurrent());
logger.info("Init buffer. Update leafkey {} {} from db", key, buffer.getCurrent());
buffer.setInitOk(true);
} catch (Exception e) {
logger.warn("Init buffer {} exception", buffer.getCurrent(), e);
}
}
}
}
return getIdFromSegmentBuffer(cache.get(key));
}
return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION);
}
public void updateSegmentFromDb(String key, Segment segment) {
StopWatch sw = new Slf4JStopWatch();
SegmentBuffer buffer = segment.getBuffer();
LeafAllocEntity leafAllocEntity;
if (!buffer.isInitOk()) {
leafAllocEntity = updateMaxIdAndGetLeafAlloc(key);
buffer.setStep(leafAllocEntity.getStep());
buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc中的step为DB中的step
} else if (buffer.getUpdateTimestamp() == 0) {
leafAllocEntity = updateMaxIdAndGetLeafAlloc(key);
buffer.setUpdateTimestamp(System.currentTimeMillis());
buffer.setStep(leafAllocEntity.getStep());
buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc中的step为DB中的step
} else {
long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp();
int nextStep = buffer.getStep();
if (duration < SEGMENT_DURATION) {
if (nextStep * 2 > MAX_STEP) {
//do nothing
} else {
nextStep = nextStep * 2;
}
} else if (duration < SEGMENT_DURATION * 2) {
//do nothing with nextStep
} else {
nextStep = nextStep / 2 >= buffer.getMinStep() ? nextStep / 2 : nextStep;
}
logger.info("leafKey[{}], step[{}], duration[{}mins], nextStep[{}]", key, buffer.getStep(), String.format("%.2f",((double)duration / (1000 * 60))), nextStep);
LeafAllocEntity temp = new LeafAllocEntity();
temp.setKey(key);
temp.setStep(nextStep);
leafAllocEntity = updateMaxIdByCustomStepAndGetLeafAlloc(temp);
buffer.setUpdateTimestamp(System.currentTimeMillis());
buffer.setStep(nextStep);
buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc的step为DB中的step
}
// must set value before set max
long value = leafAllocEntity.getMaxId() - buffer.getStep();
segment.getValue().set(value);
segment.setMax(leafAllocEntity.getMaxId());
segment.setStep(buffer.getStep());
sw.stop("updateSegmentFromDb", key + " " + segment);
}
private LeafAllocEntity updateMaxIdByCustomStepAndGetLeafAlloc(LeafAllocEntity temp) {
UpdateChain.of(LeafAllocEntity.class)
.setRaw(LeafAllocEntity::getMaxId, LEAF_ALLOC_ENTITY.MAX_ID.getName() + " + " + temp.getStep())
.where(LeafAllocEntity::getKey).eq(temp.getKey())
.update();
return leafAllocMapper.selectOneByQuery(QueryWrapper.create().select(
LEAF_ALLOC_ENTITY.KEY, LEAF_ALLOC_ENTITY.MAX_ID, LEAF_ALLOC_ENTITY.STEP).eq(LeafAllocEntity::getKey, temp.getKey()));
}
private LeafAllocEntity updateMaxIdAndGetLeafAlloc(String key) {
UpdateChain.of(LeafAllocEntity.class)
.setRaw(LeafAllocEntity::getMaxId, LEAF_ALLOC_ENTITY.MAX_ID.getName() + " + " + LEAF_ALLOC_ENTITY.STEP.getName())
.where(LeafAllocEntity::getKey).eq(key)
.update();
return leafAllocMapper.selectOneByQuery(QueryWrapper.create().select(
LEAF_ALLOC_ENTITY.KEY, LEAF_ALLOC_ENTITY.MAX_ID, LEAF_ALLOC_ENTITY.STEP).eq(LeafAllocEntity::getKey, key));
}
public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {
while (true) {
buffer.rLock().lock();
try {
final Segment segment = buffer.getCurrent();
if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {
executorService.execute(() -> {
Segment next = buffer.getSegments()[buffer.nextPos()];
boolean updateOk = false;
try {
updateSegmentFromDb(buffer.getKey(), next);
updateOk = true;
logger.info("update segment {} from db {}", buffer.getKey(), next);
} catch (Exception e) {
logger.warn(buffer.getKey() + " updateSegmentFromDb exception", e);
} finally {
if (updateOk) {
buffer.wLock().lock();
buffer.setNextReady(true);
buffer.getThreadRunning().set(false);
buffer.wLock().unlock();
} else {
buffer.getThreadRunning().set(false);
}
}
});
}
long value = segment.getValue().getAndIncrement();
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
} finally {
buffer.rLock().unlock();
}
waitAndSleep(buffer);
buffer.wLock().lock();
try {
final Segment segment = buffer.getCurrent();
long value = segment.getValue().getAndIncrement();
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
if (buffer.isNextReady()) {
buffer.switchPos();
buffer.setNextReady(false);
} else {
logger.error("Both two segments in {} are not ready!", buffer);
return new Result(EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL, Status.EXCEPTION);
}
} finally {
buffer.wLock().unlock();
}
}
}
private void waitAndSleep(SegmentBuffer buffer) {
int roll = 0;
while (buffer.getThreadRunning().get()) {
roll += 1;
if(roll > 10000) {
try {
TimeUnit.MILLISECONDS.sleep(10);
break;
} catch (InterruptedException e) {
logger.warn("Thread {} Interrupted",Thread.currentThread().getName());
break;
}
}
}
}
@Override
public void destroy() throws Exception {
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();
scheduledExecutorService.awaitTermination(10, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,27 @@
package com.cool.core.leaf.segment.entity;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.tangzc.mybatisflex.autotable.annotation.UniIndex;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Table(value = "leaf_alloc", comment = "唯一id分配")
public class LeafAllocEntity extends BaseEntity<LeafAllocEntity> {
@UniIndex(name = "uk_key")
@ColumnDefine(comment = "业务key 比如orderId", length = 20, notNull = true)
private String key;
@ColumnDefine(comment = "当前最大id", defaultValue = "1", notNull = true)
private Long maxId;
@ColumnDefine(comment = "步长", defaultValue = "500", notNull = true)
private Integer step;
@ColumnDefine(comment = "描述")
private String description;
}

View File

@@ -0,0 +1,7 @@
package com.cool.core.leaf.segment.mapper;
import com.cool.core.leaf.segment.entity.LeafAllocEntity;
import com.mybatisflex.core.BaseMapper;
public interface LeafAllocMapper extends BaseMapper<LeafAllocEntity> {
}

View File

@@ -0,0 +1,59 @@
package com.cool.core.leaf.segment.model;
import java.util.concurrent.atomic.AtomicLong;
public class Segment {
private AtomicLong value = new AtomicLong(0);
private volatile long max;
private volatile int step;
private SegmentBuffer buffer;
public Segment(SegmentBuffer buffer) {
this.buffer = buffer;
}
public AtomicLong getValue() {
return value;
}
public void setValue(AtomicLong value) {
this.value = value;
}
public long getMax() {
return max;
}
public void setMax(long max) {
this.max = max;
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
public SegmentBuffer getBuffer() {
return buffer;
}
public long getIdle() {
return this.getMax() - getValue().get();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Segment(");
sb.append("value:");
sb.append(value);
sb.append(",max:");
sb.append(max);
sb.append(",step:");
sb.append(step);
sb.append(")");
return sb.toString();
}
}

View File

@@ -0,0 +1,129 @@
package com.cool.core.leaf.segment.model;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 双buffer
*/
public class SegmentBuffer {
private String key;
private Segment[] segments; //双buffer
private volatile int currentPos; //当前的使用的segment的index
private volatile boolean nextReady; //下一个segment是否处于可切换状态
private volatile boolean initOk; //是否初始化完成
private final AtomicBoolean threadRunning; //线程是否在运行中
private final ReadWriteLock lock;
private volatile int step;
private volatile int minStep;
private volatile long updateTimestamp;
public SegmentBuffer() {
segments = new Segment[]{new Segment(this), new Segment(this)};
currentPos = 0;
nextReady = false;
initOk = false;
threadRunning = new AtomicBoolean(false);
lock = new ReentrantReadWriteLock();
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Segment[] getSegments() {
return segments;
}
public Segment getCurrent() {
return segments[currentPos];
}
public int getCurrentPos() {
return currentPos;
}
public int nextPos() {
return (currentPos + 1) % 2;
}
public void switchPos() {
currentPos = nextPos();
}
public boolean isInitOk() {
return initOk;
}
public void setInitOk(boolean initOk) {
this.initOk = initOk;
}
public boolean isNextReady() {
return nextReady;
}
public void setNextReady(boolean nextReady) {
this.nextReady = nextReady;
}
public AtomicBoolean getThreadRunning() {
return threadRunning;
}
public Lock rLock() {
return lock.readLock();
}
public Lock wLock() {
return lock.writeLock();
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
public int getMinStep() {
return minStep;
}
public void setMinStep(int minStep) {
this.minStep = minStep;
}
public long getUpdateTimestamp() {
return updateTimestamp;
}
public void setUpdateTimestamp(long updateTimestamp) {
this.updateTimestamp = updateTimestamp;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("SegmentBuffer{");
sb.append("key='").append(key).append('\'');
sb.append(", segments=").append(Arrays.toString(segments));
sb.append(", currentPos=").append(currentPos);
sb.append(", nextReady=").append(nextReady);
sb.append(", initOk=").append(initOk);
sb.append(", threadRunning=").append(threadRunning);
sb.append(", step=").append(step);
sb.append(", minStep=").append(minStep);
sb.append(", updateTimestamp=").append(updateTimestamp);
sb.append('}');
return sb.toString();
}
}

View File

@@ -0,0 +1,108 @@
package com.cool.core.lock;
import jakarta.annotation.PostConstruct;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.cache.CacheType;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class CoolLock {
// 缓存类型
@Value("${spring.cache.type}")
private String type;
@Value("${cool.cacheName}")
private String cacheName;
private final CacheManager cacheManager;
private RedisCacheWriter redisCache ;
// 非redis方式时使用
private static final Map<String, Lock> lockMap = new ConcurrentHashMap<>();
private static final String LOCK_PREFIX = "lock:";
@PostConstruct
private void init() {
this.type = type.toLowerCase();
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
redisCache = (RedisCacheWriter) Objects.requireNonNull(cacheManager.getCache(cacheName))
.getNativeCache();
}
}
/**
* 尝试获取锁
*
* @param key 锁的 key
* @param expireTime 锁的过期时间
* @return 如果成功获取锁则返回 true否则返回 false
*/
public boolean tryLock(String key, Duration expireTime) {
String lockKey = getLockKey(key);
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
Lock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
return lock.tryLock();
}
byte[] lockKeyBytes = lockKey.getBytes();
// 使用 putIfAbsent 来尝试设置锁,如果成功返回 true否则返回 false
return redisCache.putIfAbsent(cacheName, lockKeyBytes, new byte[0], expireTime) == null;
}
/**
* 释放锁
*/
public void unlock(String key) {
String lockKey = getLockKey(key);
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
Lock lock = lockMap.get(lockKey);
if (lock != null && lock.tryLock()) {
lock.unlock();
lockMap.remove(lockKey);
}
return;
}
redisCache.remove(cacheName, lockKey.getBytes());
}
/**
* 拼接锁前缀
*/
private String getLockKey(String key) {
return LOCK_PREFIX + key;
}
/**
* 等待锁
*
* @param key 锁的 key
* @param expireTime 锁的过期时间
* @return 如果成功获取锁则返回 true否则返回 false
*/
public boolean waitForLock(String key, Duration expireTime, Duration waitTime) {
long endTime = System.currentTimeMillis() + waitTime.toMillis();
while (System.currentTimeMillis() < endTime) {
if (tryLock(key, expireTime)) {
return true;
}
// 等待锁释放
try {
Thread.sleep(100); // 可以根据需要调整等待时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
package com.cool.core.mybatis.handler;
import com.cool.core.util.DatabaseDialectUtils;
import com.mybatisflex.core.util.StringUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.postgresql.util.PGobject;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public abstract class BaseJsonTypeHandler<T> extends BaseTypeHandler<T> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (DatabaseDialectUtils.isPostgresql()) {
PGobject jsonObject = new PGobject();
jsonObject.setType("json");
jsonObject.setValue(toJson(parameter));
ps.setObject(i, jsonObject);
} else {
ps.setString(i, toJson(parameter));
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
final String json = rs.getString(columnName);
return StringUtil.noText(json) ? null : parseJson(json);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
final String json = rs.getString(columnIndex);
return StringUtil.noText(json) ? null : parseJson(json);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
final String json = cs.getString(columnIndex);
return StringUtil.noText(json) ? null : parseJson(json);
}
protected abstract T parseJson(String json);
protected abstract String toJson(T object);
}

View File

@@ -0,0 +1,73 @@
package com.cool.core.mybatis.handler;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.TypeReference;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
public class Fastjson2TypeHandler extends BaseJsonTypeHandler<Object> {
private final Class<?> propertyType;
private Class<?> genericType;
private Type type;
private boolean supportAutoType = false;
public Fastjson2TypeHandler(Class<?> propertyType) {
this.propertyType = propertyType;
this.supportAutoType = propertyType.isInterface() || Modifier.isAbstract(propertyType.getModifiers());
}
public Fastjson2TypeHandler(Class<?> propertyType, Class<?> genericType) {
this.propertyType = propertyType;
this.genericType = genericType;
this.type = TypeReference.collectionType((Class<? extends Collection>) propertyType, genericType);
Type actualTypeArgument = ((ParameterizedType) type).getActualTypeArguments()[0];
if (actualTypeArgument instanceof Class) {
this.supportAutoType = ((Class<?>) actualTypeArgument).isInterface()
|| Modifier.isAbstract(((Class<?>) actualTypeArgument).getModifiers());
}
}
@Override
protected Object parseJson(String json) {
if (genericType != null && Collection.class.isAssignableFrom(propertyType)) {
if (supportAutoType) {
return JSON.parseArray(json, Object.class, JSONReader.Feature.SupportAutoType);
} else {
return JSON.parseObject(json, type);
}
} else {
if (supportAutoType) {
return JSON.parseObject(json, Object.class, JSONReader.Feature.SupportAutoType);
} else {
return JSON.parseObject(json, propertyType);
}
}
}
@Override
protected String toJson(Object object) {
if (supportAutoType) {
return JSON.toJSONString(object
, JSONWriter.Feature.WriteMapNullValue
, JSONWriter.Feature.WriteNullListAsEmpty
, JSONWriter.Feature.WriteNullStringAsEmpty, JSONWriter.Feature.WriteClassName
);
} else {
return JSON.toJSONString(object
, JSONWriter.Feature.WriteMapNullValue
, JSONWriter.Feature.WriteNullListAsEmpty
, JSONWriter.Feature.WriteNullStringAsEmpty
);
}
}
}

View File

@@ -0,0 +1,68 @@
package com.cool.core.mybatis.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.exception.FlexExceptions;
import java.io.IOException;
import java.util.Collection;
public class JacksonTypeHandler extends BaseJsonTypeHandler<Object> {
private static ObjectMapper objectMapper;
private final Class<?> propertyType;
private Class<?> genericType;
private JavaType javaType;
public JacksonTypeHandler(Class<?> propertyType) {
this.propertyType = propertyType;
}
public JacksonTypeHandler(Class<?> propertyType, Class<?> genericType) {
this.propertyType = propertyType;
this.genericType = genericType;
}
@Override
protected Object parseJson(String json) {
try {
if (genericType != null && Collection.class.isAssignableFrom(propertyType)) {
return getObjectMapper().readValue(json, getJavaType());
} else {
return getObjectMapper().readValue(json, propertyType);
}
} catch (IOException e) {
throw FlexExceptions.wrap(e, "Can not parseJson by JacksonTypeHandler: " + json);
}
}
@Override
protected String toJson(Object object) {
try {
return getObjectMapper().writeValueAsString(object);
} catch (JsonProcessingException e) {
throw FlexExceptions.wrap(e, "Can not convert object to Json by JacksonTypeHandler: " + object);
}
}
public JavaType getJavaType() {
if (javaType == null){
javaType = getObjectMapper().getTypeFactory().constructCollectionType((Class<? extends Collection>) propertyType, genericType);
}
return javaType;
}
public static ObjectMapper getObjectMapper() {
if (null == objectMapper) {
objectMapper = new ObjectMapper();
}
return objectMapper;
}
public static void setObjectMapper(ObjectMapper objectMapper) {
JacksonTypeHandler.objectMapper = objectMapper;
}
}

View File

@@ -0,0 +1,66 @@
package com.cool.core.mybatis.pg;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import java.util.Map;
/**
* PostgreSQL Identity 序列同步服务
* 解决PostgreSQL 默认的序列机制序列会自动递增当手动插入指定id时需调用同步接口否则id会重复。
*/
@Slf4j
@Service
public class PostgresSequenceSyncService {
private final JdbcTemplate jdbcTemplate;
public PostgresSequenceSyncService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void syncIdentitySequences() {
log.info("⏳ 开始同步 PostgreSQL Identity 序列...");
// 查询所有 identity 字段
String identityColumnQuery = """
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE is_identity = 'YES'
AND table_schema = 'public'
""";
List<Map<String, Object>> identityColumns = jdbcTemplate.queryForList(identityColumnQuery);
for (Map<String, Object> col : identityColumns) {
String schema = (String) col.get("table_schema");
String table = (String) col.get("table_name");
String column = (String) col.get("column_name");
String fullTable = schema + "." + table;
// 获取对应的序列名
String seqNameSql = "SELECT pg_get_serial_sequence(?, ?)";
String seqName = jdbcTemplate.queryForObject(seqNameSql, String.class, fullTable, column);
if (seqName == null) {
log.warn("⚠️ 无法获取序列:{}.{}", table, column);
continue;
}
// 获取当前最大 ID
Long maxId = jdbcTemplate.queryForObject(
String.format("SELECT COALESCE(MAX(%s), 0) FROM %s", column, fullTable),
Long.class
);
if (maxId != null && maxId > 0) { // 正确的setval 有返回值,必须用 queryForObject
String setvalSql = "SELECT setval(?, ?)";
Long newVal = jdbcTemplate.queryForObject(setvalSql, Long.class, seqName, maxId);
log.info("✅ 同步序列 [{}] -> 当前最大 ID: {}", seqName, newVal);
}
}
log.info("✅ PostgreSQL Identity 序列同步完成。");
}
}

View File

@@ -0,0 +1,202 @@
package com.cool.core.request;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.cool.core.enums.QueryModeEnum;
import com.cool.core.util.ConvertUtil;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryTable;
import com.mybatisflex.core.query.QueryWrapper;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.Data;
import org.springframework.core.env.Environment;
/**
* 查询构建器
*
* @param <T>
*/
@Data
public class CrudOption<T> {
private QueryWrapper queryWrapper;
private QueryColumn[] fieldEq;
private QueryColumn[] keyWordLikeFields;
private QueryColumn[] select;
private JSONObject requestParams;
private QueryModeEnum queryModeEnum;
private Transform<Object> transform;
public interface Transform<B> {
void apply(B obj);
}
/**
* queryModeEnum 为 CUSTOM,可设置 默认为Map
*/
private Class<?> asType;
private Environment evn;
public CrudOption(JSONObject requestParams) {
this.requestParams = requestParams;
this.queryWrapper = QueryWrapper.create();
this.evn = SpringUtil.getBean(Environment.class);
queryModeEnum = QueryModeEnum.ENTITY;
}
public QueryWrapper getQueryWrapper(Class<T> entityClass) {
return build(this.queryWrapper, entityClass);
}
public CrudOption<T> queryWrapper(QueryWrapper queryWrapper) {
this.queryWrapper = queryWrapper;
return this;
}
/**
* 按前端传上来的字段值做eq
*/
public CrudOption<T> fieldEq(QueryColumn... fields) {
this.fieldEq = fields;
return this;
}
/**
* 按前端传上来的字段值做like
*/
public CrudOption<T> keyWordLikeFields(QueryColumn... fields) {
this.keyWordLikeFields = fields;
return this;
}
/**
* 需要返回给前端的字段
*/
public CrudOption<T> select(QueryColumn... selects) {
this.select = selects;
return this;
}
/**
* 查询模式决定返回值
* 目前有三种模式,按实体查询返回、关联查询返回(实体字段上加 @RelationOneToMany 等注解)、自定义返回结果
*/
public CrudOption<T> queryModeEnum(QueryModeEnum queryModeEnum) {
this.queryModeEnum = queryModeEnum;
if (ObjUtil.equal(queryModeEnum, QueryModeEnum.CUSTOM)
&& ObjUtil.isEmpty(asType)) {
asType = Map.class;
}
return this;
}
/**
* 自定义返回结果对象类型
*/
public CrudOption<T> asType(Class<?> asType) {
this.asType = asType;
return this;
}
/**
* 转换参数,组装数据
*/
public CrudOption<T> transform(Transform<Object> transform) {
this.transform = transform;
return this;
}
/**
* 构建查询条件
*
* @return QueryWrapper
*/
private QueryWrapper build(QueryWrapper queryWrapper, Class<T> entityClass) {
if (ObjectUtil.isNotEmpty(fieldEq)) {
Arrays.stream(fieldEq).toList().forEach(filed -> {
String filedName = StrUtil.toCamelCase(filed.getName());
Object obj = requestParams.get(filedName);
if (ObjUtil.isEmpty(obj)) {
return;
}
if (obj instanceof JSONArray) {
// 集合
queryWrapper.and(filed.in(ConvertUtil.covertListByClass(filedName, (JSONArray)obj, entityClass).toArray()));
} else {
// 对象
queryWrapper.and(filed.eq(ConvertUtil.convertByClass(filedName, obj, entityClass)));
}
});
}
if (ObjectUtil.isNotEmpty(this.keyWordLikeFields)) {
Object keyWord = requestParams.get("keyWord");
if (ObjectUtil.isEmpty(keyWord)) {
// // keyWord值为空遍历keyWordLikeFields字段根据queryColumn字段名构建查询条件
for (QueryColumn queryColumn : keyWordLikeFields) {
String fieldName = queryColumn.getName();
String paramName = StrUtil.toCamelCase(fieldName);
String paramValue = requestParams.getStr(paramName);
if (ObjectUtil.isNotEmpty(paramValue)) {
queryWrapper.and(queryColumn.like(paramValue));
}
}
} else {
// keyWord值非空使用keyWord构建
// 初始化一个空的 QueryCondition
QueryCondition orCondition = null;
for (QueryColumn queryColumn : keyWordLikeFields) {
QueryCondition condition = queryColumn.like(keyWord);
if (orCondition == null) {
orCondition = condition;
} else {
orCondition = orCondition.or(condition);
}
}
queryWrapper.and(orCondition);
}
}
if (ObjectUtil.isNotEmpty(select)) {
queryWrapper.select(select);
}
// 排序
order(queryWrapper, entityClass);
return queryWrapper;
}
private void order(QueryWrapper queryWrapper, Class<T> entityClass) {
Table tableAnnotation = AnnotationUtil.getAnnotation(entityClass, Table.class);
if (ObjectUtil.isEmpty(tableAnnotation)) {
// 该对象没有@Table注解非Entity对象
return;
}
String tableAlias = "";
List<QueryTable> queryTables = (List<QueryTable>) ReflectUtil.getFieldValue(queryWrapper, "queryTables");
if (ObjectUtil.isNotEmpty(queryTables)) {
// 取主表作为排序字段别名
QueryTable queryTable = queryTables.get(0);
tableAlias = queryTable.getName() + ".";
}
String order = requestParams.getStr("order",
tableAnnotation.camelToUnderline() ? "create_time" : "createTime");
String sort = requestParams.getStr("sort", "desc");
if (StrUtil.isNotEmpty(order) && StrUtil.isNotEmpty(sort)) {
queryWrapper.orderBy(
tableAlias + (tableAnnotation.camelToUnderline() ? StrUtil.toUnderlineCase(order) : order),
sort.equals("asc"));
}
}
}

View File

@@ -0,0 +1,33 @@
package com.cool.core.request;
import java.util.List;
import com.mybatisflex.core.paginate.Page;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema( title = "分页数据模型" )
public class PageResult<T> {
@Schema( title = "分页数据" )
private List<T> list;
private Pagination pagination = new Pagination();
@Data
public static class Pagination {
@Schema( title = "页码" )
private Long page;
@Schema( title = "本页数量" )
private Long size;
@Schema( title = "总页数" )
private Long total;
}
static public <B> PageResult<B> of(Page<B> page ){
PageResult<B> result = new PageResult<B>();
result.setList(page.getRecords());
result.pagination.setPage( page.getPageNumber() );
result.pagination.setSize( page.getPageSize() );
result.pagination.setTotal( page.getTotalRow() );
return result;
}
}

View File

@@ -0,0 +1,74 @@
package com.cool.core.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 返回信息
*/
@Schema(title = "响应数据结构")
@Data
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(title = "编码1000表示成功其他值表示失败")
private int code = 1000;
@Schema(title = "消息内容")
private String message = "success";
@Schema(title = "响应数据")
private T data;
public R() {
}
public R( int code, String message, T data ) {
this.code = code;
this.message = message;
this.data = data;
}
public static R error() {
return error(1001, "请求方式不正确或服务出现异常");
}
public static R error(String msg) {
return error(1001, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.code = code;
r.message = msg;
return r;
}
public static R okMsg(String msg) {
R r = new R();
r.message = msg;
return r;
}
public static R ok() {
return new R();
}
public static <B> R<B> ok(B data) {
return new R<B>(1000 , "success", data);
}
public R<T> put(String key, Object value) {
switch (key) {
case "code" -> this.code = (int) value;
case "message" -> this.message = (String) value;
case "data" -> this.data = (T) value;
}
return this;
}
}

View File

@@ -0,0 +1,114 @@
package com.cool.core.request;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import com.cool.core.enums.UserTypeEnum;
import com.cool.core.util.BodyReaderHttpServletRequestWrapper;
import com.cool.core.util.CoolSecurityUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 封装请求参数 URL参数 和 body JSON 到同一个 JSONObject 方便读取
*/
@Component
@Order(2)
public class RequestParamsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
// 防止流读取一次后就没有了, 所以需要将流继续写出去
HttpServletRequest request = (HttpServletRequest) servletRequest;
JSONObject requestParams = new JSONObject();
String language = request.getHeader("language");
String coolEid = request.getHeader("cool-admin-eid");
Long tenantId = StrUtil.isEmpty(coolEid) ? null : Long.parseLong(coolEid);
if (StrUtil.isNotEmpty(request.getContentType()) && request.getContentType().contains("multipart/form-data")) {
servletRequest.setAttribute("requestParams", requestParams);
servletRequest.setAttribute("cool-language", language);
servletRequest.setAttribute("tenantId", tenantId);
filterChain.doFilter(servletRequest, servletResponse);
} else {
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
String body = requestWrapper.getBodyString(requestWrapper);
if (StrUtil.isNotEmpty(body) && JSONUtil.isTypeJSON(body) && !JSONUtil.isTypeJSONArray(
body)) {
requestParams = JSONUtil.parseObj(body);
}
Object jwtObj = request.getAttribute("tokenInfo");
if (jwtObj != null) {
requestParams.set("tokenInfo", ((JWT) jwtObj).getPayload().getClaimsJson());
}
// 登录状态设置用户id
Long currTenantId = setUserId(requestParams);
if (ObjUtil.isNotNull(currTenantId)) {
tenantId = currTenantId;
}
requestWrapper.setAttribute("cool-language", language);
request.setAttribute("tenantId", tenantId);
requestParams.set("body", body);
requestParams.putAll(getAllRequestParam(request));
requestWrapper.setAttribute("requestParams", requestParams);
filterChain.doFilter(requestWrapper, servletResponse);
}
}
private Long setUserId(JSONObject requestParams) {
UserTypeEnum userTypeEnum = CoolSecurityUtil.getCurrentUserType();
switch (userTypeEnum) {
// 只有登录了,才有用户类型, 不然为 UNKNOWN 状态
case ADMIN -> {
// 管理后台由于之前已经有逻辑再了,怕会影响到,如果自己有传了值不覆盖
Object o = requestParams.get("userId");
if (ObjUtil.isNull(o)) {
requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
}
}
// app端userId 为当前登录的用户id
case APP -> requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
}
return CoolSecurityUtil.getTenantId(requestParams);
}
/**
* 获取客户端请求参数中所有的信息
*
*/
private Map<String, Object> getAllRequestParam(final HttpServletRequest request) {
Map<String, Object> res = new HashMap<>();
Enumeration<?> temp = request.getParameterNames();
if (null != temp) {
while (temp.hasMoreElements()) {
String en = (String) temp.nextElement();
String value = request.getParameter(en);
res.put(en, value);
// 如果字段的值为空,判断若值为空,则删除这个字段>
if (null == res.get(en) || "".equals(res.get(en))) {
res.remove(en);
}
}
}
return res;
}
@Override
public void destroy() {
Filter.super.destroy();
}
}

View File

@@ -0,0 +1,39 @@
package com.cool.core.request;
import com.cool.core.annotation.CoolRestController;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* 通用方法rest接口
*/
@Component
public class RestInterceptor implements HandlerInterceptor {
private final static String[] rests = { "add", "delete", "update", "info", "list", "page" };
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 判断有无通用方法
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
CoolRestController coolRestController = handlerMethod.getBeanType().getAnnotation(CoolRestController.class);
if (null != coolRestController) {
String[] urls = request.getRequestURI().split("/");
String rest = urls[urls.length - 1];
if (Arrays.asList(rests).contains(rest)) {
if (!Arrays.asList(coolRestController.api()).contains(rest)) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return false;
}
}
}
}
return true;
}
}

View File

@@ -0,0 +1,17 @@
package com.cool.core.request.prefix;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* 自定义路由规则
*/
@Component
public class AutoPrefixConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new AutoPrefixUrlMapping();
}
}

View File

@@ -0,0 +1,103 @@
package com.cool.core.request.prefix;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.enums.Apis;
import com.cool.core.util.ConvertUtil;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* 自动配置模块的路由
*/
@Slf4j
public class AutoPrefixUrlMapping extends RequestMappingHandlerMapping {
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
CoolRestController[] annotations = handlerType.getAnnotationsByType(CoolRestController.class);
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
String packageName = handlerType.getPackage().getName();
if (info != null && annotations.length > 0 && annotations[0].value().length == 0
&& packageName.contains("modules")) {
if (!checkApis(annotations, info)) {
return null;
}
String prefix = getPrefix(packageName);
String cName = getCName(annotations[0].cname(), handlerType, prefix);
info = info.mutate().paths(prefix + "/" + cName).build().combine(info);
}
return info;
}
/**
* 根据配置检查是否构建路由
*
* @param annotations 注解
* @param info 路由信息
* @return 是否需要构建路由
*/
private boolean checkApis(CoolRestController[] annotations, RequestMappingInfo info) {
String[] apis = Apis.ALL_API;
if (info.getPathPatternsCondition() == null) {
return true;
}
List<String> setApis;
if (ArrayUtil.isNotEmpty(annotations)) {
CoolRestController coolRestController = annotations[0];
setApis = CollUtil.toList(coolRestController.api());
Set<String> methodPaths = info.getPathPatternsCondition().getPatternValues();
String methodPath = methodPaths.iterator().next().replace("/", "");
if (!CollUtil.toList(apis).contains(methodPath)) {
return true;
} else {
return setApis.contains(methodPath);
}
}
return false;
}
/**
* 根据Controller名称构建路由地址
*
* @param handlerType 类
* @param prefix 路由前缀
* @return url地址
*/
private String getCName(String cname, Class<?> handlerType, String prefix) {
if (ObjUtil.isNotEmpty(cname)) {
return cname;
}
String name = handlerType.getName();
String[] names = name.split("[.]");
name = names[names.length - 1];
return ConvertUtil.extractController2Path(ConvertUtil.pathToClassName(prefix), name);
}
/**
* 构建路由前缀
*
* @param packageName 包名
* @return 返回路由前缀
*/
private String getPrefix(String packageName) {
String dotPath = packageName.split("modules")[1]; // 将包路径中多于的部分截取掉
String[] dotPaths = dotPath.replace(".controller", "").split("[.]");
List<String> paths = CollUtil.toList(dotPaths);
paths.removeIf(String::isEmpty);
// 第一和第二位互换位置
String p0 = paths.get(0);
String p1 = paths.get(1);
paths.set(0, p1);
paths.set(1, p0);
dotPath = "/" + CollUtil.join(paths, "/");
return dotPath;
}
}

View File

@@ -0,0 +1,34 @@
package com.cool.core.security;
import cn.hutool.json.JSONUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
/**
* 自定401返回值
*/
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSONUtil.toJsonStr(new HashMap<String, Object>() {
{
put("code", "401");
put("message", "未登录");
}
}));
response.setStatus(401);
}
}

View File

@@ -0,0 +1,22 @@
package com.cool.core.security;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 忽略地址配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "ignored")
public class IgnoredUrlsProperties {
// 忽略后台校验权限列表
private List<String> adminAuthUrls = new ArrayList<>();
// 忽略记录请求日志列表
private List<String> logUrls = new ArrayList<>();
}

View File

@@ -0,0 +1,110 @@
package com.cool.core.security;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWT;
import com.cool.core.cache.CoolCache;
import com.cool.core.enums.UserTypeEnum;
import com.cool.core.security.jwt.JwtTokenUtil;
import com.cool.core.security.jwt.JwtUser;
import com.cool.core.util.PathUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Token过滤器
*/
@Order(1)
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
final private JwtTokenUtil jwtTokenUtil;
final private CoolCache coolCache;
final private IgnoredUrlsProperties ignoredUrlsProperties;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
if (PathUtils.isMatch(ignoredUrlsProperties.getAdminAuthUrls(), requestURI)) {
// 请求路径在忽略后台鉴权url里支持通配符放行
chain.doFilter(request, response);
return;
}
String authToken = request.getHeader("Authorization");
if (!StrUtil.isEmpty(authToken)) {
JWT jwt = jwtTokenUtil.getTokenInfo(authToken);
Object userType = jwt.getPayload("userType");
if (Objects.equals(userType, UserTypeEnum.APP.name())) {
// app
handlerAppRequest(request, jwt, authToken);
} else {
// admin
handlerAdminRequest(request, jwt, authToken);
}
}
chain.doFilter(request, response);
}
/**
* 处理app请求
*/
private void handlerAppRequest(HttpServletRequest request, JWT jwt, String authToken) {
String userId = jwt.getPayload("userId").toString();
if (ObjectUtil.isNotEmpty(userId)
&& SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = coolCache.get("app:userDetails:" + userId,
JwtUser.class);
if (jwtTokenUtil.validateToken(authToken) && userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
request.setAttribute("userId", jwt.getPayload("userId"));
request.setAttribute("tokenInfo", jwt);
}
}
}
/**
* 处理后台请求
*/
private void handlerAdminRequest(HttpServletRequest request, JWT jwt, String authToken) {
String username = jwt.getPayload("username").toString();
if (username != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = coolCache.get("admin:userDetails:" + username,
JwtUser.class);
Integer passwordV = Convert.toInt(jwt.getPayload("passwordVersion"));
Integer rv = coolCache.get("admin:passwordVersion:" + jwt.getPayload("userId"),
Integer.class);
if (jwtTokenUtil.validateToken(authToken, username) && Objects.equals(passwordV, rv)
&& userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
request.setAttribute("adminUsername", jwt.getPayload("username"));
request.setAttribute("adminUserId", jwt.getPayload("userId"));
request.setAttribute("tokenInfo", jwt);
}
}
}
}

View File

@@ -0,0 +1,152 @@
package com.cool.core.security;
import com.cool.core.annotation.TokenIgnore;
import com.cool.core.enums.UserTypeEnum;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
@EnableWebSecurity
@Configuration
@Slf4j
@RequiredArgsConstructor
public class JwtSecurityConfig {
// 用户详情
final private UserDetailsService userDetailsService;
final private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// 401
final private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
// 403
final private RestAccessDeniedHandler restAccessDeniedHandler;
// 忽略权限控制的地址
final private IgnoredUrlsProperties ignoredUrlsProperties;
final private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// 动态获取忽略的URL
configureIgnoredUrls();
return httpSecurity
.authorizeHttpRequests(
conf -> {
conf.requestMatchers(
ignoredUrlsProperties.getAdminAuthUrls().toArray(String[]::new))
.permitAll();
conf.requestMatchers("/admin/**").authenticated();
conf.requestMatchers("/app/**").hasRole(UserTypeEnum.APP.name());
})
.headers(config -> config.frameOptions(FrameOptionsConfig::disable))
// 允许网页iframe
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(config -> {
config.authenticationEntryPoint(entryPointUnauthorizedHandler);
config.accessDeniedHandler(restAccessDeniedHandler);
}).build();
}
private void configureIgnoredUrls() {
Map<RequestMappingInfo, HandlerMethod> mappings = requestMappingHandlerMapping.getHandlerMethods();
List<String> handlerCtr = new ArrayList<>();
mappings.forEach((requestMappingInfo, handlerMethod) -> {
Method method = handlerMethod.getMethod();
TokenIgnore tokenIgnore = AnnotatedElementUtils.findMergedAnnotation(method, TokenIgnore.class);
TokenIgnore tokenIgnoreCtr = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), TokenIgnore.class);
if (!handlerCtr.contains(handlerMethod.getBeanType().getName()) && tokenIgnoreCtr != null) {
requestMappingInfo.getPathPatternsCondition().getPatterns().forEach(pathPattern -> {
String[] prefixs = pathPattern.getPatternString().split("/");
// 去除最后一个路径
List<String> urls = new ArrayList<>();
for (int i = 0; i < prefixs.length - 1; i++) {
urls.add(prefixs[i]);
}
// 遍历 tokenIgnoreCtr.value()
for (String path : tokenIgnoreCtr.value()) {
ignoredUrlsProperties.getAdminAuthUrls().add(String.join("/", urls) + "/" + path);
}
if (tokenIgnoreCtr.value().length == 0) {
// 通配
ignoredUrlsProperties.getAdminAuthUrls().add(String.join("/", urls)+ "/**");
}
handlerCtr.add(handlerMethod.getBeanType().getName());
});
}
if (tokenIgnore != null) {
StringBuilder url = new StringBuilder();
RequestMapping classRequestMapping = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequestMapping.class);
if (classRequestMapping != null) {
for (String path : classRequestMapping.value()) {
url.append(path);
}
}
if (requestMappingInfo.getPathPatternsCondition() == null) {
return;
}
for (PathPattern path : requestMappingInfo.getPathPatternsCondition().getPatterns()) {
url.append(path);
}
ignoredUrlsProperties.getAdminAuthUrls().add(url.toString());
}
});
}
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(
DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes()));
}
};
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}

View File

@@ -0,0 +1,61 @@
package com.cool.core.security;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
/**
* 权限管理决断器 判断用户拥有的权限或角色是否有资源访问权限
*/
@RequiredArgsConstructor
@Slf4j
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
// 忽略权限控制的地址
final private IgnoredUrlsProperties ignoredUrlsProperties;
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if (configAttributes == null) {
return;
}
List<String> urls = ignoredUrlsProperties.getAdminAuthUrls();
String url = ((FilterInvocation) o).getRequestUrl().split("[?]")[0];
if (urls.contains(url)) {
return;
}
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute c = iterator.next();
String needPerm = c.getAttribute();
for (GrantedAuthority ga : authentication.getAuthorities()) {
// 匹配用户拥有的ga 和 系统中的needPerm
if (needPerm.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("抱歉,您没有访问权限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

View File

@@ -0,0 +1,68 @@
package com.cool.core.security;
import jakarta.annotation.Resource;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
/**
* 权限管理拦截器 监控用户行为
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
final private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Resource
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}

View File

@@ -0,0 +1,34 @@
package com.cool.core.security;
import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
/**
* 自定403返回值
*/
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
throws IOException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSONUtil.toJsonStr(new HashMap<String, Object>() {
{
put("code", "403");
put("message", "无权限");
}
}));
response.setStatus(403);
}
}

View File

@@ -0,0 +1,165 @@
package com.cool.core.security.jwt;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.JWTValidator;
import com.cool.core.config.CoolProperties;
import com.cool.modules.base.service.sys.BaseSysConfService;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* JWT工具类
*/
@Component
@RequiredArgsConstructor
public class JwtTokenUtil implements Serializable {
final private CoolProperties coolProperties;
final private BaseSysConfService baseSysConfService;
final String tokenKey = "JWT_SECRET_TOKEN";
final String refreshTokenKey = "JWT_SECRET_REFRESH_TOKEN";
public long getExpire() {
return this.coolProperties.getToken().getExpire();
}
public long getRefreshExpire() {
return this.coolProperties.getToken().getRefreshExpire();
}
public String getTokenSecret() {
String secret = baseSysConfService.getValueWithCache(tokenKey);
if (StrUtil.isBlank(secret)) {
secret = StrUtil.uuid().replaceAll("-", "");
baseSysConfService.setValue(tokenKey, secret);
}
return secret;
}
public String getRefreshTokenSecret() {
String secret = baseSysConfService.getValueWithCache(refreshTokenKey);
if (StrUtil.isBlank(secret)) {
secret = StrUtil.uuid().replaceAll("-", "");
baseSysConfService.setValue(refreshTokenKey, secret);
}
return secret;
}
/**
* 生成令牌
*
* @param tokenInfo 保存的用户信息
* @return 令牌
*/
public String generateToken(Map<String, Object> tokenInfo) {
tokenInfo.put("isRefresh", false);
Date expirationDate = new Date(System.currentTimeMillis() + getExpire() * 1000);
JWT jwt = JWT.create().setExpiresAt(expirationDate).setKey(getTokenSecret().getBytes())
.setPayload("created", new Date());
tokenInfo.forEach(jwt::setPayload);
return jwt.sign();
}
/**
* 生成令牌
*
* @param tokenInfo 保存的用户信息
* @return 令牌
*/
public String generateRefreshToken(Map<String, Object> tokenInfo) {
tokenInfo.put("isRefresh", true);
Date expirationDate = new Date(System.currentTimeMillis() + getRefreshExpire() * 1000);
JWT jwt = JWT.create().setExpiresAt(expirationDate).setKey(getRefreshTokenSecret().getBytes())
.setPayload("created", new Date());
tokenInfo.forEach(jwt::setPayload);
return jwt.sign();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
JWT jwt = JWT.of(token);
return jwt.getPayload("username").toString();
}
/**
* 获得token信息
*
* @param token 令牌
* @return token信息
*/
public JWT getTokenInfo(String token) {
return JWT.of(token);
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
JWTValidator.of(token).validateDate(DateUtil.date());
return false;
} catch (Exception e) {
return true;
}
}
/**
* 验证令牌
*
* @param token 令牌
* @param username 用户
* @return 是否有效
*/
public Boolean validateToken(String token, String username) {
if (ObjectUtil.isEmpty(token)) {
return false;
}
String tokenUsername = getUsernameFromToken(token);
String secret = getTokenSecret();
boolean isValidSignature = JWTUtil.verify(token, secret.getBytes());
return (tokenUsername.equals(username) && !isTokenExpired(token) && isValidSignature);
}
/**
* 校验token是否有效
* @param token
* @return
*/
public Boolean validateToken(String token) {
if (ObjectUtil.isEmpty(token)) {
return false;
}
String secret = getTokenSecret();
boolean isValidSignature = JWTUtil.verify(token, secret.getBytes());
return (!isTokenExpired(token) && isValidSignature);
}
/**
* 校验refresh token是否有效
* @param token
* @return
*/
public Boolean validateRefreshToken(String token) {
if (ObjectUtil.isEmpty(token)) {
return false;
}
String secret = getRefreshTokenSecret();
boolean isValidSignature = JWTUtil.verify(token, secret.getBytes());
return (!isTokenExpired(token) && isValidSignature);
}
}

View File

@@ -0,0 +1,78 @@
package com.cool.core.security.jwt;
import com.cool.core.enums.UserTypeEnum;
import java.util.Collection;
import java.util.List;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 后台用户信息
*/
@Data
public class JwtUser implements UserDetails {
/******
* 后台用户
* ********/
private Long userId;
private String username;
private String password;
private Boolean status;
private UserTypeEnum userTypeEnum;
private List<GrantedAuthority> perms;
public JwtUser(Long userId, String username, String password, List<GrantedAuthority> perms, Boolean status) {
this.userId = userId;
this.username = username;
this.password = password;
this.perms = perms;
this.status = status;
this.userTypeEnum = UserTypeEnum.ADMIN;
}
/******
* app用户
* ********/
public JwtUser(Long userId, List<GrantedAuthority> perms, Boolean status) {
this.userId = userId;
this.perms = perms;
this.status = status;
this.userTypeEnum = UserTypeEnum.APP;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return perms;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return status;
}
}

View File

@@ -0,0 +1,14 @@
package com.cool.core.tenant;
import com.cool.core.util.TenantUtil;
import com.mybatisflex.core.tenant.TenantFactory;
public class CoolTenantFactory implements TenantFactory {
public Object[] getTenantIds(){
Long tenantId = TenantUtil.getTenantId();
if (tenantId == null) {
return null;
}
return new Object[]{tenantId};
}
}

View File

@@ -0,0 +1,73 @@
package com.cool.core.util;
import com.cool.core.annotation.CoolPlugin;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
@Slf4j
public class AnnotationUtils {
/**
* 判断一个类是否有 Spring 核心注解
*
* @param clazz 要检查的类
* @return true 如果该类上添加了相应的 Spring 注解;否则返回 false
*/
public static boolean hasSpringAnnotation(Class<?> clazz) {
if (clazz == null) {
return false;
}
// 是否是接口
if (clazz.isInterface()) {
return false;
}
// 是否是抽象类
if (Modifier.isAbstract(clazz.getModifiers())) {
return false;
}
try {
if (clazz.getAnnotation(Component.class) != null || clazz.getAnnotation(Repository.class) != null
|| clazz.getAnnotation(Service.class) != null || clazz.getAnnotation(Controller.class) != null
|| clazz.getAnnotation(Configuration.class) != null) {
return true;
}
} catch (Exception e) {
log.error("出现异常:{}", e.getMessage());
}
return false;
}
/**
* 插件
*/
public static boolean hasCoolPluginAnnotation(Class<?> clazz) {
if (clazz == null) {
return false;
}
// 是否是接口
if (clazz.isInterface()) {
return false;
}
// 是否是抽象类
if (Modifier.isAbstract(clazz.getModifiers())) {
return false;
}
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (clazz.getAnnotation(
(Class<? extends Annotation>) contextClassLoader.loadClass(CoolPlugin.class.getName())) != null) {
return true;
}
} catch (Exception e) {
log.error("出现异常:{}", e.getMessage(), e);
}
return false;
}
}

View File

@@ -0,0 +1,27 @@
package com.cool.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import java.io.Serializable;
public class AutoTypeConverter {
/**
* 将字符串自动转换为数字或保留为字符串
*
* @param input 输入字符串
* @return Integer / Long / String
*/
public static Serializable autoConvert(Object input) {
if (input == null) {
return null;
}
if (NumberUtil.isInteger(input.toString())) {
return Convert.convert(Integer.class, input);
} else if (NumberUtil.isLong(input.toString())) {
return Convert.convert(Long.class, input);
} else {
return (Serializable) input;
}
}
}

View File

@@ -0,0 +1,119 @@
package com.cool.core.util;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.nio.charset.Charset;
/**
* 保存流
*/
@Slf4j
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
String sessionStream = getBodyString(request);
body = sessionStream.getBytes(Charset.forName("UTF-8"));
}
/**
* 获取请求Body
*
* @param request
* @return
*/
public String getBodyString(final ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = cloneInputStream(request.getInputStream());
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("err", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error("err", e);
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error("err", e);
}
}
}
return sb.toString();
}
/**
* Description: 复制输入流</br>
*
* @param inputStream
* @return</br>
*/
public InputStream cloneInputStream(ServletInputStream inputStream) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = inputStream.read(buffer)) > -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
byteArrayOutputStream.flush();
} catch (IOException e) {
log.error("err", e);
}
InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return byteArrayInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}

View File

@@ -0,0 +1,180 @@
package com.cool.core.util;
import cn.hutool.core.io.FileUtil;
import com.mybatisflex.processor.MybatisFlexProcessor;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.annotation.processing.Processor;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CompilerUtils {
public final static String META_INF_VERSIONS = "META-INF/versions/";
// jdk版本
private static String JVM_VERSION = null;
/**
* 获取jdk版本
*/
public static String getJdkVersion() {
if (JVM_VERSION == null) {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
JVM_VERSION = runtimeMXBean.getSpecVersion();
}
return JVM_VERSION;
}
/**
* 创建文件, 先删除在创建
*/
public static void createFile(String content, String filePathStr) {
FileUtil.del(filePathStr);
File file = FileUtil.touch(filePathStr);
FileUtil.appendString(content, file, StandardCharsets.UTF_8.name());
compileAndSave(filePathStr);
}
public static String createMapper(String actModulePath, String fileName, String mapper) {
String pathStr = actModulePath + File.separator + "mapper" + File.separator;
String filePathStr = pathStr + fileName + "Mapper.java";
createFile(mapper, filePathStr);
return filePathStr;
}
public static String createServiceImpl(String actModulePath, String fileName,
String serviceImpl) {
String pathStr = actModulePath + File.separator + "service" + File.separator + "impl" + File.separator;
String filePathStr = pathStr + fileName + "ServiceImpl.java";
createFile(serviceImpl, filePathStr);
return filePathStr;
}
public static String createService(String actModulePath, String fileName, String service) {
String pathStr = actModulePath + File.separator + "service" + File.separator;
String filePathStr = pathStr + fileName + "Service.java";
createFile(service, filePathStr);
return filePathStr;
}
public static String createEntity(String actModulePath, String fileName, String entity) {
String pathStr = actModulePath + File.separator + "entity" + File.separator;
String filePathStr = pathStr + fileName + "Entity.java";
createFile(entity, filePathStr);
return filePathStr;
}
public static String createController(String actModulePath, String fileName, String controller) {
String pathStr = actModulePath + File.separator + "controller" + File.separator + "admin" + File.separator;
String filePathStr = pathStr + "Admin" + fileName + "Controller.java";
createFile(controller, filePathStr);
return filePathStr;
}
public static String createModule(String modulesPath, String module) {
String pathStr = modulesPath + File.separator + module;
PathUtils.noExistsMk(pathStr);
return pathStr;
}
public static boolean compileAndSave(String sourceFile) {
// 获取系统 Java 编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 获取标准文件管理器
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
// 设置编译输出目录
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(new File("target" + File.separator + "classes")));
// 获取源文件
List<File> javaFiles = List.of(new File(sourceFile));
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(javaFiles);
// 创建编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
// 执行编译任务
return task.call();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static void compilerEntityTableDef(String actModulePath, String fileName, String entityPath, List<String> javaPathList) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(
entityPath);
// 设置注解处理器
Iterable<? extends Processor> processors = List.of(new MybatisFlexProcessor());
// 添加 -proc:only 选项
List<String> options = List.of("-proc:only");
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options,
null, compilationUnits);
task.setProcessors(processors);
task.call();
compilationUnits = fileManager.getJavaFileObjects(
javaPathList.toArray(new String[0]));
// 设置编译输出目录
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(new File("target/classes")));
task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
String pathStr = actModulePath + File.separator + "entity" + File.separator + "table" + File.separator;
String filePathStr = pathStr + fileName + "EntityTableDef.java";
// 需在entity之后加载
javaPathList.add(1, filePathStr);
boolean success = task.call();
if (success) {
System.out.println("Compilation and annotation processing completed successfully.");
// 指定源文件夹和目标文件夹
File sourceDir = new File("com");
File destinationDir = new File(PathUtils.getTargetGeneratedAnnotations());
// 确保目标文件夹存在
destinationDir.mkdirs();
// 移动源文件夹内容到目标文件夹
if (sourceDir.exists()) {
FileUtil.move(sourceDir, destinationDir, true);
}
if (countFiles(sourceDir) <= 1) {
FileUtil.clean(sourceDir);
FileUtil.del(sourceDir);
}
} else {
System.out.println("Compilation and annotation processing failed.");
}
} catch (IOException e) {
log.error("compilerEntityTableDefError", e);
}
}
private static int countFiles(File directory) {
File[] files = directory.listFiles();
if (files == null) {
return 0;
}
int count = 0;
for (File file : files) {
if (file.isFile()) {
count++;
} else if (file.isDirectory()) {
count += countFiles(file);
}
// If more than one file is found, no need to continue counting
if (count > 1) {
break;
}
}
return count;
}
}

View File

@@ -0,0 +1,257 @@
package com.cool.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.web.multipart.MultipartFile;
/**
* 转换
*/
public class ConvertUtil {
/**
* 对象转数组
*
* @param obj
* @return
*/
public static byte[] toByteArray(Object obj) {
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
bytes = bos.toByteArray();
oos.close();
bos.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return bytes;
}
/**
* 数组转对象
*
* @param bytes
* @return
*/
public static Object toObject(byte[] bytes) {
Object obj = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
obj = ois.readObject();
ois.close();
bis.close();
} catch (IOException | ClassNotFoundException ex) {
ex.printStackTrace();
}
return obj;
}
public static MultipartFile convertToMultipartFile(File file) {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
return new SimpleMultipartFile(file.getName(), inputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
return null;
}
}
// 简单的MultipartFile实现用于模拟Spring中的MultipartFile对象
static class SimpleMultipartFile implements MultipartFile {
private String filename;
private InputStream inputStream;
public SimpleMultipartFile(String filename, InputStream inputStream) {
this.filename = filename;
this.inputStream = inputStream;
}
@Override
public String getName() {
return null;
}
@Override
public String getOriginalFilename() {
return filename;
}
@Override
public String getContentType() {
return "application/octet-stream";
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public long getSize() {
try {
return inputStream.available();
} catch (IOException e) {
e.printStackTrace();
return 0;
}
}
@Override
public byte[] getBytes() throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
return output.toByteArray();
}
@Override
public InputStream getInputStream() throws IOException {
return inputStream;
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
try (FileOutputStream outputStream = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
}
/**
* /admin/goods 转 AdminGoods
*/
public static String pathToClassName(String path) {
// 按斜杠分割字符串
String[] parts = path.split("/");
StringBuilder className = new StringBuilder();
for (String part : parts) {
// 将每个部分的首字母大写,并追加到 StringBuilder 中
className.append(StrUtil.upperFirst(part));
}
return className.toString();
}
public static String extractController2Path(String prefix, String className) {
Pattern pattern = Pattern.compile("([A-Za-z0-9]+)Controller$");
Matcher matcher = pattern.matcher(className);
if (matcher.find()) {
String extracted = matcher.group(1);
// 将前缀拆分为单词数组
String[] prefixWords = splitCamelCase(prefix);
String[] classWords = splitCamelCase(extracted);
// 从前缀和类名中逐个匹配并去除匹配的部分
int i = 0;
for (int j = 0; i < prefixWords.length; j++) {
if (j >= classWords.length) {
break;
}
for (String prefixWord : prefixWords) {
if (prefixWord.equalsIgnoreCase(classWords[i])) {
i++;
break;
}
}
}
// 从当前位置开始,拼接剩余部分
return String.join("/", java.util.Arrays.copyOfRange(classWords, i, classWords.length)).toLowerCase();
}
return "";
}
// 拆分驼峰命名的字符串为单词数组
private static String[] splitCamelCase(String input) {
return input.split("(?<=.)(?=[A-Z])");
}
/**
* 将给定的字段值转换为可序列化的形式
* 此方法旨在将一个对象的特定字段值转换为其相应的可序列化类型
* 它在序列化和反序列化过程中特别有用,确保字段值可以被正确处理
*
* @param fieldName 字段名称,用于查找字段类型
* @param fieldValue 待转换的字段值
* @param clazz 包含该字段的类
* @return 转换后的可序列化字段值,如果无法确定字段类型,则返回原始值
*/
public static Object convertByClass(String fieldName, Object fieldValue, Class<?> clazz) {
// 检查输入参数是否为空,如果字段名或字段值为空,则直接返回字段值
if (fieldName == null || fieldValue == null) {
return fieldValue;
}
// 获取字段类型
Class<?> fieldType = getFieldType(clazz, fieldName);
// 如果字段类型为空,则直接返回字段值
if (fieldType == null) {
return fieldValue;
}
// 使用Convert类的convert方法将字段值转换为字段类型
return Convert.convert(fieldType, fieldValue);
}
public static List<Object> covertListByClass(String fieldName, List<Object> fieldValue, Class<?> clazz) {
// 检查输入参数是否为空,如果字段名或字段值为空,则直接返回字段值
if (fieldName == null || fieldValue == null) {
return fieldValue;
}
// 获取字段类型
Class<?> fieldType = getFieldType(clazz, fieldName);
// 如果字段类型为空,则直接返回字段值
if (fieldType == null) {
return fieldValue;
}
return Collections.singletonList(Convert.toList(fieldType, fieldValue));
}
/**
* 获取指定类中指定字段的类型
*
* @param clazz 目标类
* @param fieldName 字段名称
* @return 字段的类型 Class如果字段不存在则返回 null
*/
public static Class<?> getFieldType(Class<?> clazz, String fieldName) {
Field field = ReflectUtil.getField(clazz, fieldName);
return field != null ? field.getType() : null;
}
}

View File

@@ -0,0 +1,165 @@
package com.cool.core.util;
import cn.hutool.json.JSONUtil;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.plugin.consts.PluginConsts;
import com.cool.core.plugin.service.DynamicJarLoaderService;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
/**
* 插件调用封装
*/
@Slf4j
public class CoolPluginInvokers {
private static final DynamicJarLoaderService dynamicJarLoaderService = SpringContextUtils
.getBean(DynamicJarLoaderService.class);
/**
* 插件默认调用入口
*/
public static Object invokePlugin(String key, String... params) {
return invoke(key, PluginConsts.invokePluginMethodName, params);
}
/**
* 设置插件配置信息
*/
public static void setPluginJson(String key, PluginInfoEntity entity) {
invoke(key, PluginConsts.setPluginJson, JSONUtil.toJsonStr(entity.getPluginJson()));
setApplicationContext(key);
}
/**
* 设置 ApplicationContext 到插件类中
*/
public static void setApplicationContext(String key) {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread()
.setContextClassLoader(dynamicJarLoaderService.getDynamicJarClassLoader(key));
Object beanInstance = dynamicJarLoaderService.getBeanInstance(key);
Method method = beanInstance.getClass().getSuperclass()
.getMethod(PluginConsts.setApplicationContext,
ApplicationContext.class);
method.invoke(beanInstance, SpringContextUtils.applicationContext);
} catch (Exception e) {
log.error("setApplicationContext err", e);
} finally {
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
}
/**
* 反射调用插件
*
* @param key 插件key
* @param methodName 插件方法
* @param params 参数
*/
public static Object invoke(String key, String methodName, Object... params) {
Object beanInstance = dynamicJarLoaderService.getBeanInstance(key);
CoolPreconditions.checkEmpty(beanInstance, "未找到该插件:{}, 请前往插件市场进行安装",key);
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
try {
// 设置当前线程的上下文类加载器为插件的类加载器
Thread.currentThread()
.setContextClassLoader(dynamicJarLoaderService.getDynamicJarClassLoader(key));
log.info("调用插件类: {}, 方法: {} 参数: {}", key, methodName, params);
return invoke(beanInstance, methodName, params);
} catch (Exception e) {
log.error("调用插件{}.{}失败", key, methodName, e);
CoolPreconditions.alwaysThrow("调用插件{}.{}失败 {}", key, methodName, e.getMessage());
} finally {
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
return null;
}
/**
* 反射调用插件
*
* @param beanInstance 插件实例对象
* @param methodName 插件方法
* @param params 参数
*/
private static Object invoke(Object beanInstance, String methodName, Object[] params)
throws InvocationTargetException, IllegalAccessException {
Class<?>[] paramTypes = Arrays.stream(params).map(Object::getClass)
.toArray(Class<?>[]::new);
Method method = findMethod(beanInstance.getClass(), methodName, paramTypes);
CoolPreconditions.check(method == null, "No such method: {} with parameters {}", methodName,
Arrays.toString(paramTypes));
if (method.isVarArgs()) {
// 处理可变参数调用
int varArgIndex = method.getParameterTypes().length - 1;
Object[] varArgs = (Object[]) java.lang.reflect.Array.newInstance(
method.getParameterTypes()[varArgIndex].getComponentType(),
params.length - varArgIndex);
System.arraycopy(params, varArgIndex, varArgs, 0, varArgs.length);
Object[] methodArgs = new Object[varArgIndex + 1];
System.arraycopy(params, 0, methodArgs, 0, varArgIndex);
methodArgs[varArgIndex] = varArgs;
return method.invoke(beanInstance, methodArgs);
} else {
// 正常调用
return method.invoke(beanInstance, params);
}
}
// 查找方法,包括处理可变参数
private static Method findMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) {
try {
return clazz.getMethod(methodName, paramTypes);
} catch (NoSuchMethodException e) {
// Try to find a varargs method
for (Method method : clazz.getMethods()) {
if (method.getName().equals(methodName) && isAssignable(paramTypes,
method.getParameterTypes(), method.isVarArgs())) {
return method;
}
}
// If not found, try to find in superclass
if (clazz.getSuperclass() != null) {
return findMethod(clazz.getSuperclass(), methodName, paramTypes);
}
}
return null;
}
private static boolean isAssignable(Class<?>[] paramTypes, Class<?>[] methodParamTypes,
boolean isVarArgs) {
if (isVarArgs) {
if (paramTypes.length < methodParamTypes.length - 1) {
return false;
}
for (int i = 0; i < methodParamTypes.length - 1; i++) {
if (!methodParamTypes[i].isAssignableFrom(paramTypes[i])) {
return false;
}
}
Class<?> varArgType = methodParamTypes[methodParamTypes.length - 1].getComponentType();
for (int i = methodParamTypes.length - 1; i < paramTypes.length; i++) {
if (!varArgType.isAssignableFrom(paramTypes[i])) {
return false;
}
}
return true;
} else {
if (paramTypes.length != methodParamTypes.length) {
return false;
}
for (int i = 0; i < paramTypes.length; i++) {
if (!methodParamTypes[i].isAssignableFrom(paramTypes[i])) {
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,110 @@
package com.cool.core.util;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import com.cool.core.cache.CoolCache;
import com.cool.core.enums.UserTypeEnum;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.security.jwt.JwtUser;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
/**
* Security 工具类
*/
public class CoolSecurityUtil {
private static final CoolCache coolCache = SpringUtil.getBean(CoolCache.class);
/***************后台********************/
/**
* 获取后台登录的用户名
*/
public static String getAdminUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
/**
* 获得jwt中的信息
*
* @param requestParams 请求参数
* @return jwt
*/
public static JSONObject getAdminUserInfo(JSONObject requestParams) {
JSONObject tokenInfo = requestParams.getJSONObject("tokenInfo");
if (tokenInfo != null) {
tokenInfo.set("department",
coolCache.get("admin:department:" + tokenInfo.get("userId")));
tokenInfo.set("roleIds", coolCache.get("admin:roleIds:" + tokenInfo.get("userId")));
}
return tokenInfo;
}
public static Long getTenantId(JSONObject requestParams) {
JSONObject tokenInfo = requestParams.getJSONObject("tokenInfo");
if (tokenInfo != null) {
return tokenInfo.getLong("tenantId");
}
return null;
}
/**
* 后台账号退出登录
*
* @param adminUserId 用户ID
* @param username 用户名
*/
public static void adminLogout(Long adminUserId, String username) {
coolCache.del("admin:department:" + adminUserId, "admin:passwordVersion:" + adminUserId,
"admin:userInfo:" + adminUserId, "admin:userDetails:" + username);
}
/**
* 后台账号退出登录
*
* @param userEntity 用户
*/
public static void adminLogout(BaseSysUserEntity userEntity) {
adminLogout(userEntity.getId(), userEntity.getUsername());
}
/**
* 获取当前用户id
*/
public static Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
return ((JwtUser) principal).getUserId();
}
}
CoolPreconditions.check(true, 401, "未登录");
return null;
}
/**
* 获取当前用户类型
*/
public static UserTypeEnum getCurrentUserType() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
return ((JwtUser) principal).getUserTypeEnum();
}
}
// 还未登录,未知类型
return UserTypeEnum.UNKNOWN;
}
/**
* app退出登录,移除缓存信息
*/
public static void appLogout() {
coolCache.del("app:userDetails"+ getCurrentUserId());
}
}

View File

@@ -0,0 +1,56 @@
package com.cool.core.util;
import com.cool.core.exception.CoolPreconditions;
import org.dromara.autotable.core.constants.DatabaseDialect;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
/**
* 获取数据库方言
*/
public class DatabaseDialectUtils {
private static String dialect;
public static String getDatabaseDialect(DataSource dataSource) {
if (dialect == null) {
dialect = determineDatabaseType(dataSource);
}
return dialect;
}
public static boolean isPostgresql() {
DataSource dataSource = SpringContextUtils.getBean(DataSource.class);
return DatabaseDialect.PostgreSQL.equals(getDatabaseDialect(dataSource));
}
public static boolean isPostgresql(DataSource dataSource) {
return DatabaseDialect.PostgreSQL.equals(getDatabaseDialect(dataSource));
}
private static String determineDatabaseType(DataSource dataSource) {
// 从 DataSource 获取连接
try (Connection connection = dataSource.getConnection()) {
// 获取元数据
DatabaseMetaData metaData = connection.getMetaData();
String productName = metaData.getDatabaseProductName();
return inferDatabaseTypeFromProductName(productName);
} catch (SQLException e) {
throw new RuntimeException("Failed to determine database dialect", e);
}
}
private static String inferDatabaseTypeFromProductName(String productName) {
if (productName.startsWith(DatabaseDialect.MySQL)) {
return DatabaseDialect.MySQL;
} else if (productName.startsWith(DatabaseDialect.PostgreSQL)) {
return DatabaseDialect.PostgreSQL;
} else if (productName.startsWith(DatabaseDialect.SQLite)) {
return DatabaseDialect.SQLite;
}
CoolPreconditions.alwaysThrow("暂不支持!");
return "unknown";
}
}

View File

@@ -0,0 +1,121 @@
package com.cool.core.util;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Editor;
import cn.hutool.core.util.ObjUtil;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.query.QueryColumn;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
public class EntityUtils {
private static Map<String, Class<?>> TABLE_MAP;
public static Set<String> findEntityClassName() {
Set<String> entitySet = new HashSet<>();
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = null;
try {
resources = resolver.getResources("classpath*:com/cool/**/entity/**/*Entity.class");
for (Resource r : resources) {
String path = r.getURL().getPath();
String className = path.substring(path.indexOf("com/cool"),
path.lastIndexOf('.')).replace('/', '.');
entitySet.add(className);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return entitySet;
}
public static Map<String, Class<?>> findTableMap() {
if (ObjUtil.isEmpty(TABLE_MAP)) {
init();
}
return TABLE_MAP;
}
private static void init() {
Set<String> classNames = EntityUtils.findEntityClassName();
TABLE_MAP = new HashMap<>();
classNames.forEach(className -> {
Class<?> entityClass;
try {
entityClass = Class.forName(className);
Table tableAnnotation = AnnotationUtil.getAnnotation(entityClass, Table.class);
// key表名value 实体对象
TABLE_MAP.put(tableAnnotation.value(), entityClass);
} catch (Exception e) {
// do nothing
}
});
}
/**
* 获取实体类及其父类的字段名数组(排除指定字段)
*
* @return 字段名数组
*/
public static QueryColumn[] getFieldNamesWithSuperClass(QueryColumn[] queryColumns,
String... excludeNames) {
return getFieldNamesListWithSuperClass(queryColumns, excludeNames).toArray(
new QueryColumn[0]);
}
public static List<QueryColumn> getFieldNamesListWithSuperClass(QueryColumn[] queryColumns,
String... excludeNames) {
ArrayList<String> excludeList = new ArrayList<>(List.of(excludeNames));
return Arrays.stream(queryColumns).toList().stream()
.filter(o -> !excludeList.contains(o.getName())).toList();
}
/**
* 将bean的部分属性转换成map<br>
* 可选拷贝哪些属性值,默认是不忽略值为{@code null}的值的。
*
* @param bean bean
* @param ignoreProperties 需要忽略拷贝的属性值,{@code null}或空表示拷贝所有值
* @return Map
* @since 5.8.0
*/
public static Map<String, Object> toMap(Object bean, String... ignoreProperties) {
int mapSize = 16;
Editor<String> keyEditor = null;
final Set<String> propertiesSet = CollUtil.set(false, ignoreProperties);
propertiesSet.add("queryWrapper");
mapSize = ignoreProperties.length;
keyEditor = property -> !propertiesSet.contains(property) ? property : null;
// 指明了要复制的属性 所以不忽略null值
return BeanUtil.beanToMap(bean, new LinkedHashMap<>(mapSize, 1), false, keyEditor);
}
/**
* 检查字段名是否在排除列表中
*
* @param fieldName 要检查的字段名
* @param excludeNames 排除的字段名数组
* @return 是否在排除列表中
*/
private static boolean isExcluded(String fieldName, String[] excludeNames) {
for (String excludeName : excludeNames) {
if (fieldName.equals(excludeName)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,125 @@
package com.cool.core.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
@Slf4j
@Component
public class I18nUtil {
public static final String MSG_PREFIX = "msg_";
public static final String MENU_PREFIX = "menu_";
public static final String DICT_INFO_PREFIX = "dictInfo_";
public static final String DICT_TYPE_PREFIX = "dictType_";
public static boolean enable = false;
public static String path;
public static String getLanguage() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return null;
}
return (String) attributes.getAttribute("cool-language", RequestAttributes.SCOPE_REQUEST);
}
private static final Map<String, JSONObject> data = new ConcurrentHashMap<>();
private void load(String key, File file) {
try {
String content = FileUtil.readUtf8String(file);
data.put(key, JSONUtil.parseObj(content));
} catch (Exception e) {
log.error("读取国际化文件失败", e);
}
}
public boolean exist(String name) {
// 获取该目录下所有的 .json 文件
List<File> jsonFiles = FileUtil.loopFiles(getPath(), file ->
file.isFile() && file.getName().endsWith(".json")
);
AtomicReference<Boolean> flag = new AtomicReference<>(false);
jsonFiles.forEach(file -> {
String parentName = file.getParentFile().getName();
String key = parentName + "_" + file.getName().replace(".json", "");
if (key.equals(name)) {
flag.set(true);
// 加载
load(key, file);
}
});
return flag.get();
}
public static String getI18nMenu(String name) {
return getI18n(name, MENU_PREFIX);
}
public static String getI18nMsg(String name) {
return getI18n(name, MSG_PREFIX);
}
public static String getI18nDictInfo(String name) {
return getI18n(name, DICT_INFO_PREFIX);
}
public static String getI18nDictType(String name) {
return getI18n(name, DICT_TYPE_PREFIX);
}
private static String getI18n(String name, String prefix) {
if (!enable) {
return name;
}
String language = I18nUtil.getLanguage();
if (language == null) {
return name;
}
JSONObject jsonObject = data.get(prefix + language);
if (jsonObject == null) {
return name;
}
String str = jsonObject.getStr(name);
if (str == null) {
return name;
}
return str;
}
public void update(String key, JSONObject object) {
data.put(key, object);
String[] split = key.split("_");
String absolutePath = getPath();
File file = FileUtil.file(absolutePath, split[0], split[1] + ".json");
// 确保父目录存在
FileUtil.mkParentDirs(file);
// 写入内容
FileUtil.writeUtf8String(JSONUtil.toJsonStr(object), file);
}
private String getPath() {
String absolutePath = path;
if (!PathUtils.isAbsolutePath(absolutePath)) {
absolutePath = PathUtils.getUserDir() + File.separator + absolutePath;
}
return absolutePath;
}
public void clear() {
data.clear();
List<File> jsonFiles = FileUtil.loopFiles(getPath(), file ->
file.isFile() && file.getName().endsWith(".json")
);
jsonFiles.forEach(File::delete);
enable = false;
}
}

View File

@@ -0,0 +1,46 @@
package com.cool.core.util;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* IP地址
*/
@Slf4j
@Component
public class IPUtils {
/**
* 获取IP地址
* <p>
* 使用Nginx等反向代理软件 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话X-Forwarded-For的值并不止一个而是一串IP地址X-Forwarded-For中第一个非unknown的有效IP字符串则为真实IP地址
*/
public String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
log.error("IP extraction error", e);
}
return ip;
}
}

View File

@@ -0,0 +1,30 @@
package com.cool.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import java.util.Map;
public class MapExtUtil extends MapUtil {
/**
* 比较两个map key 和 value 是否一致
*/
public static boolean compareMaps(Map<String, Object> map1, Map<String, Object> map2) {
if (ObjectUtil.isEmpty(map1) || ObjectUtil.isEmpty(map2)) {
return true;
}
if (map1.size() != map2.size()) {
return false;
}
for (Map.Entry<String, Object> entry : map1.entrySet()) {
if (!map2.containsKey(entry.getKey())) {
return false;
}
if (!ObjectUtil.equal(entry.getValue(), map2.get(entry.getKey()))) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,21 @@
package com.cool.core.util;
/**
* 自定义映射算法
* 将 ID 转换为一个混淆形式的数字,然后能够逆向转换回原始 ID。
* 场景混淆订单id
*/
public class MappingAlgorithm {
private static final long ENCRYPTION_KEY = 123456789L; // 任意密钥
// 将 ID 转换为混淆的数字
public static long encrypt(long id) {
return id ^ ENCRYPTION_KEY; // 使用异或操作进行混淆
}
// 将混淆的数字恢复为原始的 ID
public static long decrypt(long encryptedId) {
return encryptedId ^ ENCRYPTION_KEY; // 逆操作恢复原始 ID
}
}

View File

@@ -0,0 +1,70 @@
package com.cool.core.util;
import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.text.AntPathMatcher;
import com.cool.CoolApplication;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class PathUtils {
private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
public static boolean isAbsolutePath(String pathStr) {
Path path = Paths.get(pathStr);
return path.isAbsolute();
}
public static String getUserDir() {
return System.getProperty("user.dir");
}
public static String getModulesPath() {
return getUserDir() + getSrcMainJava() + File.separator + CoolApplication.class.getPackageName()
.replace(".", File.separator) + File.separator + "modules";
}
public static String getSrcMainJava() {
return File.separator + "src" + File.separator + "main" + File.separator + "java";
}
public static String getTargetGeneratedAnnotations() {
return "target" + File.separator + "generated-sources" + File.separator + "annotations";
}
public static String getClassName(String filePath) {
// 定位 "/src/main/java" 在路径中的位置
int srcMainJavaIndex = filePath.indexOf(getSrcMainJava());
if (srcMainJavaIndex == -1) {
throw new IllegalArgumentException("File path does not contain 'src/main/java'");
}
// 提取 "src/main/java" 之后的路径
// 将文件分隔符替换为包分隔符
return filePath.substring(srcMainJavaIndex + ("src" + File.separator + "main" + File.separator + "java").length() + 2)
.replace(File.separator, ".").replace(".java", "");
}
/**
* 路径不存在创建
*/
public static void noExistsMk(String pathStr) {
Path path = Paths.get(pathStr);
if (PathUtil.exists(path, false)) {
PathUtil.mkParentDirs(path);
}
}
/**
* 判断给定的请求URI是否匹配列表中的任意一个URL模式
* 使用Ant风格的路径匹配来处理URL模式提供了一种通配符匹配的方法
*
* @param urls 待匹配的URL模式列表
* @param requestURI 请求的URI
* @return 如果请求URI匹配列表中的任意一个URL模式则返回true否则返回false
*/
public static boolean isMatch(List<String> urls, String requestURI) {
return urls.stream()
.anyMatch(url -> antPathMatcher.match(url, requestURI));
}
}

Some files were not shown because too many files have changed in this diff Show More