代码生成
在企业版工程中进行如下配置,然后重新生成该工程,即可获取在线表单模块的全部功能源码。源码位于工程的 common/common-online 目录内。 在默认生成的工程中,微服务的 upms 服务,单体的 application-webadmin 服务,以及多租户的 tenant-admin 服务,会自动引用该模块。
数据初始化
- 执行工程目录下的 ./zz-resource/db-scripts/online-form-script.sql 脚本,创建在线表单模块所需的数据表。
- 对于已经使用橙单的项目,需要手动补偿工程目录下 ./zz-resource/db-scripts/init-upms-data-script.sql 脚本中,与在线表单模块相关的权限数据。下图仅以白名
数据为例,其他需要手动补偿的权限数据,可在该脚本文件中搜索下图红框圈住的文字即可定位。
数据库链接
首先需要配置数据库链接,点击菜单进入数据库链接管理页面。这里我们还会给出目前已经支持的六种数据库 MySQL、PostgreSQL、Oracle、达梦、人大金仓和华为高斯的配置示例。具体信息见如下截图。
- MySQL 配置样例。
- PostgreSQL 配置样例。
- Oracle 配置样例。
配置项目
common-online 模块的配置项,请仔细阅读配置项上的注释说明。
字典管理
点击菜单进入在线表单字典管理页面,目前已支持数据表字典、URL 字典、静态字典、全局编码字典和自定义字典五种类型,如下图。
数据表字典
数据表字典,从已经配置的数据库链接中选择字典表,并指定字典 ID 和字典名称字段。
- 字典名称,字典的显示名称。
- 数据库,选择数据表所属数据库。
- 数据表,选择数据表字典所使用的数据表。
- 树状字典,树状字典数据将返回 id、name 和 parentId 三个字段,使用时需要将列表转换为树型数据结构。
- 字典父字段,当字典为树状字典时,选择字典 ID 的父 ID 字段。
- 字典键字段,选择字典 ID 字段。
- 字典值字段,选择字典显示名称字段。
- 逻辑删除字典,选择数据表中的逻辑删除字段。
- 用户过滤字段,选择数据表中的用户过滤字段,仅用于数据权限中的「仅看当前用户」、「本部门所有用户」和「本部门及子部门所有用户」策略的数据过滤。
- 部门过滤字段,选择数据表中的部门过滤字段,仅用于数据权限中的「仅查看当前部门」、「所在部门及子部门」、「多部门及子部门」和「自定义部门列表」策略的数据过滤。
- 租户过滤字段,选择数据表中的租户过滤字段,仅用于多租户项目中的租户过滤。
- 字典参数,数据表字典过滤参数。
配置过滤参数后,可在字典的使用过程中,通过为过滤参数赋值的方式进行字 典的数据过滤,具体使用方式,可参考后面表单管理的组件设置小节。
URL字典
通过指定的 URL 获取字典数据,具体配置信息见下图。
- 字典名称,字典的显示名称。
- 字典 URL,获取全部字典数据的 URL,必须为 GET 请求。点击右侧的「获取数据」按钮,会调用 URL 获取数据,并将得到的数据填充到下面的「字典键字段」、「字典值字段」和「字典父字段」所对应的下拉框中,以供选择。
- 树状字典,数据将返回 id、name 和 parentId 三个字段,使用时需要将列表转换为树型数据结构。
- 字典父字段,当字典为树状字典时,选择字典 ID 的父 ID 字段。
- 字典键字段,选择字典 ID 字段。
- 字典值字段,选择字典显示名称字段。
- 字典参数,数据表字典过滤参数。
静态字典
静态字典通常会在橙单代码生成器中配置后生成,生成后的前端代码位于 staticDict -> index.js 文件中,具体见下图。
静态字典的配置信息如下图所示。
- 字典名称,字典的显示名称。
- 字典选择,选择静态字典。
自定义字典
自定义字典可直接输入字典数据,具体配置如下。
全局编码字典
在线表单的全局编码字典,可从当前系统中,已经配置的全局编码字典中直接选择。
表单管理
点击菜单,进入报表页面管理,如下图。
基础信息
点击上图的「新建」或「编辑」按钮,可以进入页面编辑页面。
- 页面类型,表单的页面类型。普通业务的在线表单选择「业务页面」,与流程相关的页面选择「流程页面」。
- 页面编码,表单页面的唯一编码,小驼峰命名,如 contractManagement。
- 页面名称,表单页面的中文显示名称。
- 后台扩展类,表单配置的后台扩展类,更多细节请参考下一小节。
后台扩展类
这里需要输入包含包名在内的完整类名,如 com.orangeforms.webadmin.app.util.OnlineExtendExampleExecutor,同时该类必须是继承自 BaseOnlineExtendExecutor 的 Bean 对象,具体配置如下图所示。
接口触发列表
在调用以下在线表单和在线表单工作流的接口时,会触发上图配置的后台扩展执行方法。
类方法 | 表单类型 | 可能触发扩展接口 | 注意事项 |
---|---|---|---|
OnlineOperationController.addDatasource | 业务 | beforeInsert afterInsert beforeInsertWithRelation afterInsertWithRelation |
接口包含从表数据才会触发WithRelation结尾的接口方法。 |
OnlineOperationController.addOneToManyRelation | 业务 | beforeInsert afterInsert |
|
OnlineOperationController.updateDatasource | 业务 | beforeUpdate afterUpdate beforeUpdateWithRelation afterUpdateWithRelation |
接口包含从表数据才会触发WithRelation结尾的接口方法。 |
OnlineOperationController.updateOneToManyRelation | 业务 | beforeUpdate afterUpdate |
|
OnlineOperationController.deleteDatasource | 业务 | beforeDelete afterDelete |
|
OnlineOperationController.deleteOneToManyRelation | 业务 | beforeDelete afterDelete |
|
OnlineOperationController.deleteBatchDatasource | 业务 | beforeDelete afterDelete |
被多次触发。 |
OnlineOperationController.deleteBatchOneToManyRelation | 业务 | beforeDelete afterDelete |
被多次触发。 |
OnlineOperationController.listByDatasource | 业务 | beforeSelectList afterSelectList |
只触发一次。 |
OnlineOperationController.listByOneToManyRelationId | 业务 | beforeSelectList afterSelectList |
只触发一次。 |
OnlineOperationController.exportByDatasource | 业务 | beforeSelectList afterSelectList |
只触发一次。 |
OnlineOperationController.exportByOneToManyRelationId | 业务 | beforeSelectList afterSelectList |
只触发一次。 |
FlowOnlineOperationController.startAndTakeUserTask | 流程 | beforeInsert afterInsert beforeInsertWithRelation afterInsertWithRelation |
接口包含从表数据才会触发WithRelation结尾的接口方法 |
FlowOnlineOperationController.submitUserTask | 流程 | beforeInsert afterInsert beforeInsertWithRelation afterInsertWithRelation beforeUpdate afterUpdate beforeUpdateWithRelation afterUpdateWithRelation |
接口包含从表数据才会触发WithRelation结尾的接口方法。 提交新数据时触发insert系列的接口,否则update系列接口。 |
FlowOnlineOperationController.viewCopyBusinessData | 流程 | afterSelectOne beforeSelectList afterSelectList |
包含一对多从表数据将触发SelectList接口, 否则只触发afterSelectOne。 |
FlowOnlineOperationController.viewHistoricProcessInstance | 流程 | afterSelectOne beforeSelectList afterSelectList |
包含一对多从表数据将触发SelectList接口, 否则只触发afterSelectOne。 |
FlowOnlineOperationController.viewUserTask | 流程 | afterSelectOne beforeSelectList afterSelectList |
包含一对多从表数据将触发SelectList接口, 否则只触发afterSelectOne。 |
开发注意事项
- 如果配置项 common-online.enabledMultiDatabaseWrite 为 true 时,所有的数据库调用都不要使用 Mybatis 方式,而使用橙单自带的 OnlineDatasourceUtil 工具方法,如配置项 common-online.enabledMultiDatabaseWrite 为 false,可以直接使用 Mybatis 方式操作数据表,见如下代码说明。
@Slf4j
@Component
public class OnlineExtendExampleExecutor extends BaseOnlineExtendExecutor {
@Autowired
private OnlineProperties onlineProperties;
@Autowired
private CourseMapper courseMapper;
@Autowired
private OnlineDataSourceUtil onlineDataSourceUtil;
@Override
public CallResult beforeInsertWithRelation(
OnlineTable masterTable, JSONObject masterData, Map<OnlineTable, List<JSONObject>> slaveTableAndDataMap) {
String columnName = masterTable.getPrimaryKeyColumn().getColumnName();
// 最最重要的差别是,当前事务和扩展接口操作的数据表可能位于不同的数据库,因此为false的时候,可以直接使用mybatis,否则就要使用jdbc方式。
if (BooleanUtil.isFalse(onlineProperties.getEnabledMultiDatabaseWrite())) {
// 如果该配置项是false,可以直接使用Mybatis的方法。
List<Course> courses = courseMapper.selectList(new LambdaQueryWrapper<Course>().eq(Course::getId, 1));
} else {
// 如果该配置项是true,请使用onlineDataSourceUtil的工具方法。
try {
String yourVerifySql = "select * from course where id = 1";
List<Map<String, Object>> results = onlineDataSourceUtil.query(masterTable.getDblinkId(), yourVerifySql);
} catch (Exception e) {
log.error(e.getMessage(), e);
return CallResult.error(e.getMessage());
}
}
return CallResult.ok();
}
// ... ... 忽略其他方法实现。
}
- 对于 beforeInsert/beforeInsertWithRelation/beforeUpdate/beforeUpdateWithRelation 扩展方法,如果配置项 common-online.enabledMultiDatabaseWrite 为 true 时,不要执行任何数据表「增删改」的操作,因为接口中执行的操作和外部调用方法位于不同的事务,一旦中间执行失败,会导致两个事务的数据不一致。推荐使用方式如下。
@Slf4j
@Component
public class OnlineExtendExampleExecutor extends BaseOnlineExtendExecutor {
@Autowired
private OnlineProperties onlineProperties;
@Autowired
private OnlineDataSourceUtil onlineDataSourceUtil;
@Override
public CallResult beforeInsertWithRelation(
OnlineTable masterTable, JSONObject masterData, Map<OnlineTable, List<JSONObject>> slaveTableAndDataMap) {
String columnName = masterTable.getPrimaryKeyColumn().getColumnName();
// 如果该配置项是true,说明所有的数据表操作都不在同一个事务内完成。
if (BooleanUtil.isTrue(onlineProperties.getEnabledMultiDatabaseWrite())) {
try {
//case1: 执行数据库的查询操作是ok的,因为没有数据变更。
String yourVerifySql = "select count(*) from yourtable";
List<Map<String, Object>> results = onlineDataSourceUtil.query(masterTable.getDblinkId(), yourVerifySql);
//case2: 如果需要修改当前待插入的数据,可以直接操作masterData对象。key是字段名,value书数值。
//common-online.enabledMultiDatabaseWrite为true的时候,此类方法都不要执行insert/update/delete等SQL,切记。
Object testKey = masterData.get("price");
if (testKey == null) {
masterData.put("price", 0.0f);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return CallResult.error(e.getMessage());
}
}
return CallResult.ok();
}
// ... ... 忽略其他方法实现。
}
- 对于 beforeInsert/beforeInsertWithRelation/beforeUpdate/beforeUpdateWithRelation 扩展方法,如果配置项 common-online.enabledMultiDatabaseWrite 为 false 时,由于所有操作均位于相同事务之内,因此所有「增删改」操作,将没有上一条的限制。
@Slf4j
@Component
public class OnlineExtendExampleExecutor extends BaseOnlineExtendExecutor {
@Autowired
private OnlineProperties onlineProperties;
@Autowired
private CourseMapper courseMapper;
@Autowired
private OnlineDataSourceUtil onlineDataSourceUtil;
@Override
public CallResult beforeInsertWithRelation(
OnlineTable masterTable, JSONObject masterData, Map<OnlineTable, List<JSONObject>> slaveTableAndDataMap) {
String columnName = masterTable.getPrimaryKeyColumn().getColumnName();
if (BooleanUtil.isFalse(onlineProperties.getEnabledMultiDatabaseWrite())) {
// 如果该配置项是false,所有操作都在同一个事务内,因此可以根据业务需求,执行正常的增删改操作。
courseMapper.insert(...);
}
return CallResult.ok();
}
// ... ... 忽略其他方法实现。
}
- 对于 afterInsert/afterInsertWithRelation/afterUpdate/afterUpdateWithRelation 扩展方法,如果配置项 common-online.enabledMultiDatabaseWrite 为 true 时,不要使用 Mybatis 直接操作业务表,而是要使用以下方法将所有「增删改」操作,记录到 TransactionalBusinessData 的 sqlDataList 中,最后在另一个事务内一起批量执行,见如下代码和注释说明。
@Slf4j
@Component
public class OnlineExtendExampleExecutor extends BaseOnlineExtendExecutor {
@Autowired
private OnlineProperties onlineProperties;
@Autowired
private OnlineDataSourceUtil onlineDataSourceUtil;
@Override
public void afterInsertWithRelation(
OnlineTable masterTable, JSONObject masterData, Map<OnlineTable, List<JSONObject>> slaveTableAndDataMap) {
String columnName = masterTable.getPrimaryKeyColumn().getColumnName();
// 如果该配置项是false,说明所有的数据表操作都是在同一个事务内完成。
if (BooleanUtil.isFalse(onlineProperties.getEnabledMultiDatabaseWrite())) {
//这里的所有数据表操作,都会在同一个事务内,因此直接通过正常的mybatis操作即可。
} else {
//如果配置项 onlineProperties.getEnabledMultiDatabaseWrite() 是true,说明这里最终执行的数据库操作
//和当前事务不在同一个事务之内。这里需要不要直接操作数据库,而是将要执行的增删改SQL,录入到以下对象内,在提交
//当前事务后,会在另一个新的事务中,执行所有录制的SQL,包括当前要执行的insert。后处理录制的方法,一定是在正常
//的insert语句之后执行。
String yourExecSql = "insert into yourtable value(?,?)";
List<Serializable> columnValues = new LinkedList<>();
columnValues.add(1);
columnValues.add("1");
super.addTransactionalBusinessData(masterTable.getDblinkId(), yourExecSql, columnValues);
}
}
// ... ... 忽略其他方法实现。
}
数据模型
在线表单页面操作的数据表数据,均来自于这里配置的一个主表和多个关联从表。
添加主表
点击「新增数据表」按钮添加当前在线表单所依赖的数据表,其中添加的第一张数据表为当前在线表单的主表。
- 数据源名称,数据源的中文显示名。
- 数据源标识,当前页面内唯一,推荐小驼峰命名规则。
- 数据源主表,在指定数据链接中选择主表。后面的所有关联表,都必须和主表位于相同的数据库链接。
添加关联表
目前橙单在线表单仅支持添加「一对一」和「一对多」的关联从表。
- 关联名称,请给出有意义的中文显示名。
- 关联标识,当前页面内唯一,推荐使用小驼峰命名规则。
- 关联类型,选择从表和主表之间的关联类型。目前仅支持「一对一」和「一对多」。
- 关联主表,该字段此处不能修改。
- 主表关联字段,选择主表中与从表关联的字段。
- 关联从表,从表必须与主表位于同一数据库链接。
- 从表关联字段,在从表中选择与主表关联的字段。
- 是否级联删除,主表数据删除时,会在相同事务内级联删除从表中的关联数据。
- 是否左连接,仅一对一关联支持。选择「否」时为内连接。
字段管理
点击「字段管理」操作按钮,进入如下页面。在该页面可以为主从表字段,设置必要的字段属性。
基本配置
- 基础配置介绍。
- 字段刷新,当数据表字段发生变化时,可将鼠标悬浮到指定的字段上,点击「刷新」按钮即可同步。
- 显示名称,字段在表单中默认的显示名称,默认值为字段在数据表中的注释。
- 是否必填,默认值是当前字段在数据表中「是否允许为空」的设置值,这里可以根据需要自行修改。如果必填为 TRUE,在编辑表单中,该字段所对应的组件数据,即为必填数据。
- 字典数据,可以为字段绑定字典。字典的配置方式,在前面的小节已经介绍了。查询时,会根据字段的字典 ID 值,自动翻译为「字典显示名称」值。在数据添加和编辑页面中,该字段的候选值,可从已绑定字典数据的下拉框中选择。
过滤支持
在进行列表查询页面设计时,过滤组件可以绑定的候选字段。换句话说,这里没有配置支持过滤的字段,不能被查询页面过滤组件所依赖。
- 无过滤,列表查询页面中的过滤组件,无法绑定到该字段。
- 普通过滤,等同于「等于」过滤。
- 范围过滤,等同于「大于等于」 + 「小于等于」过滤,比如时间范围字段。
- 模糊过滤,应用于字符型字段,等同于「LIKE %xx%」过滤。
- 多选过滤,仅当绑定了字典的字段,才能选择该过滤。比如在下拉框中,可以选择多个值作为过滤条件,等同于「IN」过滤。
数据权限过滤
如果当前在线表单页面所对应的主表,需要进行数据权限过滤,可在此处指定数据过滤字段。数据权限的配置,与普通路由表单完全一致。具体可参考开发文档 数据权限管理章节的数据权限配置小节。
- 用户过滤字段,指定与用户 ID 相关的字段,可应用于「仅查看当前用户」、「本部门所有用户」和「本部门及子部门所有用户」等数据权限过滤策略。
- 部门过滤字段,指定与部门 ID 相关的字段,可应用于「仅查看当前部门」、「所在部门及子部门」和「多部门及子部门」和「自定义部门列表」等数据权限过滤策略。
字段类别
- 文件上传字段,此类别的字段将在表单中使用上传组件。
- 图片上传字段,此类别的字段将在表单中使用上传组件,并且回显图片。
橙单通过 JSON 格式存储文件的上传信息,因此只有字符型字段才能选择该类别。另外,如果存储类型为「本地存储」,上传的文件将存储到配置 项 common-online.uploadFileBaseDir 指定的子目录内。对于「分布式存储」,将会存储到 minio / 阿里云 OSS / 腾讯云 COS / 华为云 OBS。具体的分布式存储类型,是在橙单代码生成器中配置的,见下图。
- 富文本字段,此类别的字段将在表单中使用富文本输入框。
- 多选字段,此类别字段在表单中,可以使用下拉多选、复选框等组件表示。字段中包含的多个数据值,会使用「逗号」分隔,因此只有字符型字段才可以选择该类别。
- 创建人部门,该字段值会在数据新增时,自动填充。
- 创建人字段,该字段值会在数据新增时,自动填充。
- 创建时间,该字段值会在数据新增时,自动填充。
- 更新人字段,该字段值会在数据新增和修改时,自动填充。
- 更新时间,该字段值会在数据新增和修改时,自动填充。
- 逻辑删除字段,用于标记数据被「逻辑删除」的字段。
- 自动编码字段,仅字符型字段可以选择该类别。选择该类别后,还需要设置编码的计算规则。
- 流程状态,数据值等同于工单表中的 flow_status 字段,仅当流程完成时,才会自动同步到该字段。
- 流程审批状态,数据值等同于工单表中的 latest_approval_status 字段,流程任务审批状态变化时,会将任务的审批状态值自动同步到该字段。
字段验证
为指定字段添加前端验证规则,同一个字段可以配置多个规则。
数据脱敏
在线表单和路由表单的数据脱敏规则大同小异,因此在开始阅读本小节内容之前,请先阅读开发文档 架构进阶必读章节的数据脱敏小节。本小节仅介绍与在线表单相关的数据脱敏操作步骤和实现细节。
脱敏字段配置
和路由表单的规则一样,都是只有「字符型」字段才能配置为脱敏字段,内置的脱敏规则也和路由表单完全一致。
表单配置
可以在表单设计页面,设置此表单哪些字段需要脱敏。主要注意的是,只有查询类型的表单支持脱敏字段选择,编辑和详情等二级表单页面,脱敏规则与查询页面设置一致,如下图所示。
自定义处理器
与路由表单不同,在线表单不支持为不同的脱敏字段提供不同的自定义脱敏规则处理器对象。
- 在业务服务中,提供在线表单自定义脱敏规则处理器实现类。
- 该类必须继承自 OnlineCustomMaskFieldHandler 类。
- 该类必须为 Bean 对象,并在服务启动时,将 this 对象注册到 OnlineCustomExtFactory 的 setCustomMaskFieldHandler 方法中,以供运行时使用。
- 在实际的业务开发中,可根据 handleMask 方法参数 appCode、tableName 和 columnName 进行条件分支的判断,以处理不同字段的自定义脱敏规则。
- 最后需要强调一下,handleMask 方法中的 modelName 参数,表示在线表单表对象名,如「SysUser」,而 objectFieldName 参数表示在线表单表字段对象的属性名,如「mobilePhone」。
聚合计算
为数据主表添加基于聚合计算的虚拟字段。添加后的虚拟字段,可以在列表中被正常显示,在详情页,也可以被组件绑定后显示。与普通数据表字段的差别是,虚拟字段不能编辑,也不会存入数据表,仅为查询过程中动态计算后的显示。
新增聚合计算虚拟字段。
- 结果字段列名,字段名别名,推荐使用小驼峰命名规则。
- 结果字段显示名,中文显示名称。
- 聚合关联,仅能选择「一对多」关联。
- 聚合计算表,一对多关联从表。
- 聚合计算字段,选择从表中参与聚合计算的字段。
- 结果字段类型,计算结果值的类型。
- 聚合计算规则,聚合计算的规则,如 SUM / COUNT / AVG / MIN / MAX 等。
添加过滤条件,用于过滤参与计算的一对多从表数据。
以上两图的配置,会生成如下 SQL,用于计算基于聚合计算的虚拟字段。
-- sumOfTotalAcount 上面定义的虚拟字段别名。
SELECT contract_id, SUM(total_count) sumOfTotalAmount
-- 一对多关联从表。
FROM zz_test_flow_contract_detail
-- 上面定义的过滤条件,多个条件用 AND 连接。
WHERE total_count > 100
-- 分组字段,一对多从表中与主表关联的字段。
GROUP BY contract_id
表单设计
这里我们以独立小节的形式专门介绍「表单设计」操作。在上一小节中,我们介绍了表单的「基础信息」和「数据模型」的配置。在「数据模型」的配置页面,点击「下一步」即可进入表单设计页面,在这里可以设计线表单的页面布局以及前端交互逻辑,见下图。
新建表单
这里可以创建多个表单,最典型的场景是先创建一个「数据管理」的列表页面,再配置一个「数据增删」的编辑页面。其中「列表页面」与菜单绑定,点击该页面的「操作」按钮,即可触发「数据增删」页面的显示。
- 点击「添加表单」按钮,创建新表单。
- 表单编码,表单的唯一标示值,推荐使用小驼峰命名规则。
- 表单名称,表单显示名称。
- 表单类型,具体规则如下。
表单类型 | 应用页面 | 功能描述 |
---|---|---|
查询表单 | 仅业务 | 可显示主表或一对多从表的列表数据。 |
编辑表单 | 业务和流程 | 可编辑主表或一对多从表数据。 |
流程表单 | 流程 | 用于流程数据处理的页面。 |
工单列表 | 流程 | 显示流程工单列表的页面,可与菜单绑定,表单数据要选择表单使用的主表。 |
- 表单数据,选择主表即可操作主表及其全部关联数据,选择关联仅能操作指定关联的数据。
表单设计
从左侧拖拽组件到中间的表单设计区域,点击组件后可在右侧的属性框中编辑组件属性,点击表单空白区域可设置表单属性。
自定义字段
- 为「表单」添加自定义字段,字段名推荐使用小驼峰命名规则。
- 自定义字段在脚本中的数据结构,以及在页面加载完成后的初始化脚本。
- 组件绑定自定义字段。
- 组件编写脚本操作该自定义字段。
添加操作按钮
目前只有「查询类型」的表单和任何类型表单的「表格组件」支持添加「操作」。
我们提供了一些内置操作,如「新建」、「编辑」、「删除」、「导出」等,默认为禁用状态,既不在页面中显示,需要时可以启用。
添加「自定义操作」。
- 操作名称,操作在页面的显示名。
- 是否启用,启用后可在页面显示,否则不显示。
- 操作按钮类型,对应 css 的显示样式。
- 操作表单,选择该操作触发后显示的表单。
- 只读表单,操作的表单是否为只读。
- 操作脚本,该操作会触发的自定义脚本,可根据实际需求自行编写。
打印配置
仅当支持打印报表模块时,该功能可用!
打印模板配置
为在线表单配置打印操作之前,请先配置好该表单所需的打印模板。具体配置可参考开发文档 报表打印章节的打印模板小节。
在线表单配置
- 批量打印的表单配置。
- 详情打印的表单配置。
打印插件代码
在生成器中配置工程时,如果同时选择支持「在线表单」和「报表打印」模块,那么在生成后工程的业务服务中,就会包含用于在线表单打印的处理器类 MyOnlinePrintHandler。之所以这样设计,是为了让「在线表单」模块不会直接依赖「报表打印」模块,从而实现模块间的最大化解耦,具体说明见下图。
表单脚本
本小节只是简单介绍脚本的基本用法,更多示例可参考后面的「实例讲解」小节。现在我们切换到「脚本」标签页,可以通过脚本来控制表单的行为,内置提供了十二种不同事件的脚本,通过脚本的组合可以实现复杂的功能定制,如下图所示。
在编辑脚本窗口左侧是表单数据展示,右侧是脚本编辑区域,可以通过脚本类型来切换脚本,目前支持的脚本类型如下。
- 组件数据改变。
/**
* 组件数据改变时触发
* @params val 组件当前值
* @params this 表单组件
*/
- 组件是否禁用。
/**
* 组件是否禁用
* @params this
* @return 组件是否禁用
*/
return false;
- 组件是否可见。
/**
* 组件是否可见
* @params this
* @return 组件是否可见
*/
return true;
- 组件下拉数据改变。
/**
* 组件下拉数据改变时触发(仅用于下拉组件、级联组件、单选组件以及复选框组件)
* @params dataList 组件下拉数据
* @params this 表单组件
* @return 新的组件下拉数据
*/
return dataList;
- 日期组件设置日期是否禁用函数。
/**
* 日期组件设置日期是否禁用函数(仅用于日期组件、日期范围选择组件)
* @params date 当前日期
* @return 是否禁用
*/
return false;
- 表格读取数据后回调。
/**
* 读取列表数据后触发
* @params dataList
* @params this 表单组件
* @reutrn 新的表格数据
*/
return dataList;
- 表格读取数据前回调。
/**
* 读取表格数据前触发
* @params params 请求参数
* @params this 表单组件
* @return 新的请求参数,如果返回null则停止获取数据
*/
return params;
- 表单页面加载数据后回调。
/**
* 表单页面加载数据后触发
* @params this 表单组件
*/
- 表单页面提交数据前回调。
/**
* 表单页面提交数据前触发
* @params params 请求参数
* @params this 表单组件
* @return 新的请求参数
*/
return params;
- 表单页面创建完毕。
/**
* 表单页面创建完毕,可用于初始化表单数据
* @params this 表单组件
*/
return params;
- 操作是否可见。
/**
* 操作是否可见
* @params rowData 表格行内操作当前行数据,如果是新建操作rowData是null
* @params this 表单组件
* @return 操作是否可见
*/
return true;
- 操作是否禁用。
/**
* 操作是否禁用
* @params rowData 表格行内操作当前行数据,如果是新建操作rowData是null
* @params this 表单组件
* @return 操作是否禁用
*/
return true;
表单数据
切换到「数据」标签页,可查看当前表单所依赖的数据主表及其关联从表的字段配置信息,点击字段即可快速修改字段属性。
菜单权限
在线表单被绑定到菜单后,其用户授权方式与路由表单完全一样。
菜单绑定
- 在线表单中一定要配置「查询表单」类型的表单,否则没法绑定到菜单。
- 为新建的在线表单手动创建菜单。
- 新建的在线表单菜单,会固定包含两个按钮类型的子菜单,并且这两个按钮类型的菜单不能修改和删除,同时也不能为该菜单再创建其他子菜单了。从下图可见,相比于普通表单的高细粒度操作权限控制,在线表单只区分「查看」和「编辑」两个控制级别。
用户权限配置
- 菜单绑定在线表单后,即可与角色进行关联。
- 再将角色分配给用户,用户重新登录后,即可拥有该在线表的的正常访问权限了。
基础权限配置
- 与路由表单不同,在线表单的权限字和权限是无需手动添加的。在系统登录时,登录接口会根据当前用户所分配的在线表单菜单,动态推演出与其关联的权限字和权限。
- 每个在线表单的按钮菜单「查看」和「编辑」,都会有一个权限字与之一一对应。
- 每个权限字,又都会有一组权限 URL 与之关联,在线表单权限字与权限的关联关系,位于应用服务的配置文件中,下图以单体服务为例。
- 由此得出结论,在线表单的菜单编码和后台权限字列表数据,无需像路由表单那样,在「菜单管理」页面进行额外的配置了。
注意事项
本小节主要介绍在线表单在使用过程中的常见问题。
修改URL前缀
因为在线表单的所有代码都位于 common-online 的通用模块中,这其中也包括 Controller 接口的请求地址。但是在很多场景中,不同工程的不同服务在引用在线表单时,URL 地址前缀也可能是不同的,因此我们提供了配置项,见如下截图。
如果修改了上图中的 urlPrefix 配置项,我们必须同步修改数据库中,在线表单配置相关的权限数据,如不同步修改,则会导致返回无权访问的 403 错误码。
API权限验证
所有在线表单的 CURD 数据接口都来自于 OnlineOperationController 中包含的接口方法,由于所有表单使用单一接口,所以就无法进行表单级别的权限划分。为了解决这一问题,我们使用 PathVariable 的方式,在接口 URL 中包含了数据源的变量名信息,这样不同的数据源接口就可以通过权限的方式,进行精确的控制了,见下图。
防SQL注入
所有入口参数均为数据源 ID、数据源关联 ID 和 Java 的字段名映射,并没有对外直接暴露数据表名和字段名。在 Controller 的代码中,会根据请求参数的数据源 ID 和数据源关联 ID,自动获取本次查询的表名和字段名,而并非直接使用前端的请求参数数据,这样就从机制上彻底消除了 SQL 注入的风险。而对于有些必须包含 SQL 信息的参数,如 OrderBy,Controller 中的 makeOrderBy 方法,会根据输入字段值与关联表字段进行对比,只有完全匹配,才会视为正常的 OrderBy 字段,否则直接报错。
对于请求参数中的数据值数据,我们直接使用了 SQL 中绑定变量,这样也从机制上消除了 SQL 注入的风险。
打印详解
- 在线表单的打印接口为 OnlineOperationController 类的 print 方法。见如下代码及关键性注释。
// 该方法并不进行实际的打印工作,而是对当前请求的参数数据进行合法性验证。通过验证后,会为本次调用生成
// 唯一的打印令牌,并存入与session关联的缓存中,再将实际打印接口及生成的打印令牌返回给前端。前端收到应答后,
// 会调用返回的实际打印接口完成打印。
@PostMapping("/print/{datasourceVariableName}")
public ResponseResult<String> print(
@PathVariable("datasourceVariableName") String datasourceVariableName,
@MyRequestBody(required = true) Long datasourceId,
@MyRequestBody(required = true) Long printId,
@MyRequestBody(required = true) List<JSONArray> printParams) throws IOException {
// 这里忽略打印数据验证的相关代码 ... ...
// 为本次打印请求生成唯一的打印令牌。
String token = MyCommonUtil.generateUuid();
// 将本次请求的打印令牌和打印参数,均存入会话关联的缓存中。出于安全考虑,仅返回打印令牌。
sessionCacheHelper.putSessionPrintTokenAndInfo(token, new MyPrintInfo(printId, printParams));
// 将打印令牌作为url参数返回给前端。
return ResponseResult.success(onlineProperties.getPrintUrlPath() + "?printToken=" + token);
}
- 实际打印方法的实现。该接口为 ReportPrintController 的 print 方法。
@GetMapping("/print")
public void print(@RequestParam String printToken) throws IOException {
// 根据打印令牌参数printToken,从会话关联的缓存中读取实际的打印数据。
// 如果不存在则视为无效调用。
MyPrintInfo printInfo = sessionCacheHelper.getSessionPrintInfoByToken(printToken);
if (printInfo == null) {
ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.NO_ACCESS_PERMISSION));
return;
}
// 将请求参数转换为打印服务所需的参数对象。
List<ReportPrintParam> reportPrintParams = new LinkedList<>();
for (JSONArray printParam : printInfo.getPrintParams()) {
ReportPrintParam reportPrintParam = new ReportPrintParam();
if (CollUtil.isNotEmpty(printParam)) {
reportPrintParam = new ReportPrintParam();
for (int i = 0; i < printParam.size(); ++i) {
ReportPrintParam.FilterInfo filterInfo =
printParam.getJSONObject(i).toJavaObject(ReportPrintParam.FilterInfo.class);
reportPrintParam.add(filterInfo);
}
}
reportPrintParams.add(reportPrintParam);
}
// 执行实际的打印,并将打印结果作为应答数据流,直接返回给前端。
if (CollUtil.isNotEmpty(reportPrintParams)) {
reportOperationHelper.print(printInfo.getPrintId(), reportPrintParams, PrintRenderType.PDF);
}
}
实例讲解
本小节示例,主要基于橙单的线上 DEMO 进行介绍,访问地址为 https://demo.orangeforms.com/。
页面初始化
使用「表单创建完毕」脚本,可以初始化页面的数据,如 DEMO 中的「报销申请表单」,我们初始化了报销总金额以及报销类别组件的默认值。
/**
* 表单页面创建完毕,可用于初始化表单数据
* @params this 表单组件
*/
this.formData.dsFlowSubmit.total_amount = 0;
this.formData.dsFlowSubmit.submit_kind = 1;
表格操作禁用
使用操作是否禁用脚本,可以控制操作是否禁用,如 DEMO 中「合同管理页面」,禁用了生产合同的详情操作。
/**
* 操作是否禁用
* @params rowData 表格行内操作当前行数据
* @params this 表单组件
*/
return rowData ? rowData.contract_type == 1 : false;
组件联动
使用组件数据改变脚本,可以在某组件数据改变时,修改其他组件的数据,如 DEMO 中「新建合同审批」表单,当合同类型切换到生产合同的时候,会将提成比例设置为 10%,并且灰化。
- 设置提成比例。
/**
* 组件数据改变时触发
* @params val 组件当前值
* @params this 表单组件
*/
if (val == 1) {
this.formData.dsFlowContract.commission_rate = 10;
}
- 禁用提成比例组件,给提成比例组件添加是否禁用脚本。
/**
* 组件是否禁用
* @params this
*/
return this.formData.dsFlowContract.contract_type == 1;
综合运用
这里我们以 DEMO 中的「商品管理表单」为例,通过运用多个脚本的组合,来实现较为复杂的前端交互功能。在「商品管理表单」中,可以按照价格范围过滤商品,在使用价格范围时,希望加入快捷选项,如:低价、高价以及自定义。在线示例入口可参考下图。
- 添加在线表单字典,表单设计中的自定义字段「priceType」会引用该字典。
- 添加自定义的表单字段「priceType」。
- 添加单选组件,并与自定义字段「priceType」绑定。
- 添加「表单页面创建完毕」脚本,并将自定义字段「价格区间类型」初始化为「低价」。
/**
* 表单页面创建完毕,可用于初始化表单数据
* @params this 表单组件
*/
// 初始化为低价
this.formData.customField.priceType = "0";
- 当「价格区间类型」组件数据改变时,判断价格区间类型是否为自定义,如果为自定义,设置价格范围为 0-100000。
/**
* 组件数据改变时触发
* @params val 组件当前值
* @params this 表单组件
*/
if (this.formData.customField.priceType == "10") {
this.formData.dsProduct.cost_price = [0, 100000];
}
- 仅当「价格区间类型」为自定义时,价格区间组件才可以输入。在价格范围组件中添加是否禁用脚本,判断价格区间类型是否为自定义。
/**
* 组件是否禁用
* @params this
*/
return this.formData.customField.priceType != 10;
- 根据价格区间类型,修改查询条件中的价格区间过滤。在表格组件中添加上获取表格数据前脚本,根据价格区间类型,修改参数中价格范围过滤参数。
/**
* 读取表格数据前触发
* @params params 请求参数
* @params this 表单组件
*/
let columnValueStart;
let columnValueEnd;
// 根据价格类型设置价格区间
switch (this.formData.customField.priceType) {
// 低价
case "0":
columnValueStart = 0;
columnValueEnd = 1000;
break;
// 高价
case "2":
columnValueStart = 5000;
break;
// 自定义价格
case "10":
if (Array.isArray(this.formData.dsProduct.cost_price) && this.formData.dsProduct.cost_price.length >= 2) {
columnValueStart = this.formData.dsProduct.cost_price[0];
columnValueEnd = this.formData.dsProduct.cost_price[1];
}
break;
}
let bfind = false;
// 遍历已有过滤参数,修改价格区间过滤参数值
params.filterDtoList.forEach(item => {
if (item.columnName === 'cost_price') {
item.columnValueEnd = columnValueEnd;
item.columnValueStart = columnValueStart;
bfind = true;
}
});
// 如果没有价格区间过滤字段,则添加
if (!bfind && (columnValueStart != null || columnValueEnd != null)) {
params.filterDtoList.push({
tableName: 'zz_test_flow_product',
columnName: 'cost_price',
filterType: 2,
columnValueStart: columnValueStart,
columnValueEnd: columnValueEnd
});
}
return params;
- 获取商品数据后,给价格添加上人民币符号。在表格组件中添加上获取表格数据后脚本,给价格字段添加上人民币符号,这里我们新增了一个字段 showPrice 作为表格中显示的字段。
/**
* 读取列表数据后触发
* @params dataList
* @params this 表单组件
*/
dataList.forEach(item => {
item.showPrice = '¥ ' + item.cost_price;
});
return dataList;
新增的字段需要在表格字段中使用自定义字段来添加进去,如下图所示。
最终效果如下图所示:
结语
赠人玫瑰,手有余香,感谢您的支持和关注,选择橙单,效率乘三,收入翻番。