diff --git a/huacai-framework/pom.xml b/huacai-framework/pom.xml new file mode 100644 index 0000000..266d78e --- /dev/null +++ b/huacai-framework/pom.xml @@ -0,0 +1,64 @@ + + + + huacai + com.huacai + 3.8.7 + + 4.0.0 + + huacai-framework + + + framework框架核心 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.alibaba + druid-spring-boot-starter + + + + + pro.fessional + kaptcha + + + servlet-api + javax.servlet + + + + + + + com.github.oshi + oshi-core + + + + + com.huacai + huacai-system + + + + + diff --git a/huacai-framework/src/main/java/com/huacai/framework/aspectj/DataScopeAspect.java b/huacai-framework/src/main/java/com/huacai/framework/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..083b2fa --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,213 @@ +package com.huacai.framework.aspectj; + +import java.util.ArrayList; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import com.huacai.common.annotation.DataScope; +import com.huacai.common.core.domain.BaseEntity; +import com.huacai.common.core.domain.entity.SysRole; +import com.huacai.common.core.domain.entity.SysUser; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.core.text.Convert; +import com.huacai.common.utils.SecurityUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.framework.security.context.PermissionContextHolder; + +/** + * 数据过滤处理切面 + * 用于通过AOP实现基于角色的数据权限控制,动态拼接数据过滤SQL + * + * @author huacai + */ +// 标记为AOP切面类 +@Aspect +// 注册为Spring组件 +@Component +public class DataScopeAspect +{ + /** + * 全部数据权限(可查看所有数据) + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定义数据权限(可查看指定部门数据) + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限(可查看本部门数据) + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限(可查看本部门及子部门数据) + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限(仅可查看自己的数据) + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字(用于在SQL中占位) + */ + public static final String DATA_SCOPE = "dataScope"; + + /** + * 方法执行前拦截,处理数据权限 + * + * @param point 切点对象 + * @param controllerDataScope 方法上的@DataScope注解对象 + * @throws Throwable 可能的异常 + */ + @Before("@annotation(controllerDataScope)") // 切点表达式:拦截所有带有@DataScope注解的方法 + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable + { + // 清除之前可能残留的数据权限参数,防止干扰 + clearDataScope(point); + // 处理数据权限,生成过滤SQL + handleDataScope(point, controllerDataScope); + } + + /** + * 处理数据权限核心方法 + * + * @param joinPoint 切点对象 + * @param controllerDataScope @DataScope注解对象 + */ + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) + { + // 获取当前登录用户信息 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) + { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据(直接放行) + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) + { + // 获取注解中的权限字符(默认为上下文权限) + String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext()); + // 生成数据权限过滤SQL + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), + controllerDataScope.userAlias(), permission); + } + } + } + + /** + * 数据范围过滤,生成过滤SQL并设置到参数中 + * + * @param joinPoint 切点对象 + * @param user 当前用户对象 + * @param deptAlias SQL中部门表的别名 + * @param userAlias SQL中用户表的别名 + * @param permission 权限字符 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) + { + // 构建数据过滤SQL片段 + StringBuilder sqlString = new StringBuilder(); + // 记录已处理的权限类型,避免重复 + List conditions = new ArrayList(); + + // 遍历用户拥有的角色,根据角色的数据权限生成过滤条件 + for (SysRole role : user.getRoles()) + { + String dataScope = role.getDataScope(); + // 如果不是自定义权限且已处理过该权限类型,则跳过 + if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) + { + continue; + } + // 如果角色没有指定的权限,则跳过 + if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions()) + && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + continue; + } + + // 根据数据权限类型拼接SQL条件 + if (DATA_SCOPE_ALL.equals(dataScope)) + { + // 全部数据权限:清空SQL(无需过滤) + sqlString = new StringBuilder(); + conditions.add(dataScope); + break; // 已满足最高权限,无需继续处理其他角色 + } + else if (DATA_SCOPE_CUSTOM.equals(dataScope)) + { + // 自定义数据权限:查询角色关联的部门 + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, + role.getRoleId())); + } + else if (DATA_SCOPE_DEPT.equals(dataScope)) + { + // 本部门数据权限:部门ID等于用户所属部门 + sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } + else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) + { + // 本部门及子部门数据权限:部门ID在用户部门及子部门中 + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", + deptAlias, user.getDeptId(), user.getDeptId())); + } + else if (DATA_SCOPE_SELF.equals(dataScope)) + { + // 仅本人数据权限:用户ID等于当前用户 + if (StringUtils.isNotBlank(userAlias)) + { + sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } + else + { + // 没有用户别名时,设置一个无效条件(不查询任何数据) + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + } + // 记录已处理的权限类型 + conditions.add(dataScope); + } + + // 如果没有匹配的权限条件,设置无效条件(不查询任何数据) + if (StringUtils.isEmpty(conditions)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + + // 如果生成了有效的SQL条件,则设置到方法参数中 + if (StringUtils.isNotBlank(sqlString.toString())) + { + // 获取方法的第一个参数(假设为BaseEntity子类) + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + // 将SQL条件设置到参数的扩展属性中(用于MyBatis在XML中拼接) + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + // 注:substring(4)是为了去掉开头的" OR " + } + } + } + + /** + * 拼接权限SQL前先清空params.dataScope参数,防止SQL注入或残留条件干扰 + */ + private void clearDataScope(final JoinPoint joinPoint) + { + // 获取方法的第一个参数(假设为BaseEntity子类) + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + // 清空数据权限参数 + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/aspectj/DataSourceAspect.java b/huacai-framework/src/main/java/com/huacai/framework/aspectj/DataSourceAspect.java new file mode 100644 index 0000000..05e660c --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,88 @@ +package com.huacai.framework.aspectj; + +import java.util.Objects; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import com.huacai.common.annotation.DataSource; +import com.huacai.common.utils.StringUtils; +import com.huacai.framework.datasource.DynamicDataSourceContextHolder; + +/** + * 多数据源处理 + * 用于实现基于注解的动态数据源切换功能 + * + * @author huacai + */ +// 标记此类为AOP切面类,用于定义切入点和通知 +@Aspect +// 设置切面执行顺序为1(值越小优先级越高),确保在事务等其他切面之前执行 +@Order(1) +// 标记为Spring组件,使其被容器扫描并管理 +@Component +public class DataSourceAspect +{ + // 日志记录器,用于记录数据源切换相关日志 + protected Logger logger = LoggerFactory.getLogger(getClass()); + + // 定义切入点:匹配所有标注了@DataSource注解的方法,或包含@DataSource注解的类中的所有方法 + @Pointcut("@annotation(com.huacai.common.annotation.DataSource)" + + "|| @within(com.huacai.common.annotation.DataSource)") + public void dsPointCut() + { + // 切入点方法体为空,仅作为标记 + } + + // 定义环绕通知,在切入点匹配的方法执行前后进行增强处理 + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable + { + // 获取当前方法或类上的DataSource注解 + DataSource dataSource = getDataSource(point); + + // 如果注解存在(即需要切换数据源) + if (StringUtils.isNotNull(dataSource)) + { + // 将数据源类型存入上下文持有器,供动态数据源选择使用 + DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); + } + + try + { + // 执行目标方法(即被切入的业务方法) + return point.proceed(); + } + finally + { + // 方法执行完成后,清除数据源上下文,避免影响后续操作 + DynamicDataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 获取需要切换的数据源 + * 优先获取方法上的@DataSource注解,若方法上没有则获取类上的注解 + */ + public DataSource getDataSource(ProceedingJoinPoint point) + { + // 获取连接点的方法签名,用于获取目标方法信息 + MethodSignature signature = (MethodSignature) point.getSignature(); + // 从目标方法上查找DataSource注解 + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + // 如果方法上存在注解,直接返回 + if (Objects.nonNull(dataSource)) + { + return dataSource; + } + + // 方法上没有注解时,从目标类上查找DataSource注解并返回 + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/aspectj/LogAspect.java b/huacai-framework/src/main/java/com/huacai/framework/aspectj/LogAspect.java new file mode 100644 index 0000000..f0f1f81 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/aspectj/LogAspect.java @@ -0,0 +1,319 @@ +package com.huacai.framework.aspectj; + +import java.util.Collection; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.NamedThreadLocal; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.huacai.common.annotation.Log; +import com.huacai.common.core.domain.entity.SysUser; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.enums.BusinessStatus; +import com.huacai.common.enums.HttpMethod; +import com.huacai.common.filter.PropertyPreExcludeFilter; +import com.huacai.common.utils.SecurityUtils; +import com.huacai.common.utils.ServletUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.ip.IpUtils; +import com.huacai.framework.manager.AsyncManager; +import com.huacai.framework.manager.factory.AsyncFactory; +import com.huacai.system.domain.SysOperLog; + +/** + * 操作日志记录处理切面 + * 用于通过AOP拦截带有@Log注解的方法,记录操作日志信息 + * + * @author huacai + */ +// 标记此类为AOP切面类 +@Aspect +// 注册为Spring组件,使其被容器管理 +@Component +public class LogAspect +{ + // 定义日志记录器,用于记录切面内部的日志信息 + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + /** 排除敏感属性字段(如密码等,避免日志中记录敏感信息) */ + public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; + + /** 计算操作消耗时间的线程本地变量(每个线程独立存储,避免线程安全问题) */ + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal("Cost Time"); + + /** + * 处理请求前执行的方法 + * 用于记录方法执行的开始时间 + * + * @param joinPoint 切点对象,包含被拦截方法的信息 + * @param controllerLog 方法上的@Log注解对象,包含日志配置信息 + */ + @Before(value = "@annotation(controllerLog)") // 切点表达式:拦截所有带有@Log注解的方法 + public void boBefore(JoinPoint joinPoint, Log controllerLog) + { + // 记录当前时间戳到线程本地变量,用于后续计算方法执行耗时 + TIME_THREADLOCAL.set(System.currentTimeMillis()); + } + + /** + * 处理完请求后执行的方法(正常返回时) + * 用于在方法成功执行后记录日志 + * + * @param joinPoint 切点对象 + * @param controllerLog @Log注解对象 + * @param jsonResult 方法返回的结果对象 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) + { + // 调用日志处理核心方法,异常参数为null表示无异常 + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作(方法抛出异常时执行) + * 用于在方法执行异常时记录日志 + * + * @param joinPoint 切点对象 + * @param controllerLog @Log注解对象 + * @param e 抛出的异常对象 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) + { + // 调用日志处理核心方法,传入异常对象 + handleLog(joinPoint, controllerLog, e, null); + } + + /** + * 日志处理核心方法 + * 封装操作日志信息并异步保存到数据库 + * + * @param joinPoint 切点对象 + * @param controllerLog @Log注解对象 + * @param e 异常对象(可能为null) + * @param jsonResult 方法返回结果(可能为null) + */ + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) + { + try + { + // 获取当前登录用户信息(从Security上下文获取) + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // *========数据库日志实体构建=========*// + SysOperLog operLog = new SysOperLog(); + // 默认设置操作状态为成功 + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 获取客户端IP地址 + String ip = IpUtils.getIpAddr(); + operLog.setOperIp(ip); + // 获取请求URL并截断(避免过长) + operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); + + // 如果存在登录用户,设置操作人及部门信息 + if (loginUser != null) + { + operLog.setOperName(loginUser.getUsername()); + SysUser currentUser = loginUser.getUser(); + if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept())) + { + operLog.setDeptName(currentUser.getDept().getDeptName()); + } + } + + // 如果存在异常,更新操作状态为失败并记录异常信息 + if (e != null) + { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); // 截断异常信息避免过长 + } + + // 设置操作方法名(全类名.方法名()) + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + + // 设置请求方式(GET/POST等) + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + + // 处理@Log注解上的参数,完善日志信息 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + + // 计算方法执行耗时(当前时间 - 开始时间) + operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); + + // 异步执行日志保存(避免阻塞主线程) + AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); + } + catch (Exception exp) + { + // 记录本地异常日志(切面自身异常) + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + finally + { + // 清除线程本地变量,避免内存泄漏 + TIME_THREADLOCAL.remove(); + } + } + + /** + * 获取注解中对方法的描述信息,用于完善操作日志 + * + * @param joinPoint 切点对象 + * @param log @Log注解对象 + * @param operLog 操作日志实体 + * @param jsonResult 方法返回结果 + * @throws Exception 可能的异常 + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception + { + // 设置业务类型(从注解中获取) + operLog.setBusinessType(log.businessType().ordinal()); + // 设置操作标题(从注解中获取) + operLog.setTitle(log.title()); + // 设置操作人类别(从注解中获取) + operLog.setOperatorType(log.operatorType().ordinal()); + + // 如果注解配置需要保存请求数据,则处理请求参数 + if (log.isSaveRequestData()) + { + // 获取请求参数并设置到日志实体 + setRequestValue(joinPoint, operLog, log.excludeParamNames()); + } + + // 如果注解配置需要保存响应数据且结果不为空,则处理响应结果 + if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) + { + // 将响应结果转为JSON并截断(避免过长) + operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); + } + } + + /** + * 处理请求参数,将其保存到日志实体中 + * + * @param joinPoint 切点对象 + * @param operLog 操作日志实体 + * @param excludeParamNames 注解中指定的需要排除的参数名 + * @throws Exception 可能的异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception + { + // 获取请求参数Map(从request中获取) + Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); + String requestMethod = operLog.getRequestMethod(); + + // 如果参数Map为空,且是PUT/POST请求(通常这类请求参数在请求体中) + if (StringUtils.isEmpty(paramsMap) + && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) + { + // 从方法参数中解析请求参数 + String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); // 截断参数避免过长 + } + else + { + // 将参数Map转为JSON,同时排除敏感字段 + operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000)); + } + } + + /** + * 将方法参数数组拼接为字符串 + * + * @param paramsArray 方法参数数组 + * @param excludeParamNames 需要排除的参数名 + * @return 拼接后的参数字符串 + */ + private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) + { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) + { + for (Object o : paramsArray) + { + // 过滤空对象和不需要记录的对象(如文件上传、请求响应对象等) + if (StringUtils.isNotNull(o) && !isFilterObject(o)) + { + try + { + // 将参数对象转为JSON,同时排除敏感字段 + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); + params += jsonObj.toString() + " "; + } + catch (Exception e) + { + // 转换失败时忽略(避免影响主流程) + } + } + } + } + return params.trim(); // 去除首尾空格 + } + + /** + * 创建属性过滤规则,用于排除敏感字段(如密码) + * + * @param excludeParamNames 额外需要排除的参数名 + * @return 属性过滤对象 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) + { + // 合并默认排除字段和注解中指定的排除字段 + return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames)); + } + + /** + * 判断是否为需要过滤的对象(不记录到日志中) + * + * @param o 待判断的对象 + * @return true-需要过滤,false-不需要过滤 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) + { + Class clazz = o.getClass(); + + // 如果是数组,判断元素是否为文件上传对象 + if (clazz.isArray()) + { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } + // 如果是集合,判断元素是否为文件上传对象 + else if (Collection.class.isAssignableFrom(clazz)) + { + Collection collection = (Collection) o; + for (Object value : collection) + { + return value instanceof MultipartFile; + } + } + // 如果是Map,判断值是否为文件上传对象 + else if (Map.class.isAssignableFrom(clazz)) + { + Map map = (Map) o; + for (Object value : map.entrySet()) + { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + // 直接判断是否为文件上传、请求、响应或参数绑定结果对象 + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/aspectj/RateLimiterAspect.java b/huacai-framework/src/main/java/com/huacai/framework/aspectj/RateLimiterAspect.java new file mode 100644 index 0000000..857d7e7 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/aspectj/RateLimiterAspect.java @@ -0,0 +1,125 @@ +package com.huacai.framework.aspectj; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import com.huacai.common.annotation.RateLimiter; +import com.huacai.common.enums.LimitType; +import com.huacai.common.exception.ServiceException; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.ip.IpUtils; + +/** + * 限流处理切面 + * 用于通过AOP拦截带有@RateLimiter注解的方法,实现基于Redis的分布式限流 + * + * @author huacai + */ +// 标记为AOP切面类 +@Aspect +// 注册为Spring组件 +@Component +public class RateLimiterAspect +{ + // 日志记录器 + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); + + // Redis模板(用于执行Lua脚本) + private RedisTemplate redisTemplate; + + // 限流Lua脚本(从RedisConfig中注入) + private RedisScript limitScript; + + // 注入RedisTemplate(setter注入,避免循环依赖) + @Autowired + public void setRedisTemplate1(RedisTemplate redisTemplate) + { + this.redisTemplate = redisTemplate; + } + + // 注入限流脚本 + @Autowired + public void setLimitScript(RedisScript limitScript) + { + this.limitScript = limitScript; + } + + /** + * 方法执行前拦截,执行限流逻辑 + * + * @param point 切点对象 + * @param rateLimiter @RateLimiter注解对象 + * @throws Throwable 限流异常或其他异常 + */ + @Before("@annotation(rateLimiter)") // 切点表达式:拦截所有带有@RateLimiter注解的方法 + public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable + { + // 从注解中获取限流时间窗口(秒) + int time = rateLimiter.time(); + // 从注解中获取时间窗口内的最大请求数 + int count = rateLimiter.count(); + + // 生成限流key(结合注解key、限流类型和方法信息) + String combineKey = getCombineKey(rateLimiter, point); + // 将key放入集合(Lua脚本需要KEYS参数) + List keys = Collections.singletonList(combineKey); + + try + { + // 执行Redis限流脚本,返回当前请求计数 + Long number = redisTemplate.execute(limitScript, keys, count, time); + // 如果计数超过限制,抛出限流异常 + if (StringUtils.isNull(number) || number.intValue() > count) + { + throw new ServiceException("访问过于频繁,请稍候再试"); + } + // 记录限流日志 + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey); + } + catch (ServiceException e) + { + // 限流异常直接抛出 + throw e; + } + catch (Exception e) + { + // 其他异常包装为运行时异常 + throw new RuntimeException("服务器限流异常,请稍候再试"); + } + } + + /** + * 生成限流的唯一key + * 格式:注解key + 限流类型(如IP) + 类名 + 方法名 + * + * @param rateLimiter @RateLimiter注解对象 + * @param point 切点对象 + * @return 限流key + */ + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) + { + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); + // 如果限流类型为IP,则拼接客户端IP + if (rateLimiter.limitType() == LimitType.IP) + { + stringBuffer.append(IpUtils.getIpAddr()).append("-"); + } + // 获取方法签名信息 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + // 拼接类名和方法名 + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/ApplicationConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/ApplicationConfig.java new file mode 100644 index 0000000..7c16f3a --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/ApplicationConfig.java @@ -0,0 +1,40 @@ +package com.huacai.framework.config; + +import java.util.TimeZone; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * 程序注解配置 + * + * @author huacai + */ +/** + * 程序注解配置类 + * 用于配置Spring相关的注解扫描、AOP代理及其他全局配置 + * + * @author huacai + */ +// 标记此类为Spring配置类,相当于XML配置文件 +@Configuration +// 启用AspectJ自动代理,exposeProxy = true表示暴露代理对象,允许通过AopContext访问当前代理对象 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定MyBatis Mapper接口的扫描路径,自动生成Mapper实现类 +@MapperScan("com.huacai.**.mapper") +public class ApplicationConfig +{ + /** + * 配置Jackson的时区信息 + * 用于统一处理JSON序列化/反序列化时的时区问题 + */ + // 定义一个Bean,用于自定义Jackson的ObjectMapper构建器 + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() + { + // 返回一个自定义配置器,设置时区为系统默认时区 + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/CaptchaConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/CaptchaConfig.java new file mode 100644 index 0000000..4231a0d --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/CaptchaConfig.java @@ -0,0 +1,83 @@ +package com.huacai.framework.config; + +import java.util.Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author huacai + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.huacai.framework.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/DruidConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/DruidConfig.java new file mode 100644 index 0000000..e64160b --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/DruidConfig.java @@ -0,0 +1,166 @@ +package com.huacai.framework.config; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import com.huacai.common.enums.DataSourceType; +import com.huacai.common.utils.spring.SpringUtils; +import com.huacai.framework.config.properties.DruidProperties; +import com.huacai.framework.datasource.DynamicDataSource; + +/** + * Druid数据源配置类 + * 用于配置多数据源(主从库)及去除监控页面广告 + * + * @author huacai + */ +// 标记为Spring配置类 +@Configuration +public class DruidConfig +{ + /** + * 配置主数据源 + * + * @param druidProperties Druid数据源配置属性 + * @return 主数据源对象 + */ + @Bean + @ConfigurationProperties("spring.datasource.druid.master") // 绑定配置文件中的主库配置 + public DataSource masterDataSource(DruidProperties druidProperties) + { + // 构建Druid数据源 + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + // 应用配置属性(如用户名、密码、连接池参数等) + return druidProperties.dataSource(dataSource); + } + + /** + * 配置从数据源(条件注解:仅当配置文件中启用从库时才创建) + * + * @param druidProperties Druid数据源配置属性 + * @return 从数据源对象 + */ + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") // 绑定配置文件中的从库配置 + @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") + public DataSource slaveDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + /** + * 配置动态数据源(主从切换) + * 以主数据源为默认数据源 + * + * @param masterDataSource 主数据源 + * @return 动态数据源对象 + */ + @Bean(name = "dynamicDataSource") + @Primary // 优先使用该数据源 + public DynamicDataSource dataSource(DataSource masterDataSource) + { + // 存储备选数据源(主库+从库) + Map targetDataSources = new HashMap<>(); + targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); + // 添加从数据源(如果存在) + setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); + // 创建动态数据源(默认数据源为主库) + return new DynamicDataSource(masterDataSource, targetDataSources); + } + + /** + * 向数据源映射中添加数据源(如从库) + * + * @param targetDataSources 数据源映射Map + * @param sourceName 数据源名称(如SLAVE) + * @param beanName 数据源在Spring中的Bean名称 + */ + public void setDataSource(Map targetDataSources, String sourceName, String beanName) + { + try + { + // 从Spring容器中获取数据源Bean(如果存在) + DataSource dataSource = SpringUtils.getBean(beanName); + targetDataSources.put(sourceName, dataSource); + } + catch (Exception e) + { + // 从库不存在时忽略(不影响主库使用) + } + } + + /** + * 去除Druid监控页面底部的广告 + * 通过过滤器修改common.js内容,移除广告相关代码 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) + { + // 获取Druid监控页面的配置参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取监控页面的URL模式(默认为/druid/*) + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + // 构建common.js的URL模式(如/druid/js/common.js) + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + // common.js在Druid jar包中的路径 + final String filePath = "support/http/resources/js/common.js"; + + // 创建自定义过滤器,用于修改common.js内容 + Filter filter = new Filter() + { + @Override + public void init(javax.servlet.FilterConfig filterConfig) throws ServletException + { + // 初始化方法(无操作) + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + // 先执行过滤器链(获取原始的common.js内容) + chain.doFilter(request, response); + // 重置响应缓冲区(不重置响应头) + response.resetBuffer(); + // 从Druid资源中读取common.js内容 + String text = Utils.readFromResource(filePath); + // 正则替换,去除底部的广告链接 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + // 将修改后的内容写入响应 + response.getWriter().write(text); + } + + @Override + public void destroy() + { + // 销毁方法(无操作) + } + }; + + // 注册过滤器,只对common.js的URL生效 + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/FastJson2JsonRedisSerializer.java b/huacai-framework/src/main/java/com/huacai/framework/config/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..0d1709b --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/FastJson2JsonRedisSerializer.java @@ -0,0 +1,80 @@ +package com.huacai.framework.config; + +import java.nio.charset.Charset; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.huacai.common.constant.Constants; + +/** + * Redis使用FastJson序列化 + * 自定义Redis序列化器,基于FastJson2实现对象与字节数组的相互转换 + * + * @author huacai + * @param 序列化的对象类型 + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + // 默认字符集为UTF-8,用于字节数组与字符串的转换 + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + // 自动类型过滤器,基于系统常量中的JSON白名单字符串创建,用于反序列化时限制可解析的类 + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + // 序列化的目标类类型 + private Class clazz; + + /** + * 构造方法,指定序列化的目标类 + * + * @param clazz 要序列化的对象类型 + */ + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + /** + * 将对象序列化为字节数组 + * + * @param t 要序列化的对象 + * @return 序列化后的字节数组,若对象为null则返回空字节数组 + * @throws SerializationException 序列化过程中发生异常时抛出 + */ + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + // 使用FastJson将对象转为JSON字符串,开启WriteClassName特性以记录类名(用于反序列化) + // 再将JSON字符串按默认字符集转为字节数组 + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + /** + * 将字节数组反序列化为对象 + * + * @param bytes 要反序列化的字节数组 + * @return 反序列化后的对象,若字节数组为null或空则返回null + * @throws SerializationException 反序列化过程中发生异常时抛出 + */ + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + // 将字节数组按默认字符集转为JSON字符串 + String str = new String(bytes, DEFAULT_CHARSET); + + // 使用FastJson将JSON字符串解析为指定类型的对象,应用自动类型过滤器限制可解析的类 + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/FilterConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/FilterConfig.java new file mode 100644 index 0000000..a25b123 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/FilterConfig.java @@ -0,0 +1,89 @@ +package com.huacai.framework.config; + +import java.util.HashMap; +import java.util.Map; +import javax.servlet.DispatcherType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.huacai.common.filter.RepeatableFilter; +import com.huacai.common.filter.XssFilter; +import com.huacai.common.utils.StringUtils; + +/** + * Filter配置类 + * 用于注册和配置应用中的过滤器,如XSS过滤器和可重复读取请求体的过滤器 + * + * @author huacai + */ +@Configuration +public class FilterConfig +{ + // 从配置文件中读取XSS过滤的排除路径(不需要过滤的URL) + @Value("${xss.excludes}") + private String excludes; + + // 从配置文件中读取XSS过滤的URL模式(需要过滤的URL) + @Value("${xss.urlPatterns}") + private String urlPatterns; + + /** + * 注册XSS过滤器 + * 仅当配置文件中xss.enabled=true时才会创建该Bean + * + * @return 配置好的XSS过滤器注册Bean + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") + public FilterRegistrationBean xssFilterRegistration() + { + // 创建过滤器注册Bean + FilterRegistrationBean registration = new FilterRegistrationBean(); + // 设置过滤器适用的调度类型(此处为请求类型) + registration.setDispatcherTypes(DispatcherType.REQUEST); + // 设置要注册的过滤器实例(XSS过滤器,用于防止XSS攻击) + registration.setFilter(new XssFilter()); + // 拆分URL模式字符串为数组,设置过滤器拦截的URL + registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); + // 设置过滤器名称 + registration.setName("xssFilter"); + // 设置过滤器执行顺序(最高优先级,优先于其他过滤器执行) + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + // 创建初始化参数Map + Map initParameters = new HashMap(); + // 设置排除路径参数(从配置文件读取的excludes) + initParameters.put("excludes", excludes); + // 为过滤器设置初始化参数 + registration.setInitParameters(initParameters); + // 返回配置好的过滤器注册Bean + return registration; + } + + /** + * 注册可重复读取请求体的过滤器 + * 用于解决请求体只能读取一次的问题,方便后续过滤器或处理器多次读取 + * + * @return 配置好的RepeatableFilter注册Bean + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + public FilterRegistrationBean someFilterRegistration() + { + // 创建过滤器注册Bean + FilterRegistrationBean registration = new FilterRegistrationBean(); + // 设置要注册的过滤器实例(RepeatableFilter) + registration.setFilter(new RepeatableFilter()); + // 设置过滤器拦截所有URL + registration.addUrlPatterns("/*"); + // 设置过滤器名称 + registration.setName("repeatableFilter"); + // 设置过滤器执行顺序(最低优先级,在其他过滤器之后执行) + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + // 返回配置好的过滤器注册Bean + return registration; + } + +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/KaptchaTextCreator.java b/huacai-framework/src/main/java/com/huacai/framework/config/KaptchaTextCreator.java new file mode 100644 index 0000000..49cab60 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/KaptchaTextCreator.java @@ -0,0 +1,91 @@ +package com.huacai.framework.config; + +import java.util.Random; +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +/** + * 验证码文本生成器 + * 继承 * 继承默认文本创建器,自定义验证码文本生成逻辑,生成包含简单数学运算的验证码 + * + * @author huacai + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + // 数字字符串字符串数组,包含0-10的字符串表示 + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + /** + * 生成验证码文本 + * 生成包含简单数学运算(加、减、乘)的表达式,并在表达式后拼接结果(用于后续验证) + * + * @return 包含数学运算的验证码文本,格式为"运算式=?@结果" + */ + @Override + public String getText() + { + // 存储运算结果 + Integer result = 0; + // 随机数生成器 + Random random = new Random(); + // 生成两个0-9之间的随机数 + int x = random.nextInt(10); + int y = random.nextInt(10); + // 用于构建验证码文本的字符串构建器 + StringBuilder suChinese = new StringBuilder(); + // 随机生成运算类型(0-乘法,1-除法/加法,2-减法) + int randomoperands = random.nextInt(3); + + // 处理乘法运算 + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); // 拼接第一个数字 + suChinese.append("*"); // 拼接乘号 + suChinese.append(CNUMBERS[y]); // 拼接第二个数字 + } + // 处理除法或加法运算 + else if (randomoperands == 1) + { + // 若x不为0且y能被x整除,则进行除法运算 + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); // 拼接被除数 + suChinese.append("/"); // 拼接除号 + suChinese.append(CNUMBERS[x]); // 拼接除数 + } + // 否则进行加法运算 + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); // 拼接第一个数字 + suChinese.append("+"); // 拼接加号 + suChinese.append(CNUMBERS[y]); // 拼接第二个数字 + } + } + // 处理减法运算 + else + { + // 若x大于等于y,用x减y + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); // 拼接被减数 + suChinese.append("-"); // 拼接减号 + suChinese.append(CNUMBERS[y]); // 拼接减数 + } + // 否则用y减x(保证结果非负) + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); // 拼接被减数 + suChinese.append("-"); // 拼接减号 + suChinese.append(CNUMBERS[x]); // 拼接减数 + } + } + // 拼接表达式后缀和结果(格式为"=?@结果",用于后续解析验证) + suChinese.append("=?@" + result); + // 返回生成的验证码文本 + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/MyBatisConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/MyBatisConfig.java new file mode 100644 index 0000000..7a11439 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/MyBatisConfig.java @@ -0,0 +1,186 @@ +package com.huacai.framework.config; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import javax.sql.DataSource; +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.boot.autoconfigure.SpringBootVFS; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.util.ClassUtils; +import com.huacai.common.utils.StringUtils; + +/** + * Mybatis支持*匹配扫描包的配置类 + * 用于扩展MyBatis的包扫描能力,支持通配符匹配多个包路径 + * + * @author huacai + */ +@Configuration +public class MyBatisConfig +{ + // 注入环境变量对象,用于读取配置文件中的MyBatis相关配置 + @Autowired + private Environment env; + + // 默认的资源匹配模式,用于扫描类文件 + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + /** + * 处理类型别名包路径,支持通配符扫描 + * 将包含通配符的包路径转换为实际存在的包路径集合 + * + * @param typeAliasesPackage 配置的类型别名包路径(可能包含通配符) + * @return 处理后的实际包路径,多个包用逗号分隔 + */ + public static String setTypeAliasesPackage(String typeAliasesPackage) + { + // 创建资源模式解析器,用于解析资源路径 + ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver(); + // 创建元数据读取工厂,用于读取类资源的元数据 + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); + // 存储所有匹配到的包路径 + List allResult = new ArrayList(); + try + { + // 按逗号分割配置的包路径,处理多个包的情况 + for (String aliasesPackage : typeAliasesPackage.split(",")) + { + // 存储当前包路径匹配到的结果 + List result = new ArrayList(); + // 构建资源扫描路径:classpath*: + 转换为资源路径的包名 + /**/*.class + aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN; + // 根据路径获取所有匹配的资源(.class文件) + Resource[] resources = resolver.getResources(aliasesPackage); + // 遍历资源,提取包路径 + if (resources != null && resources.length > 0) + { + MetadataReader metadataReader = null; + for (Resource resource : resources) + { + // 仅处理可读的资源 + if (resource.isReadable()) + { + // 读取资源的元数据 + metadataReader = metadataReaderFactory.getMetadataReader(resource); + try + { + // 获取类的全限定名,提取其所在的包路径并添加到结果中 + result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName()); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + } + } + } + } + // 去重并添加到总结果中 + if (result.size() > 0) + { + HashSet hashResult = new HashSet(result); + allResult.addAll(hashResult); + } + } + // 将去重后的包路径拼接为字符串返回 + if (allResult.size() > 0) + { + typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0])); + } + else + { + // 未找到任何包时抛出异常 + throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包"); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + return typeAliasesPackage; + } + + /** + * 解析Mapper文件路径,将配置的路径转换为Resource数组 + * + * @param mapperLocations 配置的Mapper文件路径数组 + * @return 解析后的Resource数组 + */ + public Resource[] resolveMapperLocations(String[] mapperLocations) + { + // 创建资源模式解析器 + ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + // 存储解析后的资源 + List resources = new ArrayList(); + if (mapperLocations != null) + { + // 遍历每个Mapper路径,解析为Resource对象 + for (String mapperLocation : mapperLocations) + { + try + { + // 根据路径获取所有匹配的资源(Mapper.xml文件) + Resource[] mappers = resourceResolver.getResources(mapperLocation); + resources.addAll(Arrays.asList(mappers)); + } + catch (IOException e) + { + // 忽略解析异常 + } + } + } + // 转换为Resource数组返回 + return resources.toArray(new Resource[resources.size()]); + } + + /** + * 创建SqlSessionFactory实例,配置MyBatis的核心参数 + * + * @param dataSource 数据源对象 + * @return 配置好的SqlSessionFactory实例 + * @throws Exception 配置过程中发生异常时抛出 + */ + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception + { + // 从环境变量中读取MyBatis配置的类型别名包路径 + String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage"); + // 从环境变量中读取Mapper文件路径 + String mapperLocations = env.getProperty("mybatis.mapperLocations"); + // 从环境变量中读取MyBatis配置文件路径 + String configLocation = env.getProperty("mybatis.configLocation"); + + // 处理类型别名包路径,支持通配符 + typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage); + // 添加SpringBoot的VFS实现,用于扫描类路径下的资源 + VFS.addImplClass(SpringBootVFS.class); + + // 创建SqlSessionFactoryBean + final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + // 设置数据源 + sessionFactory.setDataSource(dataSource); + // 设置处理后的类型别名包路径 + sessionFactory.setTypeAliasesPackage(typeAliasesPackage); + // 解析并设置Mapper文件路径 + sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); + // 设置MyBatis配置文件路径 + sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); + // 创建并返回SqlSessionFactory实例 + return sessionFactory.getObject(); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/RedisConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/RedisConfig.java new file mode 100644 index 0000000..8870620 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/RedisConfig.java @@ -0,0 +1,95 @@ +package com.huacai.framework.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置类 + * 用于配置RedisTemplate和限流脚本 + * + * @author huacai + */ +// 标记为Spring配置类 +@Configuration +// 启用缓存注解支持(如@Cacheable等) +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport +{ + /** + * 配置RedisTemplate + * 用于自定义Redis的序列化方式 + * + * @param connectionFactory Redis连接工厂 + * @return 配置后的RedisTemplate + */ + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + // 设置Redis连接工厂 + template.setConnectionFactory(connectionFactory); + + // 使用FastJson2作为JSON序列化器 + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer序列化Redis的key + template.setKeySerializer(new StringRedisSerializer()); + // 使用FastJson2序列化Redis的value + template.setValueSerializer(serializer); + + // Hash的key也使用StringRedisSerializer序列化 + template.setHashKeySerializer(new StringRedisSerializer()); + // Hash的value使用FastJson2序列化 + template.setHashValueSerializer(serializer); + + // 初始化RedisTemplate + template.afterPropertiesSet(); + return template; + } + + /** + * 配置限流脚本(基于Redis的Lua脚本) + * 用于实现分布式限流功能 + * + * @return Redis脚本对象 + */ + @Bean + public DefaultRedisScript limitScript() + { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + // 设置脚本内容 + redisScript.setScriptText(limitScriptText()); + // 设置脚本返回值类型 + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流Lua脚本内容 + * 逻辑:在指定时间窗口内限制请求次数 + * + * @return Lua脚本字符串 + */ + private String limitScriptText() + { + return "local key = KEYS[1]\n" + // 限流key(如接口+IP) + "local count = tonumber(ARGV[1])\n" + // 最大请求次数 + "local time = tonumber(ARGV[2])\n" + // 时间窗口(秒) + "local current = redis.call('get', key);\n" + // 获取当前计数 + "if current and tonumber(current) > count then\n" + // 如果当前计数超过限制 + " return tonumber(current);\n" + // 返回当前计数(表示限流) + "end\n" + + "current = redis.call('incr', key)\n" + // 计数+1 + "if tonumber(current) == 1 then\n" + // 如果是第一次计数 + " redis.call('expire', key, time)\n" + // 设置过期时间(时间窗口) + "end\n" + + "return tonumber(current);"; // 返回当前计数(未限流) + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/ResourcesConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/ResourcesConfig.java new file mode 100644 index 0000000..4f83164 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/ResourcesConfig.java @@ -0,0 +1,89 @@ +package com.huacai.framework.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.huacai.common.config.HuaCaiConfig; +import com.huacai.common.constant.Constants; +import com.huacai.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 通用资源配置类 + * 用于配置静态资源映射、拦截器和跨域请求处理 + * + * @author huacai + */ +// 标记为Spring配置类 +@Configuration +// 实现WebMvcConfigurer接口,自定义Spring MVC配置 +public class ResourcesConfig implements WebMvcConfigurer +{ + // 注入重复提交拦截器(用于防止表单重复提交) + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + /** + * 配置静态资源处理器 + * 用于映射静态资源路径到实际存储位置 + */ + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径映射 */ + // 映射以Constants.RESOURCE_PREFIX开头的URL到本地文件系统的上传目录 + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + HuaCaiConfig.getProfile() + "/"); + + /** Swagger UI资源映射 */ + // 映射Swagger UI的URL到META-INF中的资源目录 + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic()); // 设置缓存控制(5小时) + } + + /** + * 注册自定义拦截器 + * 用于添加重复提交拦截器等自定义拦截逻辑 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + // 注册重复提交拦截器,拦截所有请求 + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + } + + /** + * 配置跨域过滤器 + * 用于处理不同域之间的请求访问限制 + */ + @Bean + public CorsFilter corsFilter() + { + // 创建跨域配置对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); // 允许携带cookie + // 设置允许访问的源(*表示允许所有域,实际生产环境应指定具体域名) + config.addAllowedOriginPattern("*"); + // 设置允许的请求头(*表示允许所有头) + config.addAllowedHeader("*"); + // 设置允许的请求方法(*表示允许所有方法:GET/POST/PUT等) + config.addAllowedMethod("*"); + // 设置预检请求的有效期(1800秒,避免频繁预检) + config.setMaxAge(1800L); + + // 创建路径映射源,注册跨域配置 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对所有路径生效 + + // 返回跨域过滤器 + return new CorsFilter(source); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/SecurityConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/SecurityConfig.java new file mode 100644 index 0000000..70ef4ea --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/SecurityConfig.java @@ -0,0 +1,148 @@ +package com.huacai.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; +import com.huacai.framework.config.properties.PermitAllUrlProperties; +import com.huacai.framework.security.filter.JwtAuthenticationTokenFilter; +import com.huacai.framework.security.handle.AuthenticationEntryPointImpl; +import com.huacai.framework.security.handle.LogoutSuccessHandlerImpl; + +/** + * spring security配置 + * + * @author huacai + */ +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter +{ + /** + * 自定义用户认证逻辑 + */ + @Autowired + private UserDetailsService userDetailsService; + + /** + * 认证失败处理类 + */ + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + + /** + * 退出处理类 + */ + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + /** + * token认证过滤器 + */ + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + /** + * 跨域过滤器 + */ + @Autowired + private CorsFilter corsFilter; + + /** + * 允许匿名访问的地址 + */ + @Autowired + private PermitAllUrlProperties permitAllUrl; + + /** + * 解决 无法直接注入 AuthenticationManager + * + * @return + * @throws Exception + */ + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception + { + return super.authenticationManagerBean(); + } + + /** + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception + { + // 注解标记允许匿名访问的url + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); + permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); + + httpSecurity + // CSRF禁用,因为不使用session + .csrf().disable() + // 禁用HTTP响应标头 + .headers().cacheControl().disable().and() + // 认证失败处理类 + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + // 基于token,所以不需要session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // 过滤请求 + .authorizeRequests() + // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + .antMatchers("/login", "/register", "/captchaImage").permitAll() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() + .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated() + .and() + .headers().frameOptions().disable(); + // 添加Logout filter + httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); + // 添加JWT filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + // 添加CORS filter + httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); + httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); + } + + /** + * 强散列哈希加密实现 + */ + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() + { + return new BCryptPasswordEncoder(); + } + + /** + * 身份认证接口 + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception + { + auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/ServerConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/ServerConfig.java new file mode 100644 index 0000000..95b6c42 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/ServerConfig.java @@ -0,0 +1,46 @@ +package com.huacai.framework.config; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import com.huacai.common.utils.ServletUtils; + +/** + * 服务相关配置类 + * 用于获取服务的基础信息(如完整请求路径等) + * + * @author huacai + */ +// 注册为Spring组件 +@Component +public class ServerConfig +{ + /** + * 获取完整的请求路径 + * 包括:域名,端口,上下文访问路径 + * + * @return 服务完整地址(如http://localhost:8080/huacai) + */ + public String getUrl() + { + // 获取当前请求对象 + HttpServletRequest request = ServletUtils.getRequest(); + // 生成并返回完整服务地址 + return getDomain(request); + } + + /** + * 从请求对象中提取服务域名及上下文路径 + * + * @param request HTTP请求对象 + * @return 服务基础地址(域名+端口+上下文路径) + */ + public static String getDomain(HttpServletRequest request) + { + // 获取请求的完整URL(如http://localhost:8080/huacai/user/list) + StringBuffer url = request.getRequestURL(); + // 获取上下文路径(如/huacai) + String contextPath = request.getServletContext().getContextPath(); + // 截取域名+端口部分,拼接上下文路径(结果如http://localhost:8080/huacai) + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/ThreadPoolConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/ThreadPoolConfig.java new file mode 100644 index 0000000..4e0642a --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/ThreadPoolConfig.java @@ -0,0 +1,84 @@ +package com.huacai.framework.config; + +import com.huacai.common.utils.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置类 + * 用于配置应用中使用的线程池,包括普通任务线程池和定时任务线程池 + * + * @author huacai + **/ +@Configuration +public class ThreadPoolConfig +{ + // 核心线程池大小:线程池维护的核心线程数量,即使线程空闲也不会被销毁 + private int corePoolSize = 50; + + // 最大可创建的线程数:线程池允许创建的最大线程数量,当核心线程都在工作且任务队列满时会创建新线程直到达到此值 + private int maxPoolSize = 200; + + // 队列最大长度:用于存放等待执行任务的阻塞队列容量 + private int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间:当线程数量超过核心线程数时,多余的空闲线程的存活时间(单位:秒) + private int keepAliveSeconds = 300; + + /** + * 创建普通任务线程池实例 + * 用于处理一般的异步任务 + * + * @return 配置好的ThreadPoolTaskExecutor实例 + */ + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() + { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); // 设置最大线程数 + executor.setCorePoolSize(corePoolSize); // 设置核心线程数 + executor.setQueueCapacity(queueCapacity); // 设置任务队列容量 + executor.setKeepAliveSeconds(keepAliveSeconds); // 设置空闲线程存活时间 + // 线程池对拒绝任务(无线程可用且队列已满)的处理策略:使用调用者所在线程执行任务 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 创建定时任务线程池 + * 专门用于执行周期性任务或定时任务 + * + * @return 配置好的ScheduledExecutorService实例 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() + { + return new ScheduledThreadPoolExecutor( + corePoolSize, // 核心线程数 + // 线程工厂:设置线程名称格式为"schedule-pool-%d",且为守护线程 + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + // 拒绝策略:使用调用者所在线程执行任务 + new ThreadPoolExecutor.CallerRunsPolicy() + ) { + /** + * 任务执行后回调方法 + * 用于在任务执行完成后处理异常信息 + * + * @param r 执行的任务 + * @param t 任务执行过程中抛出的异常(无异常则为null) + */ + @Override + protected void afterExecute(Runnable r, Throwable t) + { + super.afterExecute(r, t); + // 打印任务执行过程中发生的异常 + Threads.printException(r, t); + } + }; + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/WebMvcConfig.java b/huacai-framework/src/main/java/com/huacai/framework/config/WebMvcConfig.java new file mode 100644 index 0000000..feeb7cd --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/WebMvcConfig.java @@ -0,0 +1,22 @@ +package com.huacai.framework.config; + +import com.huacai.framework.interceptor.DemoEnvironmentInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private DemoEnvironmentInterceptor demoEnvironmentInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 演示环境拦截器 + registry.addInterceptor(demoEnvironmentInterceptor) + .addPathPatterns("/**") // 拦截所有路径 + .excludePathPatterns("/login", "/logout", "/captchaImage"); // 排除登录等必要接口 + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/properties/DruidProperties.java b/huacai-framework/src/main/java/com/huacai/framework/config/properties/DruidProperties.java new file mode 100644 index 0000000..ceb2e57 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/properties/DruidProperties.java @@ -0,0 +1,89 @@ +package com.huacai.framework.config.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import com.alibaba.druid.pool.DruidDataSource; + +/** + * druid 配置属性 + * + * @author huacai + */ +@Configuration +public class DruidProperties +{ + @Value("${spring.datasource.druid.initialSize}") + private int initialSize; + + @Value("${spring.datasource.druid.minIdle}") + private int minIdle; + + @Value("${spring.datasource.druid.maxActive}") + private int maxActive; + + @Value("${spring.datasource.druid.maxWait}") + private int maxWait; + + @Value("${spring.datasource.druid.connectTimeout}") + private int connectTimeout; + + @Value("${spring.datasource.druid.socketTimeout}") + private int socketTimeout; + + @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") + private int timeBetweenEvictionRunsMillis; + + @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") + private int minEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") + private int maxEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.validationQuery}") + private String validationQuery; + + @Value("${spring.datasource.druid.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${spring.datasource.druid.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${spring.datasource.druid.testOnReturn}") + private boolean testOnReturn; + + public DruidDataSource dataSource(DruidDataSource datasource) + { + /** 配置初始化大小、最小、最大 */ + datasource.setInitialSize(initialSize); + datasource.setMaxActive(maxActive); + datasource.setMinIdle(minIdle); + + /** 配置获取连接等待超时的时间 */ + datasource.setMaxWait(maxWait); + + /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */ + datasource.setConnectTimeout(connectTimeout); + + /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */ + datasource.setSocketTimeout(socketTimeout); + + /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ + datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + + /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ + datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); + + /** + * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 + */ + datasource.setValidationQuery(validationQuery); + /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ + datasource.setTestWhileIdle(testWhileIdle); + /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnBorrow(testOnBorrow); + /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnReturn(testOnReturn); + return datasource; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/config/properties/PermitAllUrlProperties.java b/huacai-framework/src/main/java/com/huacai/framework/config/properties/PermitAllUrlProperties.java new file mode 100644 index 0000000..19693de --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/config/properties/PermitAllUrlProperties.java @@ -0,0 +1,73 @@ +package com.huacai.framework.config.properties; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import org.apache.commons.lang3.RegExUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import com.huacai.common.annotation.Anonymous; + +/** + * 设置Anonymous注解允许匿名访问的url + * + * @author huacai + */ +@Configuration +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware +{ + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + private List urls = new ArrayList<>(); + + public String ASTERISK = "*"; + + @Override + public void afterPropertiesSet() + { + RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解 替代path variable 为 * + Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class); + Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + + // 获取类上边的注解, 替代path variable 为 * + Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class); + Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException + { + this.applicationContext = context; + } + + public List getUrls() + { + return urls; + } + + public void setUrls(List urls) + { + this.urls = urls; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/datasource/DynamicDataSource.java b/huacai-framework/src/main/java/com/huacai/framework/datasource/DynamicDataSource.java new file mode 100644 index 0000000..94e10df --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/datasource/DynamicDataSource.java @@ -0,0 +1,44 @@ +package com.huacai.framework.datasource; + +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +/** + * 动态数据源 + * 继承Spring AbstractRoutingDataSource,实现多数据源的动态切换功能 + * 可根据当前线程上下文 上下文选择不同的数据源进行操作 + * + * @author huacai + */ +public class DynamicDataSource extends AbstractRoutingDataSource +{ + /** + * 构造方法,初始化动态数据源 + * + * @param defaultTargetDataSource 默认数据源(当未指定数据源时使用) + * @param targetDataSources 目标数据源集合(key为数据源标识,value为对应的数据源实例) + */ + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) + { + // 设置默认目标数据源 + super.setDefaultTargetDataSource(defaultTargetDataSource); + // 设置目标数据源映射关系 + super.setTargetDataSources(targetDataSources); + // 初始化数据源,使配置生效 + super.afterPropertiesSet(); + } + + /** + * 确定当前使用的数据源标识 + * 该方法返回的key将用于从targetDataSources中查找对应的数据源 + * + * @return 当前数据源的标识(从动态数据源上下文持有者中获取) + */ + @Override + protected Object determineCurrentLookupKey() + { + // 从上下文持有者中获取当前线程绑定的数据源类型 + return DynamicDataSourceContextHolder.getDataSourceType(); + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/datasource/DynamicDataSourceContextHolder.java b/huacai-framework/src/main/java/com/huacai/framework/datasource/DynamicDataSourceContextHolder.java new file mode 100644 index 0000000..d0b0d5f --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/datasource/DynamicDataSourceContextHolder.java @@ -0,0 +1,45 @@ +package com.huacai.framework.datasource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 数据源切换处理 + * + * @author huacai + */ +public class DynamicDataSourceContextHolder +{ + public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); + + /** + * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, + * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 + */ + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源的变量 + */ + public static void setDataSourceType(String dsType) + { + log.info("切换到{}数据源", dsType); + CONTEXT_HOLDER.set(dsType); + } + + /** + * 获得数据源的变量 + */ + public static String getDataSourceType() + { + return CONTEXT_HOLDER.get(); + } + + /** + * 清空数据源变量 + */ + public static void clearDataSourceType() + { + CONTEXT_HOLDER.remove(); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/interceptor/DemoEnvironmentInterceptor.java b/huacai-framework/src/main/java/com/huacai/framework/interceptor/DemoEnvironmentInterceptor.java new file mode 100644 index 0000000..d5abac9 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/interceptor/DemoEnvironmentInterceptor.java @@ -0,0 +1,60 @@ +package com.huacai.framework.interceptor; + +import com.huacai.common.exception.DemoEnvironmentException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 演示环境拦截器 + * 用于在演示环境中限制非GET请求,防止数据被修改 + */ +// 注册为Spring组件 +@Component +public class DemoEnvironmentInterceptor implements HandlerInterceptor { + + // 从配置文件中读取是否启用演示环境(默认不启用) + @Value("${demo.env.enabled:false}") + private boolean isDemoEnvironment; + + // 需要排除的URL(这些URL即使在演示环境也允许所有请求) + private static final String[] EXCLUDE_URLS = { + "/login", // 登录 + "/logout", // 注销 + "/captchaImage" // 验证码 + }; + + /** + * 请求处理前拦截 + * 用于判断是否为演示环境,并限制非GET请求 + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 如果不是演示环境,直接放行 + if (!isDemoEnvironment) { + return true; + } + + // 获取当前请求的URI + String requestURI = request.getRequestURI(); + + // 检查请求URI是否在排除列表中,如果是则放行 + for (String excludeUrl : EXCLUDE_URLS) { + if (requestURI.contains(excludeUrl)) { + return true; + } + } + + // 拦截所有非GET请求(演示环境不允许修改操作) + String method = request.getMethod(); + if (!"GET".equalsIgnoreCase(method)) { + throw new DemoEnvironmentException("演示环境,不允许进行此操作"); + } + + // GET请求放行 + return true; + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/interceptor/RepeatSubmitInterceptor.java b/huacai-framework/src/main/java/com/huacai/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 0000000..82c7ad8 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,77 @@ +package com.huacai.framework.interceptor; + +import java.lang.reflect.Method; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import com.alibaba.fastjson2.JSON; +import com.huacai.common.annotation.RepeatSubmit; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.utils.ServletUtils; + +/** + * 防止重复提交拦截器 + * 抽象类,定义防止重复提交的拦截逻辑,具体验证规则由子类实现 + * + * @author huacai + */ +@Component +public abstract class RepeatSubmitInterceptor implements HandlerInterceptor +{ + /** + * 请求处理前的拦截方法 + * 用于判断当前请求是否为重复提交,若为重复提交则返回错误响应 + * + * @param request HTTP请求对象 + * @param response HTTP响应对象 + * @param handler 处理器对象(当前请求对应的Controller方法) + * @return true-继续处理请求;false-中断请求处理 + * @throws Exception 处理过程中发生异常时抛出 + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + // 判断处理器是否为HandlerMethod(即Controller中的方法) + if (handler instanceof HandlerMethod) + { + HandlerMethod handlerMethod = (HandlerMethod) handler; + // 获取当前请求对应的方法对象 + Method method = handlerMethod.getMethod(); + // 检查方法上是否标注了@RepeatSubmit注解 + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) + { + // 调用子类实现的方法判断是否为重复提交 + if (this.isRepeatSubmit(request, annotation)) + { + // 若为重复提交,构建错误响应对象 + AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + // 将错误响应转换为JSON并写入响应流 + ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); + // 返回false中断请求处理 + return false; + } + } + // 非重复提交,继续处理请求 + return true; + } + else + { + // 非Controller方法请求,直接放行 + return true; + } + } + + /** + * 验证是否重复提交(抽象方法) + * 由子类实现具体的防重复提交规则(如基于参数、令牌、时间等) + * + * @param request 请求信息 + * @param annotation 防重复提交注解,包含自定义消息和间隔时间等参数 + * @return true-是重复提交;false-不是重复提交 + * @throws Exception 验证过程中发生异常时抛出 + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/interceptor/impl/SameUrlDataInterceptor.java b/huacai-framework/src/main/java/com/huacai/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 0000000..fff68fd --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,145 @@ +package com.huacai.framework.interceptor.impl; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.huacai.common.annotation.RepeatSubmit; +import com.huacai.common.constant.CacheConstants; +import com.huacai.common.core.redis.RedisCache; +import com.huacai.common.filter.RepeatedlyRequestWrapper; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.http.HttpHelper; +import com.huacai.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 重复提交拦截器实现类(基于URL和请求数据判断) + * 判断请求的URL和数据是否与上一次相同,若相同则视为重复提交表单(默认有效时间10秒内) + * + * @author huacai + */ +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor +{ + // 用于存储请求参数的Map键名 + public final String REPEAT_PARAMS = "repeatParams"; + + // 用于存储请求时间的Map键名 + public final String REPEAT_TIME = "repeatTime"; + + // 从配置文件中获取令牌的请求头自定义标识(如Token的Header名称) + @Value("${token.header}") + private String header; + + // 注入Redis缓存工具,用于存储请求记录 + @Autowired + private RedisCache redisCache; + + /** + * 判断是否为重复提交 + * + * @param request 当前HTTP请求对象 + * @param annotation 重复提交注解,包含间隔时间等配置 + * @return true-是重复提交;false-不是重复提交 + */ + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) + { + // 存储当前请求的参数 + String nowParams = ""; + // 如果请求是可重复读取的包装类(解决请求体只能读一次的问题) + if (request instanceof RepeatedlyRequestWrapper) + { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + // 获取请求体中的参数 + nowParams = HttpHelper.getBodyString(repeatedlyRequest); + } + + // 如果body参数为空,则获取URL参数(如GET请求的query参数) + if (StringUtils.isEmpty(nowParams)) + { + nowParams = JSON.toJSONString(request.getParameterMap()); + } + + // 存储当前请求的数据(参数和时间) + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); // 当前请求参数 + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 当前请求时间戳 + + // 获取请求的URL(作为缓存的一部分key) + String url = request.getRequestURI(); + + // 从请求头中获取令牌(作为唯一标识,若为空则用空字符串) + String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); + + // 构建Redis缓存的key:前缀 + URL + 令牌(确保同一用户同一URL的唯一性) + String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; + + // 从Redis中获取之前的请求记录 + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) + { + // 将缓存中的数据转换为Map + Map sessionMap = (Map) sessionObj; + // 判断缓存中是否存在当前URL的请求记录 + if (sessionMap.containsKey(url)) + { + // 获取上一次请求的数据 + Map preDataMap = (Map) sessionMap.get(url); + // 比较参数是否相同且时间间隔是否小于注解配置的间隔时间 + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) + { + // 满足条件,视为重复提交 + return true; + } + } + } + + // 若不是重复提交,则将当前请求数据存入Redis + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + // 设置缓存过期时间为注解配置的间隔时间(毫秒) + redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); + // 不是重复提交 + return false; + } + + /** + * 比较两次请求的参数是否相同 + * + * @param nowMap 当前请求的数据Map + * @param preMap 上一次请求的数据Map + * @return true-参数相同;false-参数不同 + */ + private boolean compareParams(Map nowMap, Map preMap) + { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 比较两次请求的时间间隔是否小于指定的间隔时间 + * + * @param nowMap 当前请求的数据Map + * @param preMap 上一次请求的数据Map + * @param interval 允许的间隔时间(毫秒) + * @return true-间隔时间小于指定值;false-间隔时间大于等于指定值 + */ + private boolean compareTime(Map nowMap, Map preMap, int interval) + { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + // 若当前时间与上一次时间的差值小于间隔时间,则视为重复提交 + if ((time1 - time2) < interval) + { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/manager/AsyncManager.java b/huacai-framework/src/main/java/com/huacai/framework/manager/AsyncManager.java new file mode 100644 index 0000000..3e947ab --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/manager/AsyncManager.java @@ -0,0 +1,70 @@ +package com.huacai.framework.manager; + +import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import com.huacai.common.utils.Threads; +import com.huacai.common.utils.spring.SpringUtils; + +/** + * 异步任务管理器 + * 负责管理异步任务的调度执行,基于单例模式提供统一的异步任务处理入口 + * + * @author huacai + */ +public class AsyncManager +{ + /** + * 操作延迟时间(10毫秒) + * 表示提交的异步任务将在延迟10毫秒后执行 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + * 通过Spring工具类获取配置好的定时任务线程池实例(对应bean名称:scheduledExecutorService) + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 单例模式实现 - 私有构造方法 + * 防止外部通过new创建实例,保证全局唯一 + */ + private AsyncManager(){} + + /** + * 单例实例对象 + * 类加载时初始化,保证线程安全 + */ + private static AsyncManager me = new AsyncManager(); + + /** + * 获取单例实例 + * + * @return 异步任务管理器的唯一实例 + */ + public static AsyncManager me() + { + return me; + } + + /** + * 执行异步任务 + * 将任务提交到调度线程池,延迟指定时间(OPERATE_DELAY_TIME)后执行 + * + * @param task 待执行的定时任务(TimerTask类型) + */ + public void execute(TimerTask task) + { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + + /** + * 停止任务线程池 + * 调用线程工具类的方法,优雅关闭线程池并等待其终止 + */ + public void shutdown() + { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/manager/ShutdownManager.java b/huacai-framework/src/main/java/com/huacai/framework/manager/ShutdownManager.java new file mode 100644 index 0000000..2cd1f95 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/manager/ShutdownManager.java @@ -0,0 +1,50 @@ +package com.huacai.framework.manager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import javax.annotation.PreDestroy; + +/** + * 应用关闭管理器 + * 用于确保应用退出时确保后台线程(如异步任务线程池)被正确关闭,避免资源泄露 + * + * @author huacai + */ +@Component +public class ShutdownManager +{ + // 日志记录器,使用"sys-user"日志名 + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + /** + * 应用销毁前执行的方法 + * 标注@PreDestroy注解,在Spring容器销毁该Bean之前自动调用 + */ + @PreDestroy + public void destroy() + { + // 关闭异步任务管理器 + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务的线程池 + * 调用AsyncManager的shutdown方法,优雅关闭线程池 + */ + private void shutdownAsyncManager() + { + try + { + // 记录关闭线程池的日志 + logger.info("====关闭后台任务任务线程池===="); + // 获取异步任务管理器单例并调用shutdown方法 + AsyncManager.me().shutdown(); + } + catch (Exception e) + { + // 记录关闭过程中发生的异常 + logger.error(e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/manager/factory/AsyncFactory.java b/huacai-framework/src/main/java/com/huacai/framework/manager/factory/AsyncFactory.java new file mode 100644 index 0000000..287eca3 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/manager/factory/AsyncFactory.java @@ -0,0 +1,102 @@ +package com.huacai.framework.manager.factory; + +import java.util.TimerTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.huacai.common.constant.Constants; +import com.huacai.common.utils.LogUtils; +import com.huacai.common.utils.ServletUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.ip.AddressUtils; +import com.huacai.common.utils.ip.IpUtils; +import com.huacai.common.utils.spring.SpringUtils; +import com.huacai.system.domain.SysLogininfor; +import com.huacai.system.domain.SysOperLog; +import com.huacai.system.service.ISysLogininforService; +import com.huacai.system.service.ISysOperLogService; +import eu.bitwalker.useragentutils.UserAgent; + +/** + * 异步工厂(产生任务用) + * + * @author huacai + */ +public class AsyncFactory +{ + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) + { + final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + final String ip = IpUtils.getIpAddr(); + return new TimerTask() + { + @Override + public void run() + { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = userAgent.getOperatingSystem().getName(); + // 获取客户端浏览器 + String browser = userAgent.getBrowser().getName(); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.SUCCESS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) + { + return new TimerTask() + { + @Override + public void run() + { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/security/context/AuthenticationContextHolder.java b/huacai-framework/src/main/java/com/huacai/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..b9e06c1 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,46 @@ +package com.huacai.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息持有者 + * 用于在当前线程中存储和获取Spring Security的身份验证对象(Authentication) + * 提供线程隔离的身份信息管理,确保多线程环境下的安全性 + * + * @author huacai + */ +public class AuthenticationContextHolder +{ + // 线程本地变量,用于存储当前线程的Authentication对象 + // ThreadLocal确保每个线程只能访问自己的变量副本,实现线程隔离 + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + /** + * 获取当前线程中的身份验证对象 + * + * @return 当前线程的Authentication对象,若不存在则返回null + */ + public static Authentication getContext() + { + return contextHolder.get(); + } + + /** + * 设置当前线程的身份验证对象 + * + * @param context 要存储的Authentication对象 + */ + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + /** + * 清除当前线程中的身份验证对象 + * 通常在请求处理完成后调用,避免线程复用导致的信息泄露 + */ + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/security/context/PermissionContextHolder.java b/huacai-framework/src/main/java/com/huacai/framework/security/context/PermissionContextHolder.java new file mode 100644 index 0000000..1035177 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/security/context/PermissionContextHolder.java @@ -0,0 +1,43 @@ +package com.huacai.framework.security.context; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import com.huacai.common.core.text.Convert; + +/** + * 权限信息持有者 + * 用于在当前请求范围内存储和获取权限相关信息,基于请求上下文实现 + * + * @author huacai + */ +public class PermissionContextHolder +{ + // 权限上下文在请求属性中的存储键名 + private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT"; + + /** + * 设置当前请求的权限信息 + * 将权限字符串存入当前请求的属性中,作用域为当前请求 + * + * @param permission 要存储的权限信息字符串 + */ + public static void setContext(String permission) + { + // 获取当前请求的属性对象,将权限信息存入,作用域为REQUEST(仅当前请求有效) + RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission, + RequestAttributes.SCOPE_REQUEST); + } + + /** + * 获取当前请求的权限信息 + * 从当前请求的属性中获取之前存储的权限信息,转换为字符串 + * + * @return 当前请求的权限信息字符串,若不存在则返回空字符串 + */ + public static String getContext() + { + // 从当前请求属性中获取权限信息,转换为字符串后返回 + return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES, + RequestAttributes.SCOPE_REQUEST)); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/security/filter/JwtAuthenticationTokenFilter.java b/huacai-framework/src/main/java/com/huacai/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..ce7f95c --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,66 @@ +package com.huacai.framework.security.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.utils.SecurityUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.framework.web.service.TokenService; + +/** + * JWT身份验证令牌过滤器 + * 用于验证请求中的JWT令牌有效性,并将解析后的用户信息存入Spring Security上下文 + * 确保每个请求只被过滤一次(继承OncePerRequestFilter) + * + * @author huacai + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter +{ + // 注入令牌服务,用于处理令牌的解析、验证等操作 + @Autowired + private TokenService tokenService; + + /** + * 过滤器核心方法,处理每个请求 + * + * @param request 当前HTTP请求对象 + * @param response 当前HTTP响应对象 + * @param chain 过滤器链,用于传递请求到下一个过滤器 + * @throws ServletException 处理请求时发生的Servlet异常 + * @throws IOException 处理请求时发生的IO异常 + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException + { + // 从请求中获取登录用户信息(通过TokenService解析请求中的令牌) + LoginUser loginUser = tokenService.getLoginUser(request); + + // 若登录用户信息存在,且当前Security上下文没有认证信息 + if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) + { + // 验证令牌的有效性(如是否过期、是否被篡改等) + tokenService.verifyToken(loginUser); + + // 创建认证令牌对象,包含用户信息和权限集合 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + // 设置认证详情(如请求IP、会话ID等) + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // 将认证信息存入Security上下文,供后续权限校验使用 + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + + // 继续执行过滤器链,将请求传递给下一个过滤器或目标资源 + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/security/handle/AuthenticationEntryPointImpl.java b/huacai-framework/src/main/java/com/huacai/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..f302e22 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,48 @@ +package com.huacai.framework.security.handle; + +import java.io.IOException; +import java.io.Serializable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.huacai.common.constant.HttpStatus; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.utils.ServletUtils; +import com.huacai.common.utils.StringUtils; + +/** + * 认证失败处理类 + * 实现Spring Security的AuthenticationEntryPoint接口,用于处理未认证请求(返回未授权响应) + * + * @author huacai + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + // 序列化版本号,用于对象序列化时的版本控制 + private static final long serialVersionUID = -8970718410437077606L; + + /** + * 处理认证失败的请求 + * 当用户未认证(如未登录、令牌无效等)访问受保护资源时,该方法被调用 + * + * @param request 当前HTTP请求对象 + * @param response 当前HTTP响应对象 + * @param e 认证异常(包含认证失败的原因) + * @throws IOException 处理响应时发生的IO异常 + */ + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { + // 设置错误状态码为401(未授权) + int code = HttpStatus.UNAUTHORIZED; + // 构建错误消息,包含请求的URI和认证失败提示 + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + // 将错误信息封装为AjaxResult对象,转换为JSON并写入响应流 + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/security/handle/LogoutSuccessHandlerImpl.java b/huacai-framework/src/main/java/com/huacai/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..29cfa91 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,62 @@ +package com.huacai.framework.security.handle; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import com.alibaba.fastjson2.JSON; +import com.huacai.common.constant.Constants; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.utils.MessageUtils; +import com.huacai.common.utils.ServletUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.framework.manager.AsyncManager; +import com.huacai.framework.manager.factory.AsyncFactory; +import com.huacai.framework.web.service.TokenService; + +/** + * 自定义退出处理类 + * 实现Spring Security的LogoutSuccessHandler接口,用于处理用户退出登录成功后的逻辑 + * + * @author huacai + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler +{ + // 注入令牌服务,用于处理登录用户的令牌信息 + @Autowired + private TokenService tokenService; + + /** + * 退出登录成功后的处理方法 + * + * @param request HTTP请求对象 + * @param response HTTP响应对象 + * @param authentication 认证信息对象(包含当前登录用户信息) + * @throws IOException 处理响应时发生的IO异常 + * @throws ServletException 处理请求时发生的Servlet异常 + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException + { + // 从请求中获取当前登录用户信息 + LoginUser loginUser = tokenService.getLoginUser(request); + // 若登录用户信息存在 + if (StringUtils.isNotNull(loginUser)) + { + String userName = loginUser.getUsername(); + // 从缓存中删除该用户的登录记录(使令牌失效) + tokenService.delLoginUser(loginUser.getToken()); + // 异步记录用户退出日志(使用异步管理器执行日志记录任务) + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success"))); + } + // 向客户端返回退出成功的响应(JSON格式的成功消息) + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success")))); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/domain/Server.java b/huacai-framework/src/main/java/com/huacai/framework/web/domain/Server.java new file mode 100644 index 0000000..ff7e48b --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/domain/Server.java @@ -0,0 +1,302 @@ +package com.huacai.framework.web.domain; + +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import com.huacai.common.utils.Arith; +import com.huacai.common.utils.ip.IpUtils; +import com.huacai.framework.web.domain.server.Cpu; +import com.huacai.framework.web.domain.server.Jvm; +import com.huacai.framework.web.domain.server.Mem; +import com.huacai.framework.web.domain.server.Sys; +import com.huacai.framework.web.domain.server.SysFile; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +/** + * 服务器相关信息实体类 + * 用于封装服务器的CPU、内存、JVM、磁盘等信息 + * + * @author huacai + */ +public class Server +{ + // OSHI库获取硬件信息时的等待时间(毫秒) + private static final int OSHI_WAIT_SECOND = 1000; + + /** + * CPU相关信息 + */ + private Cpu cpu = new Cpu(); + + /** + * 内存相关信息 + */ + private Mem mem = new Mem(); + + /** + * JVM相关信息 + */ + private Jvm jvm = new Jvm(); + + /** + * 服务器相关信息 + */ + private Sys sys = new Sys(); + + /** + * 磁盘相关信息列表 + */ + private List sysFiles = new LinkedList(); + + // 获取CPU信息 + public Cpu getCpu() + { + return cpu; + } + + // 设置CPU信息 + public void setCpu(Cpu cpu) + { + this.cpu = cpu; + } + + // 获取内存信息 + public Mem getMem() + { + return mem; + } + + // 设置内存信息 + public void setMem(Mem mem) + { + this.mem = mem; + } + + // 获取JVM信息 + public Jvm getJvm() + { + return jvm; + } + + // 设置JVM信息 + public void setJvm(Jvm jvm) + { + this.jvm = jvm; + } + + // 获取服务器信息 + public Sys getSys() + { + return sys; + } + + // 设置服务器信息 + public void setSys(Sys sys) + { + this.sys = sys; + } + + // 获取磁盘信息列表 + public List getSysFiles() + { + return sysFiles; + } + + // 设置磁盘信息列表 + public void setSysFiles(List sysFiles) + { + this.sysFiles = sysFiles; + } + + /** + * 从系统中获取并填充服务器所有信息 + * 使用OSHI库获取硬件信息 + * + * @throws Exception 可能的异常(如硬件信息获取失败) + */ + public void copyTo() throws Exception + { + SystemInfo si = new SystemInfo(); // OSHI系统信息入口 + HardwareAbstractionLayer hal = si.getHardware(); // 硬件抽象层 + + // 设置CPU信息 + setCpuInfo(hal.getProcessor()); + + // 设置内存信息 + setMemInfo(hal.getMemory()); + + // 设置服务器基础信息 + setSysInfo(); + + // 设置JVM信息 + setJvmInfo(); + + // 设置磁盘信息 + setSysFiles(si.getOperatingSystem()); + } + + /** + * 设置CPU信息 + * + * @param processor OSHI的CPU处理器对象 + */ + private void setCpuInfo(CentralProcessor processor) + { + // 获取CPU负载滴答数(第一次采样) + long[] prevTicks = processor.getSystemCpuLoadTicks(); + // 等待一段时间(让CPU负载有变化) + Util.sleep(OSHI_WAIT_SECOND); + // 获取CPU负载滴答数(第二次采样) + long[] ticks = processor.getSystemCpuLoadTicks(); + + // 计算各状态的CPU时间(两次采样的差值) + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; // 低优先级用户态时间 + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; // 硬中断时间 + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; // 软中断时间 + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; // 被其他虚拟机占用的时间 + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; // 系统态时间 + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; // 用户态时间 + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; // IO等待时间 + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; // 空闲时间 + + // 总CPU时间 + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + + // 设置CPU核心数 + cpu.setCpuNum(processor.getLogicalProcessorCount()); + // 设置总CPU时间(用于计算使用率) + cpu.setTotal(totalCpu); + // 设置系统态时间 + cpu.setSys(cSys); + // 设置用户态时间 + cpu.setUsed(user); + // 设置IO等待时间 + cpu.setWait(iowait); + // 设置空闲时间 + cpu.setFree(idle); + } + + /** + * 设置内存信息 + * + * @param memory OSHI的内存对象 + */ + private void setMemInfo(GlobalMemory memory) + { + // 设置总内存(字节) + mem.setTotal(memory.getTotal()); + // 设置已使用内存(总内存 - 可用内存) + mem.setUsed(memory.getTotal() - memory.getAvailable()); + // 设置空闲内存 + mem.setFree(memory.getAvailable()); + } + + /** + * 设置服务器基础信息 + */ + private void setSysInfo() + { + Properties props = System.getProperties(); // 系统属性 + // 设置服务器名称(主机名) + sys.setComputerName(IpUtils.getHostName()); + // 设置服务器IP地址 + sys.setComputerIp(IpUtils.getHostIp()); + // 设置操作系统名称 + sys.setOsName(props.getProperty("os.name")); + // 设置系统架构 + sys.setOsArch(props.getProperty("os.arch")); + // 设置项目部署路径 + sys.setUserDir(props.getProperty("user.dir")); + } + + /** + * 设置JVM信息 + * + * @throws UnknownHostException 可能的主机信息获取异常 + */ + private void setJvmInfo() throws UnknownHostException + { + Properties props = System.getProperties(); + // 设置JVM总内存(字节) + jvm.setTotal(Runtime.getRuntime().totalMemory()); + // 设置JVM最大内存(字节) + jvm.setMax(Runtime.getRuntime().maxMemory()); + // 设置JVM空闲内存(字节) + jvm.setFree(Runtime.getRuntime().freeMemory()); + // 设置JDK版本 + jvm.setVersion(props.getProperty("java.version")); + // 设置JDK安装路径 + jvm.setHome(props.getProperty("java.home")); + } + + /** + * 设置磁盘信息 + * + * @param os OSHI的操作系统对象 + */ + private void setSysFiles(OperatingSystem os) + { + FileSystem fileSystem = os.getFileSystem(); // 文件系统 + List fsArray = fileSystem.getFileStores(); // 磁盘分区列表 + + // 遍历所有磁盘分区,封装信息 + for (OSFileStore fs : fsArray) + { + long free = fs.getUsableSpace(); // 可用空间(字节) + long total = fs.getTotalSpace(); // 总空间(字节) + long used = total - free; // 已使用空间 + + SysFile sysFile = new SysFile(); + sysFile.setDirName(fs.getMount()); // 挂载点路径 + sysFile.setSysTypeName(fs.getType()); // 文件系统类型 + sysFile.setTypeName(fs.getName()); // 分区名称 + sysFile.setTotal(convertFileSize(total)); // 总大小(格式化) + sysFile.setFree(convertFileSize(free)); // 剩余大小(格式化) + sysFile.setUsed(convertFileSize(used)); // 已使用大小(格式化) + // 计算使用率(保留两位小数) + sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100)); + sysFiles.add(sysFile); + } + } + + /** + * 字节大小转换为易读格式(如B、KB、MB、GB) + * + * @param size 字节大小 + * @return 转换后的字符串(如1.5 GB) + */ + public String convertFileSize(long size) + { + long kb = 1024; + long mb = kb * 1024; + long gb = mb * 1024; + + if (size >= gb) + { + return String.format("%.1f GB", (float) size / gb); + } + else if (size >= mb) + { + float f = (float) size / mb; + return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); // 大于100MB时取整 + } + else if (size >= kb) + { + float f = (float) size / kb; + return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); // 大于100KB时取整 + } + else + { + return String.format("%d B", size); + } + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Cpu.java b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Cpu.java new file mode 100644 index 0000000..099f8d1 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Cpu.java @@ -0,0 +1,129 @@ +package com.huacai.framework.web.domain.server; + +import com.huacai.common.utils.Arith; + +/** + * CPU相关信息实体类 + * 用于封装CPU的核心数、使用率等信息 + * + * @author huacai + */ +public class Cpu +{ + /** + * 核心数 + */ + private int cpuNum; + + /** + * CPU总的使用率(原始值,未格式化) + */ + private double total; + + /** + * CPU系统使用率(原始值,未格式化) + */ + private double sys; + + /** + * CPU用户使用率(原始值,未格式化) + */ + private double used; + + /** + * CPU当前等待率(原始值,未格式化) + */ + private double wait; + + /** + * CPU当前空闲率(原始值,未格式化) + */ + private double free; + + // 获取CPU核心数 + public int getCpuNum() + { + return cpuNum; + } + + // 设置CPU核心数 + public void setCpuNum(int cpuNum) + { + this.cpuNum = cpuNum; + } + + /** + * 获取CPU总使用率(格式化后,百分比) + * 计算方式:原始总使用率 * 100,保留2位小数 + */ + public double getTotal() + { + return Arith.round(Arith.mul(total, 100), 2); + } + + // 设置CPU总使用率原始值 + public void setTotal(double total) + { + this.total = total; + } + + /** + * 获取CPU系统使用率(格式化后,百分比) + * 计算方式:(系统使用率 / 总使用率) * 100,保留2位小数 + */ + public double getSys() + { + return Arith.round(Arith.mul(sys / total, 100), 2); + } + + // 设置CPU系统使用率原始值 + public void setSys(double sys) + { + this.sys = sys; + } + + /** + * 获取CPU用户使用率(格式化后,百分比) + * 计算方式:(用户使用率 / 总使用率) * 100,保留2位小数 + */ + public double getUsed() + { + return Arith.round(Arith.mul(used / total, 100), 2); + } + + // 设置CPU用户使用率原始值 + public void setUsed(double used) + { + this.used = used; + } + + /** + * 获取CPU当前等待率(格式化后,百分比) + * 计算方式:(等待率 / 总使用率) * 100,保留2位小数 + */ + public double getWait() + { + return Arith.round(Arith.mul(wait / total, 100), 2); + } + + // 设置CPU当前等待率原始值 + public void setWait(double wait) + { + this.wait = wait; + } + + /** + * 获取CPU当前空闲率(格式化后,百分比) + * 计算方式:(空闲率 / 总使用率) * 100,保留2位小数 + */ + public double getFree() + { + return Arith.round(Arith.mul(free / total, 100), 2); + } + + // 设置CPU当前空闲率原始值 + public void setFree(double free) + { + this.free = free; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Jvm.java b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Jvm.java new file mode 100644 index 0000000..1078309 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Jvm.java @@ -0,0 +1,162 @@ +package com.huacai.framework.web.domain.server; + +import java.lang.management.ManagementFactory; +import com.huacai.common.utils.Arith; +import com.huacai.common.utils.DateUtils; + +/** + * JVM相关信息实体类 + * 用于封装JVM的内存使用、版本、运行时间等信息 + * + * @author huacai + */ +public class Jvm +{ + /** + * 当前JVM占用的内存总数(字节,原始值) + */ + private double total; + + /** + * JVM最大可用内存总数(字节,原始值) + */ + private double max; + + /** + * JVM空闲内存(字节,原始值) + */ + private double free; + + /** + * JDK版本 + */ + private String version; + + /** + * JDK安装路径 + */ + private String home; + + /** + * 获取当前JVM占用的内存总数(M) + * 计算方式:原始字节数 / (1024*1024),保留2位小数 + */ + public double getTotal() + { + return Arith.div(total, (1024 * 1024), 2); + } + + // 设置当前JVM占用的内存总数(原始字节数) + public void setTotal(double total) + { + this.total = total; + } + + /** + * 获取JVM最大可用内存总数(M) + * 计算方式:原始字节数 / (1024*1024),保留2位小数 + */ + public double getMax() + { + return Arith.div(max, (1024 * 1024), 2); + } + + // 设置JVM最大可用内存总数(原始字节数) + public void setMax(double max) + { + this.max = max; + } + + /** + * 获取JVM空闲内存(M) + * 计算方式:原始字节数 / (1024*1024),保留2位小数 + */ + public double getFree() + { + return Arith.div(free, (1024 * 1024), 2); + } + + // 设置JVM空闲内存(原始字节数) + public void setFree(double free) + { + this.free = free; + } + + /** + * 获取JVM已使用内存(M) + * 计算方式:(总内存 - 空闲内存) / (1024*1024),保留2位小数 + */ + public double getUsed() + { + return Arith.div(total - free, (1024 * 1024), 2); + } + + /** + * 获取JVM内存使用率(百分比) + * 计算方式:(已使用内存 / 总内存) * 100 + */ + public double getUsage() + { + return Arith.mul(Arith.div(total - free, total, 4), 100); + } + + /** + * 获取JDK名称 + * 从运行时管理Bean中获取虚拟机名称 + */ + public String getName() + { + return ManagementFactory.getRuntimeMXBean().getVmName(); + } + + // 获取JDK版本 + public String getVersion() + { + return version; + } + + // 设置JDK版本 + public void setVersion(String version) + { + this.version = version; + } + + // 获取JDK安装路径 + public String getHome() + { + return home; + } + + // 设置JDK安装路径 + public void setHome(String home) + { + this.home = home; + } + + /** + * 获取JDK启动时间 + * 格式化服务器启动时间为指定字符串格式 + */ + public String getStartTime() + { + return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate()); + } + + /** + * 获取JDK运行时间 + * 计算当前时间与服务器启动时间的差值并格式化 + */ + public String getRunTime() + { + return DateUtils.timeDistance(DateUtils.getNowDate(), DateUtils.getServerStartDate()); + } + + /** + * 获取JVM运行参数 + * 从运行时管理Bean中获取输入参数并转为字符串 + */ + public String getInputArgs() + { + return ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Mem.java b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Mem.java new file mode 100644 index 0000000..aecb3a9 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Mem.java @@ -0,0 +1,81 @@ +package com.huacai.framework.web.domain.server; + +import com.huacai.common.utils.Arith; + +/** + * 内存相关信息实体类 + * 用于封装内存的总大小、已使用、空闲及使用率等信息 + * + * @author huacai + */ +public class Mem +{ + /** + * 内存总量(字节,原始值) + */ + private double total; + + /** + * 已用内存(字节,原始值) + */ + private double used; + + /** + * 剩余内存(字节,原始值) + */ + private double free; + + /** + * 获取内存总量(GB,格式化后) + * 计算方式:原始字节数 / (1024*1024*1024),保留2位小数 + */ + public double getTotal() + { + return Arith.div(total, (1024 * 1024 * 1024), 2); + } + + // 设置内存总量(原始字节数) + public void setTotal(long total) + { + this.total = total; + } + + /** + * 获取已用内存(GB,格式化后) + * 计算方式:原始字节数 / (1024*1024*1024),保留2位小数 + */ + public double getUsed() + { + return Arith.div(used, (1024 * 1024 * 1024), 2); + } + + // 设置已用内存(原始字节数) + public void setUsed(long used) + { + this.used = used; + } + + /** + * 获取剩余内存(GB,格式化后) + * 计算方式:原始字节数 / (1024*1024*1024),保留2位小数 + */ + public double getFree() + { + return Arith.div(free, (1024 * 1024 * 1024), 2); + } + + // 设置剩余内存(原始字节数) + public void setFree(long free) + { + this.free = free; + } + + /** + * 获取内存使用率(百分比) + * 计算方式:(已用内存 / 总内存) * 100 + */ + public double getUsage() + { + return Arith.mul(Arith.div(used, total, 4), 100); + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Sys.java b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Sys.java new file mode 100644 index 0000000..370f331 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/Sys.java @@ -0,0 +1,95 @@ +package com.huacai.framework.web.domain.server; + +/** + * 系统相关信息实体类 + * 用于封装服务器的基础信息(名称、IP、操作系统等) + * + * @author huacai + */ +public class Sys +{ + /** + * 服务器名称 + */ + private String computerName; + + /** + * 服务器IP地址 + */ + private String computerIp; + + /** + * 项目部署路径 + */ + private String userDir; + + /** + * 操作系统名称 + */ + private String osName; + + /** + * 系统架构(如x86_64) + */ + private String osArch; + + // 获取服务器名称 + public String getComputerName() + { + return computerName; + } + + // 设置服务器名称 + public void setComputerName(String computerName) + { + this.computerName = computerName; + } + + // 获取服务器IP地址 + public String getComputerIp() + { + return computerIp; + } + + // 设置服务器IP地址 + public void setComputerIp(String computerIp) + { + this.computerIp = computerIp; + } + + // 获取项目部署路径 + public String getUserDir() + { + return userDir; + } + + // 设置项目部署路径 + public void setUserDir(String userDir) + { + this.userDir = userDir; + } + + // 获取操作系统名称 + public String getOsName() + { + return osName; + } + + // 设置操作系统名称 + public void setOsName(String osName) + { + this.osName = osName; + } + + // 获取系统架构 + public String getOsArch() + { + return osArch; + } + + // 设置系统架构 + public void setOsArch(String osArch) + { + this.osArch = osArch; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/SysFile.java b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/SysFile.java new file mode 100644 index 0000000..0782f68 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/domain/server/SysFile.java @@ -0,0 +1,129 @@ +package com.huacai.framework.web.domain.server; + +/** + * 系统文件相关信息实体类 + * 用于封装磁盘分区的使用情况信息 + * + * @author huacai + */ +public class SysFile +{ + /** + * 盘符路径(如C:/、D:/) + */ + private String dirName; + + /** + * 盘符类型(如NTFS、ext4) + */ + private String sysTypeName; + + /** + * 文件类型描述 + */ + private String typeName; + + /** + * 总大小(格式化后,如100GB) + */ + private String total; + + /** + * 剩余大小(格式化后) + */ + private String free; + + /** + * 已使用大小(格式化后) + */ + private String used; + + /** + * 资源的使用率(百分比) + */ + private double usage; + + // 获取盘符路径 + public String getDirName() + { + return dirName; + } + + // 设置盘符路径 + public void setDirName(String dirName) + { + this.dirName = dirName; + } + + // 获取盘符类型 + public String getSysTypeName() + { + return sysTypeName; + } + + // 设置盘符类型 + public void setSysTypeName(String sysTypeName) + { + this.sysTypeName = sysTypeName; + } + + // 获取文件类型描述 + public String getTypeName() + { + return typeName; + } + + // 设置文件类型描述 + public void setTypeName(String typeName) + { + this.typeName = typeName; + } + + // 获取总大小 + public String getTotal() + { + return total; + } + + // 设置总大小 + public void setTotal(String total) + { + this.total = total; + } + + // 获取剩余大小 + public String getFree() + { + return free; + } + + // 设置剩余大小 + public void setFree(String free) + { + this.free = free; + } + + // 获取已使用大小 + public String getUsed() + { + return used; + } + + // 设置已使用大小 + public void setUsed(String used) + { + this.used = used; + } + + // 获取资源使用率 + public double getUsage() + { + return usage; + } + + // 设置资源使用率 + public void setUsage(double usage) + { + this.usage = usage; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/exception/GlobalExceptionHandler.java b/huacai-framework/src/main/java/com/huacai/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f788180 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,192 @@ +package com.huacai.framework.web.exception; + +import javax.servlet.http.HttpServletRequest; + +import com.huacai.common.exception.DemoEnvironmentException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.huacai.common.constant.HttpStatus; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.exception.DemoModeException; +import com.huacai.common.exception.ServiceException; +import com.huacai.common.utils.StringUtils; + +/** + * 全局异常处理器 + * + * @author huacai + */ +// 标记为全局控制器增强,用于统一处理控制器层的异常 +@RestControllerAdvice +public class GlobalExceptionHandler +{ + // 日志记录器,用于记录异常信息 + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 演示环境异常 + */ + // 声明处理DemoEnvironmentException类型异常的方法 + @ExceptionHandler(DemoEnvironmentException.class) + public AjaxResult handleDemoEnvironmentException(DemoEnvironmentException e) { + // 记录异常信息到日志 + log.error(e.getMessage(), e); + // 返回包含错误信息的AjaxResult + return AjaxResult.error(e.getMessage()); + } + + /** + * 权限校验异常 + */ + // 声明处理AccessDeniedException类型异常的方法,同时接收HttpServletRequest参数 + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) + { + // 获取请求的URI + String requestURI = request.getRequestURI(); + // 记录权限校验失败的日志,包含请求地址和异常信息 + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + // 返回403状态码和权限不足提示信息的AjaxResult + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + // 声明处理HttpRequestMethodNotSupportedException类型异常的方法 + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + // 获取请求的URI + String requestURI = request.getRequestURI(); + // 记录不支持请求方式的日志,包含请求地址和不支持的方法 + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + // 返回包含异常信息的AjaxResult + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + // 声明处理ServiceException类型异常的方法 + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + // 记录业务异常信息到日志 + log.error(e.getMessage(), e); + // 获取业务异常中的错误代码 + Integer code = e.getCode(); + // 根据错误代码是否存在,返回包含对应信息的AjaxResult + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 请求路径中缺少必需的路径变量 + */ + // 声明处理MissingPathVariableException类型异常的方法 + @ExceptionHandler(MissingPathVariableException.class) + public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) + { + // 获取请求的URI + String requestURI = request.getRequestURI(); + // 记录缺少路径变量的异常日志 + log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e); + // 返回包含具体缺少的路径变量信息的AjaxResult + return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName())); + } + + /** + * 请求参数类型不匹配 + */ + // 声明处理MethodArgumentTypeMismatchException类型异常的方法 + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) + { + // 获取请求的URI + String requestURI = request.getRequestURI(); + // 记录参数类型不匹配的异常日志 + log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e); + // 返回包含参数名、要求类型和实际值的错误信息的AjaxResult + return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue())); + } + + /** + * 拦截未知的运行时异常 + */ + // 声明处理RuntimeException类型异常的方法,作为运行时异常的兜底处理 + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + // 获取请求的URI + String requestURI = request.getRequestURI(); + // 记录未知运行时异常的日志 + log.error("请求地址'{}',发生未知异常.", requestURI, e); + // 返回包含异常信息的AjaxResult + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + // 声明处理Exception类型异常的方法,作为所有未被特定处理的异常的兜底 + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + // 获取请求的URI + String requestURI = request.getRequestURI(); + // 记录系统异常的日志 + log.error("请求地址'{}',发生系统异常.", requestURI, e); + // 返回包含异常信息的AjaxResult + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + // 声明处理BindException类型异常的方法(表单绑定验证失败) + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + // 记录验证异常信息到日志 + log.error(e.getMessage(), e); + // 获取第一个验证错误的默认信息 + String message = e.getAllErrors().get(0).getDefaultMessage(); + // 返回包含验证错误信息的AjaxResult + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + // 声明处理MethodArgumentNotValidException类型异常的方法(请求体参数验证失败) + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + // 记录参数验证异常信息到日志 + log.error(e.getMessage(), e); + // 获取字段验证错误的默认信息 + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + // 返回包含验证错误信息的AjaxResult + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + // 声明处理DemoModeException类型异常的方法 + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + // 返回演示模式不允许操作的提示信息 + return AjaxResult.error("演示模式,不允许操作"); + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/service/PermissionService.java b/huacai-framework/src/main/java/com/huacai/framework/web/service/PermissionService.java new file mode 100644 index 0000000..35cf932 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/service/PermissionService.java @@ -0,0 +1,182 @@ +package com.huacai.framework.web.service; + +import java.util.Set; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import com.huacai.common.constant.Constants; +import com.huacai.common.core.domain.entity.SysRole; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.utils.SecurityUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.framework.security.context.PermissionContextHolder; + +/** + * huacai首创 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author huacai +*/ +@Service("ss") +public class PermissionService +{ + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限(true-具备,false-不具备) + */ + public boolean hasPermi(String permission) + { + // 权限字符串为空时,直接返回false + if (StringUtils.isEmpty(permission)) + { + return false; + } + // 获取当前登录用户信息 + LoginUser loginUser = SecurityUtils.getLoginUser(); + // 若用户信息不存在或用户权限集合为空,返回false + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + // 将当前验证的权限存入权限上下文 + PermissionContextHolder.setContext(permission); + // 检查用户权限集合中是否包含该权限 + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限(true-不具备,false-具备) + */ + public boolean lacksPermi(String permission) + { + return !hasPermi(permission); + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以权限分隔符(PERMISSION_DELIMETER)分隔的权限列表字符串 + * @return 用户是否具有任意一个权限(true-是,false-否) + */ + public boolean hasAnyPermi(String permissions) + { + // 权限列表为空时,返回false + if (StringUtils.isEmpty(permissions)) + { + return false; + } + // 获取当前登录用户信息 + LoginUser loginUser = SecurityUtils.getLoginUser(); + // 若用户信息不存在或用户权限集合为空,返回false + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + // 将当前验证的权限列表存入权限上下文 + PermissionContextHolder.setContext(permissions); + // 获取用户的权限集合 + Set authorities = loginUser.getPermissions(); + // 拆分权限列表字符串,逐个检查是否拥有该权限 + for (String permission : permissions.split(Constants.PERMISSION_DELIMETER)) + { + if (permission != null && hasPermissions(authorities, permission)) + { + return true; + } + } + // 所有权限都不具备时返回false + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串(角色标识) + * @return 用户是否具备该角色(true-是,false-否) + */ + public boolean hasRole(String role) + { + // 角色字符串为空时,返回false + if (StringUtils.isEmpty(role)) + { + return false; + } + // 获取当前登录用户信息 + LoginUser loginUser = SecurityUtils.getLoginUser(); + // 若用户信息不存在或用户角色集合为空,返回false + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + // 遍历用户的角色列表,检查是否包含目标角色或超级管理员角色 + for (SysRole sysRole : loginUser.getUser().getRoles()) + { + String roleKey = sysRole.getRoleKey(); + if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) + { + return true; + } + } + // 不包含目标角色时返回false + return false; + } + + /** + * 验证用户是否不具备某角色,与hasRole逻辑相反 + * + * @param role 角色名称 + * @return 用户是否不具备该角色(true-是,false-否) + */ + public boolean lacksRole(String role) + { + return !hasRole(role); + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以角色分隔符(ROLE_DELIMETER)分隔的角色列表字符串 + * @return 用户是否具有任意一个角色(true-是,false-否) + */ + public boolean hasAnyRoles(String roles) + { + // 角色列表为空时,返回false + if (StringUtils.isEmpty(roles)) + { + return false; + } + // 获取当前登录用户信息 + LoginUser loginUser = SecurityUtils.getLoginUser(); + // 若用户信息不存在或用户角色集合为空,返回false + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + // 拆分角色列表字符串,逐个检查是否拥有该角色 + for (String role : roles.split(Constants.ROLE_DELIMETER)) + { + if (hasRole(role)) + { + return true; + } + } + // 所有角色都不具备时返回false + return false; + } + + /** + * 判断权限集合中是否包含目标权限 + * + * @param permissions 用户拥有的权限集合 + * @param permission 目标权限字符串 + * @return 是否包含目标权限(true-包含,false-不包含) + */ + private boolean hasPermissions(Set permissions, String permission) + { + // 若包含"全部权限"标识,或直接包含目标权限,则返回true + return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/service/SysLoginService.java b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysLoginService.java new file mode 100644 index 0000000..b9fb5f2 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysLoginService.java @@ -0,0 +1,181 @@ +package com.huacai.framework.web.service; + +import javax.annotation.Resource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.huacai.common.constant.CacheConstants; +import com.huacai.common.constant.Constants; +import com.huacai.common.constant.UserConstants; +import com.huacai.common.core.domain.entity.SysUser; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.core.redis.RedisCache; +import com.huacai.common.exception.ServiceException; +import com.huacai.common.exception.user.BlackListException; +import com.huacai.common.exception.user.CaptchaException; +import com.huacai.common.exception.user.CaptchaExpireException; +import com.huacai.common.exception.user.UserNotExistsException; +import com.huacai.common.exception.user.UserPasswordNotMatchException; +import com.huacai.common.utils.DateUtils; +import com.huacai.common.utils.MessageUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.ip.IpUtils; +import com.huacai.framework.manager.AsyncManager; +import com.huacai.framework.manager.factory.AsyncFactory; +import com.huacai.framework.security.context.AuthenticationContextHolder; +import com.huacai.system.service.ISysConfigService; +import com.huacai.system.service.ISysUserService; + +/** + * 登录校验方法 + * + * @author huacai + */ +@Component +public class SysLoginService +{ + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid) + { + // 验证码校验 + validateCaptcha(username, code, uuid); + // 登录前置校验 + loginPreCheck(username, password); + // 用户验证 + Authentication authentication = null; + try + { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } + catch (Exception e) + { + if (e instanceof BadCredentialsException) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + finally + { + AuthenticationContextHolder.clearContext(); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + } + + /** + * 登录前置校验 + * @param username 用户名 + * @param password 用户密码 + */ + public void loginPreCheck(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); + throw new UserNotExistsException(); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // IP黑名单校验 + String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); + throw new BlackListException(); + } + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + sysUser.setLoginIp(IpUtils.getIpAddr()); + sysUser.setLoginDate(DateUtils.getNowDate()); + userService.updateUserProfile(sysUser); + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/service/SysPasswordService.java b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysPasswordService.java new file mode 100644 index 0000000..04071eb --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysPasswordService.java @@ -0,0 +1,118 @@ +package com.huacai.framework.web.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.huacai.common.constant.CacheConstants; +import com.huacai.common.core.domain.entity.SysUser; +import com.huacai.common.core.redis.RedisCache; +import com.huacai.common.exception.user.UserPasswordNotMatchException; +import com.huacai.common.exception.user.UserPasswordRetryLimitExceedException; +import com.huacai.common.utils.SecurityUtils; +import com.huacai.framework.security.context.AuthenticationContextHolder; + +/** + * 登录密码服务类 + * 处理用户登录密码的验证、错误次数限制及缓存管理 + * + * @author huacai + */ +@Component +public class SysPasswordService +{ + // 注入Redis缓存工具,用于存储密码错误次数 + @Autowired + private RedisCache redisCache; + + // 从配置文件读取密码最大重试次数 + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + // 从配置文件读取密码错误锁定时间(分钟) + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 生成登录账户密码错误次数的缓存键名 + * + * @param username 用户名 + * @return 缓存键key(格式:PWD_ERR_CNT_KEY + 用户名) + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + /** + * 验证用户密码 + * 检查密码错误次数,超过限制则锁定账户,密码不匹配则累加错误次数 + * + * @param user 系统用户实体(包含数据库中的密码信息) + */ + public void validate(SysUser user) + { + // 从安全上下文获取当前认证信息(包含用户名和提交的密码) + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + // 从Redis获取该用户的密码错误次数 + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + // 若缓存中无记录,初始化错误次数为0 + if (retryCount == null) + { + retryCount = 0; + } + + // 若错误次数超过最大限制,抛出密码重试次数超限异常 + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + // 验证提交的密码与用户存储的密码是否匹配 + if (!matches(user, password)) + { + // 密码不匹配,错误次数加1,并更新Redis缓存(设置锁定时间) + retryCount = retryCount + 1; + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + // 抛出密码不匹配异常 + throw new UserPasswordNotMatchException(); + } + else + { + // 密码匹配,清除该用户的登录错误记录缓存 + clearLoginRecordCache(username); + } + } + + /** + * 验证原始密码与加密密码是否匹配 + * + * @param user 系统用户实体(包含加密后的密码) + * @param rawPassword 原始密码(用户输入的明文密码) + * @return true-密码匹配;false-密码不匹配 + */ + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + /** + * 清除用户的登录错误记录缓存 + * 通常在密码验证成功后调用,重置错误次数 + * + * @param loginName 登录用户名 + */ + public void clearLoginRecordCache(String loginName) + { + // 若缓存中存在该用户的错误记录,则删除 + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/service/SysPermissionService.java b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysPermissionService.java new file mode 100644 index 0000000..92ab8a2 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysPermissionService.java @@ -0,0 +1,97 @@ +package com.huacai.framework.web.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import com.huacai.common.core.domain.entity.SysRole; +import com.huacai.common.core.domain.entity.SysUser; +import com.huacai.system.service.ISysMenuService; +import com.huacai.system.service.ISysRoleService; + +/** + * 用户权限处理服务 + * 用于获取用户的角色权限和菜单权限,根据用户身份(管理员/普通用户)返回对应的权限集合 + * + * @author huacai + */ +@Component +public class SysPermissionService +{ + // 注入角色服务,用于查询角色相关的权限信息 + @Autowired + private ISysRoleService roleService; + + // 注入菜单服务,用于查询菜单相关的权限信息 + @Autowired + private ISysMenuService menuService; + + /** + * 获取用户的角色权限集合 + * + * @param user 用户信息对象 + * @return 角色权限标识集合(如"admin"、"user"等角色标识) + */ + public Set getRolePermission(SysUser user) + { + // 用于存储角色权限的集合(去重) + Set roles = new HashSet(); + // 若用户是管理员(通常判断用户ID是否为超级管理员ID) + if (user.isAdmin()) + { + // 管理员拥有"admin"角色权限 + roles.add("admin"); + } + else + { + // 普通用户:查询该用户ID对应的角色权限,并添加到集合中 + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取用户的菜单权限集合 + * + * @param user 用户信息对象 + * @return 菜单权限标识集合(如"system:user:list"、"*:*:*"等权限字符串) + */ + public Set getMenuPermission(SysUser user) + { + // 用于存储菜单权限的集合(去重) + Set perms = new HashSet(); + // 若用户是管理员 + if (user.isAdmin()) + { + // 管理员拥有所有菜单权限(用"*:*:*"表示) + perms.add("*:*:*"); + } + else + { + // 获取用户关联的角色列表 + List roles = user.getRoles(); + // 若用户拥有角色 + if (!CollectionUtils.isEmpty(roles)) + { + // 遍历每个角色,查询该角色对应的菜单权限 + for (SysRole role : roles) + { + // 查询角色ID对应的菜单权限集合 + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + // 为角色对象设置权限集合(用于数据权限匹配) + role.setPermissions(rolePerms); + // 将角色的菜单权限添加到用户的总权限集合中 + perms.addAll(rolePerms); + } + } + else + { + // 若用户没有关联角色,直接查询该用户ID对应的菜单权限 + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/service/SysRegisterService.java b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysRegisterService.java new file mode 100644 index 0000000..91403ca --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/service/SysRegisterService.java @@ -0,0 +1,144 @@ +package com.huacai.framework.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.huacai.common.constant.CacheConstants; +import com.huacai.common.constant.Constants; +import com.huacai.common.constant.UserConstants; +import com.huacai.common.core.domain.entity.SysUser; +import com.huacai.common.core.domain.model.RegisterBody; +import com.huacai.common.core.redis.RedisCache; +import com.huacai.common.exception.user.CaptchaException; +import com.huacai.common.exception.user.CaptchaExpireException; +import com.huacai.common.utils.MessageUtils; +import com.huacai.common.utils.SecurityUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.framework.manager.AsyncManager; +import com.huacai.framework.manager.factory.AsyncFactory; +import com.huacai.system.service.ISysConfigService; +import com.huacai.system.service.ISysUserService; +/** + * 注册校验服务类 + * 处理用户注册流程中的参数校验、验证码验证及用户信息注册逻辑 + * + * @author huacai + */ +@Component +public class SysRegisterService +{ + // 注入用户服务,用于用户信息的校验和注册操作 + @Autowired + private ISysUserService userService; + + // 注入系统配置服务,用于获取验证码开关等配置 + @Autowired + private ISysConfigService configService; + + // 注入Redis缓存工具,用于验证码的获取和删除 + @Autowired + private RedisCache redisCache; + + /** + * 处理用户注册逻辑 + * 包括参数校验、验证码验证、用户信息保存等操作 + * + * @param registerBody 注册信息对象(包含用户名、密码、验证码等) + * @return 注册结果消息(空字符串表示注册成功,否则为错误信息) + */ + public String register(RegisterBody registerBody) + { + // 初始化消息变量、用户名和密码 + String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); + // 创建系统用户对象并设置用户名 + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + + // 检查验证码开关是否开启 + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + // 验证验证码有效性 + validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); + } + + // 校验用户名是否为空 + if (StringUtils.isEmpty(username)) + { + msg = "用户名不能为空"; + } + // 校验密码是否为空 + else if (StringUtils.isEmpty(password)) + { + msg = "用户密码不能为空"; + } + // 校验用户名长度是否符合要求(2-20个字符) + else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + msg = "账户长度必须在2到20个字符之间"; + } + // 校验密码长度是否符合要求(5-20个字符) + else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + msg = "密码长度必须在5到20个字符之间"; + } + // 校验用户名是否已存在 + else if (!userService.checkUserNameUnique(sysUser)) + { + msg = "保存用户'" + username + "'失败,注册账号已存在"; + } + // 所有校验通过,执行注册逻辑 + else + { + // 设置用户昵称(默认与用户名相同) + sysUser.setNickName(username); + // 加密密码(使用安全工具类进行加密) + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + // 调用用户服务注册用户 + boolean regFlag = userService.registerUser(sysUser); + if (!regFlag) + { + // 注册失败 + msg = "注册失败,请联系系统管理人员"; + } + else + { + // 注册成功,异步记录注册日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"))); + } + } + // 返回注册结果消息 + return msg; + } + + /** + * 校验验证码的有效性 + * + * @param username 用户名(用于日志记录等场景) + * @param code 用户输入的验证码 + * @param uuid 验证码对应的唯一标识(用于从Redis获取正确的验证码) + * @throws CaptchaExpireException 验证码过期异常 + * @throws CaptchaException 验证码错误异常 + */ + public void validateCaptcha(String username, String code, String uuid) + { + // 构建Redis中存储验证码的键(前缀 + uuid) + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + // 从Redis获取验证码 + String captcha = redisCache.getCacheObject(verifyKey); + // 无论验证成功与否,都删除Redis中的验证码(防止重复使用) + redisCache.deleteObject(verifyKey); + + // 验证码不存在(已过期或未生成) + if (captcha == null) + { + throw new CaptchaExpireException(); + } + // 验证码不匹配(忽略大小写) + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException(); + } + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/service/TokenService.java b/huacai-framework/src/main/java/com/huacai/framework/web/service/TokenService.java new file mode 100644 index 0000000..5db5d5d --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/service/TokenService.java @@ -0,0 +1,280 @@ +package com.huacai.framework.web.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.huacai.common.constant.CacheConstants; +import com.huacai.common.constant.Constants; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.core.redis.RedisCache; +import com.huacai.common.utils.ServletUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.ip.AddressUtils; +import com.huacai.common.utils.ip.IpUtils; +import com.huacai.common.utils.uuid.IdUtils; +import eu.bitwalker.useragentutils.UserAgent; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * Token验证处理服务 + * 负责JWT令牌的创建、解析、验证,以及用户登录信息的缓存管理 + * + * @author huacai + */ +@Component +public class TokenService +{ + // 日志记录器 + private static final Logger log = LoggerFactory.getLogger(TokenService.class); + + // 从配置文件获取令牌在请求头中的自定义标识(如"Authorization") + @Value("${token.header}") + private String header; + + // 从配置文件获取令牌签名秘钥(用于JWT的生成和验证) + @Value("${token.secret}") + private String secret; + + // 从配置文件获取令牌有效期(分钟,默认30分钟) + @Value("${token.expireTime}") + private int expireTime; + + // 时间常量:1秒(毫秒) + protected static final long MILLIS_SECOND = 1000; + + // 时间常量:1分钟(毫秒) + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + // 时间常量:20分钟(毫秒),用于令牌自动刷新判断 + private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; + + // 注入Redis缓存工具,用于存储登录用户信息 + @Autowired + private RedisCache redisCache; + + /** + * 获取当前登录用户信息 + * 从请求中提取令牌,解析后从Redis获取对应的用户信息 + * + * @param request HTTP请求对象 + * @return 登录用户信息(LoginUser),若令牌无效则返回null + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 从请求中获取令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) + { + try + { + // 解析令牌获取声明信息 + Claims claims = parseToken(token); + // 从声明中获取登录用户唯一标识(uuid) + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + // 生成Redis中存储用户信息的键 + String userKey = getTokenKey(uuid); + // 从Redis获取登录用户信息 + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + log.error("获取用户信息异常'{}'", e.getMessage()); + } + } + return null; + } + + /** + * 设置用户身份信息到缓存 + * 通常在用户登录成功后调用,刷新令牌有效期 + * + * @param loginUser 登录用户信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 从缓存中删除用户身份信息 + * 用于用户退出登录或令牌失效时清除缓存 + * + * @param token 用户令牌 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * 生成JWT令牌并将用户信息存入Redis + * + * @param loginUser 登录用户信息 + * @return 生成的JWT令牌字符串 + */ + public String createToken(LoginUser loginUser) + { + // 生成UUID作为令牌唯一标识 + String token = IdUtils.fastUUID(); + // 设置令牌到登录用户信息中 + loginUser.setToken(token); + // 设置用户代理信息(浏览器、操作系统、IP等) + setUserAgent(loginUser); + // 刷新令牌有效期并缓存用户信息 + refreshToken(loginUser); + + // 构建JWT的声明信息 + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + // 生成并返回JWT令牌 + return createToken(claims); + } + + /** + * 验证令牌有效期 + * 当令牌剩余有效期不足20分钟时,自动刷新缓存延长有效期 + * + * @param loginUser 登录用户信息 + */ + public void verifyToken(LoginUser loginUser) + { + // 获取令牌过期时间 + long expireTime = loginUser.getExpireTime(); + // 获取当前时间 + long currentTime = System.currentTimeMillis(); + // 若剩余时间小于等于20分钟,则刷新令牌 + if (expireTime - currentTime <= MILLIS_MINUTE_TEN) + { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * 更新用户登录时间和过期时间,并重新缓存用户信息 + * + * @param loginUser 登录用户信息 + */ + public void refreshToken(LoginUser loginUser) + { + // 设置当前登录时间 + loginUser.setLoginTime(System.currentTimeMillis()); + // 计算过期时间(当前时间 + 配置的有效期) + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 生成Redis缓存键 + String userKey = getTokenKey(loginUser.getToken()); + // 将用户信息存入Redis,并设置过期时间 + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * 解析请求头中的User-Agent,获取浏览器、操作系统、IP地址及地理位置 + * + * @param loginUser 登录用户信息 + */ + public void setUserAgent(LoginUser loginUser) + { + // 解析User-Agent字符串 + UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + // 获取客户端IP地址 + String ip = IpUtils.getIpAddr(); + // 设置IP地址 + loginUser.setIpaddr(ip); + // 设置登录地理位置(根据IP解析) + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + // 设置浏览器名称 + loginUser.setBrowser(userAgent.getBrowser().getName()); + // 设置操作系统名称 + loginUser.setOs(userAgent.getOperatingSystem().getName()); + } + + /** + * 根据数据声明生成JWT令牌 + * + * @param claims 数据声明(包含用户标识等信息) + * @return 生成的JWT令牌字符串 + */ + private String createToken(Map claims) + { + // 使用HS512算法签名,生成JWT令牌 + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从JWT令牌中解析数据声明 + * + * @param token JWT令牌 + * @return 解析后的Claims对象 + */ + private Claims parseToken(String token) + { + // 使用秘钥解析令牌,获取声明信息 + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token JWT令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) + { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 从请求中获取令牌 + * 提取请求头中指定标识的令牌,并去除前缀(如"Bearer ") + * + * @param request HTTP请求对象 + * @return 处理后的令牌字符串,若不存在则返回null + */ + private String getToken(HttpServletRequest request) + { + // 从请求头获取令牌 + String token = request.getHeader(header); + // 若令牌存在且以指定前缀开头,则去除前缀 + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) + { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + /** + * 生成Redis中存储用户信息的键 + * + * @param uuid 令牌唯一标识 + * @return 缓存键(格式:LOGIN_TOKEN_KEY + uuid) + */ + private String getTokenKey(String uuid) + { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } +} \ No newline at end of file diff --git a/huacai-framework/src/main/java/com/huacai/framework/web/service/UserDetailsServiceImpl.java b/huacai-framework/src/main/java/com/huacai/framework/web/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..84ecea6 --- /dev/null +++ b/huacai-framework/src/main/java/com/huacai/framework/web/service/UserDetailsServiceImpl.java @@ -0,0 +1,94 @@ +package com.huacai.framework.web.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import com.huacai.common.core.domain.entity.SysUser; +import com.huacai.common.core.domain.model.LoginUser; +import com.huacai.common.enums.UserStatus; +import com.huacai.common.exception.ServiceException; +import com.huacai.common.utils.MessageUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.system.service.ISysUserService; + +/** + * 用户详情服务实现类 + * 实现Spring Security的UserDetailsService接口,用于加载用户信息进行身份验证 + * + * @author huacai + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService +{ + // 日志记录器 + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + // 注入用户服务,用于查询用户信息 + @Autowired + private ISysUserService userService; + + // 注入密码服务,用于密码验证 + @Autowired + private SysPasswordService passwordService; + + // 注入权限服务,用于获取用户的菜单权限 + @Autowired + private SysPermissionService permissionService; + + /** + * 根据用户名加载用户详情 + * 实现Spring Security的接口方法,用于登录时加载用户信息并验证状态 + * + * @param username 用户名 + * @return 用户详情对象(UserDetails) + * @throws UsernameNotFoundException 用户名不存在时抛出 + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + { + // 根据用户名查询系统用户信息 + SysUser user = userService.selectUserByUserName(username); + + // 验证用户是否存在 + if (StringUtils.isNull(user)) + { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } + // 验证用户是否已被删除 + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } + // 验证用户是否已被停用 + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + + // 验证用户密码(检查错误次数等) + passwordService.validate(user); + + // 创建并返回登录用户详情对象 + return createLoginUser(user); + } + + /** + * 创建登录用户详情对象 + * 将系统用户信息封装为Spring Security所需的LoginUser对象,并加载菜单权限 + * + * @param user 系统用户实体 + * @return 登录用户详情对象(LoginUser) + */ + public UserDetails createLoginUser(SysUser user) + { + // 构建LoginUser对象,包含用户ID、部门ID、用户信息及菜单权限集合 + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/huacai-framework/target/classes/com/huacai/framework/aspectj/DataScopeAspect.class b/huacai-framework/target/classes/com/huacai/framework/aspectj/DataScopeAspect.class new file mode 100644 index 0000000..c5a6523 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/aspectj/DataScopeAspect.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/aspectj/DataSourceAspect.class b/huacai-framework/target/classes/com/huacai/framework/aspectj/DataSourceAspect.class new file mode 100644 index 0000000..faf04be Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/aspectj/DataSourceAspect.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/aspectj/LogAspect.class b/huacai-framework/target/classes/com/huacai/framework/aspectj/LogAspect.class new file mode 100644 index 0000000..a0f39ee Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/aspectj/LogAspect.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/aspectj/RateLimiterAspect.class b/huacai-framework/target/classes/com/huacai/framework/aspectj/RateLimiterAspect.class new file mode 100644 index 0000000..166c7ae Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/aspectj/RateLimiterAspect.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/ApplicationConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/ApplicationConfig.class new file mode 100644 index 0000000..9181fba Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/ApplicationConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/CaptchaConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/CaptchaConfig.class new file mode 100644 index 0000000..2dbdef0 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/CaptchaConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/DruidConfig$1.class b/huacai-framework/target/classes/com/huacai/framework/config/DruidConfig$1.class new file mode 100644 index 0000000..4e0024a Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/DruidConfig$1.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/DruidConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/DruidConfig.class new file mode 100644 index 0000000..6989fed Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/DruidConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/FastJson2JsonRedisSerializer.class b/huacai-framework/target/classes/com/huacai/framework/config/FastJson2JsonRedisSerializer.class new file mode 100644 index 0000000..d336f1a Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/FastJson2JsonRedisSerializer.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/FilterConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/FilterConfig.class new file mode 100644 index 0000000..6b9cb4b Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/FilterConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/KaptchaTextCreator.class b/huacai-framework/target/classes/com/huacai/framework/config/KaptchaTextCreator.class new file mode 100644 index 0000000..9566d0f Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/KaptchaTextCreator.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/MyBatisConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/MyBatisConfig.class new file mode 100644 index 0000000..27537b2 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/MyBatisConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/RedisConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/RedisConfig.class new file mode 100644 index 0000000..56c8fe4 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/RedisConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/ResourcesConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/ResourcesConfig.class new file mode 100644 index 0000000..8a21dc9 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/ResourcesConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/SecurityConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/SecurityConfig.class new file mode 100644 index 0000000..77466bb Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/SecurityConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/ServerConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/ServerConfig.class new file mode 100644 index 0000000..b5e9ec7 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/ServerConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/ThreadPoolConfig$1.class b/huacai-framework/target/classes/com/huacai/framework/config/ThreadPoolConfig$1.class new file mode 100644 index 0000000..ff04322 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/ThreadPoolConfig$1.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/ThreadPoolConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/ThreadPoolConfig.class new file mode 100644 index 0000000..d9f09b9 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/ThreadPoolConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/WebMvcConfig.class b/huacai-framework/target/classes/com/huacai/framework/config/WebMvcConfig.class new file mode 100644 index 0000000..306b8c7 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/WebMvcConfig.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/properties/DruidProperties.class b/huacai-framework/target/classes/com/huacai/framework/config/properties/DruidProperties.class new file mode 100644 index 0000000..7df8e0e Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/properties/DruidProperties.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/config/properties/PermitAllUrlProperties.class b/huacai-framework/target/classes/com/huacai/framework/config/properties/PermitAllUrlProperties.class new file mode 100644 index 0000000..acb2087 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/config/properties/PermitAllUrlProperties.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/datasource/DynamicDataSource.class b/huacai-framework/target/classes/com/huacai/framework/datasource/DynamicDataSource.class new file mode 100644 index 0000000..c94e922 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/datasource/DynamicDataSource.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/datasource/DynamicDataSourceContextHolder.class b/huacai-framework/target/classes/com/huacai/framework/datasource/DynamicDataSourceContextHolder.class new file mode 100644 index 0000000..9c839a5 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/datasource/DynamicDataSourceContextHolder.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/interceptor/DemoEnvironmentInterceptor.class b/huacai-framework/target/classes/com/huacai/framework/interceptor/DemoEnvironmentInterceptor.class new file mode 100644 index 0000000..80c445d Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/interceptor/DemoEnvironmentInterceptor.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/interceptor/RepeatSubmitInterceptor.class b/huacai-framework/target/classes/com/huacai/framework/interceptor/RepeatSubmitInterceptor.class new file mode 100644 index 0000000..ba9ed83 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/interceptor/RepeatSubmitInterceptor.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/interceptor/impl/SameUrlDataInterceptor.class b/huacai-framework/target/classes/com/huacai/framework/interceptor/impl/SameUrlDataInterceptor.class new file mode 100644 index 0000000..5fc9c11 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/interceptor/impl/SameUrlDataInterceptor.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/manager/AsyncManager.class b/huacai-framework/target/classes/com/huacai/framework/manager/AsyncManager.class new file mode 100644 index 0000000..0943da9 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/manager/AsyncManager.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/manager/ShutdownManager.class b/huacai-framework/target/classes/com/huacai/framework/manager/ShutdownManager.class new file mode 100644 index 0000000..1d1497c Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/manager/ShutdownManager.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory$1.class b/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory$1.class new file mode 100644 index 0000000..6823173 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory$1.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory$2.class b/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory$2.class new file mode 100644 index 0000000..85ce457 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory$2.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory.class b/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory.class new file mode 100644 index 0000000..5264b5f Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/manager/factory/AsyncFactory.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/security/context/AuthenticationContextHolder.class b/huacai-framework/target/classes/com/huacai/framework/security/context/AuthenticationContextHolder.class new file mode 100644 index 0000000..5de34c0 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/security/context/AuthenticationContextHolder.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/security/context/PermissionContextHolder.class b/huacai-framework/target/classes/com/huacai/framework/security/context/PermissionContextHolder.class new file mode 100644 index 0000000..a8536b1 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/security/context/PermissionContextHolder.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/security/filter/JwtAuthenticationTokenFilter.class b/huacai-framework/target/classes/com/huacai/framework/security/filter/JwtAuthenticationTokenFilter.class new file mode 100644 index 0000000..5189b82 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/security/filter/JwtAuthenticationTokenFilter.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/security/handle/AuthenticationEntryPointImpl.class b/huacai-framework/target/classes/com/huacai/framework/security/handle/AuthenticationEntryPointImpl.class new file mode 100644 index 0000000..4e7e3dd Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/security/handle/AuthenticationEntryPointImpl.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/security/handle/LogoutSuccessHandlerImpl.class b/huacai-framework/target/classes/com/huacai/framework/security/handle/LogoutSuccessHandlerImpl.class new file mode 100644 index 0000000..fbe505c Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/security/handle/LogoutSuccessHandlerImpl.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/domain/Server.class b/huacai-framework/target/classes/com/huacai/framework/web/domain/Server.class new file mode 100644 index 0000000..9ab2b20 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/domain/Server.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Cpu.class b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Cpu.class new file mode 100644 index 0000000..370c4ff Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Cpu.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Jvm.class b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Jvm.class new file mode 100644 index 0000000..a2ea6e2 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Jvm.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Mem.class b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Mem.class new file mode 100644 index 0000000..7cc52be Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Mem.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Sys.class b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Sys.class new file mode 100644 index 0000000..96c01a9 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/Sys.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/domain/server/SysFile.class b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/SysFile.class new file mode 100644 index 0000000..9e48172 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/domain/server/SysFile.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/exception/GlobalExceptionHandler.class b/huacai-framework/target/classes/com/huacai/framework/web/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000..905be26 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/exception/GlobalExceptionHandler.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/service/PermissionService.class b/huacai-framework/target/classes/com/huacai/framework/web/service/PermissionService.class new file mode 100644 index 0000000..80ac4ca Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/service/PermissionService.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/service/SysLoginService.class b/huacai-framework/target/classes/com/huacai/framework/web/service/SysLoginService.class new file mode 100644 index 0000000..766c1ec Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/service/SysLoginService.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/service/SysPasswordService.class b/huacai-framework/target/classes/com/huacai/framework/web/service/SysPasswordService.class new file mode 100644 index 0000000..0001cfd Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/service/SysPasswordService.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/service/SysPermissionService.class b/huacai-framework/target/classes/com/huacai/framework/web/service/SysPermissionService.class new file mode 100644 index 0000000..2df06bf Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/service/SysPermissionService.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/service/SysRegisterService.class b/huacai-framework/target/classes/com/huacai/framework/web/service/SysRegisterService.class new file mode 100644 index 0000000..dba698a Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/service/SysRegisterService.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/service/TokenService.class b/huacai-framework/target/classes/com/huacai/framework/web/service/TokenService.class new file mode 100644 index 0000000..1f8be43 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/service/TokenService.class differ diff --git a/huacai-framework/target/classes/com/huacai/framework/web/service/UserDetailsServiceImpl.class b/huacai-framework/target/classes/com/huacai/framework/web/service/UserDetailsServiceImpl.class new file mode 100644 index 0000000..06f1010 Binary files /dev/null and b/huacai-framework/target/classes/com/huacai/framework/web/service/UserDetailsServiceImpl.class differ diff --git a/huacai-generator/pom.xml b/huacai-generator/pom.xml new file mode 100644 index 0000000..d87a49a --- /dev/null +++ b/huacai-generator/pom.xml @@ -0,0 +1,40 @@ + + + + huacai + com.huacai + 3.8.7 + + 4.0.0 + + huacai-generator + + + generator代码生成 + + + + + + + org.apache.velocity + velocity-engine-core + + + + + commons-collections + commons-collections + + + + + com.huacai + huacai-common + + + + + diff --git a/huacai-generator/src/main/java/com/huacai/generator/config/GenConfig.java b/huacai-generator/src/main/java/com/huacai/generator/config/GenConfig.java new file mode 100644 index 0000000..cb63779 --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/config/GenConfig.java @@ -0,0 +1,116 @@ +package com.huacai.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 读取代码生成相关配置 + * 用于加载代码生成器所需的配置参数(如作者、包路径、表前缀等) + * + * @author huacai + */ +@Component +// 指定配置属性的前缀为"gen",用于绑定配置文件中以"gen."开头的属性 +@ConfigurationProperties(prefix = "gen") +// 指定配置文件的位置为类路径下的generator.yml +@PropertySource(value = { "classpath:generator.yml" }) +public class GenConfig +{ + /** 作者(代码生成时注释中的作者信息) */ + public static String author; + + /** 生成代码的包路径(基础包结构) */ + public static String packageName; + + /** 是否自动去除表前缀(生成类名时是否排除表前缀),默认false */ + public static boolean autoRemovePre; + + /** 表前缀(生成类名时需要排除的前缀,多个前缀可用逗号分隔) */ + public static String tablePrefix; + + /** + * 获取作者信息 + * + * @return 作者姓名 + */ + public static String getAuthor() + { + return author; + } + + /** + * 设置作者信息(从配置文件注入) + * + * @param author 作者姓名 + */ + @Value("${author}") + public void setAuthor(String author) + { + GenConfig.author = author; + } + + /** + * 获取生成代码的包路径 + * + * @return 包路径(如"com.huacai.modules.system") + */ + public static String getPackageName() + { + return packageName; + } + + /** + * 设置生成代码的包路径(从配置文件注入) + * + * @param packageName 包路径 + */ + @Value("${packageName}") + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + /** + * 获取是否自动去除表前缀的标识 + * + * @return true-自动去除;false-保留 + */ + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + /** + * 设置是否自动去除表前缀(从配置文件注入) + * + * @param autoRemovePre 自动去除标识 + */ + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + /** + * 获取表前缀 + * + * @return 表前缀字符串(如"sys_") + */ + public static String getTablePrefix() + { + return tablePrefix; + } + + /** + * 设置表前缀(从配置文件注入) + * + * @param tablePrefix 表前缀 + */ + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } +} \ No newline at end of file diff --git a/huacai-generator/src/main/java/com/huacai/generator/controller/GenController.java b/huacai-generator/src/main/java/com/huacai/generator/controller/GenController.java new file mode 100644 index 0000000..368246b --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/controller/GenController.java @@ -0,0 +1,298 @@ +package com.huacai.generator.controller; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.huacai.common.annotation.Log; +import com.huacai.common.core.controller.BaseController; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.core.page.TableDataInfo; +import com.huacai.common.core.text.Convert; +import com.huacai.common.enums.BusinessType; +import com.huacai.generator.domain.GenTable; +import com.huacai.generator.domain.GenTableColumn; +import com.huacai.generator.service.IGenTableColumnService; +import com.huacai.generator.service.IGenTableService; + +/** + * 代码生成控制器 + * 提供代码生成相关的CRUD操作、表结构导入、代码预览、生成及下载等功能 + * + * @author huacai + */ +@RestController +@RequestMapping("/tool/gen") +public class GenController extends BaseController +{ + // 注入代码生成表服务,处理表结构相关业务逻辑 + @Autowired + private IGenTableService genTableService; + + // 注入代码生成表字段服务,处理表字段相关业务逻辑 + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + * 分页查询已导入的表结构列表 + * + * @param genTable 代码生成表查询条件对象 + * @return 分页表格数据(包含表结构列表) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) + { + // 开启分页 + startPage(); + // 查询表结构列表 + List list = genTableService.selectGenTableList(genTable); + // 返回分页数据 + return getDataTable(list); + } + + /** + * 获取代码生成业务详情 + * 根据表ID查询表结构详情、字段列表及所有表列表(用于关联表选择) + * + * @param tableId 表ID + * @return 包含表信息、字段列表和所有表列表的响应对象 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:query')") + @GetMapping(value = "/{tableId}") + public AjaxResult getInfo(@PathVariable Long tableId) + { + // 查询表结构详情 + GenTable table = genTableService.selectGenTableById(tableId); + // 查询所有表结构(用于关联表选择) + List tables = genTableService.selectGenTableAll(); + // 查询该表的字段列表 + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + // 封装返回数据 + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return success(map); + } + + /** + * 查询数据库表列表 + * 分页查询数据库中可导入的表结构列表 + * + * @param genTable 代码生成表查询条件对象 + * @return 分页表格数据(包含数据库表列表) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) + { + // 开启分页 + startPage(); + // 查询数据库表列表 + List list = genTableService.selectDbTableList(genTable); + // 返回分页数据 + return getDataTable(list); + } + + /** + * 查询数据表字段列表 + * 根据表ID查询该表的所有字段信息 + * + * @param tableId 表ID + * @return 包含字段列表的表格数据 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping(value = "/column/{tableId}") + public TableDataInfo columnList(Long tableId) + { + TableDataInfo dataInfo = new TableDataInfo(); + // 查询表字段列表 + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + * 将数据库表结构导入到代码生成器中 + * + * @param tables 表名列表字符串(逗号分隔) + * @return 操作结果 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:import')") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) + { + // 将表名字符串转换为数组 + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息列表 + List tableList = genTableService.selectDbTableListByNames(tableNames); + // 导入表结构 + genTableService.importGenTable(tableList); + return success(); + } + + /** + * 修改保存代码生成业务 + * 更新代码生成表的配置信息 + * + * @param genTable 代码生成表对象(包含更新信息) + * @return 操作结果 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) + { + // 验证编辑参数 + genTableService.validateEdit(genTable); + // 更新表结构信息 + genTableService.updateGenTable(genTable); + return success(); + } + + /** + * 删除代码生成记录 + * 根据表ID批量删除已导入的表结构 + * + * @param tableIds 表ID数组 + * @return 操作结果 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:remove')") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) + { + genTableService.deleteGenTableByIds(tableIds); + return success(); + } + + /** + * 预览代码 + * 根据表ID生成并预览各类型代码(如实体类、Controller、Service等) + * + * @param tableId 表ID + * @return 包含各类型代码的响应对象 + * @throws IOException IO异常 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:preview')") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException + { + // 生成代码并返回(key为文件名,value为代码内容) + Map dataMap = genTableService.previewCode(tableId); + return success(dataMap); + } + + /** + * 生成代码(下载方式) + * 根据表名生成代码并打包为ZIP文件供下载 + * + * @param response HTTP响应对象 + * @param tableName 表名 + * @throws IOException IO异常 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException + { + // 生成代码字节数组(ZIP格式) + byte[] data = genTableService.downloadCode(tableName); + // 输出ZIP文件到响应流 + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + * 根据表名生成代码并输出到配置的自定义路径 + * + * @param tableName 表名 + * @return 操作结果 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) + { + genTableService.generatorCode(tableName); + return success(); + } + + /** + * 同步数据库 + * 将数据库表结构的最新变化同步到代码生成器中 + * + * @param tableName 表名 + * @return 操作结果 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) + { + genTableService.synchDb(tableName); + return success(); + } + + /** + * 批量生成代码 + * 根据表名列表批量生成代码并打包为ZIP文件供下载 + * + * @param response HTTP响应对象 + * @param tables 表名列表字符串(逗号分隔) + * @throws IOException IO异常 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException + { + // 将表名字符串转换为数组 + String[] tableNames = Convert.toStrArray(tables); + // 批量生成代码字节数组(ZIP格式) + byte[] data = genTableService.downloadCode(tableNames); + // 输出ZIP文件到响应流 + genCode(response, data); + } + + /** + * 生成ZIP文件并输出到响应流 + * 设置响应头信息,将代码字节数组以ZIP文件形式返回给客户端 + * + * @param response HTTP响应对象 + * @param data 代码字节数组(ZIP格式) + * @throws IOException IO异常 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException + { + response.reset(); + // 设置跨域相关响应头 + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + // 设置文件下载响应头 + response.setHeader("Content-Disposition", "attachment; filename=\"huacai.zip\""); + response.addHeader("Content-Length", "" + data.length); + // 设置内容类型为二进制流 + response.setContentType("application/octet-stream; charset=UTF-8"); + // 将字节数组写入响应输出流 + IOUtils.write(data, response.getOutputStream()); + } +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/domain/GenTable.java b/huacai-generator/src/main/java/com/huacai/generator/domain/GenTable.java new file mode 100644 index 0000000..e777c28 --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/domain/GenTable.java @@ -0,0 +1,437 @@ +package com.huacai.generator.domain; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import org.apache.commons.lang3.ArrayUtils; +import com.huacai.common.constant.GenConstants; +import com.huacai.common.core.domain.BaseEntity; +import com.huacai.common.utils.StringUtils; + +/** + * 业务表实体类(对应数据库表gen_table) + * 存储代码生成器所需的表结构配置信息,如表名、实体类名、生成路径等 + * + * @author huacai + */ +public class GenTable extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号(主键ID) */ + private Long tableId; + + /** 表名称 */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** 表描述 */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** 关联父表的表名(用于主子表生成) */ + private String subTableName; + + /** 本表关联父表的外键名(用于主子表生成) */ + private String subTableFkName; + + /** 实体类名称(首字母大写) */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** 使用的模板类型(crud单表操作、tree树表操作、sub主子表操作) */ + private String tplCategory; + + /** 前端类型(如element-ui模版、element-plus模版) */ + private String tplWebType; + + /** 生成包路径 */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** 生成模块名 */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** 生成业务名 */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** 生成功能名 */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** 生成作者 */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** 生成代码方式(0-zip压缩包、1-自定义路径) */ + private String genType; + + /** 生成路径(不填默认项目路径) */ + private String genPath; + + /** 主键信息(对应的表字段对象) */ + private GenTableColumn pkColumn; + + /** 子表信息(用于主子表生成时关联子表) */ + private GenTable subTable; + + /** 表列信息列表(该表所有字段的配置) */ + @Valid + private List columns; + + /** 其它生成选项(存储额外的生成配置) */ + private String options; + + /** 树编码字段(树表生成时使用) */ + private String treeCode; + + /** 树父编码字段(树表生成时使用) */ + private String treeParentCode; + + /** 树名称字段(树表生成时使用) */ + private String treeName; + + /** 上级菜单ID字段(用于关联菜单) */ + private String parentMenuId; + + /** 上级菜单名称字段(用于关联菜单) */ + private String parentMenuName; + + // 以下为getter和setter方法 + + public Long getTableId() + { + return tableId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public String getTableName() + { + return tableName; + } + + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + public String getTableComment() + { + return tableComment; + } + + public void setTableComment(String tableComment) + { + this.tableComment = tableComment; + } + + public String getSubTableName() + { + return subTableName; + } + + public void setSubTableName(String subTableName) + { + this.subTableName = subTableName; + } + + public String getSubTableFkName() + { + return subTableFkName; + } + + public void setSubTableFkName(String subTableFkName) + { + this.subTableFkName = subTableFkName; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getTplCategory() + { + return tplCategory; + } + + public void setTplCategory(String tplCategory) + { + this.tplCategory = tplCategory; + } + + public String getTplWebType() + { + return tplWebType; + } + + public void setTplWebType(String tplWebType) + { + this.tplWebType = tplWebType; + } + + public String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + public String getModuleName() + { + return moduleName; + } + + public void setModuleName(String moduleName) + { + this.moduleName = moduleName; + } + + public String getBusinessName() + { + return businessName; + } + + public void setBusinessName(String businessName) + { + this.businessName = businessName; + } + + public String getFunctionName() + { + return functionName; + } + + public void setFunctionName(String functionName) + { + this.functionName = functionName; + } + + public String getFunctionAuthor() + { + return functionAuthor; + } + + public void setFunctionAuthor(String functionAuthor) + { + this.functionAuthor = functionAuthor; + } + + public String getGenType() + { + return genType; + } + + public void setGenType(String genType) + { + this.genType = genType; + } + + public String getGenPath() + { + return genPath; + } + + public void setGenPath(String genPath) + { + this.genPath = genPath; + } + + public GenTableColumn getPkColumn() + { + return pkColumn; + } + + public void setPkColumn(GenTableColumn pkColumn) + { + this.pkColumn = pkColumn; + } + + public GenTable getSubTable() + { + return subTable; + } + + public void setSubTable(GenTable subTable) + { + this.subTable = subTable; + } + + public List getColumns() + { + return columns; + } + + public void setColumns(List columns) + { + this.columns = columns; + } + + public String getOptions() + { + return options; + } + + public void setOptions(String options) + { + this.options = options; + } + + public String getTreeCode() + { + return treeCode; + } + + public void setTreeCode(String treeCode) + { + this.treeCode = treeCode; + } + + public String getTreeParentCode() + { + return treeParentCode; + } + + public void setTreeParentCode(String treeParentCode) + { + this.treeParentCode = treeParentCode; + } + + public String getTreeName() + { + return treeName; + } + + public void setTreeName(String treeName) + { + this.treeName = treeName; + } + + public String getParentMenuId() + { + return parentMenuId; + } + + public void setParentMenuId(String parentMenuId) + { + this.parentMenuId = parentMenuId; + } + + public String getParentMenuName() + { + return parentMenuName; + } + + public void setParentMenuName(String parentMenuName) + { + this.parentMenuName = parentMenuName; + } + + /** + * 判断当前表是否为子表(主子表模式) + * + * @return true-是子表;false-不是 + */ + public boolean isSub() + { + return isSub(this.tplCategory); + } + + /** + * 静态方法:判断模板类型是否为子表(主子表模式) + * + * @param tplCategory 模板类型 + * @return true-是子表模板;false-不是 + */ + public static boolean isSub(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + + /** + * 判断当前表是否为树表 + * + * @return true-是树表;false-不是 + */ + public boolean isTree() + { + return isTree(this.tplCategory); + } + + /** + * 静态方法:判断模板类型是否为树表 + * + * @param tplCategory 模板类型 + * @return true-是树表模板;false-不是 + */ + public static boolean isTree(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + /** + * 判断当前表是否为普通CRUD表 + * + * @return true-是CRUD表;false-不是 + */ + public boolean isCrud() + { + return isCrud(this.tplCategory); + } + + /** + * 静态方法:判断模板类型是否为普通CRUD表 + * + * @param tplCategory 模板类型 + * @return true-是CRUD模板;false-不是 + */ + public static boolean isCrud(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + /** + * 判断指定的Java字段是否为超级字段(基类中已定义的字段) + * + * @param javaField Java字段名 + * @return true-是超级字段;false-不是 + */ + public boolean isSuperColumn(String javaField) + { + return isSuperColumn(this.tplCategory, javaField); + } + + /** + * 静态方法:判断指定的Java字段是否为超级字段(基类中已定义的字段) + * 树表包含树基类和普通基类的字段,普通表仅包含普通基类的字段 + * + * @param tplCategory 模板类型 + * @param javaField Java字段名 + * @return true-是超级字段;false-不是 + */ + public static boolean isSuperColumn(String tplCategory, String javaField) + { + if (isTree(tplCategory)) + { + // 树表的超级字段包括树基类和普通基类的字段 + return StringUtils.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + // 普通表的超级字段仅包括普通基类的字段 + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} \ No newline at end of file diff --git a/huacai-generator/src/main/java/com/huacai/generator/domain/GenTableColumn.java b/huacai-generator/src/main/java/com/huacai/generator/domain/GenTableColumn.java new file mode 100644 index 0000000..328d4d4 --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/domain/GenTableColumn.java @@ -0,0 +1,492 @@ +package com.huacai.generator.domain; + +import javax.validation.constraints.NotBlank; +import com.huacai.common.core.domain.BaseEntity; +import com.huacai.common.utils.StringUtils; + +/** + * 代码生成业务字段表实体类(对应数据库表gen_table_column) + * 存储代码生成器所需的表字段配置信息,如字段名、Java类型、是否为主键等 + * + * @author huacai + */ +public class GenTableColumn extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号(主键ID) */ + private Long columnId; + + /** 归属表编号(关联gen_table的tableId) */ + private Long tableId; + + /** 数据库列名称 */ + private String columnName; + + /** 数据库列描述 */ + private String columnComment; + + /** 数据库列类型(如varchar、int等) */ + private String columnType; + + /** JAVA数据类型(如String、Integer等) */ + private String javaType; + + /** JAVA字段名(实体类中的属性名) */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** 是否主键(1-是,0-否) */ + private String isPk; + + /** 是否自增(1-是,0-否) */ + private String isIncrement; + + /** 是否必填(1-是,0-否) */ + private String isRequired; + + /** 是否为插入字段(1-是,0-否) */ + private String isInsert; + + /** 是否为编辑字段(1-是,0-否) */ + private String isEdit; + + /** 是否为列表字段(1-是,0-否) */ + private String isList; + + /** 是否为查询字段(1-是,0-否) */ + private String isQuery; + + /** 查询方式(EQ-等于、NE-不等于、GT-大于、LT-小于、LIKE-模糊、BETWEEN-范围) */ + private String queryType; + + /** 前端显示类型(input-文本框、textarea-文本域、select-下拉框等) */ + private String htmlType; + + /** 字典类型(关联字典表的类型,用于下拉选择等场景) */ + private String dictType; + + /** 排序号(用于字段展示顺序) */ + private Integer sort; + + // 以下为getter和setter方法 + + public void setColumnId(Long columnId) + { + this.columnId = columnId; + } + + public Long getColumnId() + { + return columnId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public Long getTableId() + { + return tableId; + } + + public void setColumnName(String columnName) + { + this.columnName = columnName; + } + + public String getColumnName() + { + return columnName; + } + + public void setColumnComment(String columnComment) + { + this.columnComment = columnComment; + } + + public String getColumnComment() + { + return columnComment; + } + + public void setColumnType(String columnType) + { + this.columnType = columnType; + } + + public String getColumnType() + { + return columnType; + } + + public void setJavaType(String javaType) + { + this.javaType = javaType; + } + + public String getJavaType() + { + return javaType; + } + + public void setJavaField(String javaField) + { + this.javaField = javaField; + } + + public String getJavaField() + { + return javaField; + } + + /** + * 获取首字母大写的Java字段名(用于生成getter/setter等方法) + * + * @return 首字母大写的Java字段名 + */ + public String getCapJavaField() + { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) + { + this.isPk = isPk; + } + + public String getIsPk() + { + return isPk; + } + + /** + * 判断当前字段是否为主键 + * + * @return true-是主键;false-不是 + */ + public boolean isPk() + { + return isPk(this.isPk); + } + + /** + * 静态方法:判断字段是否为主键 + * + * @param isPk 主键标识(1-是,0-否) + * @return true-是主键;false-不是 + */ + public boolean isPk(String isPk) + { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() + { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) + { + this.isIncrement = isIncrement; + } + + /** + * 判断当前字段是否为自增字段 + * + * @return true-是自增;false-不是 + */ + public boolean isIncrement() + { + return isIncrement(this.isIncrement); + } + + /** + * 静态方法:判断字段是否为自增字段 + * + * @param isIncrement 自增标识(1-是,0-否) + * @return true-是自增;false-不是 + */ + public boolean isIncrement(String isIncrement) + { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) + { + this.isRequired = isRequired; + } + + public String getIsRequired() + { + return isRequired; + } + + /** + * 判断当前字段是否为必填字段 + * + * @return true-是必填;false-不是 + */ + public boolean isRequired() + { + return isRequired(this.isRequired); + } + + /** + * 静态方法:判断字段是否为必填字段 + * + * @param isRequired 必填标识(1-是,0-否) + * @return true-是必填;false-不是 + */ + public boolean isRequired(String isRequired) + { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) + { + this.isInsert = isInsert; + } + + public String getIsInsert() + { + return isInsert; + } + + /** + * 判断当前字段是否为插入字段(新增时需要提交的字段) + * + * @return true-是插入字段;false-不是 + */ + public boolean isInsert() + { + return isInsert(this.isInsert); + } + + /** + * 静态方法:判断字段是否为插入字段 + * + * @param isInsert 插入标识(1-是,0-否) + * @return true-是插入字段;false-不是 + */ + public boolean isInsert(String isInsert) + { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) + { + this.isEdit = isEdit; + } + + public String getIsEdit() + { + return isEdit; + } + + /** + * 判断当前字段是否为编辑字段(修改时需要提交的字段) + * + * @return true-是编辑字段;false-不是 + */ + public boolean isEdit() + { + return isInsert(this.isEdit); + } + + /** + * 静态方法:判断字段是否为编辑字段 + * + * @param isEdit 编辑标识(1-是,0-否) + * @return true-是编辑字段;false-不是 + */ + public boolean isEdit(String isEdit) + { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) + { + this.isList = isList; + } + + public String getIsList() + { + return isList; + } + + /** + * 判断当前字段是否为列表字段(列表页需要显示的字段) + * + * @return true-是列表字段;false-不是 + */ + public boolean isList() + { + return isList(this.isList); + } + + /** + * 静态方法:判断字段是否为列表字段 + * + * @param isList 列表标识(1-是,0-否) + * @return true-是列表字段;false-不是 + */ + public boolean isList(String isList) + { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) + { + this.isQuery = isQuery; + } + + public String getIsQuery() + { + return isQuery; + } + + /** + * 判断当前字段是否为查询字段(查询条件中需要显示的字段) + * + * @return true-是查询字段;false-不是 + */ + public boolean isQuery() + { + return isQuery(this.isQuery); + } + + /** + * 静态方法:判断字段是否为查询字段 + * + * @param isQuery 查询标识(1-是,0-否) + * @return true-是查询字段;false-不是 + */ + public boolean isQuery(String isQuery) + { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) + { + this.queryType = queryType; + } + + public String getQueryType() + { + return queryType; + } + + public String getHtmlType() + { + return htmlType; + } + + public void setHtmlType(String htmlType) + { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getDictType() + { + return dictType; + } + + public void setSort(Integer sort) + { + this.sort = sort; + } + + public Integer getSort() + { + return sort; + } + + /** + * 判断当前字段是否为超级字段(基类中已定义的字段,无需在生成的实体类中重复定义) + * + * @return true-是超级字段;false-不是 + */ + public boolean isSuperColumn() + { + return isSuperColumn(this.javaField); + } + + /** + * 静态方法:判断字段是否为超级字段 + * 超级字段包括BaseEntity和TreeEntity中已定义的公共字段 + * + * @param javaField Java字段名 + * @return true-是超级字段;false-不是 + */ + public static boolean isSuperColumn(String javaField) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + // BaseEntity中的公共字段 + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity中的树相关字段 + "parentName", "parentId", "orderNum", "ancestors"); + } + + /** + * 判断当前字段是否为可用字段(虽为超级字段,但生成页面时需要用到,不能忽略) + * + * @return true-是可用字段;false-不是 + */ + public boolean isUsableColumn() + { + return isUsableColumn(javaField); + } + + /** + * 静态方法:判断字段是否为可用字段 + * 可用字段是超级字段中的白名单,生成页面时需要包含这些字段 + * + * @param javaField Java字段名 + * @return true-是可用字段;false-不是 + */ + public static boolean isUsableColumn(String javaField) + { + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + /** + * 解析字段注释中的枚举说明,生成前端所需的转换表达式 + * 例如:注释为"状态(0-正常 1-禁用)",则生成"0=正常,1=禁用" + * + * @return 解析后的转换表达式 + */ + public String readConverterExp() + { + // 从字段注释中提取括号内的枚举说明(格式:(key-值 key-值...)) + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) + { + // 按空格拆分枚举项 + for (String value : remarks.split(" ")) + { + if (StringUtils.isNotEmpty(value)) + { + // 提取key和值(格式:key-值) + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + // 移除最后一个逗号 + return sb.deleteCharAt(sb.length() - 1).toString(); + } + else + { + // 无枚举说明时返回原始注释 + return this.columnComment; + } + } +} \ No newline at end of file diff --git a/huacai-generator/src/main/java/com/huacai/generator/mapper/GenTableColumnMapper.java b/huacai-generator/src/main/java/com/huacai/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..06e76a6 --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,60 @@ +package com.huacai.generator.mapper; + +import java.util.List; +import com.huacai.generator.domain.GenTableColumn; + +/** + * 业务字段 数据层 + * + * @author huacai + */ +public interface GenTableColumnMapper +{ + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + public int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(Long[] ids); +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/mapper/GenTableMapper.java b/huacai-generator/src/main/java/com/huacai/generator/mapper/GenTableMapper.java new file mode 100644 index 0000000..e3dbe6d --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/mapper/GenTableMapper.java @@ -0,0 +1,83 @@ +package com.huacai.generator.mapper; + +import java.util.List; +import com.huacai.generator.domain.GenTable; + +/** + * 业务 数据层 + * + * @author huacai + */ +public interface GenTableMapper +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableByIds(Long[] ids); +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/service/GenTableColumnServiceImpl.java b/huacai-generator/src/main/java/com/huacai/generator/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..add060d --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/service/GenTableColumnServiceImpl.java @@ -0,0 +1,75 @@ +package com.huacai.generator.service; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.huacai.common.core.text.Convert; +import com.huacai.generator.domain.GenTableColumn; +import com.huacai.generator.mapper.GenTableColumnMapper; + +/** + * 业务字段服务层实现类 + * 处理代码生成器中表字段的查询、新增、修改和删除等业务逻辑 + * + * @author huacai + */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService +{ + // 注入表字段Mapper接口,用于数据库操作 + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * 根据表ID查询该表对应的所有字段信息 + * + * @param tableId 表ID(归属表编号) + * @return 业务字段集合(表字段列表) + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) + { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * 向数据库中插入一条表字段配置信息 + * + * @param genTableColumn 业务字段信息对象 + * @return 操作结果(影响的行数) + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * 根据字段ID更新表字段的配置信息 + * + * @param genTableColumn 业务字段信息对象(包含更新后的数据) + * @return 操作结果(影响的行数) + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 批量删除业务字段 + * 根据ID字符串批量删除表字段配置信息 + * + * @param ids 需要删除的字段ID字符串(多个ID用逗号分隔) + * @return 操作结果(影响的行数) + */ + @Override + public int deleteGenTableColumnByIds(String ids) + { + // 将ID字符串转换为Long数组,调用Mapper进行批量删除 + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/service/GenTableServiceImpl.java b/huacai-generator/src/main/java/com/huacai/generator/service/GenTableServiceImpl.java new file mode 100644 index 0000000..ebe1dc6 --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/service/GenTableServiceImpl.java @@ -0,0 +1,576 @@ +package com.huacai.generator.service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.huacai.common.constant.Constants; +import com.huacai.common.constant.GenConstants; +import com.huacai.common.core.text.CharsetKit; +import com.huacai.common.exception.ServiceException; +import com.huacai.common.utils.SecurityUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.generator.domain.GenTable; +import com.huacai.generator.domain.GenTableColumn; +import com.huacai.generator.mapper.GenTableColumnMapper; +import com.huacai.generator.mapper.GenTableMapper; +import com.huacai.generator.util.GenUtils; +import com.huacai.generator.util.VelocityInitializer; +import com.huacai.generator.util.VelocityUtils; + +/** + * 代码生成业务服务层实现类 + * 处理代码生成过程中的表结构查询、导入、代码生成、预览、下载等核心业务逻辑 + * + * @author huacai + */ +@Service +public class GenTableServiceImpl implements IGenTableService +{ + // 日志记录器 + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + // 注入表结构Mapper接口,用于数据库操作 + @Autowired + private GenTableMapper genTableMapper; + + // 注入表字段Mapper接口,用于字段相关数据库操作 + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 根据ID查询业务表信息 + * + * @param id 业务表ID + * @return 业务表信息对象(GenTable) + */ + @Override + public GenTable selectGenTableById(Long id) + { + GenTable genTable = genTableMapper.selectGenTableById(id); + // 从选项中解析并设置表的额外配置(如树表参数、菜单关联等) + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务表列表 + * + * @param genTable 业务表查询条件对象 + * @return 业务表集合 + */ + @Override + public List selectGenTableList(GenTable genTable) + { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询数据库中的表列表(未导入到代码生成器的表) + * + * @param genTable 业务表查询条件对象 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) + { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 根据表名数组查询数据库中的表信息 + * + * @param tableNames 表名数组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) + { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有已导入的业务表信息 + * + * @return 业务表集合 + */ + @Override + public List selectGenTableAll() + { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务表信息 + * 包含表配置和字段配置的更新 + * + * @param genTable 业务表信息对象(包含更新后的数据) + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) + { + // 将额外参数转换为JSON字符串存储 + String options = JSON.toJSONString(genTable.getParams()); + genTable.setOptions(options); + // 更新表信息 + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) + { + // 批量更新字段信息 + for (GenTableColumn cenTableColumn : genTable.getColumns()) + { + genTableColumnMapper.updateGenTableColumn(cenTableColumn); + } + } + } + + /** + * 批量删除业务表 + * 同时删除关联的字段信息 + * + * @param tableIds 业务表ID数组 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) + { + // 删除表信息 + genTableMapper.deleteGenTableByIds(tableIds); + // 删除关联的字段信息 + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 导入表结构到代码生成器 + * 初始化表和字段信息并保存到数据库 + * + * @param tableList 待导入的表信息列表 + */ + @Override + @Transactional + public void importGenTable(List tableList) + { + String operName = SecurityUtils.getUsername(); + try + { + for (GenTable table : tableList) + { + String tableName = table.getTableName(); + // 初始化表信息(设置默认包路径、类名等) + GenUtils.initTable(table, operName); + // 插入表信息 + int row = genTableMapper.insertGenTable(table); + if (row > 0) + { + // 查询数据库中该表的字段信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + // 初始化并插入字段信息 + for (GenTableColumn column : genTableColumns) + { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } + catch (Exception e) + { + throw new ServiceException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览生成的代码 + * 渲染模板并返回各文件的代码内容 + * + * @param tableId 业务表ID + * @return 代码预览映射(key为模板路径,value为渲染后的代码) + */ + @Override + public Map previewCode(Long tableId) + { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息(如果是主子表模式) + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + // 初始化Velocity模板引擎 + VelocityInitializer.initVelocity(); + + // 准备模板上下文数据 + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表(根据表类型和前端类型选择) + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * 将生成的代码打包为ZIP文件返回 + * + * @param tableName 表名 + * @return ZIP文件的字节数组 + */ + @Override + public byte[] downloadCode(String tableName) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + // 生成代码并写入ZIP流 + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * 将生成的代码输出到指定路径 + * + * @param tableName 表名 + */ + @Override + public void generatorCode(String tableName) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + // 初始化Velocity模板引擎 + VelocityInitializer.initVelocity(); + + // 准备模板上下文数据 + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 过滤不需要生成到文件系统的模板(如SQL、前端特定文件) + if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + // 获取生成路径并写入文件 + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8); + } + catch (IOException e) + { + throw new ServiceException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库表结构 + * 将数据库中表结构的变更同步到代码生成器的配置中 + * + * @param tableName 表名 + */ + @Override + @Transactional + public void synchDb(String tableName) + { + // 查询当前表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + // 将现有字段按列名映射为Map,便于查询 + Map tableColumnMap = tableColumns.stream() + .collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity())); + + // 查询数据库中最新的表字段信息 + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (StringUtils.isEmpty(dbTableColumns)) + { + throw new ServiceException("同步数据失败,原表结构不存在"); + } + // 提取数据库字段名列表,用于判断字段是否被删除 + List dbTableColumnNames = dbTableColumns.stream() + .map(GenTableColumn::getColumnName) + .collect(Collectors.toList()); + + // 处理每个数据库字段:更新现有字段或新增字段 + dbTableColumns.forEach(column -> { + GenUtils.initColumnField(column, table); + if (tableColumnMap.containsKey(column.getColumnName())) + { + // 字段已存在,更新配置(保留部分原有配置) + GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName()); + column.setColumnId(prevColumn.getColumnId()); + if (column.isList()) + { + // 列表字段保留查询方式和字典类型 + column.setDictType(prevColumn.getDictType()); + column.setQueryType(prevColumn.getQueryType()); + } + if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk() + && (column.isInsert() || column.isEdit()) + && ((column.isUsableColumn()) || (!column.isSuperColumn()))) + { + // 保留必填和显示类型配置 + column.setIsRequired(prevColumn.getIsRequired()); + column.setHtmlType(prevColumn.getHtmlType()); + } + genTableColumnMapper.updateGenTableColumn(column); + } + else + { + // 字段不存在,新增字段配置 + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + // 删除数据库中已不存在的字段配置 + List delColumns = tableColumns.stream() + .filter(column -> !dbTableColumnNames.contains(column.getColumnName())) + .collect(Collectors.toList()); + if (StringUtils.isNotEmpty(delColumns)) + { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * 将多个表的代码打包为ZIP文件返回 + * + * @param tableNames 表名数组 + * @return ZIP文件的字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) + { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成单个表的代码并写入ZIP流 + * + * @param tableName 表名 + * @param zip ZIP输出流 + */ + private void generatorCode(String tableName, ZipOutputStream zip) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + // 初始化Velocity模板引擎 + VelocityInitializer.initVelocity(); + + // 准备模板上下文数据 + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + // 添加到ZIP文件 + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, Constants.UTF8); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } + catch (IOException e) + { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 验证编辑操作的参数合法性 + * 主要验证树表和主子表的必填配置 + * + * @param genTable 业务表信息对象 + */ + @Override + public void validateEdit(GenTable genTable) + { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) + { + // 树表模板需要验证树相关配置 + String options = JSON.toJSONString(genTable.getParams()); + JSONObject paramsObj = JSON.parseObject(options); + if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))) + { + throw new ServiceException("树编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))) + { + throw new ServiceException("树父编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))) + { + throw new ServiceException("树名称字段不能为空"); + } + } + // 主子表模板需要验证子表配置 + if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) + { + if (StringUtils.isEmpty(genTable.getSubTableName())) + { + throw new ServiceException("关联子表的表名不能为空"); + } + else if (StringUtils.isEmpty(genTable.getSubTableFkName())) + { + throw new ServiceException("子表关联的外键名不能为空"); + } + } + } + + /** + * 设置主键列信息 + * 从字段列表中找出主键列并设置到表信息中 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) + { + for (GenTableColumn column : table.getColumns()) + { + if (column.isPk()) + { + table.setPkColumn(column); + break; + } + } + // 若未找到主键,默认第一个字段为主键 + if (StringUtils.isNull(table.getPkColumn())) + { + table.setPkColumn(table.getColumns().get(0)); + } + // 处理子表的主键 + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) + { + for (GenTableColumn column : table.getSubTable().getColumns()) + { + if (column.isPk()) + { + table.getSubTable().setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getSubTable().getPkColumn())) + { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * 若当前表是子表,查询并设置关联的主表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) + { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) + { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 从选项中解析并设置表的额外配置 + * 如树表参数、菜单关联等信息 + * + * @param genTable 业务表信息 + */ + public void setTableFromOptions(GenTable genTable) + { + JSONObject paramsObj = JSON.parseObject(genTable.getOptions()); + if (StringUtils.isNotNull(paramsObj)) + { + String treeCode = paramsObj.getString(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + String parentMenuId = paramsObj.getString(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + } + + /** + * 获取代码生成的目标路径 + * 根据配置的生成路径和模板计算最终文件路径 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成文件的完整路径 + */ + public static String getGenPath(GenTable table, String template) + { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) + { + // 若生成路径为根目录,默认生成到项目src目录下 + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + // 自定义路径下生成 + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/service/IGenTableColumnService.java b/huacai-generator/src/main/java/com/huacai/generator/service/IGenTableColumnService.java new file mode 100644 index 0000000..129ac3c --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/service/IGenTableColumnService.java @@ -0,0 +1,44 @@ +package com.huacai.generator.service; + +import java.util.List; +import com.huacai.generator.domain.GenTableColumn; + +/** + * 业务字段 服务层 + * + * @author huacai + */ +public interface IGenTableColumnService +{ + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/service/IGenTableService.java b/huacai-generator/src/main/java/com/huacai/generator/service/IGenTableService.java new file mode 100644 index 0000000..6ea26ed --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/service/IGenTableService.java @@ -0,0 +1,121 @@ +package com.huacai.generator.service; + +import java.util.List; +import java.util.Map; +import com.huacai.generator.domain.GenTable; + +/** + * 业务 服务层 + * + * @author huacai + */ +public interface IGenTableService +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + public void importGenTable(List tableList); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/util/GenUtils.java b/huacai-generator/src/main/java/com/huacai/generator/util/GenUtils.java new file mode 100644 index 0000000..a4c26e2 --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/util/GenUtils.java @@ -0,0 +1,302 @@ +package com.huacai.generator.util; + +import java.util.Arrays; +import org.apache.commons.lang3.RegExUtils; +import com.huacai.common.constant.GenConstants; +import com.huacai.common.utils.StringUtils; +import com.huacai.generator.config.GenConfig; +import com.huacai.generator.domain.GenTable; +import com.huacai.generator.domain.GenTableColumn; + +/** + * 代码生成器工具类 + * 提供表信息初始化、字段属性转换、名称格式处理等核心工具方法 + * + * @author huacai + */ +public class GenUtils +{ + /** + * 初始化表信息 + * 从配置和表名中自动填充类名、包路径、模块名等核心属性 + * + * @param genTable 待初始化的表信息对象 + * @param operName 操作人名称(用于创建人字段) + */ + public static void initTable(GenTable genTable, String operName) + { + // 从表名转换为实体类名(驼峰命名,首字母大写) + genTable.setClassName(convertClassName(genTable.getTableName())); + // 从配置中获取生成包路径 + genTable.setPackageName(GenConfig.getPackageName()); + // 从包路径中提取模块名(如包路径com.huacai.modules.system,模块名为system) + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + // 从表名中提取业务名(如表名sys_user,业务名为user) + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + // 从表描述中提取功能名(去除"表"等冗余字符) + genTable.setFunctionName(replaceText(genTable.getTableComment())); + // 从配置中获取作者信息 + genTable.setFunctionAuthor(GenConfig.getAuthor()); + // 设置创建人 + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + * 根据数据库字段类型自动映射Java类型、前端显示类型等属性 + * + * @param column 待初始化的字段信息对象 + * @param table 字段所属的表信息对象 + */ + public static void initColumnField(GenTableColumn column, GenTable table) + { + // 提取数据库字段类型(去除长度信息,如varchar(50)→varchar) + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + + // 设置字段关联的表ID和创建人 + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + + // 数据库字段名转换为Java属性名(下划线转驼峰,首字母小写) + column.setJavaField(StringUtils.toCamelCase(columnName)); + // 设置Java类型默认值(默认String) + column.setJavaType(GenConstants.TYPE_STRING); + // 设置查询方式默认值(默认等于查询) + column.setQueryType(GenConstants.QUERY_EQ); + + // 处理字符串/文本类型字段 + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) + { + // 获取字段长度(如varchar(50)→50) + Integer columnLength = getColumnLength(column.getColumnType()); + // 长度≥500或文本类型(text)用文本域,否则用输入框 + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) + ? GenConstants.HTML_TEXTAREA + : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } + // 处理时间类型字段 + else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) + { + column.setJavaType(GenConstants.TYPE_DATE); // 映射为Date类型 + column.setHtmlType(GenConstants.HTML_DATETIME); // 前端用日期时间控件 + } + // 处理整数类型字段(int、tinyint等) + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER_INT, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); // 前端用输入框 + column.setJavaType(GenConstants.TYPE_INTEGER); // 映射为Integer类型 + } + // 处理长整数类型字段(bigint) + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER_LONG, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); // 前端用输入框 + column.setJavaType(GenConstants.TYPE_LONG); // 映射为Long类型 + } + // 处理浮点数类型字段(decimal、double等) + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER_REAL, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); // 前端用输入框 + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); // 映射为BigDecimal类型 + } + // 处理布尔类型字段(bit) + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER_BIT, dataType)) + { + column.setHtmlType(GenConstants.HTML_RADIO); // 前端用单选框 + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); // 映射为BigDecimal类型 + } + + // 插入字段配置:默认所有字段都允许插入 + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段配置:排除主键和不允许编辑的字段(如create_time) + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) + { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段配置:排除主键和不显示在列表的字段(如content) + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) + { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段配置:排除主键和不用于查询的字段(如remark) + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) + { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 特殊查询方式配置:以"name"结尾的字段(如username)默认用模糊查询 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) + { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 特殊前端控件配置:"status"结尾的字段(如status)默认用单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) + { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 特殊前端控件配置:"type"或"sex"结尾的字段默认用下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) + { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 特殊前端控件配置:"image"结尾的字段默认用图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) + { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 特殊前端控件配置:"file"结尾的字段默认用文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) + { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 特殊前端控件配置:"content"结尾的字段默认用富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) + { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * 用于判断字段类型是否属于目标类型集合(如判断是否为字符串类型) + * + * @param arr 目标数组 + * @param targetValue 待校验的值 + * @return true-包含;false-不包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) + { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 从包路径中提取模块名 + * 规则:取包路径最后一个"."后的内容(如com.huacai.modules.system→system) + * + * @param packageName 完整包路径 + * @return 模块名 + */ + public static String getModuleName(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + return StringUtils.substring(packageName, lastIndex + 1, nameLength); + } + + /** + * 从表名中提取业务名 + * 规则:去除表前缀后,取剩余部分(如sys_user→user,若无前缀则直接用表名) + * + * @param tableName 数据库表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) + { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + return StringUtils.substring(tableName, lastIndex + 1, nameLength); + } + + /** + * 表名转换为Java实体类名 + * 规则:先去除表前缀(若配置自动去除),再转驼峰命名并首字母大写 + * + * @param tableName 数据库表名 + * @return Java实体类名 + */ + public static String convertClassName(String tableName) + { + // 获取是否自动去除表前缀的配置 + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + // 获取配置的表前缀(多个前缀用逗号分隔) + String tablePrefix = GenConfig.getTablePrefix(); + + // 若开启自动去除前缀且前缀不为空,先去除表前缀 + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) + { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + + // 表名转驼峰命名(首字母大写) + return StringUtils.convertToCamelCase(tableName); + } + + /** + * 批量替换表名前缀 + * 从前缀列表中匹配第一个符合的前缀并去除 + * + * @param replacementm 原始表名 + * @param searchList 前缀列表 + * @return 去除前缀后的表名 + */ + public static String replaceFirst(String replacementm, String[] searchList) + { + String text = replacementm; + for (String searchString : searchList) + { + // 若表名以当前前缀开头,去除该前缀并跳出循环 + if (replacementm.startsWith(searchString)) + { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换(清理表描述) + * 去除表描述中的"表"、"若依"等冗余字符,提取纯净功能名 + * + * @param text 原始表描述 + * @return 清理后的功能名 + */ + public static String replaceText(String text) + { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 提取数据库字段类型(去除长度信息) + * 如"varchar(50)"→"varchar","int"→"int" + * + * @param columnType 原始字段类型(含长度) + * @return 纯净字段类型 + */ + public static String getDbType(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + return StringUtils.substringBefore(columnType, "("); + } + else + { + return columnType; + } + } + + /** + * 提取数据库字段长度 + * 如"varchar(50)"→50,无长度信息则返回0 + * + * @param columnType 原始字段类型(含长度) + * @return 字段长度(整数) + */ + public static Integer getColumnLength(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + // 提取括号中的长度数值 + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } + else + { + return 0; + } + } +} \ No newline at end of file diff --git a/huacai-generator/src/main/java/com/huacai/generator/util/VelocityInitializer.java b/huacai-generator/src/main/java/com/huacai/generator/util/VelocityInitializer.java new file mode 100644 index 0000000..1eb174d --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/util/VelocityInitializer.java @@ -0,0 +1,34 @@ +package com.huacai.generator.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; +import com.huacai.common.constant.Constants; + +/** + * VelocityEngine工厂 + * + * @author huacai + */ +public class VelocityInitializer +{ + /** + * 初始化vm方法 + */ + public static void initVelocity() + { + Properties p = new Properties(); + try + { + // 加载classpath目录下的vm文件 + p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/huacai-generator/src/main/java/com/huacai/generator/util/VelocityUtils.java b/huacai-generator/src/main/java/com/huacai/generator/util/VelocityUtils.java new file mode 100644 index 0000000..6a0818c --- /dev/null +++ b/huacai-generator/src/main/java/com/huacai/generator/util/VelocityUtils.java @@ -0,0 +1,477 @@ +package com.huacai.generator.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.velocity.VelocityContext; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.huacai.common.constant.GenConstants; +import com.huacai.common.utils.DateUtils; +import com.huacai.common.utils.StringUtils; +import com.huacai.generator.domain.GenTable; +import com.huacai.generator.domain.GenTableColumn; + +/** + * 模板处理工具类 + * 负责Velocity模板引擎的上下文准备、模板文件选择、生成文件名计算等核心功能 + * + * @author huacai + */ +public class VelocityUtils +{ + /** 项目Java代码空间路径(Maven标准目录结构) */ + private static final String PROJECT_PATH = "main/java"; + + /** MyBatis映射文件空间路径(Maven标准目录结构) */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单ID(系统工具菜单) */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 准备Velocity模板上下文数据 + * 将表信息、字段信息等转换为模板所需的变量,供模板渲染使用 + * + * @param genTable 业务表信息对象 + * @return 填充好数据的Velocity上下文对象 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + + // 基础变量:模板类型、表名、功能名等 + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); // 实体类名(首字母大写) + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); // 实体类名(首字母小写) + velocityContext.put("moduleName", moduleName); + velocityContext.put("BusinessName", StringUtils.capitalize(businessName)); // 业务名(首字母大写) + velocityContext.put("businessName", businessName); // 业务名(首字母小写) + velocityContext.put("basePackage", getPackagePrefix(packageName)); // 基础包路径(去除最后一级) + velocityContext.put("packageName", packageName); // 完整包路径 + velocityContext.put("author", genTable.getFunctionAuthor()); // 作者 + velocityContext.put("datetime", DateUtils.getDate()); // 当前日期 + velocityContext.put("pkColumn", genTable.getPkColumn()); // 主键字段 + velocityContext.put("importList", getImportList(genTable)); // 需要导入的Java类 + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); // 权限前缀 + velocityContext.put("columns", genTable.getColumns()); // 所有字段列表 + velocityContext.put("table", genTable); // 表信息对象 + velocityContext.put("dicts", getDicts(genTable)); // 关联的字典类型列表 + + // 设置菜单相关变量 + setMenuVelocityContext(velocityContext, genTable); + + // 树表模板特有变量 + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + + // 主子表模板特有变量 + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + + return velocityContext; + } + + /** + * 设置菜单相关的模板变量 + * 主要处理上级菜单ID + * + * @param context Velocity上下文 + * @param genTable 业务表信息 + */ + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + // 获取上级菜单ID(配置优先,无配置则用默认值) + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + /** + * 设置树表模板特有变量 + * 包括树编码、父编码、树名称等树结构相关字段 + * + * @param context Velocity上下文 + * @param genTable 业务表信息 + */ + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + + // 树表核心字段(从配置中提取) + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); // 树节点展开按钮所在列 + + // 额外的树表参数 + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + /** + * 设置主子表模板特有变量 + * 包括子表信息、外键关联等字段 + * + * @param context Velocity上下文 + * @param genTable 业务表信息(主表) + */ + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = subTable.getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); // 外键字段名(驼峰) + + context.put("subTable", subTable); // 子表信息对象 + context.put("subTableName", subTableName); // 子表名 + context.put("subTableFkName", subTableFkName); // 外键名 + context.put("subTableFkClassName", subTableFkClassName); // 外键Java属性名(首字母大写) + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); // 外键Java属性名(首字母小写) + context.put("subClassName", subClassName); // 子表实体类名(首字母大写) + context.put("subclassName", StringUtils.uncapitalize(subClassName)); // 子表实体类名(首字母小写) + context.put("subImportList", getImportList(subTable)); // 子表需要导入的Java类 + } + + /** + * 获取需要渲染的模板列表 + * 根据模板类型(单表/树表/主子表)和前端类型(element-ui/element-plus)选择对应模板 + * + * @param tplCategory 模板类型(CRUD/TREE/SUB) + * @param tplWebType 前端框架类型 + * @return 模板文件路径列表 + */ + public static List getTemplateList(String tplCategory, String tplWebType) + { + // 根据前端类型选择模板目录(默认vue2,element-plus对应vue3) + String useWebType = "vm/vue"; + if ("element-plus".equals(tplWebType)) + { + useWebType = "vm/vue/v3"; + } + + List templates = new ArrayList(); + // 通用模板(所有类型都需要生成) + templates.add("vm/java/domain.java.vm"); // 实体类 + templates.add("vm/java/mapper.java.vm"); // Mapper接口 + templates.add("vm/java/service.java.vm"); // Service接口 + templates.add("vm/java/serviceImpl.java.vm"); // Service实现类 + templates.add("vm/java/controller.java.vm"); // Controller类 + templates.add("vm/xml/mapper.xml.vm"); // MyBatis映射文件 + templates.add("vm/sql/sql.vm"); // 菜单SQL脚本 + templates.add("vm/js/api.js.vm"); // 前端API请求 + + // 根据模板类型添加特有模板 + if (GenConstants.TPL_CRUD.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); // 单表列表页 + } + else if (GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add(useWebType + "/index-tree.vue.vm"); // 树表列表页 + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); // 主子表列表页 + templates.add("vm/java/sub-domain.java.vm"); // 子表实体类 + } + return templates; + } + + /** + * 计算生成文件的完整路径和名称 + * 根据模板类型和表信息生成对应的文件路径(如实体类放在domain包下) + * + * @param template 模板文件路径 + * @param genTable 业务表信息 + * @return 生成文件的完整路径和名称 + */ + public static String getFileName(String template, GenTable genTable) + { + String fileName = ""; + String packageName = genTable.getPackageName(); + String moduleName = genTable.getModuleName(); + String className = genTable.getClassName(); + String businessName = genTable.getBusinessName(); + + // 基础路径计算 + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); // Java代码根路径(包路径转目录) + String mybatisPath = MYBATIS_PATH + "/" + moduleName; // MyBatis映射文件路径 + String vuePath = "vue"; // 前端代码根路径 + + // 根据模板类型计算具体文件名 + if (template.contains("domain.java.vm")) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + // 主子表的子表实体类 + else if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("index.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取基础包路径(去除包名最后一级) + * 如com.huacai.system.user→com.huacai.system + * + * @param packageName 完整包名 + * @return 基础包路径 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + return StringUtils.substring(packageName, 0, lastIndex); + } + + /** + * 计算需要导入的Java类列表 + * 根据字段类型自动添加必要的导入(如Date、BigDecimal等) + * + * @param genTable 业务表信息 + * @return 导入类的全限定名集合(去重) + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + + // 主子表需要导入List + if (StringUtils.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + + // 根据字段类型添加导入 + for (GenTableColumn column : columns) + { + // 非超级字段且类型为Date时,导入Date和JsonFormat注解 + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + // 非超级字段且类型为BigDecimal时,导入BigDecimal + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 获取关联的字典类型列表 + * 用于前端渲染字典下拉框时导入所需字典 + * + * @param genTable 业务表信息 + * @return 字典类型字符串(用逗号分隔) + */ + public static String getDicts(GenTable genTable) + { + List columns = genTable.getColumns(); + Set dicts = new HashSet(); + addDicts(dicts, columns); + + // 主子表需要包含子表的字典 + if (StringUtils.isNotNull(genTable.getSubTable())) + { + List subColumns = genTable.getSubTable().getColumns(); + addDicts(dicts, subColumns); + } + return StringUtils.join(dicts, ", "); + } + + /** + * 向字典集合中添加字段关联的字典类型 + * 仅处理下拉框、单选框、复选框类型的字段 + * + * @param dicts 字典集合 + * @param columns 字段列表 + */ + public static void addDicts(Set dicts, List columns) + { + for (GenTableColumn column : columns) + { + // 非超级字段、有字典类型、且前端控件为选择类时添加 + if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( + column.getHtmlType(), + new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX })) + { + dicts.add("'" + column.getDictType() + "'"); + } + } + } + + /** + * 生成权限前缀 + * 格式:模块名:业务名(用于权限控制) + * + * @param moduleName 模块名 + * @param businessName 业务名 + * @return 权限前缀字符串 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID + * 从配置中提取,无配置则使用默认值 + * + * @param paramsObj 配置参数对象 + * @return 上级菜单ID + */ + public static String getParentMenuId(JSONObject paramsObj) + { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID))) + { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码字段(Java属性名) + * 从配置中提取并转换为驼峰命名 + * + * @param paramsObj 配置参数对象 + * @return 树编码字段名(驼峰) + */ + public static String getTreecode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码字段(Java属性名) + * 从配置中提取并转换为驼峰命名 + * + * @param paramsObj 配置参数对象 + * @return 树父编码字段名(驼峰) + */ + public static String getTreeParentCode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树名称字段(Java属性名) + * 从配置中提取并转换为驼峰命名 + * + * @param paramsObj 配置参数对象 + * @return 树名称字段名(驼峰) + */ + public static String getTreeName(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 计算树表展开按钮所在列的序号 + * 用于前端树表渲染时确定在哪一列显示展开/折叠按钮 + * + * @param genTable 业务表信息 + * @return 展开按钮列的序号(从1开始) + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + + // 遍历列表字段,找到树名称字段所在的列序号 + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } +} \ No newline at end of file diff --git a/huacai-generator/src/main/resources/generator.yml b/huacai-generator/src/main/resources/generator.yml new file mode 100644 index 0000000..5eb98d3 --- /dev/null +++ b/huacai-generator/src/main/resources/generator.yml @@ -0,0 +1,10 @@ +# 代码生成 +gen: + # 作者 + author: huacai + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.huacai + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: ylxt_ diff --git a/huacai-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/huacai-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..1e3746b --- /dev/null +++ b/huacai-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + column_id, table_id, column_name, column_comment, column_type, + java_type, java_field, is_pk, is_increment, is_required, + is_insert, is_edit, is_list, is_query, query_type, html_type, + dict_type, sort, create_by, create_time, update_by, update_time + from gen_table_column + + + + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() -- 创建时间为当前系统时间 + ) + + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() -- 更新时间为当前系统时间 + + where column_id = #{columnId} -- 按主键更新 + + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/huacai-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/huacai-generator/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..f5b0233 --- /dev/null +++ b/huacai-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, + class_name, tpl_category, tpl_web_type, package_name, module_name, + business_name, function_name, function_author, gen_type, gen_path, + options, create_by, create_time, update_by, update_time, remark + from gen_table + + + + + + + + + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + tpl_web_type, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{tplWebType}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() -- 创建时间为当前系统时间 + ) + + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + tpl_web_type = #{tplWebType}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() -- 更新时间为当前系统时间 + + where table_id = #{tableId} -- 按主键更新 + + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/huacai-generator/src/main/resources/vm/java/controller.java.vm b/huacai-generator/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..6617abd --- /dev/null +++ b/huacai-generator/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,162 @@ +package ${packageName}.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.huacai.common.annotation.Log; +import com.huacai.common.core.controller.BaseController; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.enums.BusinessType; +import java.io.InputStream; +import org.springframework.web.multipart.MultipartFile; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.huacai.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.huacai.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}前端控制器 + * 提供${functionName}的CRUD、导入导出等接口服务 + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + +/** + * 查询${functionName}列表 + * 支持分页查询和条件过滤 + */ +@PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") // 权限控制:需要${permissionPrefix}:list权限 +@GetMapping("/list") + #if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); // 开启分页(BaseController提供的分页工具) + // 调用Service层查询列表 + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); // 包装成分页响应对象 + } + #elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + // 树结构数据不需要分页,直接返回列表 + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); // 包装成成功响应 + } + #end + + /** + * 导出${functionName}列表 + * 将查询结果导出为Excel文件 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") // 权限控制:需要${permissionPrefix}:export权限 + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) // 操作日志记录 + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + // 查询待导出的数据列表 + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + // 使用Excel工具类导出 + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 下载导入模板 + * 提供标准的Excel导入模板供用户填写数据 + */ + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) + { + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.importTemplateExcel(response, "${functionName}数据"); + } + + /** + * 导入数据 + * 从Excel文件导入数据并批量保存 + */ + @Log(title = "${functionName}", businessType = BusinessType.IMPORT) // 操作日志记录 + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:import')") // 权限控制:需要${permissionPrefix}:import权限 + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file) throws Exception + { + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + InputStream inputStream = file.getInputStream(); + // 解析Excel文件得到数据列表 + List<${ClassName}> list = util.importExcel(inputStream ); + inputStream.close(); + // 调用Service批量插入 + int count = ${className}Service.batchInsert${ClassName}(list); + return AjaxResult.success("导入成功" + count + "条信息!"); + } + + /** + * 获取${functionName}详细信息 + * 根据主键查询单条记录详情 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") // 权限控制:需要${permissionPrefix}:query权限 + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + // 调用Service查询详情 + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + * 新增一条记录 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") // 权限控制:需要${permissionPrefix}:add权限 + @Log(title = "${functionName}", businessType = BusinessType.INSERT) // 操作日志记录 + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + // 调用Service插入数据,返回影响行数 + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + * 更新一条记录 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") // 权限控制:需要${permissionPrefix}:edit权限 + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) // 操作日志记录 + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + // 调用Service更新数据,返回影响行数 + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + * 批量删除记录 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") // 权限控制:需要${permissionPrefix}:remove权限 + @Log(title = "${functionName}", businessType = BusinessType.DELETE) // 操作日志记录 + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + // 调用Service批量删除,返回影响行数 + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} \ No newline at end of file diff --git a/huacai-generator/src/main/resources/vm/java/domain.java.vm b/huacai-generator/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..75a016c --- /dev/null +++ b/huacai-generator/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,59 @@ +package ${packageName}.domain; + + #foreach ($import in $importList) + import ${import}; + #end +import com.huacai.common.annotation.Excel; +import lombok.*; +import com.huacai.common.core.domain.BaseEntity; + +/** + * ${functionName}对象 ${tableName} + * 对应数据库表:${tableName},用于封装${functionName}相关数据 + * + * @author ${author} + * @date ${datetime} + */ + #if($table.crud || $table.sub) + #set($Entity="BaseEntity") // 单表/主子表继承基础实体类(含创建时间、更新时间等通用字段) + #elseif($table.tree) + #set($Entity="TreeEntity") // 树表继承树形实体类(含父ID、排序等树形结构字段) + #end + @EqualsAndHashCode(callSuper = true) // Lombok注解:生成equals和hashCode方法,包含父类字段 + @Data // Lombok注解:自动生成getter、setter、toString等方法 + @AllArgsConstructor // Lombok注解:生成全参构造方法 + @NoArgsConstructor // Lombok注解:生成无参构造方法 + public class ${ClassName} extends ${Entity} + { + private static final long serialVersionUID = 1L; // 序列化版本号 + + #foreach ($column in $columns) + #if(!$table.isSuperColumn($column.javaField)) // 排除父类已定义的字段(如id、createTime等) + /** $column.columnComment */ // 字段注释,取自数据库字段说明 + #if($column.list) // 仅对列表显示字段添加Excel注解(用于导出) + #set($parentheseIndex=$column.columnComment.indexOf("(")) // 处理注释中的括号内容(如状态(0正常1停用)) + #if($parentheseIndex != -1) + #set($comment=$column.columnComment.substring(0, $parentheseIndex)) // 截取括号前的纯注释 + #else + #set($comment=$column.columnComment) + #end + #if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") // 带字典转换的Excel注解 + #elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") // 日期格式化注解 + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") // 日期类型Excel注解 + #else + @Excel(name = "${comment}") // 普通字段Excel注解 + #end + #end + private $column.javaType $column.javaField; // 字段定义(类型+名称) + + #end + #end + #if($table.sub) // 主子表场景:添加子表集合字段 + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; // 子表数据列表(一对多关系) + + #end + + } diff --git a/huacai-generator/src/main/resources/vm/java/mapper.java.vm b/huacai-generator/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..7e7d7c2 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,91 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/huacai-generator/src/main/resources/vm/java/service.java.vm b/huacai-generator/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..2dffb14 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,69 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 批量新增${functionName} + * + * @param ${className}s ${functionName}List + * @return 结果 + */ + public int batchInsert${ClassName}(List<${ClassName}> ${className}s); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/huacai-generator/src/main/resources/vm/java/serviceImpl.java.vm b/huacai-generator/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..b6d2e64 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,211 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.huacai.common.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.huacai.common.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.util.CollectionUtils; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + @Autowired + private SqlSessionFactory sqlSessionFactory; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 批量新增${functionName} + * + * @param ${className}s ${functionName}List + * @return 结果 + */ + @Override + public int batchInsert${ClassName}(List<${ClassName}> ${className}s) + { + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false); + int count = 0; + if (!CollectionUtils.isEmpty(${className}s)) { + try { + for (int i = 0; i < ${className}s.size(); i++) { + int row = ${className}Mapper.insert${ClassName}(${className}s.get(i)); + // 防止内存溢出,每100次提交一次,并清除缓存 + boolean bool = (i >0 && i%100 == 0) || i == ${className}s.size() - 1; + if (bool){ + sqlSession.commit(); + sqlSession.clearCache(); + } + count = i + 1; + } + }catch (Exception e){ + e.printStackTrace(); + // 没有提交的数据可以回滚 + sqlSession.rollback(); + }finally { + sqlSession.close(); + return count; + } + } + return count; + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/huacai-generator/src/main/resources/vm/java/sub-domain.java.vm b/huacai-generator/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..3b804b0 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,47 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import com.huacai.common.annotation.Excel; +import lombok.*; +import com.huacai.common.core.domain.BaseEntity; +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end + +} diff --git a/huacai-generator/src/main/resources/vm/js/api.js.vm b/huacai-generator/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..9295524 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/huacai-generator/src/main/resources/vm/sql/sql.vm b/huacai-generator/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/huacai-generator/src/main/resources/vm/vue/index-tree.vue.vm b/huacai-generator/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..859f17e --- /dev/null +++ b/huacai-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,503 @@ + + + diff --git a/huacai-generator/src/main/resources/vm/vue/index.vue.vm b/huacai-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..d2aa807 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,712 @@ + + + diff --git a/huacai-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/huacai-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..c54d62b --- /dev/null +++ b/huacai-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/huacai-generator/src/main/resources/vm/vue/v3/index.vue.vm b/huacai-generator/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..8b25665 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/huacai-generator/src/main/resources/vm/xml/mapper.xml.vm b/huacai-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..4233b10 --- /dev/null +++ b/huacai-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,135 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + diff --git a/huacai-generator/target/classes/com/huacai/generator/config/GenConfig.class b/huacai-generator/target/classes/com/huacai/generator/config/GenConfig.class new file mode 100644 index 0000000..3f57dd0 Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/config/GenConfig.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/controller/GenController.class b/huacai-generator/target/classes/com/huacai/generator/controller/GenController.class new file mode 100644 index 0000000..e5b34af Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/controller/GenController.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/domain/GenTable.class b/huacai-generator/target/classes/com/huacai/generator/domain/GenTable.class new file mode 100644 index 0000000..eb4e12f Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/domain/GenTable.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/domain/GenTableColumn.class b/huacai-generator/target/classes/com/huacai/generator/domain/GenTableColumn.class new file mode 100644 index 0000000..44b3523 Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/domain/GenTableColumn.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/mapper/GenTableColumnMapper.class b/huacai-generator/target/classes/com/huacai/generator/mapper/GenTableColumnMapper.class new file mode 100644 index 0000000..2468bed Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/mapper/GenTableColumnMapper.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/mapper/GenTableMapper.class b/huacai-generator/target/classes/com/huacai/generator/mapper/GenTableMapper.class new file mode 100644 index 0000000..b3b41dc Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/mapper/GenTableMapper.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/service/GenTableColumnServiceImpl.class b/huacai-generator/target/classes/com/huacai/generator/service/GenTableColumnServiceImpl.class new file mode 100644 index 0000000..bb5c2b2 Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/service/GenTableColumnServiceImpl.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/service/GenTableServiceImpl.class b/huacai-generator/target/classes/com/huacai/generator/service/GenTableServiceImpl.class new file mode 100644 index 0000000..9575146 Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/service/GenTableServiceImpl.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/service/IGenTableColumnService.class b/huacai-generator/target/classes/com/huacai/generator/service/IGenTableColumnService.class new file mode 100644 index 0000000..b47bfa3 Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/service/IGenTableColumnService.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/service/IGenTableService.class b/huacai-generator/target/classes/com/huacai/generator/service/IGenTableService.class new file mode 100644 index 0000000..092aead Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/service/IGenTableService.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/util/GenUtils.class b/huacai-generator/target/classes/com/huacai/generator/util/GenUtils.class new file mode 100644 index 0000000..cf0ef73 Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/util/GenUtils.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/util/VelocityInitializer.class b/huacai-generator/target/classes/com/huacai/generator/util/VelocityInitializer.class new file mode 100644 index 0000000..7637f4d Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/util/VelocityInitializer.class differ diff --git a/huacai-generator/target/classes/com/huacai/generator/util/VelocityUtils.class b/huacai-generator/target/classes/com/huacai/generator/util/VelocityUtils.class new file mode 100644 index 0000000..32d1eff Binary files /dev/null and b/huacai-generator/target/classes/com/huacai/generator/util/VelocityUtils.class differ diff --git a/huacai-generator/target/classes/generator.yml b/huacai-generator/target/classes/generator.yml new file mode 100644 index 0000000..5eb98d3 --- /dev/null +++ b/huacai-generator/target/classes/generator.yml @@ -0,0 +1,10 @@ +# 代码生成 +gen: + # 作者 + author: huacai + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.huacai + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: ylxt_ diff --git a/huacai-generator/target/classes/mapper/generator/GenTableColumnMapper.xml b/huacai-generator/target/classes/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..3142541 --- /dev/null +++ b/huacai-generator/target/classes/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + diff --git a/huacai-generator/target/classes/mapper/generator/GenTableMapper.xml b/huacai-generator/target/classes/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..7f17e71 --- /dev/null +++ b/huacai-generator/target/classes/mapper/generator/GenTableMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + tpl_web_type, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{tplWebType}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + tpl_web_type = #{tplWebType}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + diff --git a/huacai-generator/target/classes/vm/java/controller.java.vm b/huacai-generator/target/classes/vm/java/controller.java.vm new file mode 100644 index 0000000..971bc58 --- /dev/null +++ b/huacai-generator/target/classes/vm/java/controller.java.vm @@ -0,0 +1,143 @@ +package ${packageName}.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.huacai.common.annotation.Log; +import com.huacai.common.core.controller.BaseController; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.enums.BusinessType; +import java.io.InputStream; +import org.springframework.web.multipart.MultipartFile; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.huacai.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.huacai.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 下载模板 + */ + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) + { + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.importTemplateExcel(response, "${functionName}数据"); + } + + /** + * 导入数据 + */ + @Log(title = "${functionName}", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:import')") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file) throws Exception + { + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + InputStream inputStream = file.getInputStream(); + List<${ClassName}> list = util.importExcel(inputStream ); + inputStream.close(); + int count = ${className}Service.batchInsert${ClassName}(list); + return AjaxResult.success("导入成功" + count + "条信息!"); + } + + /** + * 获取${functionName}详细信息 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} diff --git a/huacai-generator/target/classes/vm/java/domain.java.vm b/huacai-generator/target/classes/vm/java/domain.java.vm new file mode 100644 index 0000000..c608b41 --- /dev/null +++ b/huacai-generator/target/classes/vm/java/domain.java.vm @@ -0,0 +1,61 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import com.huacai.common.annotation.Excel; +#if($table.crud || $table.sub) +#elseif($table.tree) +#end +import lombok.*; +import com.huacai.common.core.domain.BaseEntity; + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end + +} diff --git a/huacai-generator/target/classes/vm/java/mapper.java.vm b/huacai-generator/target/classes/vm/java/mapper.java.vm new file mode 100644 index 0000000..7e7d7c2 --- /dev/null +++ b/huacai-generator/target/classes/vm/java/mapper.java.vm @@ -0,0 +1,91 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/huacai-generator/target/classes/vm/java/service.java.vm b/huacai-generator/target/classes/vm/java/service.java.vm new file mode 100644 index 0000000..2dffb14 --- /dev/null +++ b/huacai-generator/target/classes/vm/java/service.java.vm @@ -0,0 +1,69 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 批量新增${functionName} + * + * @param ${className}s ${functionName}List + * @return 结果 + */ + public int batchInsert${ClassName}(List<${ClassName}> ${className}s); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/huacai-generator/target/classes/vm/java/serviceImpl.java.vm b/huacai-generator/target/classes/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..b6d2e64 --- /dev/null +++ b/huacai-generator/target/classes/vm/java/serviceImpl.java.vm @@ -0,0 +1,211 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.huacai.common.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.huacai.common.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.util.CollectionUtils; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + @Autowired + private SqlSessionFactory sqlSessionFactory; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 批量新增${functionName} + * + * @param ${className}s ${functionName}List + * @return 结果 + */ + @Override + public int batchInsert${ClassName}(List<${ClassName}> ${className}s) + { + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false); + int count = 0; + if (!CollectionUtils.isEmpty(${className}s)) { + try { + for (int i = 0; i < ${className}s.size(); i++) { + int row = ${className}Mapper.insert${ClassName}(${className}s.get(i)); + // 防止内存溢出,每100次提交一次,并清除缓存 + boolean bool = (i >0 && i%100 == 0) || i == ${className}s.size() - 1; + if (bool){ + sqlSession.commit(); + sqlSession.clearCache(); + } + count = i + 1; + } + }catch (Exception e){ + e.printStackTrace(); + // 没有提交的数据可以回滚 + sqlSession.rollback(); + }finally { + sqlSession.close(); + return count; + } + } + return count; + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/huacai-generator/target/classes/vm/java/sub-domain.java.vm b/huacai-generator/target/classes/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..3b804b0 --- /dev/null +++ b/huacai-generator/target/classes/vm/java/sub-domain.java.vm @@ -0,0 +1,47 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import com.huacai.common.annotation.Excel; +import lombok.*; +import com.huacai.common.core.domain.BaseEntity; +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end + +} diff --git a/huacai-generator/target/classes/vm/js/api.js.vm b/huacai-generator/target/classes/vm/js/api.js.vm new file mode 100644 index 0000000..9295524 --- /dev/null +++ b/huacai-generator/target/classes/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/huacai-generator/target/classes/vm/sql/sql.vm b/huacai-generator/target/classes/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/huacai-generator/target/classes/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/huacai-generator/target/classes/vm/vue/index-tree.vue.vm b/huacai-generator/target/classes/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..859f17e --- /dev/null +++ b/huacai-generator/target/classes/vm/vue/index-tree.vue.vm @@ -0,0 +1,503 @@ + + + diff --git a/huacai-generator/target/classes/vm/vue/index.vue.vm b/huacai-generator/target/classes/vm/vue/index.vue.vm new file mode 100644 index 0000000..d2aa807 --- /dev/null +++ b/huacai-generator/target/classes/vm/vue/index.vue.vm @@ -0,0 +1,712 @@ + + + diff --git a/huacai-generator/target/classes/vm/vue/v3/index-tree.vue.vm b/huacai-generator/target/classes/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..c54d62b --- /dev/null +++ b/huacai-generator/target/classes/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/huacai-generator/target/classes/vm/vue/v3/index.vue.vm b/huacai-generator/target/classes/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..8b25665 --- /dev/null +++ b/huacai-generator/target/classes/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/huacai-generator/target/classes/vm/xml/mapper.xml.vm b/huacai-generator/target/classes/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..4233b10 --- /dev/null +++ b/huacai-generator/target/classes/vm/xml/mapper.xml.vm @@ -0,0 +1,135 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + diff --git a/huacai-quartz/pom.xml b/huacai-quartz/pom.xml new file mode 100644 index 0000000..728e9e1 --- /dev/null +++ b/huacai-quartz/pom.xml @@ -0,0 +1,40 @@ + + + + huacai + com.huacai + 3.8.7 + + 4.0.0 + + huacai-quartz + + + quartz定时任务 + + + + + + + org.quartz-scheduler + quartz + + + com.mchange + c3p0 + + + + + + + com.huacai + huacai-common + + + + + diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/config/ScheduleConfig.java b/huacai-quartz/src/main/java/com/huacai/quartz/config/ScheduleConfig.java new file mode 100644 index 0000000..9852b95 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.huacai.quartz.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +//import javax.sql.DataSource; +//import java.util.Properties; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author huacai +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "huacaiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "10"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("huacaiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/controller/SysJobController.java b/huacai-quartz/src/main/java/com/huacai/quartz/controller/SysJobController.java new file mode 100644 index 0000000..d11aa3b --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/controller/SysJobController.java @@ -0,0 +1,235 @@ +package com.huacai.quartz.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.huacai.common.annotation.Log; +import com.huacai.common.constant.Constants; +import com.huacai.common.core.controller.BaseController; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.core.page.TableDataInfo; +import com.huacai.common.enums.BusinessType; +import com.huacai.common.exception.job.TaskException; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.poi.ExcelUtil; +import com.huacai.quartz.domain.SysJob; +import com.huacai.quartz.service.ISysJobService; +import com.huacai.quartz.util.CronUtils; +import com.huacai.quartz.util.ScheduleUtils; + +/** + * 调度任务信息操作处理控制器 + * 负责定时任务的CRUD、状态切换、立即执行等核心功能接口 + * + * @author huacai + */ +@RestController +@RequestMapping("/monitor/job") +public class SysJobController extends BaseController +{ + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + * 支持分页和条件过滤,需"monitor:job:list"权限 + * + * @param sysJob 包含查询条件的任务对象 + * @return 分页后的任务列表数据 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) + { + startPage(); // 开启分页(继承自BaseController) + List list = jobService.selectJobList(sysJob); // 调用服务层查询列表 + return getDataTable(list); // 包装成分页响应对象 + } + + /** + * 导出定时任务列表 + * 将查询结果导出为Excel文件,需"monitor:job:export"权限 + * + * @param response HTTP响应对象 + * @param sysJob 查询条件 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) // 记录操作日志 + @PostMapping("/export") + public void export(HttpServletResponse response, SysJob sysJob) + { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); // 导出Excel + } + + /** + * 获取定时任务详细信息 + * 根据任务ID查询单条任务详情,需"monitor:job:query"权限 + * + * @param jobId 任务ID + * @return 包含任务详情的响应对象 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return success(jobService.selectJobById(jobId)); // 调用服务层查询详情 + } + + /** + * 新增定时任务 + * 包含严格的参数校验(Cron表达式、调用目标安全性等),需"monitor:job:add"权限 + * + * @param job 待新增的任务对象 + * @return 新增结果响应 + * @throws SchedulerException 调度器异常 + * @throws TaskException 任务处理异常 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException + { + // 校验Cron表达式合法性 + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + // 安全校验:禁止RMI调用 + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + // 安全校验:禁止LDAP调用 + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + // 安全校验:禁止HTTP调用 + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + // 安全校验:禁止包含违规字符串 + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + // 安全校验:必须在白名单内 + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + + job.setCreateBy(getUsername()); // 设置创建人(从当前登录用户获取) + return toAjax(jobService.insertJob(job)); // 调用服务层新增任务 + } + + /** + * 修改定时任务 + * 包含与新增相同的参数校验逻辑,需"monitor:job:edit"权限 + * + * @param job 待修改的任务对象 + * @return 修改结果响应 + * @throws SchedulerException 调度器异常 + * @throws TaskException 任务处理异常 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException + { + // 与新增任务相同的校验逻辑(Cron表达式+安全校验) + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + + job.setUpdateBy(getUsername()); // 设置更新人 + return toAjax(jobService.updateJob(job)); // 调用服务层更新任务 + } + + /** + * 定时任务状态修改 + * 用于启用/停用定时任务,需"monitor:job:changeStatus"权限 + * + * @param job 包含任务ID和目标状态的对象 + * @return 状态修改结果响应 + * @throws SchedulerException 调度器异常 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException + { + SysJob newJob = jobService.selectJobById(job.getJobId()); // 先查询原任务 + newJob.setStatus(job.getStatus()); // 更新状态 + return toAjax(jobService.changeStatus(newJob)); // 调用服务层修改状态 + } + + /** + * 定时任务立即执行一次 + * 无视Cron表达式,立即触发任务执行,需"monitor:job:changeStatus"权限 + * + * @param job 包含任务ID的对象 + * @return 执行结果响应 + * @throws SchedulerException 调度器异常 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException + { + boolean result = jobService.run(job); // 调用服务层立即执行 + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + * 批量删除指定ID的任务,需"monitor:job:remove"权限 + * + * @param jobIds 任务ID数组 + * @return 删除结果响应 + * @throws SchedulerException 调度器异常 + * @throws TaskException 任务处理异常 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException + { + jobService.deleteJobByIds(jobIds); // 调用服务层批量删除 + return success(); + } +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/controller/SysJobLogController.java b/huacai-quartz/src/main/java/com/huacai/quartz/controller/SysJobLogController.java new file mode 100644 index 0000000..cb70a92 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/controller/SysJobLogController.java @@ -0,0 +1,111 @@ +package com.huacai.quartz.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.huacai.common.annotation.Log; +import com.huacai.common.core.controller.BaseController; +import com.huacai.common.core.domain.AjaxResult; +import com.huacai.common.core.page.TableDataInfo; +import com.huacai.common.enums.BusinessType; +import com.huacai.common.utils.poi.ExcelUtil; +import com.huacai.quartz.domain.SysJobLog; +import com.huacai.quartz.service.ISysJobLogService; + +/** + * 调度日志操作处理控制器 + * 负责定时任务执行日志的查询、导出、删除、清空等管理功能 + * + * @author huacai + */ +@RestController +@RequestMapping("/monitor/jobLog") +public class SysJobLogController extends BaseController +{ + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + * 支持分页和条件过滤,需"monitor:job:list"权限 + * + * @param sysJobLog 包含查询条件的日志对象 + * @return 分页后的日志列表数据 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) + { + startPage(); // 开启分页(继承自BaseController) + List list = jobLogService.selectJobLogList(sysJobLog); // 调用服务层查询日志列表 + return getDataTable(list); // 包装成分页响应对象 + } + + /** + * 导出定时任务调度日志列表 + * 将查询结果导出为Excel文件,需"monitor:job:export"权限 + * + * @param response HTTP响应对象 + * @param sysJobLog 查询条件 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) // 记录导出操作日志 + @PostMapping("/export") + public void export(HttpServletResponse response, SysJobLog sysJobLog) + { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); // 导出Excel文件 + } + + /** + * 根据调度日志ID获取详细信息 + * 查询单条日志的详细执行记录,需"monitor:job:query"权限 + * + * @param jobLogId 日志ID + * @return 包含日志详情的响应对象 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobLogId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) + { + return success(jobLogService.selectJobLogById(jobLogId)); // 调用服务层查询日志详情 + } + + /** + * 批量删除定时任务调度日志 + * 删除指定ID的日志记录,需"monitor:job:remove"权限 + * + * @param jobLogIds 日志ID数组 + * @return 删除结果响应 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) // 记录删除操作日志 + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) + { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); // 调用服务层批量删除 + } + + /** + * 清空所有定时任务调度日志 + * 一次性删除系统中所有任务调度日志,需"monitor:job:remove"权限 + * + * @return 清空结果响应 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) // 记录清空操作日志 + @DeleteMapping("/clean") + public AjaxResult clean() + { + jobLogService.cleanJobLog(); // 调用服务层清空日志 + return success(); + } +} \ No newline at end of file diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/domain/SysJob.java b/huacai-quartz/src/main/java/com/huacai/quartz/domain/SysJob.java new file mode 100644 index 0000000..5f65a02 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/domain/SysJob.java @@ -0,0 +1,192 @@ +package com.huacai.quartz.domain; + +import java.util.Date; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.huacai.common.annotation.Excel; +import com.huacai.common.annotation.Excel.ColumnType; +import com.huacai.common.constant.ScheduleConstants; +import com.huacai.common.core.domain.BaseEntity; +import com.huacai.common.utils.StringUtils; +import com.huacai.quartz.util.CronUtils; + +/** + * 定时任务调度实体类(对应表sys_job) + * 封装定时任务的基本信息、调度规则及执行状态 + * + * @author huacai + */ +public class SysJob extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 任务ID(主键) */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) // Excel导出时的配置 + private Long jobId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名(用于分类管理任务) */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串(任务执行的目标方法,如类名.方法名) */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** cron执行表达式(任务调度的时间规则) */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** + * cron计划策略(任务错过执行时的处理策略) + * 默认值为ScheduleConstants.MISFIRE_DEFAULT(默认策略) + */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** 是否并发执行(0允许 1禁止) */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** 任务状态(0正常 1暂停) */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + // ------------------- Getter和Setter方法 ------------------- + public Long getJobId() + { + return jobId; + } + + public void setJobId(Long jobId) + { + this.jobId = jobId; + } + + /** + * 任务名称校验:非空且长度不超过64字符 + */ + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + /** + * 调用目标字符串校验:非空且长度不超过500字符 + */ + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + /** + * Cron表达式校验:非空且长度不超过255字符 + */ + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression() + { + return cronExpression; + } + + public void setCronExpression(String cronExpression) + { + this.cronExpression = cronExpression; + } + + /** + * 计算下次执行时间(通过Cron表达式解析) + * 前端展示用,不对应数据库字段 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() + { + if (StringUtils.isNotEmpty(cronExpression)) + { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy() + { + return misfirePolicy; + } + + public void setMisfirePolicy(String misfirePolicy) + { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent() + { + return concurrent; + } + + public void setConcurrent(String concurrent) + { + this.concurrent = concurrent; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + /** + * 重写toString方法,便于日志打印和调试 + */ + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} \ No newline at end of file diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/domain/SysJobLog.java b/huacai-quartz/src/main/java/com/huacai/quartz/domain/SysJobLog.java new file mode 100644 index 0000000..d061fd7 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/domain/SysJobLog.java @@ -0,0 +1,234 @@ + +package com.huacai.quartz.domain; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.huacai.common.annotation.Excel; +import com.huacai.common.core.domain.BaseEntity; + +/** + * 定时任务调度日志表对应的实体类(映射表sys_job_log) + * 用于记录定时任务每次执行的详细日志信息,包括执行状态、时间、异常等 + * + * @author huacai + */ +public class SysJobLog extends BaseEntity // 继承基础实体类,复用通用字段(如创建时间等) +{ + // 序列化版本号,用于对象序列化时的版本控制 + private static final long serialVersionUID = 1L; + + /** 日志ID(主键) */ + @Excel(name = "日志序号") // Excel导出时的列名配置 + private Long jobLogId; + + /** 任务名称(关联sys_job表的jobName) */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名(关联sys_job表的jobGroup,用于分类) */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串(记录任务执行的目标方法,与任务定义一致) */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** 日志信息(记录任务执行的简要描述,如"执行成功") */ + @Excel(name = "日志信息") + private String jobMessage; + + /** 执行状态(0正常 1失败,用于快速标识任务执行结果) */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") // 导出时自动转换编码为文字描述 + private String status; + + /** 异常信息(当执行失败时,记录具体的异常堆栈信息,便于排查问题) */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** 开始时间(任务实际开始执行的时间) */ + private Date startTime; + + /** 停止时间(任务实际执行结束的时间) */ + private Date stopTime; + + /** + * 获取日志ID + * @return 日志ID + */ + public Long getJobLogId() + { + return jobLogId; + } + + /** + * 设置日志ID + * @param jobLogId 日志ID + */ + public void setJobLogId(Long jobLogId) + { + this.jobLogId = jobLogId; + } + + /** + * 获取任务名称 + * @return 任务名称 + */ + public String getJobName() + { + return jobName; + } + + /** + * 设置任务名称 + * @param jobName 任务名称 + */ + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + /** + * 获取任务组名 + * @return 任务组名 + */ + public String getJobGroup() + { + return jobGroup; + } + + /** + * 设置任务组名 + * @param jobGroup 任务组名 + */ + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + /** + * 获取调用目标字符串 + * @return 调用目标字符串 + */ + public String getInvokeTarget() + { + return invokeTarget; + } + + /** + * 设置调用目标字符串 + * @param invokeTarget 调用目标字符串 + */ + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + /** + * 获取日志信息 + * @return 日志信息 + */ + public String getJobMessage() + { + return jobMessage; + } + + /** + * 设置日志信息 + * @param jobMessage 日志信息 + */ + public void setJobMessage(String jobMessage) + { + this.jobMessage = jobMessage; + } + + /** + * 获取执行状态(0正常 1失败) + * @return 执行状态 + */ + public String getStatus() + { + return status; + } + + /** + * 设置执行状态(0正常 1失败) + * @param status 执行状态 + */ + public void setStatus(String status) + { + this.status = status; + } + + /** + * 获取异常信息 + * @return 异常信息 + */ + public String getExceptionInfo() + { + return exceptionInfo; + } + + /** + * 设置异常信息 + * @param exceptionInfo 异常信息 + */ + public void setExceptionInfo(String exceptionInfo) + { + this.exceptionInfo = exceptionInfo; + } + + /** + * 获取开始时间 + * @return 开始时间 + */ + public Date getStartTime() + { + return startTime; + } + + /** + * 设置开始时间 + * @param startTime 开始时间 + */ + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + /** + * 获取停止时间 + * @return 停止时间 + */ + public Date getStopTime() + { + return stopTime; + } + + /** + * 设置停止时间 + * @param stopTime 停止时间 + */ + public void setStopTime(Date stopTime) + { + this.stopTime = stopTime; + } + + /** + * 重写toString方法,使用多行动格式输出对象信息 + * 便于日志打印和调试时查看对象详情 + */ + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getStopTime()) + .toString(); + } +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/mapper/SysJobLogMapper.java b/huacai-quartz/src/main/java/com/huacai/quartz/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..9c4633c --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/mapper/SysJobLogMapper.java @@ -0,0 +1,64 @@ +package com.huacai.quartz.mapper; + +import java.util.List; +import com.huacai.quartz.domain.SysJobLog; + +/** + * 调度任务日志信息 数据层 + * + * @author huacai + */ +public interface SysJobLogMapper +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll(); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * @return 结果 + */ + public int insertJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/mapper/SysJobMapper.java b/huacai-quartz/src/main/java/com/huacai/quartz/mapper/SysJobMapper.java new file mode 100644 index 0000000..cf1970e --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/mapper/SysJobMapper.java @@ -0,0 +1,67 @@ +package com.huacai.quartz.mapper; + +import java.util.List; +import com.huacai.quartz.domain.SysJob; + +/** + * 调度任务信息 数据层 + * + * @author huacai + */ +public interface SysJobMapper +{ + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * @return 操作日志集合 + */ + public List selectJobList(SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll(); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * @return 角色对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * @return 结果 + */ + public int deleteJobById(Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteJobByIds(Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int updateJob(SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int insertJob(SysJob job); +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/service/ISysJobLogService.java b/huacai-quartz/src/main/java/com/huacai/quartz/service/ISysJobLogService.java new file mode 100644 index 0000000..8c302a1 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/service/ISysJobLogService.java @@ -0,0 +1,56 @@ +package com.huacai.quartz.service; + +import java.util.List; +import com.huacai.quartz.domain.SysJobLog; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author huacai + */ +public interface ISysJobLogService +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/service/ISysJobService.java b/huacai-quartz/src/main/java/com/huacai/quartz/service/ISysJobService.java new file mode 100644 index 0000000..3fabf12 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/service/ISysJobService.java @@ -0,0 +1,102 @@ +package com.huacai.quartz.service; + +import java.util.List; +import org.quartz.SchedulerException; +import com.huacai.common.exception.job.TaskException; +import com.huacai.quartz.domain.SysJob; + +/** + * 定时任务调度信息信息 服务层 + * + * @author huacai + */ +public interface ISysJobService +{ + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public boolean run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/service/impl/SysJobLogServiceImpl.java b/huacai-quartz/src/main/java/com/huacai/quartz/service/impl/SysJobLogServiceImpl.java new file mode 100644 index 0000000..715cc2d --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/service/impl/SysJobLogServiceImpl.java @@ -0,0 +1,87 @@ +package com.huacai.quartz.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.huacai.quartz.domain.SysJobLog; +import com.huacai.quartz.mapper.SysJobLogMapper; +import com.huacai.quartz.service.ISysJobLogService; + +/** + * 定时任务调度日志信息 服务层 + * + * @author huacai + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService +{ + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) + { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) + { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) + { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) + { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) + { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() + { + jobLogMapper.cleanJobLog(); + } +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/service/impl/SysJobServiceImpl.java b/huacai-quartz/src/main/java/com/huacai/quartz/service/impl/SysJobServiceImpl.java new file mode 100644 index 0000000..624ccf6 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/service/impl/SysJobServiceImpl.java @@ -0,0 +1,296 @@ +package com.huacai.quartz.service.impl; + +import java.util.List; +import javax.annotation.PostConstruct; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.huacai.common.constant.ScheduleConstants; +import com.huacai.common.exception.job.TaskException; +import com.huacai.quartz.domain.SysJob; +import com.huacai.quartz.mapper.SysJobMapper; +import com.huacai.quartz.service.ISysJobService; +import com.huacai.quartz.util.CronUtils; +import com.huacai.quartz.util.ScheduleUtils; + +/** + * 定时任务调度信息服务层实现类 + * 负责定时任务的CRUD、状态管理、调度控制等核心业务逻辑 + * + * @author huacai + */ +@Service // 标识为Spring服务组件 +public class SysJobServiceImpl implements ISysJobService // 实现定时任务服务接口 +{ + @Autowired // 自动注入Quartz调度器实例 + private Scheduler scheduler; + + @Autowired // 自动注入任务数据访问层接口 + private SysJobMapper jobMapper; + + /** + * 项目启动时初始化定时器 + * 作用:将数据库中配置的任务同步到Quartz调度器中 + * 注意:禁止手动修改数据库中的任务ID和任务组名,否则会导致数据不一致 + * @throws SchedulerException Quartz调度器异常 + * @throws TaskException 任务处理异常 + */ + @PostConstruct // 标注此方法在Bean初始化后自动执行 + public void init() throws SchedulerException, TaskException + { + scheduler.clear(); // 清空调度器中已有的任务(防止重复加载) + List jobList = jobMapper.selectJobAll(); // 查询数据库中所有任务配置 + for (SysJob job : jobList) + { + // 为每个任务创建Quartz调度任务 + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * 支持根据条件查询任务 + * @param job 包含查询条件的任务对象 + * @return 符合条件的任务列表 + */ + @Override + public List selectJobList(SysJob job) + { + // 调用Mapper层查询任务列表 + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) + { + // 调用Mapper层根据ID查询任务 + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * 同时更新数据库状态和Quartz调度器中的任务状态 + * @param job 调度信息 + * @return 影响的行数 + * @throws SchedulerException Quartz调度器异常 + */ + @Override + @Transactional(rollbackFor = Exception.class) // 声明事务,发生异常时回滚 + public int pauseJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); // 获取任务ID + String jobGroup = job.getJobGroup(); // 获取任务组名 + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); // 设置任务状态为"暂停" + int rows = jobMapper.updateJob(job); // 更新数据库中的任务状态 + if (rows > 0) // 数据库更新成功后,同步更新调度器中的任务状态 + { + // 暂停Quartz中的任务 + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; // 返回数据库更新影响的行数 + } + + /** + * 恢复任务 + * 同时更新数据库状态和Quartz调度器中的任务状态 + * @param job 调度信息 + * @return 影响的行数 + * @throws SchedulerException Quartz调度器异常 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); // 设置任务状态为"正常" + int rows = jobMapper.updateJob(job); // 更新数据库状态 + if (rows > 0) // 同步更新调度器状态 + { + // 恢复Quartz中的任务 + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务 + * 同时删除数据库记录和Quartz调度器中的任务(包括关联的trigger) + * @param job 调度信息 + * @return 影响的行数 + * @throws SchedulerException Quartz调度器异常 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); // 从数据库删除任务 + if (rows > 0) // 同步删除调度器中的任务 + { + // 删除Quartz中的任务(会自动删除关联的trigger) + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * @param jobIds 需要删除的任务ID数组 + * @throws SchedulerException Quartz调度器异常 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds(Long[] jobIds) throws SchedulerException + { + for (Long jobId : jobIds) // 遍历每个任务ID + { + SysJob job = jobMapper.selectJobById(jobId); // 查询任务详情 + deleteJob(job); // 调用单条删除方法 + } + } + + /** + * 任务调度状态修改(切换启用/暂停状态) + * @param job 调度信息(包含目标状态) + * @return 影响的行数 + * @throws SchedulerException Quartz调度器异常 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus(SysJob job) throws SchedulerException + { + int rows = 0; + String status = job.getStatus(); // 获取目标状态 + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) + { + // 目标状态为"正常",执行恢复任务操作 + rows = resumeJob(job); + } + else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) + { + // 目标状态为"暂停",执行暂停任务操作 + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务(无视Cron表达式,触发一次执行) + * @param job 调度信息 + * @return 执行结果(true表示成功,false表示任务不存在) + * @throws SchedulerException Quartz调度器异常 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run(SysJob job) throws SchedulerException + { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + // 查询任务完整信息(包含最新配置) + SysJob properties = selectJobById(job.getJobId()); + // 构建任务参数映射(用于传递任务信息到执行器) + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + // 获取Quartz中的任务标识 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + // 检查任务是否存在 + if (scheduler.checkExists(jobKey)) + { + result = true; + // 立即触发任务执行,并传递参数 + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * 同时保存到数据库和Quartz调度器 + * @param job 调度信息 + * @return 影响的行数 + * @throws SchedulerException Quartz调度器异常 + * @throws TaskException 任务处理异常(如Cron表达式无效) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob(SysJob job) throws SchedulerException, TaskException + { + // 新任务默认状态为"暂停"(需手动启用) + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); // 保存到数据库 + if (rows > 0) // 数据库保存成功后,同步到调度器 + { + // 创建Quartz调度任务 + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的配置信息(包括时间表达式等) + * @param job 调度信息(包含更新后的配置) + * @return 影响的行数 + * @throws SchedulerException Quartz调度器异常 + * @throws TaskException 任务处理异常 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob(SysJob job) throws SchedulerException, TaskException + { + // 查询数据库中当前任务的旧配置(主要获取旧的任务组名) + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); // 更新数据库配置 + if (rows > 0) // 数据库更新成功后,同步更新调度器 + { + // 更新Quartz中的任务配置(使用旧的任务组名进行定位) + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 内部方法:更新Quartz调度器中的任务配置 + * @param job 包含新配置的任务对象 + * @param jobGroup 旧的任务组名(用于定位原有任务) + * @throws SchedulerException Quartz调度器异常 + * @throws TaskException 任务处理异常 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException + { + Long jobId = job.getJobId(); + // 获取原有任务的标识 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + // 检查任务是否存在 + if (scheduler.checkExists(jobKey)) + { + // 先删除原有任务(避免配置冲突) + scheduler.deleteJob(jobKey); + } + // 基于新配置创建任务 + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * @param cronExpression 待校验的Cron表达式 + * @return 校验结果(true表示有效,false表示无效) + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) + { + // 调用工具类进行Cron表达式校验 + return CronUtils.isValid(cronExpression); + } +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/task/RyTask.java b/huacai-quartz/src/main/java/com/huacai/quartz/task/RyTask.java new file mode 100644 index 0000000..10dabab --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/task/RyTask.java @@ -0,0 +1,37 @@ +package com.huacai.quartz.task; + +import org.springframework.stereotype.Component; +import com.huacai.common.utils.StringUtils; + +/** + * 定时任务调度 + * + * @author huacai + * @optimization 郑长川(花菜菜) + */ +@Component("ryTask") +public class RyTask +{ +// @Resource +// private IJdzsService jdzsService; +// +// public void selectJdzs(String jdzsId) { +// Jdzs jdzs = jdzsService.selectJdzsByJdzsId(jdzsId); +// System.out.println(jdzs); +// } + + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) + { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) + { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams() + { + System.out.println("执行无参方法"); + } +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/util/AbstractQuartzJob.java b/huacai-quartz/src/main/java/com/huacai/quartz/util/AbstractQuartzJob.java new file mode 100644 index 0000000..d24cba0 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/util/AbstractQuartzJob.java @@ -0,0 +1,141 @@ +package com.huacai.quartz.util; + +import java.util.Date; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.huacai.common.constant.Constants; +import com.huacai.common.constant.ScheduleConstants; +import com.huacai.common.utils.ExceptionUtil; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.bean.BeanUtils; +import com.huacai.common.utils.spring.SpringUtils; +import com.huacai.quartz.domain.SysJob; +import com.huacai.quartz.domain.SysJobLog; +import com.huacai.quartz.service.ISysJobLogService; + +/** + * 抽象quartz任务执行的抽象基类 + * 封装任务执行的通用流程(前置处理、后置处理、日志记录等),具体任务逻辑由子类实现 + * + * @author huacai + */ +public abstract class AbstractQuartzJob implements Job // 实现Quartz的Job接口,成为可调度任务 +{ + // 日志记录器,用于记录任务执行过程中的日志信息 + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + * 用于存储当前线程中任务的开始时间,避免多线程环境下的时间混乱 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + /** + * 任务执行的入口方法(实现Job接口的execute方法) + * 定义任务执行的完整流程:前置处理 -> 核心执行 -> 后置处理(含异常处理) + * + * @param context Quartz的任务执行上下文,包含任务相关的环境信息 + * @throws JobExecutionException 任务执行异常 + */ + @Override + public void execute(JobExecutionContext context) throws JobExecutionException + { + // 创建任务对象,用于接收上下文传递的任务配置 + SysJob sysJob = new SysJob(); + // 从上下文的参数映射中获取任务配置,并复制到sysJob对象 + // ScheduleConstants.TASK_PROPERTIES是存储任务配置的键 + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try + { + // 执行前置处理(如记录开始时间) + before(context, sysJob); + // 若任务配置有效,则执行核心任务逻辑 + if (sysJob != null) + { + doExecute(context, sysJob); // 抽象方法,由子类实现具体业务 + } + // 执行后置处理(如记录成功日志),无异常 + after(context, sysJob, null); + } + catch (Exception e) + { + // 记录任务执行异常日志 + log.error("任务执行异常 - :", e); + // 执行后置处理(如记录失败日志),传入异常对象 + after(context, sysJob, e); + } + } + + /** + * 任务执行前的前置处理 + * 目前主要功能:记录任务开始时间到线程本地变量 + * + * @param context 任务执行上下文 + * @param sysJob 系统计划任务配置 + */ + protected void before(JobExecutionContext context, SysJob sysJob) + { + // 存储当前时间(任务开始时间)到线程本地变量 + threadLocal.set(new Date()); + } + + /** + * 任务执行后的后置处理 + * 主要功能:记录任务执行日志(包括执行时间、耗时、状态、异常信息等) + * + * @param context 任务执行上下文 + * @param sysJob 系统计划任务配置 + * @param e 执行过程中抛出的异常(无异常则为null) + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) + { + // 从线程本地变量获取任务开始时间 + Date startTime = threadLocal.get(); + // 移除线程本地变量中的值,避免内存泄漏 + threadLocal.remove(); + + // 创建任务日志对象,记录本次执行详情 + final SysJobLog sysJobLog = new SysJobLog(); + // 设置日志的基本信息(关联的任务名称、组名、调用目标) + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + // 设置开始时间和结束时间 + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + // 计算任务执行耗时(毫秒) + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + + // 根据是否有异常设置任务执行状态 + if (e != null) + { + // 有异常:状态为失败(Constants.FAIL) + sysJobLog.setStatus(Constants.FAIL); + // 获取异常信息并截断(避免日志过长) + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } + else + { + // 无异常:状态为成功(Constants.SUCCESS) + sysJobLog.setStatus(Constants.SUCCESS); + } + + // 通过Spring工具类获取日志服务Bean,将日志写入数据库 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 任务执行的核心方法,由子类具体实现 + * 子类需根据业务需求重写此方法,实现实际的任务逻辑 + * + * @param context 任务执行上下文 + * @param sysJob 系统计划任务配置 + * @throws Exception 执行过程中可能抛出的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} \ No newline at end of file diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/util/CronUtils.java b/huacai-quartz/src/main/java/com/huacai/quartz/util/CronUtils.java new file mode 100644 index 0000000..0c52cd4 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/util/CronUtils.java @@ -0,0 +1,73 @@ +package com.huacai.quartz.util; + +import java.text.ParseException; +import java.util.Date; +import org.quartz.CronExpression; + +/** + * cron表达式工具类 + * 提供Cron表达式的有效性校验、错误信息获取及下次执行时间计算等功能 + * + * @author huacai + * + */ +public class CronUtils +{ + /** + * 校验给定的Cron表达式是否有效 + * Cron表达式是Quartz定时任务的时间规则表达式,格式错误会导致任务无法调度 + * + * @param cronExpression 需要校验的Cron表达式字符串 + * @return boolean 表达式有效返回true,无效返回false + */ + public static boolean isValid(String cronExpression) + { + // 调用Quartz的CronExpression工具类进行有效性校验 + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 获取Cron表达式的无效原因描述 + * 当表达式无效时,返回具体的错误信息(如格式错误位置),便于问题排查 + * + * @param cronExpression 需要校验的Cron表达式字符串 + * @return String 无效时返回错误描述,有效时返回null + */ + public static String getInvalidMessage(String cronExpression) + { + try + { + // 尝试创建CronExpression对象,若成功则表达式有效 + new CronExpression(cronExpression); + return null; // 有效,返回null + } + catch (ParseException pe) + { + // 解析失败,返回异常信息(包含具体错误原因) + return pe.getMessage(); + } + } + + /** + * 根据给定的Cron表达式计算下一次执行时间 + * 从当前系统时间开始,计算符合Cron规则的最近一次执行时间 + * + * @param cronExpression Cron表达式字符串 + * @return Date 下次执行时间对象,若表达式无效则抛出异常 + */ + public static Date getNextExecution(String cronExpression) + { + try + { + // 创建CronExpression对象(已隐含表达式有效性校验) + CronExpression cron = new CronExpression(cronExpression); + // 计算当前时间之后的下一个有效执行时间 + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } + catch (ParseException e) + { + // 表达式解析失败时,包装为运行时异常抛出 + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/util/JobInvokeUtil.java b/huacai-quartz/src/main/java/com/huacai/quartz/util/JobInvokeUtil.java new file mode 100644 index 0000000..7bd28df --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/util/JobInvokeUtil.java @@ -0,0 +1,238 @@ +package com.huacai.quartz.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.spring.SpringUtils; +import com.huacai.quartz.domain.SysJob; + +/** + * 任务执行工具类 + * 负责解析定时任务的调用目标(invokeTarget),通过反射机制执行目标方法 + * 支持Spring容器中的Bean方法和普通Java类的方法调用 + * + * @author huacai + */ +public class JobInvokeUtil +{ + /** + * 执行定时任务的目标方法 + * 根据任务配置中的invokeTarget解析出Bean名称、方法名和参数,通过反射执行 + * + * @param sysJob 系统任务对象,包含调用目标信息(invokeTarget) + * @throws Exception 执行过程中可能抛出的异常(如类找不到、方法不存在等) + */ + public static void invokeMethod(SysJob sysJob) throws Exception + { + // 获取任务配置的调用目标字符串(格式如:"beanName.methodName(param1, param2)") + String invokeTarget = sysJob.getInvokeTarget(); + // 解析出Bean名称(或类全名) + String beanName = getBeanName(invokeTarget); + // 解析出方法名称 + String methodName = getMethodName(invokeTarget); + // 解析出方法参数列表(包含参数值和参数类型) + List methodParams = getMethodParams(invokeTarget); + + // 判断Bean名称是否为完整类名(含包名,如"com.huacai.service.impl.MyService") + if (!isValidClassName(beanName)) + { + // 非完整类名:从Spring容器中获取Bean实例 + Object bean = SpringUtils.getBean(beanName); + // 调用Bean的目标方法 + invokeMethod(bean, methodName, methodParams); + } + else + { + // 完整类名:通过反射创建类实例(要求类有默认构造方法) + Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); + // 调用类的目标方法 + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 反射调用目标对象的指定方法 + * 根据方法名和参数列表,通过反射执行目标方法 + * + * @param bean 目标对象实例 + * @param methodName 方法名称 + * @param methodParams 方法参数列表(每个元素是包含参数值和类型的数组) + * @throws NoSuchMethodException 方法不存在时抛出 + * @throws SecurityException 安全权限异常 + * @throws IllegalAccessException 方法访问权限不足时抛出 + * @throws IllegalArgumentException 参数不匹配时抛出 + * @throws InvocationTargetException 方法执行内部抛出异常时包装后抛出 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException + { + // 判断是否有方法参数 + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) + { + // 获取方法参数类型数组 + Class[] paramTypes = getMethodParamsType(methodParams); + // 根据方法名和参数类型获取方法对象 + Method method = bean.getClass().getMethod(methodName, paramTypes); + // 获取方法参数值数组 + Object[] paramValues = getMethodParamsValue(methodParams); + // 调用方法并传入参数 + method.invoke(bean, paramValues); + } + else + { + // 无参数:直接根据方法名获取无参方法 + Method method = bean.getClass().getMethod(methodName); + // 调用无参方法 + method.invoke(bean); + } + } + + /** + * 校验字符串是否为完整的类名(含包名) + * 规则:包含至少两个"."(如"com.huacai.MyClass"含两个".") + * + * @param invokeTarget 待校验的字符串(通常是Bean名称或类名) + * @return true=是完整类名,false=不是 + */ + public static boolean isValidClassName(String invokeTarget) + { + // 统计字符串中"."的数量,大于1则认为是完整类名 + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 从调用目标字符串中解析出Bean名称(或类全名) + * 调用目标格式:"beanName.methodName(param...)" 或 "className.methodName(param...)" + * 解析逻辑:截取"("之前的部分,再截取最后一个"."之前的部分 + * + * @param invokeTarget 调用目标字符串 + * @return 解析出的Bean名称或类全名 + */ + public static String getBeanName(String invokeTarget) + { + // 截取"("之前的部分(如"beanName.methodName") + String beanName = StringUtils.substringBefore(invokeTarget, "("); + // 截取最后一个"."之前的部分(如"beanName") + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 从调用目标字符串中解析出方法名称 + * 调用目标格式:"beanName.methodName(param...)" + * 解析逻辑:截取"("之前的部分,再截取最后一个"."之后的部分 + * + * @param invokeTarget 调用目标字符串 + * @return 解析出的方法名称 + */ + public static String getMethodName(String invokeTarget) + { + // 截取"("之前的部分(如"beanName.methodName") + String methodName = StringUtils.substringBefore(invokeTarget, "("); + // 截取最后一个"."之后的部分(如"methodName") + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 从调用目标字符串中解析出方法参数列表 + * 支持字符串、布尔值、长整数、双精度浮点数和整数类型的参数解析 + * + * @param invokeTarget 调用目标字符串 + * @return 方法参数列表,每个元素是{参数值, 参数类型}的数组;无参数则返回null + */ + public static List getMethodParams(String invokeTarget) + { + // 截取"("和")"之间的部分(如"param1, param2") + String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")"); + // 无参数时返回null + if (StringUtils.isEmpty(methodStr)) + { + return null; + } + + // 按逗号分割参数(处理引号内的逗号不分割) + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + // 存储参数信息的列表 + List classs = new LinkedList<>(); + + for (int i = 0; i < methodParams.length; i++) + { + // 去除参数前后的空格 + String str = StringUtils.trimToEmpty(methodParams[i]); + + // 字符串类型:以单引号或双引号开头 + if (StringUtils.startsWithAny(str, "'", "\"")) + { + // 截取引号内的内容作为参数值,类型为String + classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class }); + } + // 布尔类型:值为true或false(不区分大小写) + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) + { + // 转换为Boolean类型 + classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); + } + // 长整数类型:以L结尾(如123L) + else if (StringUtils.endsWith(str, "L")) + { + // 截取L之前的部分转换为Long + classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class }); + } + // 双精度浮点类型:以D结尾(如3.14D) + else if (StringUtils.endsWith(str, "D")) + { + // 截取D之前的部分转换为Double + classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class }); + } + // 默认为整数类型 + else + { + // 转换为Integer + classs.add(new Object[] { Integer.valueOf(str), Integer.class }); + } + } + return classs; + } + + /** + * 从参数列表中提取参数类型数组 + * + * @param methodParams 方法参数列表(每个元素是{参数值, 参数类型}的数组) + * @return 参数类型数组 + */ + public static Class[] getMethodParamsType(List methodParams) + { + // 创建与参数数量相同的Class数组 + Class[] classs = new Class[methodParams.size()]; + int index = 0; + // 遍历参数列表,提取每个参数的类型 + for (Object[] os : methodParams) + { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 从参数列表中提取参数值数组 + * + * @param methodParams 方法参数列表(每个元素是{参数值, 参数类型}的数组) + * @return 参数值数组 + */ + public static Object[] getMethodParamsValue(List methodParams) + { + // 创建与参数数量相同的Object数组 + Object[] classs = new Object[methodParams.size()]; + int index = 0; + // 遍历参数列表,提取每个参数的值 + for (Object[] os : methodParams) + { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} \ No newline at end of file diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/util/QuartzDisallowConcurrentExecution.java b/huacai-quartz/src/main/java/com/huacai/quartz/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..834a83f --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,33 @@ +package com.huacai.quartz.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import com.huacai.quartz.domain.SysJob; + +/** + * 定时任务处理类(禁止并发执行) + * 继承抽象任务基类,实现具体执行逻辑,且通过注解限制同一任务并发执行 + * + * @author huacai + * + */ +// Quartz注解:禁止同一任务实例并发执行 +// 若任务执行时间超过Cron表达式的间隔,下一次执行会等待当前执行完成后再触发 +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob +{ + /** + * 任务执行的核心方法(实现抽象基类的抽象方法) + * 调用任务执行工具类,执行任务配置中指定的目标方法 + * + * @param context Quartz任务执行上下文,包含任务环境信息 + * @param sysJob 系统任务配置对象,包含调用目标等信息 + * @throws Exception 执行过程中可能抛出的异常(会被基类捕获并记录日志) + */ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + // 调用工具类执行任务的目标方法(反射调用) + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/util/QuartzJobExecution.java b/huacai-quartz/src/main/java/com/huacai/quartz/util/QuartzJobExecution.java new file mode 100644 index 0000000..6c2f5c8 --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/util/QuartzJobExecution.java @@ -0,0 +1,28 @@ +package com.huacai.quartz.util; + +import org.quartz.JobExecutionContext; +import com.huacai.quartz.domain.SysJob; + +/** + * 定时任务处理类(允许并发执行) + * 继承抽象任务基类,实现具体执行逻辑,默认支持同一任务的并发执行 + * + * @author huacai + */ +public class QuartzJobExecution extends AbstractQuartzJob +{ + /** + * 任务执行的核心方法(实现抽象基类的抽象方法) + * 负责调用任务执行工具类,执行具体的业务逻辑 + * + * @param context Quartz任务执行上下文,包含任务调度的环境信息(如触发时间、任务数据等) + * @param sysJob 系统任务配置对象,包含任务的调用目标、参数等核心信息 + * @throws Exception 执行过程中可能抛出的异常(会被父类捕获并记录到日志中) + */ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + // 调用任务执行工具类,通过反射执行sysJob中配置的目标方法 + JobInvokeUtil.invokeMethod(sysJob); + } +} \ No newline at end of file diff --git a/huacai-quartz/src/main/java/com/huacai/quartz/util/ScheduleUtils.java b/huacai-quartz/src/main/java/com/huacai/quartz/util/ScheduleUtils.java new file mode 100644 index 0000000..95cc4aa --- /dev/null +++ b/huacai-quartz/src/main/java/com/huacai/quartz/util/ScheduleUtils.java @@ -0,0 +1,192 @@ +package com.huacai.quartz.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.huacai.common.constant.Constants; +import com.huacai.common.constant.ScheduleConstants; +import com.huacai.common.exception.job.TaskException; +import com.huacai.common.exception.job.TaskException.Code; +import com.huacai.common.utils.StringUtils; +import com.huacai.common.utils.spring.SpringUtils; +import com.huacai.quartz.domain.SysJob; + +/** + * 定时任务工具类 + * 提供Quartz定时任务的创建、触发键/任务键生成、策略处理、白名单校验等核心功能 + * + * @author huacai + */ +public class ScheduleUtils +{ + /** + * 根据任务配置获取对应的Quartz任务执行类 + * 依据任务是否允许并发执行,返回不同的实现类 + * + * @param sysJob 执行计划(包含并发执行配置) + * @return 具体的Quartz任务执行类(允许并发/禁止并发) + */ + private static Class getQuartzJobClass(SysJob sysJob) + { + // 判断任务是否允许并发执行("0"表示允许,其他值表示禁止) + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + // 允许并发返回普通执行类,禁止并发返回带并发控制的执行类 + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发器的唯一标识(TriggerKey) + * TriggerKey由任务ID和任务组名组成,用于唯一标识一个触发器 + * + * @param jobId 任务ID + * @param jobGroup 任务组名 + * @return 触发器唯一标识对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) + { + // 触发器名称格式:TASK_CLASS_NAME + 任务ID(TASK_CLASS_NAME为常量前缀) + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务的唯一标识(JobKey) + * JobKey由任务ID和任务组名组成,用于唯一标识一个任务 + * + * @param jobId 任务ID + * @param jobGroup 任务组名 + * @return 任务唯一标识对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) + { + // 任务名称格式:TASK_CLASS_NAME + 任务ID(与触发器名称对应) + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务并注册到Quartz调度器 + * 包含任务详情构建、触发器配置、参数传递、任务状态设置等完整流程 + * + * @param scheduler Quartz调度器实例 + * @param job 任务配置信息(包含Cron表达式、状态等) + * @throws SchedulerException Quartz调度器异常 + * @throws TaskException 任务配置异常(如Cron表达式无效) + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException + { + // 获取任务对应的执行类(允许/禁止并发) + Class jobClass = getQuartzJobClass(job); + + // 从任务配置中提取ID和组名 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + + // 构建任务详情对象(JobDetail),设置唯一标识 + JobDetail jobDetail = JobBuilder.newJob(jobClass) + .withIdentity(getJobKey(jobId, jobGroup)) // 关联任务唯一标识 + .build(); + + // 构建Cron表达式调度器(根据任务配置的Cron表达式) + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + // 处理任务错过执行时的策略(根据任务配置的misfirePolicy) + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 构建触发器(Trigger),关联Cron调度策略和唯一标识 + CronTrigger trigger = TriggerBuilder.newTrigger() + .withIdentity(getTriggerKey(jobId, jobGroup)) // 关联触发器唯一标识 + .withSchedule(cronScheduleBuilder) // 关联Cron调度策略 + .build(); + + // 向任务详情中存入参数(任务配置信息),供执行时使用 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 检查任务是否已存在,若存在则先删除(避免重复注册) + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) + { + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 检查任务的Cron表达式是否有有效的下次执行时间(防止任务已过期) + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) + { + // 将任务详情和触发器注册到调度器 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 若任务状态为"暂停",则注册后立即暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务的错过执行策略(Misfire Policy) + * 根据任务配置的策略类型,配置Cron调度器的错过处理方式 + * + * @param job 任务配置(包含misfirePolicy策略) + * @param cb Cron调度器构建器 + * @return 配置后的Cron调度器构建器 + * @throws TaskException 若策略不支持则抛出异常 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException + { + // 根据任务的misfirePolicy选择对应的处理策略 + switch (job.getMisfirePolicy()) + { + case ScheduleConstants.MISFIRE_DEFAULT: + // 默认策略:遵循Quartz的默认处理方式 + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + // 忽略所有错过的执行,按正常调度继续 + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + // 立即执行一次错过的任务,然后按正常调度继续 + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + // 不处理错过的执行,等待下一个正常调度时间 + return cb.withMisfireHandlingInstructionDoNothing(); + default: + // 未知策略:抛出配置错误异常 + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查任务调用目标是否在白名单内(安全校验) + * 防止恶意调用危险类或方法,限制任务只能调用指定包路径下的类 + * + * @param invokeTarget 任务调用目标字符串(如"beanName.method(param)") + * @return 若在白名单内返回true,否则返回false + */ + public static boolean whiteList(String invokeTarget) + { + // 提取调用目标中的包名部分(方法名之前的内容) + String packageName = StringUtils.substringBefore(invokeTarget, "("); + // 统计包名中的"."数量,判断是否为完整类名(含包路径) + int count = StringUtils.countMatches(packageName, "."); + + if (count > 1) + { + // 完整类名:直接检查是否包含在白名单字符串中 + return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR); + } + else + { + // 非完整类名(Spring Bean名称):获取Bean的实际包路径 + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + String beanPackageName = obj.getClass().getPackage().getName(); + // 检查包路径是否在白名单内,且不包含危险字符串 + return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR) + && !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR); + } + } +} diff --git a/huacai-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/huacai-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..a445ed5 --- /dev/null +++ b/huacai-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + + truncate table sys_job_log + + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + \ No newline at end of file diff --git a/huacai-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/huacai-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..82b5292 --- /dev/null +++ b/huacai-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + + delete from sys_job where job_id in + + #{jobId} + + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + diff --git a/huacai-quartz/target/classes/com/huacai/quartz/controller/SysJobController.class b/huacai-quartz/target/classes/com/huacai/quartz/controller/SysJobController.class new file mode 100644 index 0000000..2fcd58c Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/controller/SysJobController.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/controller/SysJobLogController.class b/huacai-quartz/target/classes/com/huacai/quartz/controller/SysJobLogController.class new file mode 100644 index 0000000..d047f34 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/controller/SysJobLogController.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/domain/SysJob.class b/huacai-quartz/target/classes/com/huacai/quartz/domain/SysJob.class new file mode 100644 index 0000000..c17c294 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/domain/SysJob.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/domain/SysJobLog.class b/huacai-quartz/target/classes/com/huacai/quartz/domain/SysJobLog.class new file mode 100644 index 0000000..018e573 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/domain/SysJobLog.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/mapper/SysJobLogMapper.class b/huacai-quartz/target/classes/com/huacai/quartz/mapper/SysJobLogMapper.class new file mode 100644 index 0000000..f0c104c Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/mapper/SysJobLogMapper.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/mapper/SysJobMapper.class b/huacai-quartz/target/classes/com/huacai/quartz/mapper/SysJobMapper.class new file mode 100644 index 0000000..24ca7e6 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/mapper/SysJobMapper.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/service/ISysJobLogService.class b/huacai-quartz/target/classes/com/huacai/quartz/service/ISysJobLogService.class new file mode 100644 index 0000000..7ed50e7 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/service/ISysJobLogService.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/service/ISysJobService.class b/huacai-quartz/target/classes/com/huacai/quartz/service/ISysJobService.class new file mode 100644 index 0000000..391c5ad Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/service/ISysJobService.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/service/impl/SysJobLogServiceImpl.class b/huacai-quartz/target/classes/com/huacai/quartz/service/impl/SysJobLogServiceImpl.class new file mode 100644 index 0000000..25a6711 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/service/impl/SysJobLogServiceImpl.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/service/impl/SysJobServiceImpl.class b/huacai-quartz/target/classes/com/huacai/quartz/service/impl/SysJobServiceImpl.class new file mode 100644 index 0000000..b050797 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/service/impl/SysJobServiceImpl.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/task/RyTask.class b/huacai-quartz/target/classes/com/huacai/quartz/task/RyTask.class new file mode 100644 index 0000000..89fd88d Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/task/RyTask.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/util/AbstractQuartzJob.class b/huacai-quartz/target/classes/com/huacai/quartz/util/AbstractQuartzJob.class new file mode 100644 index 0000000..8805769 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/util/AbstractQuartzJob.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/util/CronUtils.class b/huacai-quartz/target/classes/com/huacai/quartz/util/CronUtils.class new file mode 100644 index 0000000..0f15b0a Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/util/CronUtils.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/util/JobInvokeUtil.class b/huacai-quartz/target/classes/com/huacai/quartz/util/JobInvokeUtil.class new file mode 100644 index 0000000..d5c3559 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/util/JobInvokeUtil.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/util/QuartzDisallowConcurrentExecution.class b/huacai-quartz/target/classes/com/huacai/quartz/util/QuartzDisallowConcurrentExecution.class new file mode 100644 index 0000000..ac88e90 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/util/QuartzDisallowConcurrentExecution.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/util/QuartzJobExecution.class b/huacai-quartz/target/classes/com/huacai/quartz/util/QuartzJobExecution.class new file mode 100644 index 0000000..4fa61bc Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/util/QuartzJobExecution.class differ diff --git a/huacai-quartz/target/classes/com/huacai/quartz/util/ScheduleUtils.class b/huacai-quartz/target/classes/com/huacai/quartz/util/ScheduleUtils.class new file mode 100644 index 0000000..e87d500 Binary files /dev/null and b/huacai-quartz/target/classes/com/huacai/quartz/util/ScheduleUtils.class differ diff --git a/huacai-quartz/target/classes/mapper/quartz/SysJobLogMapper.xml b/huacai-quartz/target/classes/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..1345282 --- /dev/null +++ b/huacai-quartz/target/classes/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + diff --git a/huacai-quartz/target/classes/mapper/quartz/SysJobMapper.xml b/huacai-quartz/target/classes/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..f161384 --- /dev/null +++ b/huacai-quartz/target/classes/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + +