若依框架
什么使用若依框架
- 快速开发:若依框架提供了许多现成的模块和工具,能够快速搭建起一个基本的应用框架,从而减少了开发的时间和工作量
- 高度可定制性:若依框架提供了丰富的插件和扩展点,可以根据项目的需求进行定制和扩展,满足不同项目的特定需求
- 简化开发流程:若依框架提供了许多常用的功能模块,如权限管理、数据字典、定时任务等,能够简化开发流程,提高开发效率
- 易于维护和管理:若依框架采用了模块化的设计,代码结构清晰,易于维护和管理,有助于降低项目的维护成本
- 社区支持和更新迭代:若依框架有一个活跃的开发社区,能够及时提供技术支持和更新迭代,保证项目的稳定性和安全性
若依框架基本介绍
若依(RuoYi)框架是一款基于SpringBoot、SpringCloud等开源框架搭建的企业级开发平台。它的主要目标是提供全面的解决方案,以简化企业级应用开发,提高开发效率。以下是对若依框架的详细介绍:
- 模块化设计:若依框架采用模块化的设计,使得应用可以根据需求进行灵活的扩展和定制。每个模块都可以独立部署和升级,这大大提高了系统的可维护性和可扩展性
- 前后端分离:框架采用前后端分离的开发模式,后端专注于数据处理和API接口的提供,前端负责页面的展示和用户交互。这种模式使得前后端职责明确,进一步提高了开发效率和代码的可维护性
- 权限管理:若依框架提供了完善的用户权限管理功能,包括角色管理、菜单授权、部门管理等,便于对系统权限进行灵活的配置和管理
- 代码生成器:框架内置代码生成器,能够快速生成前后端代码,减少重复劳动,提高开发效率。通过简单的配置即可生成基础的增删改查代码
- 定时任务:若依框架集成了定时任务模块,支持动态添加定时任务,可以通过界面对定时任务进行管理与调度。此外,框架还能实时查看系统运行日志和在线用户信息,监控服务状态和性能
- 易于集成:若依框架可以轻松地与其他常用技术栈集成,如MySQL、Redis、消息队列等。这使得开发者可以根据项目需求灵活地选择合适的技术组件,降低技术门槛和成本
此外,若依框架还集成了许多常用功能模块,如文件上传和下载、消息推送、数据字典、日志管理等等
总的来说,若依框架是一款功能丰富、易于扩展和定制的企业级开发平台,适用于各种Web应用程序的开发。它能够帮助开发者快速构建功能完备的应用,提高开发效率,降低维护成本
官网地址:RuoYi 若依官方网站
官方文档地址:介绍 | RuoYi
若依框架分为四个版本:
- 前后端不分离版RuoYi(单体)
- 前后端分离版RuoYi-Vue(单体)
- 微服务版RuoYi-Cloud
- 移动端版RuoYi-App
RuoYi-Vue快速入门
官方文档地址:介绍 | RuoYi-Vue
技术选型
由于若依框架中的某些技术版本和当前项目的版本不一致,所以需要对其单独进行修改:

准备工作
| 官方推荐 | 优化升级 |
|---|---|
| JDK >= 1.8 (推荐1.8版本) | 升级为JDK 11 资料中提供了安装包 |
| Maven >= 3.0 | 不需要安装,直接使用IDEA中自带的Maven即可 |
| Node >= 12 | 前端代码由vue2升级为vue3,需要Node 18及以上的版本 |
| Mysql >= 5.7.0 (推荐5.7版本) | 升级为MySQL 8 |
| Redis >= 3.0 | 本地启动 |
资料中的CentOS运行在VMWare中,系统中已经在Docker容器中部署了MySQL和Redis
资料的下载地址:https://pan.baidu.com/s/1qel7_7nCNzTrqjlZAh-goA?pwd=ez4h
Centos用户名root,密码:1234
| 软件名称 | 说明 |
|---|---|
| MySQL | 默认用户名:root,默认密码:heima123,端口:3306 |
| Redis | 默认密码:123456,端口:6379 |
运行后端项目
- 代码下载地址:https://gitee.com/y_project/RuoYi-Vue
- 由于若依提供的代码的包名和项目名和需求不一致,因此这里暂时不要采用Git克隆的方式下载代码,直接下载ZIP包到本地,然后先替换所有的包名和项目名,再使用IDEA打开项目
包名全局替换
- 从官方下载的代码中所有包名都为com.rouyi,且数据库连接信息需要修改,此时,可以借助若依框架修改器来一键修改
- 参考链接:https://gitee.com/lpf_project/RuoYi-MT/releases
- 具体修改如下图所示:

如果执行时出现拒绝访问的错误,那就把下载的压缩包放到桌面再执行
项目初始化
修改完毕后会生成一个名称为一串数字的文件夹,文件夹内部的zzyl就是替换后的正确项目
在IDEA中打开项目后,还需要进行以下操作:
- 在父工程pom文件中修改JDK版本:
<!-- <java.version>1.8</java.version> 原JDK版本 -->
<java.version>11</java.version>
- 打开
Project Structure,将SDK设置为11

- 在setting中找到Compiler > Java Compiler,查看版本是否为11

- 在setting中找到
Maven > Runner,检查JRE版本是否为11

- 在setting中找到
File Encodings,确保项目编码为UTF-8

- 配置MySQL数据库连接的设置,配置文件的位置在
zzyl-admin > src > main > resource > application-druid.yml - 配置Redis数据库连接的设置,配置文件的位置在
zzyl-admin > src > main > resource > application.yml - 执行sql目录下的ry_xxxx.sql,建议在文件开头加上
create database zzyl;先创建数据库,数据导入完成后如图所示:

- 启动项目的引导类在
zzyl-admin中,右键RuoYiApplication启动项目
后端项目上传到Gitee
项目运行起来后,要将代码托管到gitee仓库中,方便维护
先将当前的master分支上传到gitee,然后在master分支上创建一个develop分支;将来,每一章的业务开发都要在develop分支上创建出一个子分支,子分支的命名规则为:dev+章数
每一章的代码在子分支测试完毕后,要合并到develop分支,下一章再从develop分支创建出一个新的分支。
运行前端项目
在运行后端项目这一章,下载的项目文件中(zzyl-ui)就包含了前端代码,但是这里的前端代码是基于Vue2的,而当前项目需要基于Vue3进行开发
官方地址:RuoYi-Vue3: :tada: (RuoYi),访问链接下载基于Vue3框架的前端代码
现在完成后,进入项目中,执行npm install安装所需依赖
访问vite.config.js查看前端项目的配置,其中包含启动端口、访问后端路径等
将前端项目上传到gitee
项目运行起来后,要将代码托管到gitee仓库中,方便维护
先将当前的master分支上传到gitee,然后在master分支上创建一个develop分支;将来,每一章的业务开发都要在develop分支上创建出一个子分支,子分支的命名规则为:dev+章数
每一章的代码在子分支测试完毕后,要合并到develop分支,下一章再从develop分支创建出一个新的分支
前后端项目结构
后端代码
代码结构

以上是后端代码结构,官方文档的介绍:
com.ruoyi
├── ruoyi-admin // 后台服务 存放 controller
├── common // 工具类
│ └── annotation // 自定义注解
│ └── config // 全局配置
│ └── constant // 通用常量
│ └── core // 核心控制
│ └── enums // 通用枚举
│ └── exception // 通用异常
│ └── filter // 过滤器处理
│ └── utils // 通用类处理
├── framework // 框架核心
│ └── aspectj // 注解实现
│ └── config // 系统配置
│ └── datasource // 数据权限
│ └── interceptor // 拦截器
│ └── manager // 异步处理
│ └── security // 权限控制
│ └── web // 前端控制
├── ruoyi-generator // 代码生成(可移除)
├── ruoyi-quartz // 定时任务(可移除)
├── ruoyi-system // 系统代码 存放 domain,mapper,service
├── ruoyi-xxxxxx // 其他模块
模块之间的依赖关系:

项目中的配置
项目中的配置文件都在ruoyi-admin模块下,如下图:

- i18n:国际化处理
- META-INF:存储了项目的元信息(描述数据的数据),无需修改
- mybatis:mybatis相关的配置信息
- application.yml:项目中的核心配置
- application-druid.yml:数据库连接的配置
- banner.txt:默认的banner图标信息,项目启动,控制台打印显示
- logback.xml:日志配置
最主要的两个配置文件:application.yml和application-druid.yml
表结构
若依中默认提供了19张表,可以先简单熟悉一下每个表代表的含义:

前端代码
代码结构

详细目录或文件说明:
├── bin // 执行脚本
├── public // 公共文件
│ ├── favicon.ico // favicon图标
├── src // 源代码
│ ├── api // 所有请求,后台的api接口,开发时经常操作的目录
│ ├── assets // 主题 字体等静态资源
│ ├── components // 全局公用组件
│ ├── directive // 全局指令
│ ├── layout // 布局
│ ├── plugins // 通用方法
│ ├── router // 路由
│ ├── store // 全局 store管理
│ ├── utils // 全局公用方法
│ ├── views // view,存储vue组件,开发时经常操作的目录
│ ├── App.vue // 根组件
│ ├── main.js // 入口 加载组件 初始化等
│ ├── permission.js // 权限管理
│ └── settings.js // 系统配置
├── vite // 前端构建工具
├── .env.development // 开发环境配置
├── .env.production // 生产环境配置
├── .env.staging // 测试环境配置
├── .gitignore // git 忽略项
├── LICENSE // 许可证
├── package-lock.json // 锁定项目依赖的具体版本号
├── package.json // 配置项目的信息、名称、版本号、描述信息等
├── pnpm-lock.yaml // 锁定项目依赖的具体版本号
└── vite.config.js // 用于配置 Vue.js 项目的全局选项,可修改后台访问接口的路径
核心配置
在开发阶段,配置的修改是较少的,主要关于一个配置文件即可—>vite.config.js:
import { defineConfig, loadEnv } from 'vite'
import path from 'path'
import createVitePlugins from './vite/plugins'
const baseUrl = 'http://localhost:8080' // 后端接口
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd())
const { VITE_APP_ENV } = env
return {
// 部署生产环境和开发环境下的URL。
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
base: VITE_APP_ENV === 'production' ? '/' : '/',
plugins: createVitePlugins(env, command === 'build'),
resolve: {
// https://cn.vitejs.dev/config/#resolve-alias
alias: {
// 设置路径
'~': path.resolve(__dirname, './'),
// 设置别名
'@': path.resolve(__dirname, './src')
},
// https://cn.vitejs.dev/config/#resolve-extensions
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// 打包配置
build: {
// https://vite.dev/config/build-options.html
sourcemap: command === 'build' ? false : 'inline',
outDir: 'dist',
assetsDir: 'assets',
chunkSizeWarningLimit: 2000,
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
},
// vite 相关配置
server: {
port: 80,
host: true,
open: true,
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
target: baseUrl,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
},
// springdoc proxy
'^/v3/api-docs/(.*)': {
target: baseUrl,
changeOrigin: true,
}
}
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove()
}
}
}
}
]
}
}
}
})
功能快速开发
若依提供的代码生成功能,可以基于表结构快速生成前后端的代码
准备工作
以中州养老项目为例,首先开发护理项目模块,先要准备两部分内容:一个是一级菜单,另一个是表结构
菜单
对于护理模块而言,前端展示中是一个子菜单,需要先在若依项目中创建一个服务管理的一级菜单,方便对服务管理下的功能进行分类
最终效果如下所示:

详细配置,如下图所示:

点击确定后,刷新页面,可以看到多了一个服务管理的一级菜单
表结构
护理项目表结构定义如下:
CREATE TABLE `nursing_project` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '名称',
`order_no` int DEFAULT NULL COMMENT '排序号',
`unit` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '单位',
`price` decimal(10,2) DEFAULT NULL COMMENT '价格',
`image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图片',
`nursing_requirement` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '护理要求',
`status` int NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '更新人',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=84 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='护理项目表';
代码生成
左侧菜单中找到系统工具>代码生成,然后点击导入:

选中nursing_project表结构,点击确定:

在代码生成列表中,可以看到新增了一条数据

该数据对应了5个按钮
- 预览生成后的代码
- 修改代码生成的配置(常用)
- 删除这条数据
- 同步表结构(导入之后改动表的情况)
- 下载离线代码包
在生成之前,咱们需要对代码生成做一些配置,咱们可以点击**修改生成配置**按钮,可以看到有三部分的配置
- 基本信息
- 字段信息
- 生成信息
下图是基本信息,可以根据自己的需求修改:表、实体、表描述、作者相关的数据

下图是字段信息,一定要根据页面原型进行修改:

关于每个字段所表示的意思,如下表所示:
| 字段信息 | 描述 |
|---|---|
| 字段列名 | 表中的字段列名称 |
| 字段描述 | 字段的描述,自动读取建表语句中的comment信息,可根据实际情况更改 |
| 物理类型 | 数据库所对应的字段类型 |
| Java类型 | Java实体类中属性的类型,可改,例如,状态字段:Long类型可以修改为Integer |
| Java属性 | Java实体类中所对应的属性名称 |
| 插入 | 新增的时候,需要插入的字段 |
| 编辑 | 修改的时候,需要插入的字段 |
| 列表 | 列表查询需要展示的字段 |
| 查询 | 列表查询,需要的条件字段 |
| 查询方式 | 与上面查询条件配合,选择对应的查询方式 |
| 必填 | 在插入和新增的时候,这个字段是否是必填项,可生成校验 |
| 显示类型 | 前端代码使用的组件类型,可根据实际情况选择 |
| 字典类型 | 字典管理是用来维护可枚举的数据的,如下拉框、单选按钮等,支持自定义 |
下图是生成信息,可以按照自己的实际情况,来修改包路径,模块名称,上级菜单

修改完毕后,回到了列表页,这个时候就可以点击下载按钮,去下载代码包
下载后的代码包,解压后,包含了三部分,如下图:

添加新功能的步骤如下所示:
- 执行projectMenu.sql文件中的内容,内容为菜单的数据
- 把main目录拷贝后,找到后端ruoyi-admin模块,在main目录上点右键,选择Paste进行拷贝,效果如下:

- 将vue目录下的api和view拷贝到项目中,效果如下:

- 等代码拷贝完成后,重新启动前后端项目,打开页面,可以在服务管理中看到子菜单护理项目,如下图:

可以对护理项目进行一些操作,比如,新增、修改、删除、查询等
代码阅读
后端代码
后端代码中,若依生成的也是三层架构,且Controller层是基于Restful风格的:
@RestController
@RequestMapping("/nursing/project")
public class NursingProjectController extends BaseController
{
@Autowired
private INursingProjectService nursingProjectService;
/**
* 查询护理项目列表
*/
@PreAuthorize("@ss.hasPermi('nursing:project:list')")
@GetMapping("/list")
public TableDataInfo list(NursingProject nursingProject)
{
startPage();
List<NursingProject> list = nursingProjectService.selectNursingProjectList(nursingProject);
return getDataTable(list);
}
/**
* 导出护理项目列表
*/
@PreAuthorize("@ss.hasPermi('nursing:project:export')")
@Log(title = "护理项目", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, NursingProject nursingProject)
{
List<NursingProject> list = nursingProjectService.selectNursingProjectList(nursingProject);
ExcelUtil<NursingProject> util = new ExcelUtil<NursingProject>(NursingProject.class);
util.exportExcel(response, list, "护理项目数据");
}
/**
* 获取护理项目详细信息
*/
@PreAuthorize("@ss.hasPermi('nursing:project:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(nursingProjectService.selectNursingProjectById(id));
}
/**
* 新增护理项目
*/
@PreAuthorize("@ss.hasPermi('nursing:project:add')")
@Log(title = "护理项目", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody NursingProject nursingProject)
{
return toAjax(nursingProjectService.insertNursingProject(nursingProject));
}
/**
* 修改护理项目
*/
@PreAuthorize("@ss.hasPermi('nursing:project:edit')")
@Log(title = "护理项目", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody NursingProject nursingProject)
{
return toAjax(nursingProjectService.updateNursingProject(nursingProject));
}
/**
* 删除护理项目
*/
@PreAuthorize("@ss.hasPermi('nursing:project:remove')")
@Log(title = "护理项目", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(nursingProjectService.deleteNursingProjectByIds(ids));
}
}
当有具体返回值时,返回的时
主要关注的是分页操作,在传统的分页操作中,前端发送查询请求,后端用实体类接收,并在Service层对其进行先分页、后查询的操作
而若依框架选择的是:
- 将所有的分页操作封装到一个方法中
startPage() - 该方法又调用了
PageUtils.startPage() - 在
PageUtils.startPage()方法内 - 先获取了请求参数中的分页参数
- 然后调用PageHelper的
startPage() - PageHelper会对下一个SQL语句进行拦截,然后将SQL语句添加上分页条件
总的来说,若依框架选择的是将分页操作从Service层中抽离出来进行单独处理,而Service层只处理业务相关操作
前端代码
前端路由:

- 这个菜单的类型为菜单,菜单需要有对应的组件路径
父菜单的路由地址+当前菜单的路由地址就可以访问组件路径对应的组件- 即:
nursing/project加载组件@/views/nursing/project/index.vue
测试案例:
在服务管理中新增一个菜单(护理安排),配置如下:
- 路由地址:arrange
- 组件路径:nursing/arrange/index

在@/views/nursing/arrange目录中新增一个index.vue组件:

测试是否能正常访问这个组件:

代码迁移
在前面的功能快速开发章节中,咱们是将若依生成的功能代码直接放到了zzyl-admin模块中,将来,咱们的业务功能代码会越来越多,如果都放到zzyl-admin模块中,会让zzyl-admin模块显得比较乱。所以,咱们最好创建一个新模块,然后将功能模块的代码统一都放到新模块中。以下是具体的操作思路:
参考链接(在若依项目中创建新模块):后台手册 | RuoYi
要求:
- 创建zzyl-nursing-platform模块,效果如下:

- 在父模块中管理zzyl-nursing-platform模块的版本:
<!-- 业务模块-->
<dependency>
<groupId>com.zzyl</groupId>
<artifactId>zzyl-nursing-platform</artifactId>
<version>${zzyl.version}</version>
</dependency>
- 在zzyl-nursing-platform模块中引入zzyl-common模块
<dependencies>
<dependency>
<groupId>com.zzyl</groupId>
<artifactId>zzyl-common</artifactId>
</dependency>
</dependencies>
- 在zzyl-admin模块中引入新创建的zzyl-nursing-platform模块
<!-- 护理业务模块-->
<dependency>
<groupId>com.zzyl</groupId>
<artifactId>zzyl-nursing-platform</artifactId>
</dependency>
- 把护理项目相关的代码,都移动到新模块中 zzyl-nursing-platform
- 统一修改包名为
com.zzyl.nursing

修改完成后,重启后端程序,完成测试
代码模板改造
- 为了能够更好地通过Swagger进行接口测试,手动修改了若依生成的Controller层代码和实体类代码,加上了Swagger相关注解,未来还要让若依生成很多页面,难道每个页面都需要手动修改Controller和实体类吗?
- 若依生成的实体类中书写了大量的getter、setter、toString方法,看上去特别不优雅,能不能在若依直接生成使用Lombok注解的实体类?
- 若依生成的Mapper层代码采用的是MyBatis框架,能不能改造成它的好搭档MyBatisPlus呢?
- 若依生成的代码中日期时间都是通过传统的
Date类来处理的,能不能改造成更好用的LocalDateTime呢?
最终期望改造后的效果如下:
- 支持自动添加Swagger的注解
- 支持Lombok
- 支持MyBatisPlus
- 支持LocalDateTime
在若依框架中,生成代码的模块是zzyl-generator模块:

若依的代码生成模块背后采用的是Velocity模板引擎
上述代码中,以vm为后缀的文件,都是velocity模板引擎的对应的模板文件,如果想要改造代码生成的内容,咱们必须要先学习velocity,而后再回来看懂这些代码,才能支撑咱们去改造这些模板文件,来生成咱们想要的内容
Velocity模板引擎
Velocity是一个基于Java的模板引擎,它可以通过特定的语法获取java对象的数据 , 填充到模板中,从而实现界面和java代码的分离

常见的应用场景:
- Web应用程序 : 作为应用程序的视图, 展示数据
- 源代码生成 : Velocity可以基于模板生成Java源代码
- 自动电子邮件 : 网站注册, 认证等的电子邮件模板
- 网页静态化 : 基于velocity模板生成静态网页
快速入门
- 需求:根据下面html模板,完成对数据的填充
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>velocity快速入门</title>
</head>
<body>
<h3>心怀梦想,坚持不懈,成功即在前方。加油少年!</h3>
</body>
</html>
- 要求:加油少年,这几个字,需要使用模板引擎动态填充进来
入门案例实现步骤
- 准备模板
拷贝一份上面提供的html文档到若依项目中的ruoyi-generator模块下的reources目录中
模板文件命名:index.html.vm
如下图

模板文件内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>velocity快速入门</title>
</head>
<body>
<h3>心怀梦想,坚持不懈,成功即在前方。${message}</h3>
</body>
</html>
注意:上述代码中的 加油少年 修改为了 ${message} 这是一个动态变量,方便动态填充数据
- 编写java代码实现数据填充,并生成文件
import com.zzyl.common.constant.Constants;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
public class VelocityDemoTest {
public static void main(String[] args) throws IOException {
Properties p = new Properties();
// velocity资源加载器,告诉模板引擎到类路径下寻找资源(比如模板)
p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
// 定义字符集
p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8);
// 初始化velocity引擎
Velocity.init(p);
// 创建velocity上下文对象
VelocityContext context = new VelocityContext();
// 数据模型,这里的key需要跟模板中的变量对应上,不然填充不了数据
context.put("message", "加油朋友!!!");
// 获取模板
Template template = Velocity.getTemplate("vms/index.html.vm", "UTF-8");
// 输出
FileWriter fileWriter = new FileWriter("zzyl-generator\\src\\main\\resources\\index.html");
// 合并模板和数据模型
template.merge(context, fileWriter);
// 关闭流
fileWriter.close();
}
}
AI辅助解读Velocity语法
找一个有代表性的模板文件来解读一下,比如:domain.java.vm
提示词:
帮我给这个文件的所有代码都添加注释,重点对velocity语法做一些特别的说明
// Velocity模板开始,定义包名
package ${packageName}.domain;
// 循环导入所需的类库
#foreach ($import in $importList)
import ${import};
#end
// 导入其他必要的类库
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.zzyl.common.annotation.Excel;
#if($table.crud || $table.sub)
import com.zzyl.common.core.domain.BaseEntity;
#elseif($table.tree)
import com.zzyl.common.core.domain.TreeEntity;
#end
/**
* ${functionName}对象 ${tableName}
*
* @author ${author}
* @date ${datetime}
*/
#if($table.crud || $table.sub)
#set($Entity="BaseEntity")
#elseif($table.tree)
#set($Entity="TreeEntity")
#end
// 定义Java类,继承自BaseEntity或TreeEntity
public class ${ClassName} extends ${Entity}
{
// 序列化版本ID
private static final long serialVersionUID = 1L;
// 循环定义类中的属性
#foreach ($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
// 添加属性的注释
/** $column.columnComment */
// 根据属性是否为列表类型,决定是否添加Excel注解
#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注解的名字
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
#else
// 如果是日期类型,添加JSON格式化注解
#elseif($column.javaType == 'Date')
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd")
#else
// 其他情况直接添加Excel注解
@Excel(name = "${comment}")
#end
#end
// 定义属性
private $column.javaType $column.javaField;
#end
#end
// 如果是子表,添加子表的集合属性
#if($table.sub)
/** $table.subTable.functionName信息 */
private List<${subClassName}> ${subclassName}List;
#end
// 生成getter和setter方法
#foreach ($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
#set($AttrName=$column.javaField)
#else
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#end
// setter方法
public void set${AttrName}($column.javaType $column.javaField)
{
this.$column.javaField = $column.javaField;
}
// getter方法
public $column.javaType get${AttrName}()
{
return $column.javaField;
}
#end
#end
// 如果是子表,生成子表集合的getter和setter方法
#if($table.sub)
public List<${subClassName}> get${subClassName}List()
{
return ${subclassName}List;
}
public void set${subClassName}List(List<${subClassName}> ${subclassName}List)
{
this.${subclassName}List = ${subclassName}List;
}
#end
// 重写toString方法
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
#foreach ($column in $columns)
#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
#set($AttrName=$column.javaField)
#else
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#end
.append("${column.javaField}", get${AttrName}())
#end
#if($table.sub)
.append("${subclassName}List", get${subClassName}List())
#end
.toString();
}
}
- Velocity语法解释
${variable}: 表示插入变量值#foreach和#end: 循环结构,用于遍历列表#if和#end: 条件判断结构#set: 设置变量#elseif: 条件分支- Java类结构说明
- 包声明: 定义类所在的包
- 导入语句: 导入类所需要的类库
- 类注释: 描述类的功能和作者信息
- 类定义: 定义类并继承自
BaseEntity或TreeEntity - 属性定义: 根据模板参数定义类的属性
- 属性注释: 对属性进行描述
- Excel注解: 标记属性以方便导出Excel
- getter和setter方法: 自动生成属性的访问方法
- 子表集合: 如果是子表,则定义子表集合
- 重写toString方法: 提供类实例的字符串表示形式
代码生成流程
阅读一下代码生成功能的核心代码,代码位置:com.zzyl.generator.service.GenTableServiceImpl.generatorCode():
/**
* 生成代码
* @param tableName 表名
* @param zip 压缩输出流
*/
private void generatorCode(String tableName, ZipOutputStream zip)
{
// 查询表信息
GenTable table = genTableMapper.selectGenTableByName(tableName);
// 设置主子表信息
setSubTable(table);
// 设置主键列信息
setPkColumn(table);
// 初始化Velocity引擎
VelocityInitializer.initVelocity();
// 准备Velocity上下文 获取该表的详细数据,并设置模板所需要的数据模型
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表 读取resources/vm目录中的定义的模板文件
List<String> 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);
}
}
}
集成Lombok
- 在父工程中添加Lombok版本管理
<properties>
<lombok.version>1.18.22</lombok.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- 其他依赖省略... -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
- 在zzyl-common模块中引入lombok的依赖(通用,其他模块如需要用,只需要依赖common即可)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- 修改模板
主要修改的模板为 domain.java.vm,让它支持lombok
修改模板如下:
package ${packageName}.domain;
#foreach ($import in $importList)
import ${import};
#end
import org.apache.commons.lang3.builder.ToStringBuilder; ## 用不到了,可删
import org.apache.commons.lang3.builder.ToStringStyle; ## 用不到了,可删
import com.zzyl.common.annotation.Excel;
#if($table.crud || $table.sub)
import com.zzyl.common.core.domain.BaseEntity;
#elseif($table.tree)
import com.zzyl.common.core.domain.TreeEntity;
#end
import lombok.Data; ## 导入lombok的依赖
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
/**
* ${functionName}对象 ${tableName}
*
* @author ${author}
* @date ${datetime}
*/
#if($table.crud || $table.sub)
#set($Entity="BaseEntity")
#elseif($table.tree)
#set($Entity="TreeEntity")
#end
@Data ## 添加lombok的注解
@NoArgsConstructor
@AllArgsConstructor
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
## 以下是生成属性的get/set方法 start.... 删除这部分内容
#foreach ($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
#set($AttrName=$column.javaField)
#else
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#end
public void set${AttrName}($column.javaType $column.javaField)
{
this.$column.javaField = $column.javaField;
}
public $column.javaType get${AttrName}()
{
return $column.javaField;
}
#end
#end
## 以下是生成属性的get/set方法 end.... 删除这部分内容
## 如果是主子表,生成子表相关的get/set方法,start.... 删除这部分内容
#if($table.sub)
public List<${subClassName}> get${subClassName}List()
{
return ${subclassName}List;
}
public void set${subClassName}List(List<${subClassName}> ${subclassName}List)
{
this.${subclassName}List = ${subclassName}List;
}
#end
## 如果是主子表,生成子表相关的get/set方法,end.... 删除这部分内容
##以下代码生成 toString方法, start.... 删除这部分内容
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
#foreach ($column in $columns)
#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
#set($AttrName=$column.javaField)
#else
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#end
.append("${column.javaField}", get${AttrName}())
#end
#if($table.sub)
.append("${subclassName}List", get${subClassName}List())
#end
.toString();
}
##以下代码生成 toString方法, end.... 删除这部分内容
}
集成MyBatis-Plus
参考:https://gitee.com/tellsea/ruoyi-vue-plus
- 父工程中添加MP版本管理
<properties>
<mybatis-plus-spring-boot.version>3.5.2</mybatis-plus-spring-boot.version>
</properties>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- MyBatis-Plus 增强CRUD -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-annotation</artifactId>
<version>${mybatis-plus-spring-boot.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
- 在zzyl-common模块中引入MP的依赖
<!--MyBatisPlus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-annotation</artifactId>
</dependency>
- 修改核心配置
在zzyl-admin中修改application.yml,新增MyBatisPlus并删除原生MyBatis相关配置
# MyBatisPlus配置
mybatis-plus:
# 搜索指定包别名
typeAliasesPackage: com.zzyl.**.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 全局配置
global-config:
db-config:
id-type: auto #id生成策略为自增
configuration:
map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名
新增核心配置类,删除MyBatisConfig
package com.zzyl.framework.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Mybatis Plus 配置
*
* @author ruoyi
*/
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
// 阻断插件
interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
return interceptor;
}
/**
* 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
*/
public PaginationInnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型为mysql
paginationInnerInterceptor.setDbType(DbType.MYSQL);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInnerInterceptor.setMaxLimit(-1L);
return paginationInnerInterceptor;
}
/**
* 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html
*/
public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}
/**
* 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html
*/
public BlockAttackInnerInterceptor blockAttackInnerInterceptor() {
return new BlockAttackInnerInterceptor();
}
}
改造护理项目代码
在改造生成模板之前,先手动改造一下护理项目的代码,将MyBatisPlus集成到护理项目模块中使用,方便稍后参考护理项目模块改造后的代码来改造代码生成模板
- 改造NursingProjectMapper
package com.zzyl.nursing.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zzyl.nursing.domain.NursingProject;
import org.apache.ibatis.annotations.Mapper;
/**
* 护理项目Mapper接口
*
* @author ruoyi
* @date 2024-09-26
*/
@Mapper
public interface NursingProjectMapper extends BaseMapper<NursingProject>
{
/**
* 查询护理项目
*
* @param id 护理项目主键
* @return 护理项目
*/
public NursingProject selectNursingProjectById(Long id);
/**
* 查询护理项目列表
*
* @param nursingProject 护理项目
* @return 护理项目集合
*/
public List<NursingProject> selectNursingProjectList(NursingProject nursingProject);
/**
* 新增护理项目
*
* @param nursingProject 护理项目
* @return 结果
*/
public int insertNursingProject(NursingProject nursingProject);
/**
* 修改护理项目
*
* @param nursingProject 护理项目
* @return 结果
*/
public int updateNursingProject(NursingProject nursingProject);
/**
* 删除护理项目
*
* @param id 护理项目主键
* @return 结果
*/
public int deleteNursingProjectById(Long id);
/**
* 批量删除护理项目
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteNursingProjectByIds(Long[] ids);
}
- 改造NursingProjectService
package com.zzyl.nursing.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zzyl.nursing.domain.NursingProject;
/**
* 护理项目Service接口
*
* @author ruoyi
* @date 2024-09-26
*/
public interface INursingProjectService extends IService<NursingProject>
{
/**
* 查询护理项目
*
* @param id 护理项目主键
* @return 护理项目
*/
public NursingProject selectNursingProjectById(Long id);
/**
* 查询护理项目列表
*
* @param nursingProject 护理项目
* @return 护理项目集合
*/
public List<NursingProject> selectNursingProjectList(NursingProject nursingProject);
/**
* 新增护理项目
*
* @param nursingProject 护理项目
* @return 结果
*/
public int insertNursingProject(NursingProject nursingProject);
/**
* 修改护理项目
*
* @param nursingProject 护理项目
* @return 结果
*/
public int updateNursingProject(NursingProject nursingProject);
/**
* 批量删除护理项目
*
* @param ids 需要删除的护理项目主键集合
* @return 结果
*/
public int deleteNursingProjectByIds(Long[] ids);
/**
* 删除护理项目信息
*
* @param id 护理项目主键
* @return 结果
*/
public int deleteNursingProjectById(Long id);
}
- 改造NursingProjectServiceImpl
package com.zzyl.nursing.service.impl;
import java.util.Arrays;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zzyl.common.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.zzyl.nursing.mapper.NursingProjectMapper;
import com.zzyl.nursing.domain.NursingProject;
import com.zzyl.nursing.service.INursingProjectService;
/**
* 护理项目Service业务层处理
*
* @author ruoyi
* @date 2024-09-26
*/
@Service
public class NursingProjectServiceImpl extends ServiceImpl<NursingProjectMapper, NursingProject> implements INursingProjectService
{
@Autowired
private NursingProjectMapper nursingProjectMapper;
/**
* 查询护理项目
*
* @param id 护理项目主键
* @return 护理项目
*/
@Override
public NursingProject selectNursingProjectById(Long id) {
// return nursingProjectMapper.selectNursingProjectById(id);
return getById(id);
}
/**
* 查询护理项目列表
*
* @param nursingProject 护理项目
* @return 护理项目
*/
@Override
public List<NursingProject> selectNursingProjectList(NursingProject nursingProject)
{
return nursingProjectMapper.selectNursingProjectList(nursingProject);
}
/**
* 新增护理项目
*
* @param nursingProject 护理项目
* @return 结果
*/
@Override
public int insertNursingProject(NursingProject nursingProject)
{
nursingProject.setCreateTime(DateUtils.getNowDate());
// return nursingProjectMapper.insertNursingProject(nursingProject);
return save(nursingProject) ? 1 : 0;
}
/**
* 修改护理项目
*
* @param nursingProject 护理项目
* @return 结果
*/
@Override
public int updateNursingProject(NursingProject nursingProject)
{
nursingProject.setUpdateTime(DateUtils.getNowDate());
// return nursingProjectMapper.updateNursingProject(nursingProject);
return updateById(nursingProject) ? 1 : 0;
}
/**
* 批量删除护理项目
*
* @param ids 需要删除的护理项目主键
* @return 结果
*/
@Override
public int deleteNursingProjectByIds(Long[] ids)
{
// return nursingProjectMapper.deleteNursingProjectByIds(ids);
return removeByIds(Arrays.asList(ids)) ? 1 : 0;
}
/**
* 删除护理项目信息
*
* @param id 护理项目主键
* @return 结果
*/
@Override
public int deleteNursingProjectById(Long id)
{
// return nursingProjectMapper.deleteNursingProjectById(id);
return removeById(id) ? 1 : 0;
}
}
关于查询列表,由于不同的项目存在不同的查询参数以及不同的条件,使用MP构建不仅复杂且难以复用到模板中,所以直接使用若依框架生成的MyBatis原生代码即可
- 其他必要修改
集成MP之后,项目中的BaseEntity类中的字段有些会受影响,需要添加如下注解
由于这几个字段,并不会跟数据库中的表字段进行映射,必须要添加@TableField(exist = false)表示,表示该字段不存在于数据库表中
/**
* Entity基类
*
* @author ruoyi
*/
public class BaseEntity implements Serializable
{
private static final long serialVersionUID = 1L;
/** 搜索值 */
@JsonIgnore
@TableField(exist = false)
private String searchValue;
/** 请求参数 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)
private Map<String, Object> params;
}
注意事项:
修改后重启项目可能会出现MP依赖丢失问题,比如FileNotFoundException:...(就是说mybaits-plus文件不存在)
解决方式:设置中切换一个空的maven仓库,把所有依赖重新下载
改造代码生成模板
在目前的模板文件中,咱们需要修改的模板共有3个,分别是:
- mapper.java.vm 继承BaseMapper
- service.java.vm 继承IService
- serviceImpl.java.vm 继承ServiceImpl
常见方法的使用(单表的增删改查)
- domain.java.vm 无需修改,类名与表名一致,会自动映射,主键已经在yaml文件中配置
- controller.java.vm 无需修改,保留原有的接口方法和命名
mapper.java.vm
package ${packageName}.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import ${packageName}.domain.${ClassName};
#if($table.sub)
import ${packageName}.domain.${subClassName};
#end
/**
* ${functionName}Mapper接口
*
* @author ${author}
* @date ${datetime}
*/
@Mapper
public interface ${ClassName}Mapper extends BaseMapper<${ClassName}>
{
/**
* 查询${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
}
service.java.vm
package ${packageName}.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import ${packageName}.domain.${ClassName};
/**
* ${functionName}Service接口
*
* @author ${author}
* @date ${datetime}
*/
public interface I${ClassName}Service extends IService<${ClassName}>
{
/**
* 查询${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}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});
}
serviceImpl.java.vm
package ${packageName}.service.impl;
import java.util.List;
#foreach ($column in $columns)
#if($column.javaField == 'createTime' || $column.javaField == 'updateTime')
import com.zzyl.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.zzyl.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 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.Arrays;
/**
* ${functionName}Service业务层处理
*
* @author ${author}
* @date ${datetime}
*/
@Service
public class ${ClassName}ServiceImpl extends ServiceImpl<${ClassName}Mapper, ${ClassName}> implements I${ClassName}Service
{
@Autowired
private ${ClassName}Mapper ${className}Mapper;
/**
* 查询${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});
return getById(${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});
return save(${className}) ? 1 : 0;
#end
}
/**
* 修改${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});
return updateById(${className}) ? 1 : 0;
}
/**
* 批量删除${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);
return removeByIds(Arrays.asList(${pkColumn.javaField}s)) ? 1 : 0;
}
/**
* 删除${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});
return removeById(${pkColumn.javaField}) ? 1 : 0;
}
#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
}
其他必要修改
集成MP之后,项目中的BaseEntity类中的字段有些会受影响,需要添加如下注解
由于这几个字段,并不会跟数据库中的表字段进行映射,必须要添加@TableField(exist = false)表示,表示该字段不存在于数据库表中
/**
* Entity基类
*
* @author ruoyi
*/
public class BaseEntity implements Serializable
{
private static final long serialVersionUID = 1L;
/** 搜索值 */
@JsonIgnore
@TableField(exist = false)
private String searchValue;
/** 请求参数 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)
private Map<String, Object> params;
}
字段自动填充
MyBatis-Plus 提供的字段自动填充功能是一种非常实用的特性,它能够在插入或更新数据库记录时自动填充一些公共字段,如创建时间(createTime)、更新时间(updateTime)、创建人(createBy)、更新人(updateBy)等。这一功能极大地简化了开发过程,减少了重复的代码编写,提高了开发效率

官网链接:https://baomidou.com/guides/auto-fill-field/
在MybatisPlus中通过两步可以实现这个功能:
- 在实体类中,使用
@TableField注解,来标明哪些字段是需要自动填充的,并且需要指定填充策略
package com.zzyl.common.core.domain;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.Swagger.annotations.ApiModel;
import io.Swagger.annotations.ApiModelProperty;
/**
* Entity基类
*
* @author ruoyi
*/
@ApiModel("Entity基类")
public class BaseEntity implements Serializable
{
private static final long serialVersionUID = 1L;
/** 搜索值 */
@JsonIgnore
@TableField(exist = false)
private String searchValue;
/** 创建者 */
@ApiModelProperty(value = "创建者")
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@ApiModelProperty(value = "更新者")
@TableField(fill = FieldFill.UPDATE)
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.UPDATE)
private Date updateTime;
/** 备注 */
@ApiModelProperty(value = "备注")
private String remark;
/** 请求参数 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@ApiModelProperty(value = "请求参数")
@TableField(exist = false)
private Map<String, Object> params;
}
- 在zzyl-framework模块中新增MyMetaObjectHandler 来处理字段自动填充

详细的代码如下:
package com.zzyl.framework.interceptor;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.zzyl.common.core.domain.model.LoginUser;
import com.zzyl.common.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, DateUtils.getNowDate());
this.strictInsertFill(metaObject, "createBy", String.class, String.valueOf(getLoginUser()));
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, DateUtils.getNowDate());
this.strictUpdateFill(metaObject, "updateBy", String.class, String.valueOf(getLoginUser()));
}
/**
* 获取当前登录人的ID
*
* @return 登录人ID
*/
public Long getLoginUser() {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
return loginUser.getUserId();
}
return 1L;
}
}
字段自动填充测试
以上所有内容修改完成以后,删除新增方法和修改方法中填充字段的代码,然后重启后端项目,来测试代码的有效性,可以新增或修改护理项目,检查是否能够自动填充数据。
如果测试修改发现没有自动修改数据库表中的updateTime和updateBy字段,原因在官网中说明了:
MetaObjectHandler提供的默认方法策略是:如果属性有值则不覆盖,如果填充值为null则不填充。
此时,需要换一种更新方式,修改后的代码如下:
package com.zzyl.framework.interceptor;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.zzyl.common.core.domain.model.LoginUser;
import com.zzyl.common.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
// 自动填充创建人
this.strictInsertFill(metaObject, "createBy", String.class, String.valueOf(getLoginUser()));
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName("updateBy", String.valueOf(getLoginUser()), metaObject);
// this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
// this.strictUpdateFill(metaObject, "updateBy", String.class, String.valueOf(getLoginUser()));
}
public Long getLoginUser() {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
return loginUser.getUserId();
}
return 1L;
}
}
测试通过后,能够自动填充数据了。那么,接下来,继续修改模板,去掉新增方法和修改方法补全字段属性的代码,修改后的模板为:
package ${packageName}.service.impl;
import java.util.Arrays;
import java.util.List;
#foreach ($column in $columns)
#if($column.javaField == 'createTime' || $column.javaField == 'updateTime')
import com.zzyl.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.zzyl.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 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
/**
* ${functionName}Service业务层处理
*
* @author ${author}
* @date ${datetime}
*/
@Service
public class ${ClassName}ServiceImpl extends ServiceImpl<${ClassName}Mapper, ${ClassName}> implements I${ClassName}Service
{
@Autowired
private ${ClassName}Mapper ${className}Mapper;
/**
* 查询${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});
return getById(${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});
return save(${className}) ? 1 : 0;
#end
}
/**
* 修改${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});
return updateById(${className}) ? 1 : 0;
}
/**
* 批量删除${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);
return removeByIds(Arrays.asList(${pkColumn.javaField}s)) ? 1 : 0;
}
/**
* 删除${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});
return removeById(${pkColumn.javaField}) ? 1 : 0;
}
#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
}
集成Swagger
需要修改2个模板文件:controller.java.vm和domain.java.vm,先来修改controller.java.vm文件,具体的修改的内容如下:
package ${packageName}.controller;
import com.zzyl.common.core.domain.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
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.zzyl.common.annotation.Log;
import com.zzyl.common.core.controller.BaseController;
import com.zzyl.common.core.domain.AjaxResult;
import com.zzyl.common.enums.BusinessType;
import ${packageName}.domain.${ClassName};
import ${packageName}.service.I${ClassName}Service;
import com.zzyl.common.utils.poi.ExcelUtil;
#if($table.crud || $table.sub)
import com.zzyl.common.core.page.TableDataInfo;
#elseif($table.tree)
#end
/**
* ${functionName}Controller
*
* @author ${author}
* @date ${datetime}
*/
@RestController
@RequestMapping("/${moduleName}/${businessName}")
@Api(tags = "${functionName}相关接口")
public class ${ClassName}Controller extends BaseController
{
@Autowired
private I${ClassName}Service ${className}Service;
/**
* 查询${functionName}列表
*/
@PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')")
@GetMapping("/list")
@ApiOperation("查询${functionName}列表")
#if($table.crud || $table.sub)
public TableDataInfo<List<${ClassName}>> list(@ApiParam(value = "${functionName}查询条件") ${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")
@ApiOperation("导出${functionName}列表")
public void export(HttpServletResponse response, @ApiParam(value = "${functionName}查询条件") ${ClassName} ${className})
{
List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class);
util.exportExcel(response, list, "${functionName}数据");
}
/**
* 获取${functionName}详细信息
*/
@PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')")
@GetMapping(value = "/{${pkColumn.javaField}}")
@ApiOperation("获取${functionName}详细信息")
public R<${ClassName}> getInfo(@ApiParam(value = "${functionName}ID", required = true)
@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField})
{
## return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}));
return R.ok(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}));
}
/**
* 新增${functionName}
*/
@PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')")
@Log(title = "${functionName}", businessType = BusinessType.INSERT)
@PostMapping
@ApiOperation("新增${functionName}")
public AjaxResult add(@ApiParam(value = "${functionName}实体", required = true) @RequestBody ${ClassName} ${className})
{
return toAjax(${className}Service.insert${ClassName}(${className}));
}
/**
* 修改${functionName}
*/
@PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')")
@Log(title = "${functionName}", businessType = BusinessType.UPDATE)
@PutMapping
@ApiOperation("修改${functionName}")
public AjaxResult edit(@ApiParam(value = "${functionName}实体", required = true) @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}")
@ApiOperation("删除${functionName}")
public AjaxResult remove(@ApiParam(value = "${functionName}ID数组", required = true) @PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s)
{
return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s));
}
}
日期类型升级
若依目前提供的代码中,如果遇到日期类型则使用的是Date,由于目前使用比较流行的日期时间类型是LocalDateTime类型,咱们也可以把它改为该类型进行使用
知识扩展
Date类型与LocalDateTime的区别?
- 不可变性与线程安全性
- Date一旦创建Date对象,可以通过setTime(long time)进行修改,多线程下存在线程安全问题
- LocalDateTime是不可变的,线程安全
- 时间精度
- Date可以表示到毫秒的时间值
- LocalDateTime可以表示到纳秒的时间值
- 易用性:LocalDateTime的API设计的更现代化,易于使用
在目前代码生成中,其中字段详细这一栏中的Java类型并不支持LocalDateTime类型,所以咱们需要在前端也要修改其内容,让它支持LocalDateTime

打开前端项目,找到src/views/tool/gen/editTable.vue文件,在这个文件添加中一行代码,如下:

当前端选择了LocalDateTime类型之后,在后端的代码生成中,需要导入对应的包才可以
咱们打开VelocityUtils类,这里面有处理导入包的逻辑,找到getImportList方法,在其中添加对应的包导入
/**
* 根据列类型获取导入包
*
* @param genTable 业务表对象
* @return 返回需要导入的包列表
*/
public static HashSet<String> getImportList(GenTable genTable)
{
List<GenTableColumn> columns = genTable.getColumns();
GenTable subGenTable = genTable.getSubTable();
HashSet<String> importList = new HashSet<String>();
if (StringUtils.isNotNull(subGenTable))
{
importList.add("java.util.List");
}
for (GenTableColumn column : columns)
{
if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType()))
{
importList.add("java.util.Date");
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
}
// 如果字段类型在前端选择的是LocalDateTime,则导入对应的包
if (!column.isSuperColumn() && GenConstants.TYPE_LOCAL_DATE_TYPE.equals(column.getJavaType()))
{
importList.add("java.time.LocalDateTime");
// 导入这个是为了格式化日期
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
}
else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType()))
{
importList.add("java.math.BigDecimal");
}
}
return importList;
}
新增代码中的TYPE_LOCAL_DATE_TYPE需要定义在 GenConstants 常量类中
/** JDK8时间类型 */
public static final String TYPE_LOCAL_DATE_TYPE= "LocalDateTime";
修改模块文件domain.java.vm文件,修改的内容如下:
#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' || $column.javaType == 'LocalDateTime')
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd")
#else
@Excel(name = "${comment}")
#end
#end
@ApiModelProperty("$column.columnComment")
private $column.javaType $column.javaField;
#end
其他修改
- 修改生成包路径和生成模块名
若依默认生成的包路径和模块名都是system,效果如下:

而项目中用到的包路径和模块名都是nursing,如果要将这里的默认值改为nursing,只需要修改配置文件即可
修改zzyl-generator模块下的generator.yml配置文件,修改后的内容如下:
# 代码生成
gen:
# 作者
author: Norlcyan
# 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
packageName: com.zzyl.nursing
# 自动去除表前缀,默认是false
autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix: sys_
# 是否允许生成文件覆盖到本地(自定义路径),默认不允许
allowOverwrite: false
- 修改物理类型对应的Java类型
若依默认生成的字段信息中很多数据库中的int类型和tinyint类型对应的Java类型都是Long,如下图所示:

可以通过修改zzyl-generator模块下的GenUtils工具类中的代码来将其统一改为INTEGER类型,具体的修改位置为:
/**
* 初始化列属性字段
*/
public static void initColumnField(GenTableColumn column, GenTable table)
{
String dataType = getDbType(column.getColumnType());
String columnName = column.getColumnName();
column.setTableId(table.getTableId());
column.setCreateBy(table.getCreateBy());
// 设置java字段名
column.setJavaField(StringUtils.toCamelCase(columnName));
// 设置默认类型
column.setJavaType(GenConstants.TYPE_STRING);
column.setQueryType(GenConstants.QUERY_EQ);
if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType))
{
// 字符串长度超过500设置为文本域
Integer columnLength = getColumnLength(column.getColumnType());
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))
{
// 将日期时间类型改为LocalDateTime
column.setJavaType(GenConstants.TYPE_LOCAL_DATE_TIME);
column.setHtmlType(GenConstants.HTML_DATETIME);
}
else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType))
{
column.setHtmlType(GenConstants.HTML_INPUT);
// 如果是浮点型 统一用BigDecimal
String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ",");
if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0)
{
column.setJavaType(GenConstants.TYPE_BIGDECIMAL);
}
// 如果是整形
else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10
|| GenConstants.MYSQL_TINYINT.equals(column.getColumnType()) || GenConstants.MYSQL_INT.equals(column.getColumnType()))
{
column.setJavaType(GenConstants.TYPE_INTEGER);
}
// 长整形
else
{
column.setJavaType(GenConstants.TYPE_LONG);
}
}
......省略了后续代码
}
新增代码中的 LocalDateTime、 tinyint 和 int 类型需要定义在
GenConstants常量类中。
/** LocalDateTime时间类型 */
public static final String TYPE_LOCAL_DATE_TIME = "LocalDateTime";
/** MySql tinyint 类型 */
public static final String MYSQL_TINYINT = "tinyint";
/** MySql int 类型 */
public static final String MYSQL_INT = "int";
这样,在系统中导入表结构后,数据库中的tinyint类型和int类型对应的默认Java类型就都是Integer类型了
前后端快速开发
护理项目页面改造
列表改造
根据页面原型实现的效果图:
目前使用若依快速代码生成代码的效果图:

经过对比,有以下几点不同
- 搜索栏缺少状态条件
- 状态展示为数字,应该展示禁用或启用
- 缺少启用/禁用按钮和功能,多了几个不需要的按钮
- 没有序号,多了一列编号,显示的是数据库表中的id
- 创建时间没有展示时分秒
找到src/views/nursing/project/index.vue文件,护理项目的前端代码已经添加了详细的注释,如下:
<template>
<div class="app-container">
<!-- 搜索表单 start -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 搜索表单 end -->
<!-- 按钮区域 start -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['nursing:project:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['nursing:project:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['nursing:project:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['nursing:project:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 按钮区域 end -->
<!-- 数据展示区域 start -->
<el-table v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="排序号" align="center" prop="orderNo" />
<el-table-column label="单位" align="center" prop="unit" />
<el-table-column label="价格" align="center" prop="price" />
<el-table-column label="图片" align="center" prop="image" width="100">
<template #default="scope">
<image-preview :src="scope.row.image" :width="50" :height="50"/>
</template>
</el-table-column>
<el-table-column label="护理要求" align="center" prop="nursingRequirement" />
<el-table-column label="状态" align="center" prop="status" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['nursing:project:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['nursing:project:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 数据展示区域 end -->
<!-- 分页组件 start -->
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 分页组件 end -->
<!-- 新增或修改弹窗 start -->
<!-- 添加或修改护理项目对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="projectRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="排序号" prop="orderNo">
<el-input v-model="form.orderNo" placeholder="请输入排序号" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="form.unit" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input v-model="form.price" placeholder="请输入价格" />
</el-form-item>
<el-form-item label="图片" prop="image">
<image-upload v-model="form.image"/>
</el-form-item>
<el-form-item label="护理要求" prop="nursingRequirement">
<el-input v-model="form.nursingRequirement" placeholder="请输入护理要求" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 新增或修改弹窗 end -->
</div>
</template>
<script setup name="Project">
// 引入后端接口
import { listProject, getProject, delProject, addProject, updateProject } from "@/api/nursing/project";
// 获取当前实例代理对象,用于访问组件的数据、方法等属性。
const { proxy } = getCurrentInstance();
// 列表数据
const projectList = ref([]);
// 是否显示弹窗
const open = ref(false);
// 加载状态
const loading = ref(true);
// 是否显示搜索栏
const showSearch = ref(true);
//记录被选中的id集合
const ids = ref([]);
//是否是单选,用于是否高亮修改按钮
const single = ref(true);
//是否是多选,用于是否高亮删除按钮
const multiple = ref(true);
//总条数
const total = ref(0);
// 标题
const title = ref("");
const data = reactive({
// 新增或修改表单数据
form: {},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
name: null,
status: null,
},
// 表单校验
rules: {
status: [
{ required: true, message: "状态不能为空", trigger: "change" }
],
}
});
//toRefs将传入的数据对象的特定属性转换为响应式对象
const { queryParams, form, rules } = toRefs(data);
/** 查询护理项目列表 */
function getList() {
loading.value = true;
listProject(queryParams.value).then(response => {
projectList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
id: null,
name: null,
orderNo: null,
unit: null,
price: null,
image: null,
nursingRequirement: null,
status: null,
createBy: null,
updateBy: null,
remark: null,
createTime: null,
updateTime: null
};
proxy.resetForm("projectRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加护理项目";
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const _id = row.id || ids.value
getProject(_id).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改护理项目";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["projectRef"].validate(valid => {
if (valid) {
if (form.value.id != null) {
updateProject(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addProject(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _ids = row.id || ids.value;
proxy.$modal.confirm('是否确认删除护理项目编号为"' + _ids + '"的数据项?').then(function() {
return delProject(_ids);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('nursing/project/export', {
...queryParams.value
}, `project_${new Date().getTime()}.xlsx`)
}
getList();
</script>
序号处理
修改后的代码:
<!-- 数据展示区域 start -->
<el-table v-loading="loading" :data="projectList">
<!-- <el-table-column type="selection" width="55" align="center" /> 去除复选框 -->
<el-table-column label="序号" type="index" align="center" />
<!-- 其他代码略 -->
</el-table>
<!-- 数据展示区域 end -->
@selection-change="handleSelectionChange"可以把该方法删除,因为该方法是记录复选框选中的数据页面中展示的是数据库表中的主键id,需要将其修改为表格的自然排序序号,加上
type="index"即可
时间展示处理
修改后的代码:
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
需要将时分秒展示出来,可以在
{y}-{m}-{d}后面加上{h}:{i}:{s}
状态样式调整
修改后的代码:
<el-table-column label="状态" align="center" >
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
修改前,状态显示中都是数值表示(0或者1),而想要的效果是启用或禁用
可以使用Vue的插槽语法对其进行逻辑判断
新增状态查询条件
这里需要使用的是下拉选择的组件<el-select>
<!-- 搜索表单 start -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 搜索表单 end -->
<script setup name="Project">
//下拉选择框的选择项
const options = ref([
{
value: 1,
label: "启用"
},
{
value: 0,
label: "停用"
}
])
</script>
禁用功能开发
在目前的操作栏中,只有两个按钮,分别是修改和删除,参考这两个按钮,新增一个禁用按钮,并且需要给这个按钮绑定事件,方便提交数据到后端进行修改
注意:无需新增后端的禁用接口,可直接使用修改接口
- 新增按钮:
<!-- 数据展示区域 start -->
<el-table v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<!-- 其他代码略... -->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
v-hasPermi="['nursing:project:edit']">修改</el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['nursing:project:remove']">删除</el-button>
<el-button link type="primary" :icon="scope.row.status == 0 ? 'Lock' : 'Unlock'" @click="handleEnable(scope.row)" >{{ scope.row.status == 0 ? '启用' : '禁用' }}</el-button>
</template>
</el-table-column>
</el-table>
<!-- 数据展示区域 end -->
- 按钮中绑定方法handleEnable(scope.row),调用后端接口
/* 启用禁用按钮操作 */
function handleEnable(row) {
// 获取当前状态
const status = row.status;
// 提示信息
const msg = status === 1 ? '禁用' : '启用';
// 构建参数
const params = {
id: row.id,
status: status === 1 ? 0 : 1
};
proxy.$modal.confirm(`是否确认${msg}该护理项目的数据项?`).then(function() {
return updateProject(params);
}).then(() => {
getList();
proxy.$modal.msgSuccess(`${msg}成功`);
}).catch(() => {});
}
数据字典
字典管理用来维护字典类型的数据,例如:状态的启用、禁用;性别的男、女;订单的已下单、支付中、已完成;通常用在如下拉框、单选按钮、复选框、树选择中的数据中,方便系统管理员维护
主要功能包括:字典分类管理、字典数据管理
简单的理解:数据字典就是企业中对一些常量的统一管理

每个字典类型,可能会有多个值,比如,下面是用户性别的字典数据:

数据字典的使用方式
在刚刚开发的护理项目中,状态下拉列表就可以使用数据字典来完成数据的展示,如下图

在展示查询条件状态的时候,有两个选项值,目前代码中是写死的内容,咱们定义了一个options,如果使用字典可以先定义护理项目的状态字典,然后在组件中引用字典数据即可。下面是详细的操作步骤:
- 新增数据字典

- 新增字典数据
选中字典类型,可以进入字典数据页面,对字典数据进行新增

- 新增字典数据,添加两条数据
- 启用 1
- 禁用 0

在护理项目的组件中引用字典并使用:
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="name">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option
v-for="item in nursing_project_status"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 其他代码略 -->
</div>
</template>
<script setup name="Project">
import { listProject, getProject, delProject, addProject, updateProject } from "@/api/nursing/project";
//获取当前组件的实例
const { proxy } = getCurrentInstance();
//引用数据字典
const { nursing_project_status } = proxy.useDict("nursing_project_status");
/*
const options = ref([
{
value: 1,
label: "启用"
},
{
value: 0,
label: "停用"
}
]) */
</script>
利用proxy获取字典数据,并用
nursing_project_status接收,最后在下拉菜单中切换数据源后续当对应的字典数据发生改变时,下拉菜单中的选项也会动态发生改变
改造新增护理项目弹窗
目前若依框架给提供的新增或修改的弹窗效果如下:

经过对比,需要修复的内容如下:
- 排序号和价格字段修改为数字输入框
- 数字输入框 https://element-plus.org/zh-CN/component/input-number.html
- 新增一个状态字段,这个字段是一个单选框
- 单选框 https://element-plus.org/zh-CN/component/radio.html
最终的代码:
<!-- 添加或修改护理项目对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="projectRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="排序号" prop="orderNo">
<el-input-number v-model="form.orderNo"
placeholder="请输入"
:min="1"
:max="20" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="form.unit" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input-number v-model="form.price"
placeholder="请输入"
:min="5"
:max="100"
:step="5" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in nursing_project_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片" prop="image">
<image-upload v-model="form.image" />
</el-form-item>
<el-form-item label="护理要求" prop="nursingRequirement">
<el-input v-model="form.nursingRequirement" placeholder="请输入护理要求" type="textarea"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
修改数据状态不回显问题
修改护理项目的index组件,把状态改为字符串类型
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const _id = row.id || ids.value
getProject(_id).then(response => {
form.value = response.data;
form.value.status = String(form.value.status)
open.value = true;
title.value = "修改护理项目";
});
}
OSS集成
问题分析
在若依框架目前的实现中,是把图片存储到了服务器本地的目录,通过服务进行访问,这样做存储的是比较省事,但是缺点也有很多:
- 硬件与网络要求:服务器通常需要高性能的硬件和稳定的网络环境,以保证文件传输的效率和稳定性。这可能会增加硬件和网络资源的成本和维护难度。
- 管理难度:服务器目录需要管理员进行配置和管理,包括权限设置、备份策略等。如果管理不善或配置不当,可能会引发一些安全问题和性能问题。
- 性能瓶颈:如果服务器处理能力不足或网络带宽不够,可能会导致性能瓶颈,影响文件上传、下载和访问的速度。
- 单点故障风险:服务器故障可能导致所有存储在其上的文件无法访问,尽管可以通过备份和冗余措施来降低这种风险,但单点故障的风险仍然存在。
基于以上原因,企业中很多的文件都会存储到OSS中,OSS可以解决以上所有的问题,并且成本也不高,下面咱们就把阿里的OSS集成到若依项目中
目前使用的是若依提供的方案,对应的代码如下:
位置:zzyl-admin模块下,com.zzyl.web.controller.common.CommonController.uploadFile
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称,保存到本地的目录中
String fileName = FileUploadUtils.upload(filePath, file);
String url = nursingrConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
创建OSS模块
方案说明
- 方案一:把它集成到zzyl-framework模块中
- 方案二:单独创建一个模块,专门来管理OSS(推荐使用)
- 好处:可以实现模块化、解耦、复用和依赖管理,提高开发效率和代码质量
集成步骤
- 在父工程中创建子模块为:zzyl-oss,如下图所示

- 在父工程的pom文件中引入oss的依赖管理
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zzyl</groupId>
<artifactId>zzyl</artifactId>
<version>3.8.8</version>
<name>zzyl</name>
<url>http://www.ruoyi.vip</url>
<description>中州养老后台管理系统</description>
<properties>
<aliyun.sdk.oss>3.17.4</aliyun.sdk.oss>
</properties>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- 其他依赖省略... -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun.sdk.oss}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
- 在zzyl-oss模块中引入新的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zzyl</groupId>
<artifactId>zzyl</artifactId>
<version>3.8.8</version>
</parent>
<artifactId>zzyl-oss</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>com.zzyl</groupId>
<artifactId>zzyl-common</artifactId>
</dependency>
</dependencies>
</project>
- 创建oss配置类及客户端

- AliyunOSSProperties:配置类,读取aliyun.oss为前缀的属性值(桶名称,域名站点)
- AliyunOSSOperator:通过OSSClient对oss进行交互,提供了上传和删除两个方法
- zzyl-admin模块引入并配置
先把zzyl-oss模块注册到父工程中,在zzyl父工程的pom文件添加以下内容:
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- 其他依赖配置省略...-->
<!-- OSS模块 -->
<dependency>
<groupId>com.zzyl</groupId>
<artifactId>zzyl-oss</artifactId>
<version>${zzyl.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
在zzyl-admin模块中引入zzyl-oss:
<dependency>
<groupId>com.zzyl</groupId>
<artifactId>zzyl-oss</artifactId>
</dependency>
在application.yml文件中添加对oss的配置:
# 阿里云OSS相关配置
aliyun:
oss:
endpoint: ...
bucketName: ...
- 修改上传的接口
@Autowired
private AliyunOSSOperator aliyunOSSOperator;
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
// String fileName = FileUploadUtils.upload(filePath, file);
String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());
// String url = serverConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", url);
ajax.put("newFileName", FileUtils.getName(url));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
阅读文件上传相关的前端代码
- 文件位置:
src/components/imageUpload/index.vue
watch(() => props.modelValue, val => {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(",")
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
item = { name: baseUrl + item, url: baseUrl + item }
} else {
item = { name: item, url: item }
}
}
return item
})
} else {
fileList.value = []
return []
}
},{ deep: true, immediate: true })
baseUrl的定义:
const baseUrl = import.meta.env.VITE_APP_BASE_API
在测试环境下:
# 若依管理系统/开发环境
VITE_APP_BASE_API = '/dev-api'
测试是否能上传图片并展示:

定时任务
在入住办理模块中,在页面上发起入住申请并提交表单到后台,后台需要保存合同信息,而如果老人选择的入住开始时间在签约办理时的当前时间之后,合同状态会保存一个0:
/**
* 新增签约办理
*
* @param elder 老人信息
* @param contractNo 合同编号
* @param checkInApplyDto 入住申请信息
*/
private void insertContract(Elder elder, String contractNo, CheckInApplyDto checkInApplyDto) {
Contract contract = new Contract();
// 属性拷贝
BeanUtils.copyProperties(checkInApplyDto.getCheckInContractDto(), contract);
contract.setContractNumber(contractNo);
contract.setElderId(elder.getId());
contract.setElderName(elder.getName());
// 获取入住开始时间和入住结束时间
LocalDateTime startDate = checkInApplyDto.getCheckInConfigDto().getStartDate();
LocalDateTime endDate = checkInApplyDto.getCheckInConfigDto().getEndDate();
contract.setStartDate(startDate);
contract.setEndDate(endDate);
int status = startDate.isAfter(LocalDateTime.now()) ? 0 : 1;
contract.setStatus(status);
contractMapper.insert(contract);
}
而实际的业务场景是:随着时间的推移,如果当前时间超过了入住开始时间,就要将合同状态更新为已生效,这个需求可以借助定时任务来实现
定时任务概述
定时任务(也叫任务调度)是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了定时任务即可解放更多的人力,而是**由系统自动去执行任务**
常用业务场景案例:
- 某电商系统需要在每天上午10点,下午4点,晚上8点发放一批优惠券
- 某银行系统需要在信用卡到期还款日的前三天进行短信提醒
- 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总
- 12306会根据收票车站的不同,设置某几个时间点进行分批放票
如何实现任务调度:
- 多线程方式,结合sleep
- JDK提供的API,例如:Timer、ScheduledExecutor
- 框架,例如Quartz,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求,还有一些其他的,比如:XXL-JOB,DolphinScheduler
- SpringTask
cron表达式
在我们使用调度任务技术的时候,特别是调度框架,里面都支持使用日历的方式来设置任务制定的时间、频率等,通常情况下都会使用cron表达式来表达
cron表达式是一个字符串, 用来设置定时规则, 由七部分组成, 每部分中间用空格隔开, 每部分的含义如下表所示:
| 组成部分 | 含义 | 取值范围 |
|---|---|---|
| 第一部分 | Seconds (秒) | 0-59 |
| 第二部分 | Minutes(分) | 0-59 |
| 第三部分 | Hours(时) | 0-23 |
| 第四部分 | Day-of-Month(日) | 1月31日 |
| 第五部分 | Month(月) | 0-11或JAN-DEC |
| 第六部分 | Day-of-Week(星期) | 1-7(1表示星期日)或SUN-SAT |
| 第七部分 | Year(年) 可选 | 1970-2099 |
另外, cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:
| 符号 | 含义 |
|---|---|
| ? | 表示不确定的值。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为 “?” 。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?” |
| * | 代表所有可能的值 |
| , | 设置多个值,例如”26,29,33”表示在26分,29分和33分各自运行一次任务 |
| - | 设置取值范围,例如”5-20”,表示从5分到20分钟每分钟运行一次任务 |
| / | 设置频率或间隔,如”1/15”表示从1分开始,每隔15分钟运行一次任务 |
| L | 用于每月,或每周,表示每月的最后一天,或每个月的最后星期几,例如”6L”表示”每月的最后一个星期五” |
| W | 表示离给定日期最近的工作日,例如”15W”放在每月(day-of-month)上表示”离本月15日最近的工作日” |
| # | 表示该月第几个周X。例如”6#3”表示该月第3个周五 |
为了让大家更熟悉cron表达式的用法, 接下来我们给大家列举了一些例子, 如下表所示:
| cron表达式 | 含义 |
|---|---|
| */5 * * * * ? | 每隔5秒运行一次任务 |
| 0 0 23 * * ? | 每天23点运行一次任务 |
| 0 0 1 1 * ? | 每月1号凌晨1点运行一次任务 |
| 0 0 23 L * ? | 每月最后一天23点运行一次任务 |
| 0 26,29,33 * * * ? | 在26分、29分、33分运行一次任务 |
| 0 0/30 9-17 * * ? | 朝九晚五工作时间内每半小时运行一次任务 |
| 0 15 10 ? * 6#3 | 每月的第三个星期五上午10:15运行一次任务 |
SpringTask入门案例
- 导入maven依赖 spring-context - 项目中只要导入了springboot,相关依赖会自动导入,这一步无需操作
- 自定义定时任务类
package com.zzyl.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* @author alexis
*/
@Component
@Slf4j
public class MyTask {
/**
* 定时任务 每隔5秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask() {
log.info("定时任务开始执行:{}", LocalDateTime.now());
}
}
- 启动类添加注解 @EnableScheduling
package com.zzyl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class ZzylApplication {
public static void main(String[] args) {
SpringApplication.run(ZzylApplication.class, args);
}
}
若依中定时任务的使用
在若依框架中,也提供了定时任务相关的功能,并且做到了可视化管理,方便来管理和维护项目中的定时任务。
在使用若依提供的定时任务的时候,需要后端代码和后台系统协作才能更好的管理定时任务
编写后端代码
自定义任务类,在zzyl-nursing-platform模块下的com.zzyl.nursing.task包下新增HelloTask类,代码如下:
package com.zzyl.nursing.task;
import org.springframework.stereotype.Component;
@Component("helloTask")
public class HelloTask {
public void myTask() {
System.out.println("helloTask");
}
}
在系统中创建任务
找到系统监控–>定时任务,可以新增任务,如下:

- 任务名称:自定义,如:定时查询任务状态
- 任务分组:根据字典
sys_job_group配置,可自行进行配置 - 调用方法:设置后台任务方法名称参数
- cron表达式:可查询官方
cron表达式介绍 - 执行策略:定时任务自定义执行策略
- 是否并发:是否需要多个任务间同时执行
默认情况下,任务类只能定义在
zzyl-quartz模块下的com.zzyl.quartz.task包下,因为项目中做了白名单的设置,不然任务会创建失败,也可以修改白名单的设置:在zzyl-common模块中,找到com.zzyl.common.constant.Constants常量类
修改常量 JOB_WHITELIST_STR ,这是一个数组,可以添加任意的任务类包名 public static final String[] JOB_WHITELIST_STR = { “com.zzyl.quartz.task”, “com.zzyl.nursing.task” };
创建完成后,在定时任务列表页可以对任务进行操作

可以对任务进行:状态开启、修改、删除、查看详情、任务调度日志
定时更新合同状态
- 批量更新合同状态执行逻辑
在IContractService中新增方法,没有参数,来批量更新合同状态
/**
* 更新合同状态
*/
void updateContractStatus();
- 实现方法:
/**
* 更新合同状态
*/
@Override
public void updateContractStatus() {
// 1.查询状态为0的合同 && 合同开始时间小于等于当前时间
List<Contract> list = list(Wrappers.<Contract>lambdaQuery()
.eq(Contract::getStatus, 0)
.le(Contract::getStartDate, LocalDateTime.now())
.ge(Contract::getEndDate, LocalDateTime.now()));
// 2.修改状态为1
list.forEach(item -> {
item.setStatus(1);
});
// 3.批量更新
updateBatchById(list);
}
父子组件通信
用户在前端页面点击发起入住申请或者查看详情,会进入到入住详情页面,下图是需要用入住详情页面的前端效果图:

从效果图上可以看出来,这个页面共分为了四部分
- 基本信息
- 家属信息,可能会有多个
- 入住配置
- 签约办理
由于表单内容较多,如果把代码都写在一起,维护起来非常麻烦,一般企业中会抽取组件进行开发,最后抽取出来的组件需要父组件之间进行通信
设计思路
整体的组件设计思路如下:

- 在入住列表页面index.vue点击按钮发起入住申请或者点击查看按钮,都会进入到details.vue详情页,由于details.vue中的内容过多,因此将其拆分成4个单独的页面,然后在details.vue中统一引入,最终前端效果如下:

- 其中details.vue是父组件,components目录下的四个组件是子组件,最终四个子组件表单中的数据需要统一收集到父组件中,因此这里就涉及到了组件之间的通信
- 父组件引用子组件
- 子组件传数据到父组件
父子组件通信
先来通过下面这张图片认识一下什么是父子组件,图中有三个组件,组件A的子组件是B和C,其中B和C是兄弟关系。重点研究父子组件的通信,如果想要实现B和C的通信,需要使用第三方组件pinia才行

在 Vue中,父子组件间的通信是通过属性传递(props)和事件(events)的方式来进行的。
父组件向子组件传递数据
父组件向子组件传递数据通常是通过 props 来实现的。在 Vue 3 中,props 的定义和使用变得更加简洁
示例代码:
- 父组件(parent.vue)传递数据:
<template>
<div>
<child :message="parentMessage"></child>
</div>
</template>
<script setup>
import child from './child.vue';
const parentMessage = '来自父组件的消息';
</script>
在这个例子中,父组件通过 message 属性将数据传递给子组件 ChildComponent
- 子组件(child.vue)接收数据:
<template>
<div>{{ message }}</div>
</template>
<script setup>
defineProps({
message: String,
});
</script>
子组件通过 defineProps 钩子来声明它期望接收的 prop,并且在模板中使用这个 prop
子组件向父组件传递数据
子组件向父组件传递数据通常是通过自定义事件来实现的。
示例代码:
- 子组件(child.vue)触发事件:
<template>
<button @click="sendMessage">给父组件发消息</button>
</template>
<script setup>
const emit = defineEmits(['message-sent']);
function sendMessage() {
emit('message-sent', '来自子组件的消息');
}
</script>
在这个例子中,子组件定义了一个自定义事件 message-sent,并在按钮点击时触发这个事件,并传递一个消息。
- 父组件(parent.vue)监听事件:
<template>
<div>
<child @message-sent="handleMessage"></child>
</div>
</template>
<script setup>
import child from './child.vue';
function handleMessage(message) {
console.log('接收到了子组件的消息:', message);
}
</script>
父组件通过 @message-sent 监听子组件触发的事件,并定义了一个处理函数 handleMessage 来接收和处理子组件传递过来的消息。
表单构建
表单构建功能说明
打开菜单中的系统工具–>表单构建

使用表单构建的优点:
- 可视化操作与配置:表单构建功能提供了直观的可视化界面,开发者无需编写繁琐的代码,即可通过拖拽、配置等方式快速构建表单。这大大降低了开发难度,提高了开发效率。
- 高度自定义:若依框架支持丰富的表单字段类型,如文本框、下拉框、单选框、复选框等,并允许开发者根据需求自定义字段类型。同时,框架还提供了灵活的布局和样式设置,使得开发者能够构建出符合业务需求的个性化表单
- 数据校验与验证:表单构建功能内置了强大的数据校验和验证机制,能够确保表单数据的准确性和完整性。开发者可以为表单字段设置校验规则,如必填项、格式验证等,从而在用户提交表单时自动进行校验,减少错误数据的产生。
- 多类型导出:可根据业务情况,选择生成页面组件或者是弹窗组件
基本信息组件
以基本信息组件为例,演示一下怎样使用若依提供的表单构建功能。
效果图:

基于上图,我们可以使用表单生成来生成代码,每个组件的属性值可参考接口文档
| 字段描述 | 字段名 | 所用组件 |
|---|---|---|
| 老人姓名 | name | 单行文本 |
| 身份证号 | idCardNo | 单行文本 |
| 出生日期 | birthday | 日期选择 |
| 年龄 | age | 单行文本 |
| 性别 | sex | 单选框组 |
| 联系方式 | phone | 单行文本 |
| 家庭住址 | address | 多行文本 |
| 一寸照片 | image | 上传 |
| 身份证人像面 | idCardPortraitImg | 上传 |
| 身份证国徽面 | idCardNationalEmblemImg | 上传 |
- 相同颜色的字段为一组,它们属于一行,用到了行容器组件
- 字段名称可以参考接口文档或者elder表结构的字段名称
选择组件的效果如下图,只展示了部分

表单构建完成以后,找到导出vue文件,生成类型选择页面,把文件下载到本地备用

若依中的缓存使用
在目前的若依框架中,是使用Redis来作为缓存的,核心配置类如下:
- 在zzyl-framework模块中的com.zzyl.framework.config.RedisConfig类
- 作用:配置类,开启了缓存注解、对象序列化和反序列化
对象序列化是将对象转换为可存储或传输的字节序列的过程,这种序列化后的字节序列可以保存在文件,数据库或通过网络进行传输,将来需要用到对象时可以获取到序列化之后的结果,再反序列化为对象
想要实现序列化的类,必须实现Serializable接口
若依框架在RedisConfig中提供了序列化器和反序列化器,所以即使创建的类没有实现
Serializable接口也可以保存到Redis中
- 在com.zzyl.common.core.redis.RedisCache
- 封装了常见的操作Redis的方法,在业务开发中,可以直接使用这个类的方法来操作Redis

功能接口添加缓存
哪些数据需要添加放到缓存中
- 高频访问且变更较少的数据:用户信息(基本资料、权限)、配置信息(系统配置、业务规则等)
- 数据库查询结果:复杂的数据库查询(多表或量大且需要经常访问)
- 热门内容:热门文章、热门视频、热门评论等,访问量高的数据
- 临时数据:临时存储一些数据以便后续使用(如验证码、临时文件路径等),在内存中以便快速访问
- 需要快速访问的小数据集:某些情况下,即使数据不大,但如果需要频繁访问,可以考虑将其缓存起来以提高性能
缓存使用策略
添加缓存的思路
基本的流程如下图:

数据同步思路
- 先更新数据库,再删除缓存:这是最常用的策略之一。当数据更新时,首先更新数据库,然后删除缓存中的旧数据。这样,下次请求时缓存会重新从数据库中加载最新数据。这种方法避免了缓存和数据库之间的数据竞争,但需要注意删除缓存操作的成功性

- 设置缓存过期时间(不推荐):为缓存设置合理的过期时间,到期后缓存数据自动失效,从而触发从数据库中重新加载数据。这种方法适用于对数据一致性要求不是非常严格,但希望减少数据库访问压力的场景
相关案例查看中州养老-入住办理接口
