动态表单(DynaForm)设计方案¶
普通表单是 myApps 运行时 PC 端标准表单容器:负责加载表单定义与文档数据、动态渲染设计器 HTML 模板、展示流程操作栏与审批人状态、处理保存/流程/打印/跳转等按钮动作,并向子字段、包含元素、嵌入网格视图提供统一的上下文 API。
| 层次 | 实现位置 |
|---|---|
| 后端 / Java | cn.myapps.core.runtime.dynaform.form.ejb.Form(obpm-core/.../form/ejb/Form.java) |
FormRunTimeServiceImpl(组装 Vue3 所需的 FormDataPacket / formTemplate) |
|
cn.myapps.core.common.model.activity.ActivityType(按钮类型常量) |
|
| 表单设计器 | Form.vue(obpm-designer-vue3/src/components/Modules/Form.vue) |
FormBasic.vue、FormFormat.vue、FormOperation.vue(modulesDetail/) |
|
| 前端运行时(PC) | form_normalform.vue(obpm-runtime-web/portal/vue3/src/components/form/form_normalform.vue) |
obpm_open_container.vue(页签 / 弹出层打开容器) |
|
ActivityType.js(与后端 activity.type 一致) |
|
| 相关运行时组件 | activity.vue、collapse_activity.vue、approvers.vue、form_flow_dialog.vue、form_template.vue |
本文档 普通表单 对应后端 FORM_TYPE_NORMAL(type = 1);移动端有独立实现,下文仅描述 PC Vue3 运行时。
后端定义(Java)¶
Form 继承 FileSystemDesignTimeSerializable,实现 ActivityParent、Cloneable。通过 JAXB 序列化为 XML(根元素 <Form>),JSON 序列化时忽略空字段(@JsonInclude(NON_EMPTY))。设计态持久化在模块目录下,路径后缀见 ModelSuffix.FORM_PATH_SUFFIX / FORM_FILE_SUFFIX。
表单类型常量¶
| 常量 | 十六进制 | 十进制 | 类型名(getTypeName) | 说明 |
|---|---|---|---|---|
FORM_TYPE_NORMAL |
0x0000001 |
1 | NORMALFORM |
普通表单(NormalForm 运行时) |
FORM_TYPE_FRAGMENT |
0x0000002 |
2 | cn.myapps...fragment |
标签页 |
FORM_TYPE_FLOW_PARAMETER |
0x0000003 |
3 | — | 流程参数表(平台模式) |
FORM_TYPE_DATA_MODEL |
0x0000004 |
4 | NORMAL_DATAMODEL |
数据模型表单 |
FORM_TYPE_SUBFORM |
0x0000010 |
16 | — | 子表单 |
FORM_TYPE_SEARCHFORM |
0x0000100 |
256 | SEARCHFORM |
查询表单 |
FORM_TYPE_HOMEPAGE |
0x0001000 |
4096 | HOMEPAGE |
首页 |
FORM_TYPE_NORMAL_MAPPING |
0x0010000 |
65536 | NORMAL_MAPPING |
映射表单 |
FORM_TYPE_TEMPLATEFORM |
0x0100000 |
1048576 | TEMPLATEFORM |
模板(阅读)表单 |
设计器 FormBasic 下拉选项与上表 十进制 type 一致。
专有属性¶
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
templatecontext |
String |
— | 表单 HTML 模板源码(CDATA);设计器「内容」页保存至此 |
type |
int |
— | 表单类型(见上表) |
styleId |
String |
— | 样式库 id;运行时 getStyle() 懒加载 StyleRepositoryVO |
showType |
String |
"old" |
模板渲染模式:old / new;兼容旧值 pc→old、mobile→new |
layoutType |
String |
"horizontal" |
移动端布局:horizontal / vertical |
permissionType |
String |
"public" |
授权:public / private(setter 空值兜底为 private) |
orderno |
int |
— | 模块内排序(0–1000000,JSR303 @Range) |
version |
int |
— | 表单版本号 |
showLog |
boolean |
false |
是否记录/展示操作日志 |
showLogType |
String |
"default" |
日志展示:default / table |
recordMode |
String |
— | 记录方式:every / other / script |
recordModeScript |
String |
— | 记录方式脚本 |
openComment |
boolean |
false |
是否开通评论 |
commentTitleScript |
String |
— | 评论标题脚本 |
commentFlagScript |
String |
— | 评论标识脚本(绑定 docId) |
commentHiddenScrtipt |
String |
— | 隐藏评论区脚本(返回 true 则关闭评论) |
commentExecuteScript |
String |
— | 评论/回复提交后执行脚本 |
showWaterMark |
Boolean |
false |
是否显示水印 |
waterMarkScript |
String |
— | 水印文本脚本 |
isopenablescript |
String |
— | 是否可打开脚本(布尔);空时运行时按 "true" 处理 |
iseditablescript |
String |
— | 是否可编辑脚本(布尔);参与 Document.isEditable() |
confirmLeaveEdit |
boolean |
false |
离开编辑时弹窗确认;开启后,可编辑状态下关闭页签、返回、刷新或离开页面时提示未保存 |
mappingStr |
String |
— | 映射表 JSON/XML 字符串(映射表单) |
mapping |
TableMapping |
— | 懒加载字段映射对象 |
documentSummaryXML |
String |
— | 文档摘要配置 XML |
summaryCfg |
Collection<SummaryCfgVO> |
— | 摘要配置集合(懒加载) |
checkout / checkoutHandler |
— | — | 设计态签出状态与签出人 |
sortId |
String |
— | 排序标识 |
activities |
List<Activity> |
— | 表单工具栏按钮;懒加载自 ActivityDesignTimeService |
继承 / 关联结构¶
| 成员 | 说明 |
|---|---|
_fields |
Map<String, FormField>,字段 id → 字段对象 |
_elements |
ArrayList<FormElement>,模板元素顺序(旧版 showType=old 时字段也入列) |
_textparts |
模板文本片段 |
subFormMap |
Tab 等引用的子表单 |
module / getParent() |
所属模块 |
style |
运行时解析的样式库 VO(非 XML 直出字段) |
模板解析与 HTML 生成¶
初始化(inited()):首次调用时 TemplateParser.parseTemplate(this, templatecontext),将 templatecontext 解析为 _fields、_textparts、_elements;addField 时 showType=old 会把字段加入 _elements。
运行时 HTML(getHtmlTemplate(doc, runner, user)):
| showType | 行为 |
|---|---|
old |
遍历 _elements,各 FormElement.toHtmlTemplate(...) 拼接 |
new |
遍历 _elements 中非 FormField 节点;含 fieldid 占位时替换为对应 FormField.toHtmlTemplate |
多语言占位 {*[...]*} 在输出前编码替换。Vue3 运行时最终模板字符串由 FormRunTimeServiceImpl 组装(含 hidden 刷新域 + getHtmlTemplate 结果),写入 formTemplate.template。
文档生命周期(Form 核心方法)¶
| 方法 | 说明 |
|---|---|
createDocument(params, user) |
新建临时文档(istmp=true),分配 id,按表单类型设置 mappingId,addItems + recalculateDocument |
addItems(doc, params, user) |
遍历 ValueStoreField,按字段 fieldtype 从 params 创建 Item 并挂到文档 |
recalculateDocument(doc, params, calcAll, user) |
执行字段值脚本;受 calculateOnRefresh、isDefaultValue、新建 istmp 等条件控制 |
recalculateDocument4Local(...) |
控件变更触发的局部重算(refreshOnChanged + refreshFields) |
validate(doc, params, user) |
执行字段校验脚本;跳过 hidden 且 displayType=HIDDEN 的字段 |
findField / findFieldByName |
按 id / name 查找字段(含子表单 getAllFieldMap) |
getAllFields / getValueStoreFields |
全部字段 / 需持久化值的字段 |
getIncludeViewList |
收集 IncludeField、TabField 内嵌视图 |
findActivity / getActivityByType |
查找工具栏按钮(含 ButtonField 内嵌 activity) |
getOnActionFlow |
取「流程处理」按钮关联的 flowId |
mappingId 规则:FORM_TYPE_NORMAL / FORM_TYPE_DATA_MODEL → mappingId = docId;FORM_TYPE_NORMAL_MAPPING → 独立 UUID。
DDL 建表/变更(动态表同步)¶
普通表单(以及其它需要持久化的表单类型)在 保存即建表/改表——平台没有独立的「发布/部署」步骤。设计器 FormController 调用 FormDesignTimeService.save/update/delete 时,会**在同一事务内**把表单定义同步为运行库的物理表结构。
是否建表¶
FormTableProcessBean.isHasDynaTable(form) 决定该表单类型是否生成动态表(1 为真):
Form.type |
建表 | 表前缀 |
|---|---|---|
FORM_TYPE_NORMAL(1) |
✓ | TLK_ |
FORM_TYPE_FLOW_PARAMETER(3) |
✓ | PARM_ |
FORM_TYPE_NORMAL_MAPPING(65536) |
✓ | 自定义(mappingStr.tableName) |
FORM_TYPE_SUBFORM(16) |
✓ | TLK_ |
FORM_TYPE_DATA_MODEL(4) |
✓ | 外部指定 |
FORM_TYPE_FRAGMENT(2)/ SEARCHFORM(256)/ TEMPLATEFORM(1048576) |
✗ | — |
整体调用链¶
FormController (designer)
└─ FormDesignTimeServiceImpl.save / update / delete // 事务边界
└─ FormTableProcessBean.createDynaTable / createOrUpdateDynaTable / dropDynaTable / synDynaTable
└─ RuntimeDaoManager.getFormTableDAO(conn) // 按数据库类型分发
└─ AbstractFormTableDAO.createDynaTable / updateDynaTable / dropDynaTable
└─ ChangeLog.compare(newForm, oldForm) // 新旧表单 → 变更列表
└─ AbstractTableDefinition.processChanges(log) // 反射分发
├─ processChange(AddTableChange) → createTable
├─ processChange(AddColumnChange) → addColumn
├─ processChange(DropColumnChange) → dropColumn
├─ processChange(ColumnRenameChange)→ columnRename
├─ processChange(ColumnDataTypeChange)→ columnRename(4 步只启用第 1 步)
├─ processChange(TableRenameChange) → createTableAsSelect + dropTable
└─ processChange(DropTableChange) → dropTable
└─ SQLBuilder(各数据库子类)拼出 SQL
└─ evaluateBatch(sql) → statement.executeUpdate(sql)
设计态入口(FormDesignTimeServiceImpl)¶
| 方法 | 行为 |
|---|---|
save(user, Form) |
新建表单;事务内 tableProcess.createDynaTable(vo) 后持久化 XML |
update(user, Form) |
更新表单;遍历表单及其父表单(getSuperiors),对每个 createOrUpdateDynaTable(form, oldForm);version + 1 |
delete(user, appId, id) |
先 docProcess.doRemoveByFormName(form) 清数据,再 dropDynaTable(form) |
doChangeValidate(Form) |
保存前用 ChangeLog.compare + process.doChangeValidate(log) 做破坏性变更校验(如删列、缩长度) |
doClearColumnData(Form, fields) |
字段被删/改类型时清空对应列数据(DML:update TLK_xxx set ITEM_xx = null) |
事务由 tableProcess.beginTransaction() / commitTransaction() / rollbackTransaction() 显式包裹;失败即整体回滚,XML 与数据库保持一致。
表名与列名命名规则¶
命名常量集中在 DQLASTUtil:
| 常量 | 值 | 用途 |
|---|---|---|
TBL_PREFIX |
TLK_ |
普通表单/子表单/主页表单动态表前缀 |
PARM_PREFIX |
PARM_ |
流程参数表前缀 |
LOG_PREFIX |
LOG_ |
操作日志表前缀(加在 TLK_ 之后) |
ITEM_FIELD_PREFIX |
ITEM_ |
业务字段列前缀 |
TEMP_PREFIX |
TMP_ |
改列类型时的临时列前缀 |
T_DOCUMENT |
T_DOCUMENT |
所有映射表单共用的文档头表 |
表名与列名映射核心在 TableMapping(构造时按 Form.type 分支):
FORM_TYPE_NORMAL / SUBFORM / HOMEPAGE / SEARCHFORM:表名TLK_ + form.name;列名规则:$开头字段 → 去掉$大写;其它 →ITEM_+ 字段名大写。FORM_TYPE_FLOW_PARAMETER:表名PARM_ + form.name;列名ITEM_+ 字段名大写。FORM_TYPE_NORMAL_MAPPING:解析mappingStrJSON,表名与列名完全由设计器配置;主键列固定MAPPINGID。FORM_TYPE_DATA_MODEL:表名留空(外部指定),列名按字段名直取。
DQLASTUtil.getItemTblName(formname, tableType) 负责按表类型加 LOG_ 等前缀;TableMapping.getTableName(tableType) 在其上再加 schema 前缀。
建表字段映射¶
字段类型映射分两步:
步骤 1:FormField.fieldtype → JDBC Types(FieldConstant.getTypeCode)
| fieldtype | JDBC Types |
|---|---|
null |
Types.NULL(0) |
VALUE_TYPE_VARCHAR |
Types.VARCHAR(1) |
VALUE_TYPE_NUMBER |
Types.NUMERIC(2) |
VALUE_TYPE_DATE |
Types.TIMESTAMP(3) |
VALUE_TYPE_TEXT |
Types.CLOB(4) |
VALUE_TYPE_BLOB |
Types.BLOB(5) |
步骤 2:JDBC Types → 各数据库列类型(各 *Builder 构造函数 registerColumnType)
| JDBC Type | MySQL | Oracle | SQL Server | PostgreSQL | KingBase | DM | OceanBase | Oscar |
|---|---|---|---|---|---|---|---|---|
VARCHAR |
VARCHAR(100) |
VARCHAR2(100) |
NVARCHAR(100) |
VARCHAR(100) |
VARCHAR(100) |
VARCHAR(100) |
VARCHAR(100) |
VARCHAR(100) |
LONGVARCHAR / CLOB |
LONGTEXT |
CLOB |
NTEXT |
TEXT |
TEXT |
CLOB |
TEXT |
TEXT |
NUMERIC |
DECIMAL(22,10) |
NUMBER(22,5) |
NUMERIC(22,5) |
DECIMAL(22,10) |
DECIMAL(22,10) |
DECIMAL(22,10) |
DECIMAL(22,10) |
DECIMAL(22,10) |
INTEGER |
INT |
NUMBER(20,0) |
NUMERIC(10,0) |
INT |
INT |
INT |
INT |
INT |
BIT |
BIT(1) |
NUMBER(1,0) |
BIT |
BOOLEAN |
BIT(1) |
BIT |
BIT(1) |
BIT(1) |
TIMESTAMP |
DATETIME |
TIMESTAMP(6) |
DATETIME |
TIMESTAMP(14) |
TIMESTAMP(6) |
TIMESTAMP |
DATETIME |
TIMESTAMP(6) |
BLOB |
MEDIUMBLOB |
BLOB |
IMAGE |
BYTEA |
BLOB |
BINARY |
BLOB |
BLOB |
长度/精度由 Column.length / Column.precision 覆盖(SQLBuilder.getSqlTypeName)。
固定列(公共系统字段)¶
每张动态表(TLK_* / PARM_* / LOG_TLK_*)除了 ITEM_ 业务列之外,都会先建一组**固定系统字段**承载文档元数据。固定列清单在 DDL 端由 ChangeLog.getFixedColumns() 维护(不含主键,主键在 createTableByType 末尾按 TableMapping.getPrimaryKeyName() 单独追加),共 34 列。
列详细定义(按建表顺序)
| # | 列名 | JDBC 类型 | 取值来源(setBaseParameters / doc.*) |
用途 |
|---|---|---|---|---|
| 1 | PARENT |
VARCHAR |
doc.getParentid() |
父文档 id;IncludeField/子表单文档关联父文档 |
| 2 | LASTMODIFIED |
TIMESTAMP |
doc.getLastmodified() |
最后修改时间(增量同步 LASTMODIFIED > ? 用) |
| 3 | FORMNAME |
VARCHAR |
doc.getFormname() |
表单名(含模块路径);用于按表单过滤/分组 |
| 4 | STATE |
VARCHAR |
doc.getStateid() |
当前流程状态 id |
| 5 | AUDITUSER |
VARCHAR |
doc.getAudituser() |
当前审批人 id |
| 6 | AUDITDATE |
TIMESTAMP |
doc.getAuditdate() |
审批时间 |
| 7 | AUTHOR |
VARCHAR |
doc.getAuthor().getId() |
创建人 id |
| 8 | AUTHORDEPTID |
VARCHAR |
doc.getAuthorDeptId() |
创建人所属部门 id |
| 9 | AUTHOR_DEPT_INDEX |
VARCHAR(2000) |
doc.getAuthorDeptIndex() |
作者部门索引(路径型,长度 2000) |
| 10 | AUTHOR_USER_INDEX |
VARCHAR(2000) |
doc.getAuthorUserIndex() |
作者用户索引(路径型,长度 2000) |
| 11 | CREATED |
TIMESTAMP |
doc.getCreated() |
创建时间 |
| 12 | FORMID |
VARCHAR |
((Document)doc).getFormid() |
表单 id(设计态) |
| 13 | SUBFORMIDS |
CLOB |
((Document)doc).getSubFormids() |
子表单 id 列表(多值序列化) |
| 14 | INITIATOR |
VARCHAR |
doc.getInitiator() |
流程发起人 id |
| 15 | ISTMP |
BIT |
doc.getIstmp() ? 1 : 0 |
是否临时文档(新建未保存/草稿) |
| 16 | VERSIONS |
INTEGER |
doc.getVersions() + 1 |
文档版本(每次更新 +1) |
| 17 | APPLICATIONID |
VARCHAR |
doc.getApplicationid() |
所属软件 id(多租户隔离) |
| 18 | STATEINT |
INTEGER |
doc.getStateInt() |
流程状态数值(运行/草稿/挂起/结束等) |
| 19 | STATELABEL |
VARCHAR |
doc.getStateLabel() |
流程状态显示标签 |
| 20 | AUDITORNAMES |
CLOB |
doc.getAuditorNames() |
当前审批人姓名列表(多值) |
| 21 | LASTFLOWOPERATION |
VARCHAR |
doc.getLastFlowOperation() |
最后一次流程操作类型 |
| 22 | LASTMODIFIER |
VARCHAR |
doc.getLastmodifier() |
最后修改人 |
| 23 | DOMAINID |
VARCHAR |
doc.getDomainid() |
所属域 id(多租户隔离) |
| 24 | AUDITORLIST |
CLOB |
doc.getAuditorList() |
审批人候选列表(结构化) |
| 25 | COAUDITORLIST |
CLOB |
doc.getCoAuditorList() |
协办审批人候选列表 |
| 26 | STATELABELINFO |
CLOB |
doc.getStateLabelInfo() |
流程状态扩展信息 |
| 27 | PREVAUDITNODE |
CLOB |
doc.getPrevAuditNode() |
上一审批节点信息 |
| 28 | PREVAUDITUSER |
CLOB |
doc.getPrevAuditUser() |
上一审批审批人 |
| 29 | OPTIONITEM |
CLOB |
((Document)doc).getOptionItem() |
选项项缓存(用于下拉/复选快速渲染) |
| 30 | SIGN |
CLOB |
((Document)doc).getSign() |
签名数据 |
| 31 | KINGGRIDSIGNATURE |
CLOB |
doc.getKinggridSignature() |
金格签章数据 |
| 32 | SECRET |
VARCHAR |
(由密级相关逻辑写入) | 文档密级 |
特殊列(非 getFixedColumns() 维护)
| 列名 | 类型 | 来源 | 说明 |
|---|---|---|---|
ID |
VARCHAR |
createTableByType 追加;INSERT 写 doc.getId();UPDATE 不写 |
普通表单/子表单/主页/参数表主键(primaryKey=true) |
MAPPINGID |
VARCHAR |
同上(TableMapping.MAPPINGID) |
映射表单(FORM_TYPE_NORMAL_MAPPING)主键 |
DOC_ID |
VARCHAR |
createTableByType 在 TABEL_TYPE_LOG 时追加 |
操作日志表(LOG_TLK_*)关联主文档 id |
注释掉的列(已废弃,源码中保留但不建表)
| 列名 | 原用途 |
|---|---|
ISSUBDOC |
是否子文档 |
FLOWID |
流程 id |
SORTID |
排序 id |
ID(在 getFixedColumns 内) |
已挪到 createTableByType 末尾按主键单独追加 |
三种表的列差异
| 表类型 | 表名前缀 | 主键 | 额外列 | 固定列来源 |
|---|---|---|---|---|
| 内容表(CONTENT) | TLK_ |
ID |
— | getFixedColumns() + ID |
| 流程参数表(PARM) | PARM_ |
ID |
— | 同上 |
| 映射表(MAPPING) | 自定义 | MAPPINGID |
— | 同上(列名由 mappingStr 配置) |
| 操作日志表(LOG) | LOG_TLK_ |
ID |
DOC_ID |
getFixedColumns() + ID + DOC_ID |
注:映射表(
FORM_TYPE_NORMAL_MAPPING)的列名由设计器mappingStr.columnMappings配置,**不**走ITEM_前缀规则;但固定列名仍按上表。
写入流程(INSERT/UPDATE)
固定列由 AbstractDocStaticTblDAO.setBaseParameters(...) 统一赋值:
```496:578:obpm-core/src/main/java/cn/myapps/core/runtime/dynaform/document/dao/AbstractDocStaticTblDAO.java protected int setBaseParameters(int currentIndex, PreparedStatement statement, IDocument doc, int tabelType, boolean withoutBaseAttirbute,boolean isUpdate) throws Exception { if (tabelType == DQLASTUtil.TABEL_TYPE_CONTENT) { if(!isUpdate){ statement.setString(++currentIndex, doc.getId()); // ID(仅 INSERT) } } else if (tabelType == DQLASTUtil.TABEL_TYPE_LOG) { statement.setString(++currentIndex, Sequence.getTimeSequence()); // LOG 表主键 }
statement.setString(++currentIndex, doc.getParentid()); // PARENT
// ...LASTMODIFIED / FORMNAME / STATE / STATEINT / INITIATOR / AUDITUSER / AUDITDATE
// / AUTHOR / AUTHORDEPTID / AUTHOR_DEPT_INDEX / AUTHOR_USER_INDEX / CREATED
// / FORMID / SUBFORMIDS / ISTMP / VERSIONS(=doc.getVersions()+1)
// / APPLICATIONID / STATELABEL / OPTIONITEM / SIGN / AUDITORNAMES
// / LASTFLOWOPERATION / LASTMODIFIER / DOMAINID / AUDITORLIST / COAUDITORLIST
// / STATELABELINFO / PREVAUDITNODE / PREVAUDITUSER / KINGGRIDSIGNATURE / INITIATOR
return currentIndex;
}
- `AUTHOR_DEPT_INDEX` / `AUTHOR_USER_INDEX`:固定长度 `2000`(通过 `new Column("", "AUTHOR_DEPT_INDEX", Types.VARCHAR, "2000")` 传入,覆盖默认 `100`)。
- `VERSIONS`:写入时自增(`doc.getVersions() + 1`),UPDATE 时同样自增,**每次保存版本号都会前进**。
- `withoutBaseAttirbute` 标志:日志表与内容表对 `INITIATOR` / `KINGGRIDSIGNATURE` 的写入顺序略有差异(控制这两个字段在 SQL 列中的位置)。
- `MAPPINGID`:仅映射表单的 INSERT/UPDATE 末尾写入。
**读出流程(SELECT)**
读取结果集时,固定列会被 `DQLASTUtil.isDocumentFixedColumn(name)` 过滤掉,**不会**被误当业务 Item 拼成 `ITEM_xxx` 写回 `Document.items`:2062:2111:obpm-core/src/main/java/cn/myapps/core/runtime/dynaform/document/dao/AbstractDocStaticTblDAO.java
protected String resolveItemFieldName(String columnLabel, TableMapping tableMapping,
Form form, ResultSetMetaData metaData) throws SQLException {
// 1. 优先按 ITEM_ 前缀解析
if (DQLASTUtil.isDocumentFixedColumn(columnLabel)) {
return null; // 固定列不当业务 Item
}
// 2. 解析 ITEM_xxx 前缀,剥离后得到业务字段名
// 3. 不存在 ITEM_ 前缀时回退裸列名
// ...
}
```
三方同步约束(关键)
固定列清单在 DDL 端与 DML/DQL 端**各维护一份**,必须**手动保持同步**:
| 端 | 维护位置 | 作用 |
|---|---|---|
| DDL(建表) | ChangeLog.getFixedColumns() |
决定建表建哪些列 |
| DML(写库) | AbstractDocStaticTblDAO 的 INSERT/UPDATE SQL 列清单 + setBaseParameters |
决定写入哪些列、写入什么值 |
| DQL(读库) | DQLASTUtil.DOCUMENT_FIXED_COLUMNS |
读取时过滤,不把固定列当业务 Item |
新增固定列时,必须同时修改三处,否则会出现以下错位: - 只改 DDL:DML 不写值,列始终为
NULL; - 只改 DML:DDL 未建列,INSERT 报「列不存在」; - 只改 DQL:固定列被当成业务 Item 写入Document.items,且会以ITEM_xxx列名参与 DQL,引发查询错误。重要:
DQLASTUtil.DOCUMENT_FIXED_COLUMNS还包含了MAPPINGID/DOC_ID这两个主键/日志表专用列,覆盖范围比ChangeLog.getFixedColumns()略广(38 vs 34),目的是让 DQL 过滤一次性覆盖所有特殊列。
表变更(ALTER)类型¶
变更事件抽象在 obpm-common 的 cn.myapps.common.util.table.alteration 包,基类 ModelChange:
| 变更类 | DDL 动作 | 生成处 |
|---|---|---|
AddTableChange |
CREATE TABLE |
ChangeLog.compare:旧表不存在时 |
DropTableChange |
DROP TABLE |
ChangeLog.compare:新表为空、旧表存在时 |
TableRenameChange |
CREATE TABLE AS SELECT + DROP TABLE |
表单改名时(processChange 注释掉了独立 create+copy,改用 CTAS) |
AddColumnChange |
ALTER TABLE ADD |
新字段、固定列缺失 |
DropColumnChange |
ALTER TABLE DROP COLUMN |
字段被删除 |
ColumnRenameChange |
见方言 | 字段改名 |
ColumnDataTypeChange |
仅执行改名步骤 | 字段类型改变(add/copy/drop 三步被注释,等价于 rename) |
ColumnRemarksChange |
修改列注释 | 列描述变化 |
特殊抵消:ChangeLog.filter() 会把「同列名+同类型」的 AddColumnChange 与 DropColumnChange 互相抵消(避免无意义 DDL)。
多数据库方言差异(DDL 层)¶
| 操作 | MySQL | Oracle | SQL Server | PostgreSQL / KingBase | DM |
|---|---|---|---|---|---|
| 改列名 | ALTER TABLE t CHANGE old new TYPE |
ALTER TABLE t RENAME COLUMN old TO new |
EXEC SP_RENAME 't.old', 'new', 'COLUMN' |
ALTER TABLE t ALTER old TYPE(合并改名与改类型) |
RENAME COLUMN |
| 改列类型 | CHANGE col col NEWTYPE |
MODIFY col NEWTYPE |
ALTER COLUMN |
ALTER COLUMN ... TYPE |
CHANGE col col NEWTYPE |
| COMMENT | 行内 COMMENT 'xxx' |
独立 COMMENT ON COLUMN |
无原生 | 无原生 | 独立 COMMENT ON COLUMN |
| 表全名 | schema.t |
schema.t |
DBO.t |
schema.t |
schema.t |
| schema 强制 | — | — | — | KingBase / OceanBase 强制 PUBLIC. |
— |
说明:
processChange(ColumnDataTypeChange)在AbstractTableDefinition行 110–143 中只启用了 4 步流程的第 1 步(columnRename),第 2–4 步(add 新列、copy 数据、drop 临时列)被注释掉了。这是平台为避免数据迁移风险做的简化,副作用是改类型不会真正改库表列类型——需在变更前/后由doClearColumnData清空数据。
数据库方言分发¶
| 入口 | 行为 |
|---|---|
DbTypeUtil.getDBType(conn) |
通过 conn.getMetaData().getDatabaseProductName() 判断数据库类型,返回字符串常量(MYSQL / ORACLE / MSSQL / POSTGRESQL / DM / KINGBASE / OCEANBASE / OSCAR / DB2 / HSQLDB / H2) |
RuntimeDaoManager.getFormTableDAO(conn) |
按数据库类型 new 出对应 *FormTableDAO(MysqlFormTableDAO / OracleFormTableDAO / …) |
RuntimeDaoManager.getFormTableDAODtId(conn, datasourceId) |
多数据源场景:根据 datasourceId 取连接后分发 |
各 *FormTableDAO |
实现两个工厂方法:getTableDefinition() 返回对应 *TableDefinition,getValidator() 返回对应 *Validator |
每个数据库一套实现(*Builder 生成 SQL + *TableDefinition 执行 + *Validator 校验破坏性变更),统一位于 obpm-core/src/main/java/cn/myapps/core/designtime/table/ddlutil/<db>/。共 10 套(MySQL / Oracle / SQL Server / PostgreSQL / KingBase / DM / OceanBase / Oscar / DB2 / HSQLDB)。
操作日志表(LOG 表)¶
showLog=true 的表单额外维护一张操作日志表:
- 表名 =
LOG_ + 原表名(如TLK_FORM1→LOG_TLK_FORM1,由DQLASTUtil.getItemTblName(name, TABEL_TYPE_LOG)生成) - 列结构与主表一致,额外加
DOC_ID列关联主文档 - 生命周期由
AbstractFormTableDAO.createDynaTable/updateDynaTable/dropDynaTable内部if (form.isShowLog())分支驱动:开启日志 → 创建/更新 LOG 表;关闭日志 → 删除 LOG 表
多数据源同步(synDynaTable)¶
支持把同一表单同步到额外数据源(用于读写分离、报表库等):
FormTableProcessBean.setDatasourceId(dsId)指定目标数据源;- 调用
synDynaTable(newForm, oldForm)→ 经DataSourceDesignTimeService查到DataSource→RuntimeDaoManager.getFormTableDAODtId(conn, dsId)在该数据源连接上执行同样的updateDynaTable(newForm, oldForm, dt)。
入口:设计器 DataSourceController 在数据源维护时遍历表单触发同步。
系统初始化建表(区别于动态表)¶
平台有两套独立的 DDL 机制,不要混淆:
| 机制 | 用途 | 入口 |
|---|---|---|
| 动态表 DDL(本节主题) | 表单设计→运行库表 | FormTableDAO + TableDefinition + SQLBuilder |
| 系统表 DDL | 平台自身表(用户、部门、流程实例等) | AbstractApplicationInitDAO(initTables + compare,只做新增表/列,不做类型变更) |
| Schema 查询 | 查库表结构供 ChangeLog 对比 | AbstractSystemSchemaDAO(MYSQLSchemaDAO / ORACLESchemaDAO 等) |
| Liquibase(数据模型表单专用) | FORM_TYPE_DATA_MODEL 的版本化迁移 |
LiquibaseAPIGenerator(**不**用于普通动态表单 DDL) |
启动时 InitApplicationTable 会对所有应用的所有表单调用 formService.update(...),确保 DT(设计态)与 RT(运行态)数据库表结构同步。
运行时数据包组装(FormRunTimeServiceImpl)¶
GET .../forms/{formId}/documents/{docId} 与 empty 流程最终由 FormRunTimeServiceImpl 读取 Form + Document,输出 FormDataPacket:
| Form 属性 | 下发 / 行为 |
|---|---|
isopenablescript |
空则视为 "true";脚本非 Boolean false → formTemplate.template 为 HTML,否则 template=false |
iseditablescript |
Document.isEditable() 与流程权限叠加;false 则整单只读 |
confirmLeaveEdit |
→ formTemplate.confirmLeaveEdit;true 且文档可编辑时,离开编辑场景弹窗确认 |
showWaterMark + waterMarkScript |
→ waterMarkText |
openComment + 评论脚本 |
隐藏脚本可关闭;→ openComment、commentTitle、commentFlag |
showLog + showLogType |
→ formTemplate.showLog / showLogType;default 模式标记 isModified |
showType |
→ formTemplate.showType |
styleId |
样式库 → style.content |
description |
优先作为 formTemplate.formName |
| 各字段 | toAttributes(...) → fields[](Tab 递归 buildTabModifyField) |
activities |
工具栏按钮列表(含排序、只读标记) |
继承自 FileSystemDesignTimeSerializable 的常用属性¶
| 属性 | 说明 |
|---|---|
id / name |
表单标识与名称 |
applicationid |
所属软件 id |
description / remark |
描述与备注 |
parent |
所属模块(Module) |
uri |
资源 URI,脚本 ScriptLabel 使用 |
设计器配置¶
表单设计器入口为模块详情页 Form.vue,页签「基本」由 FormBasic.vue 承载。保存时 FormBasic.save() 与 FormFormat.getHtmlTemplate() 合并为 templatecontext 一并提交。
设计器接口:GET /designer/api/designtime/applications/{appId}/modules/forms/{formId}(getModuleForm);样式库列表 getStyleLibsList。
设计器页签(Form.vue)¶
| 页签 | 组件 | 普通表单(type=1) | 说明 |
|---|---|---|---|
| 基本 | FormBasic |
✓ | 表单元数据、脚本、评论、日志、水印等 |
| 内容 | FormFormat |
✓ | 可视化/源码编辑表单 HTML 模板 |
| 操作 | FormOperation |
✓(已保存且 type≠2、256) | 表单工具栏按钮(activities) |
| 摘要 | FormSummary |
✓(已保存且 type≠2、256、1048576) | 流程摘要提醒 |
| 映射 | FormMapping |
—(仅 type=65536) | 映射表单专用 |
新建未保存时仅「基本」「内容」可用;操作 / 摘要 需先保存拿到 formId。
表单类型(设计器 type)¶
| type | 值 | 设计器标签 | 运行时组件 |
|---|---|---|---|
| 普通表单 | 1 |
normal_forms | form_normalform |
| 标签页 | 2 |
tab | 标签页专用 |
| 查询表单 | 256 |
search_form | form_searchform / form_template |
| 映射表单 | 65536 |
normal_mapping | 映射运行时 |
| 模板表单 | 1048576 |
template_form | NormalForm + realformId / templateForm |
基本页签可配置属性(FormBasic.params)¶
| 属性 | 类型 | 默认值 | 可见性 | 说明 |
|---|---|---|---|---|
name |
String |
空 | 全部 | 表单名称;新建可编辑,已保存后默认只读展示 |
type |
Number |
1 |
全部 | 表单类型;编辑时不可改 |
styleId |
String |
"" |
全部 | 样式库 id |
permissionType |
String |
"public" |
type≠2 且 ≠256 | public / private |
orderno |
Number |
1 |
全部 | 模块内排序号 |
description / remark |
String |
空 | 全部 | 描述 / 备注 |
showType |
String |
"old" |
— | old / new,对应 formTemplate.showType |
isopenablescript |
String |
空 | type≠2 | 是否可打开脚本(布尔) |
iseditablescript |
String |
空 | type≠2 | 是否可编辑脚本(布尔) |
confirmLeaveEdit |
Boolean |
false |
type≠2 | 离开编辑时弹窗确认 |
showWaterMark |
Boolean |
false |
type=1 / 65536 / 1048576 | 是否启用水印 |
waterMarkScript |
String |
空 | 同上 | 水印脚本 |
openComment |
Boolean |
false |
type≠256 且 ≠2 | 是否开通评论 |
commentTitleScript |
String |
空 | 开通评论时 | 评论标题脚本 |
commentFlagScript |
String |
空 | 同上 | 评论标识脚本 |
commentHiddenScrtipt |
String |
空 | 同上 | 隐藏评论区脚本 |
commentExecuteScript |
String |
空 | 同上 | 评论/回复执行脚本 |
showLog |
Boolean |
false |
type≠2 | 是否显示操作日志 |
showLogType |
String |
"default" |
showLog=true |
default / table |
recordMode |
String |
"every" |
showLog=true |
every / other / script |
recordModeScript |
String |
空 | recordMode=script |
记录方式脚本 |
mappingStr |
Object |
{} |
映射表单 | 映射表数据 |
templatecontext |
String |
空 | — | 保存时由 FormFormat 注入 |
脚本类字段支持 textarea + ScriptEditor 弹窗编辑。
保存流程(Form.vue.saveBtn)¶
FormBasic.save():校验名称,返回params;FormFormat.getHtmlTemplate()→basicData.templatecontext;- 映射表单:
FormMapping.verifyMapping()→mappingStr; updateForm/createForms。
操作按钮、摘要、流程预览在 操作 / 摘要 页签及内容设计器中维护,运行时由 activities、formTemplate.processPreview 下发。
前端运行时(PC)¶
NormalForm 是 LINK_TYPE.FORM 在 PC 运行时的默认实现,由 obpm_open_container 按 openParams.linkType 动态加载。
挂载场景¶
| 场景 | 挂载方式 |
|---|---|
| 主框架页签打开表单 | obpm_open_container → form_normalform |
| 视图列跳转 / 按钮弹出层 | TopWindowUtils、o_action_dialog |
| 树形视图内嵌表单 | view_treeview.vue |
| 列表 Widget 内打开 | view_listview_widget.vue |
| 自定义消息弹窗内嵌 | NormalForm 自身 el-dialog + form_template |
组件接口¶
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
openParams |
Object |
是 | 表单打开上下文(见 openParams) |
callback |
Function |
否 | 预留回调 |
emptyParams |
Array |
否 | 视图列带参新建的空白字段默认值 |
openType |
String |
否 | 打开类型标识(如弹出层) |
Inject(宿主 provide)
| 键 | 用途 |
|---|---|
addTab |
选项卡内打开新页签 |
openInCurrentContainer |
当前容器栈内再开视图/表单 |
onCloseBtnClick |
流程提交成功后关闭表单标签 |
doBack |
返回、关闭表单 |
Provide(向子组件暴露)
| 键 | 说明 |
|---|---|
findField / getAllFields / getStateId |
字段查找、流程状态 |
buildFormData / checkData |
提交快照 / 校验快照 |
refresh / initialRefresh |
字段重计算刷新 |
registerGridviewCommitAll |
嵌入网格行内编辑提交注册 |
updateFormSubDocumentsData |
表单子文档(IncludeField 网格) |
clickControlBtn / flowHandle |
按钮控件 / 流程按钮入口 |
refreshId |
当前实例 id(openParams.id) |
addTabs、getOpenParams、promptBox 等 |
页签、参数、提示 |
Emit:event(加载完成)、action(保存并新建等)、treeLeftRefresh、refresh。
页面结构¶
formwrapper (#formTemplate_{docId})
├── formcontent (act-btns + approvers)
│ ├── activity × N ← label/name 不含 "/"
│ ├── collapse_activity ← label/name 含 "/"
│ └── approvers
├── formTable-wrap (el-scrollbar)
│ ├── processPreview
│ ├── form_signlist / form_stamp / form_signature
│ └── dynamic formTemplate
├── form_flow_dialog / 审批人 / 终止 / 邮件短信 等弹窗
├── o_action_dialog
└── el-dialog + form_template
操作栏显示:isActBox 且(有 stateId + isRouterAlive,或 isShowFormActivity)。formTemplate.template == false 时仅渲染 <authority />。
后端配置在前端的消费¶
| 后端 / 设计器来源 | 前端行为 |
|---|---|
formTemplate.template |
动态编译为 formHtml;false → authority |
style.content |
注入 #formTemplate_{docId} 作用域 CSS |
waterMarkText |
Watermark.set |
openComment / commentTitle / commentFlag |
追加 <form_comment> 虚拟字段 |
showLog + showLogType=table |
_template 嵌入 form_comment |
showType |
弹出层滚动、操作日志展示等 |
confirmLeaveEdit |
可编辑时:doBack、页签关闭、beforeunload 等离开路径弹窗确认;配合 formSuppressBeforeUnloadOnce 跳过单次提示 |
Tab 内 openComment / commentFlag |
initForm / loadForm 的 loopOpe 动态插入评论 |
数据加载¶
入口:onBeforeMount → initForm({ appId, formId, docId })(docId ← openParams._select,空为新建)。
编辑(docId 非空)→ GET /runtime/{appId}/forms/{formId}/documents/{docId}(getFormAPI):
- 合并
exparams/formParams/urlParams、只读、重算等参数; - 响应后处理:字段
%解码、注入运行时属性、评论/样式/水印/流程预览、包含元素内隐藏返回按钮。
新建(docId 空)→ GET .../empty(getDocumentEmptyAPI)→ 可选新建后脚本 → loadForm 拉完整表单。
带旧数据新建:保存后 oldBuildFormData 在下次 loadForm 按字段 name 写回。
动态模板(_template)¶
| 条件 | 渲染 |
|---|---|
template == false |
<authority /> |
| 评论或 table 型操作日志 | formHtml + form_comment |
| 默认 | 仅 formHtml |
注入 methods:findField、refresh、checkData、getAllFields、addTabs、getStateId。
表单提交(buildFormData)¶
字段纳入 items:有 name 且非 ButtonField、非 hidden 空值(例外见源码 addInTtems / Checkbox 默认项 / 附件 isEdit)。模板表单:realformId + templateForm。
嵌入网格视图同步¶
gridviewCommitAllCallbacks:onAction 前 commitAllGridviews(),确保行内编辑落盘后再保存/流程;子文档经 formSubDocumentsData → subDocuments。
按钮动作¶
| 入口 | 说明 |
|---|---|
onAction(act) |
工具栏 / 折叠区 |
clickControlBtn(act) |
表单内 ButtonField |
流程类(flowHandle):流程处理(5)、启动(33)、回退/催办/挂起/恢复/撤回/终止、编辑审批人(53)、点评/补签/加签(55–58)、邮件短信(37) 等。成功后 notifyFlowActionSuccess() → 本地 reload + 通知父视图 refreshId(不向自身 id dispatch,防死循环)。
脚本流水线类:prepareActionContext → before 脚本 → 业务 API → after 脚本。含保存(34)、保存并返回(11)、保存并新建(42)、草稿(19)、复制(21)、打印(14/30)、导出、跳转(43) 等。
简单动作:返回(10) → doBack()。
openParams 关键字段¶
| 字段 | 说明 |
|---|---|
appId / actionContent / _select |
软件 id、表单 id、docId(空=新建) |
realformId / templateForm |
模板表单 |
id / refreshId |
实例 id / 父级刷新目标 |
parentId / isRelate |
包含元素 / 关联新建 |
type / openType / showtype |
打开方式;include/tab 内隐藏返回 |
exparams / formParams / urlParams / queryString |
扩展参数 |
isNewCreate / runAfterParams |
视图新建后脚本 |
originalFormDocid |
保存并新建带旧数据 |
isIncludeCreate / randomRefreshId |
包含元素新建后刷新父视图 |
dialogId / jumpDialogId |
弹出层内 IncludeField |
字段查找与刷新¶
findField(id):主字段按 id 后缀段匹配(复制文档兼容);Tab 内完整 id;未找到返回 HIDDEN 占位。refresh(fieldId):refreshFormAPI原地更新字段;IncludeField 改refreshNumber;InputField 设addInTtems。checkData(true):返回buildFormData()快照。
辅助能力¶
| 能力 | 要点 |
|---|---|
| 操作日志 | isModified → hisLogsStore |
| 盖章 / 签章 | form_stamp、form_signature |
| 网页打印 | 路由 formPrint 或对话框 form_print |
| 流程预览 | 内嵌或弹窗,previewWorkflow |
| 跳转 | o_action_dialog.openOverlayJump,dialogHost: 'form' |
| 离开确认 | formTemplate.confirmLeaveEdit=true 时启用;beforeunload + 容器内返回/关页签;formSuppressBeforeUnloadOnce 可跳过单次浏览器提示 |
| 父视图刷新 | dispatchRefresh(refreshId) |
与 form_template 的区别¶
| 维度 | form_normalform | form_template |
|---|---|---|
| 用途 | 完整运行时(按钮栏 + 流程 + 保存) | 轻量渲染(查询表单、消息弹窗) |
| 操作按钮 | 完整 activities | 无 |
| 数据加载 | initForm + loadForm 全分支 | 简化 initForm |
运行时 API 汇总¶
| 方法 | 路径 / 接口 | 用途 |
|---|---|---|
| GET | /runtime/{appId}/forms/{formId}/empty |
新建空文档 |
| GET | /runtime/{appId}/forms/{formId}/documents/{docId} |
加载表单与文档 |
| PUT | /runtime/{appId}/documents/{docId} |
保存文档 |
| — | saveWithoutValidDocument |
保存草稿 |
| — | refreshFormAPI |
字段刷新重计算 |
| — | runBeforeActionScriptAPI / runAfterActionScriptAPI |
按钮前后脚本 |
| — | initWorkFlow 等 |
流程启动与处理 |
已知限制与注意事项¶
- 动态模板安全:
formTemplate.template为 HTML 字符串,非沙箱隔离。 - fieldId 匹配:复制文档依赖 id 后缀匹配,Tab 内仍用完整 id。
- refreshId 死循环:向自身
openParams.iddispatch 会导致 remount 死循环,已排除。 - 流程状态栏:审批人变更后
isRouterAlive强制 remountapprovers。 - 新表单弹出层滚动:
showType=new且弹出层,或存在isModified时修正滚动条。 - PDF 导出按钮:新建未保存时表单内 EXPORT_PDF 按钮设为只读。
- 移动端:PC 与移动为不同代码库。
- 保存即建表:平台无独立「发布」步骤,
FormDesignTimeService.save/update/delete在事务内直接触发 DDL;表单保存失败会回滚 XML 与库表。 - 改列类型不迁移数据:
processChange(ColumnDataTypeChange)仅执行改名,第 2–4 步(add→copy→drop)被注释;变更字段类型前应清空该列数据(由doClearColumnData完成),否则类型可能不一致。 - 固定列清单需三方同步:新增文档元数据列时,
ChangeLog.getFixedColumns()(DDL)、DQLASTUtil.DOCUMENT_FIXED_COLUMNS(DQL/DML 过滤)、AbstractDocStaticTblDAO的 INSERT/UPDATE SQL 列清单必须同时维护,否则会出现「建表有列但读写漏列」或反之。 - 动态表 DDL ≠ 系统表 DDL:
FormTableDAO仅供表单动态表使用;平台自身表走AbstractApplicationInitDAO,数据模型表单可选用 Liquibase——三套机制互不相通。