1 快速开始
1.1 项目介绍
01.介绍
a.概念
JeecgBoot 是一款基于代码生成器的低代码开发平台!
前后端分离架构 SpringBoot2.x和3.x,SpringCloud,Ant Design Vue3,Mybatis-plus,Shiro,JWT,支持微服务。
强大的代码生成器让前后端代码一键生成,实现低代码开发!
JeecgBoot集成AI模型能力,引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE),
帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
b.功能
JeecgBoot 提供了一系列低代码模块,实现在线开发真正的零代码:
Online表单开发、Online报表、报表配置能力、在线图表设计、仪表盘设计、大屏设计、移动配置能力、
表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)等等!
c.宗旨
简单功能由OnlineCoding配置实现,做到零代码开发;
复杂功能由代码生成器生成进行手工Merge 实现低代码开发,既保证了智能又兼顾灵活;
实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
d.业务流程
采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案:表单设计器、online配置表单、编码表单。
同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
02.使用项目
a.概念
Jeecg-Boot低代码开发平台,可以应用在任何J2EE项目的开发中,支持信创国产化(默认适配达梦和人大金仓)。
尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,
其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
b.项目说明
| 项目名 | 说明
|----------------|------------------------------
| jeecg-boot | 后端源码JAVA(SpringBoot微服务架构)
| jeecgboot-vue3 | 前端源码VUE3(vue3+vite5+ts最新技术栈)
| jeecg-uniapp | 配套APP框架 适配多个终端,支持APP、小程序、H5
c.主要特色
快速开发
低代码
Online表单
表单设计器
工作流引擎
流程设计器
Online报表
移动APP
-----------------------------------------------------------------------------------------------------
仪表盘设计
报表设计器
大屏设计器
代码生成器
数据权限
OA办公
微服务/分布式
SAAS
03.商业功能
a.汇总
表单设计器
仪表盘设计器
流程设计器
工作流引擎
大屏设计器
低代码应用
门户设计器
APP移动办公
APP布局设计
OA办公
Online图表
b.功能模块
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─AI助手
│ ├─系统编码规则
│ ├─系统校验规则
├─积木报表设计器
│ ├─打印设计器
│ ├─数据报表设计
│ ├─图形报表设计(支持echart)
├─系统管理
│ ├─用户管理
│ ├─角色管理
│ ├─菜单管理
│ ├─权限设置(支持按钮权限、数据权限)
│ ├─表单权限(控制字段禁用、隐藏)
│ ├─部门管理
│ ├─我的部门(二级管理员)
│ └─字典管理
│ └─分类字典
│ └─系统公告
│ └─职务管理
│ └─通讯录
│ ├─多数据源管理
│ └─多租户管理(租户管理、租户角色、我的租户)
├─消息中心
│ ├─消息管理
│ ├─模板管理
├─代码生成器(低代码)
│ ├─代码生成器功能(一键生成前后端代码,生成后无需修改直接用,绝对是后端开发福音)
│ ├─代码生成器模板(提供4套模板,分别支持单表和一对多模型,不同风格选择)
│ ├─代码生成器模板(生成代码,自带excel导入导出)
│ ├─查询过滤器(查询逻辑无需编码,系统根据页面配置自动生成)
│ ├─高级查询器(弹窗自动组合查询条件)
│ ├─Excel导入导出工具集成(支持单表,一对多 导入导出)
│ ├─平台移动自适应支持
├─系统监控
│ ├─Gateway路由网关
│ ├─性能扫描监控
│ │ ├─监控 Redis
│ │ ├─Tomcat
│ │ ├─jvm
│ │ ├─服务器信息
│ │ ├─请求追踪
│ │ ├─磁盘监控
│ ├─定时任务
│ ├─系统日志
│ ├─消息中心(支持短信、邮件、微信推送等等)
│ ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
│ ├─系统通知
│ ├─SQL监控
│ ├─swagger-ui(在线接口文档)
│─报表示例
│ ├─曲线图
│ └─饼状图
│ └─柱状图
│ └─折线图
│ └─面积图
│ └─雷达图
│ └─仪表图
│ └─进度条
│ └─排名列表
│ └─等等
│─大屏模板
│ ├─作战指挥中心大屏
│ └─物流服务中心大屏
│─常用示例
│ ├─自定义组件
│ ├─对象存储(对接阿里云)
│ ├─JVXETable示例(各种复杂ERP布局示例)
│ ├─单表模型例子
│ └─一对多模型例子
│ └─打印例子
│ └─一对多TAB例子
│ └─内嵌table例子
│ └─常用选择组件
│ └─异步树table
│ └─接口模拟测试
│ └─表格合计示例
│ └─异步树列表示例
│ └─一对多JEditable
│ └─JEditable组件示例
│ └─图片拖拽排序
│ └─图片翻页
│ └─图片预览
│ └─PDF预览
│ └─分屏功能
│─封装通用组件
│ ├─行编辑表格JEditableTable
│ └─省略显示组件
│ └─时间控件
│ └─高级查询
│ └─用户选择组件
│ └─报表组件封装
│ └─字典组件
│ └─下拉多选组件
│ └─选人组件
│ └─选部门组件
│ └─通过部门选人组件
│ └─封装曲线、柱状图、饼状图、折线图等等报表的组件(经过封装,使用简单)
│ └─在线code编辑器
│ └─上传文件组件
│ └─验证码组件
│ └─树列表组件
│ └─表单禁用组件
│ └─等等
│─更多页面模板
│ ├─各种高级表单
│ ├─各种列表效果
│ └─结果页面
│ └─异常页面
│ └─个人页面
├─高级功能
│ ├─提供单点登录CAS集成方案
│ ├─提供APP发布方案
│ ├─集成Websocket消息通知机制
│─更多商业功能
│ ├─流程设计器
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─。。。
04.技术架构
a.后端
IDE建议: IDEA (必须安装lombok插件 )
语言:Java 8+ (支持17)
依赖管理:Maven
基础框架:Spring Boot 2.7.18
微服务框架: Spring Cloud Alibaba 2021.0.1.0
持久层框架:MybatisPlus 3.5.3.2
报表工具: JimuReport 1.7.6
安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
数据库连接池:阿里巴巴Druid 1.1.22
日志打印:logback
缓存:Redis
其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
默认数据库脚本:MySQL5.7+
b.前端
前端IDE建议:WebStorm、Vscode
采用 Vue3.0+TypeScript+Vite+Ant-Design-Vue等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
最新技术栈:Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
依赖管理:node、npm、pnpm
c.前端环境要求
本地环境安装 Node.js 、npm 、pnpm
Node.js 版本建议v20.15.0,要求Node 20+ 版本以上
( 因为Vite5 不再支持已 EOL 的 Node.js 14 / 16 / 17 / 19,现在需要 Node.js 18 / 20+ )
d.支持库
MySQL
Oracle11g
Sqlserver2017
PostgreSQL
MariaDB
MariaDB
达梦
人大金仓
TiDB
e.微服务解决方案
1、服务注册和发现 Nacos
2、统一配置中心 Nacos
3、路由网关 gateway(三种加载方式)
4、分布式 http feign
5、熔断降级限流 Sentinel
6、分布式文件 Minio、阿里OSS
7、统一权限控制 JWT + Shiro
8、服务监控 SpringBootAdmin
9、链路跟踪 Skywalking 参考文档
10、消息中间件 RabbitMQ
11、分布式任务 xxl-job
12、分布式事务 Seata
13、轻量分布式日志 Loki+grafana套件
14、支持 docker-compose、k8s、jenkins
15、CAS 单点登录
16、路由限流
05.JEECG 社区开源项目下载
a.JeecgBoot 快速开发平台(SpringBoot+AntDesignVue+Mybatis —— 前后端分离版本)
授权方式: 开源免费
平台源码: https://github.com/jeecgboot/JeecgBoot
快速入门: 入门教程 | 技术文档 | 常见问题
QQ交流群: ⑩716488839、⑨808791225(满)、其他(满)
版本日志: 版本日志
b.JimuReport积木报表(企业级Web报表工具,像搭建积木一样在线设计报表)
授权方式: 免费
功能说明: 类似于excel操作风格,通过拖拽完成报表设计,功能涵盖: 数据报表、打印设计、图表报表、大屏设计等!
QQ交流群: ③596660273、其他 (满)
源码下载: https://github.com/jeecgboot/JimuReport
快速入门: 集成文档 | 新手视频
版本日志: 版本日志
c.Jeecg-Uniapp 移动解决方案 (Uniapp+ColorUI —— APP开发框架)
授权方式: 开源免费
源码下载: https://github.com/jeecgboot/jeecg-uniapp
快速入门: 入门教程
d.MiniDao 持久层(轻量级JAVA持久层)
授权方式: 开源免费
下载地址: https://github.com/jeecgboot/MiniDao
版本日志: 版本日志
e.AutoPOI (Excel和 Word简易工具类——JeecgBoot新版采用)
授权方式: 开源免费
源码下载: https://github.com/jeecgboot/autopoi
版本日志: 版本日志
f.Jeewx-api 微信接口封装SDK(1.3 版本)
授权方式: 开源免费
下载地址: https://github.com/jeecgboot/jeewx-api
版本日志: 版本日志
g.vue-print-nb-jeecg 打印组件(JeecgBoot采用)
授权方式: 开源免费
源码下载: https://github.com/jeecgboot/vue-print-nb-jeecg
06.SAS权限校验
a.背景
Spring 团队正式宣布 Spring Security OAuth 停止维护,该项目将不会再进行任何的迭代
目前 Spring 生态中的 OAuth2 授权服务器是 Spring Authorization Server 已经可以正式生产使用
作为 SpringBoot 3.0 的最新权限方案,JeecgBoot springboot3_sas分支,
已经完成了采用Spring Authorization Server 替换 Shiro工作。
b.源码
JeecgBoot SAS分支
Date: 2024-01-17
技术栈: SpringBoot3+ Spring Authorization Server+jdk18
重要提示:当前是beta阶段,正式项目不建议使用
源码下载:
后端:https://github.com/jeecgboot/jeecg-boot/tree/springboot3_sas
前端:https://github.com/jeecgboot/jeecgboot-vue3/tree/springboot3_sas
c.登录对接
a.登录对接
jeecg 基于Spring Authorization Server扩展了四种登录实现,加上默认提供的四种,共计有8种登录方式,额外还有OpenID Connect模式。
本文不讲解授权码模式、客户端模式、刷新码模式、设备码模式、OpenID Connect模式,
只会讲解jeecg实际应用了的四种扩展模式,其它模式请查阅Spring Authorization Server官方原文。
https://docs.spring.io/spring-authorization-server/reference/overview.html
注意:OpenID Connect应当仅为认证阶段使用,不可作为权限校验阶段使用。
b.密码模式和APP模式
密码模式在Oauth2.1协议中被放弃,Spring Authorization Server并没有对该模式提供实现,该实现是基于Spring Authorization Server提供的扩展入口实现的。
密码模式实现源码:package org.jeecg.config.security.password;
APP模式实现源码:package org.jeecg.config.security.app;
密码模式与APP模式实现完全一致,不过防止额外需求偏差,所以进行了分开实现。
c.phone模式
phone模式用于手机+验证码登录场景。
phone模式实现源码:package org.jeecg.config.security.phone;
d.自定义新登录规则
可以直接参考password目录或者phone目录去写,
jeecg-boot-base-core/org/jeecg/config/security/password
jeecg-boot-base-core/org/jeecg/config/security/phone
创建casConvert、casToken、casProvider,在provider里面就照着passwordProvider里面的抄,唯一不同的就是根据什么条件获取用户信息,
比如password,就是根据用户名找用户信息,再来匹配密码是否正确,
phone就是根据手机号+验证号,然后ticket看里面有什么信息,如果只有用户名,也可以单纯用用户名来获取用户信息,然后后面操作就都是一样的了,直接搬过去。
不过这样的话,等于默认ticket是合法的,只要是从ticket获取到的用户名,我们就认为是合法的,这时我们就需要对ticket进行验伪操作,验证ticket是合法的
d.权限校验
可用于方法或类上,将基于注解的权限code,针对性处理方法或当前类的所有接口进行权限拦截。
a.基于角色
// shiro用法
@RequiresRoles("admin")
// 可替换为 spring authorization server 用法
@PreAuthorize("jps.requiresRoles('admin')")
b.基于权限
// shiro用法
@RequiresPermissions("sys:role")
// 可替换为 spring authorization server 用法
@PreAuthorize("jps.requiresPermissions('sys:role')")
// 角色和权限组合使用
- @PreAuthorize("@jps.requiresPermissions('system:quartzJob:add') or @jps.requiresRoles('admin')")
c.免登录配置
jeecg:
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/jmreport/bigscreen2/**
# 替换为
security:
oauth2:
client:
ignore-urls:
- /test/jeecgDemo/demo3
- /test/jeecgDemo/redisDemo/**
- /jmreport/bigscreen2/**




1.2 开发环境
00.克隆JeecgBoot_20241120_init
a.第1处
jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
-----------------------------------------------------------------------------------------------------
datasource:
master:
url: jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
-----------------------------------------------------------------------------------------------------
#redis 配置
redis:
database: 0
host: 127.0.0.1
port: 6379
password: myslayers
#分布式锁
redisson:
address: 127.0.0.1:6379
password: myslayers
type: STANDALONE
enabled: true
b.第2处
系统监控 -> 数据库管理
-----------------------------------------------------------------------------------------------------
数据源名称:本地测试库
数据库类型:MySQL5.5
驱动类:com.mysql.jdbc.Driver
数据源地址:jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false
用户名:root
密码:123456
c.第3处
低代码开发 -> 积木报表设计 -> 任意报表 -> 数据集管理 -> 维护数据源
-----------------------------------------------------------------------------------------------------
数据源名称:本地测试库
数据库类型:MySQL5.5
驱动类:com.mysql.jdbc.Driver
数据源地址:jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false
用户名:root
密码:123456
d.第4处
低代码开发 -> 仪表盘设计器 -> 任意仪表盘 -> 数据源头
-----------------------------------------------------------------------------------------------------
数据源名称:本地测试库
数据库类型:MySQL5.5
驱动类:com.mysql.jdbc.Driver
数据源地址:jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false
用户名:root
密码:123456
e.快速启动
admin
123456
管理员
-----------------------------------------------------------------------------------------------------
zhangsan
qwER159263!
临时角色
-----------------------------------------------------------------------------------------------------
admin
admin@Xxgcz2023
01.环境搭建
a.前端环境
node v20.15.0
pnpm store prune
pnpm install
b.后端环境
JDK17
maven3.9.8
Mysql8.0
Redis 3.0.504
c.开发工具
IDEA后端开发工具
Webstorm前端开发工具
Navicat数据库连接工具
02.启动JAVA项目:jeecg-boot
a.初始化数据库,要求 mysql5.7+
执行Sql脚本: jeecg-boot/db/jeecgboot-mysql-5.7.sql
脚本作用:会自动创建库jeecg-boot, 并初始化数据
b.安装Maven依赖
通过右侧父POM进行install(下载依赖和打包)
c.修改配置文件
jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
-----------------------------------------------------------------------------------------------------
datasource:
master:
url: jdbc:mysql://127.0.0.1:3307/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
-----------------------------------------------------------------------------------------------------
#redis 配置
redis:
database: 0
host: 127.0.0.1
port: 6379
password: myslayers
#分布式锁
redisson:
address: 127.0.0.1:6379
password: myslayers
type: STANDALONE
enabled: true
d.启动项目
jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java
e.接口文档
http://localhost:8080/jeecg-boot/doc.html
03.启动VUE项目:jeecgboot-vue3
a.下载前端依赖
pnpm store prune
执行命令 pnpm install 或者双击 pinstall
b.配置后台接口
修改接口地址 .env.development
-----------------------------------------------------------------------------------------------------
# 跨域代理,您可以配置多个, 请注意没有换行符
#后台接口全路径地址(必填)
VITE_PROXY = [["/jeecgboot","http://localhost:8080/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
VITE_GLOB_DOMAIN_URL=http://localhost:8080/jeecg-boot
c.启动前端项目
npm run dev
d.访问
http://localhost:3100
admin/123456
04.MongoDB配置
a.MongoDB的yml配置
spring:
data:
mongodb:
uri: mongodb://账号:密码@127.0.0.1:27017/数据库名称?readPreference=secondaryPreferred&maxIdleTimeMS=60000&waitQueueTimeoutMS=2000&minPoolSize=5&maxPoolSize=100&maxLifeTimeMS=0&connectTimeoutMS=2000&socketTimeoutMS=2000
print: true
slowQuery: true
slowTime: 1000
b.配置MongoDB连接池
spring.data.mongodb.uri=mongodb://username:[email protected]:27017,192.168.1.2:27017,192.168.1.3:27017/jeecg?readPreference=secondaryPreferred&maxIdleTimeMS=60000&waitQueueTimeoutMS=2000&minPoolSize=5&maxPoolSize=100&maxLifeTimeMS=0&connectTimeoutMS=2000&socketTimeoutMS=2000
1.3 开发目录
01.后端目录
a.jeecg-boot
jeecg-boot目录结构
├─jeecg-boot-parent 父POM: 项目依赖、modules组织
│ ├─jeecg-boot-base-core 共通模块: 工具类、config、权限、查询过滤器、注解、接口等
│ ├─jeecg-module-demo 示例代码
│ ├─jeecg-module-system System系统管理目录
│ │ ├─jeecg-system-biz System系统管理权限
│ │ ├─jeecg-system-start 单体启动项目(8080)
│ │ ├─jeecg-system-api System系统管理模块对外api
│ │ │ ├─jeecg-system-cloud-api System模块对外提供的微服务接口
│ │ │ ├─jeecg-system-local-api System模块对外提供的单体接口
│ ├─jeecg-server-cloud 微服务模块
├─jeecg-cloud-gateway 微服务网关模块(9999)
├─jeecg-cloud-nacos Nacos服务模块(8848)
├─jeecg-system-cloud-start System微服务启动项目(7001)
├─jeecg-demo-cloud-start Demo微服务启动项目(7002)
├─jeecg-visual
├─jeecg-cloud-monitor 微服务监控模块 (9111)
├─jeecg-cloud-xxljob 微服务xxljob定时任务服务端 (9080)
├─jeecg-cloud-sentinel sentinel服务端 (9000)
├─jeecg-cloud-test 微服务测试示例(各种例子)
├─jeecg-cloud-test-more 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
├─jeecg-cloud-test-rabbitmq 微服务测试示例(rabbitmq)
├─jeecg-cloud-test-seata 微服务测试示例(seata分布式事务)
├─jeecg-cloud-test-shardingsphere 微服务测试示例(分库分表)
b.jeecg-boot-starter
jeecg-boot-starter目录结构
├── jeecg-boot-starter starter父模块
├── jeecg-boot-common 底层常量、redis等基础依赖
├── jeecg-boot-starter-cloud 微服务启动starter
├── jeecg-boot-starter-job xxl-job定时任务starter
├── jeecg-boot-starter-lock 分布式锁starter
├── jeecg-boot-starter-rabbitmq 消息中间件starter
├── jeecg-boot-starter-seata 分布式事务starter
├── jeecg-boot-starter-shardingsphere 分库分表starter
├── jeecg-boot-starter-mongon mongon集成starter
├──jeecg-boot-starter-chatgpt chatGpt集成start
02.前端目录
├── build # 打包脚本相关
│ ├── config # 配置文件
│ ├── generate # 生成器
│ ├── script # 脚本
│ └── vite # vite配置
├── mock # mock文件夹
├── public # 公共静态资源目录
├── src # 主目录
│ ├── api # 接口文件
│ ├── assets # 资源文件
│ │ ├── icons # icon sprite 图标文件夹
│ │ ├── images # 项目存放图片的文件夹
│ │ └── svg # 项目存放svg图片的文件夹
│ ├── components # 公共组件
│ ├── design # 样式文件
│ ├── directives # 指令
│ ├── enums # 枚举/常量
│ ├── hooks # hook
│ │ ├── component # 组件相关hook
│ │ ├── core # 基础hook
│ │ ├── event # 事件相关hook
│ │ ├── setting # 配置相关hook
│ │ └── web # web相关hook
│ ├── layouts # 布局文件
│ │ ├── default # 默认布局
│ │ ├── iframe # iframe布局
│ │ └── page # 页面布局
│ ├── locales # 多语言
│ ├── logics # 逻辑
│ ├── main.ts # 主入口
│ ├── router # 路由配置
│ ├── settings # 项目配置
│ │ ├── componentSetting.ts # 组件配置
│ │ ├── designSetting.ts # 样式配置
│ │ ├── encryptionSetting.ts # 加密配置
│ │ ├── localeSetting.ts # 多语言配置
│ │ ├── projectSetting.ts # 项目配置
│ │ └── siteSetting.ts # 站点配置
│ ├── store # 数据仓库
│ ├── utils # 工具类
│ └── views # 页面
├── test # 测试
│ └── server # 测试用到的服务
│ ├── api # 测试服务器
│ ├── upload # 测试上传服务器
│ └── websocket # 测试ws服务器
├── types # 类型文件
├── vite.config.ts # vite配置文件
└── windi.config.ts # windcss配置文件
03.新建module模块
a.说明
新建一个单体的模块,并引入到system中使用
请按照包名规则 org.jeecg.modules.* 进行初始化,不然bean扫描不到!
执行mvn 命令生成erp模块, IDEA 可以直接图形化使用
b.方式1:使用archetype命令新建module模块
// 注意: windows下可直接复制执行, Linux/Macos下 ^ 修改成 \
mvn archetype:generate ^
-DgroupId=org.jeecgframework.boot ^
-DartifactId=jeecg-module-erp ^
-Dversion=3.7.0 ^
-DarchetypeGroupId=org.jeecgframework.archetype ^
-DarchetypeArtifactId=jeecg-boot-gen ^
-DarchetypeVersion=2.0
说明: -DarchetypeVersion=2.0版本号固定不需要修改,3.7.0根据自己项目的版本号进行调整。
c.方式2:使用IDEA图形新建模块
a.导航
新建模块 -> Java -> jeecg-module-oa -> Maven、jdk-1.8.0_281、JEECG BOOT 3.7.1
b.简单说明
jeecg-boot-module-system作为启动项目,所以其他模块不要引用system。
jeecg-boot-base-core作为基础Core,所以新建模块一定要引用。
-------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
c.如果业务模块需要调用system里面的业务方法怎么办呢?
a.单体模式,可以引入jeecg-system-local-api
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
b.微服务模式,直接引入jeecg-boot-starter-cloud即可
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-cloud</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
d.swagger配置: 在swagger配置新模块的swagger扫描bean.找到Swagger2Config。追加如下Bean配置。
@Bean
public Docket loanApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()).groupName("会员模块")
.select()
//此包路径下的类,才生成接口文档
.apis(RequestHandlerSelectors.basePackage("org.jeecg.crm")) --修改成自己的包
//加了ApiOperation注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) --配置ApiOperation项
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securityScheme()));
//.globalOperationParameters(setHeaderToken());
}
e.mybatis-plus配置
a.jeecg-server\jeecg-module-system\jeecg-system-start\src\main\resources\application-dev.yml
mybatis-plus:
mapper-locations: classpath*:org/jeecg/modules/**/xml/*Mapper.xml,classpath*:com/sanyitu/yuanqu/**/xml/*Mapper.xml
b.org/jeecg/config/mybatis/MybatisPlusSaasConfig.java
@Configuration
@MapperScan(value={"org.jeecg.modules.**.mapper*","org.jeecg.loan.**.mapper*","org.jeecg.camunda.**.mapper*","org.jeecg.story.**.mapper*"})
public class MybatisPlusSaasConfig
c.jeecg-server\jeecg-module-system\jeecg-system-start\src\main\java\org\jeecg\JeecgSystemApplication.java
/**
* 单体启动类
* 报错提醒: 未集成mongo报错,可以打开启动类上面的注释 exclude={MongoAutoConfiguration.class}
*/
@Slf4j
@SpringBootApplication
@ComponentScan({"org.jeecg", "org.myslayers"})
//@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
public class JeecgSystemApplication extends SpringBootServletInitializer
f.新建HelloController代码
package org.jeecg.modules.hello;
import org.jeecg.common.api.vo.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
/**
* 测试新的module
* @author chengtg
*
*/
@Slf4j
@Api(tags="新建module--jm")
@RestController
@RequestMapping("/hello")
public class HelloController {
@ApiOperation("测试hello方法")
@GetMapping(value="/")
public Result<String> hello(){
Result<String> result = new Result<String>();
result.setResult("hello word!");
result.setSuccess(true);
return result;
}
}
g.去掉token拦截限制
如果不需要token可以访问,可以在 jeecg-boot-module-system 项目中修改 ShiroConfig.java 配置类
jeecg-server\jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\ShiroConfig.java
-------------------------------------------------------------------------------------------------
@Slf4j
@Configuration
public class ShiroConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
@Autowired
private Environment env;
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired(required = false)
private RedisProperties redisProperties;
/**
* Filter Chain定义说明
*
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//支持yml方式,配置拦截排除
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
String[] permissionUrl = shiroExcludeUrls.split(",");
for(String url : permissionUrl){
filterChainDefinitionMap.put(url,"anon");
}
}
}
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/hello/**", "anon"); //测试新添加的module,不带token访问
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
}
}

1.4 上线发布
01.前端vue3构建
a.配置后台接口和编译项目
a.配置后台接口地址
修改.env.production
VITE_GLOB_API_URL=/jeecgboot
VITE_GLOB_DOMAIN_URL=http://api3.boot.jeecg.com
重要提醒:请把api3.boot.jeecg.com换成自己的后台域名。
b.编译打包项目
进入根目录
cd jeecgboot-vue3
-------------------------------------------------------------------------------------------------
打包编译
pnpm install
pnpm run build
b.通过nginx部署前端
a.通过 nginx 配置请求转发到后台接口
boot3.jeecg.com是官方Vue3前端演示地址,请把下面的boot3.jeecg.com 全部换成自己的前端域名。
/jeecgboot/对应的是 步骤一.1中的参数 VITE_GLOB_API_URL,这个可以不改避坑。
b.配置文件
upstream boot3.jeecg.com {
server 127.0.0.1:80;
}
server {
listen 80;
server_name boot3.jeecg.com;
#前端打的dist资源存放目录
root /srv/www/project;
location / {
# 用于配合 browserHistory使用
try_files $uri $uri/ /index.html;
}
location /jeecgboot/ {
#后台接口地址(我们部署去掉了/jeecg-boot项目名,如果你有请加上)
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
#真实IP获取
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
set $my_proxy_add_x_forwarded_for $proxy_add_x_forwarded_for;
if ($proxy_add_x_forwarded_for ~* "127.0.0.1"){
set $my_proxy_add_x_forwarded_for $remote_addr;
}
proxy_set_header X-Forwarded-For $my_proxy_add_x_forwarded_for;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
c.前端dist说明
将dist内容,放到服务器的 /srv/www/project 目录下。
/srv/www/project 允许自定义,但需要与nginx里面的配置一致。
d.编译后修改后台配置
dist\_app.config.js
e.其他技巧
前端不缓存html,防止程序更新后缓存继续生效
location / {
# 不缓存html,防止程序更新后缓存继续生效
if ($request_filename ~* .*\.(?:htm|html)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
access_log on;
}
}
02.JAR部署方案
a.服务器建议
建议服务器常规配置:4核+8G内存+50G硬盘。当然配置越高越好,为了节省成本2核/4GB也可以跑
b.jeecg-boot项目打JAR包
a.修改配置文件 application-prod.yml
修改数据库连接、缓存redis、上传附件等配置
b.切换Maven为生产模式
dev/prod/test
c.通过jeecg-boot-parent打包
install
d.拿到 jeecg-system-start-{版本号}.jar 包
jeecg-system-start-3.5.0.jar
c.通过JAR方式启动后台
Window启动命令:
java -jar jeecg-system-start-3.5.0.jar
-----------------------------------------------------------------------------------------------------
Linux下后台进程启动命令:
nohup java -jar jeecg-system-start-3.5.0.jar >catalina.out 2>&1 &
-----------------------------------------------------------------------------------------------------
关掉项目:
ps -ef|grep java
kill 进程号
d.通过nginx部署Vue3前端
a.配置接口地址
配置文件:.env.development
请把http://localhost:8080/jeecg-boot 的两个地方替换成自己后台访问地址,其他不用改。
-------------------------------------------------------------------------------------------------
#后台接口父地址(必填)
VITE_GLOB_API_URL=/jeecgboot
# 跨域代理,您可以配置多个 ,请注意,没有换行符
VITE_PROXY = [["/jeecgboot","http://localhost:8080/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://localhost:8080/jeecg-boot
b.nginx配置前端
配置说明:
boot3.jeecg.com是官方Vue3前端演示地址,请把下面的boot3.jeecg.com 全部换成自己的前端域名
/jeecgboot/下面的proxy_pass对应的是 通过 nginx 配置请求转发到后台接口
-------------------------------------------------------------------------------------------------
upstream boot3.jeecg.com {
server 127.0.0.1:80;
}
server {
listen 80;
server_name boot3.jeecg.com;
#前端打的dist资源存放目录
root /srv/www/project;
location / {
# 用于配合 browserHistory使用
try_files $uri $uri/ /index.html;
}
location /jeecgboot/ {
#后台接口地址(我们部署去掉了/jeecg-boot项目名,如果你有请加上)
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
#真实IP获取
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
set $my_proxy_add_x_forwarded_for $proxy_add_x_forwarded_for;
if ($proxy_add_x_forwarded_for ~* "127.0.0.1"){
set $my_proxy_add_x_forwarded_for $remote_addr;
}
proxy_set_header X-Forwarded-For $my_proxy_add_x_forwarded_for;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
c.前端dist位置说明
将dist内容,放到服务器的 /srv/www/project 目录下。
/srv/www/project 允许自定义,但需要与nginx里面的配置一致。
d.nginx配置完,需要重启nginx
略
e.访问应用
通过:http://你的域名 访问项目,出现如下页面,使用账户/密码:admin/123456 登录成功即可
f.后台配置独立api域名(nginx映射)
nginx监听:80端口
绑定域名(示例):api3.boot.jeecg.com
-----------------------------------------------------------------------------------------------------
upstream api3.boot.jeecg.com {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name api3.boot.jeecg.com;
location / {
root html;
index index.html index.htm;
proxy_pass http://api3.boot.jeecg.com;
#ip remote_addr
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
g.Websocket配置
提醒: 有的版本nginx配置需要加上 Upgrade 和Connection 协议头,不然WebSocket会404
#支持websocket得这么写,不然CentOS上可能失败
# proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
03.开启Nginx压缩
a.开启Nginx压缩,解决前端访问慢问题
略
b.nginx.conf 的 http 中加入以下片断
# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
04.ChatGPTAI助手
a.说明
JeecgBoot 从3.6.3+版本起,默认提供了ChatGPT AI助手对话功能。
b.配置文件
修改application-dev.yml配置文件,先把jeecg.enabled开关打开。
jeecg:
# ChatGPT对接配置
ai-chat:
# 是否开启
enabled: true
# openAi接口秘钥,填写自己的apiKey
apiKey: ???
# openAi域名,有代理就填代理的域名。默认:openAI官方apiHost
apiHost: https://api.openai.com
# 超时时间单位:s。默认 60s
timeout: 60
c.ChatGPT 的apiKey申请
配置文件主要需要ChatGPT 的apiKey和ChatGPT的域名地址,apiKey通过ChatGPT 官方申请即可;
ChatGPT的域名国内被禁的情况下,可以自己搭建代理
d.ChartGPT 搭建境外代理步骤
购买境外的云服务器,并确保ip地址的ASN归属地是海外。
登录服务器,安装nginx。
编辑nginx配置文件vim /etc/nginx/nginx.conf,将所有请求转发到api.openai.com
server {
# ...other setting ....
location / {
proxy_pass https://api.openai.com/;
proxy_ssl_server_name on;
proxy_set_header Host api.openai.com;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ...other setting ....
}
05.上线安全配置
a.说明
低代码开发因为智能所以存在一些特殊的写法,可能存在被攻击的情况,所以为了系统安全稳固,
jeecg推出 平台上线安全配置,上线正式发布可以选择关闭在线配置能力,彻底杜绝被攻击的风险。
重点参数:jeecg.firewall.lowCodeMode 此参数开启会关闭所有在线配置能力,admin账号和允许开发角色保留权限。
-----------------------------------------------------------------------------------------------------
v3.5.5+ 生效
jeecg:
# 平台上线安全配置
firewall:
# 数据源安全 (开启后,Online报表和图表的数据源为必填)
dataSourceSafe: true
# 禁止online报表使用 * 查询(3.7.1+)
disableSelectAll: true
# 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
lowCodeMode: prod
# 表字典安全模式(white:白名单——配置了白名单的表才能通过表字典方式访问,black:黑名单——配置了黑名单的表不允许表字典方式访问)
tableDictMode: white
b.表白名单配置
a.说明
在我们的系统中,提供了许多允许用户自定义 SQL 语句的功能, 例如 online报表配置sql 与 字典组件配置查询字段 等。
这些自定义的 SQL 或配置查询字段可能导致敏感数据泄露。
尽管可以通过配置黑名单来避免此问题,但不能确保所有敏感表或字段都被包括在内。
因此,我们建议您配置白名单,以限定可以查询的表和字段。
注意:该白名单配置适用于 online报表、online图表 和其他的表字典查询组件。
b.自动配置
在开发环境中,你可以将 lowCodeMode 设置为 dev。
这样,如果在您执行查询语句时存在未被包含在白名单中的表或字段,系统会自动向数据库中添加,将其配置为白名单。
c.手动配置
本项目提供了一个手动配置白名单的交互界面。你可以系统管理 --> 表白名单配置进行访问和管理。
请注意:白名单内的表名必须是唯一的。如果有重复的表名,你可以编辑相关记录。若需要配置多个字段名,请使用半角逗号进行分割。
c.SQL表字典黑名单
a.说明
在jeecg系统中有很多地方允许用户自己定义sql语句,如online报表配置sql、如字典组件配置查询字段等等,
这些定义的sql语句或是配置的查询字段名,可能会造成一些敏感数据的泄露。
所以需要一个配置,申明特定的表或字段是禁止查询的。
b.配置类
org.jeecg.common.util.security.AbstractQueryBlackListHandler
c.配置说明
1.往ruleMap中配置键值对,key是表名,value是字段,多个字段以逗号隔开
2.ruleMap.put("sys_user", "*") 表示 sys_user所有的字段不支持查询
3.ruleMap.put("sys_user", "username,password")表示sys_user中的username和password不支持查询
d.测试online报表效果
定义sql: select password from sys_user 解析报错:
d.系统数据源安全
a.说明
配置路径:jeecg.firewall.dataSourceSafe
默认不开启,设置为true后开启安全模式。
b.开启后的效果
Online报表的数据源字段变为必填
Online图标的数据源字段变为必填
e.在线配置功能拦截地址
a.说明
平台上线安全配置:低代码模式切换成prod后,配置的请求地址将被禁止访问;
b.配置类
org.jeecg.config.firewall.interceptor.enums.LowCodeUrlsEnum
c.配置说明:
请求地址添加到LowCodeUrlsEnum后,低代码>发布模式下,请求将被禁止访问。
06.国产化适配
a.东方通TongWeb部署方案
略
b.JeecgBoot集成东方通TongRDS
略
c.JeecgBoot集成TiDB
略
d.宝兰德AppServer部署方案
略
e.JeecgBoot集成宝兰德CacheDB
略
07.JAR分离打包部署(Springboot瘦身(lib和程序分开打包):Version: 3.5+ 版本)
a.首先用mvn clean package正常打出jar包
这个jar包可能有几百兆大小,lib占了绝大多数
将jar包解压,将 BOOT-INF 下的 lib 包拿出单独存放
b.修改jeecg-system-start 项目的pom重新打包
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout>
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
再次用 mvn clean package 打出来的包就不包含lib了
c.运行jar包
#C:\111111\lib为lib包路径,执行下面命令启动项目。
java -Dloader.path=C:\111111\lib -jar jeecg-system-start-3.4.0.jar
d.目录结构
注意:必须jar同级目录创建config,并把system下的yml拷贝到config里面
08.WAR部署方案
a.方案
采用nginx+tomcat部署方案
后端服务发布到tomcat中
前端部署到nginx中
b.后台项目jeecg-boot打war包
后台项目jeecg-system-start 打war包之前进行如下改动
a.pom.xml文件中项目格式设置为war
<packaging>war</packaging>
b.pom.xml文件删除插件spring-boot-maven-plugin
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
c.注释掉WebSocketConfig的 ServerEndpointExporter注册方法
路径:jeecg-boot-base/jeecg-boot-base-core/org.jeecg.config.WebSocketConfig
d.修改配置文件application-prod.yml
a.修改数据库连接
b.修改缓存redis配置
c.修改上传附件配置
d.切换生产模式打包
e.首先执行下jeecg-boot-parent的install 操作
f.然后 maven package 打war包
c.后台项目jeecg-boot部署到tomcat中
1、设置tomcat端口号 8080,设置tomcat编码 URIEncoding="UTF-8"
2、部署项目到tomcat的目录webapps/jeecg-boot工程目录下
部署完后通过http://localhost:8080/jeecg-boot/doc.html 访问项目接口地址,说明部署成功!!
注意:
1.tomcat解压war后的目录名称即你访问的根路径,这里是jeecg-boot
d.前台Vue3项目 jeecgboot-vue3 build打包
a.修改接口地址配置 .env.production
# 跨域代理,您可以配置多个 ,请注意,没有换行符
VITE_PROXY = [["/jeecgboot","http://localhost:8080/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://localhost:8080/jeecg-boot
b.build打包
使用build命令打包项目
build完成后 ,会生成一个dist的目录。
c.nginx部署前端项目
拷贝dist下的代码到nginx安装目录下html目录中,即可
e.前台Vue3项目的nginx配置
a.说明
方案说明:前端配置一个域名、后端配置一个域名,分别通过nginx映射。
示例中:java api的服务域名 api.boot.jeecg.com 通过ngnix映射域名到8080端口
示例中:vue3前端域名 boot.jeecg.com
提醒:请把配置中涉及的 api.boot.jeecg.com boot.jeecg.com 换成自己的域名
b.前端nginx配置
nginx监听:80端口
绑定域名(示例):boot.jeecg.com
server {
listen 80;
server_name 前端访问域名;
#解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题
location / {
root html;
index index.html index.htm;
if (!-e $request_filename) {
rewrite ^(.*)$ /index.html?s=$1 last;
break;
}
}
}
c.后端nginx配置
nginx监听:80端口
绑定域名(示例):api.boot.jeecg.com
upstream api.boot.jeecg.com {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name api.boot.jeecg.com;
location / {
root html;
index index.html index.htm;
proxy_pass http://api.boot.jeecg.com;
#ip remote_addr
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
d.Websocket配置
提醒: 有的版本nginx配置需要加上 Upgrade 和Connection 协议头,不然WebSocket会404
#支持websocket得这么写,不然CentOS上可能失败
# proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
f.访问应用
通过 http://你的前端域名 访问项目,出现如下页面说明成功。
账户/密码:admin/123456
09.作为乾坤子应用发布
a.乾坤主应用的配置
首先要配置好jeecg-vue3作为乾坤子应用时,在乾坤主应用中的entry地址,以下为参考配置:
{
name: 'jeecg-vue3',
// 注意将 qiankun.test.com 替换成你自己的项目地址
entry: '//qiankun.test.com/jeecg-vue3',
container: '#subapp-viewport',
activeRule: '/jeecg-vue3',
props: {
// 隐藏侧边栏(菜单)
// hideSider: true,
// 隐藏顶部
// hideHeader: true,
// 隐藏 多Tab 切换
// hideMultiTabs: true,
},
// ...other
},
可以看到当jeecg-vue3作为乾坤子应用启动时,可以传递一些参数,当你需要时解开其注释即可
b.修改项目配置
a.配置文件为.env.production
1.解除VITE_GLOB_QIANKUN_MICRO_APP_NAME的注释,并且改为和乾坤主应用的子应用name配置保持一致
# 填写后将作为乾坤子应用启动,主应用注册时AppName需保持一致
VITE_GLOB_QIANKUN_MICRO_APP_NAME=jeecg-vue3
2.解除VITE_GLOB_QIANKUN_MICRO_APP_ENTRY的注释,并且改为和乾坤主应用的子应用entry配置保持一致
# 作为乾坤子应用启动时必填,需与qiankun主应用注册子应用时填写的 entry 保持一致
VITE_GLOB_QIANKUN_MICRO_APP_ENTRY=//qiankun.test.com/jeecg-vue3
3.修改 VITE_PUBLIC_PATH,改为和乾坤主应用的子应用activeRule配置保持一致
# 发布路径
VITE_PUBLIC_PATH = /jeecg-vue3
4.配置后台接口地址
VITE_GLOB_API_URL=/jeecgboot
VITE_GLOB_DOMAIN_URL=http://api3.test.com
重要提醒:请把http://api3.test.com换成自己的后台域名。
b.配置文件参考
VITE_USE_MOCK = true
VITE_PUBLIC_PATH = /jeecg-vue3
VITE_BUILD_COMPRESS = 'none'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
VITE_GLOB_API_URL=/jeecgboot
VITE_GLOB_DOMAIN_URL=http://api3.test.com
VITE_GLOB_API_URL_PREFIX=
VITE_GLOB_QIANKUN_MICRO_APP_NAME=jeecg-vue3
VITE_GLOB_QIANKUN_MICRO_APP_ENTRY=//qiankun.test.com/jeecg-vue3
c.编译打包项目
a.进入根目录
cd jeecgboot-vue3
b.打包编译
pnpm install
pnpm run build
d.通过nginx部署前端
a.说明
通过 nginx 配置请求转发到后台接口
重要说明:
boot3.jeecg.com是官方Vue3前端演示地址,请把下面的boot3.jeecg.com 全部换成自己的前端域名。
/jeecgboot/对应的是上方配置文件中的参数 VITE_GLOB_API_URL,这个可以不改避坑。
b.配置文件
server {
listen 80;
# 注意:替换成你的真实地址
server_name qiankun.test.com;
# 前端打的dist资源存放目录
# 注意,必须将资源存放在二级目录 jeecg-vue3(与activeRule 保持一致) 下,参考下图
root /usr/share/nginx/jeecg;
# 这里和乾坤主应用的子应用 activeRule 配置保持一致
location /jeecg-vue3 {
# 用于配合 browserHistory使用
try_files $uri $uri/ /jeecg-vue3/index.html;
# 添加允许跨域,仅允许 http://qiankun.main.com 地址跨域
# 注意:这里要替换成你的乾坤主应用的地址
add_header 'Access-Control-Allow-Origin' 'http://qiankun.main.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' '*';
}
location /jeecg-vue3/jeecgboot {
# 后台接口地址(我们部署去掉了/jeecg-boot项目名,如果你有请加上)
proxy_pass http://api3.test.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
e.编译后修改后台配置
dist\_app.config.js
特别注意:VITE_GLOB_QIANKUN_MICRO_APP_NAME 和 VITE_GLOB_QIANKUN_MICRO_APP_ENTRY
这两个参数在编译后不可以通过此方式进行修改,如果要修改这两个参数,只能重新编译并打包上传覆盖所有文件!
f.其他技巧
前端不缓存html,防止程序更新后缓存继续生效
location / {
# 不缓存html,防止程序更新后缓存继续生效
if ($request_filename ~* .*\.(?:htm|html)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
access_log on;
}
}
1.5 Docker启动
01.Docker一键启动前后端
a.下载项目
git clone https://github.com/jeecgboot/JeecgBoot.git
b.修改后台配置
cd jeecg-boot\jeecg-module-system\jeecg-system-start\src\main\resources
修改application-dev.yml 的数据库和redis连接分别改成jeecg-boot-mysql 和 jeecg-boot-redis
c.编译后台项目
进入后台
cd JeecgBoot/jeecg-boot
编译
mvn clean install
d.修改前端配置
配置文件:jeecgboot-vue3/.env.production
VITE_GLOB_DOMAIN_URL=http://jeecg-boot-system:8080/jeecg-boot
重要提醒:
本地测试,默认配置http://jeecg-boot-system:8080/jeecg-boot
如果你是部署服务器,需要将.env.production的VITE_GLOB_DOMAIN_URL修改成外网IP,如:http://199.45.125.150:8080/jeecg-boot
e.编译前端项目
进入前端
cd JeecgBoot/jeecgboot-vue3
编译
pnpm i
npm run build
f.启动镜像容器组
进入JeecgBoot根路径
执行docker命令:docker-compose up -d
docker组启动成功
g.访问系统
等待1分钟,待mysql数据初始化完成后,访问系统
http://localhost:80
02.Docker一键启动微服务前后端
a.下载项目
git clone https://github.com/jeecgboot/JeecgBoot.git
b.编译后台项目
进入后台
cd JeecgBoot/jeecg-boot
编译
mvn clean install -Pdev,SpringCloud
c.修改前端配置
配置文件:jeecgboot-vue3/.env.production
VITE_GLOB_DOMAIN_URL=http://jeecg-boot-gateway:9999
重要提醒:
本地测试,默认配置http://jeecg-boot-gateway:9999
如果你是部署服务器,需要将.env.production的VITE_GLOB_DOMAIN_URL修改成外网IP,如:http://199.45.125.150:9999
d.编译前端项目
进入前端
cd JeecgBoot/jeecgboot-vue3
编译
pnpm i
pnpm run build
e.修改前端Dockerfile
修改配置文件:jeecgboot-vue3/Dockerfile
把Host的 jeecg-boot-system 替换成 jeecg-boot-gateway
把proxy_pass的http://jeecg-boot-system:8080/jeecg-boot改成http://jeecg-boot-gateway:9999
-----------------------------------------------------------------------------------------------------
location /jeecgboot/ {
# 必须有/
proxy_pass http://jeecg-boot-gateway:9999/;
proxy_redirect off;
proxy_set_header Host jeecg-boot-gateway;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
-----------------------------------------------------------------------------------------------------
注意:location和proxy_pass最后以/结尾,否则gateway访问会出错
f.启动镜像容器组
进入JeecgBoot根路径
执行docker命令:docker-compose -f docker-compose-cloud.yml up -d
Docker组启动成功
g.访问系统
等待1分钟,待mysql数据初始化完成后,访问系统
http://localhost:80
03.前后端分开启动
a.Docker启动后台
a.下载项目
git clone https://github.com/jeecgboot/JeecgBoot.git
b.本地配置host
# jeecgboot
127.0.0.1 jeecg-boot-mysql
127.0.0.1 jeecg-boot-redis
127.0.0.1 jeecg-boot-system
注意: 如果本地安装了mysql和redis,启动容器前先停掉本地服务,不然端口冲突。
-------------------------------------------------------------------------------------------------
net stop redis
net stop mysql
c.修改数据库和redis配置
cd jeecg-boot\jeecg-module-system\jeecg-system-start\src\main\resources
修改application-dev.yml 的数据库连接和redis连接,将连接改成host方式
d.进入 jeecg-boot 项目根目录,执行maven打包命令
进入项目
cd JeecgBoot/jeecg-boot
mvn clean install
e.进入jeecg-boot根路径,执行docker命令, 启动镜像容器组
docker-compose up -d
f.访问后台接口地址
请等待一分钟,待docker容器启动完成后,访问下面链接
http://jeecg-boot-system:8080/jeecg-boot/doc.html
看到下面接口文档,说明后台启动成功
b.Docker镜像启动前端 (对接单体后台)
a.下载项目
git clone https://github.com/jeecgboot/JeecgBoot.git
b.配置host
提醒: 注意需要把127.0.0.1替换成真实IP, 比如192.开头,不然后端不通。
127.0.0.1 jeecg-boot-system
127.0.0.1 jeecg-boot-gateway
可以使用工具 SwitchHosts 快速配置host.
可以使用工具 SwitchHosts 快速配置host.
c.配置后台域名 (可忽略)
进入前端项目
cd JeecgBoot/jeecgboot-vue3
-------------------------------------------------------------------------------------------------
修改 .env.production
VITE_GLOB_API_URL=/jeecgboot
VITE_GLOB_DOMAIN_URL=http://jeecg-boot-system:8080/jeecg-boot
-------------------------------------------------------------------------------------------------
此处的 jeecg-boot-system 对应host的域名配置。
d.进入项目根目录,执行编译命令
pnpm i
npm run build
e.构建镜像
docker build -t jeecgboot-vue3 .
f.启动镜像
docker run --name jeecgboot-vue3-nginx --network jeecg_boot -p 80:80 -d jeecgboot-vue3
说明:如果启动提示docker: Error response from daemon: network jeecg_boot not found需要先启动后台docker.
g.访问前台项目
http://localhost:80
h.启动后台项目
docker单体后台
i.修改后台host地址为IP
如果遇到获取不到验证码情况,请把后台的host改成真实ip,不能用127.0.0.1。
参考下图,改完重启前端的docker
重启后刷新就能获取到验证码了。
c.Docker镜像启动前端 (对接微服务后台)
a.修改后台域名为gateway地址
.env.production
-------------------------------------------------------------------------------------------------
VITE_GLOB_API_URL=/jeecgboot
VITE_GLOB_DOMAIN_URL=http://jeecg-boot-gateway:9999
-------------------------------------------------------------------------------------------------
docker启动微服务后台
b.修改Dockerfile文件
把 http://jeecg-boot-system:8080/jeecg-boot 替换成 http://jeecg-boot-gateway:9999
把 jeecg-boot-system 替换成 jeecg-boot-gateway
-------------------------------------------------------------------------------------------------
location /jeecgboot/ {
# 必须有/
proxy_pass http://jeecg-boot-gateway:9999/;
proxy_redirect off;
proxy_set_header Host jeecg-boot-gateway;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
注意:微服务部署Dockerfile里location和proxy_pass最后都必须带/,否则链接gateway会报错
c.进入项目根目录,执行编译命令
npm run build
d.构建镜像
docker build -t jeecgboot-vue3 .
e.启动镜像
docker run --name jeecgboot-vue3-nginx --network jeecg_boot -p 80:80 -d jeecgboot-vue3
说明:如果启动提示docker: Error response from daemon: network jeecg_boot not found需要先启动后台docker.
f.访问前台项目
http://localhost:80
d.Docker启动微服务(新)
a.说明
本文旨在:通过docker方式启动微服务项目后台
只兼容 2024-06-30之后下载的代码
b.下载项目
git clone https://github.com/jeecgboot/JeecgBoot.git
c.配置host
此步骤必须配置,不然会导致nacos、数据库等连不上。
127.0.0.1 jeecg-boot-mysql
127.0.0.1 jeecg-boot-redis
127.0.0.1 jeecg-boot-nacos
127.0.0.1 jeecg-boot-system
127.0.0.1 jeecg-boot-gateway
127.0.0.1 jeecg-boot-sentinel
127.0.0.1 jeecg-boot-xxljob
127.0.0.1 jeecg-boot-rabbitmq
d.编译后台项目
进入jeecg-boot根目录
cd JeecgBoot/jeecg-boot
-------------------------------------------------------------------------------------------------
执行打包命令
mvn clean install -Pdev,SpringCloud
e.创建微服务容器组
进入jeecg-server-cloud根目录
cd jeecg-server-cloud
-------------------------------------------------------------------------------------------------
执行命令
docker-compose up -d
f.访问gateway地址
等1分半钟, 待所有服务启动完成,访问网关地址
http://localhost:9999
看到swagger接口文档,说明微服务启动成功
e.Docker启动微服务3.7
a.说明
本文旨在:通过docker方式启动微服务项目
兼容v3.7.0 之前版本
b.下载项目
git clone https://github.com/jeecgboot/JeecgBoot.git
c.配置host
此步骤必须配置,不然会导致nacos、数据库等连不上。
127.0.0.1 jeecg-boot-mysql
127.0.0.1 jeecg-boot-redis
127.0.0.1 jeecg-boot-nacos
127.0.0.1 jeecg-boot-system
127.0.0.1 jeecg-boot-gateway
127.0.0.1 jeecg-boot-sentinel
127.0.0.1 jeecg-boot-xxljob
127.0.0.1 jeecg-boot-rabbitmq
//注意: 如果本地安装了mysql和redis,启动容器前先停掉本地服务,不然会端口冲突。
net stop redis
net stop mysql
d.编译后台项目
进入jeecg-boot根目录
cd JeecgBoot/jeecg-boot
-------------------------------------------------------------------------------------------------
执行打包命令
mvn clean install -Pdev,SpringCloud
e.创建mysql和redis容器
进入jeecg-server-cloud根目录
cd jeecg-server-cloud
-------------------------------------------------------------------------------------------------
执行下面的命令,让 docker-compose-base.yml 创建mysql和redis容器
docker-compose -f docker-compose-base.yml up -d
f.创建微服务模块容器组
等1分钟 待mysql初始化完,执行命令创建容器组
docker-compose up -d
g.访问gateway接口地址
等1分钟 待服务都启动完成,访问下面地址
http://localhost:9999
看到swagger接口文档,说明微服务启动成功
-------------------------------------------------------------------------------------------------
http://localhost:8848/nacos
nacos/nacos
h.常见测试地址
http://localhost:8848/nacos
nacos/nacos
-------------------------------------------------------------------------------------------------
http://localhost:1567
guest/guest
-------------------------------------------------------------------------------------------------
http://localhost:9080/xxl-job-admin
admin/123456
-------------------------------------------------------------------------------------------------
http://localhost:9000
sentinel/sentine
1.6 附:代码规范
01.建表规范
a.表设计规范
1.主键必须为:ID,类型 [Long(19)] 唯一索引,因为历史原因暂时用String(32)类型
2.外键字段命名:{【关联表名】去掉业务前缀}+“_”+ {关联字段名},例如:order_main_id
3.区分位: iz_* [String(1)] 1表示是 0表示否,(禁用 is_,代码生成实体有问题 )
4.状态位: *_status [String(1-2)] 状态字段必须加注释说明每个值代表含义
5.字段命名,多单词采用下划线分隔 例如:school_id
6.索引命名: 主键索引命名为:pk_表名缩写_字段名(索引要求全库唯一,为兼容多数据库);
唯一索引命名为:uniq_表名缩写_字段名 或 uk_表名缩写_字段名;
普通索引命令为: idx_表名缩写_字段名(表名缩写: 下划线分隔单词首字母组合)
7.区分、状态、类型字段,尽量用String类型,避免数字类型的一些问题;如果需要考虑性能建议用int类型(禁用tinyint类型,需要兼容其他数据库);
8.字段默认值(字段尽量不设置默认值,采用编码方式加默认值) 因为在转库的过程中,不同数据库会有丢失默认值的情况
b.表业务前缀 和 建表标准字段
1.表命名必须带上业务前缀:例如 sys_开头(系统表前缀)
2.所有的表加字段:所属部门,用于部门数据权限
3.所有的表加字段:创建时间,创建者,最后更新时间,更新人
4.逻辑删除字段,del_flag [int(1)],1表示删除 0表示未删除 ,可选择加
5.乐观锁字段, update_count[Integer],可选择加,乐观锁用法参考文档
6.字符串类型字段,varchar类型长度不允许超过1000(过长转库会变类型)
7.大文本尽量少用,字段类型采用text、longtext,禁用blob系列类型(必须用要确认)
c.帮助脚本
ALTER TABLE `表名`
ADD COLUMN `create_by` varchar(32) NULL COMMENT '创建人',
ADD COLUMN `create_time` datetime NULL COMMENT '创建时间' AFTER `create_by`,
ADD COLUMN `update_by` varchar(32) NULL COMMENT '修改人' AFTER `create_time`,
ADD COLUMN `update_time` datetime NULL COMMENT '修改时间' AFTER `update_by`,
ADD COLUMN `del_flag` int(1) NULL COMMENT '0表示未删除,1表示删除' AFTER `update_time`;
d.其他说明
表字段注释,每个字段必须设置注释说明;
表字段注释,状态类型的字段必须说明取值规则(比如性别sex取值规则)
比如:'性别 0男,1女'
索引,查询频率高的字段加索引(单字段索引 、组合索引、唯一索引);
状态、类型字段,尽量用字符串varchar类型1-2长度,少用int类型,避免不必要的问题。
02.编码规范
a.SVN代码提交和修改规范
jeecg-boot-module-system/docs/规范/编码必看规范
b.代码注释规范
类注释、方法名\参数注释 、实体字段注释 (合理的代码结构、分层、命名、注释)
参考: https://my.oschina.net/jeecg/blog/3198358
c.代码格式化说明
新增代码必须格式化,老代码禁止格式化
d.Review规范
review问题,及时记比较以免忘记(有道云、图文并茂)
e.代码规范扫描
代码扫描检查插件安装: pc3
1、不允许代码里面写变量值,需要使用常量定义(统一变量文件或者一个模块一个常量文件)
2、复杂代码逻辑,要增加步骤说明
3、类命名、参数、字段命名要明确,可以百度翻译驼峰缩写
f.TODO 规范,记录缺陷点(及时纠正)
代码有缺陷或者未完善的地方,加TODO标记(前端、后端都适用)
g.建表规范
jeecg-boot-module-system/docs/规范/设计样板/JEECG-开发说明书-XX模块-建表规范v1.0.xlsx
h.开源issue处理
回复issue具体修复问题方法 或 回复下个版本修复
03.代码扫码插件
a.方式一:代码检查插件安装p3c
Alibaba Java Coding Guidelines
Alibaba Java Coding Guidelines (XenoAmess TPM)
b.方式二:使用阿里p3c-pmd 代码规范插件
通过右上角Maven插件进行代码规范扫码:jeecg-boot-module-system -> Plugins -> pmd -> pmd:check
c.方式三:安装Eclipse Code Formatter插件
Eclipse Code Formatter
04.前端编码规范
a.提交代码要求
1、提交代码必须review,确认无问题再提交
2、提交代码,必须进行SVN对比,看是否有多余的修改
3、任何与功能无关的修改不允许提交,比如: 格式化、行位去空格、修改顺序、缩进(要求只提交最精简的改动)
4、提交代码,修改日志必须同步提交
5、修改代码导致功能使用变化,必须同步写文档
b.代码注释和日志规范
a.【第一部分】代码注释规范
[1].SVN提交注释
---author:scott---date:20130203---for:[bug号]树机构调整---
[2].代码修改日志注释
---author:scott---date:2021/11/22-----for:[bug号]树机构调整---
[3].代码修改痕迹注释
//update-begin---author:scott ---date:20140212 for:[bug号]树机构调整------------
//update-end---author:scott ---date:20140212 for:[bug号]树机构调整--------------
[4].代码缺陷或遗留未完成功能标记
//TODO author:zhangdaihao for:插入未完成 date:20130711
b.【第二部分】日志文件
代码修改日志 :/doc/代码修改日志
数据库修改日志:/doc/DB修改日志.sql
c.代码格式化要求
1、新代码必须格式化
2、老代码不允许格式化(除非大重构)
05.新文档规范
1、章节名称用中文,章节路径必须用 “路径+英文”
2、同类型同功能,必须归集到一个路径目录下面
3、路径目录不要超过3级(尽量2级以内)
2 单体开发
01.分类1
代码生成器
表单设计器
报表设计器
大屏设计器
仪表盘设计
工作流引擎
流程设计器
02.分类2
SAAS
OA办公
移动APP
数据权限
微服务/分布式
2.1 系统权限
00.总结
请求权限控制
表单权限文档
数据权限
二级部门权限
列字段导出权限
部门机构编码规则
01.请求权限控制
a.常见注解
@RequiresRoles("admin") // 控制角色
@RequiresPermissions("sys:role") // 控制权限指令
b.后台请求权限控制
通过给control的请求加Shiro注解 @RequiresPermissions
-----------------------------------------------------------------------------------------------------
@RequestMapping(value = "/add", method = RequestMethod.POST)
@RequiresPermissions("user:add")
public Result<SysUser> add(@RequestBody JSONObject jsonObject) {
-----------------------------------------------------------------------------------------------------
注解要生效,需要重启项目。
c.通过菜单配置访问权限
进入后台菜单管理页面配置访问权限标识(选择按钮类型),
配置方式与按钮权限一样,即同一个授权标识,可以同时控制后台请求和前台按钮显示控制。
-----------------------------------------------------------------------------------------------------
a. 打开系统菜单
b. 找到对应的列表,添加下级按钮权限
d.给角色分配权限
进入角色管理授权访问权限(授权后退出重新登录,才可访问该请求)
e.退出登录访问请求
注意:目前做的规则,请求注解类的权限必须退出重新登录才生效。
02.表单权限文档
a.表单权限
a.显隐控制
a.语法
import { usePermission } from '/@/hooks/web/usePermission';
const { hasPermission } = usePermission();
hasPermission 函数接收后台权限编码,可传递数组hasPermission (['user'])
b.方式1:BaseForm中使用方式
a.show方式控制
通过 show 属性动态判断当前组件是否显示,css 控制,不会删除 dom
{
field: 'field1',
component: 'Input',
label: '字段1',
show: ({ values }) => {
return hasPermission('user:add');
}
}
b.ifShow方式控制
通过 ifShow 属性动态判断当前组件是否显示,js 控制,会删除 dom
{
field: 'field2',
component: 'Input',
label: '字段2',
ifShow: ({ values }) => {
return hasPermission('user:add');
}
}
c.v-auth(通过权限控制显隐)
demo:order:auth 标识来源于系统菜单创建的按钮。然后通过角色授权控制该表单项在哪些角色下可见。v3.6.4+
{
field: 'orderAuth',
component: 'Input',
label: '指令权限',
helpMessage: ['有权限右侧的"选中值"可见,否则不可见'],
colProps: {
span: 12,
},
},
{
field: 'orderAuth',
auth: 'demo:order:auth',
component: 'JEllipsis',
label: '选中值',
colProps: { span: 12 },
},
d.插槽中使用方式
<template #jSelectUser="{model, field }">
<JSelectUser v-model:value="model[field]" v-if="hasPermission('user:add')"/>
</template>
c.方式2:原生表单
a.使用
使用v-auth指令和isDisabledAuth()函数
b.示例
1、控制字段的显示与隐藏
2、控制字段的禁用
3、特殊情况处理
如果给必填字段设置隐藏权限,将会导致字段隐藏了,但是校验还存在的问题,造成该问题的原因是表单的渲染顺序导致的。
方案:想要解决该问题,就不能仅仅使用 v-auth 的形式来控制表单了,还需要通过编码的方式,
动态给该表单的必填校验设置为false,将rules设置成computed计算属性可以获得更好的性能。
d.权限配置
a.配置按钮/权限
授权标识:user:add
授权策略:可见/可访问
状态:有效
b.角色授权
详情见页面
c.使用说明
hasPermission('user:add') 指令值“name”为授权标识,可对该授权标识进行“显示/访问”控制
权限编码在【系统管理--菜单管理】中配置,添加按钮类型的菜单数据,授权标识配置值user:add,
策略选择显示/访问,状态选择有效
-----------------------------------------------------------------------------------------
控制规则:
使用hasPermission后,菜单权限中若没有对应指令编码的配置,则不显示控件
策略:显示/访问,未授权时不显示,授权后显示
灵活: 一个授权标识码,可以控制多个控件,也可用于列表列字段的控制(请灵活使用)
b.禁用控制
a.语法
import { usePermission } from '/@/hooks/web/usePermission';
const { isDisabledAuth} = usePermission();
isDisabledAuth函数接收后台权限编码,可传递数组isDisabledAuth(['user'])
b.用法
a.BaseForm中控制
通过 dynamicDisabled 动态判断当前组件是否禁用
{
field: 'field3',
component: 'DatePicker',
label: '字段3',
dynamicDisabled: ({ values }) => {
return isDisabledAuth('user:add');
},
},
b.单独组件中控制
<a-checkbox :disabled="isDisabledAuth('user:add')"></a-checkbox>
c.权限配置
a.配置按钮/权限
授权标识:user:add
授权策略:可见/可访问
状态:有效
b.角色授权
详情见页面
c.使用说明
:disabled="isDisabledAuth('user:add')" 调用方法disabledAuth,方法参数user:add为授权标识,该方法根据授权规则返回true/false,控制是否禁用
权限编码在【系统管理--菜单管理】中配置,添加按钮类型的菜单数据,授权标识配置值user:add,策略选择可编辑,状态选择有效
-----------------------------------------------------------------------------------------
控制规则:
菜单权限中若没有对应指令编码的配置,则不进行禁用控制,
策略:可编辑,未授权时控件禁用,授权后可编辑
b.列表权限
a.按钮权限控制
a.前端页面通过指令 v-auth等方式控制权限
a.按钮显示与隐藏控制
a.函数方式(hasPermission) user 为后台添加的权限编码
引入方法 hasPermission
import { usePermission } from '/@/hooks/web/usePermission'
const { hasPermission } = usePermission();
------------------------------------------------------------------------------------
使用方法 hasPermission 控制按钮权限
<a-button type="primary" v-if="hasPermission('user:add')"> 新增</a-button>
b.指令方式(v-auth) user 为后台添加的权限编码
<a-button type="primary" v-auth="'user:add'" @click="handleCreate" > 新增</a-button>
b.按钮禁用控制
按钮禁用通过 antd vue 组件提供的禁用属性进行控制
<a-button type="primary" :disabled="isDisabledAuth('system:user:export')"> 导出</a-button>
c.操作列(TableAction)按钮显示隐藏控制
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
auth: 'user:add', //通过权限指令控制显示(有权限显示/无权限不显示)
},
b.配置按钮权限菜单
后台进入菜单管理页面配置按钮权限菜单
c.后台通过java注解控制按钮请求
如果因为没有权限导致无法操作,可以暂时注释掉@RequiresPermissions,授权后再放
d.角色授权按钮
进入角色管理授权按钮(授权后即可看见按钮)
b.列字段显隐控制
a.用法
列表字段显示隐藏控制通过BaseTable提供的 auth 和ifShow属性进行控制
---------------------------------------------------------------------------------------------
import { usePermission } from '/@/hooks/web/usePermission';
const { hasPermission } = usePermission();
{
title: '用户姓名',
dataIndex: 'realname',
width: 100,
auth: 'user:add'
}
{
title: '用户姓名',
dataIndex: 'realname',
width: 100,
ifShow: () => {
return hasPermission('user:add');
},
}
b.权限配置
a.配置按钮/权限
略
b.角色授权
略
c.使用说明
hasPermission('user:add') 指令值“name”为授权标识,可对该授权标识进行“显示/访问”控制
权限编码在【系统管理--菜单管理】中配置,添加按钮类型的菜单数据,授权标识配置值user:add,策略选择显示/访问,状态选择有效
-----------------------------------------------------------------------------------------
控制规则:
使用hasPermission后,菜单权限中若没有对应指令编码的配置,则不显示控件
策略:显示/访问,未授权时不显示,授权后显示
c.行编辑组件权限
a.显隐控制
a.需要用到authPre参数,配置一个权限前缀。
略
b.在菜单管理页面,找到JVxeTable所在的菜单页面,添加下级按钮权限
授权标识的格式为:authPre:columnKey
比如此处填写的authPre是vxe,所以需要以vxe:开头,冒号后面的是列的key,
授权策略要选择可见/可访问
c.配置完权限后,就可以在角色管理页面,对相应的角色进行授权,没有授权的角色,不会显示出该列。
b.禁用控制
a.需要用到authPre参数,配置一个权限前缀。
authPre在此处可以随便写,但最终在菜单上配置的要对应上
b.在菜单管理页面,找到JVxeTable所在的菜单页面,添加下级按钮权限
授权标识的格式为:authPre:columnKey
比如此处填写的authPre是vxe,所以需要以vxe:开头,冒号后面的是列的key,
授权策略要选择可编辑
c.配置完权限后,就可以在角色管理页面,对相应的角色进行授权,没有授权的角色,不会显示出该列。
略
03.数据权限
a.数据权限规则篇
a.数据权限配置说明
①规则字段是驼峰写法,对应mybatis实体的字段
②条件规则:大于/大于等于/小于/小于等于/等于/包含/模糊/不等于
③规则值:指定值 ( 固定值/系统上下文变量 )
日期默认值格式:2020-04-10
时间默认值格式:2020-04-13 12:00:00
④条件规则是包含: 规则值用逗号分隔
b.数据权限规则篇
a.当前用户上下文变量
注意:数据权限配置,规则值可以填写系统上下文变量(当前登录人信息),从而根据当前登录人信息进行权限控制。
规则值,配置写法如下:#{sys_user_code}
---------------------------------------------------------------------------------------------
| 编码 | 描述
|--------------------|----------------------
| sys_user_code | 当前登录用户登录账号
| sys_user_name | 当前登录用户真实名称
| sys_date | 当前系统日期
| sys_time | 当前系统时间
| sys_org_code | 当前登录用户部门编号
| sys_multi_org_code | 当前登录用户拥有的所有机构编码,逗号分隔
| tenant_id | 当前登录用户租户ID
b.建表规范(系统标准字段)
如果需要通过当前登录人、登录部门,进行数据权限控制,则业务表必须有以下系统标准字段;
比如:创建人,创建时间,创建人所属部门,有了这些标准字段,就可以通过当前登录人进行数据隔离控制;
---------------------------------------------------------------------------------------------
数据添加和编辑,jeecg会通过拦截器自动注入操作人的信息。
---------------------------------------------------------------------------------------------
字段英文名 字段中文名
create_by 创建人账号
create_time 创建时间
sys_org_code 创建人登录部门
c.组织机构邮编规则
JEECG组织机构支持无限层级,上下级关系通过组织机构编码实现,组织机构编码规则类似邮编方式,看下图;
邮编规则优势: 邮编规则,上下级编码固定规律,便于定位下级和上级;
b.数据权限用法篇
a.权限介绍
数据权限,是通过数据权限控制行数据展示,让不同的人看不同的数据;
比如: 销售人员只能看自己的数据;销售经理可以看所有下级销售人员的数据;财务只看金额大于5000的数据等等;
-------------------------------------------------------------------------------------------------
示例: 以用户管理列表为例,实现只查询用户账号带1的用户
数据权限用法分两种
A方式:通过前端组件名匹配数据权限
B方式:通过请求URL匹配数据权限
b.A方式: 通过前端组件实现权限
基于注解@PermissionData(pageComponent="system/UserList")实现具体步骤如下:
a.配置页面菜单
略
b.给JAVA方法加注解
给请求方法添加注解 @PermissionData(pageComponent="system/UserList") 参数pageComponent值与菜单的前端组件值保持一致
c.给菜单配置数据权限
找到需要配置权限的页面菜单,这里是用户管理菜单,直接在该菜单上配置数据规则。
数据权限配置(只看性别为男的用户)
d.规则字段配置说明
规则字段说明
这里对应的是实体的字段名,一般是驼峰写法,比如:创建人配置的是createBy,而不是create_by
---------------------------------------------------------------------------------------------
特别注意
当使用自定义SQL时,就需要使用数据库字段名称create_by
---------------------------------------------------------------------------------------------
参考文档:权限配置自定义SQL
规则字段配置详细说明(非常重要)
①规则字段:使用实体字段名 (如果是online需要配置数据库字段名)
②规则值:指定值 ( 固定值/系统上下文变量 )
③条件规则:大于/大于等于/小于/小于等于/等于/包含/模糊/不等于/自定义SQL
e.角色授权
略
f.测试权限
退出重新登录,访问用户管理界面发现数据被过滤了,只看到男的数据,说明权限生效!
c.B方式: 通过请求URL实现数据权限
a.介绍
需要针对每个请求都配置一个权限菜单,这样针对简单页面有点麻烦;
但是也有自己的优势,如果一个路由页面有多个列表,需要分别做权限控制,就需要用到本方案了。
b.添加注解
在后台请求方法上加注解 @PermissionData (如下图)
设计目的:为了提高系统运行效率,这样就可以指定请求走权限过滤的逻辑,而非一棍子打死,让所有请求都去筛选一下权限
c.新增二级数据权限菜单
进入【系统管理】-->【菜单管理】界面 新增一个权限菜单(如下图)
d.配置数据权限规则
找到步骤2新增的菜单,点击操作列更多中的数据规则,配置,只查询用户账号带1的用户(如下图)
e.角色授权
进入【系统管理】-->【角色管理】界面找到当前用户对应的角色,点击 更多->授权 操作,
右侧弹出框中找到步骤2菜单,点击后勾选权限规则,保存(如下图)
f.测试
退出重新登录系统,访问用户管理界面发现数据被过滤了,即权限生效!
c.自定义权限SQL
in 查询 复杂SQL条件定义,通过自定义SQL片段
create_by in ('admin','#{sys_user_code}')
d.子表数据权限
a.需求
主子表,代码生成后,子表数据可以通过表单查看,现在需要控制子表数据的查看权限。
b.操作步骤
a.添加权限菜单
找到controller中加载子表数据的请求方法,一般是queryxxxListByMainId,获取其请求路径,添加菜单
只需要填写前四项,其中菜单路径填写请求路径
b.添加数据规则
给步骤1新增的菜单添加数据规则:
c.角色授权
进入【系统管理】-->【角色管理】将步骤1、2新增的菜单/数据规则授权给需要的角色。
d.代码修改
controller,给步骤1中的方法需要添加注解@PermissionData
mapper,修改mapper方法,给方法添加一个参数
mapper.xml,修改查询语句
serviceImpl, 查询数据前获取权限sql,传入mapper
---------------------------------------------------------------------------------------------
String sql = QueryGenerator.installAuthJdbc(xxx.class);
对于ERP风格的表单,只需步骤1中controller的方法需要添加注解@PermissionData即可,其他文件无需修改
f.手工编码数据权限(未用查询过滤器)
a.背景
现有的列表数据权限,需要在后端list请求中调用QueryGenerator.initQueryWrapper方法,
否则配置权限无法生效,故而本章节讲述的是如何手工获取数据权限,不通过上述方法,而是自己在外部处理。
b.准备案例:(权限配置细节不作赘述,请查看相关文档)
查看菜单 常见案例-->单表模型示例 ,没有权限时,数据如下:
配置一个数据权限规则如下图,并且授权
则过滤后的数据如下:
c.实现方案
a.将数据权限规则转化成sql,程序拿到这个sql,可以拼接在mybatis的xml中
// controller代码
// 需要将前端将list请求地址改成此地址
@GetMapping(value = "/sqlList")
@PermissionData(pageComponent="jeecg/JeecgDemoList")
public Result<IPage<JeecgDemo>> loadSqlPermissonList(JeecgDemo jeecgDemo, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
Result<IPage<JeecgDemo>> result = new Result<IPage<JeecgDemo>>();
IPage<JeecgDemo> pageList = jeecgDemoService.queryListWithPermission(pageSize, pageNo);
result.setSuccess(true);
result.setResult(pageList);
return result;
}
//service代码
public IPage<JeecgDemo> queryListWithPermission(int pageSize,int pageNo) {
Page<JeecgDemo> page = new Page<>(pageNo, pageSize);
//就是调用这个QueryGenerator.installAuthJdbc方法获取权限sql
String sql = QueryGenerator.installAuthJdbc(JeecgDemo.class);
return this.baseMapper.queryListWithPermission(page, sql);
}
//mapper接口方法定义
IPage<JeecgDemo> queryListWithPermission(Page<JeecgDemo> page,@Param("permissionSql")String permissionSql);
//xml代码
<select id="queryListWithPermission" parameterType="Object" resultType="org.jeecg.modules.demo.test.entity.JeecgDemo">
select * from demo where 1=1 ${permissionSql}
</select>
b.还是使用mybatisplus ,在外部自己往queryWrapper 里面设置,
//contoller代码
// 需要将前端将list请求地址改成此地址
@GetMapping(value = "/mpList")
@PermissionData(pageComponent="jeecg/JeecgDemoList")
public Result<IPage<JeecgDemo>> loadMpPermissonList(@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
Result<IPage<JeecgDemo>> result = new Result<IPage<JeecgDemo>>();
//此处使用的是QueryWrapper,如果你需要用LambdaQueryWrapper,请先new一个QueryWrapper,接着加载数据权限,然后通过queryWrapper.lambda()将其转化成LambdaQueryWrapper,最后你就能使用LambdaQueryWrapper处理自己的逻辑了
QueryWrapper<JeecgDemo> queryWrapper = new QueryWrapper<JeecgDemo>();
//调用这个QueryGenerator.installAuthMplus方法加载数据权限
QueryGenerator.installAuthMplus(queryWrapper, JeecgDemo.class);
Page<JeecgDemo> page = new Page<JeecgDemo>(pageNo, pageSize);
IPage<JeecgDemo> pageList = jeecgDemoService.page(page, queryWrapper);
result.setSuccess(true);
result.setResult(pageList);
return result;
}
04.二级部门权限
a.介绍
二级部门权限管理主要是为部门管理人员提供设置部门角色、分配部门权限的功能。
b.步骤
1.首先给部门授权:在部门管理中,选中一个部门给部门授权
2.添加部门角色:在我的部门-部门角色中添加部门角色
3.给部门角色授权:在我的部门-部门角色中给所添加的部门角色授权
4.用户管理中将用户设置部门、身份设为上级、设置负责部门
5.在我的部门-用户信息中为用户分配部门角色:分配好部门角色以后,用户再登录时就有所设置的权限了
05.列字段导出权限
a.介绍
针对数据列表的列进行权限控制后,列表导出数据时与列权限相对应
需基于列表列字段权限控制已完成
b.举例
打卡时间字段已做权限控制,在列表中隐藏,则在导出时,也不会导出
c.权限控制步骤
后台导出方法添加权限逻辑,获取需导出的字段,将所有需要导出字段逗号隔开,拼接字符串
@RequestMapping(value = "/exportXls")
@PermissionData(pageComponent = "jeecg/JeecgDemoList")
public ModelAndView exportXls(HttpServletRequest request, JeecgDemo jeecgDemo) {
//获取导出表格字段
String exportFields = jeecgDemoService.getExportFields();
return super.exportXls(request, jeecgDemo, JeecgDemo.class, "单表模型",exportFields);
}
-----------------------------------------------------------------------------------------------------
public String getExportFields() {
//获取当前登录人
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//权限配置列导出示例
List<String> noAuthList = new ArrayList<>();
List<String> exportFieldsList = new ArrayList<>();
//1.此前缀必须与列表字段权限控制前缀一致
String permsPrefix = "testdemo:";
//查询配置菜单有效字段
List<String> allAuth = this.baseMapper.queryAllAuth(permsPrefix);
//查询已授权字段
List<String> userAuth = this.baseMapper.queryUserAuth(sysUser.getId(),permsPrefix);
//列出未授权字段
for(String perms : allAuth){
if(!userAuth.contains(perms)){
noAuthList.add(perms.substring(permsPrefix.length()));
}
}
//实体类中字段与未授权字段比较,列出需导出字段
Field[] fileds = JeecgDemo.class.getDeclaredFields();
List<Field> list = new ArrayList(Arrays.asList(fileds));
for(Field field : list){
if(!noAuthList.contains(field.getName())){
exportFieldsList.add(field.getName());
}
}
return exportFieldsList != null && exportFieldsList.size()>0 ? String.join(",", exportFieldsList) : "";
}
-----------------------------------------------------------------------------------------------------
JeecgDemoMapper.xml中查询字段的参考:
<!-- 查询所有符合前缀且有效字段 -->
<select id="queryAllAuth" resultType="java.lang.String">
select perms from sys_permission
where perms
like concat(concat('%',#{permsPrefix}),'%')
and del_flag=0
and status='1'
</select>
<!-- 查询用户已授权字段 -->
<select id="queryUserAuth" resultType="java.lang.String">
select DISTINCT perms from sys_user_role sur,
sys_role_permission srp,
sys_permission sp
where sur.role_id = srp.role_id
and sp.id = srp.permission_id
and sur.user_id = #{userId}
and sp.perms like concat(concat('%',#{permsPrefix}),'%')
</select>
06.部门机构编码规则
a.JeecgBoot平台设计的部门有 机构编码 字段类似邮编 递增,可体现上下级关系,用于标识上下级和数据权限使用。
例如:
A001
A001A002
b.机构编码规则
目前部门的机构生成规则是,首位一个字母+2位数字,递增生成。
机构编码生成规则(字母按照序号递增; 数字范围1~99,从1开始递增,最大数字到99,位数不够可以增加数字位数)
-----------------------------------------------------------------------------------------------------
增加数字位数,修改这个属性即可。
如果你想个性化,可以自己扩展这个类:org\jeecg\common\util\YouBianCodeUtil.java
2.2 代码生成
00.代码生成
a.表字段
-- 创建表: sys_user
CREATE TABLE `sys_user_temp` (
`id` VARCHAR(32) NOT NULL COMMENT '主键ID,历史原因使用String类型',
`username` VARCHAR(100) NOT NULL COMMENT '用户名',
`password` VARCHAR(255) NOT NULL COMMENT '用户密码',
`email` VARCHAR(255) NULL COMMENT '邮箱地址',
`phone` VARCHAR(20) NULL COMMENT '联系电话',
`department_id` VARCHAR(32) NULL COMMENT '所属部门ID',
`status` VARCHAR(1) NOT NULL DEFAULT '1' COMMENT '状态: 1正常, 0禁用',
`iz_admin` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '是否管理员: 1是, 0否',
`create_by` VARCHAR(32) NULL COMMENT '创建人',
`create_time` DATETIME NULL COMMENT '创建时间',
`update_by` VARCHAR(32) NULL COMMENT '修改人',
`update_time` DATETIME NULL COMMENT '修改时间',
`del_flag` INT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0未删除, 1已删除',
`update_count` INT NULL COMMENT '乐观锁字段',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uniq_sysu_username` (`username`) USING BTREE,
UNIQUE KEY `uniq_sysu_email` (`email`) USING BTREE,
KEY `idx_sysu_department_id` (`department_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户管理表';
b.说明
a.主键
id 字段采用 VARCHAR(32),考虑历史原因,保持唯一索引。
b.字段命名
状态字段使用 _status,例如:status。
区分位字段以 iz_ 开头,例如:iz_admin。
c.索引命名
主键索引:pk_sysu_id(遵循命名规范,这里已省略)。
唯一索引:如 uniq_sysu_username 和 uniq_sysu_email。
普通索引:如 idx_sysu_department_id。
d.默认值
status 和 iz_admin 设置默认值以方便业务逻辑。
del_flag 字段默认值为 0,便于逻辑删除处理。
e.字段注释
每个字段均添加注释,状态和类型字段明确取值规则。
f.其他字段
标准字段包括 create_by、create_time、update_by、update_time。
逻辑删除字段 del_flag,以及可选的乐观锁字段 update_count。
00.介绍
JeecgBoot 提供强大的代码生成器,让前后端代码一键生成,实现低代码开发。
支持单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
01.online代码生成单表
a.通过online表单在线建表
找到菜单 在线开发 -> Online表单开发,在线配置表单,点击同步数据库,会在数据库生成表
b.生成代码界面
点击online表单上方的代码生成按钮,选中需要生成的表单,就可以调出代码生成的界面
c.将生成的前端代码拷贝到前端项目
将生成的前端代码放在前端项目的view文件夹下面 自定义的子包名
d.执行菜单SQL
执行生成的sql,会插入菜单和此功能对应的权限
e.角色授权
授权成功后刷新页面即可看见新增的菜单
重启后台可访问菜单功能
f.常见问题
遇到这个问题,需要重启后台
两种表单风格:选择不同表单风格,生成的前端目录名字不同,分别是 vue3和vue3Native
代码生成支持模型:支持单表、树表、一对多模型和不同风格代码生成
02.online代码生成一对多
a.Online配置一对多表单
分别配置主表和子表,子表的外键配置如下
a) 子表引用主表主键ID作为外键,外键字段必须以_ID结尾,例如 ORDER_ID
b) 主表和子表的外键字段名字,必须相同(除主键ID外);
b.通过online表单在线建表
找到菜单 在线开发 -> Online表单开发,在线配置表单,点击同步数据库,会在数据库生成表
c.生成代码界面
点击online表单上方的代码生成按钮,选中需要生成的表单,就可以调出代码生成的界面
03.配置文件
a.位置
jeecg-module-system\jeecg-system-start\src\main\resources\jeecg\jeecg_config.properties
b.自定义代码生成根路径
#code_generate_project_path
project_path=E:\\workspace\\jeecg-boot
#bussi_package[User defined]
bussi_package=org.jeecg.modules.demo
#default code path
#source_root_package=src
#webroot_package=WebRoot
#maven code path
source_root_package=src.main.java
webroot_package=src.main.webapp
#ftl resource url
templatepath=/jeecg/code-template
system_encoding=utf-8
#db Table id [User defined]
db_table_id=id
#db convert flag[true/false]
db_filed_convert=true
#page Search Field num [User defined]
page_search_filed_num=1
#page_filter_fields
page_filter_fields=create_time,create_by,update_time,update_by
exclude_table=act_,ext_act_,design_,onl_,sys_,qrtz_
c.自定义数据库
a.注释掉类
jeecg-module-system\jeecg-system-biz\src\main\java\org\jeecg\config\init\CodeGenerateDbConfig.java
b.修改配置文件
jeecg-boot-module-system/src/main/resources/jeecg/jeecg_database.properties
#mysql
diver_name=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jeecg-boot?useUnicode=true&characterEncoding=UTF-8
username=root
password=root
database_name=jeecg-boot
c.重要配置注意
略
d.PostgreSQL模式配置
在配置文件jeecg/jeecg_database.properties,增加属性schemaName,多个逗号分隔
schemaName=public,test
d.GUI代码生成器
a.位置1
jeecg-server\jeecg-module-system\jeecg-system-start\src\main\java\org\jeecg\codegenerate\JeecgOneGUI.java
b.位置2
jeecg-server\jeecg-module-system\jeecg-system-start\src\main\java\org\jeecg\codegenerate\JeecgOneToMainUtil.java
04.模板说明
a.Vue3 支持两种风格代码生成
vue3封装表单:Vue3版vben风格包装写法vue3
vue3原生表单:Vue3版原生写法未经过包装vue3Native
-----------------------------------------------------------------------------------------------------
vue3和vue3Native主要区别为:
`vue3`表单数据和查询数据均在`*.data.ts页面`,均以json的格式进行填写,
而`vue3Native`以`Ant Design Vue`原生写法实现,更加灵活
b.模板 vue3和vue3Native的区别
目前Vue3 通过Online代码生成支持vue3和vue3Native
JeecgBoot提供两套风格的vue3代码生成,分别是Vue3原生和Vue3(经过封装的)
c.目录结构
a.vue3包装表单
*list.vue如(TestCustomerList.vue):vue列表页
*.data.list如(TestCustomer.data.ts):数据页面,包含列渲染数据、查询区域渲染数据及表单渲染数据,以json的方式进行配置
*.api.ts如(TestCustomer.api.ts):接口页面
*.sql如(TestCustomer_menu_insert.sql):可执行的菜单升级sql
*Modal.vue如(TestCustomerModal.vue):表单弹窗渲染页面
b.vue3原生表单
*list.vue如(TestCustomerList.vue):vue列表页
*.api.ts如(TestCustomer.api.ts):接口页面
*.sql如(TestCustomer_menu_insert.sql):可执行的菜单升级sql
*Modal.vue如(TestCustomerModal.vue):弹窗页面
*Form.vue如(TestCustomerForm.vue):表单渲染页面
d.vue和vue3Native本质区别
vue3表单数据和查询数据均在*.data.ts页面,均以json的格式进行填写,
而vue3Native以Ant Design Vue原生写法实现,更加灵活,

2.3 数据库表
00.总结
a.用户
sys_user:用户表
sys_user_agent:用户代理人设置
sys_user_depart:关联表
sys_user_position:关联表
sys_user_role:关联表
sys_user_tenant:关联表
b.角色
sys_role:角色表
sys_role_index:关联表
sys_role_permission:关联表
c.菜单
sys_permission:菜单表
sys_permission_data_rule:中间表
d.部门
sys_depart:部门表
sys_depart_permission:关联表
sys_depart_role:关联表
sys_depart_role_permission:关联表
sys_depart_role_user:关联表
01.权限系统
a.组织表
sys_depart
sys_depart_permission
sys_depart_role
sys_depart_role_permission
sys_depart_role_user
b.菜单表
sys_permission
sys_permission_data_rule
sys_permission_v2
c.职位表
sys_position
d.角色表
sys_role
sys_role_index
sys_role_permission
f.三方表
sys_third_account
g.用户表
sys_user
sys_user_agent
sys_user_depart
sys_user_role
02.部门管理
a.关系图
北京卓尔互动
研发部
财务部
北京国炬软件
销售公关岗位
市场部
研发部
研发经理
财务部
b.编码图
北京卓尔互动 A02
研发部 A02A02
财务部 A02A01
北京国炬软件
销售公关岗位 A01A06
市场部 A01A03
研发部 A01A05
研发经理 A01A05A01
财务部 A01A04
99.api与biz
a.jeecg-system-api
a.这是一个接口模块(API层),通常包含以下内容
Feign接口定义: 用于微服务之间的远程调用。比如,通过 @FeignClient 注解定义对其他微服务的访问接口。
DTO(Data Transfer Object)定义: 定义服务间的数据传输对象,确保服务调用时的数据格式统一。
公共接口声明: 定义系统中需要暴露给其他模块或服务使用的接口。这些接口不包含具体实现,只是方法的定义。
常量、枚举、通用工具类: 系统中一些跨模块使用的公共常量、枚举值以及工具类。
依赖最少化: 该模块通常只依赖于 Spring Boot 和相关的基础依赖,尽量避免引入具体的业务逻辑依赖。
b.使用示例
jeecg-system-api 是系统之间解耦的关键。
例如,服务A调用 jeecg-system-biz 的业务逻辑时,A只需要依赖 jeecg-system-api 的接口定义,不需要直接与 jeecg-system-biz 耦合。
b.jeecg-system-biz
a.这是业务实现模块,主要用于实际的业务逻辑开发,包含以下内容
业务逻辑实现: 实现 jeecg-system-api 中定义的接口,将具体的业务逻辑写在这里。
Service层: 使用 @Service 注解的类,负责处理具体的业务逻辑。
Controller层: 使用 @RestController 注解的类,负责接收客户端请求,调用业务逻辑。
Mapper(DAO层): 使用 MyBatis 或其他 ORM 工具进行数据库访问操作。
实体类定义: 数据库表对应的实体类,通常与 jeecg-system-api 中的 DTO 区分开来。
配置信息: 包含与业务相关的配置,如 application.yml 中的数据库配置、缓存配置等。
b.使用示例
客户端发送一个 HTTP 请求到 jeecg-system-biz 的 Controller,Controller 调用 Service 处理业务逻辑,Service 使用 Mapper 访问数据库,最后返回结果。
c.总结
jeecg-system-api:负责 接口定义,用于服务之间的交互,包含 DTO、Feign 客户端、通用常量等。
jeecg-system-biz:负责 业务逻辑实现,包含 Controller、Service、Mapper、数据库实体等。
99.RBAC模型,Role-Based Access Control,基于角色的访问控制
a.用户(User)
a.定义
用户是系统中的实际操作主体,例如管理员、员工等。
b.与角色的关系:
用户与角色一般是 多对多 的关系:一个用户可以拥有多个角色,一个角色也可以分配给多个用户。
通过分配角色,用户继承该角色的权限。
c.与部门的关系:
用户通常从属于一个或多个部门。
部门可以用来限制用户的操作范围,例如只能查看所属部门的数据。
d.与菜单的关系:
用户不能直接访问菜单,必须通过角色间接获取访问菜单的权限。
b.角色(Role)
a.定义
角色是权限的载体,用于对权限进行分类和分组。
b.与用户的关系:
角色是分配给用户的,以便用户获得相应的权限。
c.与菜单的关系:
角色与菜单是 多对多 的关系:一个角色可以访问多个菜单,一个菜单可以被多个角色访问。
分配权限时,角色可以配置能访问哪些菜单及具体操作(如增删改查权限)。
d.与部门的关系:
角色可以设置为部门级别的访问控制。例如,一个角色只能操作某部门的数据。
c.菜单(Menu)
a.定义
菜单表示系统的功能模块或界面入口,如用户管理、部门管理、报表统计等。
b.与角色的关系:
菜单权限控制由角色定义:只有拥有该菜单权限的角色才能让用户访问相应的功能模块。
菜单可以细化到按钮权限(如新增、编辑、删除)。
c.与用户的关系:
用户通过角色间接获得访问菜单的权限。
d.与部门的关系:
菜单权限通常不会直接与部门关联,但可以通过部门数据权限间接限制菜单访问范围。
d.部门(Department)
a.定义
部门是组织架构的抽象,例如公司中的部门或分支机构。
b.与用户的关系:
用户通常与部门关联,用于确定用户的职责范围。
部门可以限制用户操作的数据范围(如只能查看本部门数据)。
c.与角色的关系:
部门可能与角色配合,定义基于部门的数据权限。例如,财务部门的角色只能访问财务相关的数据。
d.与菜单的关系:
部门本身不直接影响菜单,但通过用户所属部门,限制访问的内容或操作范围。
e.整体控制逻辑总结
a.核心关系:
用户通过分配角色来获得访问菜单的权限。
角色是权限的载体,连接用户和菜单。
部门则用于限定数据权限范围。
b.典型数据权限控制模型:
角色权限控制:角色定义用户能做什么(菜单级权限)。
部门数据控制:部门限定用户能操作什么范围的数据。
f.ER图
职位表(Postion)
↓
三方表(Third)<---> 用户(User) <---> 角色(Role) <---> 菜单(Menu)
↑ ↑ ↑
部门(Depart)------------------------
2.4 常见问题
00.存在的BUG
a.数字字典
a.对比1,经测试可以使用
a.新增
字典名称:请假类型
字典编码:vilgoLeaveType
描述:请假类型
b.vilgoLeaveType
名称 数据值 颜色值 描述 排序 是否启用
日假 rj 蓝 1 启用
月假 yj 浅蓝 2 启用
年假 nj 绿 3 启用
b.对比2,经测试可以使用
a.新增
字典名称:请假类型
字典编码:vilgo_leave_type
描述:请假类型
b.vilgoLeaveType
名称 数据值 颜色值 描述 排序 是否启用
日假 日假 蓝 1 启用
月假 月假 浅蓝 2 启用
年假 年假 绿 3 启用
b.自定义包名,如org.vilgo.demo
a.功能测试
发现,功能均无问题;但是生成代码后,存在问题
b.封装表单(BasicForm)
a.姓名:模糊查询不生效
发现【查询-表单控件】,【查询,按钮】会调用后端接口
总结,普通输入框 只能【全部】查询,而不能【模糊】查询
若要模糊查询,请使用【高级查询】中的模糊功能
b.请假类型:下拉框
表单:正常
表格:异常
---------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\common\aspect\DictAspect.java
将
@Pointcut("(@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Controller) || @annotation(org.jeecg.common.aspect.annotation.AutoDict)) " +
"&& execution(public org.jeecg.common.api.vo.Result org.jeecg..*.*(..)) ")
public void excudeService() {
}
更改为
@Pointcut("(@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Controller) || @annotation(org.jeecg.common.aspect.annotation.AutoDict)) " +
"&& execution(public org.jeecg.common.api.vo.Result org.jeecg..*.*(..)) || " +
"execution(public org.jeecg.common.api.vo.Result org.vilgo..*.*(..)))")
public void excudeService() {
}
c.原生表单(a-form)
a.姓名:输入框丢失
jeecg-ui\src\views\vilgo\demo12\VilgoLeave2List.vue
-------------------------------------------------------------------------------------------------
将
<a-form-item name="name">
<template #label><span title="姓名">姓名</span></template>
<JRangeNumber v-model:value="queryParam.name" class="query-group-cust"></JRangeNumber>
</a-form-item>
更改为如下内容
<a-form-item name="name">
<template #label><span title="姓名">姓名</span></template>
<a-input v-model:value="queryParam.name" class="query-group-cust" placeholder="请输入姓名"></a-input>
</a-form-item>
b.请假类型:下拉框
表单:正常
表格:正常
c.缓存功能
a.菜单:示例管理 -> 测试_请假单
菜单类型:子菜单
菜单名称:测试_请假单
上级菜单:示例管理
访问路径:/demo11/vilgoLeaveList
前端组件:vilgo/demo11/VilgoLeaveList
组件名称:空,提示(demo11-vilgoLeaveList)
菜单图标:ant-design:file-text-outlined
排序:1
是否路由菜单:是
隐藏路由:否
隐藏Tab:否
是否缓存路由:否
聚合路由:否
打开方式:否
b.views/vilgo/demo11/VilgoLeaveList.vue
<script lang="ts" name="demo11-vilgoLeave" setup>
c.修改规则
此处名称应和vue组件的name属性保持一致。
组件名称不能重复,主要用于路由缓存功能。
如果组件名称和vue组件的name属性不一致,则会导致路由缓存失效。
非必填,留空则会根据访问路径自动生成
-------------------------------------------------------------------------------------------------
默认,虽然 组件名称:空,提示(demo11-vilgoLeaveList) 不跟 demo11-vilgoLeave 对于,但是可以识别
如果要修改 VilgoLeaveList.vue 组件名,则 示例管理 -> 测试_请假单 -> 前端组件:vilgo/demo11/VilgoLeaveList 也得修改
-------------------------------------------------------------------------------------------------
例如:
views/vilgo/demo11/VilgoLeaveList.vue name="demo11VilgoLeave"
示例管理 -> 测试_请假单 -> 前端组件 demo11VilgoLeave
注意:重新调整菜单 组件名称后,需要使用【角色管理】重新授权给【用户】。即使,之前分配菜单正常,也需要再次授权
01.图1
原生表单(a-form):姓名:输入框丢失
封装表单(BasicForm):请假类型:下拉框
02.图2
没有权限,请联系管理员:1.重启JAVA项目 2.再次授权
3 微服务开发
3.1 项目介绍
01.微服务解决方案
SpringCloud Alibaba 技术栈
1、服务注册发现 Nacos √
2、统一配置中心 Nacos √
3、路由网关 Gateway √
4、分布式 http feign √
5、熔断降级、限流 Sentinel √
6、分布式任务 xxl-job √
7、Redisson分布式锁 √
8、服务监控 SpringBootAdmin √
9、消息中间件 RabbitMQ √
10、消息总线 √
11、分布式事务 Seata √
12、支持 docker-compose、k8s、jenkins √
13、分库分表 shardingsphere √
14、分布式文件 Minio、阿里OSS √
15、统一权限控制 JWT + Shiro √
16、CAS 单点登录 √
17、链路跟踪 Skywarking √
18、轻量分布式日志 Loki+grafana套件
02.架构核心依赖
依赖 版本
Spring Boot Spring Boot 2.6.6
Spring Cloud 2021.0.3
Spring Cloud Alibaba 2021.0.1.0
Mybatis Plus 3.5.1
Shiro 1.8.0
Jwt 3.11.0


3.2 开发环境
01.开始
a.目录
jeecg-server-cloud 微服务模块
├─jeecg-cloud-gateway 微服务网关模块(9999)
├─jeecg-cloud-nacos Nacos服务模块(8848)
├─jeecg-system-cloud-start System微服务启动项目(7001)
├─jeecg-demo-cloud-start Demo微服务启动项目(7002)
├─jeecg-visual
├─jeecg-cloud-monitor 微服务监控模块 (9111)
├─jeecg-cloud-xxljob 微服务xxljob定时任务服务端 (9080)
├─jeecg-cloud-sentinel sentinel服务端 (9000)
├─jeecg-cloud-test 微服务测试示例(各种例子)
├─jeecg-cloud-test-more 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
├─jeecg-cloud-test-rabbitmq 微服务测试示例(rabbitmq)
├─jeecg-cloud-test-seata 微服务测试示例(seata分布式事务)
├─jeecg-cloud-test-shardingsphere 微服务测试示例(分库分表)
b.汇总README
a.jeecg-cloud-gateway
http://localhost:9111
账号密码:admin/admin
b.jeecg-cloud-nacos
访问地址: http://localhost:8848/nacos
账号密码:nacos/nacos
# 使用方法
- 1、目前只做了关闭鉴权模式
- 2、此项目与官方同步,只是为了简化微服务部署
- 3、如何不用此模块,使用自己的naocs,请创建下面目录中的配置文件
目录:jeecg-cloud-nacos/docs/config
配置文件: YAML
# 常见问题
- UnsupportedOperationException: Cannot determine JNI library name for ARCH='x86' OS='windows 10'
解决方案:http://t.zoukankan.com/mindzone-p-15808190.html
c.jeecg-demo-cloud-start
采用jar启动必须设置-Dfile.encoding=utf-8 ,不然会加载不到naocs文件
java -Dfile.encoding=utf-8 -jar xxxx.jar
d.jeecg-system-cloud-start
采用jar启动必须设置-Dfile.encoding=utf-8 ,不然会加载不到naocs文件
java -Dfile.encoding=utf-8 -jar xxxx.jar
e.jeecg-visual
a.jeecg-cloud-monitor
http://localhost:9111
账号密码:admin/admin
b.jeecg-cloud-sentinel
访问地址: http://localhost:9000
账号密码:sentinel/sentinel
# 使用方法
- 1、第一次登录sentinel内容是空的,必须访问了微服务实例的请求才会出现配置
- 2、sentinel做了深度改造,支持持久化到nacos中
- 3、目前只针对gateway做的控制,其他服务不需要
c.jeecg-cloud-xxljob
- 初始化脚本(mysql)
db\tables_xxl_job.sql
- 修改数据库连接
jeecg-cloud-xxljob\src\main\resources\application.yml
- 启动项目
jeecg-cloud-xxljob\src\main\java\com\xxl\job\admin\XxlJobAdminApplication.java
- 访问项目
http://127.0.0.1:9080/xxl-job-admin/toLogin
admin/123456
- docker方式安装
https://my.oschina.net/jeecg/blog/4729020
概念说明
1、手工创建执行器,AppName对应服务名字 比如: jeecg-demo
2、手工创建定时任务,选择执行器(服务)、JobHandler对应XxlJob的值
02.启动JAVA项目:jeecg-boot-cloud
a.配置host
127.0.0.1 jeecg-boot-redis
127.0.0.1 jeecg-boot-mysql
127.0.0.1 jeecg-boot-nacos
127.0.0.1 jeecg-boot-gateway
127.0.0.1 jeecg-boot-system
127.0.0.1 jeecg-boot-sentinel
127.0.0.1 jeecg-boot-xxljob
127.0.0.1 jeecg-boot-rabbitmq
b.勾选中Maven Profile
dev和SpringCloud
二者需要同时勾选中,微服务启动模块出来了,说明maven profile勾选成功
c.创建微服务所需数据库
手工执行jeecgboot提供的初始化SQL,创建jeecg-boot、nacos、xxl_job 三个库。
-----------------------------------------------------------------------------------------------------
jeecg-boot创建到3307端口下,数据库名为jeecg-boot-cloud
-----------------------------------------------------------------------------------------------------
本WIN10由于有其余项目工程,故自定义内容如下
nacos使用本机服务,插入如下数据
INSERT INTO `config_info` VALUES (216, 'jeecg-dev.yaml', 'DEFAULT_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n encoding: UTF-8\n locations: classpath:flyway/sql/mysql\n sql-migration-prefix: V\n sql-migration-separator: __\n placeholder-prefix: \'#(\'\n placeholder-suffix: )\n sql-migration-suffixes: .sql\n validate-on-migrate: true\n baseline-on-migrate: true\n clean-disabled: true\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n elasticsearch:\n cluster-name: jeecg-ES\n cluster-nodes: jeecg-boot-es:9200\n check-enabled: false\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: false\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"????\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '703b0207484e9ae01c54469bdd1b200b', '2021-03-03 13:01:11', '2024-06-11 16:26:59', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (217, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n tomcat:\n max-swallow-size: -1\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: [email protected]\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '94dbdad61f7e2e3ace5a4fc07bb8c2a2', '2021-03-03 13:01:42', '2024-06-11 16:09:40', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (218, 'jeecg-gateway-router.json', 'DEFAULT_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\",\n \"_genkey_5\": \"/drag/**\",\n \"_genkey_6\": \"/actuator/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', '708c0948118bdb96bdfaa87200a14432', '2021-03-03 13:02:14', '2024-06-11 16:15:50', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'json', '', '');
INSERT INTO `config_info` VALUES (219, 'jeecg-sharding.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2022-04-13 03:12:28', '2022-08-07 13:13:57', 'nacos', '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', NULL);
INSERT INTO `config_info` VALUES (220, 'jeecg-gateway-dev.yaml', 'DEFAULT_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: database\n data-id: jeecg-gateway-router\nspring:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:\nknife4j:\n production: false', '98e211c54b43a73f7189d92f1c77f815', '2022-08-04 16:36:11', '2024-06-11 16:13:15', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (221, 'jeecg-sharding-multi.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0,ds1\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n ds1:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n props:\n sql-show: true\n rules:\n replica-query:\n load-balancers:\n round-robin:\n type: ROUND_ROBIN\n props:\n default: 0\n data-sources:\n prds:\n primary-data-source-name: ds0\n replica-data-source-names: ds1\n load-balancer-name: round_robin\n sharding:\n binding-tables:\n - sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n database-inline:\n type: INLINE\n props:\n algorithm-expression: ds$->{operate_type % 2}\n tables:\n sys_log:\n actual-data-nodes: ds$->{0..1}.sys_log$->{0..1}\n database-strategy:\n standard:\n sharding-column: operate_type\n sharding-algorithm-name: database-inline\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', '0fc2b030ca8c0008f148c84ecbd2a8c7', '2022-08-07 06:09:01', '2022-08-07 13:03:52', 'nacos', '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', NULL);
INSERT INTO `config_info` VALUES (222, 'jeecg-dev.yaml', 'JEECGDEV_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n elasticsearch:\n cluster-name: jeecg-ES\n cluster-nodes: jeecg-boot-es:9200\n check-enabled: false\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n mode: dev\n is_verify_token: false\n verify_methods: remove,delete,save,add,update\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: true\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '6c0ec1ace75d0341a1f83978c864c0b3', '2023-07-17 10:05:06', '2023-07-17 11:49:49', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (223, 'jeecg.yaml', 'JEECGDEV_GROUP', 'server:\n tomcat:\n max-swallow-size: -1\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: [email protected]\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/modules/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '2117a96ba08e8fd0f66825e87416af27', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (224, 'jeecg-gateway-router.json', 'JEECGDEV_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', 'be6548051d99309d7fa5ac4398404201', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'json', NULL, '');
INSERT INTO `config_info` VALUES (225, 'jeecg-sharding.yaml', 'JEECGDEV_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (226, 'jeecg-gateway-dev.yaml', 'JEECGDEV_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: nacos\n data-id: jeecg-gateway-router\nspring:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:', 'eeb45ae799de89f4d789139de7a7d12d', '2023-07-17 10:05:06', '2023-07-17 11:49:21', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (227, 'jeecg-sharding-multi.yaml', 'JEECGDEV_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0,ds1\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n ds1:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n props:\n sql-show: true\n rules:\n replica-query:\n load-balancers:\n round-robin:\n type: ROUND_ROBIN\n props:\n default: 0\n data-sources:\n prds:\n primary-data-source-name: ds0\n replica-data-source-names: ds1\n load-balancer-name: round_robin\n sharding:\n binding-tables:\n - sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n database-inline:\n type: INLINE\n props:\n algorithm-expression: ds$->{operate_type % 2}\n tables:\n sys_log:\n actual-data-nodes: ds$->{0..1}.sys_log$->{0..1}\n database-strategy:\n standard:\n sharding-column: operate_type\n sharding-algorithm-name: database-inline\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', '0fc2b030ca8c0008f148c84ecbd2a8c7', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (228, 'jeecg-dev.yaml', 'DEFAULT_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n data:\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n encoding: UTF-8\n locations: classpath:flyway/sql/mysql\n sql-migration-prefix: V\n sql-migration-separator: __\n placeholder-prefix: \'#(\'\n placeholder-suffix: )\n sql-migration-suffixes: .sql\n validate-on-migrate: true\n baseline-on-migrate: true\n clean-disabled: true\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n elasticsearch:\n cluster-name: jeecg-ES\n cluster-nodes: jeecg-boot-es:9200\n check-enabled: false\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: false\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"????\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '91c29720dfb424916a769201a25200cf', '2024-02-29 09:10:12', '2024-06-11 16:24:46', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (229, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n undertow:\n # max-http-post-size: 10MB\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: [email protected]\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', 'ce1ca3b6f8431e884aed94ab29be43a9', '2024-02-29 09:10:12', '2024-06-11 16:24:11', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (230, 'jeecg-gateway-router.json', 'DEFAULT_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\",\n \"_genkey_5\": \"/oauth2/**\",\n \"_genkey_6\": \"/drag/**\",\n \"_genkey_7\": \"/actuator/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', '9794beb09d30bc6b835f2ee870781587', '2024-02-29 09:10:12', '2024-06-11 16:25:56', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'json', '', '');
INSERT INTO `config_info` VALUES (231, 'jeecg-sharding.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2024-02-29 09:10:12', '2024-02-29 09:10:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (232, 'jeecg-gateway-dev.yaml', 'DEFAULT_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: database\n data-id: jeecg-gateway-router\nspring:\n data:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:\nknife4j:\n production: false', '19d7cd93eeb85a582c8a6942d499c7f7', '2024-02-29 09:10:12', '2024-06-11 16:26:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (233, 'jeecg-sharding-multi.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0,ds1\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n ds1:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n props:\n sql-show: true\n rules:\n replica-query:\n load-balancers:\n round-robin:\n type: ROUND_ROBIN\n props:\n default: 0\n data-sources:\n prds:\n primary-data-source-name: ds0\n replica-data-source-names: ds1\n load-balancer-name: round_robin\n sharding:\n binding-tables:\n - sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n database-inline:\n type: INLINE\n props:\n algorithm-expression: ds$->{operate_type % 2}\n tables:\n sys_log:\n actual-data-nodes: ds$->{0..1}.sys_log$->{0..1}\n database-strategy:\n standard:\n sharding-column: operate_type\n sharding-algorithm-name: database-inline\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', '0fc2b030ca8c0008f148c84ecbd2a8c7', '2024-02-29 09:10:12', '2024-02-29 09:10:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', NULL, NULL, 'yaml', NULL, '');
-----------------------------------------------------------------------------------------------------
修改jeecg-dev.yaml配置文件,比如mysql、redis、rabbitmq
spring:
datasource:
dynamic:
datasource:
master:
url: jdbc:mysql://jeecg-boot-mysql:3307/jeecg-boot-cloud?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
database: 0
host: jeecg-boot-redis
password: myslayers
port: 6379
rabbitmq:
host: jeecg-boot-rabbitmq
username: guest
password: guest
port: 5672
publisher-confirms: true
publisher-returns: true
virtual-host: /
listener:
simple:
acknowledge-mode: manual
concurrency: 1
max-concurrency: 1
retry:
enabled: true
redisson:
address: jeecg-boot-redis:6379
password: myslayers
type: STANDALONE
enabled: true
-----------------------------------------------------------------------------------------------------
修改xxx.yaml配置文件,由于内容过多,请参考【JeecgBoot_dev\jeecg-server-cloud\docs\DEFAULT_GROUP】
方式1:使用ry-config_20241127_145715.sql覆盖配置
方式2:使用DEFAULT_GROUP文件夹中的逐个添加
-----------------------------------------------------------------------------------------------------
xxl_job使用默认3306端口下的xxl_job,由于以前就有数据,无需任何操作
d.启动微服务顺序如下
第1步:JeecgNacosApplication:Nacos服务模块(8848),WIN10默认开机启动,因此不需要启动
第2步:JeecgSystemCloudApplication:System微服务启动项目(7001)
第3步:JeecgGatewayApplication:微服务网关模块(9999)
第4步:JeecgDemoCloudApplication:Demo微服务启动项目(7002),缩短命令行(缩短命令行)
扩展:XxlJobAdminApplication:微服务xxljob定时任务服务端 (9080),http://localhost:9080/xxl-job-admin/toLogin,admin/123456
扩展:JeecgSentinelApplication:sentinel服务端 (9000),http://localhost:9000,sentinel/sentinel
扩展:JeecgMonitorApplication:微服务监控模块 (9111),http://localhost:9111/login,admin/admin
扩展:阿里巴巴Druid:排查慢SQL,http://localhost:3100/monitor/druid
jeecg-server-cloud 微服务模块
├─jeecg-cloud-gateway 微服务网关模块(9999)
├─jeecg-cloud-nacos Nacos服务模块(8848)
├─jeecg-system-cloud-start System微服务启动项目(7001)
├─jeecg-demo-cloud-start Demo微服务启动项目(7002)
├─jeecg-visual
├─jeecg-cloud-monitor 微服务监控模块 (9111)
├─jeecg-cloud-xxljob 微服务xxljob定时任务服务端 (9080)
├─jeecg-cloud-sentinel sentinel服务端 (9000)
├─jeecg-cloud-test 微服务测试示例(各种例子)
├─jeecg-cloud-test-more 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
├─jeecg-cloud-test-rabbitmq 微服务测试示例(rabbitmq)
├─jeecg-cloud-test-seata 微服务测试示例(seata分布式事务)
├─jeecg-cloud-test-shardingsphere 微服务测试示例(分库分表)
03.启动VUE项目:jeecgboot-vue3
a.下载前端依赖
执行命令 pnpm install 或者双击 pinstall
b.配置后台接口
修改接口地址 .env.development
-----------------------------------------------------------------------------------------------------
VITE_PROXY = [["/jeecgboot","http://localhost:9999"],["/upload","http://localhost:3300/upload"]]
VITE_GLOB_DOMAIN_URL=http://localhost:9999
c.启动前端项目
npm run dev
d.访问
http://localhost:3100
admin/123456
3.3 开发目录
01.开始
jeecg-server-cloud 微服务模块
├─jeecg-cloud-gateway 微服务网关模块(9999)
├─jeecg-cloud-nacos Nacos服务模块(8848)
├─jeecg-system-cloud-start System微服务启动项目(7001)
├─jeecg-demo-cloud-start Demo微服务启动项目(7002)
├─jeecg-visual
├─jeecg-cloud-monitor 微服务监控模块 (9111)
├─jeecg-cloud-xxljob 微服务xxljob定时任务服务端 (9080)
├─jeecg-cloud-sentinel sentinel服务端 (9000)
├─jeecg-cloud-test 微服务测试示例(各种例子)
├─jeecg-cloud-test-more 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
├─jeecg-cloud-test-rabbitmq 微服务测试示例(rabbitmq)
├─jeecg-cloud-test-seata 微服务测试示例(seata分布式事务)
├─jeecg-cloud-test-shardingsphere 微服务测试示例(分库分表)
02.新建一个微服务模块
a.说明
新建一个单体的模块,并引入到system中使用
请按照包名规则 org.jeecg.modules.* 进行初始化,不然bean扫描不到!
执行mvn 命令生成erp模块, IDEA 可以直接图形化使用
b.方式1:使用archetype命令新建module模块
// 注意: windows下可直接复制执行, Linux/Macos下 ^ 修改成 \
mvn archetype:generate ^
-DgroupId=org.jeecgframework.boot ^
-DartifactId=jeecg-module-erp ^
-Dversion=3.7.0 ^
-DarchetypeGroupId=org.jeecgframework.archetype ^
-DarchetypeArtifactId=jeecg-boot-gen ^
-DarchetypeVersion=2.0
说明: -DarchetypeVersion=2.0版本号固定不需要修改,3.7.0根据自己项目的版本号进行调整。
c.方式2:使用IDEA图形新建模块
a.导航
新建模块 -> Java -> jeecg-module-oa -> Maven、jdk-1.8.0_281、JEECG BOOT 3.7.1
b.简单说明
jeecg-boot-module-system作为启动项目,所以其他模块不要引用system。
jeecg-boot-base-core作为基础Core,所以新建模块一定要引用。
c.如果业务模块需要调用system里面的业务方法怎么办呢?
a.单体模式,可以引入jeecg-system-local-api
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
</dependency>
b.微服务模式,直接引入jeecg-boot-starter-cloud即可
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-cloud</artifactId>
</dependency>
d.swagger配置: 在swagger配置新模块的swagger扫描bean.找到Swagger2Config。追加如下Bean配置。
@Bean
public Docket loanApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()).groupName("会员模块")
.select()
//此包路径下的类,才生成接口文档
.apis(RequestHandlerSelectors.basePackage("org.jeecg.crm")) --修改成自己的包
//加了ApiOperation注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) --配置ApiOperation项
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securityScheme()));
//.globalOperationParameters(setHeaderToken());
}
e.mybatis-plus配置
a.在application.yml中的配置
mybatis-plus:
mapper-locations: classpath*:org/jeecg/modules/**/xml/*Mapper.xml,classpath*:com/sanyitu/yuanqu/**/xml/*Mapper.xml
b.MybatisPlusSaasConfig配置mapper文件扫描路径
@Configuration
@MapperScan(value={"org.jeecg.modules.**.mapper*","org.jeecg.loan.**.mapper*","org.jeecg.camunda.**.mapper*","org.jeecg.story.**.mapper*"})
public class MybatisPlusSaasConfig {
}
---------------------------------------------------------------------------------------------
@Configuration
@MapperScan(value={"org.jeecg.modules.**.mapper*","com.sanyitu.yuanqu.**.mapper*"})
public class MybatisPlusSaasConfig {
}
f.新建HelloController代码
package org.jeecg.modules.hello;
import org.jeecg.common.api.vo.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
/**
* 测试新的module
* @author chengtg
*
*/
@Slf4j
@Api(tags="新建module--jm")
@RestController
@RequestMapping("/hello")
public class HelloController {
@ApiOperation("测试hello方法")
@GetMapping(value="/")
public Result<String> hello(){
Result<String> result = new Result<String>();
result.setResult("hello word!");
result.setSuccess(true);
return result;
}
}
g.去掉token拦截限制
如果不需要token可以访问,可以在jeecg-boot-module-system项目中修改 ShiroConfig.java 配置类
@Slf4j
@Configuration
public class ShiroConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
@Autowired
private Environment env;
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired(required = false)
private RedisProperties redisProperties;
/**
* Filter Chain定义说明
*
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//支持yml方式,配置拦截排除
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
String[] permissionUrl = shiroExcludeUrls.split(",");
for(String url : permissionUrl){
filterChainDefinitionMap.put(url,"anon");
}
}
}
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/hello/**", "anon"); //测试新添加的module,不带token访问
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
}
}
4 新建工程
4.1 单体架构
00.版本记录
a.JeecgBoot_20241120_init
a.内容1
新建模块:jeecg-module-vilgo,无内容
b.内容2
# 自定义包名
bussi_package=org.jeecg.demo
b.JeecgBoot_20241130_online
a.内容1
模块:jeecg-module-vilgo
单表:vilgo_leave
b.内容2
# 自定义包名
bussi_package=org.vilgo.demo
b.JeecgBoot_20241201_online
a.内容1
模块:jeecg-module-vilgo
单表:vilgo_leave
b.内容2
# 自定义包名
bussi_package=org.jeecg.demo
00.克隆JeecgBoot_20241120_init
a.第1处
jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
-----------------------------------------------------------------------------------------------------
datasource:
master:
url: jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
-----------------------------------------------------------------------------------------------------
#redis 配置
redis:
database: 0
host: 127.0.0.1
port: 6379
password: myslayers
#分布式锁
redisson:
address: 127.0.0.1:6379
password: myslayers
type: STANDALONE
enabled: true
b.第2处
系统监控 -> 数据库管理
-----------------------------------------------------------------------------------------------------
数据源名称:本地测试库
数据库类型:MySQL5.5
驱动类:com.mysql.jdbc.Driver
数据源地址:jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false
用户名:root
密码:123456
c.第3处
低代码开发 -> 积木报表设计 -> 任意报表 -> 数据集管理 -> 维护数据源
-----------------------------------------------------------------------------------------------------
数据源名称:本地测试库
数据库类型:MySQL5.5
驱动类:com.mysql.jdbc.Driver
数据源地址:jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false
用户名:root
密码:123456
d.第4处
低代码开发 -> 仪表盘设计器 -> 任意仪表盘 -> 数据源头
-----------------------------------------------------------------------------------------------------
数据源名称:本地测试库
数据库类型:MySQL5.5
驱动类:com.mysql.jdbc.Driver
数据源地址:jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false
用户名:root
密码:123456
e.快速启动
admin
123456
管理员
-----------------------------------------------------------------------------------------------------
zhangsan
qwER159263!
临时角色
-----------------------------------------------------------------------------------------------------
admin
admin@Xxgcz2023
01.环境搭建
a.前端环境
node v20.15.0
pnpm store prune
pnpm install
b.后端环境
JDK17
maven3.9.8
Mysql8.0
Redis 3.0.504
c.开发工具
IDEA后端开发工具
Webstorm前端开发工具
Navicat数据库连接工具
02.启动JAVA项目:jeecg-boot
a.初始化数据库,要求 mysql5.7+
执行Sql脚本: jeecg-boot/db/jeecgboot-mysql-5.7.sql
脚本作用:会自动创建库jeecg-boot, 并初始化数据
b.安装Maven依赖
通过右侧父POM进行install(下载依赖和打包)
c.修改配置文件
jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
-----------------------------------------------------------------------------------------------------
datasource:
master:
url: jdbc:mysql://127.0.0.1:3307/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
-----------------------------------------------------------------------------------------------------
#redis 配置
redis:
database: 0
host: 127.0.0.1
port: 6379
password: myslayers
#分布式锁
redisson:
address: 127.0.0.1:6379
password: myslayers
type: STANDALONE
enabled: true
d.启动项目
jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java
e.接口文档
http://localhost:8080/jeecg-boot/doc.html
03.启动VUE项目:jeecgboot-vue3
a.下载前端依赖
pnpm store prune
执行命令 pnpm install 或者双击 pinstall
b.配置后台接口
修改接口地址 .env.development
-----------------------------------------------------------------------------------------------------
# 跨域代理,您可以配置多个, 请注意没有换行符
#后台接口全路径地址(必填)
VITE_PROXY = [["/jeecgboot","http://localhost:8080/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
VITE_GLOB_DOMAIN_URL=http://localhost:8080/jeecg-boot
c.启动前端项目
npm run dev
d.访问
http://localhost:3100
admin/123456
04.MongoDB配置
a.MongoDB的yml配置
spring:
data:
mongodb:
uri: mongodb://账号:密码@127.0.0.1:27017/数据库名称?readPreference=secondaryPreferred&maxIdleTimeMS=60000&waitQueueTimeoutMS=2000&minPoolSize=5&maxPoolSize=100&maxLifeTimeMS=0&connectTimeoutMS=2000&socketTimeoutMS=2000
print: true
slowQuery: true
slowTime: 1000
b.配置MongoDB连接池
spring.data.mongodb.uri=mongodb://username:[email protected]:27017,192.168.1.2:27017,192.168.1.3:27017/jeecg?readPreference=secondaryPreferred&maxIdleTimeMS=60000&waitQueueTimeoutMS=2000&minPoolSize=5&maxPoolSize=100&maxLifeTimeMS=0&connectTimeoutMS=2000&socketTimeoutMS=2000

4.2 微服务架构
01.开始
a.目录
jeecg-server-cloud 微服务模块
├─jeecg-cloud-gateway 微服务网关模块(9999)
├─jeecg-cloud-nacos Nacos服务模块(8848)
├─jeecg-system-cloud-start System微服务启动项目(7001)
├─jeecg-demo-cloud-start Demo微服务启动项目(7002)
├─jeecg-visual
├─jeecg-cloud-monitor 微服务监控模块 (9111)
├─jeecg-cloud-xxljob 微服务xxljob定时任务服务端 (9080)
├─jeecg-cloud-sentinel sentinel服务端 (9000)
├─jeecg-cloud-test 微服务测试示例(各种例子)
├─jeecg-cloud-test-more 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
├─jeecg-cloud-test-rabbitmq 微服务测试示例(rabbitmq)
├─jeecg-cloud-test-seata 微服务测试示例(seata分布式事务)
├─jeecg-cloud-test-shardingsphere 微服务测试示例(分库分表)
b.汇总README
a.jeecg-cloud-gateway
http://localhost:9111
账号密码:admin/admin
b.jeecg-cloud-nacos
访问地址: http://localhost:8848/nacos
账号密码:nacos/nacos
# 使用方法
- 1、目前只做了关闭鉴权模式
- 2、此项目与官方同步,只是为了简化微服务部署
- 3、如何不用此模块,使用自己的naocs,请创建下面目录中的配置文件
目录:jeecg-cloud-nacos/docs/config
配置文件: YAML
# 常见问题
- UnsupportedOperationException: Cannot determine JNI library name for ARCH='x86' OS='windows 10'
解决方案:http://t.zoukankan.com/mindzone-p-15808190.html
c.jeecg-demo-cloud-start
采用jar启动必须设置-Dfile.encoding=utf-8 ,不然会加载不到naocs文件
java -Dfile.encoding=utf-8 -jar xxxx.jar
d.jeecg-system-cloud-start
采用jar启动必须设置-Dfile.encoding=utf-8 ,不然会加载不到naocs文件
java -Dfile.encoding=utf-8 -jar xxxx.jar
e.jeecg-visual
a.jeecg-cloud-monitor
http://localhost:9111
账号密码:admin/admin
b.jeecg-cloud-sentinel
访问地址: http://localhost:9000
账号密码:sentinel/sentinel
# 使用方法
- 1、第一次登录sentinel内容是空的,必须访问了微服务实例的请求才会出现配置
- 2、sentinel做了深度改造,支持持久化到nacos中
- 3、目前只针对gateway做的控制,其他服务不需要
c.jeecg-cloud-xxljob
- 初始化脚本(mysql)
db\tables_xxl_job.sql
- 修改数据库连接
jeecg-cloud-xxljob\src\main\resources\application.yml
- 启动项目
jeecg-cloud-xxljob\src\main\java\com\xxl\job\admin\XxlJobAdminApplication.java
- 访问项目
http://127.0.0.1:9080/xxl-job-admin/toLogin
admin/123456
- docker方式安装
https://my.oschina.net/jeecg/blog/4729020
概念说明
1、手工创建执行器,AppName对应服务名字 比如: jeecg-demo
2、手工创建定时任务,选择执行器(服务)、JobHandler对应XxlJob的值
02.启动JAVA项目:jeecg-boot-cloud
a.配置host
127.0.0.1 jeecg-boot-redis
127.0.0.1 jeecg-boot-mysql
127.0.0.1 jeecg-boot-nacos
127.0.0.1 jeecg-boot-gateway
127.0.0.1 jeecg-boot-system
127.0.0.1 jeecg-boot-sentinel
127.0.0.1 jeecg-boot-xxljob
127.0.0.1 jeecg-boot-rabbitmq
b.勾选中Maven Profile
dev和SpringCloud
二者需要同时勾选中,微服务启动模块出来了,说明maven profile勾选成功
c.创建微服务所需数据库
手工执行jeecgboot提供的初始化SQL,创建jeecg-boot、nacos、xxl_job 三个库。
-----------------------------------------------------------------------------------------------------
jeecg-boot创建到3307端口下,数据库名为jeecg-boot-cloud
-----------------------------------------------------------------------------------------------------
本WIN10由于有其余项目工程,故自定义内容如下
nacos使用本机服务,插入如下数据
INSERT INTO `config_info` VALUES (216, 'jeecg-dev.yaml', 'DEFAULT_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n encoding: UTF-8\n locations: classpath:flyway/sql/mysql\n sql-migration-prefix: V\n sql-migration-separator: __\n placeholder-prefix: \'#(\'\n placeholder-suffix: )\n sql-migration-suffixes: .sql\n validate-on-migrate: true\n baseline-on-migrate: true\n clean-disabled: true\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n elasticsearch:\n cluster-name: jeecg-ES\n cluster-nodes: jeecg-boot-es:9200\n check-enabled: false\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: false\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"????\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '703b0207484e9ae01c54469bdd1b200b', '2021-03-03 13:01:11', '2024-06-11 16:26:59', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (217, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n tomcat:\n max-swallow-size: -1\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: [email protected]\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '94dbdad61f7e2e3ace5a4fc07bb8c2a2', '2021-03-03 13:01:42', '2024-06-11 16:09:40', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (218, 'jeecg-gateway-router.json', 'DEFAULT_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\",\n \"_genkey_5\": \"/drag/**\",\n \"_genkey_6\": \"/actuator/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', '708c0948118bdb96bdfaa87200a14432', '2021-03-03 13:02:14', '2024-06-11 16:15:50', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'json', '', '');
INSERT INTO `config_info` VALUES (219, 'jeecg-sharding.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2022-04-13 03:12:28', '2022-08-07 13:13:57', 'nacos', '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', NULL);
INSERT INTO `config_info` VALUES (220, 'jeecg-gateway-dev.yaml', 'DEFAULT_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: database\n data-id: jeecg-gateway-router\nspring:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:\nknife4j:\n production: false', '98e211c54b43a73f7189d92f1c77f815', '2022-08-04 16:36:11', '2024-06-11 16:13:15', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (221, 'jeecg-sharding-multi.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0,ds1\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n ds1:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n props:\n sql-show: true\n rules:\n replica-query:\n load-balancers:\n round-robin:\n type: ROUND_ROBIN\n props:\n default: 0\n data-sources:\n prds:\n primary-data-source-name: ds0\n replica-data-source-names: ds1\n load-balancer-name: round_robin\n sharding:\n binding-tables:\n - sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n database-inline:\n type: INLINE\n props:\n algorithm-expression: ds$->{operate_type % 2}\n tables:\n sys_log:\n actual-data-nodes: ds$->{0..1}.sys_log$->{0..1}\n database-strategy:\n standard:\n sharding-column: operate_type\n sharding-algorithm-name: database-inline\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', '0fc2b030ca8c0008f148c84ecbd2a8c7', '2022-08-07 06:09:01', '2022-08-07 13:03:52', 'nacos', '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', NULL);
INSERT INTO `config_info` VALUES (222, 'jeecg-dev.yaml', 'JEECGDEV_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n elasticsearch:\n cluster-name: jeecg-ES\n cluster-nodes: jeecg-boot-es:9200\n check-enabled: false\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n mode: dev\n is_verify_token: false\n verify_methods: remove,delete,save,add,update\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: true\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '6c0ec1ace75d0341a1f83978c864c0b3', '2023-07-17 10:05:06', '2023-07-17 11:49:49', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (223, 'jeecg.yaml', 'JEECGDEV_GROUP', 'server:\n tomcat:\n max-swallow-size: -1\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: [email protected]\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/modules/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '2117a96ba08e8fd0f66825e87416af27', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (224, 'jeecg-gateway-router.json', 'JEECGDEV_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', 'be6548051d99309d7fa5ac4398404201', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'json', NULL, '');
INSERT INTO `config_info` VALUES (225, 'jeecg-sharding.yaml', 'JEECGDEV_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (226, 'jeecg-gateway-dev.yaml', 'JEECGDEV_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: nacos\n data-id: jeecg-gateway-router\nspring:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:', 'eeb45ae799de89f4d789139de7a7d12d', '2023-07-17 10:05:06', '2023-07-17 11:49:21', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (227, 'jeecg-sharding-multi.yaml', 'JEECGDEV_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0,ds1\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n ds1:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n props:\n sql-show: true\n rules:\n replica-query:\n load-balancers:\n round-robin:\n type: ROUND_ROBIN\n props:\n default: 0\n data-sources:\n prds:\n primary-data-source-name: ds0\n replica-data-source-names: ds1\n load-balancer-name: round_robin\n sharding:\n binding-tables:\n - sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n database-inline:\n type: INLINE\n props:\n algorithm-expression: ds$->{operate_type % 2}\n tables:\n sys_log:\n actual-data-nodes: ds$->{0..1}.sys_log$->{0..1}\n database-strategy:\n standard:\n sharding-column: operate_type\n sharding-algorithm-name: database-inline\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', '0fc2b030ca8c0008f148c84ecbd2a8c7', '2023-07-17 10:05:06', '2023-07-17 10:05:06', NULL, '0:0:0:0:0:0:0:1', '', 'ac14ab82-51f8-4f0c-aa5b-25fb8384bfb6', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (228, 'jeecg-dev.yaml', 'DEFAULT_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n data:\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n encoding: UTF-8\n locations: classpath:flyway/sql/mysql\n sql-migration-prefix: V\n sql-migration-separator: __\n placeholder-prefix: \'#(\'\n placeholder-suffix: )\n sql-migration-suffixes: .sql\n validate-on-migrate: true\n baseline-on-migrate: true\n clean-disabled: true\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n elasticsearch:\n cluster-name: jeecg-ES\n cluster-nodes: jeecg-boot-es:9200\n check-enabled: false\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: false\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"????\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '91c29720dfb424916a769201a25200cf', '2024-02-29 09:10:12', '2024-06-11 16:24:46', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (229, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n undertow:\n # max-http-post-size: 10MB\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: [email protected]\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', 'ce1ca3b6f8431e884aed94ab29be43a9', '2024-02-29 09:10:12', '2024-06-11 16:24:11', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (230, 'jeecg-gateway-router.json', 'DEFAULT_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\",\n \"_genkey_5\": \"/oauth2/**\",\n \"_genkey_6\": \"/drag/**\",\n \"_genkey_7\": \"/actuator/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', '9794beb09d30bc6b835f2ee870781587', '2024-02-29 09:10:12', '2024-06-11 16:25:56', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'json', '', '');
INSERT INTO `config_info` VALUES (231, 'jeecg-sharding.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2024-02-29 09:10:12', '2024-02-29 09:10:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (232, 'jeecg-gateway-dev.yaml', 'DEFAULT_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: database\n data-id: jeecg-gateway-router\nspring:\n data:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:\nknife4j:\n production: false', '19d7cd93eeb85a582c8a6942d499c7f7', '2024-02-29 09:10:12', '2024-06-11 16:26:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (233, 'jeecg-sharding-multi.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0,ds1\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n ds1:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n props:\n sql-show: true\n rules:\n replica-query:\n load-balancers:\n round-robin:\n type: ROUND_ROBIN\n props:\n default: 0\n data-sources:\n prds:\n primary-data-source-name: ds0\n replica-data-source-names: ds1\n load-balancer-name: round_robin\n sharding:\n binding-tables:\n - sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n database-inline:\n type: INLINE\n props:\n algorithm-expression: ds$->{operate_type % 2}\n tables:\n sys_log:\n actual-data-nodes: ds$->{0..1}.sys_log$->{0..1}\n database-strategy:\n standard:\n sharding-column: operate_type\n sharding-algorithm-name: database-inline\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', '0fc2b030ca8c0008f148c84ecbd2a8c7', '2024-02-29 09:10:12', '2024-02-29 09:10:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', NULL, NULL, 'yaml', NULL, '');
-----------------------------------------------------------------------------------------------------
修改jeecg-dev.yaml配置文件,比如mysql、redis、rabbitmq
spring:
datasource:
dynamic:
datasource:
master:
url: jdbc:mysql://jeecg-boot-mysql:3307/jeecg-boot-cloud?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
database: 0
host: jeecg-boot-redis
password: myslayers
port: 6379
rabbitmq:
host: jeecg-boot-rabbitmq
username: guest
password: guest
port: 5672
publisher-confirms: true
publisher-returns: true
virtual-host: /
listener:
simple:
acknowledge-mode: manual
concurrency: 1
max-concurrency: 1
retry:
enabled: true
redisson:
address: jeecg-boot-redis:6379
password: myslayers
type: STANDALONE
enabled: true
-----------------------------------------------------------------------------------------------------
修改xxx.yaml配置文件,由于内容过多,请参考【JeecgBoot_dev\jeecg-server-cloud\docs\DEFAULT_GROUP】
方式1:使用ry-config_20241127_145715.sql覆盖配置
方式2:使用DEFAULT_GROUP文件夹中的逐个添加
-----------------------------------------------------------------------------------------------------
xxl_job使用默认3306端口下的xxl_job,由于以前就有数据,无需任何操作
d.启动微服务顺序如下
第1步:JeecgNacosApplication:Nacos服务模块(8848),WIN10默认开机启动,因此不需要启动
第2步:JeecgSystemCloudApplication:System微服务启动项目(7001)
第3步:JeecgGatewayApplication:微服务网关模块(9999)
第4步:JeecgDemoCloudApplication:Demo微服务启动项目(7002),缩短命令行(缩短命令行)
扩展:XxlJobAdminApplication:微服务xxljob定时任务服务端 (9080),http://localhost:9080/xxl-job-admin/toLogin,admin/123456
扩展:JeecgSentinelApplication:sentinel服务端 (9000),http://localhost:9000,sentinel/sentinel
扩展:JeecgMonitorApplication:微服务监控模块 (9111),http://localhost:9111/login,admin/admin
扩展:阿里巴巴Druid:排查慢SQL,http://localhost:3100/monitor/druid
jeecg-server-cloud 微服务模块
├─jeecg-cloud-gateway 微服务网关模块(9999)
├─jeecg-cloud-nacos Nacos服务模块(8848)
├─jeecg-system-cloud-start System微服务启动项目(7001)
├─jeecg-demo-cloud-start Demo微服务启动项目(7002)
├─jeecg-visual
├─jeecg-cloud-monitor 微服务监控模块 (9111)
├─jeecg-cloud-xxljob 微服务xxljob定时任务服务端 (9080)
├─jeecg-cloud-sentinel sentinel服务端 (9000)
├─jeecg-cloud-test 微服务测试示例(各种例子)
├─jeecg-cloud-test-more 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
├─jeecg-cloud-test-rabbitmq 微服务测试示例(rabbitmq)
├─jeecg-cloud-test-seata 微服务测试示例(seata分布式事务)
├─jeecg-cloud-test-shardingsphere 微服务测试示例(分库分表)
03.启动VUE项目:jeecgboot-vue3
a.下载前端依赖
执行命令 pnpm install 或者双击 pinstall
b.配置后台接口
修改接口地址 .env.development
-----------------------------------------------------------------------------------------------------
VITE_PROXY = [["/jeecgboot","http://localhost:9999"],["/upload","http://localhost:3300/upload"]]
VITE_GLOB_DOMAIN_URL=http://localhost:9999
c.启动前端项目
npm run dev
d.访问
http://localhost:3100
admin/123456
4.3 自定义模块
00.注意
1.单体模式,【不支持模块独立启动,只能启动jeecg-system-start,其他模块引入到jeecg-system-start里】!!!
2.微服务模式,支持服务模块独立启动,参考文档
3.还有一种情况报错,就是自定义了包路径(把默认的org.jeecg根路径改了)
01.新建模块:vilgo
a.第1步:创建vilgo模块
根目录(jeecg-server) -> 右键 -> 新建模块 -> 类型:Java
名称:jeecg-module-vilgo
位置:D:\software_yare\JeecgBoot_20241130_online\jeecg-server
构建系统:Maven
JDK:jdk-1.8.0_281
父项:JEECG BOOT 3.7.1
b.第2步:jeecg-system-start 添加 依赖 【jeecg-module-vilgo】
a.配置文件
D:\software_yare\JeecgBoot_20241130_online\jeecg-server\jeecg-module-system\jeecg-system-start\pom.xml
b.新增内容
<!-- vilgo 示例模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-vilgo</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
c.新增后pom.xml(完整版)
<?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">
<parent>
<artifactId>jeecg-module-system</artifactId>
<groupId>org.jeecgframework.boot</groupId>
<version>3.7.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-system-start</artifactId>
<dependencies>
<!-- SYSTEM 系统管理模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-biz</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- DEMO 示例模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-demo</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- vilgo 示例模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-vilgo</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- flyway 数据库自动升级 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
c.第3步:jeecg-module-vilgo 自身需要添加依赖
a.规则如下
jeecg-boot-module-system作为启动项目,所以其他模块不要引用system。
jeecg-boot-base-core作为基础Core,所以新建模块一定要引用。
-------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
b.如果业务模块需要调用system里面的业务方法怎么办呢?
a.单体模式,可以引入jeecg-system-local-api
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
b.微服务模式,直接引入jeecg-boot-starter-cloud即可
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-cloud</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
c.新增后pom.xml(完整版),此处为【单体模式】
<?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>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.7.1</version>
</parent>
<artifactId>jeecg-module-vilgo</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
</dependencies>
</project>
02.新建模块:oa
a.第1步:创建oa模块
根目录(jeecg-server) -> 右键 -> 新建模块 -> 类型:Java
名称:jeecg-module-oa
位置:D:\software_yare\JeecgBoot_20241130_online\jeecg-server
构建系统:Maven
JDK:jdk-1.8.0_281
父项:JEECG BOOT 3.7.1
b.第2步:jeecg-system-start 添加 依赖 【jeecg-module-oa】
a.配置文件
D:\software_yare\JeecgBoot_20241130_online\jeecg-server\jeecg-module-system\jeecg-system-start\pom.xml
b.新增内容
<!-- OA 示例模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-oa</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
c.新增后pom.xml(完整版)
<?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">
<parent>
<artifactId>jeecg-module-system</artifactId>
<groupId>org.jeecgframework.boot</groupId>
<version>3.7.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-system-start</artifactId>
<dependencies>
<!-- SYSTEM 系统管理模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-biz</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- DEMO 示例模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-demo</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- OA 示例模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-oa</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- flyway 数据库自动升级 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
c.第3步:jeecg-module-oa 自身需要添加依赖
a.规则如下
jeecg-boot-module-system作为启动项目,所以其他模块不要引用system。
jeecg-boot-base-core作为基础Core,所以新建模块一定要引用。
-------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
b.如果业务模块需要调用system里面的业务方法怎么办呢?
a.单体模式,可以引入jeecg-system-local-api
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
b.微服务模式,直接引入jeecg-boot-starter-cloud即可
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-cloud</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
c.新增后pom.xml(完整版),此处为【单体模式】
<?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>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.7.1</version>
</parent>
<artifactId>jeecg-module-oa</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
</dependencies>
</project>

4.4 自定义包名
01.自定义包名,必选
a.(必选)修改配置
a.位置
jeecg-module-system\jeecg-system-start\src\main\resources\jeecg\jeecg_config.properties
b.修改配置
# 代码生成项目路径
project_path=D:\\workspace\\jeecg-boot
# 自定义包名
bussi_package=org.vilgo.demo
b.(必选)Swagger2Config
a.位置
jeecg-boot-base-core\src\main\java\org\jeecg\config\Swagger2Config.java
b.修改配置
@Configuration
@EnableSwagger2WebMvc
@Import(BeanValidatorPluginsConfiguration.class)
public class Swagger2Config implements WebMvcConfigurer {
@Bean
public Docket vilgoApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()).groupName("示例管理")
.select()
//此包路径下的类,才生成接口文档
.apis(RequestHandlerSelectors.basePackage("org.vilgo.demo"))
//加了ApiOperation注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securityScheme()))
.securityContexts(securityContexts())
.globalOperationParameters(setHeaderToken());
}
}
c.(必选)mybatis-plus配置
a.jeecg-server\jeecg-module-system\jeecg-system-start\src\main\resources\application-dev.yml
mybatis-plus:
mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml,classpath*:org/vilgo/**/xml/*Mapper.xml
b.org/jeecg/config/mybatis/MybatisPlusSaasConfig.java
@Configuration
@MapperScan(value={"org.jeecg.**.mapper*", "org.vilgo.**.mapper*"})
public class MybatisPlusSaasConfig {
c.jeecg-server\jeecg-module-system\jeecg-system-start\src\main\java\org\jeecg\JeecgSystemApplication.java
@Slf4j
@SpringBootApplication
@ComponentScan({"org.jeecg", "org.vilgo"})
//@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
public class JeecgSystemApplication extends SpringBootServletInitializer
d.(必选)解决字典值不显示
a.jeecg-boot-base-core\src\main\java\org\jeecg\common\aspect\DictAspect.java
将
@Pointcut("(@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Controller) || @annotation(org.jeecg.common.aspect.annotation.AutoDict)) " +
"&& execution(public org.jeecg.common.api.vo.Result org.jeecg..*.*(..)) ")
public void excudeService() {
}
更改为
@Pointcut("(@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Controller) || @annotation(org.jeecg.common.aspect.annotation.AutoDict)) " +
"&& execution(public org.jeecg.common.api.vo.Result org.jeecg..*.*(..)) || " +
"execution(public org.jeecg.common.api.vo.Result org.vilgo..*.*(..)))")
public void excudeService() {
}
02.自定义包名,可选
a.(可选)自定义数据库
a.注释掉类
jeecg-module-system\jeecg-system-biz\src\main\java\org\jeecg\config\init\CodeGenerateDbConfig.java
b.修改配置文件
jeecg-boot-module-system/src/main/resources/jeecg/jeecg_database.properties
#mysql
diver_name=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3307/jeecg-boot?useUnicode=true&characterEncoding=UTF-8
username=root
password=123456
database_name=jeecg-boot
c.重要配置注意
略
d.PostgreSQL模式配置
在配置文件jeecg/jeecg_database.properties,增加属性schemaName,多个逗号分隔
schemaName=public,test
b.(可选)新建HelloController代码
package org.vilgo.controller;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping(value = "/")
public Result<String> hello() {
Result<String> result = new Result<String>();
result.setResult("hello word!");
result.setSuccess(true);
return result;
}
}
c.(可选)去掉token拦截限制
如果不需要token可以访问,可以在 jeecg-boot-module-system 项目中修改 ShiroConfig.java 配置类
jeecg-server\jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\ShiroConfig.java
-------------------------------------------------------------------------------------------------
@Slf4j
@Configuration
public class ShiroConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
@Autowired
private Environment env;
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired(required = false)
private RedisProperties redisProperties;
/**
* Filter Chain定义说明
*
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//支持yml方式,配置拦截排除
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
String[] permissionUrl = shiroExcludeUrls.split(",");
for(String url : permissionUrl){
filterChainDefinitionMap.put(url,"anon");
}
}
}
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/hello/**", "anon"); //测试新添加的module,不带token访问
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
}
}

4.5 自定义路由
00.总结
a.方式1
debugger
console.log()
b.方式2
Vue.js devtools、JetBrains IDE Support
c.方式3
源代码/来源
01.vue.config.js
a.配置
a.productionSourceMap 配置
这是 Vue CLI 提供的直接选项,用于控制生产环境是否生成 Source Map。
productionSourceMap: true, // 或 false
-------------------------------------------------------------------------------------------------
作用:当设置为 true 时,生产环境会生成 .map 文件,便于调试和错误追踪。
当设置为 false 时,不生成 .map 文件,能提高构建速度,并减少打包文件的大小,避免敏感代码暴露。
适用环境:生产环境。
b.devtool 配置
在 configureWebpack 中通过 devtool 属性进一步控制 Source Map 的生成类型。
-------------------------------------------------------------------------------------------------
configureWebpack: {
devtool: 'cheap-module-source-map', // 或其他类型
}
-------------------------------------------------------------------------------------------------
常用选项:
eval: 生成代码的 eval 执行,构建速度最快,但调试体验较差。
source-map: 完整的 Source Map,构建较慢,但调试信息最详细。
cheap-module-source-map: 不包括列信息的 Source Map,构建速度和调试体验折中。
none: 不生成任何 Source Map。
b.完整文件
'use strict'
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const port = process.env.port || process.env.npm_config_port || 80 // 端口
// vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
// 这里只列一部分,具体配置参考文档
module.exports = {
// 部署生产环境和开发环境下的URL。
// 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
// 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist)
outputDir: 'dist',
// 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
assetsDir: 'static',
// 是否开启eslint保存检测,有效值:ture | false | 'error'
lintOnSave: process.env.NODE_ENV === 'development',
// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
productionSourceMap: true,
// webpack-dev-server 相关配置
devServer: {
host: '0.0.0.0',
port: port,
open: true,
proxy: {
// detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: {
target: `http://localhost:8080`,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
},
disableHostCheck: true
},
css: {
loaderOptions: {
sass: {
sassOptions: { outputStyle: "expanded" }
}
}
},
configureWebpack: {
name: name,
devtool:'cheap-module-source-map',
resolve: {
alias: {
'@': resolve('src')
}
},
plugins: [
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
new CompressionPlugin({
cache: false, // 不启用文件缓存
test: /\.(js|css|html)?$/i, // 压缩文件格式
filename: '[path].gz[query]', // 压缩后的文件名
algorithm: 'gzip', // 使用gzip压缩
minRatio: 0.8 // 压缩率小于1才会压缩
})
],
},
chainWebpack(config) {
config.plugins.delete('preload') // TODO: need test
config.plugins.delete('prefetch') // TODO: need test
// set svg-sprite-loader
config.module
.rule('svg')
.exclude.add(resolve('src/assets/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/assets/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
config
.when(process.env.NODE_ENV !== 'development',
config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/
}])
.end()
config
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
config.optimization.runtimeChunk('single'),
{
from: path.resolve(__dirname, './public/robots.txt'), //防爬虫文件
to: './' //到根目录下
}
}
)
}
}
02.vite.config.ts
a.配置
build.sourcemap
类型: boolean | 'inline' | 'hidden'
默认: false
构建后是否生成 source map 文件。如果为 true,将会创建一个独立的 source map 文件。
如果为 'inline',source map 将作为一个 data URI 附加在输出文件中。'hidden' 的工作原理与 true 相似,
只是 bundle 文件中相应的注释将不被保留。
b.完整文件
import type { UserConfig, ConfigEnv } from 'vite';
import pkg from './package.json';
import dayjs from 'dayjs';
import { loadEnv } from 'vite';
import { resolve } from 'path';
import { generateModifyVars } from './build/generate/generateModifyVars';
import { createProxy } from './build/vite/proxy';
import { wrapperEnv } from './build/utils';
import { createVitePlugins } from './build/vite/plugin';
import { OUTPUT_DIR } from './build/constant';
import topLevelAwait from 'vite-plugin-top-level-await';
function pathResolve(dir: string) {
return resolve(process.cwd(), '.', dir);
}
const { dependencies, devDependencies, name, version } = pkg;
const __APP_INFO__ = {
pkg: { dependencies, devDependencies, name, version },
lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
};
export default ({ command, mode }: ConfigEnv): UserConfig => {
const root = process.cwd();
const env = loadEnv(mode, root);
// The boolean type read by loadEnv is a string. This function can be converted to boolean type
const viteEnv = wrapperEnv(env);
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_DROP_CONSOLE } = viteEnv;
const isBuild = command === 'build';
return {
base: VITE_PUBLIC_PATH,
root,
resolve: {
alias: [
{
find: 'vue-i18n',
replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
},
// /@/xxxx => src/xxxx
{
find: /\/@\//,
replacement: pathResolve('src') + '/',
},
// /#/xxxx => types/xxxx
{
find: /\/#\//,
replacement: pathResolve('types') + '/',
},
],
},
server: {
// Listening on all local IPs
host: true,
https: false,
port: VITE_PORT,
// Load proxy configuration from .env
proxy: createProxy(VITE_PROXY),
},
build: {
minify: 'esbuild',
target: 'es2015',
cssTarget: 'chrome80',
outDir: OUTPUT_DIR,
// 开启 source map 以便调试
sourcemap: true,
terserOptions: {
compress: {
keep_infinity: true,
// Used to delete console in production environment
drop_console: VITE_DROP_CONSOLE,
drop_debugger: true,
},
},
// Turning off brotliSize display can slightly reduce packaging time
reportCompressedSize: false,
chunkSizeWarningLimit: 2000,
// 添加这个配置,防止组件被 tree-shaking 删除
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('components/Form/src/jeecg')) {
return 'jeecg';
}
},
},
},
},
define: {
// setting vue-i18-next
// Suppress warning
__INTLIFY_PROD_DEVTOOLS__: false,
__APP_INFO__: JSON.stringify(__APP_INFO__),
},
css: {
preprocessorOptions: {
less: {
modifyVars: generateModifyVars(),
javascriptEnabled: true,
},
},
},
// The vite plugin used by the project. The quantity is large, so it is separately extracted and managed
plugins: [
createVitePlugins(viteEnv, isBuild),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`,
}),
],
optimizeDeps: {
esbuildOptions: {
target: 'es2020',
},
// @iconify/iconify: The dependency is dynamically and virtually loaded by @purge-icons/generated, so it needs to be specified explicitly
include: ['@vue/runtime-core', '@vue/shared', '@iconify/iconify', 'ant-design-vue/es/locale/zh_CN', 'ant-design-vue/es/locale/en_US'],
},
};
};
03.项目调试
a.java项目
a.将mybatis日志变为debug
jeecg-server\jeecg-module-system\jeecg-system-start\src\main\resources\application-dev.yml
-------------------------------------------------------------------------------------------------
#Mybatis输出sql日志
logging:
level:
org.flywaydb: debug
org.jeecg.modules.system.mapper: info --变为debug
-------------------------------------------------------------------------------------------------
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl --打开注释
# 返回类型为Map,显示null对应的字段
call-setters-on-nulls: true
b.logback日志
jeecg-server\jeecg-module-system\jeecg-system-start\src\main\resources\logback-spring.xml
-------------------------------------------------------------------------------------------------
<!-- 日志输出级别 -->
<root level="INFO"> --变为DEBUG
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
<appender-ref ref="HTML" />
<appender-ref ref="FILE_HTML" />
</root>
b.vue项目
a.背景
jeecg前端属于【纯VUE项目】,F12打开控制台查看资源,要访问单个组件,必须要【配置路由】。
故而,不像【OMS调控云平台,html+css+js】那样,可以查看全部JS源码。
b.方式1:F12 -> 源代码/来源
http://localhost:3100/src/views/vilgo/demo11/VilgoLeaveList.vue 可以打断点
http://localhost:3100/src/views/vilgo/demo11/VilgoLeaveList.vue?t=1732957568016 不可以打断点
http://localhost:3100/src/views/vilgo/demo11/VilgoLeaveList.vue?vue&type=style&index=0&scoped=97a6aced&lang.less 不可以打断点
c.方式2:Vue.js devtools
定位按钮:Alt + S,Select component in the page
d.方式3:常见配置
a.插件
Vue.js devtools、JetBrains IDE Support
b.设置调试器端口、浏览器实时刷新
Build,Execution,Deployment -> Debugger -> Port:63342
Build,Execution,Deployment -> Debugger -> Can accept external connections
Build,Execution,Deployment -> Debugger -> Allow unsigned requests
c.添加调试配置
JavaScript Debug -> Name:Debug
JavaScript Debug -> URL:http://localhost:8080/
JavaScript Debug -> Remote URL:webpack:///src
JavaScript Debug -> Ensure breakpoints are detected when loading scripts
d.配置(vue.config.js)
module.exports = {
configureWebpack: {
devtool: 'source-map'
}
}
e.配置(vite.config.js)
export default defineConfig({
build: {
sourcemap: true,
}
});
f.配置(vite.config.ts)
export default defineConfig({
build: {
sourcemap: true,
}
});
g.启动
Breakpoint -> npm run serve:Run -> JavaScript Debug:Debug
h.需要构建到dist中,才可以查看源码吗
不,不必须需要构建到 dist/ 目录才能查看源码。
你可以在 开发环境 中使用 Vite 的 source map 来查看源码,而无需构建到 dist/ 目录。
Vite 默认在 开发模式 下会启用 source map,并且你可以直接在浏览器的开发者工具中查看源码。
---------------------------------------------------------------------------------------------
但是,如果你想在 生产环境 中查看源码(即构建后的代码),你需要通过 构建 生成文件到 dist/ 目录,
并确保构建过程中生成了相应的 source map 文件。
---------------------------------------------------------------------------------------------
开发环境:你不需要构建到 dist/ 目录,只需要启动开发服务器(npm run dev),即可在浏览器中查看源代码。
生产环境:如果要查看构建后的代码,需要先构建项目(npm run build),并确保 build.sourcemap 设置为 true,以生成源映射文件。
c.vue访问
a.方式1:通过路由访问
如果你的项目使用了 vue-router 来管理页面导航,那么通常会在路由配置中定义路径(如 /demo12/vilgo_leave),然后通过路由渲染这个组件。
import Demo12VilgoLeave from './Demo12VilgoLeave.vue';
const routes = [
{
path: '/demo12/vilgo_leave',
component: Demo12VilgoLeave,
},
];
-------------------------------------------------------------------------------------------------
在这种情况下,你需要通过定义好的路由路径(如 http://yourdomain/demo12/vilgo_leave)来访问页面。
b.方式2:直接挂载访问
如果不使用路由管理,可以直接将该组件挂载到页面上的一个 DOM 元素中,通过实例化 Vue 应用来访问:
-------------------------------------------------------------------------------------------------
<div id="app"></div>
import { createApp } from 'vue';
import Demo12VilgoLeave from './Demo12VilgoLeave.vue';
const app = createApp(Demo12VilgoLeave);
app.mount('#app');
-------------------------------------------------------------------------------------------------
在这种情况下,只需打开页面即可直接访问组件。
c.直接访问和路由访问的区别
a.直接访问
适合简单的单页面应用
页面逻辑独立,没有路由控制的复杂性
不能通过 URL 动态切换不同页面,需手动加载组件
b.路由访问
适合多页面应用
支持 URL 与组件的映射,用户可以通过不同路径访问对应页面
提供导航守卫等功能,增强用户体验和安全性
c.困惑:这个页面能否直接访问?
可以。
如果没有配置 vue-router,你只需要直接挂载组件到 HTML 中的某个 DOM 节点即可访问。
只不过在这种情况下,页面导航功能(如 新增、编辑)会比较局限,因为它们无法利用路由的 URL 切换特性。
d.总结
直接通过路径访问 .vue 文件(例如 http://127.0.0.1:8080/MyApp.vue)是不可能的,
因为 Vue 单文件组件(SFC)需要通过构建工具(如 Vite 或 Webpack)编译成普通的 JavaScript、HTML 和 CSS 文件后才能在浏览器中使用。
99.方式1:通过RBAC,逐个配置菜单、角色权限(考虑到admin脚手架有用户概念,设置白名单路由失效,故采用该方式)
a.一级菜单
菜单类型:一级菜单
菜单名称:示例管理
访问路径:/vilgo2
前端组件:layouts/default/index
组件名称:空,提示(vilgo2)
默认跳转地址:空
菜单图标:ant-design:slack-square-outlined
排序:1.1
-----------------------------------------------------------------------------------------------------
是否路由菜单:是
隐藏路由:否
隐藏Tab:否
是否缓存路由:否
聚合路由:否
打开方式:否
b.二级菜单
菜单类型:子菜单
菜单名称:Demo11
上级菜单:示例管理2
访问路径:/demo11/index
前端组件:vilgo/demo11/index
组件名称:demo11-index
菜单图标:ant-design:appstore-outlined
排序:1
是否路由菜单:是
隐藏路由:否
隐藏Tab:否
是否缓存路由:否
聚合路由:否
打开方式:否
c.菜单SQL
INSERT INTO `jeecg-boot_20241201_online`.`sys_permission`
(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
VALUES
('8237459381247836259', '1868914171970646017', 'Demo11', '/demo11/index', 'vilgo2/demo11/index', 1, '', NULL, 1, NULL, '0', 11.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('7594830291847362901', '1868914171970646017', 'Demo12', '/demo12/index', 'vilgo2/demo12/index', 1, '', NULL, 1, NULL, '0', 12.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('2389571927384612956', '1868914171970646017', 'Demo13', '/demo13/index', 'vilgo2/demo13/index', 1, '', NULL, 1, NULL, '0', 13.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('6723851923847621938', '1868914171970646017', 'Demo14', '/demo14/index', 'vilgo2/demo14/index', 1, '', NULL, 1, NULL, '0', 14.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('4927381648275910394', '1868914171970646017', 'Demo21', '/demo21/index', 'vilgo2/demo21/index', 1, '', NULL, 1, NULL, '0', 21.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('1057384920615473829', '1868914171970646017', 'Demo22', '/demo22/index', 'vilgo2/demo22/index', 1, '', NULL, 1, NULL, '0', 22.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('2849571639728394756', '1868914171970646017', 'Demo23', '/demo23/index', 'vilgo2/demo23/index', 1, '', NULL, 1, NULL, '0', 23.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('5984372104956283789', '1868914171970646017', 'Demo24', '/demo24/index', 'vilgo2/demo24/index', 1, '', NULL, 1, NULL, '0', 24.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('3746281957362819470', '1868914171970646017', 'Demo31', '/demo31/index', 'vilgo2/demo31/index', 1, '', NULL, 1, NULL, '0', 31.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('8295731963840276184', '1868914171970646017', 'Demo32', '/demo32/index', 'vilgo2/demo32/index', 1, '', NULL, 1, NULL, '0', 32.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('9038745619283746523', '1868914171970646017', 'Demo33', '/demo33/index', 'vilgo2/demo33/index', 1, '', NULL, 1, NULL, '0', 33.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0),
('1726384957284036172', '1868914171970646017', 'Demo34', '/demo34/index', 'vilgo2/demo34/index', 1, '', NULL, 1, NULL, '0', 34.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2024-12-17 15:04:36', 'admin', '2024-12-17 15:05:20', 0, 0, NULL, 0);
d.角色SQL
使用图形化界面操作
99.方式2(废弃):自动将文件夹下的,构建为路由
a.src/router/routes/vilgo.ts
import type { AppRouteRecordRaw } from '/@/router/types';
// 动态导入 /src/views/vilgo 目录下所有子文件夹的 index.vue 文件
const vilgoModules = import.meta.glob('/src/views/vilgo/*/index.vue', { eager: true });
const vilgoRouteList: AppRouteRecordRaw[] = [];
// 遍历每个动态导入的模块,生成路由配置
Object.keys(vilgoModules).forEach((key) => {
// 从路径中提取子文件夹名称,用 - 连接路径
const modulePath = key
.replace('/src/views', '') // 移除 src/views 部分
.replace('.vue', '') // 移除 .vue 后缀
.replaceAll('/', '-'); // 用 - 连接路径
// 生成路由配置
const route: AppRouteRecordRaw = {
path: `/vilgo${modulePath}`, // 动态路径
name: `Vilgo${modulePath}`, // 路由名称
component: vilgoModules[key], // 对应的组件
meta: {
title: `Vilgo ${modulePath} 页面`, // 页面标题
skipAuth: true, // 添加 skipAuth 字段跳过守卫
},
};
// 添加到路由列表
vilgoRouteList.push(route);
});
// 导出动态路由列表
export const vilgoRoutes = vilgoRouteList;
b.src/router/index.ts
import type { RouteRecordRaw } from 'vue-router';
import type { App } from 'vue';
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
import { basicRoutes } from './routes';
import { vilgoRoutes } from '/@/router/routes/vilgo';
// 白名单应该包含基本静态路由
const WHITE_NAME_LIST: string[] = [];
const getRouteNames = (array: any[]) =>
array.forEach((item) => {
WHITE_NAME_LIST.push(item.name);
getRouteNames(item.children || []);
});
getRouteNames(basicRoutes);
// app router
export const router = createRouter({
history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
routes: [...basicRoutes, ...vilgoRoutes], --加入到这里
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
});
// TODO 【QQYUN-4517】【表单设计器】记录分享路由守卫测试
router.beforeEach(async (to, from, next) => {
//console.group('【QQYUN-4517】beforeEach');
//console.warn('from', from);
//console.warn('to', to);
//console.groupEnd();
next();
});
// reset router
export function resetRouter() {
router.getRoutes().forEach((route) => {
const { name } = route;
if (name && !WHITE_NAME_LIST.includes(name as string)) {
router.hasRoute(name) && router.removeRoute(name);
}
});
}
// config router
export function setupRouter(app: App<Element>) {
app.use(router);
}
5 架构设计
5.1 模块修改
01.操作说明
a.更新内容
LICENSE
README.md
backend/README.md
frontend/README.md
b.使用工具
IDEA
Navicat
SubLime替换
XCodeMap插件
jd-gui-1.6.6
ClassFinal加密
c.建议操作
clean -> pisces-boot-base-core(install) -> compile
d.替换包名
项目 -> 代码 -> 重新设置代码格式
重命名 -> 项目文件 改成 所有位置
整理import(Ctrl + Shift + O) -> Alt批量cn.myslayers
-----------------------------------------------------------------------------------------------------
Ctrl + R -> 区分大小写 -> 替换 -> 保存全部
e.jeecg第三方包
hibernate-re-3.4.4-beta.jar --加密,可以删除
jeecg-boot-common-3.4.4.jar --已修改
jeecg-boot-starter-cloud-3.4.4.jar --已修改
jeecg-boot-starter-job-3.4.4.jar --已修改
jeecg-boot-starter-lock-3.4.4.jar --已修改
jeecg-boot-starter-rabbitmq-3.4.4.jar --已修改
jeecg-boot-starter-shardingsphere-3.4.4.jar --已修改
jeewx-api-1.5.0.jar
jimureport-spring-boot-starter-1.6.5.jar
02.模块名、author、TODO
a.关键字
a.分类1
jeecg
jeecgboot
jeecg-boot
b.分类2
todo
TODO
-------------------------------------------------------------------------------------------------
@time
issues
@author
@Author
@Author: Jeecg
@author: jeecg-boot
@author: JeecgBoot
-------------------------------------------------------------------------------------------------
---date
//begin
//end
//update-begin
//update-end
//update-begin-author
//update-end-author
c.分类3
\/\/TODO.* --【//TODO】开头
\/\/ TODO.* --【// TODO】开头
@author:\s*jeecg-boot --【@author: jeecg-boot】
\s*\*\s*@author.*\n --【消除@author该行】
\s*\*\s*@Author.*\n --【消除@Author该行】
\s*\*\s*@time.*\n --【消除@time该行】
\/\/.*issues.*\n? --【消除issues该行】
\/\/--.* --【//--】开头
<!--\s*update.*?--> --【<!-- udapte】开头
<!--\s*udapte.*?--> --【<!-- udapte】开头
\/\/begin.* --【//begin】开头
\/\/end.* --【//end】开头
\/\/ begin.* --【// begin】开头
\/\/ end.* --【// end】开头
\/\/update.*$ --【//update】开头
\/\/ update.*$ --【// update】开头
\/\/update-begin.* --【//update-begin】开头
\/\/update-end.* --【//update-end】开头
\/\/ update-begin.* --【// update-begin】开头
\/\/ update-end.* --【// update-end】开头
\/\/update-begin-author:.* --【//update-begin-author:】开头
\/\/update-end-author:.* --【//update-end-author:】开头
\/\/ update-begin-author:.* --【// update-begin-author:】开头
\/\/ update-ebd-author:.* --【// update-end-author:】开头
d.重要说明
后端 servlet: context-path: /pisces-boot
对应
前端 frontend\.env.development 的 VITE_GLOB_API_URL=/piscesboot
原因
前端 frontend\mock\_util.ts 的 export const baseUrl = '/piscesboot/mock';
前端【会将piscesboot】自动访问【/pisces-boot】,这个前端做了特别处理,故而不必纠结。
配置位置不详,若改此配置,请根据router路由,去查找,本次不作更改!!!
b.backend
a.文件1
backend/pisces-module-system/pisces-system-start/src/main/resources/application.yml
backend/pisces-module-system/pisces-system-start/src/main/resources/application-dev.yml
backend/pisces-module-system/pisces-system-start/src/main/resources/application-dm8.yml
backend/pisces-module-system/pisces-system-start/src/main/resources/application-local.yml
backend/pisces-module-system/pisces-system-start/src/main/resources/application-prod.yml
backend/pisces-module-system/pisces-system-start/src/main/resources/application-test.yml
backend/pisces-module-system/pisces-system-start/src/main/resources/banner.txt
backend/pisces-module-system/pisces-system-start/src/main/resources/logback-spring.xml
---------------------------------------------------------------------------------------------
servlet:
context-path: /pisces-boot
b.文件2
backend/docker-compose.yml
---------------------------------------------------------------------------------------------
version: '2'
services:
pisces-boot-mysql:
build:
context: ./db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_ROOT_HOST: '%'
TZ: Asia/Shanghai
restart: always
container_name: pisces-boot-mysql
image: pisces-boot-mysql
command:
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
--explicit_defaults_for_timestamp=true
--lower_case_table_names=1
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 3306:3306
pisces-boot-redis:
image: redis:5.0
ports:
- 6379:6379
restart: always
hostname: pisces-boot-redis
container_name: pisces-boot-redis
pisces-boot-system:
build:
context: ./pisces-module-system/pisces-system-start
restart: on-failure
depends_on:
- pisces-boot-mysql
- pisces-boot-redis
container_name: pisces-boot-system
image: pisces-boot-system
hostname: pisces-boot-system
ports:
- 8080:8080
c.文件3
backend/pisces-module-system/pisces-system-start/Dockerfile
---------------------------------------------------------------------------------------------
FROM anapsix/alpine-java:8_server-jre_unlimited
MAINTAINER [email protected]
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
#RUN mkdir -p /pisces-boot/config/pisces/
WORKDIR /pisces-boot
EXPOSE 8080
#ADD ./src/main/resources/pisces ./config/pisces
ADD ./target/pisces-system-start-3.4.4.jar ./
CMD sleep 60;java -Djava.security.egd=file:/dev/./urandom -jar pisces-system-start-3.4.4.jar
d.文件4
backend/pisces-server-cloud/docker-compose.yml
---------------------------------------------------------------------------------------------
version: '2'
services:
pisces-boot-nacos:
restart: always
build:
context: ./pisces-cloud-nacos
ports:
- 8848:8848
container_name: pisces-boot-nacos
hostname: pisces-boot-nacos
pisces-boot-system:
depends_on:
- pisces-boot-nacos
build:
context: ./pisces-system-cloud-start
container_name: pisces-system-start
hostname: pisces-boot-system
restart: on-failure
environment:
- TZ=Asia/Shanghai
pisces-boot-demo:
depends_on:
- pisces-boot-nacos
build:
context: ./pisces-demo-cloud-start
container_name: pisces-demo-start
hostname: pisces-boot-demo
restart: on-failure
environment:
- TZ=Asia/Shanghai
pisces-boot-gateway:
restart: on-failure
build:
context: ./pisces-cloud-gateway
ports:
- 9999:9999
depends_on:
- pisces-boot-nacos
- pisces-boot-system
container_name: pisces-boot-gateway
hostname: pisces-boot-gateway
# pisces-boot-sentinel:
# restart: on-failure
# build:
# context: ./pisces-visual/pisces-cloud-sentinel
# ports:
# - 9000:9000
# depends_on:
# - pisces-boot-nacos
# - pisces-boot-demo
# - pisces-boot-system
# - pisces-boot-gateway
# container_name: pisces-boot-sentinel
# hostname: pisces-boot-sentinel
#
# pisces-boot-xxljob:
# build:
# context: ./pisces-visual/pisces-cloud-xxljob
# ports:
# - 9080:9080
# container_name: pisces-boot-xxljob
# hostname: pisces-boot-xxljob
e.文件5
pisces-server-cloud/docker-compose-base.yml:1
---------------------------------------------------------------------------------------------
version: '2'
services:
pisces-boot-mysql:
build:
context: ../db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_ROOT_HOST: '%'
TZ: Asia/Shanghai
restart: always
container_name: pisces-boot-mysql
command:
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
--explicit_defaults_for_timestamp=true
--lower_case_table_names=1
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 3306:3306
pisces-boot-redis:
image: redis:5.0
ports:
- 6379:6379
restart: always
container_name: pisces-boot-redis
hostname: pisces-boot-redis
# pisces-boot-rabbitmq:
# image: rabbitmq:3.7.7-management
# ports:
# - 5672:5672
# - 15672:15672
# restart: always
# container_name: pisces-boot-rabbitmq
# hostname: pisces-boot-rabbitmq
# environment:
# RABBITMQ_DEFAULT_USER: guest
# RABBITMQ_DEFAULT_PASS: guest
c.frontend
a.文件1
frontend\.env.development
VITE_PROXY = [["/piscesboot","http://localhost:9994/pisces-boot"],["/upload","http://localhost:3300/upload"]]
VITE_GLOB_API_URL=/piscesboot
VITE_GLOB_DOMAIN_URL=http://localhost:9994/pisces-boot
---------------------------------------------------------------------------------------------
#微前端qiankun应用,命名必须以VITE_APP_SUB_开头,jeecg-app-1为子应用的项目名称,也是子应用的路由父路径
VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
b.文件2
frontend\.env.production
---------------------------------------------------------------------------------------------
VITE_GLOB_API_URL=/piscesboot
VITE_GLOB_DOMAIN_URL=http://localhost:9994/pisces-boot
c.文件3
frontend\.env
---------------------------------------------------------------------------------------------
VITE_GLOB_APP_SHORT_NAME = PiscesBootAdmin
---------------------------------------------------------------------------------------------
不动
# 文件预览地址
VITE_GLOB_ONLINE_VIEW_URL=http://fileview.jeecg.com/onlinePreview
d.文件4
frontend\.env.test
---------------------------------------------------------------------------------------------
VITE_GLOB_API_URL=/piscesboot
VITE_GLOB_DOMAIN_URL=http://localhost:9994/pisces-boot
e.文件5
frontend\package.json
---------------------------------------------------------------------------------------------
"name": "piscesboot-vue3",
"version": "3.4.4",
"author": {
"name": "pisces",
"email": "[email protected]",
},
---------------------------------------------------------------------------------------------
删除
"repository": {
"type": "git",
"url": "git+https://github.com/jeecgboot/jeecgboot-vue3.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/jeecgboot/jeecgboot-vue3/issues"
},
"homepage": "https://github.com/jeecgboot/jeecgboot-vue3",
---------------------------------------------------------------------------------------------
不动
@jeecg/online": "3.4.4-RC",
"vue-print-nb-jeecg": "^1.0.10",
f.文件6
frontend\Dockerfile
---------------------------------------------------------------------------------------------
FROM nginx
MAINTAINER [email protected]
VOLUME /tmp
ENV LANG en_US.UTF-8
RUN echo "server { \
listen 80; \
location /piscesboot/ { \
proxy_pass http://pisces-boot-system:8080/pisces-boot/; \
proxy_redirect off; \
proxy_set_header Host pisces-boot-system; \
proxy_set_header X-Real-IP \$remote_addr; \
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; \
} \
#解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题 \
location / { \
root /var/www/html/; \
index index.html index.htm; \
if (!-e \$request_filename) { \
rewrite ^(.*)\$ /index.html?s=\$1 last; \
break; \
} \
} \
access_log /var/log/nginx/access.log ; \
} " > /etc/nginx/conf.d/default.conf \
&& mkdir -p /var/www \
&& mkdir -p /var/www/html
ADD dist/ /var/www/html/
EXPOSE 80
EXPOSE 443
g.文件7
frontend\mock\_util.ts
---------------------------------------------------------------------------------------------
export const baseUrl = '/piscesboot/mock';
h.文件8
frontend\src\components\jeecg\OnLine\hooks\usePopBiz.ts
---------------------------------------------------------------------------------------------
删除
//update-begin-author:taoyan date:20220401 for: VUEN-583【vue3】JeecgBootException: sql黑名单校验不通过,请联系管理员!,前台无提示
//update-end-author:taoyan date:20220401 for: VUEN-583【vue3】JeecgBootException: sql黑名单校验不通过,请联系管理员!,前台无提示
i.文件9
frontend\src\components\JVxeCustom\src\components\JVxeSelectDictSearchCell.ts
---------------------------------------------------------------------------------------------
删除
//【jeecgboot-vue3/issues/I5QRT8】JVxeTypes.selectDictSearch sync问题
j.文件10
frontend\src\settings\siteSetting.ts
---------------------------------------------------------------------------------------------
// repo
export const GITHUB_URL = 'https://git.myslayers.cn/server/my-erp/frontend';
// doc
export const DOC_URL = 'https://e.myslayers.cn';
// url
export const SITE_URL = 'https://e.myslayers.cn';
k.文件11
frontend\src\views\demo\jeecg\JCodeEditDemo.vue
---------------------------------------------------------------------------------------------
defaultValue: 'Hello PiscesBoot',
l.文件12
frontend\src\views\monitor\datasource\datasource.data.ts
---------------------------------------------------------------------------------------------
const dbUrlMap = {
// MySQL 数据库
'1': { dbUrl: 'jdbc:mysql://127.0.0.1:3306/pisces-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false' },
//MySQL5.7+ 数据库
'4': {
dbUrl:
'jdbc:mysql://127.0.0.1:3306/pisces-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai',
},
// Oracle
'2': { dbUrl: 'jdbc:oracle:thin:@127.0.0.1:1521:ORCL' },
// SQLServer 数据库
'3': { dbUrl: 'jdbc:sqlserver://127.0.0.1:1433;SelectMethod=cursor;DatabaseName=piscesboot' },
// Mariadb 数据库
'5': { dbUrl: 'jdbc:mariadb://127.0.0.1:3306/pisces-boot?characterEncoding=UTF-8&useSSL=false' },
// Postgresql 数据库
'6': { dbUrl: 'jdbc:postgresql://127.0.0.1:5432/pisces-boot' },
// 达梦 数据库
'7': { dbUrl: 'jdbc:dm://127.0.0.1:5236/?pisces-boot&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8' },
// 人大金仓 数据库
'8': { dbUrl: 'jdbc:kingbase8://127.0.0.1:54321/pisces-boot' },
// 神通 数据库
'9': { dbUrl: 'jdbc:oscar://192.168.1.125:2003/pisces-boot' },
// SQLite 数据库
'10': { dbUrl: 'jdbc:sqlite://opt/test.db' },
// DB2 数据库
'11': { dbUrl: 'jdbc:db2://127.0.0.1:50000/pisces-boot' },
// Hsqldb 数据库
'12': { dbUrl: 'jdbc:hsqldb:hsql://127.0.0.1/pisces-boot' },
// Derby 数据库
'13': { dbUrl: 'jdbc:derby://127.0.0.1:1527/pisces-boot' },
// H2 数据库
'14': { dbUrl: 'jdbc:h2:tcp://127.0.0.1:8082/pisces-boot' },
// 其他数据库
'15': { dbUrl: '' },
};
m.文件13
frontend\src\views\sys\login\TokenLoginPage.vue
---------------------------------------------------------------------------------------------
<div class="app-loading-title">PiscesBoot平台</div>
l.文件14
if (id.includes('components/Form/src/jeecg')) {
return 'jeecg';
}
m.重点文件
a.文件1
frontend\.env.development
---------------------------------------------------------------------------------------------
#微前端qiankun应用,命名必须以VITE_APP_SUB_开头,jeecg-app-1为子应用的项目名称,也是子应用的路由父路径
VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
c.文件3
frontend\.env
---------------------------------------------------------------------------------------------
不动
# 文件预览地址
VITE_GLOB_ONLINE_VIEW_URL=http://fileview.jeecg.com/onlinePreview
g.文件7
frontend\mock\_util.ts
---------------------------------------------------------------------------------------------
export const baseUrl = '/piscesboot/mock';
n.依赖
e.文件5
frontend\package.json
---------------------------------------------------------------------------------------------
不动
@jeecg/online": "3.4.4-RC",
"vue-print-nb-jeecg": "^1.0.10",
---------------------------------------------------------------------------------------------
frontend\pnpm-lock.yaml
'@jeecg/online':
specifier: 3.4.4-RC
version: https://registry.npmmirror.com/@jeecg/online/-/online-3.4.4-RC.tgz
vue-print-nb-jeecg:
specifier: ^1.0.10
version: https://registry.npmmirror.com/vue-print-nb-jeecg/-/vue-print-nb-jeecg-1.0.11.tgz
---------------------------------------------------------------------------------------------
frontend\yarn.lock
"@jeecg/[email protected]":
version "3.4.4-RC"
resolved "https://registry.npmmirror.com/@jeecg/online/-/online-3.4.4-RC.tgz#bc669c999bf4e95d7588071e5bffbbc1deafecfb"
integrity sha512-EjibCo++GszsWuLn3+kqaxt12Zim7f0XWSiHzypef4aAqBKYDnG30Z5K3j5W1g4g22fjVUkrb1rQ8rSLTlPdBw==
03.version号、common模块1
a.常用
<!--=======================================system模块=======================================-->
<!--=======================================core模块=======================================-->
<!--=======================================local模块=======================================-->
<!--=======================================ejia模块(公共)=======================================-->
<!--=======================================yunzhijia模块(公共)=======================================-->
<!--=======================================各类功能模块=======================================-->
<!--=======================================微服务=======================================-->
<!--=======================================开源服务=======================================-->
b.常见依赖
<!--=======================================core模块=======================================-->
<dependency>
<groupId>cn.myslayers</groupId>
<artifactId>pisces-boot-base-core</artifactId>
<version>${parent.version}</version>
</dependency>
<!--=======================================system模块=======================================-->
<dependency>
<groupId>cn.myslayers</groupId>
<artifactId>pisces-system-biz</artifactId>
<version>${parent.version}</version>
</dependency>
<!--=======================================ejia模块(公共)=======================================-->
<dependency>
<groupId>cn.myslayers</groupId>
<artifactId>pisces-module-ejia</artifactId>
<version>${parent.version}</version>
<scope>compile</scope>
</dependency>
<!--=======================================开源服务=======================================-->
c.新建模块说明
第1步:引用如下模块
【必须】:核心模块
<dependency>
<groupId>cn.myslayers</groupId>
<artifactId>pisces-boot-base-core</artifactId>
<version>1.0.0</version>
</dependency>
【可选】:业务模块 调用 系统模块
<dependency>
<groupId>cn.myslayers</groupId>
<artifactId>pisces-system-biz</artifactId>
<version>1.0.0</version>
</dependency>
【公共】:业务模块 调用 ejia
<dependency>
<groupId>cn.myslayers</groupId>
<artifactId>pisces-module-ejia</artifactId>
<version>1.0.0</version>
</dependency>
第2步:需要去【pisces-module-system/pisces-system-start/pom.xml】注册【业务模块】,如:
<dependency>
<groupId>cn.myslayers</groupId>
<artifactId>pisces-module-demo</artifactId>
<version>1.0.0</version>
</dependency>
第3步:【可选】,自定义包,需要配置【扫描器】
请移步至【根目录/releases】查看【自定义包名.md】
d.核心路径
pisces-boot-base-core\src\main\java\cn\myslayers\common\desensitization\aspect\SensitiveDataAspect.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\constant\enums\RoleIndexConfigEnum.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\aspect\DictAspectTask.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\aspect\DictAspect.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\mybatis\aspect\DynamicTableAspect.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\mybatis\MybatisPlusSaasConfig.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\JeecgBaseConfig.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\system\util\ResourceUtil.java --包路径
pisces-boot-base-core\src\main\java\cn\myslayers\common\util\dynamic\db\DbTypeUtils.java
pisces-boot-starter\pisces-boot-base-core\src\main\java\cn\myslayers\common\util\DySmsEnum.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\JeecgBaseConfig.java
pisces-boot-starter\pisces-boot-base-core\src\main\java\cn\myslayers\config\shiro\ShiroConfig.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\AutoPoiConfig.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\Swagger2Config.java
-------------------------------------------------------------------------------------------------
pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\modules\cas\util\CasServiceUtil.java
pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\modules\monitor\domain\RedisInfo.java
pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\modules\quartz\job\ScheduledSyncEjiaUser.java
e.将 AbstractQueryBlackListHandler 合并到 DictQueryBlackListHandler 类种,删掉抽象类
@Slf4j
public abstract class AbstractQueryBlackListHandler {
/**
* key-表名
* value-字段名,多个逗号隔开
* 两种配置方式-- 全部配置成小写
* ruleMap.put("sys_user", "*")sys_user所有的字段不支持查询
* ruleMap.put("sys_user", "username,password")sys_user中的username和password不支持查询
*/
public static Map<String, String> ruleMap = new HashMap<>();
static {
ruleMap.put("sys_user", "password,salt");
}
/**
* 根据 sql语句 获取表和字段信息,需要到具体的实现类重写此方法-
* 不同的场景 处理可能不太一样 需要自定义,但是返回值确定
*
* @param sql
* @return
*/
protected abstract List<QueryTable> getQueryTableInfo(String sql);
/**
* 校验sql语句 成功返回true
*
* @param sql
* @return
*/
public boolean isPass(String sql) {
List<QueryTable> list = null;
try {
list = this.getQueryTableInfo(sql.toLowerCase());
} catch (Exception e) {
log.warn("校验sql语句,解析报错:{}", e.getMessage());
}
if (list == null) {
return true;
}
log.info("--获取sql信息--", list.toString());
boolean flag = true;
for (QueryTable table : list) {
String name = table.getName();
String fieldString = ruleMap.get(name);
// 有没有配置这张表
if (fieldString != null) {
if ("*".equals(fieldString) || table.isAll()) {
flag = false;
log.warn("sql黑名单校验,表【" + name + "】禁止查询");
break;
} else if (table.existSameField(fieldString)) {
flag = false;
break;
}
}
}
return flag;
}
public String getError() {
return "系统设置了安全规则,敏感表和敏感字段禁止查询,联系管理员授权!";
}
/**
* 查询的表的信息
*/
protected class QueryTable {
//表名
private String name;
//表的别名
private String alias;
// 字段名集合
private Set<String> fields;
// 是否查询所有字段
private boolean all;
public QueryTable() {
}
public QueryTable(String name, String alias) {
this.name = name;
this.alias = alias;
this.all = false;
this.fields = new HashSet<>();
}
public void addField(String field) {
this.fields.add(field);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<String> getFields() {
return new HashSet<>(fields);
}
public void setFields(Set<String> fields) {
this.fields = fields;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public boolean isAll() {
return all;
}
public void setAll(boolean all) {
this.all = all;
}
/**
* 判断是否有相同字段
*
* @param fieldString
* @return
*/
public boolean existSameField(String fieldString) {
String[] arr = fieldString.split(",");
for (String exp : fields) {
for (String config : arr) {
if (exp.equals(config)) {
// 非常明确的列直接比较
log.warn("sql黑名单校验,表【" + name + "】中字段【" + config + "】禁止查询");
return true;
} else {
// 使用表达式的列 只能判读字符串包含了
String aliasColumn = config;
if (alias != null && alias.length() > 0) {
aliasColumn = alias + "." + config;
}
if (exp.indexOf(aliasColumn) > 0) {
log.warn("sql黑名单校验,表【" + name + "】中字段【" + config + "】禁止查询");
return true;
}
}
}
}
return false;
}
@Override
public String toString() {
return "QueryTable{" +
"name='" + name + '\'' +
", alias='" + alias + '\'' +
", fields=" + fields +
", all=" + all +
'}';
}
}
}
-----------------------------------------------------------------------------------------------------
@Component("dictQueryBlackListHandler")
public class DictQueryBlackListHandler extends AbstractQueryBlackListHandler {
@Override
protected List<QueryTable> getQueryTableInfo(String dictCodeString) {
if (dictCodeString != null && dictCodeString.indexOf(SymbolConstant.COMMA) > 0) {
String[] arr = dictCodeString.split(SymbolConstant.COMMA);
if (arr.length != 3 && arr.length != 4) {
return null;
}
String tableName = getTableName(arr[0]);
QueryTable table = new QueryTable(tableName, "");
// 无论什么场景 第二、三个元素一定是表的字段,直接add
table.addField(arr[1].trim());
String filed = arr[2].trim();
if (oConvertUtils.isNotEmpty(filed)) {
table.addField(filed);
}
List<QueryTable> list = new ArrayList<>();
list.add(table);
return list;
}
return null;
}
/**
* 取where前面的为:table name
*
* @param str
* @return
*/
private String getTableName(String str) {
String[] arr = str.split("\\s+(?i)where\\s+");
return arr[0];
}
}
04.version号、common模块2
a.权限
a.shiro位置
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/constant/CommonConstant.java
-------------------------------------------------------------------------------------------------
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:cn.jeecg.config.shiro.ShiroRealm.authorizationCache:";
/**
* ::非常重要::
* 注意:这四个常量值如果修改,需要与 jeecg-boot-starter/jeecg-boot-common/cn.jeecg.config.FeignConfig 类中的值保持一致。
*/
String X_ACCESS_TOKEN = "X-Access-Token";
String X_SIGN = "X-Sign";
String X_TIMESTAMP = "X-TIMESTAMP";
b.其他
a.服务名称
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/constant/ServiceNameConstants.java
-------------------------------------------------------------------------------------------------
/**
* 服务名称
*/
public interface ServiceNameConstants {
/**
* 微服务名:系统管理模块
*/
String SERVICE_SYSTEM = "jeecg-system";
/**
* 微服务名: demo模块
*/
String SERVICE_DEMO = "jeecg-demo";
/**
* gateway通过header传递根路径 basePath
*/
String X_GATEWAY_BASE_PATH = "X_GATEWAY_BASE_PATH";
}
c.目前jeecg
a.pisces-boot-base-core
D:\software_yare\my-erp_20241226\backend\pisces-boot-starter\pisces-boot-base-core\src\main\java\cn\myslayers\common\util\dynamic\db\FreemarkerParseFactory.java
-------------------------------------------------------------------------------------------------
import org.jeecgframework.codegenerate.generate.util.SimpleFormat;
-------------------------------------------------------------------------------------------------
加入内部类,已拷贝codegenerate-1.4.3源码版本
// 该方法使用hutools进行重写
public String timestampToString(Object obj, String pattern) {
if (obj == null) {
return "";
}
try {
SimpleDateFormat inputFormat = new SimpleDateFormat("dd-MM-yy");
SimpleDateFormat outputFormat = new SimpleDateFormat(pattern);
Date date = inputFormat.parse(obj.toString());
return outputFormat.format(date);
} catch (java.text.ParseException e) {
log.error("日期解析错误", e);
return "error";
}
}
-------------------------------------------------------------------------------------------------
此时把 pisces-boot-base-core 的 pom.xml文件做了修改,删除codegenerate-1.4.3
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>codegenerate</artifactId>
<version>1.4.3</version>
</dependency>
05.代码生成
a.版本3.7.1
a.位置
jeecg-module-system\jeecg-system-biz\src\main\java\org\jeecg\modules\system\service\impl\SysBaseApiImpl.java
jeecg-boot-base-core\src\main\java\org\jeecg\config\firewall\interceptor\enums\LowCodeUrlsEnum.java
b.文件
jeecg-module-system\jeecg-system-biz\src\main\java\org\jeecg\modules\system\service\impl\SysBaseApiImpl.java
-------------------------------------------------------------------------------------------------
/**
* 获取带参数的报表地址,因为多个参数可能顺序会变,所以要将参数顺序重排,获取所有可能的地址集合
* 如下:参数顺序调整使用in查询,就能查询出菜单数据
* /online/cgreport/1624393012494286850?name=1&age=16
* /online/cgreport/1624393012494286850?age=16&name=1
* @param path
* @return
*/
private List<String> getOnlinePossiblePaths(String path){
List<String> result = new ArrayList<>();
1743行 log.info(" path = "+ path);
if (path.indexOf("?") >= 0 && (path.contains("/online/cgreport/") || path.contains("/online/cgformList/") || path.contains("/online/graphreport/"))) {
//包含?说明有多个参数
String[] pathArray = path.split("\\?");
if(oConvertUtils.isNotEmpty(pathArray[1])){
String[] params = pathArray[1].split("&");
if(params.length==1){
result.add(path);
}else{
result = anm(pathArray[0], Arrays.asList(params));
}
}else{
result.add(path);
}
}else{
result.add(path);
}
return result;
}
b.版本3.4.4
a.位置
Online表单开发
http://localhost:3100/jeecgboot/online/cgform/head/list?column=createTime&order=desc&pageNo=1&pageSize=10©Type=0&_t=1735890750976
b.代码模板
a.注意
codegenerate-1.4.3.jar已加密
b.位置1
backend/pisces-module-system/pisces-system-biz/src/main/java/cn/myslayers/config/init/CodeTemplateInitListener.java
---------------------------------------------------------------------------------------------
Resource[] resources = resolver.getResources("classpath*:jeecg/code-template-online/**/*");
String createFilePath = configPath + filepath.substring(filepath.indexOf("jeecg/code-template-online"));
c.位置2
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/constant/enums/CgformEnum.java
---------------------------------------------------------------------------------------------
ONE(1, "one", "/jeecg/code-template-online", "default.one", "经典风格", new String[]{"vue3", "vue", "vue3Native"}),
MANY(2, "many", "/jeecg/code-template-online", "default.onetomany", "经典风格", new String[]{"vue"}),
d.位置3
pisces-module-demo\src\main\java\cn\myslayers\modules\demo\mock\vxe\controller\VxeMockController.java 扫描org/jeecg/
---------------------------------------------------------------------------------------------
String path = "classpath:cn/myslayers/modules/demo/mock/vxe/json/dlglong.json";
e.位置4
pisces-module-demo\src\main\java\cn\myslayers\modules\demo\mock\MockController.java
---------------------------------------------------------------------------------------------
private final String JSON_PATH = "classpath:org/jeecg/modules/demo/mock/json";
f.位置5
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\one\java\${bussiPackage}\${entityPackage}\vue\${entityName}List.vuei
---------------------------------------------------------------------------------------------
import { JeecgListMixin } from '@/mixins/JeecgListMixin'
g.位置6
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\one2\java\${bussiPackage}\vue\${entityPackage}\${entityName}List.vuei
---------------------------------------------------------------------------------------------
import { JeecgListMixin } from '@/mixins/JeecgListMixin'
h.位置7
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\onetomany\java\${bussiPackage}\${entityPackage}\vue\${entityName}List.vuei
---------------------------------------------------------------------------------------------
import { JeecgListMixin } from '@/mixins/JeecgListMixin'
i.位置8
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\onetomany\java\${bussiPackage}\${entityPackage}\vue3\${entityName}__data.tsi
---------------------------------------------------------------------------------------------
import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types'
j.位置9
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\onetomany2\java\${bussiPackage}\${entityPackage}\vue\modules\${entityName}Modal.vuei
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\onetomany2\java\${bussiPackage}\${entityPackage}\vue\modules\[1-n]Modal.vuei
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\onetomany2\java\${bussiPackage}\${entityPackage}\vue\${entityName}List.vuei
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template\onetomany2\java\${bussiPackage}\${entityPackage}\vue\[1-n]List.vuei
---------------------------------------------------------------------------------------------
import JDate from 、'@/components/jeecg/JDate'
k.位置10
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template-online\common\form\native\vue3NativeImport.ftl
---------------------------------------------------------------------------------------------
'/@/components/Form/src/jeecg/components/JPopup.vue';
l.位置11
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template-online\default\one\java\${bussiPackage}\${entityPackage}\vue\${entityName}List.vuei
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template-online\default\one\java\${bussiPackage}\${entityPackage}\vue3Native\${entityName}List.vuei
---------------------------------------------------------------------------------------------
import { JeecgListMixin } from '@/mixins/JeecgListMixin'
m.位置12
pisces-module-system\pisces-system-biz\src\main\resources\pisces\code-template-online\default\onetomany\java\${bussiPackage}\${entityPackage}\vue\${entityName}List.vuei
---------------------------------------------------------------------------------------------
import { JeecgListMixin } from '@/mixins/JeecgListMixin'
c.sys_permission
id parent_id name url component
1455100420297859074 低代码开发 /online layouts/default/index
1455101470794850305 1455100420297859074 Online表单开发 /online/cgform super/online/cgform/index
1460888189937176577 1455100420297859074 Online报表配置 /online/cgreport super/online/cgreport/index
1461270075543347202 1455100420297859074 Online表单视图 /online/copyform/:code super/online/cgform/CgformCopyList
1461291438825558017 1455100420297859074 AUTO在线报表 /online/cgreport/:id super/online/cgreport/auto/OnlCgReportList
1463073196350701569 1455100420297859074 AUTO在线图表 /online/graphreport/chart/:code super/online/graphreport/auto/GraphreportAutoChart
1465686870713782273 1455100420297859074 AUTO在线表单 /online/cgformList/:id super/online/cgform/auto/default/OnlineAutoList
1509417558230999041 1455100420297859074 AUTO树表单列表 /online/cgformTreeList/:id super/online/cgform/auto/tree/OnlineAutoTreeList
1580833876002627585 1447763172274495489 Online报表例子 /online/cgreport/1256627801873821698 1
1580834045490257922 1447763172274495489 Online表单例子 /online/cgformList/d35109c3632c4952a19ecc094943dd71 1
-------------------------------------------------------------------------------------------------
http://localhost:3100/jeecgboot/online/cgform/head/list?column=createTime&order=desc&pageNo=1&pageSize=10©Type=0&_t=1735890750976
d.版本3.4.4
将 AbstractQueryBlackListHandler 合并到 DictQueryBlackListHandler 类种,删掉抽象类
pisces-boot-base-core\src\main\java\cn\myslayers\common\util\dynamic\db\FreemarkerParseFactory.java
backend/pisces-module-system/pisces-system-biz/src/main/java/cn/myslayers/config/init/CodeTemplateInitListener.java
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/constant/enums/CgformEnum.java
-------------------------------------------------------------------------------------------------
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/desensitization/util/SensitiveInfoUtil.java
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/aspect/PermissionDataAspect.java
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/aspect/annotation/PermissionData.java
backend/pisces-boot-base-core/src/main/java/cn/myslayers/common/aspect/UrlMatchEnum.java
e.版本3.4.4
jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\ShiroRealm.java
jeecg-boot-base-core\src\main\java\org\jeecg\common\util\TokenUtils.java
jeecg-boot-base-core\src\main\java\org\jeecg\common\desensitization\util\SensitiveInfoUtil.java
jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\ShiroRealm.java
jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\filters\JwtFilter.java
jeecg-boot-base-core\src\main\java\org\jeecg\common\aspect\PermissionDataAspect.java
-------------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\ShiroRealm.java
/**
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
*
* @param auth 用户登录的账号密码信息
* @return 返回封装了用户信息的 AuthenticationInfo 实例
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
String token = (String) auth.getCredentials();
if (token == null) {
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
throw new AuthenticationException("token为空!");
}
// 校验token有效性
LoginUser loginUser = null;
try {
103行 loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
e.printStackTrace();
return null;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
-----------------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\common\util\TokenUtils.java
/**
* 获取登录用户
*
* @param commonApi
* @param username
* @return
*/
public static LoginUser getLoginUser(String username, CommonAPI commonApi, RedisUtil redisUtil) {
LoginUser loginUser = null;
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
//【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
if (redisUtil.hasKey(loginUserKey)) {
try {
loginUser = (LoginUser) redisUtil.get(loginUserKey);
//解密用户
115行 SensitiveInfoUtil.handlerObject(loginUser, false);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} else {
// 查询用户信息
loginUser = commonApi.getUserByName(username);
}
return loginUser;
}
-------------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\common\desensitization\util\SensitiveInfoUtil.java
/**
* 处理Object
* @param obj 方法返回值
* @param isEncode 是否加密(true: 加密操作 / false:解密操作)
* @return
* @throws IllegalAccessException
*/
public static Object handlerObject(Object obj, boolean isEncode) throws IllegalAccessException {
log.debug(" obj --> "+ obj.toString());
long startTime=System.currentTimeMillis();
if (oConvertUtils.isEmpty(obj)) {
return obj;
}
// 判断是不是一个对象
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
boolean isSensitiveField = field.isAnnotationPresent(SensitiveField.class);
if(isSensitiveField){
// 必须有SensitiveField注解 才作处理
if(field.getType().isAssignableFrom(String.class)){
//必须是字符串类型 才作处理
field.setAccessible(true);
String realValue = (String) field.get(obj);
if(realValue==null || "".equals(realValue)){
continue;
}
SensitiveField sf = field.getAnnotation(SensitiveField.class);
if(isEncode==true){
//加密
String value = SensitiveInfoUtil.getEncodeData(realValue, sf.type());
field.set(obj, value);
}else{
//解密只处理 encode类型的
if(sf.type().equals(SensitiveEnum.ENCODE)){
String value = SensitiveInfoUtil.getDecodeData(realValue);
field.set(obj, value);
}
}
}
}
}
//long endTime=System.currentTimeMillis();
//log.info((isEncode ? "加密操作," : "解密操作,") + "当前程序耗时:" + (endTime - startTime) + "ms");
101行 return obj;
}
-------------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\ShiroRealm.java
/**
* 校验token的有效性
*
* @param token
*/
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解密获得username,用于和数据库进行对比
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token非法无效!");
}
// 查询用户信息
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
//LoginUser loginUser = commonApi.getUserByName(username);
if (loginUser == null) {
throw new AuthenticationException("用户不存在!");
}
// 判断用户状态
if (loginUser.getStatus() != 1) {
throw new AuthenticationException("账号已被锁定,请联系管理员!");
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
}
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
140行 String userTenantIds = loginUser.getRelTenantIds();
if(oConvertUtils.isNotEmpty(userTenantIds)){
String contextTenantId = TenantContext.getTenant();
String str ="0";
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
String[] arr = userTenantIds.split(",");
if(!oConvertUtils.isIn(contextTenantId, arr)){
throw new AuthenticationException("用户租户信息变更,请重新登陆!");
}
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
}
}
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
return loginUser;
}
-------------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\filters\JwtFilter.java
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
// update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
if (oConvertUtils.isEmpty(token)) {
token = httpServletRequest.getParameter("token");
}
// update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
76行 return true;
}
-------------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\config\shiro\filters\JwtFilter.java
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
51行 return true;
} catch (Exception e) {
JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
return false;
//throw new AuthenticationException("Token失效,请重新登录", e);
}
}
-------------------------------------------------------------------------------------------------
jeecg-boot-base-core\src\main\java\org\jeecg\common\aspect\PermissionDataAspect.java
@Pointcut("@annotation(cn.myslayers.common.aspect.annotation.PermissionData)")
public void pointCut() {
}
@Around("pointCut()")
public Object arround(ProceedingJoinPoint point) throws Throwable{
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
PermissionData pd = method.getAnnotation(PermissionData.class);
String component = pd.pageComponent();
String requestMethod = request.getMethod();
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
requestPath = filterUrl(requestPath);
//update-begin-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
//先判断是否online报表请求
// TODO 参数顺序调整有隐患
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0){
// 获取地址栏参数
String urlParamString = request.getParameter(CommonConstant.ONL_REP_URL_PARAM_STR);
if(oConvertUtils.isNotEmpty(urlParamString)){
requestPath+="?"+urlParamString;
}
}
//update-end-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
log.info("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
72行 String username = JwtUtil.getUserNameByToken(request);
//查询数据权限信息
//TODO 微服务情况下也得支持缓存机制
List<SysPermissionDataRuleModel> dataRules = commonApi.queryPermissionDataRule(component, requestPath, username);
if(dataRules!=null && dataRules.size()>0) {
//临时存储
JeecgDataAutorUtils.installDataSearchConditon(request, dataRules);
//TODO 微服务情况下也得支持缓存机制
SysUserCacheInfo userinfo = commonApi.getCacheUser(username);
JeecgDataAutorUtils.installUserInfo(request, userinfo);
}
return point.proceed();
}
c.版本3.4.4
a.位置
<!--hibernate(非开源,且加密)-->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>hibernate-re</artifactId>
<version>3.4.4-beta</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.7.Final</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
b.报错
添加了上述【关于代码生成器】的依赖,就会报错
Exception in thread "main" org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [cn.myslayers.PiscesSystemStandaloneApplication];
nested exception is java.io.FileNotFoundException: class path resource [org/jeecg/common/util/security/AbstractQueryBlackListHandler.class] cannot be opened because it does not exist
at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:610)
at org.springframework.context.annotation.ConfigurationClassParser.access$800(ConfigurationClassParser.java:111)
at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGroupingHandler.lambda$processGroupImports$1(ConfigurationClassParser.java:812)
at java.util.ArrayList.forEach(ArrayList.java:1259)
-------------------------------------------------------------------------------------------------
会去找这个类org/jeecg/common/util/security/AbstractQueryBlackListHandler.class
c.修改为org/jeecg后,报错如下
org\jeecgframework\boot\hibernate-re\3.4.4-beta\hibernate-re-3.4.4-beta.jar!\org\jeecg\modules\online\auth\a\a.class
-------------------------------------------------------------------------------------------------
2025-01-09 14:51:34.746 [main] WARN com.alibaba.druid.pool.DruidAbstractDataSource:1260 - oracle.jdbc.driver.OracleDriver is deprecated.Having use oracle.jdbc.OracleDriver.
2025-01-09 14:51:40.594 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext:591 - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'onlCgformAuthController': Unsatisfied dependency expressed through field 'onlCgformFieldService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'onlCgformFieldServiceImpl' defined in URL [jar:file:/C:/Users/mysla/.m2/repository/org/jeecgframework/boot/hibernate-re/3.4.4-beta/hibernate-re-3.4.4-beta.jar!/org/jeecg/modules/online/cgform/service/a/c.class]: Post-processing of merged bean definition failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [org.jeecg.modules.online.cgform.service.a.c] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
2025-01-09 14:51:40.638 [main] INFO org.apache.catalina.core.StandardService:173 - Stopping service [Tomcat]
2025-01-09 14:51:40.654 [main] INFO o.s.b.a.l.ConditionEvaluationReportLoggingListener:136 -
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'onlCgformAuthController': Unsatisfied dependency expressed through field 'onlCgformFieldService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'onlCgformFieldServiceImpl' defined in URL [jar:file:/C:/Users/mysla/.m2/repository/org/jeecgframework/boot/hibernate-re/3.4.4-beta/hibernate-re-3.4.4-beta.jar!/org/jeecg/modules/online/cgform/service/a/c.class]: Post-processing of merged bean definition failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [org.jeecg.modules.online.cgform.service.a.c] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:659)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:639)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:740)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:415)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301)
at cn.myslayers.PiscesSystemApplication.main(PiscesSystemApplication.java:25)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'onlCgformFieldServiceImpl' defined in URL [jar:file:/C:/Users/mysla/.m2/repository/org/jeecgframework/boot/hibernate-re/3.4.4-beta/hibernate-re-3.4.4-beta.jar!/org/jeecg/modules/online/cgform/service/a/c.class]: Post-processing of merged bean definition failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [org.jeecg.modules.online.cgform.service.a.c] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:597)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:656)
... 20 more
Caused by: java.lang.IllegalStateException: Failed to introspect Class [org.jeecg.modules.online.cgform.service.a.c] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
at org.springframework.util.ReflectionUtils.getDeclaredFields(ReflectionUtils.java:743)
at org.springframework.util.ReflectionUtils.doWithLocalFields(ReflectionUtils.java:675)
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.buildResourceMetadata(CommonAnnotationBeanPostProcessor.java:377)
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.findResourceMetadata(CommonAnnotationBeanPostProcessor.java:358)
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessMergedBeanDefinition(CommonAnnotationBeanPostProcessor.java:306)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyMergedBeanDefinitionPostProcessors(AbstractAutowireCapableBeanFactory.java:1116)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
... 29 more
Caused by: java.lang.NoClassDefFoundError: Lorg/jeecg/common/system/api/ISysBaseAPI;
at java.lang.Class.getDeclaredFields0(Native Method)
at java.lang.Class.privateGetDeclaredFields(Class.java:2583)
at java.lang.Class.getDeclaredFields(Class.java:1916)
at org.springframework.util.ReflectionUtils.getDeclaredFields(ReflectionUtils.java:738)
... 35 more
Caused by: java.lang.ClassNotFoundException: org.jeecg.common.system.api.ISysBaseAPI
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 39 more
-------------------------------------------------------------------------------------------------
主要问题:NoClassDefFoundError 和 ClassNotFoundException
核心原因:org.jeecg.common.system.api.ISysBaseAPI 类在运行时无法加载。这通常是因为:
相关依赖没有正确引入或配置。
项目中有依赖冲突,导致需要的类未被正确加载。
类加载器未能找到该类的定义。
-------------------------------------------------------------------------------------------------
oracle.jdbc.driver.OracleDriver is deprecated
这是一个警告,建议使用 oracle.jdbc.OracleDriver 替代,但与主要问题无关。
UnsatisfiedDependencyException
onlCgformAuthController 的 onlCgformFieldService 依赖注入失败。
依赖注入失败的原因是 onlCgformFieldServiceImpl 的定义解析失败。
NoClassDefFoundError: org/jeecg/common/system/api/ISysBaseAPI
根本问题在于 ISysBaseAPI 类无法加载。
日志指向 jeecg-boot-base-common 模块或相关模块的依赖缺失或加载失败。
d.一些常见接口
org\jeecgframework\boot\hibernate-re\3.4.4-beta\hibernate-re-3.4.4-beta.jar!\org\jeecg\modules\online\cgform\c\d.class
@GetMapping({"/list"})
@PermissionData
public Result<IPage<OnlCgformHead>> a(OnlCgformHead var1, @RequestParam(name = "pageNo",defaultValue = "1") Integer var2, @RequestParam(name = "pageSize",defaultValue = "10") Integer var3, HttpServletRequest var4) {
Result var5 = new Result();
QueryWrapper var6 = QueryGenerator.initQueryWrapper(var1, var4.getParameterMap());
Page var7 = new Page((long)var2, (long)var3);
IPage var8 = this.onlCgformHeadService.page(var7, var6);
if (var1.getCopyType() != null && var1.getCopyType() == 0) {
this.onlCgformHeadService.initCopyState(var8.getRecords());
}
var5.setSuccess(true);
var5.setResult(var8);
return var5;
}
d.codegenerate-1.4.3.jar
a.文件1
public class a
{
private static final Logger q = LoggerFactory.getLogger(a.class);
private static final String r = "jeecg/jeecg_database";
private static final String s = "jeecg/jeecg_config";
private static ResourceBundle t = c("jeecg/jeecg_database"); static {
if (t == null) {
q.debug("通过class目录加载配置文件 jeecg/jeecg_database");
t = ResourceBundle.getBundle("jeecg/jeecg_database");
}
} private static ResourceBundle u = c("jeecg/jeecg_config"); static {
if (u == null) {
q.debug("通过class目录加载配置文件 jeecg/jeecg_config");
u = ResourceBundle.getBundle("jeecg/jeecg_config");
}
}
-------------------------------------------------------------------------------------------------
String str = System.getProperty("user.dir") + File.separator + "config" + File.separator + paramString + ".properties";
public static String c = "jdbc:mysql://localhost:3306/jeecg-boot?useUnicode=true&characterEncoding=UTF-8";
public static String f = "c:/workspace/jeecg";
-------------------------------------------------------------------------------------------------
y = (null != Class.forName("org.jeecg.modules.online.desform.entity.DesignForm"));
y = (null != Class.forName("org.jeecg.modules.online.desform.entity.DesignForm"));
A = (null != Class.forName("org.jeecg.modules.aspect.SysUserAspect"));
B = (null != Class.forName("org.jeecg.modules.extbpm.listener.execution.ProcessEndListener"));
b.文件2
g = (null != Class.forName("org.jeecg.modules.online.desform.entity.DesignForm"));
h = (null != Class.forName("org.jeecg.modules.online.desform.service.IDesignFormAuthService"));
i = (null != Class.forName("org.jeecg.modules.aspect.SysUserAspect"));
j = (null != Class.forName("org.jeecg.modules.extbpm.listener.execution.ProcessEndListener"))
c.文件3
if (str.indexOf("/BOOT-INF/classes!") != -1 || str.indexOf("/BOOT-INF/lib/") != -1 || str.indexOf(".jar!") != -1) {
str = System.getProperty("user.dir") + File.separator + "config/jeecg/code-template-online/".replace("/", File.separator);
a.debug("---JAR--config--classpath-------" + str);
}
d.文件4
a.debug("----jeecg---Code----Generation----[单表模型:" + this.b.getTableName() + "]------- 生成中。。。");
if (a(str2, "/").equals("jeecg/code-template")) {
str2 = "/" + a(str2, "/") + "/one";
a.b(str2);
}
e.文件5
public static void main(String[] args) {
MainTableVo mainTableVo = new MainTableVo();
mainTableVo.setTableName("jeecg_order_main");
mainTableVo.setEntityName("TestOrderMain");
mainTableVo.setEntityPackage("test2");
mainTableVo.setFtlDescription("订单");
ArrayList<SubTableVo> arrayList = new ArrayList();
SubTableVo subTableVo1 = new SubTableVo();
subTableVo1.setTableName("jeecg_order_customer");
subTableVo1.setEntityName("TestOrderCustom");
subTableVo1.setEntityPackage("test2");
subTableVo1.setFtlDescription("客户明细");
subTableVo1.setForeignKeys(new String[] { "order_id" });
arrayList.add(subTableVo1);
SubTableVo subTableVo2 = new SubTableVo();
subTableVo2.setTableName("jeecg_order_ticket");
subTableVo2.setEntityName("TestOrderTicket");
subTableVo2.setEntityPackage("test2");
subTableVo2.setFtlDescription("产品明细");
e.一些由jeecg变成pisces引发的报错1
a.报错
2025-01-03 17:37:52.846 [http-nio-9994-exec-5] WARN org.apache.shiro.authc.AbstractAuthenticator:216 - Authentication failed for token submission [cn.myslayers.config.shiro.JwtToken@7aa36112]. Possible unexpected error? (Typical or expected login exceptions should extend from AuthenticationException).
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve type id 'org.jeecg.common.system.vo.LoginUser' as a subtype of `java.lang.Object`: no such class found
at [Source: (byte[])"["org.jeecg.common.system.vo.LoginUser",{"id":"IeYObuSa00K741IFwOv28hJ0HmTTuJxHVNAypLbgTR0=","username":"grXDeTwZNGStnSu4yQuIQA==","realname":"UTQuOhZCZXJmP76PKkcB/A==","password":"zXbRTxWkeGhiRRQn1xPLhg==","orgCode":"A02A18A03","avatar":"","birthday":["java.util.Date","2018-12-05"],"sex":1,"email":"f28NhDpxZuAdWRLU7uWS8w==","phone":"5nve2G2QIL/+BASLYhjl0g==","status":1,"delFlag":0,"activitiSync":1,"createTime":["java.util.Date",1561110850000],"userIdentity":2,"departIds":"","post":"Mdl9ext9euNJ"[truncated 66 bytes]; line: 1, column: 41]; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'org.jeecg.common.system.vo.LoginUser' as a subtype of `java.lang.Object`: no such class found
-------------------------------------------------------------------------------------------------
org.jeecg.common.system.vo.LoginUser
b.解决
去找org.jeecg.common.system.vo.LoginUser,说明redis有缓存
redis:
database: 2
host: localhost
port: 6379
password: xxxxxx
-----------------------------------------------------------------------------------------------------
清除2中全部信息
c.可能的排查方向
org.jeecg.modules.aspect.SysUserAspect
org.jeecg.common.system.vo.LoginUser.java
org.jeecg.modules.system.entity.SysUser.java
org.jeecg.common.aspect.annotation.PermissionData
f.一些由jeecg变成pisces引发的报错2
a.报错
2025-01-05 11:07:25.115 [http-nio-9994-exec-5] ERROR o.jeecg.common.exception.JeecgBootExceptionHandler:79
- Invalid bound statement (not found): org.jeecg.modules.system.mapper.SysDepartMapper.queryUserDeparts
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
org.jeecg.modules.system.mapper.SysDepartMapper.queryUserDeparts
b.解决
@Configuration
@MapperScan(value={"org.jeecg.**.mapper*"})
public class MybatisPlusSaasConfig {
-------------------------------------------------------------------------------------------------
mybatis-plus:
mapper-locations: classpath*:org/jeecg/**/mapper/xml/*.xml
c.对于 @MapperScan:
@MapperScan(value={"cn.myslayers.**.mapper*"})
-------------------------------------------------------------------------------------------------
cn.myslayers: 指定了扫描从该包名开始。
**: 表示可以匹配任意数量的中间子包层级(包括零个)。
mapper*: 表示包名以 mapper 开头,后面可以跟任意字符。
-------------------------------------------------------------------------------------------------
实际覆盖范围:
cn.myslayers.mapper
cn.myslayers.sub.mapper
cn.myslayers.sub1.sub2.mapperA
d.对于 mapper-locations
mapper-locations: classpath*:org/jeecg/**/mapper/xml/*.xml
-------------------------------------------------------------------------------------------------
classpath*:: 表示从类路径的所有位置查找资源。
org/jeecg: 表示从 org.jeecg 包路径开始。
**/mapper/xml: 可以匹配多个中间层级,直到 mapper/xml 目录。
*.xml: 匹配所有以 .xml 结尾的文件。
-------------------------------------------------------------------------------------------------
实际覆盖范围:
org/jeecg/mapper/xml/*.xml
org/jeecg/module/mapper/xml/*.xml
org/jeecg/a/b/c/mapper/xml/*.xml
e.常见原因
没有扫描到mapper接口包,通常需要在启动类使用注解扫描这个包 比如@MapperScan(“com.xxx.mapper”)
mapper.xml中的namespace和实际的mapper文件不一致
mapper接口中的方法名和mapper.xml中的id标签不一致
-------------------------------------------------------------------------------------------------
那就是配置问题,检查置文件中扫描Mapper.xml的配置
mybatis.type-aliases-package=com.inkyi.*.entity
mybatis.mapper-locations=classpath:mapper/*/*.xml
将 mybatis.mapper-locations=classpath:mapper/*/*.xml 改为 mybatis.mapper-locations=classpath*:mapper/*/*.xml
classpath:只会扫描本项目下的class目录
classpath*: 会扫描Jar包中的class目录
-------------------------------------------------------------------------------------------------
【经测试】,
jeecg-module-common\target\classes\org\jeecg\modules\system\mapper\SysAnnouncementMapper.class
mapper没有找到对应xml
jeecg-module-common\target\classes\org\jeecg\modules\system\mapper\xml\SysAnnouncementSendMapper.xml
-------------------------------------------------------------------------------------------------
【最终解决版本】,pom指定resource保留src/main/java里面的xml内容
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.json</include>
<include>**/*.ftl</include>
</includes>
</resource>
</resources>
-------------------------------------------------------------------------------------------------
如果上述方法都不能解决问题,建议:
清理IDE缓存并重启IDE
删除target目录,重新clean和compile项目
检查是否有其他配置文件覆盖了MyBatis的配置
06.pisces-boot-starter、pisces-boot-biz
a.pisces-boot-starter-autopoi
a.文件1
pisces-boot-starter-autopoi\autopoi-web\src\main\java\cn\myslayers\poi\excel\def\MapExcelConstants.java
-------------------------------------------------------------------------------------------------
public final static String JEECG_MAP_EXCEL_VIEW = "jeecgMapExcelView";
b.文件2
pisces-boot-starter-autopoi\autopoi-web\src\main\java\cn\myslayers\poi\excel\def\NormalExcelConstants.java
-------------------------------------------------------------------------------------------------
public final static String JEECG_ENTITY_EXCEL_VIEW = "jeecgEntityExcelView";
c.文件3
pisces-boot-starter-autopoi\autopoi-web\src\main\java\cn\myslayers\poi\excel\def\TemplateExcelConstants.java
-------------------------------------------------------------------------------------------------
public final static String JEECG_TEMPLATE_EXCEL_VIEW = "jeecgTemplateExcelView";
d.文件4
pisces-boot-starter-autopoi\autopoi-web\src\main\java\cn\myslayers\poi\excel\def\TemplateWordConstants.java
-------------------------------------------------------------------------------------------------
public final static String JEECG_TEMPLATE_WORD_VIEW = "jeecgTemplateWordView";
b.pisces-module-demo(SQL涉及jeecg)
a.文件1
pisces-module-demo\src\main\java\cn\myslayers\modules\dlglong\controller\DlMockController.java
-------------------------------------------------------------------------------------------------
String path = "classpath:org/jeecg/modules/dlglong/json/dlglong.json";
b.代码生成
D:\software_yare\my-erp_20241226\backend\pisces-module-biz\pisces-module-demo\src\main\java\cn\myslayers\modules\demo\mock\json\asyn_tree_list_2.json
-------------------------------------------------------------------------------------------------
[
{
"id": 21,
"name": "弹框选择Demo",
"component": "pisces/SelectDemo",
"orderNum": 1,
"hasChildren": false
},
{
"id": 22,
"name": "单表模型示例",
"component": "pisces/PiscesDemoList",
"orderNum": 2,
"hasChildren": false
},
{
"id": 23,
"name": "一对多Tab示例",
"component": "pisces/tablist/PiscesOrderDMainList",
"orderNum": 3,
"hasChildren": false
}
]
c.剩余jeecg
D:\software_yare\my-erp_20241226\backend\pisces-module-biz\pisces-module-demo\src\main\java\cn\myslayers\modules\demo\test\mapper\PiscesOrderCustomerMapper.java:
18 * @return
19 */
20: @Delete("DELETE FROM JEECG_ORDER_CUSTOMER WHERE ORDER_ID = #{mainId}")
21 public boolean deleteCustomersByMainId(String mainId);
22
..
27 * @return 订单客户集合
28 */
29: @Select("SELECT * FROM JEECG_ORDER_CUSTOMER WHERE ORDER_ID = #{mainId}")
30 public List<PiscesOrderCustomer> selectCustomersByMainId(String mainId);
31 }
D:\software_yare\my-erp_20241226\backend\pisces-module-biz\pisces-module-demo\src\main\java\cn\myslayers\modules\demo\test\mapper\PiscesOrderTicketMapper.java:
18 * @return
19 */
20: @Delete("DELETE FROM JEECG_ORDER_TICKET WHERE ORDER_ID = #{mainId}")
21 public boolean deleteTicketsByMainId(String mainId);
22
..
27 * @return 返回订单机票集合
28 */
29: @Select("SELECT * FROM JEECG_ORDER_TICKET WHERE ORDER_ID = #{mainId}")
30 public List<PiscesOrderTicket> selectTicketsByMainId(String mainId);
31 }
-------------------------------------------------------------------------------------------------
已修改
c.pisces-module-sales
a.文件1
pisces-module-sales\src\main\java\cn\myslayers\controller\SaleArticleController.java
-------------------------------------------------------------------------------------------------
String str = "http://hr.app.myslayers.cn/jeecg-boot/sys/common/static/temp/倒运申请表_1699579055315.docx";
d.pisces-module-ejia
a.文件1
pisces-module-ejia/src/main/java/cn/myslayers/ejia/mapper/xml/WorkFlowCallbackLogMapper.xml
-------------------------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.myslayers.modules.hazard.mapper.WorkFlowCallbackLogMapper">
</mapper>
07.pisces-module-system
a.pisces-system-api
a.文件1
pisces-module-system\pisces-system-api\pisces-system-cloud-api\src\main\java\cn\myslayers\common\system\api\ISysBaseAPI.java
-------------------------------------------------------------------------------------------------
@ConditionalOnMissingClass("org.jeecg.modules.system.service.impl.SysBaseApiImpl")
b.pisces-system-biz
a.文件1
pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\modules\message\handle\enums\SendMsgTypeEnum.java
-------------------------------------------------------------------------------------------------
SMS("1", "org.jeecg.modules.message.handle.impl.SmsSendMsgHandle")
b.文件2
pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\modules\system\util\RandImageUtil.java
-------------------------------------------------------------------------------------------------
public static final String KEY = "JEECG_LOGIN_KEY";
c.文件3
pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\modules\system\util\SecurityUtil.java
-------------------------------------------------------------------------------------------------
private static String key = "JEECGBOOT1423670";
c.pisces-system-start
xxx
88.规范说明
a.规范说明
D:\software_yare\my-erp_20241226\backend\pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\utils
D:\software_yare\my-erp_20241226\backend\pisces-module-system\pisces-system-biz\src\main\java\cn\myslayers\utils\UserInfoUtil.java
/**
* 项目名称:E家对接公共类
* 获取登录晋钢E家当前用户信息
* 为了解决循环依赖问题,只能将该工具类从module-ejia中copy到system-biz中
*/
-----------------------------------------------------------------------------------------------------
每层规范
@Api(tags = "文章通告")
@RestController
@RequestMapping("api/hazard/article")
@Slf4j
org.jeecg.common.api.vo.Result
-----------------------------------------------------------------------------------------------------
工具类,必须要求hutool
-----------------------------------------------------------------------------------------------------
嵌套for+if,改用stream+
版本号,由于本公司自行使用,统一使用1.0.0
ComponentScan位置
99.修改工程
a.提问
针对这个 @backend 我需要涉及 jeecg的地方,都改成pisces(源点数字,公司名称)。
涉及修改的地方,包括包名、模块名、代码引用、....。凡是包括 jeecg的地方都需要修改。
-----------------------------------------------------------------------------------------------------
972条记录,可以帮我分类吗?然后汇总给我
-----------------------------------------------------------------------------------------------------
你关联下 @backend 这个里面 全面的 pom.xml文件
-----------------------------------------------------------------------------------------------------
我现在 要求 换成 PISCESBOOT,然后换一遍写法,润色下
b.建议1
项目结构层面:
目录名称:如 jeecg-module-system → pisces-module-system
模块名称:如 jeecg-boot-base-core → pisces-boot-base-core
-----------------------------------------------------------------------------------------------------
代码层面:
包名:com.jeecg → com.pisces
包引用:所有 import 语句中的 jeecg 相关包
注解中的包名引用
配置文件中的包扫描路径
-----------------------------------------------------------------------------------------------------
配置文件层面:
pom.xml 中的:
groupId
artifactId
依赖引用
项目名称和描述
application.yml/properties 中的:
包扫描配置
其他 jeecg 相关的配置项
-----------------------------------------------------------------------------------------------------
数据库层面:
表前缀
可能存在的固定配置数据
-----------------------------------------------------------------------------------------------------
执行步骤:
1.首先进行项目备份
2.搜索分析阶段:
使用工具搜索所有包含 "jeecg" 的文件
分类整理需要修改的内容
评估修改的影响范围
3.修改顺序:
先修改 pom.xml 父模块配置
修改各子模块的 pom.xml
修改包名和目录结构
修改代码引用
修改配置文件
修改数据库相关配置
4.测试验证:
编译验证
启动验证
功能测试
接口测试
c.建议2
a.第一阶段 - 核心配置修改:
a.修改根目录的 pom.xml:
groupId: org.jeecgframework.boot → com.pisces.boot
artifactId: jeecg-boot-parent → pisces-boot-parent
所有模块引用
b.重命名核心模块目录:
jeecg-boot-base-core → pisces-boot-base-core
jeecg-module-system → pisces-module-system
jeecg-module-demo → pisces-module-demo
jeecg-module-yunzhijia → pisces-module-yunzhijia
jeecg-module-ejia → pisces-module-ejia
jeecg-module-erp → pisces-module-erp
b.第二阶段 - 包名修改:
a.基础包名:
com.jeecg → com.pisces
org.jeecgframework → com.pisces.framework
b.子模块包名:
com.jeecg.yunzhijia → com.pisces.yunzhijia
com.jeecg.modules → com.pisces.modules
c.第三阶段 - 配置文件修改:
a.application.yml 中的配置:
包扫描路径
其他 jeecg 相关配置项
b.其他配置文件:
logback 配置
mybatis 配置
spring 配置
d.第四阶段 - 代码引用修改:
import 语句修改
注解中的包名引用
配置类中的包名引用
文档注释中的引用
e.建议的执行顺序:
先创建一个新的分支进行修改
从最核心的配置开始修改(pom.xml)
然后是目录结构
接着是包名
最后是具体的代码引用
d.建议3
a.提问
972条记录,可以帮我分类吗?然后汇总给我
b.项目结构相关(目录名称):
核心模块:
jeecg-boot-base-core
jeecg-server-cloud
业务模块:
jeecg-module-system
jeecg-module-demo
jeecg-module-ejia
jeecg-module-yunzhijia
jeecg-module-erp
c.配置文件相关:
Maven 配置:
根目录 pom.xml
各模块的 pom.xml
依赖引用中的 jeecg 相关配置
应用配置:
application.yml
application-*.yml
logback-spring.xml
d.包名相关:
基础包名:
org.jeecgframework → com.pisces.framework
org.jeecg.modules → com.pisces.modules
com.jeecg → com.pisces
e.代码文件相关:
实体类:
JeecgDemo.java
JeecgOrderMain.java
JeecgOrderCustomer.java
JeecgOrderTicket.java
控制器:
JeecgDemoController.java
JeecgOrderMainController.java
JeecgDynamicDataController.java
服务类:
IJeecgDemoService.java
IJeecgOrderMainService.java
Mapper:
JeecgDemoMapper.java
JeecgOrderMainMapper.java
f.数据库相关:
表前缀
配置数据
SQL文件中的引用
g.注释和文档相关:
Java文件中的注释
README.md
其他文档文件
h.修改数量统计:
目录重命名:约 7 个主要目录
配置文件:约 10-15 个文件
Java 文件:约 100-150 个文件(包括实体类、控制器、服务类等)
数据库相关:约 20-30 张表
文档文件:约 5-10 个文件
i.建议的修改顺序:
先修改配置文件(pom.xml)
修改目录结构
修改包名
修改具体的代码文件
修改数据库相关内容
最后修改文档和注释
e.建议4
a.需要修改的范围:
包名(package):从 jeecg 改为 pisces
模块名:jeecg-boot 改为 pisces-boot
数据库相关:表名前缀、SQL文件名等
配置文件:application.yml、docker配置等
Maven配置:groupId、artifactId等
代码引用:import语句等
注释和文档
b.修改顺序
1. Maven 配置修改
- 修改 pom.xml 中的 groupId 从 org.jeecg 改为 com.pisces
- 修改所有模块的 artifactId,把 jeecg 替换为 pisces
- 修改依赖引用
2. 包名修改
- 将所有 org.jeecg/cn.jeecg 包名改为 com.pisces
- 修改对应的目录结构
- 修改所有类中的 package 声明
- 修改所有 import 语句
3. 配置文件修改
- application.yml/properties 中的配置项
- docker 相关配置
- 数据库配置
4. 数据库相关
- 修改表名前缀从 jeecg_ 到 pisces_
- 修改 SQL 文件名
- 修改 SQL 文件内容中的表名
5. 代码修改
- Controller/Service/Mapper 等类名中的 Jeecg 替换为 Pisces
- 注释中的相关描述
- 其他代码中的引用
6. 其他
- 项目名称
- README 等文档
- 日志配置
5.2 单体架构
00.目标
a.请求
JwtUtil
TokenUtils
ShiroConfig
ShiroRealm
JimuReportTokenService
LoginController
SysUserController
SysDictController
SysDepartController
SysDictServiceImpl
SysCommentServiceImpl
-----------------------------------------------------------------------------------------------------
http://127.0.0.1:3100/piscesboot/online/cgform/head/list
http://127.0.0.1:3100/piscesboot/online/cgreport/head/list
-----------------------------------------------------------------------------------------------------
onl_cgform_head表 对应 Online表单开发(主页)
onl_cgreport_head表 对应 Online报表配置(主页)
b.当前思路
gateway,后端路由端口转发
系统模块,地址回溯,A -> B -> A
统一调用各个服务,aop注解,id+secret
研究springcloud,远程调用接口
每个服务,配置文件可以覆盖,直接放到根路径,pisces-module-start 或者 pisces-module-biz直接src
-----------------------------------------------------------------------------------------------------
单个启动,必须依赖路由功能,网关
code、system统一配置文件,根目录,子模块可以覆盖
不是网关不同路由,是同一路由下,分批启动服务
-----------------------------------------------------------------------------------------------------
qiankun启动多个模块
自动登录,任意token
-----------------------------------------------------------------------------------------------------
暴露:可能指某些接口或数据暴露在不安全的环境中,存在风险。
访问时的认证:使用固定的用户名(user)和密码(pass)进行访问;使用ID(如cccccc)和token进行身份验证;支持第三方登录(如OAuth等)。
公共接口:某些接口可能是公开的,不需要特定的权限即可访问。
鉴权:需要对访问进行权限控制,确保只有授权用户或系统可以访问特定资源。
新的注解方式:可能指在代码中使用新的注解(如Spring中的@PreAuthorize、@Secured等)来实现权限控制。
-----------------------------------------------------------------------------------------------------
一个基于 YAML 配置(不使用数据库)实现的权限接口设计方案说明,该方案支持4种类型(例如类似于 yunzhijia、dingding、wechat 等),
并通过类注解和方法注解实现细粒度的权限控制。假设我们的需求是将各个接口按“组”划分,例如:
A组:对应权限 a、b、c
B组:对应其他权限
C组:对应另外一些权限
D组:如果需要,可扩展为第四种类型
01.移除redis,跳过登录
01.说明
a.类
JwtUtil
TokenUtils
ShiroConfig
ShiroRealm
JimuReportTokenService
LoginController
SysUserController
SysDictController
SysDepartController
SysDictServiceImpl
SysCommentServiceImpl
b.移除结果
无法移除,仍需要redis,用于【hibernate-re/codegenerate加密jar包】或者【CacheConstant这个类关联的代码】
c.20250110
20250110,已将涉及【sys:cache:encrypt:user::admin】对应的【CacheConstant这个类关联的代码】全部删除
虽然【登录+代码生成器】没有问题,但是控制台报错
ERROR o.s.d.redis.listener.RedisMessageListenerContainer:665 - Connection failure occurred. Restarting subscription task after 5000 ms
-------------------------------------------------------------------------------------------------
现在关闭【redis】报错日志,最终测试,发现关闭仍然报错,【最终方案:yml配置redis连接信息,不让其报错,但实际并未使用】
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
logging:
level:
...
org.springframework.data.redis: warn # Redis相关日志级别
org.springframework.data.redis.connection: warn # Redis连接相关日志级别
org.springframework.data.redis.listener: warn # Redis监听器相关日志级别
io.lettuce.core: warn # Lettuce Redis客户端日志级别
02.说明2
a.现在redis必须存在该键
sys:cache:encrypt:user::admin
b.值示例
{
"org.jeecg.common.system.vo.LoginUser": {
"id": "IeYObuSa00K741IFwOv28hJ0HmTTuJxHVNAypLbgTR0=",
"username": "grXDeTwZNGStnSu4yQuIQA==",
"realname": "UTQuOhZCZXJmP76PKkcB/A==",
"password": "G7p9v3dfc4utuXCFbXnNPw==",
"orgCode": "A02A18A03",
"avatar": "",
"birthday": [
"java.util.Date",
"2018-12-05"
],
"sex": 1,
"email": "f28NhDpxZuAdWRLU7uWS8w==",
"phone": "5nve2G2QIL/+BASLYhjl0g==",
"status": 1,
"delFlag": 0,
"activitiSync": 1,
"createTime": [
"java.util.Date",
1561110850000
],
"userIdentity": 2,
"departIds": "",
"post": "Mdl9ext9euNJ79uDwKbe/w==",
"telephone": null,
"relTenantIds": "",
"clientId": null
}
}
03.说明3
a.org.jeecg.common.constant.CacheConstant.java
public interface CacheConstant {
String SYS_DICT_CACHE = "sys:cache:dict";
String SYS_ENABLE_DICT_CACHE = "sys:cache:dictEnable";
String SYS_DICT_TABLE_CACHE = "sys:cache:dictTable";
String SYS_DICT_TABLE_BY_KEYS_CACHE = "sys:cache:dictTableByKeys";
String SYS_DATA_PERMISSIONS_CACHE = "sys:cache:permission:datarules";
String SYS_USERS_CACHE = "sys:cache:encrypt:user"; --缓存user
String SYS_DEPARTS_CACHE = "sys:cache:depart:alldata";
String SYS_DEPART_IDS_CACHE = "sys:cache:depart:allids";
String TEST_DEMO_CACHE = "test:demo";
String SYS_DYNAMICDB_CACHE = "sys:cache:dbconnect:dynamic:";
String GATEWAY_ROUTES = "sys:cache:cloud:gateway_routes";
String ROUTE_JVM_RELOAD_TOPIC = "gateway_jvm_route_reload_topic";
String PLUGIN_MALL_RANKING = "pluginMall::rankingList";
String PLUGIN_MALL_PAGE_LIST = "pluginMall::queryPageList";
String ONLINE_LIST = "sys:cache:online:list";
String ONLINE_FORM = "sys:cache:online:form";
String ONLINE_RP = "sys:cache:online:rp";
String ONLINE_GRAPH = "sys:cache:online:graph";
String DRAG_PAGE_CACHE = "drag:cache:page";
}
b.CacheConstant被使用的地方
a.TokenUtils.java
public static LoginUser getLoginUser(String username, CommonAPI commonApi, RedisUtil redisUtil) {
LoginUser loginUser = null;
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
//【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
if (redisUtil.hasKey(loginUserKey)) {
try {
loginUser = (LoginUser) redisUtil.get(loginUserKey);
//解密用户
SensitiveInfoUtil.handlerObject(loginUser, false);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} else {
// 查询用户信息
loginUser = commonApi.getUserByName(username);
}
return loginUser;
}
b.LoginController.java
@RequestMapping(value = "/logout")
public Result<Object> logout(HttpServletRequest request, HttpServletResponse response) {
//用户退出逻辑
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
if (oConvertUtils.isEmpty(token)) {
return Result.error("退出登录失败!");
}
String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username);
if (sysUser != null) {
baseCommonService.addLog("用户名: " + sysUser.getRealname() + ",退出成功!", CommonConstant.LOG_TYPE_1, null, sysUser);
log.info(" 用户名: " + sysUser.getRealname() + ",退出成功! ");
//清空用户登录Token缓存
redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
//清空用户登录Shiro权限缓存
redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId());
//清空用户的缓存信息(包括部门信息),例如sys:cache:user::<username>
redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername()));
//调用shiro的logout
SecurityUtils.getSubject().logout();
return Result.ok("退出登录成功!");
} else {
return Result.error("Token无效!");
}
}
c.SysUserOnlineController.java
@RequestMapping(value = "/forceLogout", method = RequestMethod.POST)
public Result<Object> forceLogout(@RequestBody SysUserOnlineVO online) {
//用户退出逻辑
if (oConvertUtils.isEmpty(online.getToken())) {
return Result.error("退出登录失败!");
}
String username = JwtUtil.getUsername(online.getToken());
LoginUser sysUser = sysBaseApi.getUserByName(username);
if (sysUser != null) {
baseCommonService.addLog("强制: " + sysUser.getRealname() + "退出成功!", CommonConstant.LOG_TYPE_1, null, sysUser);
log.info(" 强制 " + sysUser.getRealname() + "退出成功! ");
//清空用户登录Token缓存
redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + online.getToken());
//清空用户登录Shiro权限缓存
redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId());
//清空用户的缓存信息(包括部门信息),例如sys:cache:user::<username>
redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername()));
//调用shiro的logout
SecurityUtils.getSubject().logout();
return Result.ok("退出登录成功!");
} else {
return Result.error("Token无效!");
}
}
d.SysUserServiceImpl.java
@Override
@CacheEvict(value = {CacheConstant.SYS_USERS_CACHE}, allEntries = true)
public Result<?> resetPassword(String username, String oldpassword, String newpassword, String confirmpassword) {
SysUser user = userMapper.getUserByName(username);
String passwordEncode = PasswordUtil.encrypt(username, oldpassword, user.getSalt());
if (!user.getPassword().equals(passwordEncode)) {
return Result.error("旧密码输入错误!");
}
if (oConvertUtils.isEmpty(newpassword)) {
return Result.error("新密码不允许为空!");
}
if (!newpassword.equals(confirmpassword)) {
return Result.error("两次输入密码不一致!");
}
String password = PasswordUtil.encrypt(username, newpassword, user.getSalt());
this.userMapper.update(new SysUser().setPassword(password), new LambdaQueryWrapper<SysUser>().eq(SysUser::getId, user.getId()));
return Result.ok("密码重置成功!");
}
02.代码生成器,跳过token验证
a.功能
a.需求
自动登录,token任意值
b.当前分析
先不要生成代码,先理解以前你修改的逻辑。
当前版本,token仍需要验证,我希望可以不需要token验证。
先不要生成代码,先告诉我你的思路
-------------------------------------------------------------------------------------------------
让我分析一下当前的认证逻辑和如何实现不需要token验证的思路:
当前认证逻辑分析:
系统使用了Shiro作为权限认证框架
通过@RequiresRoles和@RequiresPermissions注解进行权限控制
Token验证主要在ShiroConfig和ShiroRealm中实现
用户请求需要携带token,系统会验证token的有效性
实现免token验证的思路:
a) 主要修改方向:
修改Shiro配置,设置部分URL无需认证即可访问
在ShiroConfig中的shiroFilterFactoryBean方法中添加匿名访问路径
可以针对特定接口或者所有接口进行放行
b) 具体实现思路:
在ShiroConfig中添加匿名访问配置:
将需要开放的URL添加到filterChainDefinitionMap.put("/your/api/**", "anon")
"anon"表示无需认证即可访问
c) 可以完全禁用Shiro的认证检查
在controller层去掉@RequiresRoles和@RequiresPermissions注解
将所有接口设置为匿名访问
-------------------------------------------------------------------------------------------------
安全考虑:
需要评估哪些接口确实需要开放免认证访问
可以保留部分关键接口的认证要求
建议添加其他安全措施(如IP限制、请求频率限制等)
b.当前方案1【废弃】
a.思路
a.JwtFilter
在 ShiroConfig.java 中,您将 JwtFilter 添加到过滤器链中,并将其命名为 jwt。
这意味着在请求处理过程中,JwtFilter 会在请求到达其他处理逻辑之前执行。
您需要检查 JwtFilter 的实现,确认它是否在请求中验证 token。
如果 JwtFilter 中的逻辑在放行规则之前执行,并且 token 无效或缺失,则会导致请求被拒绝。
b.TokenUtils
在 TokenUtils.java 中,verifyToken 方法负责验证 token 的有效性。
这个方法会检查 token 是否为空、是否有效、用户是否存在等。
如果在请求处理过程中调用了这个方法,并且 token 验证失败,则会抛出异常,导致请求被拒绝。
c.ShiroRealm
在 ShiroRealm.java 中,doGetAuthenticationInfo 方法会在身份认证时调用 checkUserTokenIsEffect 方法,
该方法也会验证 token 的有效性。如果 token 无效,抛出 AuthenticationException,这将导致请求被拒绝。
d.JwtUtil
Jwt生成方法
e.测试
直接访问 http://127.0.0.1:9994/piscesboot/sys/user/list
直接访问 http://127.0.0.1:3100/piscesboot/sys/user/list
b.JwtFilter
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// try {
// executeLogin(request, response);
// return true;
// } catch (Exception e) {
// JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
// return false;
// }
return true;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
//
// if (oConvertUtils.isEmpty(token)) {
// token = httpServletRequest.getParameter("token");
// }
//
// JwtToken jwtToken = new JwtToken(token);
// // 提交给realm进行登入,如果错误他会抛出异常并被捕获
// getSubject(request, response).login(jwtToken);
// // 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// String token = TokenUtils.getTokenByRequest((HttpServletRequest) request);
// if (StringUtils.isBlank(token) || !TokenUtils.verifyToken(token, commonApi)) {
// // 处理 token 验证失败的逻辑
// return false; // 拒绝访问
// }
// 允许访问
return true; // 直接放行
}
c.TokenUtils
/**
* 验证Token
*/
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi) {
// log.debug(" -- url --" + request.getRequestURL());
// String token = getTokenByRequest(request);
// return verifyToken(token, commonApi);
return true; // 直接返回 true,表示验证通过
}
d.ShiroRealm
/**
* 用户信息认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
// log.debug("===============Shiro身份认证开始============");
// String token = (String) auth.getCredentials();
// if (token == null) {
// HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
// log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
// throw new AuthenticationException("token为空!");
// }
// // 校验token有效性
// LoginUser loginUser = this.checkUserTokenIsEffect(token);
// return new SimpleAuthenticationInfo(loginUser, token, getName());
// String token = (String) auth.getCredentials();
// 直接返回一个默认的用户信息
LoginUser loginUser = new LoginUser(); // 创建一个默认的用户对象
return new SimpleAuthenticationInfo(loginUser, null, getName());
}
/**
* 校验token的有效性
*/
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// // 解密获得username,用于和数据库进行对比
// String username = JwtUtil.getUsername(token);
// if (username == null) {
// throw new AuthenticationException("token非法无效!");
// }
// // 查询用户信息
// LoginUser loginUser = commonApi.getUserByName(username);
// if (loginUser == null) {
// throw new AuthenticationException("用户不存在!");
// }
// // 判断用户状态
// if (loginUser.getStatus() != 1) {
// throw new AuthenticationException("账号已被锁定,请联系管理员!");
// }
// // 校验token是否有效
// if (!JwtUtil.verify(token, username, loginUser.getPassword())) {
// throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
// }
// //校验用户的tenant_id
// String userTenantIds = loginUser.getRelTenantIds();
// if(oConvertUtils.isNotEmpty(userTenantIds)){
// String contextTenantId = TenantContext.getTenant();
// String str ="0";
// if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
// String[] arr = userTenantIds.split(",");
// if(!oConvertUtils.isIn(contextTenantId, arr)){
// throw new AuthenticationException("用户租户信息变更,请重新登陆!");
// }
// }
// }
// return loginUser;
return new LoginUser();
}
e.JwtUtil
/**
* 根据request中的token获取用户账号
*
* @param request
* @return
* @throws JeecgBootException
*/
public static String getUserNameByToken(HttpServletRequest request) throws JeecgBootException {
String accessToken = request.getHeader("X-Access-Token");
accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzY2Njc5MDMsInVzZXJuYW1lIjoiYWRtaW4ifQ.xSH-RBDsfmo_GMlNRw-416RHJVx-bJQ7CkYBrx09x3o";
String username = getUsername(accessToken);
if (oConvertUtils.isEmpty(username)) {
throw new JeecgBootException("未获取到用户");
}
return username;
}
c.当前方案2【生效,仅修改LoginController.java 调用 JwtUtil.java】
a.请求
http://127.0.0.1:9994/pisces-boot/online/cgform/head/list?column=createTime&order=desc&pageNo=1&pageSize=10©Type=0&_t=1736672071399
http://127.0.0.1:3100/piscesboot/online/cgform/head/list?column=createTime&order=desc&pageNo=1&pageSize=10©Type=0&_t=1736672071399
b.路径
PermissionDataAspect:arround方法
JwtUtil:getUserNameByToken,获取request.getHeader("X-Access-Token");
c.注意,JwtUtil.java两个生成token的方法
// code,【不存在】时间限制
public static String sign(String username, String secret) {
return JWT.create().withClaim("username", username).sign(Algorithm.HMAC256(secret));
}
// jeecg,【存在】时间限制
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
d.注意,JwtUtil.java两个验证token的方法
// 验证,【不存在】时间限制
public static boolean verifyWithoutExpired(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.ignoreIssuedAt()
.build();
verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
// 验证,【存在】时间限制
public static boolean verify(String token, String username, String secret) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
e.目前修改点
a.LoginController.java 调用 JwtUtil.java【生效】
String token = JwtUtil.sign(username, syspassword);
变成
String token = JwtUtil.signWithoutExpired(username, syspassword);
---------------------------------------------------------------------------------------------
永不过期
b.ShiroConfig.java【废弃】
filterChainDefinitionMap.put("/online/**", "anon");
---------------------------------------------------------------------------------------------
允许匿名访问
c.代码生成器,最简单的失效方法【废弃】
PermissionDataAspect.java
@Pointcut("@annotation(org.jeecg.common.aspect.annotation.PermissionData2)")
public void pointCut() {
---------------------------------------------------------------------------------------------
从PermissionData变成PermissionData2
f.Main函数
public class CodeGenerate {
public static void main(String[] args) {
// 生成token,【存在】时间限制
String token = JwtUtil.signWithoutExpired("admin", "123456");
System.out.println(token);
// 生成token,【存在】时间限制
String token2 = JwtUtil.sign("admin", "123456");
System.out.println(token2);
// 获得token中的信息
// String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzY2Nzg4MTYsInVzZXJuYW1lIjoiYWRtaW4ifQ.r45sieaBn6dsTOv7GnWI8cV5vALQTOf3uwY0SpAhsFM";
// System.out.println(JwtUtil.getUsername(token));
// 校验token是否正确
// boolean vflag = JwtUtil.verifyWithoutExpired(token, "admin", "123456");
// System.out.println(vflag);
}
}
-------------------------------------------------------------------------------------------------
20250112,pisces的Token(存在时间限制),可以去 code的Token(不存在时间限制),完成验证
token请求头位置 X-Access-Token
pisces的Token(存在时间限制) eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.XyBp2NDHlDBIO-CwWrvLY838WcpUvXrmrcZKS1B-Z28
code的Token(不存在时间限制) eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzY3MzgyOTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.vY85tTCCT_F9tL9BEYtZLLvYYxhTjGNyMpQ7JJ5qKAI
03.代码生成器,嵌入到系统中,完整功能
a.积木报表
菜单类型:子菜单
菜单名称:报表设计器
上级菜单:低代码开发
访问路径:/jmreport
前端组件:{{ window._CONFIG['domianURL'] }}/jmreport/list?token=${token}
组件名称:jmreport
菜单图标:无
排序:3
是否路由菜单:是
隐藏路由:否
隐藏Tab:否
是否缓存路由:否
聚合路由:否
打开方式:否
b.代码生成
菜单名称 组件 路径
Online表单开发 super/online/cgform/index /online/cgform
Online报表配置 super/online/cgreport/index /online/cgreport
AUTO在线报表 super/online/cgreport/auto/OnlCgReportList /online/cgreport/:id
报表设计器 {{ window._CONFIG['domianURL'] }}/jmreport/list?token=${token} /jmreport
AUTO在线表单 super/online/cgform/auto/default/OnlineAutoList /online/cgformList/:id
AUTO树表单列表 super/online/cgform/auto/tree/OnlineAutoTreeList /online/cgformTreeList/:id
系统编码规则 system/fillRule/index /system/fillrule
系统校验规则 system/checkRule/index /system/checkrule
Online表单视图 super/online/cgform/CgformCopyList /online/copyform/:code
AUTO在线图表 super/online/graphreport/auto/GraphreportAutoChart /online/graphreport/chart/:code
c.当前思路
a.问题分析
当前服务(9994端口)收到了一些以 /online/ 开头的请求
这些请求需要转发到另一个服务(9984端口)处理
目前这些请求返回404,说明当前服务无法处理这些请求
b.使用 Spring Cloud Gateway(推荐)
如果是微服务架构,可以使用 Spring Cloud Gateway 作为网关
在网关层配置路由规则,将 /online/** 的请求转发到9984端口
c.使用 Nginx 反向代理
在 Nginx 配置中添加路由规则
将 /online/ 开头的请求转发到9984端口
d.使用 Spring MVC 的转发机制
在当前服务中实现一个转发控制器
使用 RestTemplate 或 WebClient 将请求转发到目标服务
e.建议实现方案
考虑到当前情况,我建议使用第三种方案(Spring MVC转发),因为:
实现简单,无需额外部署网关服务
可以在应用层面灵活控制转发逻辑
便于调试和维护
f.需要注意的点
需要处理请求参数的传递
需要处理请求头的传递
需要正确处理响应状态码和响应体
需要考虑超时和异常处理
需要考虑跨域问题
d.方案1【废弃】:使用SpringMVC转发机制
a.阶段1
a.使用 Spring MVC 的转发机制
在当前服务中实现一个转发控制器
使用 RestTemplate 或 WebClient 将请求转发到目标服务
b.配置类准备
- 创建一个配置类(如 `ForwardConfig`)
- 配置目标服务的基础URL(9984端口)
- 注册 RestTemplate Bean,配置连接池等参数
- 可以通过配置文件(application.yml)来管理转发的目标地址
c.转发控制器设计
- 创建专门的转发控制器(如 `OnlineForwardController`)
- 使用 `@RequestMapping("/online/**")` 拦截所有 /online 开头的请求
- 控制器方法需要能处理所有 HTTP 方法(GET/POST/PUT/DELETE等)
- 保留原始请求的所有信息(请求头、参数、body等)
d.请求转发处理流程
a) 请求信息提取
- 获取原始请求路径
- 获取请求方法(GET/POST等)
- 获取请求头信息
- 获取请求参数
- 获取请求体数据
b) 请求转换
- 构建新的请求URL(替换为9984端口)
- 复制原始请求头
- 复制请求参数
- 复制请求体
c) 发送请求
- 使用 RestTemplate 发送请求到目标服务
- 处理不同的 HTTP 方法
- 设置适当的超时时间
d) 响应处理
- 接收目标服务的响应
- 复制响应状态码
- 复制响应头
- 复制响应体
e.错误处理
- 实现全局异常处理
- 处理连接超时异常
- 处理目标服务不可用的情况
- 返回合适的错误响应
f.性能优化考虑
- 使用连接池管理 RestTemplate 连接
- 配置适当的超时时间
- 可以考虑添加简单的缓存机制
- 添加请求日志记录
g.安全性考虑
- 保留原始请求的认证信息
- 转发时保持会话状态
- 处理跨域问题(CORS)
- 可以添加请求来源验证
b.阶段2
a.提问
这种方式需要 写到每个接口的 注解上面吗
不需要为每个接口单独写注解,我们可以使用通配符来一次性处理所有 /online/** 的请求。
b.URL模式匹配
@RequestMapping("/online/**") // 一个注解就能匹配所有/online开头的请求
c.动态处理方法
- 可以使用一个方法处理所有类型的请求
- 使用 `@RequestMapping` 配合 `method = RequestMethod.ALL`
- 或者直接使用 `@RequestMapping` 不指定method属性
d.示例控制器结构
@RestController
public class OnlineForwardController {
@RequestMapping("/online/**")
public ResponseEntity<Object> forwardRequest(HttpServletRequest request) {
// 一个方法处理所有/online/**的请求
// ...转发逻辑
}
}
e.路径提取
- 可以通过 request.getRequestURI() 获取完整请求路径
- 保持原始路径中 /online/ 后面的所有部分
- 自动拼接到新的目标URL
c.阶段3
a.RestTemplateConfig【使用】
package cn.myslayers.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
* 优雅的http请求方式RestTemplate
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory); // 使用默认的 Jackson 配置
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);
factory.setConnectTimeout(15000);
return factory;
}
}
---------------------------------------------------------------------------------------------
RestTemplate 默认使用的是 Jackson 来解析 JSON,
b.RestTemplateConfig【废弃】
package cn.myslayers.config;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
* 优雅的http请求方式RestTemplate
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
// 使用 FastJson 消息转换器
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
// 设置支持的MediaType
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
// 添加fastjson转换器
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
messageConverters.add(fastConverter);
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
//ms毫秒
factory.setReadTimeout(5000);
//ms毫秒
factory.setConnectTimeout(15000);
return factory;
}
}
---------------------------------------------------------------------------------------------
项目中使用的是 FastJSON 消息转换器,但是目标服务器默认为Spring的Jackson来解析json
c.SysOnlieController【使用】
package cn.myslayers.system.modules.system.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* 在线表单代理转发控制器
* 用于将在线表单的请求转发到实际的处理服务器
*/
@Slf4j
@RestController
public class SysOnlieController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private ObjectMapper objectMapper;
private String targetUrl = "http://localhost:9984";
/**
* 转发所有 /online/** 请求到目标服务器
* @param request HTTP请求
* @return 目标服务器的响应
* @throws IOException IO异常
*/
@RequestMapping("/online/**")
public Object forwardRequest(HttpServletRequest request) throws IOException {
String method = request.getMethod();
String forwardPath = request.getRequestURI();
try {
// 1. 获取并解析查询参数
Map<String, String> queryParams = parseQueryParams(request);
// 2. 构建 URI
URI uri = buildTargetUri(forwardPath, queryParams);
// 3. 处理请求头和请求体
HttpHeaders headers = copyRequestHeaders(request);
Object requestBody = null;
if (isRequestWithBody(method)) {
requestBody = readRequestBody(request);
}
// 4. 发送请求并处理响应
HttpEntity<?> httpEntity = new HttpEntity<>(requestBody, headers);
// 记录请求信息
log.debug("Forwarding request to: {}, method: {}, headers: {}, body: {}",
uri, method, headers, requestBody);
return handleResponse(uri, HttpMethod.valueOf(method), httpEntity);
} catch (Exception e) {
log.error("Forward failed to: {}, query: {}", forwardPath, request.getQueryString(), e);
throw new RuntimeException("转发请求失败: " + e.getMessage());
}
}
/**
* 解析请求的查询参数
*/
private Map<String, String> parseQueryParams(HttpServletRequest request) throws IOException {
String queryString = request.getQueryString();
Map<String, String> queryParams = new HashMap<>();
if (queryString != null) {
queryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8.name());
String[] pairs = queryString.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = pair.substring(0, idx);
String value = pair.substring(idx + 1);
queryParams.put(key, value);
}
}
// 特殊处理SQL参数的编码
if (queryParams.containsKey("sql")) {
String sql = queryParams.get("sql");
queryParams.put("sql", URLEncoder.encode(sql, StandardCharsets.UTF_8.name()));
}
}
return queryParams;
}
/**
* 构建目标URI
*/
private URI buildTargetUri(String forwardPath, Map<String, String> queryParams) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(targetUrl).path(forwardPath);
queryParams.forEach(builder::queryParam);
return builder.build().toUri();
}
/**
* 复制请求头
*/
private HttpHeaders copyRequestHeaders(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
// 只复制必要的请求头
if (isHeaderNeeded(headerName)) {
headers.add(headerName, headerValue);
}
}
}
return headers;
}
/**
* 判断请求头是否需要转发
*/
private boolean isHeaderNeeded(String headerName) {
Set<String> neededHeaders = new HashSet<>(Arrays.asList(
"content-type",
"authorization",
"x-access-token",
"tenant-id",
"x-version",
"x-timestamp",
"x-sign"
));
return neededHeaders.contains(headerName.toLowerCase());
}
/**
* 读取请求体
*/
private String readRequestBody(HttpServletRequest request) throws IOException {
String contentType = request.getContentType();
if (contentType != null && contentType.contains("application/json")) {
// 读取原始 JSON 字符串
return StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
}
return null;
}
/**
* 判断是否是包含请求体的请求方法
*/
private boolean isRequestWithBody(String method) {
return HttpMethod.POST.matches(method) ||
HttpMethod.PUT.matches(method) ||
HttpMethod.PATCH.matches(method);
}
/**
* 处理目标服务器的响应
*/
private Object handleResponse(URI uri, HttpMethod method, HttpEntity<?> httpEntity) {
ResponseEntity<String> response = restTemplate.exchange(
uri,
method,
httpEntity,
String.class
);
String contentType = response.getHeaders().getContentType() != null
? response.getHeaders().getContentType().toString().toLowerCase()
: "";
String body = response.getBody();
try {
// 获取当前请求路径
String requestPath = uri.getPath();
// 处理代码生成接口的特殊响应
if (requestPath.contains("/online/cgform/api/codeGenerate")) {
try {
if (body != null) {
return objectMapper.readValue(body, Object.class);
}
} catch (Exception e) {
log.error("Parse code generate response failed", e);
}
return body;
}
// 处理常规响应
if (contentType.contains("application/json")) {
return objectMapper.readValue(body, Object.class);
} else {
return body;
}
} catch (Exception e) {
log.error("Response parsing failed for URI: {}", uri, e);
return body;
}
}
}
---------------------------------------------------------------------------------------------
这种方案有缺陷,部分请求能请求到,但是返回处理有问题。
比如http://127.0.0.1:3100/piscesboot/online/cgform/api/codeGenerate,生成代码有null空指针
d.阶段4
a.请求
http://127.0.0.1:3100/piscesboot/online/cgreport/head/parseSql?sql=select * from pisces_order_customer&_t=1736738845799
curl "http://127.0.0.1:3100/piscesboot/online/cgform/api/codeGenerate" --compressed -X POST -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0" -H "Accept: application/json, text/plain, */*" -H "Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" -H "Accept-Encoding: gzip, deflate, br, zstd" -H "Content-Type: application/json;charset=UTF-8" -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzY3MzgyOTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.vY85tTCCT_F9tL9BEYtZLLvYYxhTjGNyMpQ7JJ5qKAI" -H "X-Access-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzY3MzgyOTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.vY85tTCCT_F9tL9BEYtZLLvYYxhTjGNyMpQ7JJ5qKAI" -H "X-TIMESTAMP: 1736739235779" -H "X-Sign: E19D6243CB1945AB4F7202A1B00F77D5" -H "tenant-id: 0" -H "X-Version: v3" -H "Origin: http://127.0.0.1:3100" -H "Connection: keep-alive" -H "Referer: http://127.0.0.1:3100/online/cgform" -H "Sec-Fetch-Dest: empty" -H "Sec-Fetch-Mode: cors" -H "Sec-Fetch-Site: same-origin" -H "Priority: u=0" --data-raw "{""projectPath"":""D:\\\\workspace\\\\"",""jspMode"":""one"",""ftlDescription"":""pisces_order_ticket"",""jformType"":""1"",""tableName_tmp"":""pisces_order_ticket"",""entityName"":""PiscesOrderTicket"",""entityPackage"":""sss"",""packageStyle"":""service"",""vueStyle"":""vue3"",""codeTypes"":""controller,service,dao,mapper,entity,vue"",""code"":""2c9a6081944e11f501944e11f92c0003"",""tableName"":""pisces_order_ticket""}"
b.报错
AbstractQueryBlackListHandler.java
public boolean isPass(String sql) {
List<QueryTable> list = null;
try {
list = this.getQueryTableInfo(sql.toLowerCase());
} catch (Exception e) {
log.warn("校验sql语句,解析报错:{}",e.getMessage());
}
c.分析
错误的核心在于:
Caused by: net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: "%" "%" at line 1, column 7.
原始 SQL 被编码成了:select%20*%20from%20pisces_order_main
而不是正常的:select * from pisces_order_main
---------------------------------------------------------------------------------------------
问题原因:
SQL 语句中的空格被 URL 编码成了 %20
这个 URL 编码的 SQL 直接传递给了 SQL 解析器
JSqlParser 无法正确解析包含 URL 编码的 SQL 语句
d.解决
1.在转发请求之前,需要对 SQL 进行 URL 解码
2.或者在目标服务端(接收请求的一方)处理 SQL 之前进行解码
---------------------------------------------------------------------------------------------
方案1:在构建 URI 之前,手动对 SQL 查询字符串进行 URL 编码
方案2:将 SQL 语句用 Base64 编码后传递,接收端再解码
方案3:修改请求方式,将 SQL 放在请求体中而不是查询参数中
e.20250113
放弃该方案
这种方案有缺陷,部分请求能请求到,但是返回处理有问题。
比如http://127.0.0.1:3100/piscesboot/online/cgform/api/codeGenerate,生成代码有null空指针
e.方案2【生效】:使用Vite代理
a.阶段1
a.提问
当前方案是
使用 Spring MVC 的转发机制
在当前服务中实现一个转发控制器
使用 RestTemplate 或 WebClient 将请求转发到目标服务
---------------------------------------------------------------------------------------------
但是这种方案有缺陷,部分请求能请求到,但是返回处理有问题。
不要动代码进行操作,我已经手动恢复。
当前方案使用
在 Nginx 配置中添加路由规则
将 /online/ 开头的请求转发到9984端口
---------------------------------------------------------------------------------------------
但是略有不同,目前是 vite驱动的前端。请在代码层面解决这个问题。
具体项目为 @web , @.env.development 这个文件配置路由映射。
但是 该文件目前需要添加 对 /online/** 转发到9984端口。如果这里不能实现,请找到拦截器位置,对代码进行修改。先不要生成代码,先告诉我你的思路
b.回答
直接修改 VITE_PROXY 配置
- 在现有代理配置中添加新的代理规则
- 将 /online/** 请求转发到 9984 端口
- 需要考虑路径前缀问题(是否需要添加 /pisces-boot)
---------------------------------------------------------------------------------------------
路由拦截器方案
- 在前端路由层面拦截 /online 开头的请求
- 修改请求的 baseURL
- 可能需要修改 axios 配置或添加请求拦截器
b.阶段2
a.流程分析
a) 前端发起请求
- 基础URL: http://127.0.0.1:3100
- API前缀: /piscesboot
- 实际路径: /online/xxx
b) 请求匹配顺序
1. 先匹配 "/piscesboot/online" -> 转发到 9984端口
2. 其他 "/piscesboot" 请求 -> 转发到 9994端口
b.官方文档
server.proxy
类型: Record<string, string | ProxyOptions>
为开发服务器配置自定义代理规则。期望接收一个 { key: options } 对象。
任何请求路径以 key 值开头的请求将被代理到对应的目标。
如果 key 值以 ^ 开头,将被识别为 RegExp。
configure 选项可用于访问 proxy 实例。如果请求匹配任何配置的代理规则,该请求将不会被 Vite 转换。
---------------------------------------------------------------------------------------------
请注意,如果使用了非相对的 基础路径 base,则必须在每个 key 值前加上该 base。
继承自 http-proxy。完整选项详见 此处。
---------------------------------------------------------------------------------------------
在某些情况下,你可能也想要配置底层的开发服务器。
(例如添加自定义的中间件到内部的 connect 应用中)为了实现这一点,你需要编写你自己的 插件 并使用 configureServer 函数。
---------------------------------------------------------------------------------------------
示例:
export default defineConfig({
server: {
proxy: {
// 字符串简写写法:
// http://localhost:5173/foo
// -> http://localhost:4567/foo
'/foo': 'http://localhost:4567',
// 带选项写法:
// http://localhost:5173/api/bar
// -> http://jsonplaceholder.typicode.com/bar
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
// 正则表达式写法:
// http://localhost:5173/fallback/
// -> http://jsonplaceholder.typicode.com/
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, ''),
},
// 使用 proxy 实例
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
configure: (proxy, options) => {
// proxy 是 'http-proxy' 的实例
}
},
// 代理 websockets 或 socket.io 写法:
// ws://localhost:5173/socket.io
// -> ws://localhost:5174/socket.io
// 在使用 `rewriteWsOrigin` 时要特别谨慎,因为这可能会让
// 代理服务器暴露在 CSRF 攻击之下
'/socket.io': {
target: 'ws://localhost:5174',
ws: true,
rewriteWsOrigin: true,
},
},
},
})
c.涉及文件
.env.development
# 是否打开mock
VITE_USE_MOCK = true
# 发布路径
VITE_PUBLIC_PATH = /
# 跨域代理,您可以配置多个 ,请注意,没有换行符
#VITE_PROXY = [["/piscesboot","http://localhost:9994/pisces-boot"],["/upload","http://localhost:3300/upload"]]
# 控制台不输出
VITE_DROP_CONSOLE = false
.env.development
#后台接口父地址(必填)
VITE_GLOB_API_URL=/piscesboot
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://localhost:9994/pisces-boot
# 接口前缀
VITE_GLOB_API_URL_PREFIX=
#微前端qiankun应用,命名必须以VITE_APP_SUB_开头,pisces-app-1为子应用的项目名称,也是子应用的路由父路径
VITE_APP_SUB_jeecg-app-1 = 'http://localhost:8092'
---------------------------------------------------------------------------------------------
build\utils.ts
export function wrapperEnv(envConf: Recordable): ViteEnv {
const ret: any = {};
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, '\n');
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
if (envName === 'VITE_PORT') {
realName = Number(realName);
}
if (envName === 'VITE_PROXY' && realName) {
try {
realName = JSON.parse(realName.replace(/'/g, '"'));
} catch (error) {
realName = '';
}
}
ret[envName] = realName;
if (typeof realName === 'string') {
process.env[envName] = realName;
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName);
}
}
return ret;
}
---------------------------------------------------------------------------------------------
proxy.ts
export function createProxy(list: ProxyList = []) {
const ret: ProxyTargetList = {};
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target);
// https://github.com/http-party/node-http-proxy#options
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
// https is require secure=false
...(isHttps ? { secure: false } : {}),
};
}
return ret;
}
---------------------------------------------------------------------------------------------
vite.config.ts
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_DROP_CONSOLE } = viteEnv;
proxy: createProxy(VITE_PROXY),
d.配置.env.development【废弃】
http://127.0.0.1:3100/piscesboot/online/cgreport/head/parseSql?sql=select * from pisces_order_customer&_t=1736738845799
curl "http://127.0.0.1:3100/piscesboot/online/cgform/api/codeGenerate" --compressed -X POST -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0" -H "Accept: application/json, text/plain, */*" -H "Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" -H "Accept-Encoding: gzip, deflate, br, zstd" -H "Content-Type: application/json;charset=UTF-8" -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzY3MzgyOTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.vY85tTCCT_F9tL9BEYtZLLvYYxhTjGNyMpQ7JJ5qKAI" -H "X-Access-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzY3MzgyOTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.vY85tTCCT_F9tL9BEYtZLLvYYxhTjGNyMpQ7JJ5qKAI" -H "X-TIMESTAMP: 1736739235779" -H "X-Sign: E19D6243CB1945AB4F7202A1B00F77D5" -H "tenant-id: 0" -H "X-Version: v3" -H "Origin: http://127.0.0.1:3100" -H "Connection: keep-alive" -H "Referer: http://127.0.0.1:3100/online/cgform" -H "Sec-Fetch-Dest: empty" -H "Sec-Fetch-Mode: cors" -H "Sec-Fetch-Site: same-origin" -H "Priority: u=0" --data-raw "{""projectPath"":""D:\\\\workspace\\\\"",""jspMode"":""one"",""ftlDescription"":""pisces_order_ticket"",""jformType"":""1"",""tableName_tmp"":""pisces_order_ticket"",""entityName"":""PiscesOrderTicket"",""entityPackage"":""sss"",""packageStyle"":""service"",""vueStyle"":""vue3"",""codeTypes"":""controller,service,dao,mapper,entity,vue"",""code"":""2c9a6081944e11f501944e11f92c0003"",""tableName"":""pisces_order_ticket""}"
---------------------------------------------------------------------------------------------
VITE_PROXY = [["/piscesboot","http://localhost:9994/pisces-boot"],["/upload","http://localhost:3300/upload"]]
变成
VITE_PROXY = [["/piscesboot/online/**","http://localhost:9984/pisces-boot"],["/piscesboot","http://localhost:9994/pisces-boot"],["/upload","http://localhost:3300/upload"]]
变成
VITE_PROXY = [
["/piscesboot/online", {
target: "http://localhost:9984/pisces-boot",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/piscesboot\/online/, '/online')
}],
["/piscesboot", {
target: "http://localhost:9994/pisces-boot",
changeOrigin: true
}],
["/upload","http://localhost:3300/upload"]
]
变成
VITE_PROXY = [
["/piscesboot", {
target: "http://localhost:9994/pisces-boot", // 默认转发到9994
changeOrigin: true,
rewrite: (path) => path.replace(/^\/piscesboot/, '')
}],
["/piscesboot/online", {
target: "http://localhost:9984/pisces-boot", // online相关转发到9984
changeOrigin: true,
rewrite: (path) => path.replace(/^\/piscesboot/, '')
}],
["/upload","http://localhost:3300/upload"]
]
变成
VITE_PROXY = [["/piscesboot/online", {target: "http://localhost:9984/pisces-boot",changeOrigin: true,rewrite: (path) => path.replace(/^\/piscesboot/, '')}],["/piscesboot", {target: "http://localhost:9994/pisces-boot",changeOrigin: true,rewrite: (path) => path.replace(/^\/piscesboot/, '')}],["/upload","http://localhost:3300/upload"]]
变成
VITE_PROXY=[["/piscesboot/online","http://localhost:9984/pisces-boot"],["/piscesboot","http://localhost:9994/pisces-boot"],["/upload","http://localhost:3300/upload"]]
---------------------------------------------------------------------------------------------
代理规则优先级
- 需要确保 /piscesboot/online/* 的规则优先于 /piscesboot/*
- 更具体的路径应该有更高的优先级
---------------------------------------------------------------------------------------------
废弃原因,因为 proxy.ts 的 createProxy 解析 .env.development 的 VITE_PROXY 有问题。
故而,直接在vite.config.ts配置 proxy,而不是通过 createProxy 函数转换。
e.配置vite.config.ts【生效】
proxy: createProxy(VITE_PROXY),
变成
server: {
// Listening on all local IPs
host: true,
https: false,
port: VITE_PORT,
// Load proxy configuration from .env
proxy: {
// online 相关请求转发到 9984 端口
'^/piscesboot/online': {
target: 'http://localhost:9984/pisces-boot',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/piscesboot/, ''),
},
// 其他 piscesboot 请求转发到 9994 端口
'/piscesboot': {
target: 'http://localhost:9994/pisces-boot',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/piscesboot/, ''),
},
// 上传请求转发
'/upload': {
target: 'http://localhost:3300',
changeOrigin: true,
},
},
},
04.代码生成器,嵌入到系统中,仅保存代码生成
a.分析报错
bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.context.annotation.Lazy(value=true), @javax.annotation.Resource(shareable=true, lookup=, name=, description=, authenticationType=CONTAINER, type=class java.lang.Object, mappedName=)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1799)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1355)
at org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver$1.getTarget(ContextAnnotationAutowireCandidateResolver.java:95)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:195)
at com.sun.proxy.$Proxy79.getUserByName(Unknown Source)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
at com.sun.proxy.$Proxy79.getUserByName(Unknown Source)
at org.jeecg.config.shiro.ShiroRealm.checkUserTokenIsEffect(ShiroRealm.java:98)
at org.jeecg.config.shiro.ShiroRealm.doGetAuthenticationInfo(ShiroRealm.java:83)
at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:571)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:273)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:275)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:260)
at org.jeecg.config.shiro.filters.JwtFilter.executeLogin(JwtFilter.java:72)
at org.jeecg.config.shiro.filters.JwtFilter.isAccessAllowed(JwtFilter.java:48)
at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162)
at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:223)
at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:198)
at org.jeecg.config.shiro.filters.JwtFilter.preHandle(JwtFilter.java:103)
at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:154)
at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
b.分析顺序
bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.context.annotation.Lazy(value=true), @javax.annotation.Resource(shareable=true, lookup=, name=, description=, authenticationType=CONTAINER, type=class java.lang.Object, mappedName=)}
at org.jeecg.config.shiro.ShiroRealm.checkUserTokenIsEffect(ShiroRealm.java:98)
at org.jeecg.config.shiro.ShiroRealm.doGetAuthenticationInfo(ShiroRealm.java:83)
at org.jeecg.config.shiro.filters.JwtFilter.executeLogin(JwtFilter.java:72)
at org.jeecg.config.shiro.filters.JwtFilter.isAccessAllowed(JwtFilter.java:48)
at org.jeecg.config.shiro.filters.JwtFilter.preHandle(JwtFilter.java:103)
c.分析说明
应该只分析org.jeecg.config自己写的东西
顺序应该是从下到上,JwtFilter.java:103 -> JwtFilter.java:48 -> JwtFilter.java:72 -> ShiroRealm.java:83 -> ShiroRealm.java:98
d.最终结果
public class SysBaseApiImpl implements ISysBaseAPI { --代码生成器,必须实现这个类
实现
public interface ISysBaseAPI extends CommonAPI {
继承
public interface CommonAPI {
05.积木报表,嵌入到系统中
a.原本方案
报表设计器
路径:/jmreport
组件:{{ window._CONFIG['domianURL'] }}/jmreport/list?token=${token}
组件:{{ window._CONFIG['reportURL'] }}/jmreport/list?token=${token}
组件:http://localhost:9974/pisces-jimu/jmreport/list
组件:http://localhost:9974/pisces-jimu/jmreport/list?token=123456
-----------------------------------------------------------------------------------------------------
积木报表例子
路径:/jmreport/view/961455b47c0b86dc961e90b5893bff05
组件:{{ window._CONFIG['domianURL'] }}/jmreport/view/961455b47c0b86dc961e90b5893bff05?token=${token}
组件:http://localhost:9974/pisces-jimu/jmreport/view/961455b47c0b86dc961e90b5893bff05?token=${token}
-----------------------------------------------------------------------------------------------------
生产销售监控大屏
路径:/test/bigScreen1
组件:{{ window._CONFIG['domianURL'] }}/test/bigScreen/templat/index1
组件:http://localhost:9974/pisces-jimu/test/bigScreen/templat/index1
-----------------------------------------------------------------------------------------------------
智慧物流监控大屏
路径:/test/bigScreen2
组件:{{ window._CONFIG['domianURL'] }}/test/bigScreen/templat/index2
组件:http://localhost:9974/pisces-jimu/test/bigScreen/templat/index2
c.当前方案
报表设计器
路径:/jmreport
组件:{{ window._CONFIG['reportURL'] }}/jmreport/list dev环境
组件:{{ window._CONFIG['reportURL'] }}/jmreport/list?token=${token} prod环境
-----------------------------------------------------------------------------------------------------
积木报表例子
路径:/jmreport/view/961455b47c0b86dc961e90b5893bff05
组件:{{ window._CONFIG['reportURL'] }}/jmreport/view/961455b47c0b86dc961e90b5893bff05 dev环境
组件:{{ window._CONFIG['reportURL'] }}/jmreport/view/961455b47c0b86dc961e90b5893bff05?token=${token} prod环境
组件:http://localhost:9974/pisces-jimu/jmreport/view/961455b47c0b86dc961e90b5893bff05?token=${token}
-----------------------------------------------------------------------------------------------------
生产销售监控大屏
路径:/test/bigScreen1
组件:{{ window._CONFIG['reportURL'] }}/test/bigScreen/templat/index1
组件:http://localhost:9974/pisces-jimu/test/bigScreen/templat/index1
-----------------------------------------------------------------------------------------------------
智慧物流监控大屏
路径:/test/bigScreen2
组件:{{ window._CONFIG['reportURL'] }}/test/bigScreen/templat/index2
组件:http://localhost:9974/pisces-jimu/test/bigScreen/templat/index2
d.vite.config.ts向window._CONFIG添加http://localhost:9974/pisces-jimu
define: {
// setting vue-i18-next
// Suppress warning
__INTLIFY_PROD_DEVTOOLS__: false,
__APP_INFO__: JSON.stringify(__APP_INFO__),
'window._CONFIG': JSON.stringify({
VITE_GLOB_REPORT_URL: 'http://localhost:9974/pisces-jimu',
}),
},
06.vite项目部署到nginx
a.打包
pnpm install --安装全部依赖
npm run build --执行打包命令
-----------------------------------------------------------------------------------------------------
# 清理依赖和缓存
rm -rf node_modules
rm -rf dist
npm cache clean --force
-----------------------------------------------------------------------------------------------------
# 重新安装依赖并构建
npm install
npm run build
b.生产环境特性
根据 .env.production 的配置:
关闭了 Mock 数据: VITE_USE_MOCK = false
开启了控制台日志移除: VITE_DROP_CONSOLE = true
启用了 gzip 压缩: VITE_BUILD_COMPRESS = 'gzip'
开启了图片压缩: VITE_USE_IMAGEMIN = true
默认不开启 PWA: VITE_USE_PWA = false
默认不开启旧浏览器兼容: VITE_LEGACY = false
c.配置nginx
nginx.exe --启动
nginx.exe -s reload --重新加载配置
nginx.exe -s stop --停止服务
-----------------------------------------------------------------------------------------------------
logs/error.log --错误日志
logs/access.log --访问日志
Get-Content logs/access.log -Wait --PowerShell查看实时日志
Get-Content logs/error.log -Wait --PowerShell查看实时日志
访问日志:logs/access.log
d.查看环境变量
在浏览器控制台(F12)直接输入:
console.log(window._CONFIG['domianURL']);
console.log(window._CONFIG['reportURL']);
-----------------------------------------------------------------------------------------------------
在 index.html 中,window._CONFIG 被初始化为空对象。
<script>window._CONFIG = {};</script>
-----------------------------------------------------------------------------------------------------
而环境变量被注入到了 window.__PRODUCTION__PISCESBOOTADMIN__CONF__ 中:
window.__PRODUCTION__PISCESBOOTADMIN__CONF__ = {
"VITE_GLOB_REPORT_URL": "http://localhost:9974/pisces-jimu"
// ...
};
-----------------------------------------------------------------------------------------------------
现在其实可以这样设置
{{ window._CONFIG['reportURL'] }}/jmreport/list?token=${token}
变成
{{ window.__PRODUCTION__PISCESBOOTADMIN__CONF__.VITE_GLOB_REPORT_URL }}/jmreport/list?token=${token}
-----------------------------------------------------------------------------------------------------
我们需要在前端初始化时将 VITE_GLOB_REPORT_URL 赋值给 window._CONFIG['reportURL']。
修改 Vite 的构建配置,确保 define 在生产环境也生效
define: {
// setting vue-i18-next
// Suppress warning
__INTLIFY_PROD_DEVTOOLS__: false,
__APP_INFO__: JSON.stringify(__APP_INFO__),
// 确保在生产环境下也能正确注入配置
'window._CONFIG': JSON.stringify({
reportURL: env.VITE_GLOB_REPORT_URL || VITE_GLOB_REPORT_URL,
domianURL: env.VITE_GLOB_DOMAIN_URL || VITE_GLOB_DOMAIN_URL,
}),
// 注入所有环境变量
'process.env': JSON.stringify(env),
},
e.最终解决办法
20250115,在frontend的根目录,index.html直接写死
<script>
window._CONFIG = {
reportURL: 'http://localhost:9974/pisces-jimu',
domianURL: 'http://localhost:9994/pisces-boot',
};
</script>
5.3 微服务架构
00.常用配置
a.第1步:配置host
127.0.0.1 pisces-boot-redis
127.0.0.1 pisces-boot-mysql
127.0.0.1 pisces-boot-nacos
127.0.0.1 pisces-boot-gateway
127.0.0.1 pisces-boot-system
127.0.0.1 pisces-boot-sentinel
127.0.0.1 pisces-boot-xxljob
127.0.0.1 pisces-boot-rabbitmq
127.0.0.1 pisces-boot-redisson
127.0.0.1 pisces-boot-file-view
127.0.0.1 pisces-boot-elasticsearch
b.第2步:导入数据库
a.MySQL-8.0.20
导入1:pisces-cloud-parent
导入2:pisces-cloud-nacos
导入3:pisces-cloud--job
b.Nacos
导入1:pisces.yaml
导入2:pisces-dev.yaml --修改mysql、redis、rabbitmq、redisson
导入3:pisces-gateway-dev.yaml --修改redis
c.第3步:将pisces-module-system-cloud模块注册到maven中
a.操作
勾选中Maven Profile中的dev和SpringCloud
b.注意
二者需要同时勾选中,微服务启动模块出来了,说明maven profile勾选成功
d.第4步:启动后端
a.结构
pisces-module-system-cloud
├─pisces-cloud-gateway 微服务网关,9999端口
├─pisces-cloud-nacos 服务注册与发现,8848端口
├─pisces-system-cloud-start 主系统,7001端口
├─pisces-demo-cloud-start 子系统,7002端口
├─pisces-visual
├─pisces-cloud-monitor 微服务监控模块,9111端口
├─pisces-cloud-xxljob 定时任务服务端,9080端口
├─pisces-cloud-sentinel sentinel服务端,9000端口
├─pisces-cloud-test 微服务测试示例(各种例子)
├─pisces-cloud-test-more 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
├─pisces-cloud-test-rabbitmq 微服务测试示例(rabbitmq)
├─pisces-cloud-test-seata 微服务测试示例(seata分布式事务)
├─pisces-cloud-test-shardingsphere 微服务测试示例(分库分表)
b.启动【核心】:启动顺序
第1步:NacosApplication,服务注册与发现,8848端口
第2步:GatewayApplication,微服务网关,9999端口
第2步:SystemCloudApplication:主系统,7001端口
第4步:DemoCloudApplication:子系统,7002端口
c.启动【扩展】:顺序任意
扩展:XxlJobAdminApplication,定时任务服务端,9080端口
扩展:SentinelApplication:sentinel服务端,9000端口
扩展:MonitorApplication:微服务监控模块,9111端口
e.第5步:启动前端
a.下载前端依赖
pnpm install --安装
rimraf node_modules --删除node_modules
pnpm store prune --清除索引
b.开发(env.development)/生产(env.production)
#后台接口父地址(必填)
#维格数智管理系统-网关(必填)
#上传接口(必填)
VITE_GLOB_API_URL=/piscesboot
VITE_GLOB_DOMAIN_URL=http://localhost:9999
VITE_GLOB_UPLOAD_URL=http://localhost:3300
c.启动前端项目
npm run dev
f.第5步:网站登录
a.system
http://localhost:3100
admin/123456
b.nacos
http://127.0.0.1:8858/nacos
nacos
nacos
c.xxl-job-admin:定时任务服务端
http://localhost:9080/xxl-job-admin
admin
123456
d.sentinel:sentinel服务端
http://localhost:9000
sentinel
sentinel
e.monitor:微服务监控模块
http://localhost:9111/login
admin
admin
f.阿里巴巴Druid:排查慢SQL
http://localhost:3100/monitor/druid
admin
123456
01.【规划】:架构设计
a.需求
a.一个统一的管理平台
服务管理(启停、路由配置)
权限管理(模块访问控制)
监控面板(服务状态、访问统计)
b.完整的权限控制体系
模块级别的资源保护
接口级别的访问控制
动态权限配置
c.可视化的监控系统
服务健康状态
访问量监控
异常告警
d.开发方向
微服务+微前端
单体服务+单体前端
uniapp+小程序+公众号
b.架构分类
a.核心框架集成
基础框架:
- Spring Cloud Alibaba
- Nacos(注册+配置中心)
- Spring Cloud Gateway(网关)
服务调用:
- OpenFeign(HTTP调用)
- Dubbo(RPC调用)
- Ribbon(负载均衡)
安全防护:
- Sentinel(限流熔断)
- Sa-Token(认证授权)
- JustAuth(第三方登录)
b.基础工具集成
开发工具:
- Lombok(代码简化)
- Hutool(工具集)
- MapStruct(对象转换)
数据层:
- MyBatis-Plus(ORM)
- Druid(连接池+监控)
- ShardingSphere(分库分表)
文档处理:
- EasyExcel(Excel)
- Fastjson(JSON)
- knife4j(API文档)
c.分布式解决方案
分布式事务:
- Seata
分布式任务:
- XXL-Job
分布式锁:
- Redisson
消息队列:
- RocketMQ
d.监控运维
监控:
- SkyWalking(链路追踪)
- Sentinel Dashboard(实时监控)
- Spring Boot Admin(应用监控)
日志:
- SLF4J + Logback
- ELK套件(日志收集)
运维:
- Docker + K8s
- Jenkins(CI/CD)
e.扩展功能
工作流:
- Flowable(复杂流程)
- LiteFlow(轻量规则引擎)
报表:
- JimuReport(报表)
- ECharts(图表)
AI集成:
- Spring AI
f.最佳实践建议
第一阶段(基础架构):
- Spring Cloud Alibaba + Nacos
- Gateway + Sa-Token
- MyBatis-Plus + Druid
- Sentinel + SkyWalking
第二阶段(功能增强):
- Seata(分布式事务)
- XXL-Job(任务调度)
- Flowable(工作流)
- JimuReport(报表)
第三阶段(性能优化):
- ShardingSphere(分库分表)
- RocketMQ(消息队列)
- Redis + Redisson
- Docker + K8s
02.【设计】:在同一个路由下实现服务的热插拔
a.思路
a.网关和路由需求
需要一个统一的网关服务(Gateway)来处理路由转发
所有服务必须依赖网关功能
同一路由下需要支持分批启动不同的服务
b.服务调用追踪
需要实现服务调用链的追踪功能
特别是处理 A -> B -> A 这种循环调用的场景
需要能够回溯调用地址
c.统一服务调用机制
使用 AOP 注解方式实现统一调用
需要通过 id + secret 的方式进行服务认证
需要研究 Spring Cloud 的远程调用接口实现
d.配置文件管理
配置文件采用分层覆盖机制
根目录下放置基础配置(pisces-module-start 或 pisces-module-biz)
code、system 等模块有统一的配置文件
子模块可以覆盖父级配置
e.服务启动机制
支持单个服务独立启动
服务之间通过网关进行通信
在同一路由下支持分批启动不同的服务
这些需求主要围绕着微服务架构的以下几个核心点:
f.服务网关
服务注册与发现
服务调用链追踪
服务间安全认证
配置管理
服务启动管理
g.建议使用 Spring Cloud Alibaba 套件来实现这些功能
使用 Spring Cloud Gateway 作为网关
使用 Nacos 做服务注册与配置中心
使用 Sentinel 做服务保护
使用 Sleuth + Zipkin 做调用链追踪
使用 Spring Cloud Security + JWT 做认证授权
b.提问
a.提问
@pom.xml 这是主系统,启动文件为 @application-dev.yml 。端口为9994.
@pom.xml 这是Demo01子系统,启动文件为 @application.yml ,启动端口暂时设置为9991
@pom.xml 这是Demo02子系统,启动文件为 @application.yml ,启动端口暂时设置为9992
@pom.xml 这是Demo01、Demo02父依赖,直接依赖 @pom.xml 主系统。
我现在不想通过网关来实现,我希望主系统启动一次,然后开发时,可以把Demo01、Demo02子系统写的API可以随意启动。实现这一部分API可以随时修改,然后方便开发。
你先分析我给你的pom.xml、application.yml文件,然后告诉我你的完整想法
b.项目结构分析
pisces-boot-parent (主系统,端口9994)
├── pisces-module-system (系统模块)
└── pisces-module-biz (业务模块父依赖)
├── pisces-module-demo (Demo01,端口9991)
└── pisces-module-demo2 (Demo02,端口9992)
c.依赖关系
pisces-module-biz 依赖于主系统 pisces-boot-parent
pisces-module-demo 和 pisces-module-demo2 依赖于 pisces-module-biz
所有模块共享了相同的基础配置(数据库、Redis等)
d.配置文件分析
每个模块都有独立的 application.yml
配置内容基本相同,主要区别在于:spring.application.name、server.port
e.您的需求
主系统作为基础服务持续运行
Demo01和Demo02可以独立启动和开发
不使用网关,直接通过端口访问各个服务
开发时能够快速修改和测试API
f.实现建议
a.配置文件调整:
将公共配置抽取到一个基础配置文件
每个子模块只保留必要的个性化配置
添加开发环境的配置覆盖
b.依赖管理:
在子模块中只引入必要的依赖
使用依赖管理来统一版本
避免重复依赖
c.启动类设计:
每个子模块有独立的启动类
使用不同的包扫描范围
配置独立的端口和服务名
g.具体实施步骤
1) 创建公共配置文件
2) 修改子模块配置
3) 调整依赖关系
4) 配置启动类
5) 设置开发环境
c.微服务:在同一个路由下实现服务的热插拔
a.路由统一
所有服务都在 /pisces-boot 路由下
主系统(9994)和子系统(9991/9992)共享相同的context-path
不是通过不同路由来区分服务
b.服务热插拔
主系统(9994)提供基础服务,持续运行
Demo01(9991)和Demo02(9992)可以随时启动/停止
当Demo启动时,它的API自动加入到服务中
当Demo停止时,它的API自动从服务中移除
c.实现思路
/pisces-boot (统一路由)
├── /system/** (主系统API,端口9994)
├── /demo1/** (Demo01 API,端口9991)
└── /demo2/** (Demo02 API,端口9992)
d.技术方案
使用Spring Cloud的服务注册与发现
所有服务注册到同一个注册中心
通过服务名和API路径来区分不同模块
不需要网关,直接通过服务注册实现路由
e.优势
开发时可以只启动需要的模块
修改代码后只需重启对应模块
不影响其他模块的运行
便于调试和开发
d.单体架构:实现服务热插拔
a.基本架构
/pisces-boot (统一路由)
├── system (主系统,必须运行,9994)
├── demo1 (可选服务,9991)
└── demo2 (可选服务,9992)
b.实现思路
使用 Spring Boot 的内置功能
通过配置文件区分服务
使用条件注解控制Bean的加载
通过包扫描隔离不同模块
c.具体方案
主系统(9994)
- 提供核心功能和基础服务
- 配置数据库、Redis等公共资源
- 管理共享的配置和工具类
子系统(9991/9992)
- 独立的Spring Boot应用
- 共享主系统的配置
- 独立的Controller和Service
- 通过相同的context-path访问
d.关键点
所有模块使用相同的数据源配置
共享Session和Redis配置
通过包名区分不同模块的Bean
使用@ConditionalOnProperty控制模块启用
e.开发流程
主系统持续运行
开发时启动需要的子系统
修改代码后只重启对应子系统
其他服务不受影响
e.网络方案
a.主要搜索关键词
Spring Boot Hot Module Reload
Spring Boot Module Hot Swap
Spring Boot Dynamic Module Loading
Spring Boot Plugin System
Spring Boot Module System without Microservices
b.PF4J-Spring-Boot
GitHub: https://github.com/pf4j/pf4j-spring
特点:使用 PF4J 框架实现插件化架构
可以在运行时动态加载/卸载模块
c.Spring Boot Module System
GitHub: https://github.com/kekingcn/spring-boot-klock-starter
特点:通过自定义starter实现模块化
支持模块的动态加载
d.Dynamic Modules in Spring Boot
GitHub: https://github.com/tomsquest/q-java-modularity
展示了如何在Spring Boot中实现模块化架构
不依赖微服务架构
e.Spring Plugin
GitHub: https://github.com/spring-projects/spring-plugin
Spring官方的插件框架
可以实现模块的动态装载
f.再次提问
a.提问
后端这几个系统,只是端口不同,相同context-path。确定可以适配nginx的配置吗?
b.解答
当前nginx配置:
location /piscesboot {
proxy_pass http://localhost:9994/pisces-boot; # 只配置了9994端口
}
-------------------------------------------------------------------------------------------------
问题分析:
所有请求都转发到9994端口
三个系统使用相同的context-path (/pisces-boot)
仅通过不同端口区分服务(9994/9991/9992)
-------------------------------------------------------------------------------------------------
这样会导致:
nginx只能访问到9994端口的服务
9991和9992的服务无法被访问到
相同的context-path会造成冲突
-------------------------------------------------------------------------------------------------
方案一:修改nginx配置,使用不同路径
location /piscesboot/system {
proxy_pass http://localhost:9994/pisces-boot;
}
location /piscesboot/demo1 {
proxy_pass http://localhost:9991/pisces-boot;
}
location /piscesboot/demo2 {
proxy_pass http://localhost:9992/pisces-boot;
}
-------------------------------------------------------------------------------------------------
方案二:使用负载均衡
upstream backend_servers {
server localhost:9994;
server localhost:9991;
server localhost:9992;
}
location /piscesboot {
proxy_pass http://backend_servers/pisces-boot;
}
-------------------------------------------------------------------------------------------------
方案三:修改后端设计
主系统(9994)作为统一入口
Demo1和Demo2作为主系统的模块
不使用额外的端口
通过包路径和注解区分服务
c.提问
方案2、方案3,是不是不符合 单体架构:实现服务热插拔
d.解答
单体架构下的服务热插拔的核心特点:
1.主系统必须运行
2.子系统可以随时启动/停止
3.修改代码后只重启子系统
4.不影响其他系统运行
-------------------------------------------------------------------------------------------------
当前问题:
1.nginx配置指向9994端口
2.所有系统使用相同context-path
3.想要实现热插拔开发
-------------------------------------------------------------------------------------------------
我之前的方案确实有问题:
方案二(负载均衡)=> 这是微服务的思路
方案三(统一入口)=> 失去了热插拔的意义
-------------------------------------------------------------------------------------------------
是否必须使用不同端口?
如果都用9994端口,但用不同的context-path
比如:
主系统:/pisces-boot
Demo1:/pisces-boot-demo1
Demo2:/pisces-boot-demo2
-------------------------------------------------------------------------------------------------
或者调整nginx配置?
保持系统当前设计
修改nginx配置支持多端口
但这样可能违背了单体架构的初衷
g.综合分析
a.当前环境约束:
前端统一调用:/pisces-boot
Nginx统一转发:9994端口
后端多服务:
- 主系统(9994)/pisces-boot
- Demo1(9991)/pisces-boot
- Demo2(9992)/pisces-boot
b.单体架构实现热插拔的问题:
相同context-path导致服务冲突
nginx只能转发到一个端口
无法实现真正的热插拔
多个Spring Boot应用使用相同路径会产生端口占用
c.更合适的解决方案 - 轻量级网关
前端 -> Nginx -> Gateway(9994) -> 各个服务
|-> 主系统(9995)
|-> Demo1(9991)
|-> Demo2(9992)
d.轻量级网关方案的优势
只使用Gateway的路由功能
不需要服务注册与发现
不需要配置中心
通过配置文件管理路由
保持前端调用方式不变
实现真正的热插拔
e.具体实现
spring:
cloud:
gateway:
routes:
- id: main-system
uri: http://localhost:9995
predicates:
- Path=/pisces-boot/system/**
- id: demo1
uri: http://localhost:9991
predicates:
- Path=/pisces-boot/demo1/**
- id: demo2
uri: http://localhost:9992
predicates:
- Path=/pisces-boot/demo2/**
f.开发流程:
Gateway(9994)必须运行
主系统(9995)必须运行
Demo1(9991)和Demo2(9992)可以随时启停
修改代码只需重启对应服务
h.轻量级网关
a.Soul/Apache ShenYu
地址:https://github.com/apache/shenyu
特点:
Apache顶级项目
高性能微服务API网关
支持热插拔
可以不依赖注册中心
配置简单,支持动态配置
支持多种协议
b.Zuul Standalone
地址:https://github.com/Netflix/zuul
特点:
Netflix开源
可以独立使用,不需要Eureka
配置简单
性能稳定
社区成熟
c.Gravitee.io
地址:https://github.com/gravitee-io/gravitee-gateway
特点:
轻量级API网关
独立部署
支持动态路由
提供管理UI
d.Orange
地址:https://github.com/orlabs/orange
特点:
基于OpenResty
性能高
配置简单
支持动态更新
e.Spring Cloud Gateway Standalone
使用Spring Cloud Gateway但不引入其他Spring Cloud组件
只使用路由功能
通过配置文件管理路由
与Spring Boot无缝集成
03.【设计】:对不同模块的服务进行资源保护
a.思路:不依赖第三方认证系统
a.密钥体系设计
密钥结构:
- moduleId: 模块标识
- secretKey: 模块密钥
- permissions: 权限范围
b.认证方案
请求头格式:
Authorization: Bearer {moduleId}.{timestamp}.{sign}
sign = MD5(moduleId + secretKey + timestamp)
c.实现层次
A. 全局层面(适用所有模块)
- 基础认证
- 时间戳验证
- 防重放攻击
B. 模块层面(每个模块独立)
- 模块专属密钥
- 模块权限控制
- 资源访问控制
d.注解设计
// 模块认证注解
@ModuleAuth(moduleId = "demo1")
public class Demo1Controller {
// 具体接口权限
@ResourceAuth(permissions = "read")
public Result getData() {
// ...
}
}
e.配置管理
module:
auth:
# 全局配置
global:
expire-time: 60 # 签名有效期(秒)
timestamp-offset: 300 # 时间戳偏移容忍度(秒)
# 模块配置
modules:
demo1:
moduleId: "demo1"
secretKey: "xxxxxx"
permissions: ["read","write"]
demo2:
moduleId: "demo2"
secretKey: "yyyyyy"
permissions: ["read"]
f.安全考虑
密钥定期轮换机制
请求防重放
时间戳校验
日志审计
IP白名单
g.使用示例
@Configuration
public class ModuleAuthConfig {
@Bean
public AuthInterceptor authInterceptor() {
return new AuthInterceptor();
}
}
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, ...) {
// 1. 提取认证信息
// 2. 验证时间戳
// 3. 验证签名
// 4. 检查模块权限
return true;
}
}
h.异常处理
@ControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(AuthException.class)
public Result handleAuthException(AuthException e) {
return Result.fail(e.getMessage());
}
}
i.这个方案的优点:
轻量级:不依赖第三方认证系统
灵活性:可以针对不同模块设置不同的权限
安全性:通过签名和时间戳保证请求的安全性
可扩展:易于添加新的模块和权限
易维护:配置集中管理,便于修改
b.思路:其他方案
a.JWT + 模块密钥方案
请求流程:
1. 模块先获取JWT令牌(包含模块信息和权限)
2. 请求时携带令牌
3. 网关统一验证
-------------------------------------------------------------------------------------------------
优点:
- 无状态
- 自包含权限信息
- 易于扩展
b.双重认证方案
认证层次:
1. 应用级密钥(AppId + AppSecret)
2. 模块级密钥(ModuleId + ModuleSecret)
-------------------------------------------------------------------------------------------------
特点:
- 更细粒度的控制
- 可以分别控制应用和模块权限
c.动态密钥方案
密钥生成:
- 基础密钥 + 时间因子
- 定期自动更新
- 支持多个版本密钥并存
-------------------------------------------------------------------------------------------------
优势:
- 更高的安全性
- 支持密钥轮换
- 降低密钥泄露风险
d.基于OAuth2的简化方案
流程:
1. 客户端凭证模式
2. 为每个模块分配client_id和client_secret
3. 获取access_token访问资源
好处:
- 标准协议
- 成熟的实现
- 易于集成第三方
f.API Key + 访问策略方案
组成:
- API Key:模块唯一标识
- 访问策略:JSON格式的权限定义
- 时间窗口:访问有效期
-------------------------------------------------------------------------------------------------
特点:
- 配置灵活
- 易于理解和使用
- 支持细粒度控制
g.多因素认证方案
认证因素:
- 密钥
- IP白名单
- 时间窗口
- 请求频率
-------------------------------------------------------------------------------------------------
优势:
- 多重保护
- 防止滥用
- 可追踪性强
h.基于Redis的动态权限方案
实现:
- Redis存储模块权限配置
- 实时更新权限
- 支持临时权限
-------------------------------------------------------------------------------------------------
优点:
- 实时生效
- 易于管理
- 支持权限动态调整
i.分层认证方案
认证层次:
1. 网关层:基础认证
2. 服务层:模块认证
3. 接口层:操作认证
-------------------------------------------------------------------------------------------------
特点:
- 职责分明
- 灵活组合
- 易于维护
c.方案1:OAuth2方案与现有系统的集成
a.现有单体认证集成
现有系统认证 + OAuth2并行:
Client -> 请求 -> OAuth2认证层
|- 模块间调用:OAuth2 (client credentials)
|- 用户访问:原有认证 (如session/token)
实现方式:
- 使用Spring Security OAuth2
- 配置多重认证过滤器
- 根据请求类型选择认证方式
b.向微服务架构演进
OAuth2在微服务中的扩展:
1. 认证中心化
- Authorization Server:统一认证中心
- Resource Server:各个微服务
2. 认证模式扩展
- 用户认证:authorization_code/password
- 服务认证:client_credentials
- 第三方认证:implicit/authorization_code
3. 权限管理
- 统一的权限配置
- 基于角色的访问控制(RBAC)
- 细粒度的权限控制
c.架构演进路径
Phase 1: 单体 + OAuth2
├── 主系统
│ ├── 用户认证(原有)
│ └── OAuth2 Client Credentials(模块间)
└── 子模块
└── OAuth2 Resource Server
Phase 2: 微服务过渡
├── 认证服务(Authorization Server)
├── 网关服务(Gateway + Resource Server)
└── 业务服务(Resource Server)
Phase 3: 完整微服务
├── 认证中心
├── 配置中心
├── 服务发现
└── 业务服务群
d.优势
平滑过渡:可以逐步迁移
标准化:使用统一的认证协议
扩展性:支持更多认证场景
安全性:成熟的安全实现
e.注意事项
1. 性能考虑
- Token存储方式
- 缓存策略
- 验证频率
2. 安全考虑
- Token加密
- 密钥管理
- 过期策略
3. 运维考虑
- 配置管理
- 监控告警
- 日志追踪
f.具体实施建议
// 1. OAuth2配置
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) {
clients.inMemory()
.withClient("demo1")
.secret("secret1")
.scopes("read", "write")
.authorizedGrantTypes("client_credentials");
}
}
// 2. 资源服务配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) {
http.authorizeRequests()
.antMatchers("/api/**").authenticated()
.antMatchers("/user/**").permitAll();
}
}
// 3. 安全配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http.requestMatchers()
.antMatchers("/user/**")
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
g.这种方案的最大优势是:
现在可以解决模块间的认证问题
未来可以平滑过渡到微服务架构
不影响现有的用户认证逻辑
提供了标准的安全实现
d.方案2:通过yml配置暴露API
a.说明
一个基于 YAML 配置(不使用数据库)实现的权限接口设计方案说明,该方案支持4种类型(例如类似于 yunzhijia、dingding、wechat 等),
并通过类注解和方法注解实现细粒度的权限控制。假设我们的需求是将各个接口按“组”划分,例如:
A组:对应权限 a、b、c
B组:对应其他权限
C组:对应另外一些权限
D组:如果需要,可扩展为第四种类型
b.YAML配置设计
不依赖数据库,所有认证信息都在 YAML 文件中静态配置,
例如将配置信息放在 application.yml 或单独的配置文件(例如 access-config.yml)中
-------------------------------------------------------------------------------------------------
access:
strategies:
# yunzhijia 类型,对应 A组,权限 a、b、c
yunzhijia:
token: "token_yzj_1234" # 该 token 作为认证凭证
group: "A"
permissions:
- "a"
- "b"
- "c"
# dingding 类型,对应 B组
dingding:
token: "token_dd_5678"
group: "B"
permissions:
- "read"
- "write"
# wechat 类型,对应 C组
wechat:
token: "token_wc_9012"
group: "C"
permissions:
- "query"
- "update"
# 其他类型,可扩展为第四种(如其他接口系统)
other:
token: "token_other_3456"
group: "D"
permissions:
- "exec"
-------------------------------------------------------------------------------------------------
说明:
每个策略(strategy)通过 token 唯一标识,并有所属 group 及其可用的 permissions。
这里的 token 可以是生成的密钥或约定字符串,在请求过程中的携带方式(例如通过 HTTP Header)用于认证。
c.注解设计
a.类级别注解:@AccessType
用于标识当前 Controller 对应的认证组,如 A、B、C 等。
---------------------------------------------------------------------------------------------
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface AccessType {
/**
* 指定需要的认证组,比如 "A", "B", "C" 或者 "D"
*/
String value();
}
b.方法级别注解:@AccessPermission
用于标注具体方法需要的权限。
---------------------------------------------------------------------------------------------
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface AccessPermission {
/**
* 指定调用该方法需要的权限,如 "a", "b", "c", "read", "write" 等
*/
String value();
}
d.配置属性类
创建一个配置属性类(例如 AccessProperties),利用 Spring Boot 的 @ConfigurationProperties 注解读取上面的 YAML 配置,使配置信息自动加载到内存供后续使用。
-------------------------------------------------------------------------------------------------
@ConfigurationProperties(prefix = "access")
public class AccessProperties {
private Map<String, Strategy> strategies = new HashMap<>();
public static class Strategy {
private String token;
private String group;
private List<String> permissions = new ArrayList<>();
// getter setter ...
}
// getter、setter...
}
e.拦截器或 AOP 切面的设计
设计一个拦截器或 AOP 切面(例如 AuthInterceptor 或 AccessCheckAspect),在每次请求时完成以下工作:
1.提取请求的 token
从请求头(例如 Authorization 或自定义 Header 如 X-Access-Token)中获取 token。
2.从配置中查找对应的策略
根据 token 在加载的 AccessProperties 中查找对应的策略信息,获得该 token 的所属组及权限列表。
3.获取控制器和方法上的权限注解
如果 Controller 上存在 @AccessType 注解,标识该接口要求的认证组。
如果方法上存在 @AccessPermission 注解,则对应具体的权限要求。
4.验证匹配
检查配置中 token 对应的组是否与 @AccessType 指定相符。
检查配置中 token 配置的权限列表中是否包含方法上要求的权限。
5.放行或拒绝
验证通过则允许请求继续,验证失败则返回一个错误响应(例如返回 HTTP 401/403)。
f.使用示例
在 Controller 中使用注解:
@AccessType("A") // 表示当前 Controller 归属于A组(yunzhijia类型)
@RestController
@RequestMapping("/demo")
public class DemoController {
@AccessPermission("a") // 表示调用该方法需要权限 "a"
@GetMapping("/getData")
public Result getData() {
// 业务逻辑...
return Result.success();
}
@AccessPermission("b") // 另一权限示例
@PostMapping("/updateData")
public Result updateData() {
// 更新逻辑...
return Result.success();
}
}
-------------------------------------------------------------------------------------------------
当客户端请求接口时,需要在请求头中带上相应的 token(例如 "token_yzj_1234"),拦截器(或 AOP 切面)
会进行验证,只有当 token 对应的 group 为 "A",且权限数组中包含所请求的方法权限(比如 "a" 或 "b")时,
才能通过认证。
g.总结
这种基于 YAML 配置且无需数据库的权限接口方案,主要包括以下设计点:
静态配置:所有 access token、所属组及权限通过 YAML 文件定义,动态性通过配置文件管理(可与 Nacos 等配置中心结合,实现实时刷新)。
注解方式:通过 @AccessType 和 @AccessPermission 注解,将权限要求直接标注在 Controller 和接口方法上,方便实现细粒度控制。
拦截器/AOP 实现:在请求拦截时解析 token 并与注解信息进行校验,保证每个请求只能由符合权限的 token 访问。
这种方案兼顾轻量、安全和配置集中管理,能够满足类似于 yunzhijia、dingding、wechat 类型的认证需求,同时还可以根据实际业务对不同组(A组:a、b、c;B组;C组等)进行模块化的权限管理。
04.【实现】服务注册与发现:pisces-cloud-nacos
a.主启动类,必须扫描com.alibaba.nacos
@EnableScheduling
@ServletComponentScan
@SpringBootApplication(scanBasePackages = {"cn.myslayers.nacos", "com.alibaba.nacos"})
public class NacosApplication {
/** 是否单机模式启动 */
private static String standalone = "true";
/** 是否开启鉴权 */
private static String enabled = "false";
public static void main(String[] args) {
System.setProperty("nacos.standalone", standalone);
System.setProperty("nacos.core.auth.enabled", enabled);
System.setProperty("server.tomcat.basedir","logs");
//自定义启动端口号
System.setProperty("server.port","8858");
SpringApplication.run(NacosApplication.class, args);
}
/**
* 默认跳转首页
*
* @param model
* @return
*/
@GetMapping("/")
public String index(Model model, HttpServletResponse response) {
// 视图重定向 - 跳转
return "/nacos";
}
}
b.配置文件
spring:
datasource:
# 若为mysql,默认数据目录:{user.home}/nacos/data/derby-data
platform: mysql
# 若为embedded,则启动一次
# platform: embedded
db:
num: 1
password:
'0': ${MYSQL-PWD:root}
url:
'0': jdbc:mysql://${MYSQL-HOST:pisces-boot-mysql}:${MYSQL-PORT:3306}/${MYSQL-DB:nacos}?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
user:
'0': ${MYSQL-USER:root}
c.启动
http://127.0.0.1:8858/nacos
nacos
nacos
d.说明
20250205,暂无官方成熟的方案支持在 Spring Boot 子模块中直接嵌入并启动 Nacos 服务器
05.【实现】微服务网关:pisces-cloud-gateway
a.示例配置
a.配置
spring:
application:
name: pisces-gateway
main:
allow-circular-references: true
cloud:
nacos:
config:
server-addr: localhost:8858
file-extension: yml
shared-configs:
- dataId: pisces.yml
group: DEFAULT_GROUP
refresh: true
extension-configs:
- dataId: pisces-gateway-router.json
group: DEFAULT_GROUP
refresh: true
- dataId: pisces-gateway-dev.yml
group: DEFAULT_GROUP
refresh: true
- dataId: pisces-dev.yml
group: DEFAULT_GROUP
refresh: true
discovery:
server-addr: ${spring.cloud.nacos.config.server-addr}
b.配置加载顺序(优先级从低到高):
1.application.yml(本地配置)
2.shared-configs(共享配置)
pisces.yml
3.extension-configs(扩展配置,按数组顺序)
pisces-gateway-router.json
pisces-gateway-dev.yml
pisces-dev.yml
c.具体配置内容:
1.pisces.yml:基础配置
服务器配置
数据库连接
Redis设置
日志配置等
2.pisces-gateway-router.json:网关路由配置
路由规则
服务转发
断言配置
3.pisces-gateway-dev.yml:网关开发环境配置
网关特定配置
开发环境参数
4.pisces-dev.yml:开发环境通用配置
开发环境特定的配置
可能会覆盖之前的配置
b.【核心功能1】:路由转发
a.配置管理:
方式1(定):使用本地配置文件
方式2:使用配置中心(Apollo、Consul等)
方式3:自己开发简单的配置管理界面
b.服务发现
方式1:使用 DNS
方式2:使用 Kubernetes Service
方式3(定):直接配置 IP
c.其他功能
基础路由转发(HTTP)
WebSocket 支持
路径重写
全局过滤:Token处理、请求路径处理、基础路径添加
c.【核心功能2】:限流熔断(IP限流/用户限流/接口限流/Sentinel限流规则)(自定义异常响应/限流响应处理)
a.Redis + RequestRateLimiter 方案
# 需要添加 spring-boot-starter-data-redis 依赖
spring:
cloud:
gateway:
routes:
- id: rate_limit_route
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒允许的请求数
redis-rate-limiter.burstCapacity: 20 # 令牌桶容量
key-resolver: "#{@ipKeyResolver}" # 限流维度
---------------------------------------------------------------------------------------------
优点:分布式限流、精确控制速率、支持多种限流维度
缺点:依赖 Redis、配置相对复杂、性能有 Redis 瓶颈
b.Sentinel 方案
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
scg:
rules:
flow:
- resource: api-resource
count: 100
intervalSec: 1
controlBehavior: 0
---------------------------------------------------------------------------------------------
优点:本地限流,性能好、支持多种限流策略、提供控制台
缺点:单机限流、规则同步需要额外配置、重启后规则丢失
d.【核心功能3,Jeecg网关,模式分析】:允许系统模块【pisces-module-system】向网关发送路由更新请求,以【刷新网关Redis中的路由】配置
a.路由配置信息的来源配置
a.路由数据源类型
在文件 RouterDataType.java 中,定义了三种数据类型:
database:即从数据库或类似存储中加载路由配置,这里其实利用 Redis 作为存储介质。
yml:通过本地 yml 文件加载。
nacos:通过 Nacos 配置中心加载。
b.决定加载数据的方式
在 DynamicRouteLoader.java 文件的 init(BaseMap baseMap) 方法中,
会根据 gatewayRoutersConfig.getDataType() 的值来判断采用哪种方式加载动态路由。
例如,当数据源为 database 时,就会调用 loadRoutesByRedis(baseMap) 方法。
b.Redis中路由配置信息的接收过程
a.通过 RedisUtil 获取路由信息
在 DynamicRouteLoader.java 的 loadRoutesByRedis(BaseMap baseMap) 方法中,调用了方法:
Object configInfo = redisUtil.get(CacheConstant.GATEWAY_ROUTES);
这里通过 redisUtil.get 方法从 Redis 中获取 key 为 CacheConstant.GATEWAY_ROUTES 的值。
这个值预期为一个 JSON 数组格式的字符串,包含了多个路由配置。
b.解析 JSON 数据
如果获取到的 configInfo 非空,会使用 FastJSON 将字符串解析为 JSONArray。
随后调用 getRoutesByJson(JSONArray array) 方法,将 JSON 数组中的每一项转换为自定义的
MyRouteDefinition 对象(注意:MyRouteDefinition 继承自 Spring Cloud Gateway 的 RouteDefinition,
并增加了路由状态字段 status)。
c.根据状态决定路由操作
在 loadRoutesByRedis 中,对每个解析出来的 MyRouteDefinition 对象:
如果 status 为 0,则调用 dynamicRouteService.delete(definition.getId()) 删除该路由配置。
如果路由状态为正常,则调用 dynamicRouteService.add(definition) 将路由添加到内存中。
c.动态路由的实现
a.内存存储路由配置
最终路由信息被存储在 MyInMemoryRouteDefinitionRepository.java 中。
这个类实现了 Spring Cloud Gateway 的 RouteDefinitionRepository 接口,
通过一个线程安全的 Map 存储所有的路由定义。
b.服务更新及刷新机制
在 DynamicRouteService.java 中,新增、更新或删除路由操作内部都会调用方法
publisher.publishEvent(new RefreshRoutesEvent(this)),
这会触发 Spring Cloud Gateway 内部的路由刷新机制。
当应用接收到刷新事件时,网关会重新加载 RouteDefinitionRepository 中的路由,
从而使最新的路由配置生效。
c.动态刷新触发时机
除了通过 RedisUtil 在加载时获取 Redis 中的配置信息,
当配置发生变化或调用刷新接口时(例如调用 DynamicRouteLoader.refresh(BaseMap baseMap) 方法),
都会再次执行加载过程,从而实现动态更新路由。
d.总结:通过整合 Redis 存储、JSON 解析、内存路由仓库和刷新事件机制,实现了网关的动态路由功能
a.获取配置
当网关配置的路由数据源为 database 时,会调用 DynamicRouteLoader.loadRoutesByRedis 方法;
该方法利用 redisUtil.get(CacheConstant.GATEWAY_ROUTES) 从 Redis 中获取存储的 JSON 格式路由信息;
解析后的数据转换为一组 MyRouteDefinition 对象。
b.更新路由
根据每条路由的状态(status),网关决定是调用删除操作还是新增操作;
这些操作通过 DynamicRouteService 完成,该服务围绕 MyInMemoryRouteDefinitionRepository 进行操作存储路由定义。
c.刷新机制
每次添加、更新或删除路由后,都会发布 RefreshRoutesEvent 事件;
Spring Cloud Gateway 监听该事件后重新加载路由,从而动态调整网关路由信息。
e.【核心功能4,Jeecg网关,3种模式】:允许系统模块【pisces-module-system】向网关发送路由更新请求,以【刷新网关Redis中的路由】配置
a.database模式:从 Redis 中读取路由数据并动态更新路由的大致流程
a.配置选择
# 可选: database、nacos、yml
data-type: database
-------------------------------------------------------------------------------------------------
此配置会在启动时被加载到 GatewayRoutersConfig(或类似配置类)中,用来决定路由数据的来源。
b.初始化路由加载
在网关启动时,会调用 DynamicRouteLoader 类的 init(BaseMap baseMap) 方法。
其中根据配置判断数据源类型
if (RouterDataType.database.toString().endsWith(gatewayRoutersConfig.getDataType())) {
loadRoutesByRedis(baseMap);
}
当 data-type 为 database 时,就执行 loadRoutesByRedis(baseMap) 方法。
c.从 Redis 读取路由数据
在 loadRoutesByRedis(BaseMap baseMap) 方法中,主要流程为:
通过 redisUtil.get(CacheConstant.GATEWAY_ROUTES) 从 Redis 中获取存储的路由配置信息。
获取到的数据通常为 JSON 格式的字符串,表示一个路由配置的数组。
---------------------------------------------------------------------------------------------
如下代码所示:
Object configInfo = redisUtil.get(CacheConstant.GATEWAY_ROUTES);
if (ObjectUtil.isNotEmpty(configInfo)) {
log.info("获取网关当前配置:\r\n{}", configInfo);
JSONArray array = JSON.parseArray(configInfo.toString());
// 将数组解析为 List<MyRouteDefinition> 对象
routes = getRoutesByJson(array);
} else {
log.warn("ERROR: 从Redis获取网关配置为空,请确认system服务是否启动成功!");
}
d.解析 JSON 数据
方法 getRoutesByJson(JSONArray array) 会遍历 JSON 数组,每个 JSON 对象会映射成一个 MyRouteDefinition 对象。
其中包括:
路由 ID
路由状态(status):判断路由是否有效
路由的 URI 地址
Predicate(路由匹配规则)
Filter(过滤处理逻辑)
此步骤确保 Redis 中存储的 JSON 能转换成 Gateway 所需的 RouteDefinition 对象(通过子类 MyRouteDefinition 扩展)。
e.更新内存中的路由配置
遍历解析后的路由列表后,根据每个路由对象的 status 属性决定:
如果 status 为 0,表示该路由需要删除,则调用 dynamicRouteService.delete(definition.getId());
如果路由状态正常,则调用 dynamicRouteService.add(definition) 将其添加到内存中。
具体代码如下:
for (MyRouteDefinition definition : routes) {
log.info("update route : {}", definition.toString());
Integer status = definition.getStatus();
if (status.equals(0)) {
dynamicRouteService.delete(definition.getId());
} else {
dynamicRouteService.add(definition);
}
}
f.发布刷新事件
完成路由的新增或删除后,会通过调用 this.publisher.publishEvent(new RefreshRoutesEvent(this)) 发布一个刷新路由的事件。
Spring Cloud Gateway 监听到此事件后,会重新加载 MyInMemoryRouteDefinitionRepository 中的所有路由,从而使更新立即生效。
g.总结流程
1.配置选择:data-type: database 使得网关确定数据源从 Redis 读取。
2.初始化:网关启动时,在 DynamicRouteLoader.init() 方法中根据数据源类型调用 loadRoutesByRedis()。
3.读取数据:在 loadRoutesByRedis() 中,通过 Redis 工具类读取 key 为 CacheConstant.GATEWAY_ROUTES 的 JSON 路由数据。
4.解析转换:使用 getRoutesByJson() 方法将 JSON 数组解析成 MyRouteDefinition 对象列表。
5.更新路由仓库:根据每条数据的状态,调用 DynamicRouteService 的 add 或 delete 方法修改内存中的路由信息(存储在 MyInMemoryRouteDefinitionRepository)。
6.刷新路由:发布 RefreshRoutesEvent 事件,最终使 Gateway 实时刷新路由,完成动态路由更新。
b.nacos模式:网关读取路由配置信息以及动态更新路由的主要流程
a.配置选择
# 可选: database、nacos、yml
data-type: nacos
-------------------------------------------------------------------------------------------------
此配置会在启动时被加载到 GatewayRoutersConfig(或类似配置类)中,用来决定路由数据的来源。
b.初始化路由加载
网关启动时,会调用 DynamicRouteLoader 类中的 init(BaseMap baseMap) 方法,其中包括如下判断:
if (RouterDataType.nacos.toString().endsWith(gatewayRoutersConfig.getDataType())) {
loadRoutesByNacos();
}
这一步根据配置是否为 nacos 判断是否调用 loadRoutesByNacos() 方法。
c.创建 Nacos 配置服务
在 loadRoutesByNacos() 方法内部,会调用 createConfigService() 方法。
该方法通过 Nacos 提供的 NacosFactory.createConfigService(Properties) 创建一个 ConfigService 实例。
配置项如 serverAddr、namespace、username 和 password 会从 gatewayRoutersConfig 中读取,并作为 Properties 传入。
这样就建立了与 Nacos 配置中心的连接。
d.读取路由配置
利用创建好的 ConfigService 对象,通过如下方法从 Nacos 读取配置信息:
String configInfo = configService.getConfig(
gatewayRoutersConfig.getDataId(),
gatewayRoutersConfig.getRouteGroup(),
DEFAULT_TIMEOUT);
---------------------------------------------------------------------------------------------
gatewayRoutersConfig.getDataId() 指定路由配置信息的 dataId。
gatewayRoutersConfig.getRouteGroup() 指定所使用的配置分组(group)。
DEFAULT_TIMEOUT 为获取配置时的超时时间。
返回的 configInfo 应为一个 JSON 格式字符串,里面包含了一个路由对象数组。
e.解析 Nacos 返回的 JSON 数据
如果 configInfo 不为空,就会对其进行解析:
routes = JSON.parseArray(configInfo, RouteDefinition.class);
这一步会将 JSON 数组转换为一组 RouteDefinition 对象,从而得到当前网关场景下所需的路由配置。
f.更新内存路由配置
遍历得到的 RouteDefinition 列表,然后对于每个路由:
使用 dynamicRouteService.add(definition); 将路由添加到网关内存中,该过程底层会存储到 MyInMemoryRouteDefinitionRepository 对象中。
每完成一次路由操作(比如新增路由),还会调用:this.publisher.publishEvent(new RefreshRoutesEvent(this));
发布刷新路由的事件,确保网关及时更新并使最新的路由定义生效。
g.注册 Nacos 监听器以实现动态更新
除了初始化获取配置外,loadRoutesByNacos() 方法紧接着会调用:
dynamicRouteByNacosListener(gatewayRoutersConfig.getDataId(), gatewayRoutersConfig.getRouteGroup());
---------------------------------------------------------------------------------------------
在这个方法中,通过 configService.addListener(dataId, group, Listener) 注册一个监听器。
当 Nacos 中配置发生变化时,监听器的 receiveConfigInfo(String configInfo) 方法被触发,该方法会解析新的路由配置,并调用 dynamicRouteService.update(definition) 更新已有的路由。
这样,当 Nacos 中的路由配置更新时,网关能自动感知并动态刷新其路由定义。
h.总结流程
1.配置决定数据源:在配置文件中设置 data-type: nacos,网关启动时就会进入 nacos 模式。
2.初始化阶段:在 DynamicRouteLoader.init() 方法中,根据配置调用 loadRoutesByNacos()。
3.创建 Nacos 连接 & 读取数据:
通过 createConfigService() 与 Nacos 建立连接。
使用 getConfig() 方法根据 dataId 和 group 读取 JSON 格式的路由信息。
4.解析与更新路由:
利用 FastJSON 解析 JSON 数据成 RouteDefinition 列表;
遍历并将每个路由通过 dynamicRouteService.add() 添加到内存中;
发布 RefreshRoutesEvent 事件,使路由更新生效。
5.动态监听配置变更:
注册 Nacos 监听器,当配置中心中的路由数据变更时,自动调用 receiveConfigInfo() 方法解析并更新路由配置,实现动态路由的无缝更新。
c.yml模式【可用】:网关读取路由配置信息以及动态更新路由的主要流程
a.配置选择
# 可选: database、nacos、yml
data-type: yml
-------------------------------------------------------------------------------------------------
此配置会在启动时被加载到 GatewayRoutersConfig(或类似配置类)中,用来决定路由数据的来源。
这表示网关使用静态的 YAML 配置来定义路由,而不是从外部(Redis 或 Nacos)动态获取路由信息。
b.路由加载流程:在这种模式下,路由的加载流程主要依赖于 Spring Boot 自身的配置加载机制
a.静态配置加载
在应用启动时,Spring Boot 会读取 classpath 下的 application.yml(或者其他指定的配置文件),
如果你已在 application.yml 中配置了静态路由(例如通过 spring.cloud.gateway.routes 来定义路由),
Spring Cloud Gateway 会自动将这些配置解析并加载为内存中的路由定义。
b.DynamicRouteLoader 处理逻辑
在 DynamicRouteLoader 类中,init(BaseMap baseMap) 方法根据 gatewayRoutersConfig.getDataType() 判断数据源类型。
这段代码只对 nacos 或 database 类型做了处理:
if (RouterDataType.nacos.toString().endsWith(gatewayRoutersConfig.getDataType())) {
loadRoutesByNacos();
}
if (RouterDataType.database.toString().endsWith(gatewayRoutersConfig.getDataType())) {
loadRoutesByRedis(baseMap);
}
-----------------------------------------------------------------------------------------
如果你选择 yml 模式,上述条件都不满足,因此不会调用 loadRoutesByNacos() 或 loadRoutesByRedis()。
同样在 refresh(BaseMap baseMap) 方法中,有如下判断:
if (!RouterDataType.yml.toString().endsWith(gatewayRoutersConfig.getDataType())) {
this.init(baseMap);
}
-----------------------------------------------------------------------------------------
当 data-type 为 yml 时,条件为 false,从而不会调用 init() 方法重新加载数据。
也就是说,静态的 YAML 路由只会在应用启动时加载一次,后续修改不会自动生效。
c.动态刷新和更新
与 database 或 nacos 模式不同,yml 模式下没有动态数据源。
当配置通过 YAML 静态定义后,路径信息就写死在配置文件中。
如果需要更新路由信息,你必须修改 YAML 配置,并重启应用才能使变更生效。
d.总结流程
1.启动加载:当 data-type 配置为 yml 时,网关在启动时直接读取 application.yml 中定义的路由,使用 Spring Boot 的标准配置加载机制完成解析和初始化。
2.不支持动态刷新:由于 DynamicRouteLoader.init() 方法只针对 nacos 和 database 模式进行数据加载,使用 yml 模式时不会去获取外部数据;同时 refresh() 方法也不会重新调用数据加载逻辑,这意味着路由配置在启动后不再动态更新。
3.使用场景:使用 yml 模式适合静态路由配置场景,即路由信息在应用启动期间不会发生频繁变化。如果你的路由信息需要在运行时动态调整,建议选择其他的动态数据源模式(如 nacos 或 database)。
f.报错1
a.问题
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of method modifyRequestBodyGatewayFilterFactory in org.springframework.cloud.gateway.config.GatewayAutoConfiguration required a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' in your configuration.
b.说明
knife4j-spring-boot-starter 是为 Spring MVC (servlet) 设计的
它会引入 spring-boot-starter-web 和 spring-webmvc 相关依赖
而 Gateway 是基于 webflux (reactive) 的
这导致了 servlet 和 reactive 的依赖冲突
c.解决:将一部分common抽离到common-api
当前问题的根本原因在于 LoderRouderHandler 内部引用了网关专用的实现类(如 DynamicRouteLoader),导致在公共模块中无意间加载了网关逻辑。解决思路主要有两种:
拆分模块:将公共接口和常量剥离到独立模块,将网关实现单独归类,并且只有在真正需要网关功能的环境中才引入完整实现。
接口抽象与条件装配:在公共模块仅定义接口,实现交由网关模块负责,然后通过条件装配确保不同环境下不会加载不必要的 bean。
这些思路在官网、GitHub 多模块示例以及 StackOverflow 上都是常见的做法,可以根据你的项目具体情况选择合适的方案。
06.【实现】主服务:pisces-system-cloud-start
07.【实现】子服务:pisces-demo-cloud-start
08.【实现】监控和测试例子:pisces-visual
5.4 集成框架
00.未来规划
代码生成器
表单设计器
报表设计器
大屏设计器
仪表盘设计
工作流引擎
流程设计器
01.JustAuth
a.介绍
https://gitee.com/yadong.zhang/JustAuth
特点:
开箱即用的整合第三方登录的开源组件
支持OAuth2.0、OpenID等标准协议
可以作为认证中心的基础
b.特点
登录认证:多端登录、单端登录、同端互斥登录、七天免登录…… 多种登录策略只需改个配置即可完成
权限认证:权限认证、角色认证、会话二级认证、注解鉴权、路由鉴权……多种姿势灵活鉴权
踢人下线:强制注销、踢人下线、账号封禁、身份切换、自动续签 …… 提供完善的会话管理方案
Redis集成:提供 Redis 集成方案、项目重启数据不丢失、多系统数据互通,可自定义数据持久化策略
前后端分离:内置多种 Token 读取策略,适配APP、小程序、SPA单页应用等前后端分离场景
单点登录:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离……提供各种架构下的SSO接入方案
OAuth2.0:轻松搭建 OAuth2.0 认证中心,支持四种授权模式,支持 openid 授权机制,支持二次扩展开发
微服务支持:分布式 Session 会话、网关统一鉴权、RPC调用鉴权……提供开箱即用的微服务认证方案
开箱即用:提供SpringMVC、WebFlux、Solon、jwt 等常见框架集成包,真正的开箱即用
02.sa-token
a.介绍
地址:https://gitee.com/dromara/sa-token
特点:
轻量级权限认证框架
支持OAuth2.0
支持注解鉴权
支持动态权限修改
支持模块化配置
b.特点
丰富的OAuth平台:支持国内外数十家知名的第三方平台的OAuth 登录
自定义state:支持自定义State和缓存方式,开发者可根据实际情况选择任意缓存插件
自定义OAuth:提供统一接口,支持接入任意OAuth网站,快速实现OAuth登录功能。
自定义Http:接口HTTP工具,开发者可以根据自己项目的实际情况选择相对应的HTTP工具。
自定义Scope:支持自定义scope,以适配更多的业务场景,而不仅仅是为了登录。
-----------------------------------------------------------------------------------------------------
目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权
登录认证:单端登录、多端登录、同端互斥登录、七天内免登录
权限认证:权限认证、角色认证、会话二级认证
Session会话:全端共享Session、单端独享Session、自定义Session
踢人下线:根据账号id踢人下线、根据Token值踢人下线
账号封禁:登录封禁、按照业务分类封禁、按照处罚阶梯封禁
持久层扩展:可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
分布式会话:提供jwt集成、共享数据中心两种分布式会话方案
微服务网关鉴权:适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
单点登录:内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
OAuth2.0认证:轻松搭建 OAuth2.0 服务,支持openid模式
二级认证:在已登录的基础上再次认证,保证安全性
Basic认证:一行代码接入 Http Basic 认证
独立Redis:将权限缓存与业务缓存分离
临时Token认证:解决短时间的Token授权问题
模拟他人账号:实时操作任意用户状态数据
临时身份切换:将会话身份临时切换为其它账号
前后端分离:APP、小程序等不支持Cookie的终端
同端互斥登录:像QQ一样手机电脑同时在线,但是两个手机上互斥登录
多账号认证体系:比如一个商城项目的user表和admin表分开鉴权
Token风格定制:内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
c.Sa-Token和Spring Cloud Gateway关系
a.Sa-Token的部署方式
方式一:单体部署
- 直接集成到 Spring Boot 应用
- 不需要 Gateway
方式二:微服务部署
- 需要 Spring Cloud Gateway
- Sa-Token 提供了 Gateway 的集成包
b.依赖关系
<!-- 单体部署 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<!-- 微服务部署 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
c.选择建议
如果只需要认证授权:仅使用 Sa-Token
如果需要服务热插拔:必须使用 Gateway
如果两者都需要:Sa-Token + Gateway 配合使用
d.架构对比
单体架构:
Client -> Sa-Token -> Services
微服务架构:
Client -> Gateway(Sa-Token集成) -> Services
d.Sa-Token本身提供了一些基础的第三方登录支持,但如果要实现完整的微信、钉钉等第三方登录,通常需要结合其他组件
a.Sa-Token + JustAuth
JustAuth:优秀的第三方登录组件
特点:
- 支持微信(公众号、开放平台、企业微信)
- 支持钉钉
- 支持飞书
- 支持企业微信
- 支持各种主流第三方登录
b.Sa-Token + Sa-OAuth2
Sa-Token官方的OAuth2模块
特点:
- 可以自建OAuth2服务
- 可以对接第三方OAuth2服务
- 可以自定义授权流程
c.集成示例
// 1. 添加依赖
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${latest.version}</version>
</dependency>
// 2. 配置第三方平台参数
justauth:
enabled: true
type:
DINGTALK:
client-id: xxxxxx
client-secret: xxxxxxx
WECHAT_ENTERPRISE:
client-id: xxxxxx
client-secret: xxxxxx
agent-id: xxxxxx
// 3. 结合Sa-Token使用
@Autowired
private AuthRequestFactory factory;
@RequestMapping("/{type}/login")
public void login(@PathVariable String type, HttpServletResponse response) {
AuthRequest authRequest = factory.get(type);
// 生成授权URL
String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
response.sendRedirect(authorizeUrl);
}
@RequestMapping("/{type}/callback")
public Object login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
// 获取第三方用户信息
AuthResponse<AuthUser> response = authRequest.login(callback);
// 结合Sa-Token处理登录
StpUtil.login(response.getData().getUsername());
return response;
}
d.完整功能支持
认证流程:
1. 第三方平台认证
2. 获取用户信息
3. 本地账号绑定
4. 签发Sa-Token令牌
-------------------------------------------------------------------------------------------------
权限管理:
1. 角色分配
2. 权限控制
3. 会话管理
-------------------------------------------------------------------------------------------------
数据同步:
1. 用户信息同步
2. 组织架构同步
3. 权限数据同步
e.建议实施方案
第一步:基础认证
- 实现基本的第三方登录
- 完成用户信息获取
- 建立本地用户体系
-------------------------------------------------------------------------------------------------
第二步:深度集成
- 实现组织架构同步
- 配置权限映射
- 处理数据关联
-------------------------------------------------------------------------------------------------
第三步:扩展功能
- 实现多端登录
- 添加登录日志
- 配置安全策略
91.开源项目:基于OAuth2认证方案
a.项目
a.JustAuth
https://gitee.com/yadong.zhang/JustAuth
特点:
开箱即用的整合第三方登录的开源组件
支持OAuth2.0、OpenID等标准协议
可以作为认证中心的基础
-------------------------------------------------------------------------------------------------
登录认证:多端登录、单端登录、同端互斥登录、七天免登录…… 多种登录策略只需改个配置即可完成
权限认证:权限认证、角色认证、会话二级认证、注解鉴权、路由鉴权……多种姿势灵活鉴权
踢人下线:强制注销、踢人下线、账号封禁、身份切换、自动续签 …… 提供完善的会话管理方案
Redis集成:提供 Redis 集成方案、项目重启数据不丢失、多系统数据互通,可自定义数据持久化策略
前后端分离:内置多种 Token 读取策略,适配APP、小程序、SPA单页应用等前后端分离场景
单点登录:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离……提供各种架构下的SSO接入方案
OAuth2.0:轻松搭建 OAuth2.0 认证中心,支持四种授权模式,支持 openid 授权机制,支持二次扩展开发
微服务支持:分布式 Session 会话、网关统一鉴权、RPC调用鉴权……提供开箱即用的微服务认证方案
开箱即用:提供SpringMVC、WebFlux、Solon、jwt 等常见框架集成包,真正的开箱即用
b.sa-token
地址:https://gitee.com/dromara/sa-token
特点:
轻量级权限认证框架
支持OAuth2.0
支持注解鉴权
支持动态权限修改
支持模块化配置
-------------------------------------------------------------------------------------------------
丰富的OAuth平台:支持国内外数十家知名的第三方平台的OAuth 登录
自定义state:支持自定义State和缓存方式,开发者可根据实际情况选择任意缓存插件
自定义OAuth:提供统一接口,支持接入任意OAuth网站,快速实现OAuth登录功能。
自定义Http:接口HTTP工具,开发者可以根据自己项目的实际情况选择相对应的HTTP工具。
自定义Scope:支持自定义scope,以适配更多的业务场景,而不仅仅是为了登录。
c.pig
地址:https://gitee.com/log4j/pig
特点:
完整的微服务架构
OAuth2认证
支持多租户
可以参考其认证模块
d.lamp-cloud
地址:https://gitee.com/zuihou111/lamp-cloud
特点:
微服务多租户架构
基于OAuth2的统一认证
支持服务的动态注册
b.对比
a.MaxKey
定位:企业级身份管理和认证产品
特点:
- 完整的IAM解决方案
- 提供管理界面
- 支持多种认证协议
- 企业级SSO功能
支持协议:
- OAuth 2.0
- SAML 2.0
- OpenID Connect
- CAS
- JWT
- SCIM 2.0
应用场景:
- 企业统一认证
- 多系统SSO
- 身份管理
b.JustAuth
定位:第三方登录整合组件
特点:
- 专注第三方登录
- 轻量级接入
- 开箱即用
- 只关注认证
支持平台:
- 微信(公众号/开放平台)
- 企业微信
- 钉钉
- 飞书
- Github
- Gitee等
应用场景:
- 社交账号登录
- 第三方账号集成
c.Sa-Token
定位:轻量级权限认证框架
特点:
- 注重轻量化
- 功能模块化
- 配置简单
- 专注权限控制
核心功能:
- 会话管理
- 权限认证
- 单点登录
- 令牌验证
- 路由拦截
应用场景:
- 应用内认证
- 权限管理
- 基础安全控制
d.选择建议
如果您需要:
- 完整的企业级认证方案 -> MaxKey
- 只需要第三方登录 -> JustAuth
- 轻量级认证授权 -> Sa-Token
如果您需要组合使用:
- 基础认证授权:Sa-Token
- 第三方登录:JustAuth
- 企业级需求:MaxKey
e.集成建议
小型项目:
- Sa-Token + JustAuth
中型项目:
- Sa-Token + JustAuth + 自定义管理界面
企业级项目:
- MaxKey(完整解决方案)
- 或 Sa-Token + MaxKey(混合部署)
c.方案
a.第一阶段:认证体系
使用sa-token
- 轻量级
- 易于集成
- 支持模块化
b.第二阶段:服务热插拔
结合Spring Cloud Gateway
- 动态路由
- 服务保护
- 权限控制
c.第三阶段:监控和管理
整合Sentinel
- 流量控制
- 服务监控
- 实时告警
d.分析
a.Sa-Token
资源保护能力:
- 支持模块级别的权限控制
- 支持注解式鉴权
- 支持动态权限变更
- 支持多账号体系
-------------------------------------------------------------------------------------------------
集成Gateway:
- 提供Gateway集成方案
- 可以实现统一鉴权
- 支持动态权限策略
b.Spring Cloud Gateway
UI管理方案:
- 官方无UI界面
- 可选方案:
1. SCG-Admin(开源项目,提供UI界面)
2. Spring Boot Admin(可监控Gateway)
3. 自定义管理界面
-------------------------------------------------------------------------------------------------
功能支持:
- 动态路由配置
- 服务保护策略
- 限流熔断
c.Sentinel
监控能力:
- 可以监控Gateway
- 支持自定义监控指标
- 提供Dashboard界面
-------------------------------------------------------------------------------------------------
资源保护:
- 支持服务级别的流控
- 支持接口级别的限流
- 支持自定义规则
92.开源项目:Other
a.网关UI
https://github.com/zouchanglin/spring-cloud-gateway-admin
https://github.com/iamazy/spring-cloud-gateway-management
https://github.com/apache/apisix-dashboard
https://github.com/fizzgate/fizz-gateway-node
5.5 接入三方
00.关系图
职位表(Postion)
↓
三方表(Third)<---> 用户(User) <---> 角色(Role) <---> 菜单(Menu)
↑ ↑ ↑
部门(Depart)------------------------
---------------------------------------------------------------------------------------------------------
IC13060002
123456
---------------------------------------------------------------------------------------------------------
J057240
qwER159263!
00.关系表
表名 新增 更新 删除
组织表 sys_depart √ √ √
组织表 sys_depart_permission
组织表 sys_depart_role 后续版本添加
组织表 sys_depart_role_permission 后续版本添加
组织表 sys_depart_role_user 后续版本添加
菜单表 sys_permission
菜单表 sys_permission_data_rule
菜单表 sys_permission_v2
职位表 sys_position √ √ √
角色表 sys_role 系统维护 系统维护 系统维护
角色表 sys_role_index
角色表 sys_role_permission
三方表 sys_third_account √ √ √
用户表 sys_user √ √ √
用户表 sys_user_role,关联角色 √,固定user √,固定user √
用户表 sys_user_depart,关联部门 √ √ √
用户表 sys_user_agent,关联代理人,表字段有代理人、所属部门、所属公司
用户表 sys_user_position,关联职位,表不存在,冗余到sys_user的post字段,存储方式为【处长助理】或【devleader,总经理】
用户表 sys_user_third,关联三方,表不存在,信息冗余到sys_third_account
01.第1种:共同数据,找到 jobNo/username 相同,但 age/sex/birthday 至少有一个不同的元素,待更新
sys_user表 【待更新字段】 user表(云之家)【对应字段】
id 主键id
username 登录账号 jobNo
realname 真实姓名 √ name
password 密码
salt md5密码盐
avatar 头像 √ photoUrl
birthday 生日 √ birthday
sex 性别(0-默认未知,1-男,2-女) √ gender
email 电子邮件 √ email
phone 电话 √ phone
org_code 登录会话的机构编码 √ org_id
status 性别(1-正常,2-冻结)
del_flag 删除状态(0-正常,1-已删除)
third_id 第三方登录的唯一标识 √ openid
third_type 第三方类型
activiti_sync 同步工作流引擎(1-同步,0-不同步)
work_no 工号,唯一键 √ jobNo
post 职务,关联职务表 √ jobTitle
telephone 座机号
create_by 创建人
create_time 创建时间
update_by 更新人 √ 固定admin
update_time 更新时间 √ yyyy-MM-dd HH:mm:ss
user_identity 身份(1普通成员 2上级)
depart_ids 负责部门
rel_tenant_ids 多租户标识
client_id 设备ID
file_no 档案号 √ fileNo
sys_user_depart表 【待更新字段】
id id
user_id 用户id,对应sys_user表中的id
dep_id 部门id √
sys_third_account表 【待更新字段】 user表(云之家)【对应字段】
id 编号
sys_user_id 第三方登录id,对应sys_user表中的id
avatar 头像 √ photoUrl
status 状态(1-正常,2-冻结)
del_flag 删除状态(0-正常,1-已删除)
realname 真实姓名 √ name
third_user_uuid 第三方账号
third_user_id 第三方app用户账号,对应sys_user表中的id √ openid
create_by 创建人登录名称
create_time 创建日期
update_by 更新人登录名称 √ 固定admin
update_time 更新日期 √ yyyy-MM-dd HH:mm:ss
third_type 登录来源
02.第2种:只在 本地用户 存在:待删除。
方案1:将 用户 中的 del_flag 设置为 1,逻辑删除,放到回收站
方案2:将该用户的信息全部删除
【本次采用方案1,冗余信息】。
【不采用方案2,不冗余信息】。理由:云之家 会与 本系统 用户信息 强同步,无法创建额外比如 test1、test2 等本想系统独有的用户。
注意,两种方案,请设置白名单。排除内容大致为 admin,test1,test2,jeecg,ceshi
03.第3种:只在 云之家数据 存在:待新增
sys_user表 【待新增字段】 user表(云之家)【对应字段】
id 主键id -,随机19位
username 登录账号 - jobNo
realname 真实姓名 √ name
password 密码 -,默认123456
salt md5密码盐 -,随机
avatar 头像 √ photoUrl
birthday 生日 √ birthday
sex 性别(0-默认未知,1-男,2-女) √ gender
email 电子邮件 √ email
phone 电话 √ phone
org_code 登录会话的机构编码 √ org_id
status 性别(1-正常,2-冻结) -,默认1-正常
del_flag 删除状态(0-正常,1-已删除) -,默认0-正常
third_id 第三方登录的唯一标识 √ openid
third_type 第三方类型 -,默认yunzhijia
activiti_sync 同步工作流引擎(1-同步,0-不同步) -,默认0-不同步
work_no 工号,唯一键 √ jobNo
post 职务,关联职务表 √ jobTitle
telephone 座机号 null
create_by 创建人 -,默认admin
create_time 创建时间 -,当前时间
update_by 更新人 √ 固定admin
update_time 更新时间 √ yyyy-MM-dd HH:mm:ss
user_identity 身份(1普通成员 2上级) -,默认1普通成员
depart_ids 负责部门 null
rel_tenant_ids 多租户标识 null
client_id 设备ID null
file_no 档案号 √ fileNo
sys_user_depart表 【待新增字段】 user表(云之家)【对应字段】
id id -,随机19位
user_id 用户id,对应sys_user表中的id -,对应sys_user表中的id,随机19位
dep_id 部门id √
sys_third_account表 【待新增字段】 user表(云之家)【对应字段】
id 编号 -,随机19位
sys_user_id 第三方登录id,对应sys_user表中的id -,对应sys_user表中的id,随机19位
avatar 头像 √ photoUrl
status 状态(1-正常,2-冻结) -,默认1-正常
del_flag 删除状态(0-正常,1-已删除) -,默认0-正常
realname 真实姓名 √ name
third_user_uuid 第三方账号 -,随机19位
third_user_id 第三方app用户账号,对应sys_user表中的id √ openid
create_by 创建人登录名称 -,固定admin
create_time 创建日期 -,yyyy-MM-dd HH:mm:ss
update_by 更新人登录名称 √ 固定admin
update_time 更新日期 √ yyyy-MM-dd HH:mm:ss
third_type 登录来源 -,固定yunzhijia
91.工厂类处理SQL
private <T> void handleBatchOperation(List<T> list, String sqlOperationName) {
if (ObjectUtil.isEmpty(list)) {
return;
}
final int batchSize = 3000;
for (int i = 0, sizes = list.size(); i < sizes; i += batchSize) {
List<T> batch = list.subList(i, Math.min(i + batchSize, sizes));
try {
switch (sqlOperationName) {
case "insertSysUserBatch":
if (!(batch.get(0) instanceof SysUser)) {
throw new IllegalArgumentException("insertSysUserBatch 批量数据类型不匹配,期望 SysUser 类型");
}
userMapper.upsertSysUserBatch((List<SysUser>) batch);
break;
case "updateSysUserBatch":
if (!(batch.get(0) instanceof SysUser)) {
throw new IllegalArgumentException("updateSysUserBatch 批量数据类型不匹配,期望 SysUser 类型");
}
userMapper.upsertSysUserBatch((List<SysUser>) batch);
break;
case "bulkDeleteSysUser":
if (!(batch.get(0) instanceof String)) {
throw new IllegalArgumentException("bulkDeleteSysUser 批量数据类型不匹配,期望 String 类型");
}
userMapper.bulkDeleteSysUser((List<String>) batch);
break;
case "insertSysThirdAccountBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("insertSysThirdAccountBatch 批量数据类型不匹配,期望 SysThirdAccount 类型");
}
userMapper.upsertSysThirdAccountBatch((List<SysThirdAccount>) batch);
break;
case "updateSysThirdAccountBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("updateSysThirdAccountBatch 批量数据类型不匹配,期望 SysThirdAccount 类型");
}
userMapper.upsertSysThirdAccountBatch((List<SysThirdAccount>) batch);
break;
case "bulkDeleteSysThirdAccount":
if (!(batch.get(0) instanceof String)) {
throw new IllegalArgumentException("bulkDeleteSysThirdAccount 批量数据类型不匹配,期望 String 类型");
}
userMapper.bulkDeleteSysThirdAccount((List<String>) batch);
break;
case "insertSysUserRoleBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("insertSysUserRoleBatch 批量数据类型不匹配,期望 SysUserRole 类型");
}
sysUserRoleMapper.upsertSysUserRoleBatch((List<SysUserRole>) batch);
break;
case "updateSysUserRoleBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("updateSysUserRoleBatch 批量数据类型不匹配,期望 SysUserRole 类型");
}
sysUserRoleMapper.upsertSysUserRoleBatch((List<SysUserRole>) batch);
break;
case "bulkDeleteSysUserRole":
if (!(batch.get(0) instanceof String)) {
throw new IllegalArgumentException("bulkDeleteSysUserRole 批量数据类型不匹配,期望 String 类型");
}
sysUserRoleMapper.bulkDeleteSysUserRole((List<String>) batch);
break;
default:
throw new IllegalArgumentException("不支持的操作类型: " + sqlOperationName);
}
} catch (Exception e) {
// 执行数据库,运行过程抛出异常抛出异常
throw new RuntimeException("操作数据库异常:" + sqlOperationName, e);
}
}
}
---------------------------------------------------------------------------------------------------------
private <T> boolean handleBatchOperation(List<T> list, String sqlOperationName) {
if (ObjectUtil.isEmpty(list)) {
return true;
}
boolean vFlag = true;
final int batchSize = 3000;
for (int i = 0, sizes = list.size(); i < sizes; i += batchSize) {
List<T> batch = list.subList(i, Math.min(i + batchSize, sizes));
try {
switch (sqlOperationName) {
case "insertSysUserBatch":
if (!(batch.get(0) instanceof SysUser)) {
throw new IllegalArgumentException("insertSysUserBatch 批量数据类型不匹配,期望 SysUser 类型");
}
vFlag = userMapper.upsertSysUserBatch((List<SysUser>) batch);
break;
case "updateSysUserBatch":
if (!(batch.get(0) instanceof SysUser)) {
throw new IllegalArgumentException("updateSysUserBatch 批量数据类型不匹配,期望 SysUser 类型");
}
vFlag = userMapper.upsertSysUserBatch((List<SysUser>) batch);
break;
case "bulkDeleteSysUser":
if (!(batch.get(0) instanceof String)) {
throw new IllegalArgumentException("bulkDeleteSysUser 批量数据类型不匹配,期望 String 类型");
}
vFlag = userMapper.bulkDeleteSysUser((List<String>) batch);
break;
case "insertSysThirdAccountBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("insertSysThirdAccountBatch 批量数据类型不匹配,期望 SysThirdAccount 类型");
}
vFlag = userMapper.upsertSysThirdAccountBatch((List<SysThirdAccount>) batch);
break;
case "updateSysThirdAccountBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("updateSysThirdAccountBatch 批量数据类型不匹配,期望 SysThirdAccount 类型");
}
vFlag = userMapper.upsertSysThirdAccountBatch((List<SysThirdAccount>) batch);
break;
case "bulkDeleteSysThirdAccount":
if (!(batch.get(0) instanceof String)) {
throw new IllegalArgumentException("bulkDeleteSysThirdAccount 批量数据类型不匹配,期望 String 类型");
}
vFlag = userMapper.bulkDeleteSysThirdAccount((List<String>) batch);
break;
case "insertSysUserRoleBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("insertSysUserRoleBatch 批量数据类型不匹配,期望 SysUserRole 类型");
}
vFlag = sysUserRoleMapper.upsertSysUserRoleBatch((List<SysUserRole>) batch);
break;
case "updateSysUserRoleBatch":
if (!(batch.get(0) instanceof SysThirdAccount)) {
throw new IllegalArgumentException("updateSysUserRoleBatch 批量数据类型不匹配,期望 SysUserRole 类型");
}
vFlag = sysUserRoleMapper.upsertSysUserRoleBatch((List<SysUserRole>) batch);
break;
case "bulkDeleteSysUserRole":
if (!(batch.get(0) instanceof String)) {
throw new IllegalArgumentException("bulkDeleteSysUserRole 批量数据类型不匹配,期望 String 类型");
}
vFlag = sysUserRoleMapper.bulkDeleteSysUserRole((List<String>) batch);
break;
default:
throw new IllegalArgumentException("不支持的操作类型: " + sqlOperationName);
}
} catch (Exception e) {
// 执行数据库,运行过程抛出异常抛出异常
throw new RuntimeException("操作数据库异常:" + sqlOperationName, e);
} finally {
if (!vFlag) {
throw new IllegalStateException("操作数据库未成功,但未发生运行时异常:" + sqlOperationName);
}
}
}
return vFlag;
}
92.收集错误日志信息
/**
* 【同步用户信息】:收集同步过程中的错误信息
*/
private void syncCollectErrInfo(Exception e, Object data, SyncInfoVo syncInfo, String sqlOperationName) {
String msg = "";
// 获取详细的错误信息
if (e instanceof DuplicateKeyException) {
msg = e.getCause().getMessage();
} else {
msg = e.getMessage();
}
// 根据传入的 data 对象类型动态处理错误信息
String timestamp = DateUtil.format(DateUtil.date(), "yyyy-MM-dd HH:mm:ss");
if (data instanceof Department) {
Department department = (Department) data;
String errorStr = String.format("部门 %s(%s) 同步失败!错误信息:%s,时间:%s", department.getName(), department.getId(), msg, timestamp);
syncInfo.addFailInfo(errorStr);
} else if (data instanceof User) {
User user = (User) data;
String errorStr = String.format("用户 %s(%s) 同步失败!错误信息:%s,时间:%s", user.getJobNo(), user.getName(), msg, timestamp);
syncInfo.addFailInfo(errorStr);
} else if (data instanceof SysUser) {
SysUser sysUser = (SysUser) data;
String errorStr = String.format("系统用户 %s 同步失败!错误信息:%s,时间:%s", sysUser.getUsername(), msg, timestamp);
syncInfo.addFailInfo(errorStr);
} else if (data instanceof SysThirdAccount) {
SysThirdAccount sysThirdAccount = (SysThirdAccount) data;
String errorStr = String.format("第三方账户 %s 同步失败!错误信息:%s,时间:%s", sysThirdAccount.getRealname(), msg, timestamp);
syncInfo.addFailInfo(errorStr);
} else {
String errorStr = String.format("未知数据类型同步失败!错误信息:%s,时间:%s", msg, timestamp);
syncInfo.addFailInfo(errorStr);
}
}
93.备份batchDeleteSysUser
private Map<String, Object> batchDeleteSysUser(List<SysUser> sysUserList, List<User> yzjUserList, List<SysThirdAccount> sysThirdAccountList) {
// 只在 本地用户 存在
List<SysUser> onlyInSysUserList = sysUserList.stream()
.filter(sysUser -> yzjUserList.stream().noneMatch(user -> user.getJobNo().equals(sysUser.getUsername())))
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 白名单
String[] whiteIds = new String[]{"admin", "test*", "jeecg*", "ceshi*"};
// 方案1:将 用户 中的 del_flag 设置为 1,逻辑删除,放到回收站
// 删除sys_user表
// 使用 Stream 过滤不匹配白名单正则的用户
List<SysUser> filteredUserList = onlyInSysUserList.stream()
.filter(user ->
whitePatterns.stream().noneMatch(pattern -> pattern.matcher(user.getUsername()).matches())
)
.peek(user -> user.setDelFlag(1))
.collect(Collectors.toList());
// 批量更新 sys_user表
handleBatchOperation(filteredUserList, "upsertSysUserBatch");
// 方案2:将该用户的信息全部删除
// 删除sys_user表
// 使用 Stream 过滤不匹配白名单正则的元素
List<SysUser> requireDeleteUserList = onlyInSysUserList.stream()
.filter(sysUser ->
Arrays.stream(whiteIds).map(Pattern::compile).collect(Collectors.toList()).stream()
.noneMatch(pattern -> pattern.matcher(sysUser.getUsername()).matches())
)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 删除sys_third_account表
List<SysThirdAccount> requireDeleteThirdAccountList = new ArrayList<>();
if (!ObjectUtil.isEmpty(requireDeleteUserList)) {
requireDeleteThirdAccountList = requireDeleteUserList.stream()
.map(sysUser -> {
// 找到sys_third_account表该条记录
SysThirdAccount sysThirdAccount = sysThirdAccountList.stream()
.filter(user -> user.getThirdUserId().equals(sysUser.getThirdId()))
.findFirst()
.orElse(null);
// 如果为null,直接跳过
if (sysThirdAccount == null) {
return null;
}
return sysThirdAccount;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
return MapBuilder.create(new HashMap<String, Object>())
.put("sys_user", requireDeleteUserList)
.put("sys_third_account", requireDeleteThirdAccountList)
.build();
}
94.备份sysThirdAccountList更新方式
// 更新sys_third_account表
List<SysThirdAccount> requireUpdateThirdAccountList = new ArrayList<>();
if (!ObjectUtil.isEmpty(requireUpdateUserList)) {
requireUpdateThirdAccountList = requireUpdateUserList.stream()
.map(sysUser -> {
// 找到sys_third_account表该条记录
SysThirdAccount matchingUser = sysThirdAccountList.stream()
.filter(user -> user.getThirdUserId().equals(sysUser.getThirdId()))
.findFirst()
.filter(Objects::nonNull)
.orElse(null);
// 如果为null,直接跳过
if (matchingUser == null) {
return null;
}
// 合并新旧属性
SysThirdAccount sysThirdAccount = new SysThirdAccount();
BeanUtils.copyProperties(matchingUser, sysThirdAccount);
sysThirdAccount.setAvatar(sysUser.getAvatar());
sysThirdAccount.setRealname(sysUser.getRealname());
sysThirdAccount.setUpdateBy("admin");
sysThirdAccount.setUpdateTime(new Date());
return sysThirdAccount;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
95.操作日志
a.修改
延长请求时长:按钮异步,然后redis接口幂等性,执行成功,发送消息
延长token时间
b.索引
表名 名 字段 索引类型 索引方法
sys_user uniq_sys_user_work_no work_no UNIQUE BTREE 必须设置
sys_user uniq_sys_user_username username UNIQUE BTREE 必须设置
sys_user uniq_sys_user_phone phone UNIQUE BTREE 可以不设置
sys_user uniq_sys_user_email email UNIQUE BTREE 可以不设置
sys_user idx_su_status status NORMAL BTREE 必须设置
sys_user idx_su_del_flag del_flag NORMAL BTREE 必须设置
sys_user idx_su_del_username username, del_flag NORMAL BTREE 必须设置
-----------------------------------------------------------------------------------------------------
sys_third_account uniq_stat_third_type_user_id third_type, third_user_id UNIQUE BTREE 必须设置
sys_third_account uniq_sta_third_user_id_third_type third_user_id, third_type, tenant_id UNIQUE BTREE 可以不设置
sys_third_account uniq_sta_third_user_uuid_third_type third_user_uuid, third_type, tenant_id UNIQUE BTREE 可以不设置
-----------------------------------------------------------------------------------------------------
sys_postion uniq_code code UNIQUE BTREE 必须设置
-----------------------------------------------------------------------------------------------------
sys_role uniq_sys_role_role_code role_code UNIQUE BTREE 必须设置
sys_role idx_sr_role_code role_code NORMAL BTREE 建议删除,UNIQUE有NORMAL功能
-----------------------------------------------------------------------------------------------------
sys_user_role idx_sur_user_id user_id NORMAL BTREE 必须设置
sys_user_role idx_sur_role_id role_id NORMAL BTREE 必须设置
sys_user_role idx_sur_user_role_id user_id, role_id UNIQUE BTREE 必须设置
-----------------------------------------------------------------------------------------------------
sys_user_depart idx_sud_user_id user_id NORMAL BTREE 必须设置
sys_user_depart idx_sud_dep_id dep_id NORMAL BTREE 必须设置
sys_user_depart idx_sud_user_dep_id user_id, dep_id UNIQUE BTREE 必须设置
-----------------------------------------------------------------------------------------------------
sys_depart uniq_depart_org_code org_code UNIQUE BTREE 必须设置
sys_depart uniq_depart_qywx_identifier qywx_identifier、depart_name UNIQUE BTREE 必须设置
sys_depart idx_sd_parent_id parent_id NORMAL BTREE 必须设置
sys_depart idx_sd_depart_order depart_order NORMAL BTREE 必须设置
sys_depart idx_sd_org_code org_code NORMAL BTREE 建议删除,UNIQUE有NORMAL功能
-----------------------------------------------------------------------------------------------------
sys_depart_permission uniq_depart_permission depart_id、permission_id UNIQUE BTREE 必须设置
-----------------------------------------------------------------------------------------------------
sys_depart_role uniq_depart_role depart_id、role_code UNIQUE BTREE 必须设置
-----------------------------------------------------------------------------------------------------
sys_depart_role_permission idx_sdrp_role_per_id role_id、permission_id NORMAL BTREE 必须设置
sys_depart_role_permission idx_sdrp_role_id role_id NORMAL BTREE 必须设置
sys_depart_role_permission idx_sdrp_per_id permission_id NORMAL BTREE 必须设置
sys_depart_role_permission idx_sdrp_dep_role_per_id depart_id、role_id、permission_id UNIQUE BTREE 必须设置
-----------------------------------------------------------------------------------------------------
sys_depart_role_user idx_sdr_user_id user_id NORMAL BTREE 必须设置
sys_depart_role_user idx_sdr_role_id drole_id NORMAL BTREE 必须设置
sys_depart_role_user uniq_sdr_user_role_id user_id、drole_id UNIQUE BTREE 必须设置
c.模块
ThirdAppController.java
YunZhiJiaOrganizationServiceAPI.java
ThirdAppYunZhiJiaServiceImpl.java
org.jeecg.common.util.oConvertUtils
-----------------------------------------------------------------------------------------------------
LoginController.java
SysUserController.java
d.定时
http://127.0.0.1:3100/jeecgboot/sys/thirdApp/sync/yunZhiJia/user/toLocal
org\jeecg\modules\quartz\job\ScheduledSyncEjiaUser.java
6 低代码开发
6.1 表单:单表
00.表操作
a.表结构
CREATE TABLE `vilgo_leave` (
`id` varchar(36) NOT NULL,
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新日期',
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门',
`name` varchar(32) DEFAULT NULL COMMENT '姓名',
`start_time` date DEFAULT NULL COMMENT '开始时间',
`end_time` date DEFAULT NULL COMMENT '结束时间',
`type` varchar(32) DEFAULT NULL COMMENT '请假类型',
`reason` longtext COMMENT '请假事由',
`info` longtext COMMENT '说明',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
b.表说明
id 必填,主键ID
create_by 必填,创建人
create_time 必填,创建日期
update_by 必填,更新人
update_time 必填,更新日期
sys_org_code 必填,所属部门
01.新增
a.基础属性
表名:vilgo_leave
表描述:测试_请假单
表类型:单表
表单分类:测试表单
主键策略:ID_WORKER(分布式自增)
显示复选框:是
主题模板:默认主题
表单风格:一列
滚动条:有
是否分页:是
b.数据库属性
# 字段名称 字段备注 字段长度 小数点 默认值 字段类型 主键 允许空值 同步数据库
1 id 主键 36 0 String √ √
2 create_by 创建人 50 0 String √ √
3 create_time 创建日期 0 0 Datetime √ √
4 update_by 更新人 50 0 String √ √
5 update_time 更新日期 0 0 Datetime √ √
6 sys_org_code 所属部门 64 0 String √ √
7 name 姓名 32 0 String √ √
8 start_time 开始时间 0 0 Date √ √
9 end_time 结束时间 0 0 Date √ √
10 type 请假类型 32 0 String √ √
11 reason 请假事由 0 0 Text √ √
12 info 说明 0 0 Text √ √
c.页面属性
# 字段名称 字段备注 表单显示 列表显示 是否排序 是否只读 控件类型 控件长度 是否查询 查询类型 控件默认值 定义转换器 扩展参数 更多配置
1 id 主键 √ 文本框 200 普通查询
2 create_by 创建人 文本框 200 普通查询
3 create_time 创建日期 日期(年月日时分秒) 200 普通查询
4 update_by 更新人 文本框 200 普通查询
5 update_time 更新日期 日期(年月日时分秒) 200 普通查询
6 sys_org_code 所属部门 √ √ 文本框 120 普通查询
7 name 姓名 √ √ 文本框 120 √ 模糊查询
8 start_time 开始时间 √ √ 日期(年月日) 120 √ 范围查询
9 end_time 结束时间 √ √ 日期(年月日) 120 √ 范围查询
10 type 请假类型 √ √ 下拉框 120 √ 普通查询
11 reason 请假事由 √ √ 多行文本 120 普通查询
12 info 说明 √ √ 多行文本 120 普通查询
d.校验字段
# 字段名称 字段备注 字段Href 验证规则 校验必填 字典Table 字典Code 字典Text
1 id 主键 空
2 create_by 创建人 空
3 create_time 创建日期 空
4 update_by 更新人 空
5 update_time 更新日期 空
6 sys_org_code 所属部门 空
7 name 姓名 空
8 start_time 开始时间 空
9 end_time 结束时间 空
10 type 请假类型 空 vilgoLeaveType
11 reason 请假事由 空
12 info 说明 空
e.外键
# 字段名称 字段备注 主表名 主表字段
1 id 主键
2 create_by 创建人
3 create_time 创建日期
4 update_by 更新人
5 update_time 更新日期
6 sys_org_code 所属部门
7 name 姓名
8 start_time 开始时间
9 end_time 结束时间
10 type 请假类型
11 reason 请假事由
12 info 说明
f.索引
# 索引名称 索引栏位 索引类型
g.个性查询配置
# 字段名称 字段备注 控件类型 字典Table 字典Code 字典Text 默认值 是否启用
1 id 主键
2 create_by 创建人
3 create_time 创建日期
4 update_by 更新人
5 update_time 更新日期
6 sys_org_code 所属部门
7 name 姓名
8 start_time 开始时间
9 end_time 结束时间
10 type 请假类型
11 reason 请假事由
12 info 说明
02.数字字典
a.新增
字典名称:请假类型
字典编码:vilgoLeaveType
描述:请假类型
b.vilgoLeaveType
名称 数据值 颜色值 描述 排序 是否启用
日假 rj 蓝 1 启用
月假 yj 浅蓝 2 启用
年假 nj 绿 3 启用
03.菜单管理
a.示例管理
菜单类型:一级菜单
菜单名称:示例管理
访问路径:/vilgo
前端组件:layouts/default/index
组件名称:空,提示(vilgo)
默认跳转地址:空
菜单图标:ant-design:slack-square-outlined
排序:1.1
-----------------------------------------------------------------------------------------------------
是否路由菜单:是
隐藏路由:否
隐藏Tab:否
是否缓存路由:否
聚合路由:否
打开方式:否
b.示例管理 -> 测试_请假单(代码生成)
菜单类型:子菜单
菜单名称:测试_请假单
上级菜单:示例管理
访问路径:/demo11/vilgoLeaveList
前端组件:vilgo/demo11/VilgoLeaveList
组件名称:空,提示(demo11-vilgoLeaveList)
菜单图标:ant-design:file-text-outlined
排序:1
是否路由菜单:是
隐藏路由:否
隐藏Tab:否
是否缓存路由:否
聚合路由:否
打开方式:否
c.示例管理 -> 测试_请假单(在线挂载)
菜单类型:子菜单
菜单名称:测试_请假单
上级菜单:示例管理
访问路径:/online/cgformList/f34ff52b748c444f8a675d54d33f49fa
前端组件:layouts/default/index
组件名称:空,提示(/online/cgformList/f34ff52b748c444f8a675d54d33f49fa)
菜单图标:ant-design:tool-outlined
排序:3
是否路由菜单:否【在线挂载,必须选择'否'!!!】
隐藏路由:否
隐藏Tab:否
是否缓存路由:否
聚合路由:否
打开方式:否
6.2 表单:树表
6.3 表单:主子表
6.4 报表设计器
00.总结
a.位置
低代码开发 -> Online报表配置
b.录入
a.基本信息
报表编码:vilgo_leave_report
报表名字:测试_请假单_报表
动态数据源:【不需要选择】
报表SQL:SELECT name,start_time,end_time,type,reason,info FROM vilgo_leave
b.动态报表配置明细
字段名 字段文本 字段类型 是否显示 字段href 查询模式 取值表达式 字典code 分组标题 是否查询 是否合计
name 姓名 字符 √ √
type 请假类型 字符 √ vilgoLeaveType √
reason 请假事由 字符 √ √
info 说明 字符 √ √
start_time 开始时间 日期 √
end_time 结束时间 日期 √
c.报表参数
无

6.5 仪表盘设计器
00.总结
a.新建文件夹
a.位置
低代码开发 -> 仪表盘设计器
b.新建
所属文件夹:文件夹
文件夹名称:示例管理
b.全部文件夹
a.仪表盘设计
物业消防巡检状态
公司年度招聘看板
菜单示例@医美行业网络统计
我的第一个大屏
单表示例
双11
b.门户设计
无
c.示例管理
无
01.配置流程
a.新增仪表盘
名称:统计请假情况
所属文件夹:示例管理
封面图:
保护码:
b.数据源
数据源名称:MySQL5.7
数据源类型:MySQL5.7+
驱动类:com.mysql.cj.jdbc.Driver
数据源地址:jdbc:mysql://127.0.0.1:3307/jeecg-boot_20241201_online?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
用户名:root
密码:123456
c.数据集列表
a.新增分组
名称:示例管理
b.新增数据集
a.基础信息
数据集名称:测试_请假单_仪表盘
分组:示例管理
数据类型:SQL
数据源:MySQL5.7
查询SQL:SELECT * FROM vilgo_leave
b.列表字段
字段名 字段文本 字段类型 排序 是否显示 是否总计 字典Table 字典Code 字典Text
id 主键 字符 0 √ ×
create_by 创建人 字符 1 √ ×
create_time 创建日期 日期 2 √ ×
update_by 更新人 字符 3 √ ×
update_time 更新日期 日期 4 √ ×
sys_org_code 所属部门 字符 5 √ ×
name 姓名 字符 6 √ ×
start_time 开始时间 日期 7 √ ×
end_time 结束时间 日期 8 √ ×
type 请假类型 字符 9 √ × vilgoLeaveType
reason 请假事由 字符 10 √ ×
info 说明 字符 11 √ ×
c.参数字段
无
d.新建页面
a.图表 -> 饼图
a.样式
略
b.数据
数据源类型:动态数据
数据集:测试_请假单
c.映射关系
字段 映射
维度 type
数值 day

6.6 附:SpringShiro
01.常用信息
a.问题描述
我在一个Spring的项目中使用shiro搭建权限控制框架。
主要通过shiro-spring-boot-web-starter包快速集成Shiro。
但是项目无法启动,报没有authorizer的bean的错误:
-----------------------------------------------------------------------------------------------------
No bean named 'authorizer' available
我只好又在自己的Configuration中又配置了Authorizer,才能正常启动。
@Configuration
public class ShiroConfig {
@Bean
public Authorizer authorizer(){
return new ModularRealmAuthorizer();
}
}
-----------------------------------------------------------------------------------------------------
但是奇怪的明明athorizer是SecurityManager中一个重要的组件,
为什么没有在shiro starter的Configuration中被声明为Bean?同样的,Authenticator就没问题
b.明确shiro-spring-boot-web-starter是否有对应的声明
a.介绍
我们在pom文件中声明了shiro-spring-boot-web-starter。就从对应的jar包开始找起。
首先是META-INF中的spring.factories文件。
我们知道spring-boot-starter都是通过在该文件中声明Configuraion来达到集成自身配置的目的。
-------------------------------------------------------------------------------------------------
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,\
org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration
上述声明了两个Configration:ShiroWebAutoConfiguration和ShiroWebFilterConfiguration。
b.ShiroWebFilterConfiguration
先从简单的配置说起,ShiroWebFilterConfiguration是以添加Filter的方式来达到authentication的目的。
这个和我们的问题无关,简单带过。
c.ShiroWebAutoConfiguration
@Configuration
@AutoConfigureBefore(ShiroAutoConfiguration.class)
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
@Bean
@ConditionalOnMissingBean
@Override
protected AuthenticationStrategy authenticationStrategy() {
return super.authenticationStrategy();
}
@Bean
@ConditionalOnMissingBean
@Override
protected Authenticator authenticator() {
return super.authenticator();
}
@Bean
@ConditionalOnMissingBean
@Override
protected Authorizer authorizer() {
return super.authorizer();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SubjectDAO subjectDAO() {
return super.subjectDAO();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionStorageEvaluator sessionStorageEvaluator() {
return super.sessionStorageEvaluator();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SubjectFactory subjectFactory() {
return super.subjectFactory();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionFactory sessionFactory() {
return super.sessionFactory();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionDAO sessionDAO() {
return super.sessionDAO();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionManager sessionManager() {
return super.sessionManager();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionsSecurityManager securityManager(List<Realm> realms) {
return createSecurityManager();
}
@Bean
@ConditionalOnMissingBean(name = "sessionCookieTemplate")
@Override
protected Cookie sessionCookieTemplate() {
return super.sessionCookieTemplate();
}
@Bean
@ConditionalOnMissingBean
@Override
protected RememberMeManager rememberMeManager() {
return super.rememberMeManager();
}
@Bean
@ConditionalOnMissingBean(name = "rememberMeCookieTemplate")
@Override
protected Cookie rememberMeCookieTemplate() {
return super.rememberMeCookieTemplate();
}
@Bean
@ConditionalOnMissingBean
@Override
protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
return super.shiroFilterChainDefinition();
}
}
d.分析
大致内容其实和ShiroWebAutoConfiguration很类似,只是ShiroWebAutoConfiguration将一些组件替换成了
WEB环境相关的组件。但是ShiroWebAutoConfiguration声明了它的配置要在ShiroAutoConfiguration之前,
而且根据ConditionalOnMissingBean的条件,得出Bean的配置应该是以ShiroWebAutoConfiguration中声明的为准。
但是死马当活马医,配置文件中添加shiro.enabled为false的条件,再试试。。。果然还是不行。
c.DEBUG
a.介绍
毫无办法的办法就是DEBUG大法。
首先从Configuration中生命的Bean是如何被容器加载的过程入手,找到了ConfigurationClassPostProcessor。
同样是一个PostProcessor,猜想应该是在configuration bean的后置处理中进行了@Bean方法的解析。
主要的处理过程在processConfigBeanDefinition这个方法中,对这个方法做个简单的说明
b.细节
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
//获取registry中的bean definition
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
//bean definition 有configuration的属性,说明已经被解析处理过
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
//判断是否是configuration的bean,是则加入候选
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// 如果没有发现候选者,则返回
if (configCandidates.isEmpty()) {
return;
}
// 排序
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});
// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}
if (this.environment == null) {
this.environment = new StandardEnvironment();
}
// 开始解析configuration 的bean definition
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// 如果候选者不为空,则继续解析
do {
// 解析过程
parser.parse(candidates);
// 校验
parser.validate();
// 获取新解析的config class
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
// 移除掉已经解析过的部分
configClasses.removeAll(alreadyParsed);
// 创建reader,添加bean definition
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
candidates.clear();
//如果bean definition数量 大于 候选者的数量,说明有新的bean加入
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
//不在旧的candidate中,说明是新加入的
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
//未被解析的config class,添加到candidates中,等下一轮解析
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
//更新候选者
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}
if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}
c.操作
1)parser.parse后设置断点,看ConfigurationClassParser是否能将ShiroWebAutoConfiguration中的@Bean正常的解析
可以看到authorizer确实已经被ShiroWebAutoConfiguration加载。
2)解析没问题,那就看加载是否成功:
继续往下走,看reader.loadBeanDefinitions发生了什么:
找出ShiroWebAutoConfiguration对应的ConfigurationClass,看到SkippedBeanMethods中有authorizer!!!
也就是说虽然解析出了authorizer,但是在加载的时候却被选择跳过了。。。
3)问题就变得比较清晰了,找出为什么被跳过的原因
顺着代码找到ConfigurationClassBeanDefinitionReader的loadBeanDefinitionsForConfigurationClass的方法,
负责处理的BeanMethond的过程是在loadBeanDefitionsForBeanMethod中。
确实在方法开始前,有一个判断是否需要跳过的条件:
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}
shouldSkip这个方法是根据@Bean上的@Conditional注解,来判断是否需要加载该Bean。
回忆上文我们的ShiroWebAutoConfiguration中,确实在authorizer的方法上有@ConditionalOnMissingBean的注解。
也就是说应该是哪里声明authorizer的Bean,导致配置中的Bean没有被加载。
4)OnBeanCondition.getMatchOutcome():处理@Bean的@Condtional条件,并输出结果。
最后发现被跳过的原因竟然是
found beans of type 'org.apache.shiro.authz.Authorizer' authorizer, thirdPartyRealm, userRealm
我自定义的Realm竟然和authorizer冲突了。Spring认为已经有authorizer的bean,而不再加载配置中的authorizer。
5)为什么Realm和authorizer冲突?原来在获取相匹配的Bean时候还是通过容器本身(BeanFactory)的getNamesForType方法
Set<String> getNamesForType(Class<?> type) {
updateTypesIfNecessary();
//便利容器中所有的bean类型,将类型匹配的Type全部返回。注意这里还用了isAssiginableFrom,因此这里的查询类型的子类也会满足
return this.beanTypes.entrySet().stream()
.filter((entry) -> entry.getValue() != null
&& type.isAssignableFrom(entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
反观我们的Realm对象:AuthorizingRealm实现了Authorizer接口。真相大白。
02.常用信息
a.shiro-spring-boot-starter-1.12.0,spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,\
org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration,\
org.apache.shiro.spring.config.web.autoconfigure.ShiroWebMvcAutoConfiguration,\
org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration,\
org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration,\
org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer = \
org.apache.shiro.spring.boot.autoconfigure.ShiroNoRealmConfiguredFailureAnalyzer
org.springframework.boot.env.EnvironmentPostProcessor=\
org.apache.shiro.spring.config.web.autoconfigure.ShiroEnvironmentPostProcessor
b.目前配置
a.位置
pisces-boot-base-core\src\main\java\cn\myslayers\common\aspect\AutoLogAspect
pisces-boot-base-core\src\main\java\cn\myslayers\common\constant\CommonConstant.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\exception\PiscesBootExceptionHandler.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\system\base\controller\PiscesController.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\system\util\JwtUtil.java
pisces-boot-base-core\src\main\java\cn\myslayers\common\util\encryption\AesEncryptUtil.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\PiscesBaseConfig.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\mybatis\MybatisInterceptor.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\shiro\JwtToken.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\shiro\ShiroConfig.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\shiro\ShiroRealm.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\shiro\filters\CustomShiroFilterFactoryBean.java:
pisces-boot-base-core\src\main\java\cn\myslayers\config\shiro\filters\JwtFilter.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\shiro\filters\ResourceCheckFilter.java
pisces-boot-base-core\src\main\java\cn\myslayers\config\vo\Shiro.java
pisces-boot-base-core\src\main\java\cn\myslayers\modules\base\service\impl\BaseCommonServiceImpl.java
b.jeecg-boot-starter-3.4.3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.jeecg.boot.starter.lock.config.RedissonConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.jeecg.boot.starter.job.config.XxlJobConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.jeecg.boot.starter.job.config.XxlJobConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.jeecg.boot.starter.lock.config.RedissonConfiguration
c.pisces-boot-starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.jeecgframework.minidao.auto.MinidaoAutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.jeecg.modules.jmreport.config.init.JimuReportConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.myslayers.boot.starter.lock.config.RedissonConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.myslayers.boot.starter.lock.config.RedissonConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.myslayers.boot.starter.job.config.XxlJobConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.myslayers.boot.starter.job.config.XxlJobConfiguration
c.报错1
a.报错
Please create bean of type 'Realm' or add a shiro.ini in the root classpath
(src/main/resources/shiro.ini) or in the META-INF folder (src/main/resources/META-INF/shiro.ini).
b.解决
解决在主启动类上,添加@ComponentScan(basePackages = {"cn.myslayers"}),扫描该路径,进行装配Bean
@Slf4j
@EnableScheduling
@SpringBootApplication
@ComponentScan(basePackages = {"org.jeecg", "cn.myslayers"})
public class PiscesSystemApplication extends SpringBootServletInitializer {
d.报错2
a.报错
Description:
Parameter 0 of method authorizationAttributeSourceAdvisor in
org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration
required a bean named ‘authorizer’ that could not be found.
Action:
Consider defining a bean named ‘authorizer’ in your configuration.
b.分析
报错的意思是,在类 ShiroAnnotationProcessorAutoConfiguration中的这个方法的第0个参数,
也就是securityManager ,缺少一个叫authorizer 的 Bean
-------------------------------------------------------------------------------------------------
因为依赖注入,这个方法需要参数securityManager,但是发现还没有这个Bean,于是进行实例化这个Bean,
但是在实例化的时候,又依赖了一个叫 authorizer 的Bean,但发现无法实例化 authorizer ,于是报错。
通过调试可以看到,报错具体在类AbstractShiroConfiguration中
-------------------------------------------------------------------------------------------------
理论上说,authorizer 是 Shiro 中很重要的一个 Bean,用于登录认证,Springboot 肯定会帮我们注入的。
查找一下,发现在ShiroWebAutoConfiguration这个类中,有自动注入。
-------------------------------------------------------------------------------------------------
事实上,因为有 @ConditionalOnMissingBean 注解,意思是如果已存在这个类型的 Bean ,
就不执行本方法。这个方法没有得到真正的执行。
-------------------------------------------------------------------------------------------------
实际上这个时候如果你对Spring的 Bean加载很熟悉的话,完全可以debug进去,
看看为什么会出现已经有这个类型的 Bean 了。非常详细,参见:
https://blog.csdn.net/weixin_34289454/article/details/94562045
-------------------------------------------------------------------------------------------------
结论就是,因为你自定义的 Realm 间接地实现了 Authorizer 接口,所以它是 Authorizer 类型的 Bean。
所以刚才那里,@ConditionalOnMissingBean 会查到,已经有这个类型的 Bean了,不再注入了。
但问题是,为什么刚才打断点的地方:
securityManager.setAuthorizer(authorizer());
中的 authorizer() 方法不执行?实际上是因为Spring的代理类去执行,会找名为 authorizer 的 Bean,
发现没有,才报错。
c.解决
a.方法一:手动注入这个Bean
@Configuration
public class ShiroConfig {
@Bean
public Authorizer authorizer(){
return new ModularRealmAuthorizer();
}
}
b.方法二:不用Springboot自动注入的sercurityManager了,我自己在 ShiroConfig中注入。
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(adminLoginRealm()) ;
}
c.方法三:我把自己写的Realm的Bean名字定为"authorizer"
@Component("authorizer")
public class AdminLoginRealm m extends AuthorizingRealm {
6.7 附:quartz定时任务
01.实战使用
a.位置
jeecg-server\jeecg-module-system\jeecg-system-biz\src\main\java\org\jeecg\modules\quartz
b.目录
controller
QuartzJobController.java
entity
QuartzJob.java
job
AsyncJob.java
SampleJob.java
SampleParamJob.java
mapper
xml
QuartzJobMapper.xml
QuartzJobMapper.java
service
impl
QuartzJobServiceImpl.java
IQuartzJobService.java
c.org.jeecg.modules.quartz.job.AsyncJob
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
@Slf4j
public class AsyncJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info(" --- 同步任务调度开始 --- ");
try {
//此处模拟任务执行时间 5秒 任务表达式配置为每秒执行一次:0/1 * * * * ? *
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//测试发现 每5秒执行一次
log.info(" --- 执行完毕,时间:"+DateUtils.now()+"---");
}
}
d.org.jeecg.modules.quartz.job.SampleJob
@Slf4j
public class SampleJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info(" Job Execution key:"+jobExecutionContext.getJobDetail().getKey());
log.info(String.format(" Jeecg-Boot 普通定时任务 SampleJob ! 时间:" + DateUtils.getTimestamp()));
}
}
e.org.jeecg.modules.quartz.job.SampleParamJob
@Slf4j
public class SampleParamJob implements Job {
/**
* 若参数变量名修改 QuartzJobController中也需对应修改
*/
private String parameter;
public void setParameter(String parameter) {
this.parameter = parameter;
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info(" Job Execution key:"+jobExecutionContext.getJobDetail().getKey());
log.info( String.format("welcome %s! Jeecg-Boot 带参数定时任务 SampleParamJob ! 时间:" + DateUtils.now(), this.parameter));
}
}