# jeedp-report
**Repository Path**: ydc123/jeedp-report
## Basic Information
- **Project Name**: jeedp-report
- **Description**: java快速开发平台-报表平台版
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-09
- **Last Updated**: 2026-09-14
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Jeedp 快速开发框架
**Java 企业级快速开发平台 | 开箱即用 | 多端统一**
---
## 简介
Jeedp 是一套基于 Spring Boot 的企业级快速开发平台,提供完整的用户权限管理、数据权限控制、操作审计、系统监控等基础能力。前端采用 Vue 3 + Arco Design,移动端采用 UniApp,实现 Web 端与移动端统一架构。
## 技术栈
| 层级 | 技术 | 版本 |
|------|------|------|
| 后端框架 | Spring Boot | 2.7.18 |
| 权限认证 | Sa-Token | 1.46.0 |
| ORM | MyBatis-Plus | 3.5.17 |
| 连接池 | Druid | 1.2.28 |
| 缓存 | Redis | - |
| 配置加密 | Jasypt | 3.0.5 |
| 工具库 | Hutool | 5.8.47 |
| JSON | Fastjson2 | 2.0.65 |
| 系统监控 | OSHI | 6.12.0 |
| 表格导出 | Apache Fesod (incubating) | 2.0.1-incubating |
| 报表引擎 | 积木报表 JimuReport | 2.3.4 |
| 大屏/仪表盘 | 积木BI JimuBI | 2.3.2.1 |
| Web 前端 | Vue 3 + Arco Design | Vue 3.4 / Arco 2.56 |
| 移动端 | UniApp | Vue 3 |
| 构建工具 | Vite | 5.3 |
| 数据库 | PostgreSQL | - |
| JDK | JDK | 1.8 |
## 功能模块
### 系统管理
| 模块 | 说明 |
|------|------|
| 用户管理 | 系统用户配置,支持数据权限过滤 |
| 机构管理 | 组织机构树形维护(公司/部门/小组) |
| 岗位管理 | 用户职务配置 |
| 角色管理 | 角色权限分配、数据范围划分、子角色配置 |
| 菜单管理 | 系统菜单、路由、按钮权限标识配置 |
| 字典管理 | 系统常用固定数据维护 |
| 参数管理 | 系统动态配置参数 |
### 系统监控
| 模块 | 说明 |
|------|------|
| 在线用户 | 当前活跃用户状态监控,支持强退 |
| 定时任务 | 在线添加/修改/删除任务调度,含执行日志 |
| 操作日志 | 系统正常操作与异常信息日志记录 |
| 登录日志 | 登录记录查询,包含异常登录信息 |
| 服务监控 | CPU、内存、磁盘、JVM 等系统信息 |
| 缓存监控 | 系统缓存信息查询与命令统计 |
### 通知公告
系统公告信息发布与维护,支持已读状态追踪。
### 报表中心
| 模块 | 说明 |
|------|------|
| 报表设计 | 积木报表在线设计器(`/jmreport/list`),数据源/数据集/导出 |
| 大屏设计 | 积木BI 页面设计器(`/drag/list`),仪表盘与大屏 |
| 报表挂菜单 | 菜单管理里直接选用报表/大屏作为内链菜单,自动回填路由、内链地址与权限标识 |
| 入口鉴权 | 预览页按 `jmreport:view:{id}` / `drag:view:{id}` 判定,拿到 URL 也无法打开 |
## 项目结构
```
jeedp-report/
├── jeedp-report-server/ # 后端服务(Spring Boot)
│ ├── src/main/java/com/jeedp/
│ │ ├── common/ # 公共模块(工具类、注解、异常、常量)
│ │ ├── config/ # 配置类(拦截器、数据源、加密等)
│ │ ├── framework/ # 框架集成(Sa-Token 权限实现)
│ │ ├── monitor/ # 监控模块(在线用户、任务调度、服务监控)
│ │ ├── report/ # 积木报表集成(资源只读查询、入口鉴权、地址约定)
│ │ └── system/ # 系统管理模块(用户、角色、菜单、机构等)
│ ├── sql/ # 数据库初始化脚本(单文件版,内部分 4 段)
│ ├── tools/ # 构建期校验工具(字节码链接检查等)
│ └── pom.xml
├── jeedp-report-web/ # Web 管理端(Vue 3 + Arco Design)
│ ├── src/
│ │ ├── api/ # 接口请求
│ │ ├── views/ # 页面组件
│ │ ├── layout/ # 布局组件
│ │ ├── router/ # 路由配置
│ │ └── store/ # 状态管理(Pinia)
│ └── package.json
├── jeedp-app-uniapp/ # 移动端(UniApp + Vue 3)
│ ├── pages/ # 页面
│ ├── api/ # 接口请求
│ ├── store/ # 状态管理(Pinia)
│ └── manifest.json
└── jeedp-report-doc/ # 项目文档
```
## 快速开始
### 环境要求
- JDK 1.8+
- PostgreSQL 12+
- Redis 6+
- Node.js 16+
- Maven 3.6+
### 1. 克隆项目
```bash
git clone <仓库地址>
```
### 2. 初始化数据库
```bash
# 建库(库名含连字符必须加引号,且与 application-dev.yml 里的 url 保持一致)
psql -U postgres -c "CREATE DATABASE \"jeedp-report\" WITH ENCODING 'UTF8'"
# 一个脚本建好全部对象:jeedp 业务表 + 积木报表/积木BI 23 张表 + 报表中心菜单
psql -U postgres -d jeedp-report -f jeedp-report-server/sql/jeedp_report_full_postgres.sql
```
脚本内部分 4 段,老库可以只跑需要的段落:
| 段 | 内容 | 能不能重复执行 |
|----|------|----------------|
| 1 | jeedp 业务表 `sys_*`(自带 `DROP TABLE IF EXISTS`) | 会把 `sys_*` 现有数据一并清掉,**只在全新库跑** |
| 2 | 积木报表 / 积木BI 共 23 张表 | `IF NOT EXISTS`,不动已设计好的报表 |
| 3 | 报表中心菜单(`sys_menu`,内链菜单 + 权限码) | `ON CONFLICT DO NOTHING`,可重复跑 |
| 4 | 积木表的 DROP 语句 | **整段注释**,仅重建列类型时手动放开(会删光报表/大屏定义) |
> 积木报表的列类型必须与实体字段一致:字符串列一律 `text`。若建成 `int4`,PostgreSQL 会在保存数据集时
> 报 `column "db_type" is of type integer but expression is of type character varying`,随后同一事务里的
> 语句全部变成 `SQLSTATE 25P02(current transaction is aborted)`。
> 列类型是怎么从厂商 jar 里推出来的,第 2 段开头的注释写了完整依据;改列类型时按第 4 段先删再建。
### 3. 启动后端
```bash
cd jeedp-report-server
# 方式一:IDEA 直接运行(需配置环境变量)
# 环境变量:JASYPT_ENCRYPTOR_PASSWORD=jeedp2026!
# 方式二:命令行启动
mvn spring-boot:run -Dspring-boot.run.arguments="--jasypt.encryptor.password=jeedp2026!"
```
后端服务默认启动在 `http://localhost:8080`
### 4. 启动 Web 前端
```bash
cd jeedp-report-web
npm install
npm run dev
```
Web 前端默认启动在 `http://localhost:5173`
### 5. 启动移动端(可选)
使用 HBuilderX 打开 `jeedp-app-uniapp` 目录,运行到浏览器或模拟器。
### 默认账号
| 账号 | 密码 | 角色 |
|------|------|------|
| admin | admin123 | 超级管理员 |
## 核心特性
### 权限体系
- **接口权限**:基于 Sa-Token 的 `@SaCheckPermission` 注解,按钮级权限控制
- **数据权限**:`@DataScope` 注解 + AOP 切面,支持全部/自定义/机构/本人等 5 种数据范围
- **子角色机制**:支持角色层级配置,用户管理时自动按子角色范围过滤可选角色
- **超级管理员**:admin 账号自动拥有通配权限 `*:*:*`
### 积木报表与角色授权
报表本身存在厂商自有的表里(`jimu_report`、`onl_drag_page`),jeedp 只读不写。
所谓「把报表挂进系统」就是建一条**内链菜单**:`component=InnerLink` + `is_frame='0'`(是否外链「是」),
后端 `SysLoginController#getInnerLink` 会把 `query` 字段当作 iframe 地址下发到路由 `meta.link`,
前端 `views/InnerLink.vue` 拼上登录 token 后内嵌打开。
#### 前置(一次性)
1. 执行 `sql/jeedp_report_full_postgres.sql`(已跑过初始化就不用重复执行):
其中第 2 段是 23 张厂商表,第 3 段建出「报表中心」目录 + 报表设计器(`/jmreport/list`)
+ 大屏设计器(`/drag/list`)三个菜单。
2. 后端重启、前端 `npm run dev`(改过 Java/前端必须重启,浏览器再 Ctrl+F5 硬刷新一次)。
3. 从「报表中心 → 积木报表」进设计器把报表做好并**保存**——要挂的是已存在的资源,不是草稿。
#### 一、挂一张报表(推荐用弹窗,不用手抄 id)
`系统管理 → 菜单管理 → 新增`,菜单类型先选**菜单**,然后点「路由参数」输入框右侧的
**选择积木报表**,在弹窗里按名称/类型(积木报表、积木BI 大屏)检索并「选用」,
以下字段会被自动回填:
| 字段 | 回填值 |
|------|--------|
| 菜单类型 | `C`(菜单) |
| 组件路径 | `InnerLink` |
| 是否外链 | 是(`is_frame='0'`) |
| 路由地址 | `jmreport-view-{id}` / `drag-view-{id}`(保证同父级下不重复) |
| 路由参数 | 报表 `/jmreport/view/{报表id}`;大屏 `/drag/view?pageId={大屏id}` |
| 权限标识 | `jmreport:view:{报表id}`;大屏 `drag:view:{大屏id}` |
再补一个菜单名称与显示排序,保存。(菜单名称建议与报表同名,方便回溯。)
#### 二、手工填(不用弹窗时的等价做法)
三个硬性条件必须同时满足,少一个后端就不认它是内链(`isInnerLink` 判定):
```
menu_type = C 菜单
component = InnerLink
is_frame = 0 是否外链选「是」——与 component 一起才构成内链
query = /jmreport/view/{报表id} 或 /drag/view?pageId={大屏id}
perms = jmreport:view:{报表id} 或 drag:view:{大屏id}
```
`perms` 漏填也没关系:新增/修改菜单时 `JimuResourceService#normalizeInnerLinkPerms`
会从 `query` 反推资源并补上(为空则填、已有则追加、已含则不动)。
#### 三、授权与生效
1. `系统管理 → 角色管理 → 菜单权限` 勾选这条菜单,保存。
2. **该用户重新登录**(路由与权限是登录时算的),侧边栏就会出现这张报表。
3. 点开即是内嵌的预览页,带宿主登录态,不需要二次登录积木。
权限码的含义(`JimuViewAuthInterceptor` 按此判定):
| 权限码 | 给了谁能干什么 |
|--------|----------------|
| `jmreport:view:{id}` | 只能看这一张报表;拿到别人的 URL 也打不开(403 提示页) |
| `drag:view:{id}` | 只能看这一个大屏 |
| `jmreport:list` | 报表设计器入口,能看/改**任意**报表——只给设计员 |
| `drag:list` | 大屏设计器入口 |
| admin(`user_id=1`) | 全部放行,无需授权 |
批量初始化可以直插 `sys_menu` + `sys_role_menu`(参照 `sql/jeedp_report_full_postgres.sql`
第 3 段末尾的授权示例),效果与界面上操作一致。
几个必须知道的边界:
- **不只是“菜单可不可见”**:Sa-Token 对 `/jmreport/**`、`/drag/**` 整段放行,厂商只校验登录态;
`JimuViewAuthInterceptor` 补上了入口级判定,拿到 URL 直接访问也会返回 403 页面。
拥有设计器权限(`jmreport:list` / `drag:list`)的人可直接看任意报表,否则设计员点预览会被自己拦下。
- **只管“进页面”这一层**:厂商把 `/jmreport/show`、`getCharData` 等取数接口列为免登白名单,
本层不改它们,因此这是**页面级**而非**数据接口级**的管控;需要按行过滤数据时仍要用报表自身的数据集参数。
- **资源查询接口的位置是有意的**:报表/大屏列表挂在 `/system/jimuResource/**` 而不是 `/jmreport/**` 下,
后者在 Sa-Token 白名单里,挂上去等于绕过鉴权;该接口只读厂商表(`jimu_report` / `onl_drag_page`),不写。
- **排障开关**:`jeedp.jimu.view-auth-enabled: false` 可临时关掉这层拦截(只保留登录态校验)。
- **单点登录靠三种携带方式,缺一不可**:内链页以 iframe 打开,`InnerLink.vue` 会把 token
① 拼到 iframe 地址的 `?token=` 上(入口页自身请求靠它);② 写进同源 localStorage 的
`JmReport-Access-Token`(厂商 `core/request.js` 只从这个键取值拼 `X-Access-Token` 头,
报表设计器 `design.js` 也只读不写,所以必须由宿主系统塞进去);③ 写成同源 Cookie
(会话级,不设 expires),给 iframe 直指后端端口、读不到宿主 localStorage 的场景兜底。
后端 `JimuViewAuthInterceptor` 在任何入口页请求上都会把带过来的 token 回写一次 `token` Cookie
(与权限判定无关,`view-auth-enabled: false` 时照旧生效),
保证从 `:8080/jmreport/list?token=xxx` 直接打开时,页内 ajax 也带得上登录态。
退出登录时 `removeToken()` 会连带清掉 `JmReport-Access-Token`,避免下一个人沿用上一个账号的凭证。
- **厂商日志 `Token校验失败!请求无权限(GET):/jmreport/xxx` 先看我们补的那行 WARN**:
厂商只说“失败”,不说原因。`JimuReportTokenServiceImpl#verifyToken` 会补一条
`积木报表鉴权失败:请求未携带 token,GET /jmreport/userinfo:X-Access-Token 头缺失/Authorization 头缺失/token 参数缺失/Cookie缺失`,
按它分流即可:**各来路都缺失** → iframe 地址没带 `?token=`、或宿主系统与报表页不同源且 Cookie 被挡;
**token 已携带但未通过校验** → 拿的是别的后端/别处登录发的 token(Redis 里查不到)、
或已过 `sa-token.active-timeout`(当前 1800 秒)被冻结,重新登录即可;
入口页 `401 提示页`与它是同一个原因的两个表现。
- **报表设计器报 `getReportConfigJson is not defined` 时先分清是谁的锅**:该函数不在任何 .js 里
(已逐字节扫过 starter jar),而是厂商在服务端渲染页面时写进内联脚本的
`function getReportConfigJson(){ let str = '${reportConfig}'; return JSON.parse(str) }`,
`reportConfig` 即 `JmReportBaseConfig`(`jeecg.jmreport.*`)的 JSON。因此报错只有两种来源:
控制台**首个**错误是 `SyntaxError` → 某个配置值里带了单引号或换行,把整段内联脚本打断;
只有 `ReferenceError` → 浏览器拿到的 HTML 根本不是完整的设计器页(代理指向了别的后端、
被入口拦截器换成提示页、或模板渲染中途抛异常导致 HTML 截断)。
本地同时留着两个工程时最容易碰到这种:`vite` 代理固定转发 `localhost:8080`,
若该端口上跑的不是本工程,`/jmreport/**` 会直接 500。
#### 挂完不生效,按这个顺序查
1. **侧边栏没有该菜单** → 角色没勾这条菜单,或用户没重新登录(路由是登录时下发的)。
2. **点开是 403 提示页** → 该用户既没有 `jmreport:view:{id}` 也没有 `jmreport:list`;
确认方法是看后端日志 `用户 xxx 试图访问 /jmreport/view/…,缺少权限 […],已拒绝`。
3. **点开白屏 / 报 `Token校验失败`** → 对应上面两条边界(token 三种携带方式 + 新增的 WARN 分流),
先硬刷新 + 重新登录,再看后端那行 `积木报表鉴权失败:…` 给的结论。
4. **报表在设计器里被删了、或另存/复制成了新报表** → 资源 id 变了,菜单的 `query` 与 `perms`
要重新点一次「选用」回填(菜单不会自动跟随)。
5. **想在一张页里放多张报表** → 每张建一条菜单(一表一权限码是这套集成的授权粒度),
或者在大屏里组合后用 `drag:view:{id}` 挂大屏。
#### 报表自身的两个坑:分页与导出
这两条症状看着像集成出的问题,其实全在厂商自己的配置与数据集上,与宿主系统无关:
- **预览「每页显示条数」不生效、一屏出全部** → 分页开关在**数据集**上:厂商取数时判
`"1".equals(jimu_report_db.is_page)`,只有勾了「分页」才会拼分页 SQL(PostgreSQL 在 `dyndb` 方言表里是支持的),
否则一次返回全部行,这个下拉框怎么改都没用。在设计器里给对应数据集勾上分页即可;
同时 `jeecg.jmreport.pageSize`(分页条候选条数,厂商**无默认值**)要配上,否则分页条是空的。
- **导出 Excel 报 `导出失败:undefined`,后端 `JmReportExportExcelImpl` 抛 NPE** → 先查
`jeecg.jmreport.exportExcelPattern` 是否填了非法值:厂商只认 `fast`(默认)/ `primary` / 留空,
代码里只有这三个取值对应的分支会创建 Sheet,填其它值时 Sheet 为 null,导出就 NPE。
另:导出用 `pageSizeNumber`(默认 10000)且前端写死 `pageSize=1E4`,
所以**导出总是全量**,与预览分页无关,这不是 bug。
### 安全防护
- **配置加密**:数据库密码、Redis 密码等敏感配置采用 Jasypt AES 加密存储
- **敏感字段加密**:手机号、邮箱等字段数据库存储密文,查询时自动加解密
- **脱敏显示**:列表接口自动脱敏手机号、邮箱等敏感信息
- **密码策略**:支持密码强度校验、登录失败锁定、密码过期提醒
### 依赖治理
- **版本天花板(JDK 1.8)**:本工程运行于 JDK8,依赖不能盲升「仓库最新版」——高版本发行包的字节码可能是
Java9/Java17,装上就是 `UnsupportedClassVersionError`。用 `tools/check-class-version.ps1` 读 class 主版本号,
当前实测上限:`oshi 6.12.0`(7.x 起为 Java9)、`yauaa 7.32.0`(8.x 起为 Java17)。
`module-info.class` 与 `META-INF/versions/` 下的内容已排除(JDK8 不加载它们,否则几乎每个 jar 都会报假警)。
改依赖后可直接扫「产品里实际带的每个 jar」:
```powershell
powershell -File jeedp-report-server/tools/check-class-version.ps1 com.github.oshi:oshi-core:6.12.0
# 全量扫打包产物(先跑链接校验把依赖解包到 target\linkcheck\lib)
dir jeedp-report-server\target\linkcheck\lib\*.jar | % FullName | set-content jeedp-report-server\target\libs.txt
powershell -File jeedp-report-server/tools/check-class-version.ps1 -ListFile jeedp-report-server/target/libs.txt
```
- **版本对齐**:直接声明的依赖会以「最近路径优先」顶掉第三方 starter 的传递依赖,编译与启动都不报错,
只在功能被触发时抛 `NoSuchMethodError`。故对易被厂商传递引入的库统一在 `dependencyManagement` 钉版本:
`fastjson2` / `fastjson2-extension` / `com.alibaba:fastjson` 三件套必须同版本(现 2.0.65,压到 2.0.52 会让报表
预览因缺少 `ObjectWriterProvider#clear()` 而失败);`poi` / `poi-ooxml` / `poi-ooxml-full` / `poi-scratchpad`
四件套拉平到 5.2.5(`poi-scratchpad 5.4.1` 引用了 5.3.0 才有的 `org.apache.poi.logging.PoiLogManager`)。
- **空 jar 不一定是依赖丢了**:Sa-Token 1.44+ 把 Redis 集成拆到了 `sa-token-redis-template` + `sa-token-jackson`,
`sa-token-redis-jackson` 只剩一个不带任何 class 的聚合包(实现换为 `SaTokenDaoForRedisTemplate`)。
它自建 `StringRedisTemplate`(不走业务侧 `RedisTemplate`),对象序列化改由 `SaJsonTemplateForJackson` 完成,
所以**存储格式与 1.39 不兼容**:升级上线后请清掉 Redis 里旧的 `satoken:*` 键(否则旧会话读不出,
表现是登录态异常而不是报错)。
- **字节码链接校验**:`tools/run-method-link-check.ps1` 解包 `target/jeedp-report-server.jar`,读取各 jar 常量池中的
字段/方法引用,逐一比对运行期 classpath 上是否真实存在,可一次性排查同类版本劈叉。
```powershell
powershell -File jeedp-report-server/tools/run-method-link-check.ps1 # 打包并全量扫描
powershell -File jeedp-report-server/tools/run-method-link-check.ps1 -SkipBuild # 复用已有 jar
# 只关心某些包时显式传入;把已移除的库也列进来,可用「引用数为 0」反证它确实不再被需要
powershell -File jeedp-report-server/tools/run-method-link-check.ps1 -SkipBuild `
-Packages "com.alibaba.fastjson2,org.apache.poi,com.alibaba.fastjson.,net.sf.jsqlparser,org.apache.ibatis,com.github.pagehelper,com.baomidou.mybatisplus"
```
### 分页查询
分页统一由 MyBatis-Plus 的 `PaginationInnerInterceptor` 承担,**不再引入 PageHelper**;`jsqlparser` 也不单独声明,
由 `mybatis-plus-jsqlparser-4.9` 带入(4.9 与 `minidao-spring-boot-starter-jsqlparser-4.9` 的编译版本一致,
若另引一份会同时存在两个 `net.sf.jsqlparser`)。注意 MyBatis-Plus 3.5.9 起做过模块拆分:
`PaginationInnerInterceptor` 位于 `mybatis-plus-jsqlparser-4.9`,`IService`/`ServiceImpl` 位于 `mybatis-plus-spring`
的 `com.baomidou.mybatisplus.spring.service[.impl]`。
新增一个列表接口的写法(与代码生成器模板一致):
```java
// Controller:只负责组装,分页结果里已带 total
public TableDataInfo list(XxxQuery query, PageQuery pageQuery) {
return getDataTable(xxxService.selectXxxPage(query, pageQuery));
}
// Service:PageQuery.build() 产生分页页,PageQuery.unpaged() 是「取全量」令牌(size < 0 时插件跳过 count 与 limit)
public IPage selectXxxPage(XxxQuery query, PageQuery pageQuery) {
Page page = pageQuery.build();
page.setRecords(baseMapper.selectXxxList(query, page));
return page;
}
// Mapper:分页参数以 @Param("page") 传入,XML 中所有取值都要带 query. 前缀
List selectXxxList(@Param("query") XxxQuery query, @Param("page") IPage page);
```
- **必须带 `query.` 前缀**:mapper 方法一旦有多个参数,MyBatis 就会改用 ParamMap,裸写 `#{configName}` 会
**静默解析成 null**——条件消失、列表返回全量,却不抛任何异常。`tools/check-query-params.ps1` 用于守住这条约束。
- **不要设置 `maxLimit`**:分页插件在 `size < 0`(不分页)时若 `maxLimit` 非空会把它当成页大小,导出这类
「一次取全量」的查询会被静默截断。
- **越界行为**:`MybatisPlusConfig` 开了 `setOverflow(true)`,页码越界回到**第一页**(PageHelper 的 `reasonable`
是回到最后一页),迁移时不必再单独配置。
```powershell
powershell -File jeedp-report-server/tools/check-query-params.ps1
```
### 操作审计
- **操作日志**:`@Log` 注解自动记录增删改操作,含请求参数和响应结果
- **登录日志**:记录登录 IP、浏览器、操作系统、登录状态
- **在线用户**:实时查看当前活跃会话,支持强制下线
## 相关文档
- [后端使用说明](jeedp-report-server/README.md)
- [Web 前端说明](jeedp-report-web/README.md)
## License
本项目仅供学习和内部使用。