若依(ruoyi)前后端分离项目集成积木报表
致敬:
1:致敬若依开源项目(本文使用的是前后端分离版本)
若依官网:https://www.ruoyi.vip/
2:致敬积木开源项目
积木报表官网:https://www.jimureport.com/
3:致敬CSDN大神的文章(我是根据他的原文一步步配置的,如有版权问题,可随时联系本人删除)
原文地址:https://blog.csdn.net/qq_55896432/article/details/145060090
背景:
写在前面:
搭建这个项目是因为自己想记录日常使用的一些数据(练琴统计,钢琴课程数量,花费等等这些),然后找了一些BI发现都不是很适合,目前积木是最适合的(集成程度高),所以记录了一下整体的操作步骤。
目前整体使用感觉还是可以的,数据源数据集设置都很方便,必要的控件也都够用。
项目运行的前提条件:
能正常运行若依前后端分离版本就OK.
效果(部分截图)
普通报表管理菜单
普通报表设计
数据源&数据集设置
大屏报表管理菜单
大屏设计
数据源&数据集设置
后端配置
ruoyi-common模块下pom.xml文件添加依赖
<!-- 积木报表 --><dependency><groupId>org.jeecgframework.jimureport</groupId><artifactId>jimureport-spring-boot-starter</artifactId><version>1.9.2</version></dependency><!--积木BI大屏--><dependency><groupId>org.jeecgframework.jimureport</groupId><artifactId>jimubi-spring-boot-starter</artifactId><version>1.9.1</version></dependency>
添加完重新加载maven
ruoyi-admin模块修改application.yml文件,新增积木报表相关配置
jeecg :# 权限配置permission:# 报表权限配置report:# 查询权限符号,配置此权限代表只能查看报表query: jeecg:report:query# 修改权限符号,配置此权限代表拥有报表所有权限edit: jeecg:report:edit# 大屏权限配置drag:# 查询权限符号,配置此权限代表只能查看大屏query: jeecg:drag:query# 修改权限符号,配置此权限代表拥有大屏所有权限edit: jeecg:drag:editjmreport:#自定义项目前缀# 现在是开发环境下的前端访问前缀,部署项目时,需要切换到生成环境访问前缀customPrePath: /dev-api
注意:1.目前报表和大屏分别设置两个类型的权限,其中query权限表示只能查看,edit权限表示用于一切权限。2.customPrePath是用来配置前端访问后端的接口的前缀,当前是开发环境配置为/dev-api没问题。3.项目发布时,一定将这个配置改为生产环境下访问前缀,如:/prod-api。
允许匿名访问ruoyi-framework模块下修改SecurityConfig.java文件
增加积木报表和大屏匿名访问权限。
, "/jmreport/**", "/drag/**"
新建积木报表相关配置类
在com.ruoyi.framework包下增加包report,在report包下新增config包,在cofig包下新增ReportConfig.java类,并在此类中添加以下代码。
package com.ruoyi.framework.report.config;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class ReportConfig {// 报表查询权限@Value("${jeecg.permission.report.query}")private String reportQueryPermission;// 报表修改权限@Value("${jeecg.permission.report.edit}")private String reportEditPermission;// 大屏查看权限@Value("${jeecg.permission.drag.query}")private String dragQueryPermission;//大屏修改权限@Value("${jeecg.permission.drag.edit}")private String dragEditPermission;public String getReportQueryPermission() {return reportQueryPermission;}public void setReportQueryPermission(String reportQueryPermission) {this.reportQueryPermission = reportQueryPermission;}public String getReportEditPermission() {return reportEditPermission;}public void setReportEditPermission(String reportEditPermission) {this.reportEditPermission = reportEditPermission;}public String getDragQueryPermission() {return dragQueryPermission;}public void setDragQueryPermission(String dragQueryPermission) {this.dragQueryPermission = dragQueryPermission;}public String getDragEditPermission() {return dragEditPermission;}public void setDragEditPermission(String dragEditPermission) {this.dragEditPermission = dragEditPermission;}
}
重写若依框架getLoginUser()方法
打开web.service包下的TokenService.java文件,将自带的getLoginUser方法注释掉
新增代码
/***************************积木报表修改,注释掉了原来的getLoginUser*******************************/// 修改public LoginUser getLoginUser(HttpServletRequest request) {// 获取请求携带的令牌String token = getToken(request);return getLoginUser(token);}public LoginUser getLoginUser(String token) {if (StringUtils.isNotEmpty(token)) {try {Claims claims = parseToken(token);// 解析对应的权限以及用户信息String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);String userKey = getTokenKey(uuid);LoginUser user = redisCache.getCacheObject(userKey);return user;} catch (Exception e) {log.error("获取用户信息异常'{}'", e.getMessage());}}return null;}/***************************积木报表修改,注释掉了原来的getLoginUser*******************************/
在report包下新增service包下,在service包下新增ReportTokenService.java类,并且在此类中添加以下代码
package com.ruoyi.framework.report.service;import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.report.config.ReportConfig;
import com.ruoyi.framework.web.service.TokenService;
import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;@Component
public class ReportTokenService implements JmReportTokenServiceI {// 若依框架token@Value("${token.header}")private String ryHeader;// 积木报表tokenprivate String jmHeader = "X-Access-Token";@Autowiredprivate ReportConfig reportConfig;@Autowiredprivate TokenService tokenService;@Overridepublic String getUsername(String s) {LoginUser loginUser = tokenService.getLoginUser(s);return loginUser.getUsername();}@Overridepublic String[] getRoles(String s) {LoginUser loginUser = tokenService.getLoginUser(s);SysUser user = loginUser.getUser();List<SysRole> roles = user.getRoles();String[] roleNameArray = roles.stream().map(SysRole::getRoleName).toArray(String[]::new);return roleNameArray;}@Overridepublic Boolean verifyToken(String s) {LoginUser loginUser = tokenService.getLoginUser(s);if (StringUtils.isNotNull(loginUser)){tokenService.refreshToken(loginUser);SysUser user = loginUser.getUser();// 超级管理员放权if (StringUtils.isNotNull(user) && user.isAdmin()) {return true;} else {Set<String> permissions = loginUser.getPermissions();if (StringUtils.isNotNull(permissions) && (permissions.contains(reportConfig.getReportQueryPermission()) || permissions.contains(reportConfig.getReportEditPermission()) || permissions.contains(reportConfig.getDragQueryPermission()) || permissions.contains(reportConfig.getDragEditPermission()))) {return true;}}}return false;}@Overridepublic String getToken(HttpServletRequest request) {String token = request.getParameter("token");if (StringUtils.isNull(token)) {token = request.getHeader(jmHeader);}if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)){token = token.replace(Constants.TOKEN_PREFIX, "");}return token;}@Overridepublic Map<String, Object> getUserInfo(String token) {token = token.replace(Constants.TOKEN_PREFIX, "");LoginUser loginUser = tokenService.getLoginUser(token);Map<String, Object> map = new HashMap<>();map.put(SYS_USER_CODE, loginUser.getUserId());map.put(SYS_ORG_CODE, loginUser.getDeptId());return map;}@Overridepublic HttpHeaders customApiHeader() {HttpHeaders headers = new HttpHeaders();headers.add(ryHeader, Constants.TOKEN_PREFIX + getToken());headers.add(jmHeader, getToken());return headers;}
}
贴一下原作者的注意事项:
配置拦截器
在report包下新增interceptor包,在interceptor包下新增ReportInterceptor.java类,并且在此类中添加以下代码
package com.ruoyi.framework.report.interceptor;import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.report.config.ReportConfig;
import com.ruoyi.framework.report.service.ReportTokenService;
import com.ruoyi.framework.web.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;@Component
public class ReportInterceptor implements HandlerInterceptor {@Autowiredprivate TokenService tokenService;@Autowiredprivate ReportConfig reportConfig;@Autowiredprivate ReportTokenService reportTokenService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = reportTokenService.getToken(request);LoginUser loginUser = tokenService.getLoginUser(token);
// String uri = request.getRequestURI();
// System.out.println(uri);if (StringUtils.isNotNull(loginUser)) {SysUser user = loginUser.getUser();// 超级管理员放权if (StringUtils.isNotNull(user) && user.isAdmin()) {return true;} else {//获取权限集合Set<String> permissions = loginUser.getPermissions();//如果拥有设计器的权限,则无需view权限,也可以通过校验if (StringUtils.isNotNull(permissions)) {String uri = request.getRequestURI();// 如果访问报表if (uri.contains("/jmreport/")) {// 如果有操作权限,直接放行if (permissions.contains(reportConfig.getReportEditPermission())) {return true;} else {// 设置查询报表的路径,没有带报表编码的路径Set<String> queryReportSet = new HashSet<>();queryReportSet.add("/jmreport/getQueryInfo");queryReportSet.add("/jmreport/show");// 设置查询报表的路径,带报表编码的路径ArrayList<String> queryReportList = new ArrayList<>();queryReportList.add("/jmreport/view/");queryReportList.add("/jmreport/addViewCount/");queryReportList.add("/jmreport/checkParam/");// 如果有查询权限if (permissions.contains(reportConfig.getReportQueryPermission())) {// 如果是没有报表编码的路径,放行if (queryReportSet.contains(uri)) {return true;} else {// 如果是带报表编码的路径,放行for (int i = 0; i < queryReportList.size(); i++) {String s = queryReportList.get(i);if (uri.contains(s)) {return true;}}}}}//如果访问大屏} else if (uri.contains("/drag/")) {// 如果有操作权限,直接放行if (permissions.contains(reportConfig.getDragEditPermission())) {return true;} else {// 设置查询大屏的路径,完全路径Set<String> queryDragSet = new HashSet<>();queryDragSet.add("/drag/page/queryById");queryDragSet.add("/drag/page/addVisitsNumber");// 设置查询大屏的路径,带有包含部分路径ArrayList<String> queryDragList = new ArrayList<>();queryDragList.add("/drag/share/view/");queryDragList.add("/drag/mock/json/");// 如果有查询权限,并且访问的查询接口,放行if (permissions.contains(reportConfig.getDragQueryPermission()) && queryDragSet.contains(uri)) {return true;}}// 拦截非报表和大屏路径处理,正确配置,不会出现此情况。} else {return true;}}}}AjaxResult ajaxResult = AjaxResult.error(403, "当前操作没有权限");ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));return false;}
}
原作者说的注意事项:
打开config包下的ResourcesConfig.java文件,注入ReportInterceptor拦截器,并且注册拦截器和设置拦截规则
修改addInterceptors()方法
/*** 自定义拦截规则*/@Overridepublic void addInterceptors(InterceptorRegistry registry){registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**")// 积木报表新增:不拦截静态资源.excludePathPatterns("/*.**, /**/*.html, /**/*.css, /**/*.js, /**/*.png, /**/*.jpg, /**/*.woff, /**/*.woff2, /**/*.ttf, /**/*.svg, /**/*.ico, /**/*.map" );}
如果出现未设置的静态资源,需要自己进行设置。
实现数据格式转换器
在report包下新增adapter包,在adapter包下,新增ReportDataConvertAdapter.java文件,并且在此类中添加以下代码
package com.ruoyi.framework.report.adapter;import com.alibaba.fastjson.JSONObject;
import org.jeecg.modules.jmreport.desreport.render.handler.convert.ApiDataConvertAdapter;
import org.springframework.stereotype.Component;@Component("reportDataConvertAdapter")
public class ReportDataConvertAdapter implements ApiDataConvertAdapter {@Overridepublic String getData(JSONObject jsonObject) {if(jsonObject.containsKey("data")){String data = jsonObject.getString("data");return data;}else if(jsonObject.containsKey("rows")){return jsonObject.getString("rows");}else {return jsonObject.toJSONString();}}
}
去官网拿积木的数据结构,在数据库执行
官网地址:
https://github.com/jeecgboot/jimureport/blob/master/db/jimureport.mysql5.7.create.sql
前端配置
新建jeecg文件夹,并且jeecg文件夹下,新增request.js文件,并且在此文件中添加以下代码
import axios from 'axios'
import { Notification, MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams } from "@/utils/ruoyi";
import cache from '@/plugins/cache'// 是否显示重新登录
export let isRelogin = { show: false };axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({// axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: process.env.VUE_APP_BASE_API,// 超时timeout: 10000
})// request拦截器
service.interceptors.request.use(config => {// 是否需要设置 tokenconst isToken = (config.headers || {}).isToken === false// 是否需要防止数据重复提交const isRepeatSubmit = (config.headers || {}).repeatSubmit === falseif (getToken() && !isToken) {config.headers['X-Access-Token'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改}// get请求映射params参数if (config.method === 'get' && config.params) {let url = config.url + '?' + tansParams(config.params);url = url.slice(0, -1);config.params = {};config.url = url;}if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {const requestObj = {url: config.url,data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,time: new Date().getTime()}const sessionObj = cache.session.getJSON('sessionObj')if (sessionObj === undefined || sessionObj === null || sessionObj === '') {cache.session.setJSON('sessionObj', requestObj)} else {const s_url = sessionObj.url; // 请求地址const s_data = sessionObj.data; // 请求数据const s_time = sessionObj.time; // 请求时间const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {const message = '数据正在处理,请勿重复提交';console.warn(`[${s_url}]: ` + message)return Promise.reject(new Error(message))} else {cache.session.setJSON('sessionObj', requestObj)}}}return config
}, error => {console.log(error)Promise.reject(error)
})// 响应拦截器
service.interceptors.response.use(res => {// 未设置状态码则默认成功状态const code = res.data.code || 200;// 获取错误信息const msg = errorCode[code] || res.data.msg || res.data.message || errorCode['default']// 二进制数据则直接返回if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {return res}if (code === 401) {if (!isRelogin.show) {isRelogin.show = true;MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {isRelogin.show = false;store.dispatch('LogOut').then(() => {location.href = '/index';})}).catch(() => {isRelogin.show = false;});}return Promise.reject('无效的会话,或者会话已过期,请重新登录。')} else if (code === 500) {Message({ message: msg, type: 'error' })return Promise.reject(new Error(msg))} else if (code === 601) {Message({ message: msg, type: 'warning' })return Promise.reject('error')} else if (code !== 200) {Notification.error({ title: msg })return Promise.reject('error')} else {return res.data}},error => {console.log('err' + error)let { message } = error;if (message == "Network Error") {message = "后端接口连接异常";} else if (message.includes("timeout")) {message = "系统接口请求超时";} else if (message.includes("Request failed with status code")) {message = "系统接口" + message.substr(message.length - 3) + "异常";}Message({ message: message, type: 'error', duration: 5 * 1000 })return Promise.reject(error)}
)export default service
在jeecg文件夹下,新增report.js文件,并且在此文件中添加以下代码
import request from "@/api/jeecg/request";
let jmreportUrl = "/jmreport";
import { getToken } from "@/utils/auth";
let paramObj = {token: "Bearer " + getToken()
}
// 获取填报报表
export function listReport(queryParams) {let params = {...paramObj,...queryParams}return request({url: jmreportUrl + "/excelQuery",method: 'get',params: params})
}// 获取填报报表列表
export function listFillReport(queryParams) {paramObj["reportType"] = "1011126161407836160";let params = {...paramObj,...queryParams}return request({url: jmreportUrl + "/excelQuery",method: 'get',params: params})
}
在src/view文件夹下新增jeecg文件夹,在jeecg文件夹下新增report文件夹,在report文件夹下新增index.vue文件,并且在此文件中添加以下代码
<template><div><i-frame :src="reportUrl"></i-frame></div>
</template>
<script>
import { getToken } from "@/utils/auth";
import iFrame from "@/components/iFrame/index";
export default {name: "PyJeecgReport",components: { iFrame },data() {return {reportUrl: process.env.VUE_APP_BASE_API + "/jmreport/list?token=Bearer " + getToken()}}
}
</script>
在report文件夹下新增view文件夹,在view文件夹下新增index.vue文件,并且在此文件中添加以下代码。
<template><div><i-frame :src="reportUrl"></i-frame></div>
</template>
<script>
import { getToken } from "@/utils/auth";
import iFrame from "@/components/iFrame/index";
export default {name: "PyJeecgReportView",components: { iFrame },data() {return {reportUrl: ""}},created() {let query = this.$route.query;let code = undefined;let paramsString = "";if (query) {for (const key in query) {if (Object.hasOwnProperty.call(query, key)) {const param = query[key];if (key == "code") {code = param;} else {if (paramsString != "") {paramsString += paramsString + "&" + key + "=" + param;} else {paramsString = "&" + key + "=" + param;}}}}}if (code) {this.reportUrl = process.env.VUE_APP_BASE_API + "/jmreport/view/" + code + "?token=Bearer " + getToken() + paramsString;} else {this.$modal.msgError("报表编码(code)为空,无法打开报表");}}
}
</script>
在jeecg文件夹下新增drag文件夹,在drag文件夹下新增index.vue文件,并且在此文件中添加以下代码
<template><div><i-frame :src="dragUrl"></i-frame></div>
</template>
<script>
import { getToken } from "@/utils/auth";
import iFrame from "@/components/iFrame/index";
export default {name: "PyJeecgDrag",components: { iFrame },data() {return {dragUrl: process.env.VUE_APP_BASE_API + "/drag/list?token=Bearer " + getToken()}}
}
</script>
在drag文件夹下新增view文件夹,在view文件夹下新增index.vue文件,并且在此文件中添加以下代码。
<template><div><i-frame :src="dragUrl"></i-frame></div>
</template>
<script>
import { getToken } from "@/utils/auth";
import iFrame from "@/components/iFrame/index";
export default {name: "PyJeecgDragView",components: { iFrame },data() {return {dragUrl: ""}},created() {let query = this.$route.query;let code = undefined;let paramsString = "";if (query) {for (const key in query) {if (Object.hasOwnProperty.call(query, key)) {const param = query[key];if (key == "code") {code = param;} else {if (paramsString != "") {paramsString += paramsString + "&" + key + "=" + param;} else {paramsString = "&" + key + "=" + param;}}}}}if (code) {this.dragUrl = process.env.VUE_APP_BASE_API + "/drag/share/view/" + code + "?token=Bearer " + getToken() + paramsString;} else {this.$modal.msgError("大屏编码(code)为空,无法打开大屏");}}
}
</script>