代码生成
- 在「工程应用」页面,选支持「报表打印」,生成后的源码位于工程的 common/common-report 目录内。
- 选择「集成报表」的服务。在单体工程中,只有一个业务服务 application-webadmin,因此只能选择该服务。而微服务工程可能存在多个业务服务,那么「有且只有一个」业务服务能够集成报表打印模块,并对外提供接口服务。
数据初始化
- 执行工程目录下的 ./zz-resource/db-scripts/report-script.sql 脚本,创建报表打印模块所需的数据表。
- 对于已经使用橙单的项目,需要手动补偿工程目录下 ./zz-resource/db-scripts/init-upms-data-script.sql 脚本中,与报表打印模块相关的权限数据。下图仅以白名单数据为例,其他需要手动补偿的权限数据,可在该脚本文件中搜索下图红框圈住的文字即可定位。
数据库链接
首先需要配置数据库链接,点击菜单进入数据库链接管理页面。这里我们还会给出目前已经支持的八种数据库 MySQL、PostgreSQL、Oracle 、达梦、人大金仓、华为高斯、Doris 和 Clickhouse 的配置示例,具体信息见如下截图。
- 数据库链接列表。
- MySQL 配置样例。
- PostgreSQL 配置样例。
- Oracle 配置样例。
- Clickhouse 配置样例。
报表字典
点击菜单进入报表字典管理页面,目前已支持数据表字典、自定义字典和全局编码字典三种类型。如下图。
数据表字典
数据表字典,从已经配置的数据库链接中,选择某一数据表作为字典的数据源。
自定义字典
自定义字典,手动输入字典的键值数据。
全局编码字典
报表引用的全局编码字典,可从当前系统中,已经配置的全局编码字典中直接选择。
数据集
目前支持「数据表」和「SQL结果集」类型的数据集,后面会支持更多类型的数据集,如 API 接口的结果集等。
表数据集
对于数据表类型的数据集,直接选择数据库链接,然后选择数据表即可。数据集名称,目前仅用于显示。
SQL数据集
支持 SQL 语句的数据集,同时可以为该 SQL 语句传递参数变量,从而得到更优的数据灵活性和查询效率。
- 带有变量的 SQL 语句示例,变量用 ${} 括起。
- 为参数变量设置默认值。如果设置了默认值,下一步的数据预览,将使用该默认值替换参数变量,否则使用 1 = 1 恒成立条件替换上图的 grade_id = ${gradeId},以此忽略带有可变参数的过滤条件。
- 在后续的使用中,SQL 语句会以子查询的方式出现在 FROM 从句中,见如下代码。
SELECT grade_id, subject_id, SUM(class_hour) "sumOfClassHour" FROM
-- 这里子查询就是上面SQL数据集中定义的SQL语句。
(SELECT * FROM zz_course WHERE grade_id = 1) tmp
WHERE price > ? GROUP BY grade_id, subject_id ORDER BY grade_id, subject_id
- 无论是报表页面的组件,还是打印模板中的段落 (Fragment),都可以使用 SQL 数据集作为数据源,其使用方式和「表数据集」大同小异。唯一的差别是「SQL数据集」的变量参数传递,这个过程在表数据集中是不存在的。下面介绍一下「SQL数据集」变量参数的过滤规则。
- 这里以上图的 SELECT * FROM zz_course WHERE grade_id = ${gradeId} 为例。
- 如果使用中传递了 ${gradeId} 变量值,查询时就会用该值替换 SQL 语句中的变量部分。
- 如果没有传递 ${gradeId} 变量值,则使用配置SQL数据集时定义的默认值,如上图中的 1。
- 如果既没有传递也没有定义默认值,那么就用 1 = 1 的恒成立条件替换上面 SQL 中的 grade_id = ${gradeId} 部分。
- 报表页面组件和打印模板片段的 SQL 数据集变量参数的设置,会在后面给出详细的说明和图示。
接口数据集
支持通过配置 URL、Method、接口参数和结果集字段来接入第三方接口数据集,如下图所示。
- 如果接口方法为 GET,配置参数将会以 query_param 形势拼在 URL 的后面。
- 如果接口方法为 POST,配置的参数将会以 JSON 的方式存储于请求体中,并传递给第三方接口。
- 在字段管理中,必须包含一个以「data」命名的根节点,根节点必须是 Object 或者 Array 类型。
- 字段必须输入显示名、字段名以及字段类型,否则不可保存。
- API 数据源字段目前并不支持字典绑定,字典数据请在 API 接口中自行关联。
- 只有 Object 和 Array 类型的字段才可以添加子字段。
- 在目前版本中,API 数据集的实现和配置,存在以下几点的重要规则约束。请仔细阅读代码中的注释说明。
- 我们默认在 API 接口的「请求头」和「请求参数」中,提供了当前用户的身份信息,键名为「Authorization」。具体可参考以下代码中的注释说明。
- API 请求接口中,会传递固定的「分页」和「排序」参数对象。API 接口的实现者,尽量按照该默认的规则实现数据的「分页」和「排序」逻辑。具体可参考以下代码中的注释说明。
- API 请求接口的默认返回结构,缺省为 ResponseResult<MyPageData<Map<String, Object>>>。需要重点说明的是,为了方便上层组件的统一处理,无论是否需要分页,都要按照 MyPageData 的对象结构返回,当然开发者用户也可以根据需求自行调整。具体可参考以下代码中的注释说明。
private MyPageData<Map<String, Object>> getApiDataList(
ReportDataset dataset,
List<ReportFilterParam> datasetFilterParams,
List<ViewOrderData> orderDataList,
MyPageParam pageParam) {
ReportDatasetInfo datasetInfo = JSON.parseObject(dataset.getDatasetInfo(), ReportDatasetInfo.class);
// 为了保证更好的兼容性,我们在请求头和请求参数中,均添加了用于默认权限验证的数据。
// headerMap中的Authorization是请求头中的验证数据,param中的Authorization则是请求参数中的验证数据。
// Token值我们直接使用了当前用户登录时返回的Token数据。该方式对于橙单系统的API接口,是无需有任何修改的。
Map<String, String> headerMap = new HashMap<>(1);
headerMap.put("Authorization", TokenData.takeFromRequest().getToken());
Map<String, Object> param = new HashMap<>(4);
param.put("Authorization", TokenData.takeFromRequest().getToken());
if (CollUtil.isNotEmpty(datasetFilterParams)) {
datasetFilterParams.stream().filter(p -> p.getParamValue() != null)
.forEach(p -> param.put(p.getParamName(), StrUtil.replace(p.getParamValue(), " ", "%20")));
}
// 这里是排序数据列表,API接口的实现,需要自行解析该默认规则中的排序信息。
if (CollUtil.isNotEmpty(orderDataList)) {
String orderBy = JSON.toJSONString(orderDataList);
orderBy = StrUtil.replace(orderBy, " ", "%20");
param.put("orderParam", orderBy);
}
HttpResponse response;
if (StrUtil.equals(datasetInfo.getMethod(), "get")) {
// 这里是GET请求的分页参数,API接口的实现,需要自行提取该默认分页信息。
if (pageParam != null) {
param.put("pageNum", pageParam.getPageNum());
param.put("pageSize", pageParam.getPageSize());
}
response = HttpUtil.createGet(datasetInfo.getUrl()).form(param).addHeaders(headerMap).execute();
} else {
// 这里是POST请求的分页参数,API接口的实现,需要自行提取该默认分页信息。
if (pageParam != null) {
param.put("pageParam", pageParam);
}
headerMap.put("Content-Type", "application/json; charset=utf-8");
String url = datasetInfo.getUrl() + "?Authorization=" + TokenData.takeFromRequest().getToken();
response = HttpUtil.createPost(url).body(JSON.toJSONString(param)).addHeaders(headerMap).execute();
}
if (!response.isOk()) {
throw new MyRuntimeException(response.body());
}
// 下面一行代码是对请求结果的JSON解析,返回的数据对象,必须和橙单的
// ResponseResult<MyPageData<Map<String, Object>>> 类对象结构保持一致。
// 即便API接口无需分页,也需要返回分页对象,MyPageData中的totalCount默认为数据列表的数据size。
// 对于分页接口,totalCount则为总数据量。
ResponseResult<MyPageData<Map<String, Object>>> responseResult =
JSON.parseObject(response.body(), new TypeReference<ResponseResult<MyPageData<Map<String, Object>>>>() {
});
if (!responseResult.isSuccess()) {
throw new MyRuntimeException(responseResult.getErrorMessage());
}
return responseResult.getData();
}
数据预览
数据预览页面,可以查看数据集返回的数据列表,以便于调试。
字段管理
为数据集中的每个字段,配置所需的关联或标记信息。
- 显示名,数据集字段显示名称。
- 字典绑定,所有使用该字段的地方,都会被自动翻译为关联的字典显示值数据。如报表组件和打印模板中的单元格数据。
- 图片字段,配置打印模板时,如果某一单元格绑定到该字段,结果会以图片的形式,渲染到目标文件中。
- 数据权限,功能逻辑和业务表单的数据权限完全一致。这里可指定参与「部门」和「用户」过滤的指定字段。
- 逻辑删除字段,是否为逻辑删除标记字段。
- 维度 / 指标,配置报表页面时,图表和透视表组件会使用该标记。
数据关联
数据关联的逻辑是在 Java 代码中实现的,因此可以支持跨库的数据关联,具体实现方式如下。
- 读取主表数据列表。
- 根据主表和关联从表的关联字段,查询并过滤从表数据。
- 根据主从表中的关联字段值,完成数据的自动组装。组装后的从表数据,会赋值给主表「关联标识」字段中。
- 在每一个主表数据对象中,会包含关联的从表对象 (一对一关联) 或对象列表 (一对多) 数据。
上图中「关联标识」的注意事项。
- 在同一数据集内必须唯一。
- 该值是主表与从表关联的字段名称,因此只能是字符串,推荐使用小驼峰格式。
报表页面
点击菜单,进入报表页面管理,如下图。
基础信息
点击「新建页面」进入新建报表页面窗口,如下图。
- 所属分组,报表页面所属分组。
- 页面名称,页面显示名称。
- 页面编码,页面唯一编码。
表单设计
点击下一步进入页面表单设计窗口,如下图。
设计页面的具体说明如下。
- 左侧是组件选择区域,可以点击或者拖拽组件到设计区域。
- 中间为设计区域,可以拖拽调整组件的顺序和位置。
- 右侧为属性编辑区域,可以选中组件来编辑组件的属性,当选中空白区域的时候,可以编辑页面的属性,例如添加页面参数。
- 页面参数是由调用方设置,可以用于组件过滤。
组件过滤
如果组件的数据源是 SQL 数据集,并且包含参数变量,可以如下图所示为该 SQL 数据集设置参数变量的实际值。
在组件属性区域,可以通过过滤器设置组件的过滤条件,一个组件可以同时包含多个过滤字段,每个过滤字段之间是 AND 关系,如下图。
- 过滤字段,选择要过滤的字段。
- 是否必填,此过滤值是否必填。
- 过滤规则,此过滤的规则。
- 过滤值类型,过滤值类型,目前支持表单参数、组件数据、字典数据、字段数据和自定义参数值 5 种过滤类型。
- 表单参数,由外部传入的报表页面参数作为过滤值。
- 组件数据,报表页面上组件的选中值作为过滤值。
- 字典数据,选择某个字典的值作为过滤值。
- 字段数据,选择组件使用的数据集的某个字段的值作为过滤值。
- 自定义参数,手动输入过滤值。
下钻设置
通过视图组件的事件,可以进行下钻功能的设置。下钻事件可通过双击视图组件中的数据项触发,如下图。
- 所属分组,下钻页面所属分组。
- 下钻页面,选择下钻页面。
- 页面参数,下钻页面表单参数设置。
权限配置
创建完报表页面后,需要给报表页面创建权限,如下图。
在权限管理菜单中,给新建的报表页面创建权限,目前只需要创建获取视图数据的权限,规则为
/admin/report/reportOperation/listData/${pageCode} ,pageCode 为报表页面的页面编码,创建好报表页面权限后,需要创建一个报表权限字,如下图。
最后,可以通过新建菜单把报表页面和菜单绑定起来,如下图。
Word打印模板
点击菜单进入打印管理页面,如下图。
基础信息
编辑打印模板的基础信息,具体说明如下。
- 模板名称,仅用于显示。
- 模板标识,仅能输入英文字符,且全局唯一。
- 模板输入参数,这里定义的是参数的变量名,可以直接在下拉框上输入。在后续的操作中,给「段落」绑定数据集时,该变量可作为过滤字段值使用,具体配置会在后面给出。
- 模板类型,注意该选项配置后不能修改。
模板配置
- 点击下一步,进入打印模板配置的主页面。
- 在线下手动编辑 Word 打印模板文件,具体语法可参考「poi-tl」的官方文档 https://deepoove.com/poi-tl/。可能需要翻墙访问。下面仅给出我们自己手写的模板示例,后面会根据实际配置,给出更详细的说明。
- 配置数据集,这里重点介绍「数据标识」配置项。具体功能见以下几张截图和文字描述。
- 配置数据集,这里重点介绍「是否迭代」配置项。具体功能见以下几张截图和文字描述。
- 配置数据集字段,这里重点介绍「图片字段」的配置。具体功能见以下几张截图和文字描述。
- 最后将线下手动编写的 Word 模板文件上传。
模板预览
Word 打印模板的渲染结果并不支持在线预览,仅能直接下载,如有在线预览需求,可自行搭建 kkfileview 开源库,将渲染后的 Word 文件转换为 PDF 返回前端,浏览器通常支持 PDF 文件的在线打开和打印。
配置示例结果
本节所使用的打印模板和数据集配置的最终渲染结果如下图所示。
Excel打印模板
点击菜单进入打印管理页面,如下图。
基础信息
编辑打印模板的基础信息,具体说明如下。
- 模板名称,仅用于显示。
- 模板标识,仅能输入英文字符,且全局唯一。
- 模板输入参数,这里定义的是参数的变量名,可以直接在下拉框上输入。在后续的操作中,给「段落」绑定数据集时,该变量可作为过滤字段值使用,具体配置会在后面给出。
- 模板类型,注意该选项配置后不能修改。
模板设计
点击下一步,进入打印模板编辑的主页面。打印模板配置步骤如下。
- 静态内容。可以在「段落」之外的单元格中,直接填写静态文本。如下图红框圈住的单元格。
- 创建「段落」。
- 「段落」设置。
- 「段落」数据过滤和排序。同时支持多个过滤条件,多个条件之间是 AND 关系。也可以同时设置多个排序字段。
- 「段落」数据过滤的注意事项。
重点!打印模板中,如果存在多个「段落」,每个段落可绑定到独立的数据集或数据集关联,段落之间没有任何过滤联动,切记。
「基本信息」段落绑定到数据集主表,同时存在基于主表字段的过滤条件。
「详情」段落绑定到该数据集的一对多从表,其过滤条件与其他段落之间,没有任何联动性。即该段落关联的最终数据,只和当前绑定的过滤条件相关。如下图 zz_course_section.course_id = ${courseId}。
- 单元格数据绑定。
SQL数据集过滤
如果打印段落 (Fragment) 绑定的数据源是 SQL 数据集,并且该数据集存在变量参数,那么就可以在下图中为该变量参数指定实际的值。
图片打印
图片打印比较复杂,我们需要逐步讲解并给出实例说明。
- 配置打印字段。
- 橙单的代码中,由于报表模块是公用模块,所以我们会以 HTTP 的形式请求图片数据,从而降低与业务模块的耦合性。下图的配置示例来自于业务服务的配置文件。common-report.imageDownloadUrl 配置项,要给出实际可下载到图片的接口地址。
- 下面是 common-report 模块 ReportPrintServiceImpl 类中,与单元格图片打印相关的代码片段解析。
// 该私有方法位于ReportPrintServiceImpl.java文件。
private XEasyPdfCell makePdfCell(
ReportSheetCell reportCell, ReportSheetCellValue cellValue, float cellWidth, float cellHeight) {
XEasyPdfCell cell;
if (ObjectUtil.equals(cellValue.getCellType(), ReportSheetCellValue.IMAGE_CELL)) {
// 这里比较特殊,我们假设的是使用橙单的表单上传接口保存的数据。所以数据结构是橙单框架内部定义的对象类型。
// 今后我们会支持自定义数据源的,这个地方也会做相应的修改。
List<UploadResponseInfo> infos = JSON.parseArray(cellValue.getValue(), UploadResponseInfo.class);
if (CollUtil.isNotEmpty(infos)) {
// 这里需要注意,如果存在多个图片地址,这里只是取出第一个图片数据的信息。
UploadResponseInfo info = infos.get(0);
StringBuilder sb = new StringBuilder(128);
// 读取出上面配置的pictureUrl字段中的数据,并提取uriPath和filename两个数据字段
// 用户拼接请求。
// reportProperties.getImageDownloadUrl()是当前工程的配置项。见上图。
sb.append(reportProperties.getImageDownloadUrl())
.append("?uriPath=").append(info.getUploadPath())
.append("&filename=").append(info.getFilename());
XEasyPdfImageType imageType = XEasyPdfImageType.PNG;
// 目前先仅仅支持这两种图片格式,更多可以自己手动修改代码扩充。
if (StrUtil.endWithIgnoreCase(info.getFilename(), "jpg")) {
imageType = XEasyPdfImageType.JPEG;
}
try {
// 利用hutool的HTTP工具类,获取图片数据。
byte[] bytes = HttpUtil.downloadBytes(sb.toString());
try (InputStream is = new ByteArrayInputStream(bytes)) {
// 根据HTTP返回的图片字节流,构建图片类型的单元格。下面的代码都是x-easypdf的api了。
XEasyPdfImage image = XEasyPdfHandler.Image.build(is, imageType, (int) cellWidth, (int) cellHeight);
cell = XEasyPdfHandler.Table.Row.Cell.build(cellWidth, cellHeight).addContent(image);
} catch (IOException e) {
throw new MyRuntimeException(e);
}
} catch (HttpException e) {
throw new MyRuntimeException(e);
}
} else {
// 如果没有图片数据,就用空文本内容替代即可。
XEasyPdfText text = XEasyPdfHandler.Text.build("");
cell = XEasyPdfHandler.Table.Row.Cell.build(cellWidth, cellHeight).addContent(text);
}
} else if {
// 这里省略若干不相干代码。
}
- 部署专门为打印图片字段而准备的下载接口。该示例代码,存在于默认生成的 common-report 模块中,但是所有代码被全部注释。
- 尽量不要直接打开注释。推荐将上图中的 ReportExampleController 文件,copy 到业务服务的 controller 包内,然后再恢复注释。如果存在编译错误,一般也是 import 包的路径问题,手动修改即可。最最重要的是该接口的 URI 路径,一定要和前面截图中 common-report.imageDownloadUrl 配置项的值一致才行。
第三方打印
本小节主要介绍第三方系统接入橙单的自定义打印模块。
路由表单示例
可在橙单生成的业务 Controller 接口中新增该方法,同时注意新增 /print 的权限资源。
/**
* 打印指定线下课程数据。
*
* @param courseId 课程Id。
*/
@GetMapping("/print")
public void print(@RequestParam Long courseId) throws IOException {
Course course = courseService.getById(courseId);
if (course == null) {
ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST));
return;
}
ReportPrintParam printParam = new ReportPrintParam();
ReportPrintParam.FilterInfo filterInfo = new ReportPrintParam.FilterInfo();
filterInfo.setParamName("courseId");
// 如果是数组,请用setParamValueList字段,数据格式是JSON数组。
filterInfo.setParamValue(courseId.toString());
printParam.add(filterInfo);
CallResult renderResult = reportPrintService.render("previewTest", printParam);
if (!renderResult.isSuccess()) {
ResponseResult.output(
HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(renderResult));
}
}
可视化大屏
橙单大屏是集成了 GoView 的独立部署模块。后台由专门生成的单体工程进行支持。
代码生成
- 在个人中心模块管理购买后,即可直接下载。我们目前仅提供了此下载方式。
- 可在下载弹框中选择工程的英文名称、包名和数据库类型。
后台启动
- 在生成后的后台工程中,修改数据库的连接信息。主要包括「数据库主机地址、端口号、数据库名、用户名和密码」。需要补充说明的是,这几个数据源可以同库。
- 执行数据库脚本,其中 common-report.sql 脚本,要在下图中 common-report 数据源所指向的数据库中执行,而另外两个脚本 visualization-user-script.sql 和 global-dict-script.sql,则需要在 main 数据源指向的数据库中执行。
- 通过 docker-compose.yml 启动 Redis 服务。
- 正常启动 application-visualization 后台服务,该服务所监听的端口号为 8084,开发者可按需修改。
前端启动
- 环境配置。
- 启动工程,npm run dev。
- 打包命令,npm run build
Node 需要 18 以上版本,如果遇到打包失败,请参考 GoView 文档。建议内存设置为 40960。
数据权限
管理员可以查看所有用户创建的数据。包括大屏项目、数据库链接、数据集和数据字典,而普通用户只能查看自己创建的相关数据。
结语
赠人玫瑰,手有余香,感谢您的支持和关注,选择橙单,效率乘三,收入翻番。