【若依 | day36 2-底层原理】

若依三大核心组件底层原理解析

拔高原理篇 - 飞书云文档 前三节


在若依框架的生态中,代码生成器、RBAC 权限模型、异步任务管理器是支撑企业级开发的 “三驾马车”—— 它们分别解决了 “重复编码”“权限管控”“异步任务调度” 的核心痛点。但你是否真正理解它们的底层设计逻辑?如何在实战中灵活运用?这篇深度解析将通过问答形式,带你吃透这三大组件的本质与实现。

一、若依代码生成器:为何能做到 “表结构驱动生成代码”?底层设计逻辑是什么?

问题 1:若依代码生成器的 “数据底座” 是什么?如何存储表结构与生成规则?

若依代码生成器的核心是 **“结构化数据 + 模板引擎”**,其 “数据底座” 依赖两张核心表(gen_tablegen_table_column)实现表结构与生成规则的持久化:

1. gen_table(业务表元信息表)—— 存储 “表级配置”
核心字段作用
table_name数据库表名(如tb_node),关联物理表
table_comment表描述(如 “点位表”),用于生成实体类注释、页面标题
class_name生成的实体类名(如Node),支持自定义
module_name生成代码所属模块(如manage),决定代码存放路径(com.ruoyi.manage
gen_path代码生成根路径(默认/),结合模块名形成完整路径
author代码作者(如admin),写入类注释
template_id关联模板 ID(若依支持多模板配置,如 “Vue2 模板”“Vue3 模板”)
2. gen_table_column(字段元信息表)—— 存储 “字段级配置”
核心字段作用
column_name数据库字段名(如node_name
column_type字段类型(如varchar(50)),用于映射 Java 类型(String
column_comment字段描述(如 “点位名称”),生成页面标签、字段注释
is_pk是否主键(1是 /0否),主键字段会自动生成@TableId注解
is_required是否必填(1是 /0否),生成前端表单校验规则
html_type前端组件类型(如input/select/textarea),决定页面渲染组件
dict_type字典类型(如business_type),关联字典表实现下拉框选项

设计逻辑:通过这两张表,若依将 “物理表结构” 转换为 “可配置的元数据”,后续代码生成只需读取元数据,结合模板即可输出代码 —— 这也是 “表结构驱动” 的本质。

问题 2:若依代码生成器的 “模板引擎” 如何工作?模板与数据如何结合?

若依采用Freemarker作为模板引擎(也支持 Velocity),其核心是 “模板文件 + 数据模型 = 生成代码”:

1. 模板文件的结构(以后端为例)

若依的模板存放在resources/templates/generator目录下,按功能划分:

  • java/domain.java.ftl:实体类模板(生成Node.java);
  • java/mapper.java.ftl:Mapper 接口模板;
  • java/service.java.ftl:Service 接口与实现类模板;
  • java/controller.java.ftl:Controller 接口模板;
  • vue/index.vue.ftl:前端列表页模板。
2. 数据模型的构建

生成代码时,若依会将gen_tablegen_table_column的数据封装为Map结构的模型,例如:

java

运行

Map<String, Object> dataModel = new HashMap<>();
dataModel.put("tableName", genTable.getTableName()); // 表名
dataModel.put("className", genTable.getClassName()); // 实体类名
dataModel.put("columns", genTableColumnList); // 字段列表(包含每个字段的配置)
dataModel.put("moduleName", genTable.getModuleName()); // 模块名
3. 模板渲染流程

java

运行

// 获取模板文件
Template template = configuration.getTemplate("java/domain.java.ftl");
// 将数据模型写入模板,生成代码字符串
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
// 将字符串写入文件
FileUtils.writeStringToFile(new File(outputPath + "/Node.java"), writer.toString(), "UTF-8");

核心优势:模板与数据分离,开发者可通过修改模板(如调整 Controller 返回格式)实现自定义代码风格,无需改动生成逻辑。


二、若依 RBAC 权限模型:如何实现从 “菜单权限” 到 “按钮权限” 的细粒度管控?

问题 1:若依 RBAC 的核心表结构是什么?如何实现 “用户 - 角色 - 权限” 的关联?

若依的 RBAC 是 **“用户 - 角色 - 菜单(权限)” 三层模型 **,依赖 5 张核心表实现关联:

1. 基础表关系
表名作用核心关联字段
sys_user用户表user_id(主键)
sys_role角色表role_id(主键)
sys_menu菜单 / 权限表menu_id(主键)
sys_user_role用户 - 角色关联表user_id + role_id
sys_role_menu角色 - 菜单关联表role_id + menu_id
2. sys_menu表的特殊设计(关键!)

sys_menu不仅存储菜单(如 “点位管理”),还存储按钮级权限(如 “新增点位”“删除点位”),通过type字段区分:

  • type=0:目录(如 “系统管理”);
  • type=1:菜单(如 “用户管理”);
  • type=2:按钮 / 权限(如 “sys:user:add”)。

举例:“删除点位” 按钮对应的sys_menu记录:

  • menu_name:删除点位;
  • permissionmanage:node:remove
  • type:2;
  • parent_id:关联 “点位管理” 菜单的menu_id
3. 权限关联逻辑
  • 一个用户可关联多个角色(sys_user_role);
  • 一个角色可关联多个菜单 / 权限(sys_role_menu);
  • 最终用户的权限集合 = 所属角色的权限集合的并集。

问题 2:若依如何在代码层面实现权限校验?从 “登录鉴权” 到 “按钮控制” 的流程是什么?

1. 登录时的权限加载

用户登录成功后,若依会通过SysMenuService查询该用户的所有权限(permission字段),并存储到SecurityContextHolder中:

java

运行

// 获取用户权限列表
List<String> permissions = menuService.selectMenuPermsByUserId(userId);
// 将权限存入Authentication
UsernamePasswordAuthenticationToken authentication = 
    new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
2. 接口级权限校验(@PreAuthorize 注解)

Controller 层通过@PreAuthorize注解校验权限:

java

运行

@PreAuthorize("@ss.hasPermi('manage:node:add')")
@PostMapping("/add")
public AjaxResult add(@RequestBody Node node) {
    return toAjax(nodeService.insertNode(node));
}

其中@ss.hasPermi()是若依自定义的权限校验方法,会对比当前用户的权限集合是否包含manage:node:add

3. 前端按钮级权限控制(v-hasPermi 指令)

前端通过自定义指令v-hasPermi控制按钮显示:

html

预览

<el-button 
  type="primary" 
  icon="Plus" 
  @click="handleAdd" 
  v-hasPermi="['manage:node:add']"
>新增</el-button>

指令内部会读取 Vuex 中存储的权限集合,若不包含该权限则隐藏按钮:

javascript

运行

// v-hasPermi指令实现
export default {
  mounted(el, binding) {
    const { value } = binding;
    const permissions = store.getters.permissions; // 用户权限集合
    if (!permissions.includes(value[0])) {
      el.parentNode.removeChild(el); // 移除无权限的按钮
    }
  }
};
4. 菜单路由的动态生成

前端路由并非硬编码,而是通过后端返回的用户菜单列表动态生成:

javascript

运行

// 获取用户菜单并生成路由
export function generateRoutes() {
  return new Promise((resolve) => {
    menuApi.getRouters().then(res => {
      const asyncRoutes = filterAsyncRoutes(res.data); // 过滤出有权限的路由
      router.addRoutes(asyncRoutes); // 动态添加路由
      resolve(asyncRoutes);
    });
  });
}

核心价值:从 “URL 拦截” 到 “按钮显示” 的全链路权限管控,避免越权操作。


三、若依异步任务管理器:基于 Quartz 的任务调度如何保证可靠执行与监控?

问题 1:若依异步任务管理器的底层依赖是什么?如何存储任务配置与执行日志?

若依的异步任务管理器基于Quartz 框架实现,同时扩展了任务配置表和日志表,确保任务可配置、可监控:

1. 核心依赖

xml

<!-- Quartz核心依赖 -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>
<!-- Spring整合Quartz -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2. 任务配置表:sys_job
核心字段作用
job_id任务 ID(主键)
job_name任务名称
job_group任务分组(默认DEFAULT
invoke_target执行目标(如com.ruoyi.quartz.task.NodeTask::syncNodeData
cron_expressionCron 表达式(如0 0/5 * * * ?,每 5 分钟执行)
status任务状态(0正常 /1暂停)
concurrent是否并发(0允许 /1禁止)
3. 任务日志表:sys_job_log

记录任务执行结果:job_log_id(日志 ID)、job_id(关联任务)、status(执行状态)、error_msg(异常信息)、times(执行耗时)等。

问题 2:若依如何实现任务的 “动态调度”?暂停、恢复、立即执行的底层逻辑是什么?

1. 任务的初始化与启动

项目启动时,若依会读取sys_job中状态为 “正常” 的任务,通过 Quartz 的 API 创建JobDetailTrigger

java

运行

// 创建JobDetail(绑定任务执行类)
JobDetail jobDetail = JobBuilder.newJob(QuartzJobExecution.class)
    .withIdentity(job.getJobName(), job.getJobGroup())
    .build();
// 设置JobDataMap(存储执行目标)
jobDetail.getJobDataMap().put("invokeTarget", job.getInvokeTarget());
// 创建CronTrigger(绑定Cron表达式)
CronTrigger trigger = TriggerBuilder.newTrigger()
    .withIdentity(job.getJobName(), job.getJobGroup())
    .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
    .build();
// 将任务添加到调度器
scheduler.scheduleJob(jobDetail, trigger);

其中QuartzJobExecution是若依自定义的任务执行类,负责反射调用invoke_target指定的方法。

2. 任务的暂停与恢复
  • 暂停任务:通过调度器暂停 Trigger;

    java

    运行

    scheduler.pauseTrigger(TriggerKey.triggerKey(job.getJobName(), job.getJobGroup()));
    
  • 恢复任务:恢复 Trigger 的状态;

    java

    运行

    scheduler.resumeTrigger(TriggerKey.triggerKey(job.getJobName(), job.getJobGroup()));
    
3. 任务的立即执行

无需等待 Cron 表达式触发,直接通过triggerJob方法执行:

java

运行

scheduler.triggerJob(JobKey.jobKey(job.getJobName(), job.getJobGroup()));
4. 任务执行的异常处理

若任务执行抛出异常,会捕获并记录到sys_job_log

java

运行

try {
    // 反射执行任务方法
    MethodInvokeUtil.invokeMethod(invokeTarget);
    // 记录成功日志
    jobLog.setStatus("0");
} catch (Exception e) {
    // 记录失败日志
    jobLog.setStatus("1");
    jobLog.setErrorMsg(e.getMessage());
} finally {
    jobLogService.insertJobLog(jobLog);
}

核心优势:任务配置可视化(页面可编辑 Cron 表达式)、执行状态可追溯(日志记录)、支持动态调整(无需重启项目)。

总结:三大组件的协同价值

若依的代码生成器、RBAC、异步任务管理器并非孤立存在 —— 代码生成器可一键生成带权限注解的业务代码,RBAC 保障接口与按钮的权限安全,异步任务管理器支撑定时业务(如点位数据同步),三者共同构建了 “高效开发 + 安全管控 + 可靠调度” 的企业级开发体系。理解它们的底层设计,不仅能灵活使用若依,更能借鉴其思想构建自己的业务框架。

<think>嗯,用户想了解达梦数据库中是否有类似DB2的last_day函数的能。根据引用内容,DB2的last_day函数用于获取当前日期所在月份的最后一天,例如last_day(sysdate)返回当月的最后一天。 从引用来看,达梦数据库的文档并没有直接提到last_day函数。不过引用中提到了DBMS_STATS包的使用,这是达梦的性能优化能,与日期函数无关。引用中倒是提到了DB2的日期函数实现逻辑:last_day(sysdate)等价于(current date + 1 month) - day(current date +1 month) days。这个逻辑很清晰:先获取下个月的第一天,然后减去一天就是当月的最后一天。 既然用户明确需要达梦的等效方案,我需要考虑达梦的日期处理能力。达梦作为国产数据库,兼容Oracle语法,应该支持日期计算。可以这样实现: 1. 先用add_months获取下个月的同一天 2. 用trunc截断到月初 3. 然后减去1天 写成SQL表达式就是:trunc(add_months(sysdate,1),'MM') - 1。这里trunc(日期,'MM')能直接得到当月第一天,所以加一个月再减一天正好是当月最后一天。 用户可能还会需要first_day能,虽然没问但可以一并提供。在达梦里更简单:trunc(sysdate,'MM')就能返回当月第一天。 需要提醒用户的是,虽然达梦没有内置last_day函数,但完全可以用现有函数组合实现同等效果,性能也应该不错。毕竟引用里提到DB2底层也是类似计算的。 考虑到用户之前问过性能优化问题(引用提到统计信息采集耗时),说明可能是DBA或开发者,所以示例要给出完整可执行的SQL格式。 </think>在达梦数据库中没有直接命名为 `LAST_DAY` 的内置函数,但可以通过**日期计算组合**实现与 DB2 的 `LAST_DAY` 完全相同的能: ### 实现方案(达梦 7.0+ 适用) ```sql SELECT TRUNC(ADD_MONTHS(SYSDATE, 1), 'MM') - 1 AS LAST_DAY; -- 当月最后一天 ``` **等价于 DB2 的**: ```sql VALUES (LAST_DAY(CURRENT DATE)); -- DB2 语法[^3] ``` --- ### 原理解析 1. **`ADD_MONTHS(SYSDATE, 1)`** 将当前日期增加 1 个月(如 `2023-05-15` → `2023-06-15`) 2. **`TRUNC(..., 'MM')`** 截断到下个月的**第一天**(`2023-06-15` → `2023-06-01`) 3. **`- 1`** 向前减 1 天得到**当月最后一天**(`2023-06-01` → `2023-05-31`) --- ### 扩展:获取当月第一天 ```sql SELECT TRUNC(SYSDATE, 'MM') AS FIRST_DAY; -- 达梦语法 ``` 等价于 DB2 的 `FIRST_DAY`[^3] --- ### 性能说明 此计算仅涉及简单日期运算,**无全表扫描**,性能远优于引用[^1]中的统计信息采集操作(`DBMS_STATS.GATHER_SCHEMA_STATS`),可安全用于高频查询。 --- ### 示例验证 ```sql -- 达梦语法示例 SELECT SYSDATE AS 当前日期, TRUNC(SYSDATE, 'MM') AS 当月第一天, TRUNC(ADD_MONTHS(SYSDATE, 1), 'MM') - 1 AS 当月最后一天; ``` 输出: | 当前日期 | 当月第一天 | 当月最后一天 | |------------|------------|--------------| | 2023-05-15 | 2023-05-01 | 2023-05-31 | --- ### 对比其他数据库方案 | 数据库 | 函数 | 等价实现 | |--------------|---------------------|---------------------------------------------| | **达梦** | 无原生函数 | `TRUNC(ADD_MONTHS(date,1),'MM')-1` | | **Oracle** | `LAST_DAY(date)` | 原生支持 | | **DB2** | `LAST_DAY(date)` | 原生支持[^3] | | **MySQL** | `LAST_DAY(date)` | 原生支持 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值