数据查询、统计与文档批量操作¶
本章面向**业务数据层**:从一次 SQL 查询、一条聚合统计,到批量改字段、自动建文档、批量删单等"数据侧"操作。前置阅读 getting-started.md(特别是 GraalVM 差异 一节,平台返回的
queryBySQL/queryByDSName结果集是 JavaCollection,遍历用.iterator()、长度用.size(),不要混用 JS 数组写法)。
场景清单¶
| 场景 | 主要落点 | 关键 API / 素材 |
|---|---|---|
| SQL 查询与聚合统计(标杆) | 值脚本 / 计算脚本 / 报表 | queryBySQL、findBySQL、SQL SUM/COUNT/AVG |
| 记录存在判断与计数 | 校验脚本 / 值脚本 | countBySQL、findBySQL |
| 跨数据源查询 | 值脚本 / 定时任务 | queryByDSName、countByDSName |
| 通过 HTTP/API 取数 | API 响应 / 值脚本 / 计算 | httpGet/httpPost、外部接口 |
| SQL 批量更新表单字段 | 操作后置 | updateByDSName |
| 自动创建文档并启动流程 | 操作后置 / 定时 | docProcess.doNew、doCreate、doStartFlow |
| 批量生成子表 / 复制字段 | 视图确认 / 操作后置 / 表单按钮 | getRelateDocument、DOC.buileDocument |
| 批量删除文档 | 视图操作(前/后置) | docProcess.doView、doRemove |
| 取网格视图数据转 JSON / 导出 | 计算脚本 / 其它 | getViewProcess、view.getSQL |
本章通用约定:业务表表名是
tlk_<表单名>,业务字段在库中是item_<字段名>;查询平台表(用户/角色/部门/流程实例等)须带DOMAINID过滤,业务表通常也建议加domainid限定当前域,避免跨域读到脏数据。
SQL 查询与聚合统计¶
业务目标:在表单字段、计算脚本或报表数据源里,按 SQL 查询并聚合业务数据(求和、平均、分组、最大最小等),把结果回填到当前字段或返回给前端。
写在哪儿:
- 表单 → 表单字段 → 属性「值脚本」(.form,属性 valueScript,Label FORM:FIELD_VALUE);或表单控件的「计算脚本」。
- 视图列 → 列值脚本(Label VIEW:COLUMN_VALUE,做"展示聚合"时常用)。
- 报表 → 报表数据源脚本(Label REPORT:DATASOURCE_SQL,详见 12-report-and-chart.md)。
触发时机与上下文:表单渲染/字段值计算时;可用环境变量 getCurrentDocument()、getWebUser()、getApplication()。
返回值契约:值脚本返回任意值(数字 / 字符串 / JSON 串)赋给当前字段;计算脚本同理。
示例代码(求"金额"字段总和 + 分类小计,标准聚合套路):
(function () {
var domainId = getWebUser().getDomainid();
// 1) 单值聚合:用 findBySQL 取一行带 SUM/COUNT 的结果
var sumSql = "SELECT SUM(item_金额) AS total, COUNT(*) AS cnt " +
"FROM tlk_报销单 " +
"WHERE domainid = '" + domainId + "'";
var row = findBySQL(sumSql);
var total = row != null ? (row.getItemValueAsDouble("total") || 0) : 0;
var cnt = row != null ? (row.getItemValueAsDouble("cnt") || 0) : 0;
// 2) 分组聚合:用 queryBySQL 取多行,遍历用 .iterator()
var groupSql = "SELECT item_费用类型, SUM(item_金额) AS sub " +
"FROM tlk_报销单 " +
"WHERE domainid = '" + domainId + "' " +
"GROUP BY item_费用类型";
var groups = queryBySQL(groupSql);
var breakdown = {};
if (groups != null && groups.size() > 0) {
for (var it = groups.iterator(); it.hasNext(); ) {
var d = it.next();
var key = d.getItemValueAsString("费用类型");
var sub = d.getItemValueAsDouble("sub") || 0;
breakdown[key] = sub;
}
}
return JSON.stringify({
total: total,
count: cnt,
average: cnt > 0 ? total / cnt : 0,
breakdown: breakdown
});
})()
代码说明:
- findBySQL(sql):执行 SQL 返回单条记录(无结果返回 null),适合放聚合行;签名见 iscript-guid/api → curdb。
- queryBySQL(sql):返回多条记录的 Collection;用 .size() 取长度、.iterator() 遍历,详见 iscript-guid/api → curdb。
- doc.getItemValueAsDouble("字段名") / getItemValueAsString("字段名"):按类型取字段值,见 iscript-guid/functions → doc-functions。
- SQL 聚合(SUM/COUNT/AVG/MAX/MIN/GROUP BY)以及 CONCAT/DATE_FORMAT/CASE WHEN 等常用函数式样,参见 common-script-collection → sql-common-functions 与 sum-numeric-field。
变体与扩展:
- 日期范围求和:WHERE item_日期字段 >= '...' AND item_日期字段 <= '...'。
- 父文档汇总子表:WHERE parent = '<父文档ID>'(子表查询不需要 domainid)。
- 遍历累加:当聚合逻辑复杂(如条件过滤后再加权),用 queryBySQL 取明细,在 JS 里循环累加——避免把业务逻辑塞进 SQL。
- 空结果处理:SUM 在无记录时返回 null,务必 || 0 兜底。
常见坑:
- 把 Collection 当 JS 数组:queryBySQL(...).length ❌、queryBySQL(...).forEach(...) ❌;正确写法见 GraalVM 差异「Java List .size() vs JS 数组」。row.getItemValueAsDouble("count") 取出来可能是 Double,做算术前 || 0 防 null。
- 表名前缀漏写:业务表必须 tlk_ 开头、业务字段必须 item_ 开头;平台表(如 t_user、t_document)不带 tlk_ 但要带 DOMAINID。
- SQL 注入:拼接进来的字符串(用户输入、URL 参数)须先校验/转义,绝不要直接拼 ' 与 -- 等危险字符;条件允许时优先用 DQL 过滤(见 09-view-filter-and-column.md)。
记录存在判断与计数¶
业务目标:判断"是否已存在符合条件的记录"(用于查重、防重复提交、唯一性校验),或返回记录条数用于业务分支。
写在哪儿:表单 → 字段校验脚本(.form,属性 validateScript,Label FORM:FIELD_VALIDATE);也可放值脚本做展示。
触发时机与上下文:表单提交前;可用 getCurrentDocument()、getWebUser()。
返回值契约:校验脚本——"" 表示通过;非空字符串即"失败提示"(校验契约)。
示例代码(提交报销时按"申请人 + 日期"查重):
(function () {
var doc = getCurrentDocument();
var applicant = doc.getItemValueAsString("申请人");
var applyDate = doc.getItemValueAsString("申请日期");
var domainId = getWebUser().getDomainid();
// 排除当前编辑中的文档自身:id <> '<curId>'
var curId = doc.getId();
var sql = "SELECT domainid FROM tlk_报销单 " +
"WHERE item_申请人 = '" + applicant + "' " +
"AND item_申请日期 = '" + applyDate + "' " +
"AND id <> '" + curId + "' " +
"AND domainid = '" + domainId + "'";
var count = countBySQL(sql);
if (count > 0) {
return "已存在该申请人当天的报销单,请勿重复提交(共 " + count + " 条)";
}
return ""; // 校验通过
})()
代码说明:
- countBySQL(sql):返回记录数(Integer);签名见 iscript-guid/api → curdb。
- 仅判断存在性时 countBySQL 比 queryBySQL 更高效(不传输字段数据)。
- 若既要判存在又要取记录详情,用 findBySQL(sql) 判 null;详见 common-script-collection → check-record-exists-or-count。
变体与扩展:
- 多字段查重:AND 串接更多 item_xxx 条件。
- 按 ID 判存在:SELECT id FROM tlk_xxx WHERE id = '...' + findBySQL 判 null。
- 分类计数:分别 countBySQL 多个条件,组装成 JSON 返回(值脚本场景)。
常见坑:
- 编辑时把自己算进去:一定要加 id <> '<当前文档ID>' 排除自身。
- 校验返回布尔:旧示例里常看到 return true;——校验脚本契约是**字符串**,true 会被当成"非空串"误判为失败。详见 GraalVM 差异 同节"返回值契约一览"。
- 跨域:业务表查询必须带 domainid,否则多租户环境下会跨域读到其它域数据。
跨数据源查询¶
业务目标:当前域的默认数据源之外,还配置了其它数据源(如独立的 MySQL 业务库、报表库、第三方系统库),需要从这些数据源取数或写入。
写在哪儿:值脚本 / 计算脚本 / 定时任务主脚本(.task,Label TASK:SCRIPT,无用户上下文,详见 13-task-api-widget.md)。
触发时机与上下文:表单字段渲染时;定时任务按调度触发;可用 getApplication(),定时任务里 getWebUser() 不可用。
返回值契约:返回数据集或聚合结果(值脚本赋给字段)。
示例代码(从外部 MySQL 数据源查最新 10 条同步记录):
(function () {
var dsName = "mysql_business"; // 在「数据源」配置里登记的名字
var sql = "SELECT id, name, amount FROM ext_orders ORDER BY id DESC LIMIT 10";
var datas = queryByDSName(dsName, sql);
var rows = [];
if (datas != null && datas.size() > 0) {
for (var it = datas.iterator(); it.hasNext(); ) {
var d = it.next();
rows.push({
id: d.getItemValueAsString("id"),
name: d.getItemValueAsString("name"),
amount: d.getItemValueAsDouble("amount")
});
}
}
return JSON.stringify(rows);
})()
代码说明:
- queryByDSName(dsName, sql):按数据源名查询,返回 Collection;countByDSName / updateByDSName / insertByDSName / deleteByDSName 是同族 API;事务方法 beginTransaction / commitTransaction / rollbackTransaction 也在同一对象上,签名见 iscript-guid/api → database。
- 数据源本身(驱动、URL、账号密码)在「管理 → 数据源」里配置,脚本里只引用数据源名称,不要把账号密码硬编码进脚本。
- MySQL 5.x 用驱动 com.mysql.jdbc.Driver;MySQL 8.x 必须用 com.mysql.cj.jdbc.Driver,且 URL 通常要加 serverTimezone=Asia/Shanghai;详细配置见 common-script-collection → mysql-database-config。
变体与扩展:
- 写入外部库:updateByDSName(dsName, "UPDATE ext_orders SET ... WHERE id = '...'")、insertByDSName、deleteByDSName。
- 事务:多步写入用 beginTransaction → 各 updateByDSName → commitTransaction,异常时 rollbackTransaction。
- 测试连通性:SELECT 1 AS test + findByDSName 判空。
常见坑:
- 外部表名不带 tlk_、字段也不带 item_:跨数据源查询的是**外部业务库的真实表名**,命名规则由对方库决定,不能套用平台前缀。
- queryByDSName 仍返回 Java Collection:遍历同样用 .iterator()、长度用 .size()(参见 GraalVM 差异「Java List .size() vs JS 数组」);与 queryBySQL 返回类型一致,不要混用 .length。
- 外部库 SQL 方言:Oracle/SQL Server 的分页、日期函数、聚合写法与 MySQL 不同;拼 SQL 前先确认对方库类型。
通过 HTTP/API 取数¶
业务目标:从平台外的 HTTP 接口(自研系统、第三方 SaaS、平台自身 REST API)拉取数据或数量,再回到脚本里加工。
写在哪儿:API 响应脚本(.api,Label API:RESPONSE,详见 13-task-api-widget.md)/ 值脚本 / 计算脚本。
触发时机与上下文:API 调用或字段渲染时;可用 getWebUser()、getApplication()(API 公开模式下 getWebUser() 可能为空)。
返回值契约:返回 JSON 串或解析后的数值;API 响应脚本则直接写成响应体。
示例代码(调用外部接口取"今日订单数"并返回数字):
(function () {
var apiUrl = "http://api.example.com/orders/count";
var token = getToken(); // 平台提供的访问令牌
var headers = {
"Authorization": "Bearer " + token,
"Content-Type": "application/json"
};
var response = httpGet(apiUrl, headers); // 同步 GET,返回字符串
if (response == null || response.trim().length === 0) {
return 0;
}
var obj = JSON.parse(response);
return obj.count || 0;
})()
代码说明:
- httpGet(url, headers) / httpPost(url, body, headers):HTTP 同步请求,签名见 iscript-guid/functions → http-functions。
- getToken():取当前用户的访问 token;详见 common-script-collection → get-token。
- 调用平台自身 REST API 时,若需要本机 baseUrl,可用 getParamsTable().getHttpRequest() 拼出 scheme://host:port,再拼接 /api/... 路径;详见 common-script-collection → api-get-data-count。
变体与扩展:
- POST 复杂查询:httpPost(url, JSON.stringify({ formName: "...", conditions: {...} }), headers),返回里取 total。
- 缓存结果:把接口结果存到私有空间缓存(appendix-toolbox.md 的"跨脚本缓存"主题),避免每次渲染都打外部接口。
- 错误处理:包 try/catch,根据 e.getMessage() 区分网络/认证/解析错误。
常见坑:
- 字符串判空用 .length 不是 .length():response.trim().length === 0 是 JS 字符串属性;详见 [GraalVM 差异「字符串长度:用 .length 属性,禁用 .le」](getting-started.md#graalvm-差异)。
- **同步阻塞**:httpGet/httpPost是同步调用,外部接口慢会拖垮表单渲染;务必设超时或加缓存。
- **认证信息泄漏**:不要把生产 token / 账号密码硬编码到脚本里,优先用getToken()` 或从私有空间取。
SQL 批量更新表单字段¶
业务目标:在某个事件后用一条或几条 SQL UPDATE 直接改业务表数据,不走文档对象(性能高,但绕过表单校验)。
写在哪儿:视图/表单 → 操作后置脚本(.activity,Label ACTIVITY:AFTER);也可放定时任务。
触发时机与上下文:操作按钮提交完成后;可用 getCurrentDocument()、getParameter("_selects")。
返回值契约:通常无返回值,做副作用(写库);如需给前端提示,返回字符串。
示例代码("开票计划"按合同编号 + 日期批量更新已开票额):
(function () {
var doc = getCurrentDocument();
var contractNum = doc.getItemValueAsString("合同编号");
var startTime = doc.getItemValueAsString("计划开票日期");
var amount = doc.getItemValueAsDouble("本次开票额");
var sql = "UPDATE tlk_开票计划 " +
"SET item_计划已开票额 = item_计划已开票额 + " + amount + " " +
"WHERE item_合同编号 = '" + contractNum + "' " +
"AND item_计划开票日期 = '" + startTime + "'";
updateByDSName("数据源名称", sql);
})()
代码说明:
- updateByDSName(dsName, sql):在指定数据源执行 UPDATE/INSERT/DELETE,签名见 iscript-guid/api → database。
- 字段名、表名严格按 tlk_<表单名> / item_<字段名> 规则;多个条件用 AND 串接。
- 更多示例(单/多条件、批量循环更新)见 common-script-collection → change-form-field-value。
变体与扩展:
- 批量按 ID 列表更新:先 queryBySQL 拿到 ID 集合,再循环 updateByDSName。
- 更新多个字段:SET item_a = '...', item_b = '...' WHERE ...。
- 与文档对象配合:若需要触发表单联动 / 流程事件,请改用 findItem().setValue() 走文档对象(见 01-form-data.md);updateByDSName 不会触发任何 iScript 钩子。
常见坑:
- SQL 注入:本场景里把字段值直接拼进 SQL 是最危险的——contractNum 若含 ' 会导致注入或语法错误。条件允许时优先用文档对象的 setValue;必须用 SQL 时,对用户输入做严格校验(白名单、长度、字符集)。
- 绕过文档对象:updateByDSName 不会更新 lastmodified、不触发流程节点脚本、不刷新视图缓存。批量改完通常需要让用户刷新视图。
- amount 是数字:拼数字字段无需加单引号;拼字符串字段必须加单引号。
自动创建文档并启动流程¶
业务目标:在某个事件后,自动按指定表单新建一条文档、写入业务字段,并启动指定流程(生成首节点待办或直接送下一节点)。
写在哪儿:视图/表单 → 操作后置(Label ACTIVITY:AFTER);定时任务主脚本(Label TASK:SCRIPT)。
触发时机与上下文:操作提交后 / 定时触发;可用 getCurrentDocument()、getWebUser()、getApplication()。
返回值契约:通常无返回值;可返回新建文档 ID 字符串便于排错。
示例代码(标准三步:拿表单 → doNew 写字段 → doCreate 落库 → doStartFlowOrUpdate 启流程):
(function () {
var doc = getCurrentDocument();
var user = getWebUser();
// 1) 拿表单对象("到账" 是目标表单名)
var formProcess = getFormProcess();
var targetForm = formProcess.doViewByFormName("到账", getApplication());
// 2) doNew 出新文档并写字段
var docProcess = getDocumentProcess();
var newDoc = docProcess.doNew(targetForm, user, createParamsTable());
newDoc.setAuthor(user.getId());
newDoc.setIstmp(false);
newDoc.setApplicationid(getApplication());
newDoc.setDomainid(user.getDomainid());
newDoc.setStateLabel("自动创建-到账认领");
newDoc.addDateItem("到账日期", doc.getItemValueAsDate("到账日期"));
newDoc.addDoubleItem("到账金额", doc.getItemValueAsDouble("到账金额"));
newDoc.addStringItem("付款单位", doc.getItemValueAsString("付款单位"));
// 3) 落库
docProcess.doCreate(newDoc);
// 4) 启动流程(_flowid 在系统流程定义表里查)
var params = createParamsTable();
params.setParameter("_flowid", "11e4-adc4-fb24905a-b517-695d7f9ea8d3");
docProcess.doStartFlowOrUpdate(newDoc, params, user); // 在首节点启动
})()
代码说明:
- getFormProcess().doViewByFormName(name, application):按表单名取表单对象;getDocumentProcess().doNew(form, user, paramsTable):新建空文档;详见 iscript-guid/api → doc 与 iscript-guid/functions → curdoc-functions。
- doc.addStringItem("字段名", value) / addDoubleItem / addDateItem:按类型写字段(与 findItem().setValue 等价,但是新文档首选)。
- doStartFlowOrUpdate(doc, params, user):在**首节点**启动流程;doStartFlow(doc, params, user) 则送至下一节点。两种启动方式与"注入 $CURRDOC 上下文"的细节见 common-script-collection → create-form-start-flow。
变体与扩展:
- 作为子表新建:newDoc.setParent(parentDocId) 建立主子关系(与「批量生成子表 / 复制字段」通用)。
- 直接 new Document() + setFormid:第二种方法绕过 doViewByFormName,但必须用 cn.myapps.util.sequence.Sequence 生成随机 ID,不能用固定 ID——详见源文档。
- 批量新建:循环调用,但注意事务/性能;大批量建议改放定时任务。
常见坑:
- 启动人不在首节点:调用 doStartFlowOrUpdate 的 user 必须是该流程首节点的处理人,否则流程引擎报错。
- _flowid 必填:流程 ID 在平台系统表(流程定义)里查,不是表单 ID,也不是节点 ID。
- 字段类型与 addXxxItem 匹配:日期字段必须 addDateItem、数值字段必须 addDoubleItem,类型不匹配会静默失败。
- newDoc.setId(...) 慎用:除非有特殊业务主键约束,**不要**自行设置 ID;让引擎用 Sequence 生成。
批量生成子表 / 复制字段¶
业务目标:从视图选中的若干条记录(或当前表单的某字段集合),批量生成目标表单的子表记录;或将选中源文档的字段复制到当前文档。
写在哪儿:
- 视图 → 操作按钮「确认脚本 / 跳转脚本」(Label ACTIVITY:BEFORE 或 ACTIVITY:AFTER,按是否阻断分前后)。
- 表单 → 表单按钮「点击脚本」(操作后置)。
触发时机与上下文:用户在视图勾选行 → 点按钮;可用 getRelateDocument()(关联主文档)、getParameter("_selects")(选中 ID 串,以 ; 分隔)。
返回值契约:操作前置可返回 false/非空串阻断;操作后置通常无返回,或返回统计串。
示例代码(视图选中 → 为主表生成子表记录;含去重检查,最常见套路):
(function () {
var relate = getRelateDocument(); // 关联主文档(目标父文档)
var selectId = getParameter("_selects"); // 选中 ID:"id1;id2;id3"
if (selectId == null || selectId.trim().length === 0) {
return "请先勾选要生成的记录";
}
var relateId = relate.getId();
// 拼成 SQL IN 条件:('id1','id2','id3')
var inClause = "('" + selectId.replace(/;/g, "','") + "')";
var sql = "SELECT domainid, id, item_字段1, item_字段2 " +
"FROM tlk_源表单 WHERE id IN " + inClause;
var datas = queryBySQL(sql);
if (datas == null || datas.size() === 0) {
return "源数据为空";
}
var docProcess = getDocumentProcess();
var formProcess = getFormProcess();
var subForm = formProcess.doViewByFormName("子表表单名", getApplication());
var success = 0, exist = 0;
for (var it = datas.iterator(); it.hasNext(); ) {
var data = it.next();
var dataId = data.getId();
// 去重:该主表下若已存在相同源 ID 的子表记录则跳过
var checkSql = "SELECT domainid FROM tlk_子表表单 " +
"WHERE parent = '" + relateId + "' " +
"AND item_源ID = '" + dataId + "'";
if (countBySQL(checkSql) > 0) {
exist++;
continue;
}
var subDoc = docProcess.doNew(subForm, getWebUser(), createParamsTable());
subDoc.setParent(relateId);
subDoc.setAuthor(getWebUser().getId());
subDoc.setIstmp(false);
subDoc.setApplicationid(getApplication());
subDoc.setDomainid(getWebUser().getDomainid());
subDoc.addStringItem("字段1", data.getItemValueAsString("字段1"));
subDoc.addStringItem("字段2", data.getItemValueAsString("字段2"));
subDoc.addStringItem("源ID", dataId);
docProcess.doCreate(subDoc);
success++;
}
return "已生成 " + success + " 条,跳过已存在 " + exist + " 条";
})()
代码说明:
- getRelateDocument():视图操作里取关联主文档(跳转/确认场景的目标父文档);getParameter("_selects"):选中行的文档 ID 串。
- replace(/;/g, "','") 把 "id1;id2" 转为 'id1','id2' 用于 SQL IN。
- 主子关系靠 subDoc.setParent(relateId);docProcess.doCreate(subDoc) 落库。
- 三种等价写法见 common-script-collection → view-confirm-script 与 view-jump-generate-subtable。
变体与扩展:
- 用 DOC.buileDocument 简化:参数表式构造,无需手动 setAuthor/setDomainid 等——见 common-script-collection → form-button-generate-subtable。注意 API 名是 buileDocument(拼写沿用历史,不要"修正"为 buildDocument)。
- 复制源文档字段到当前文档:docProcess.doView(sourceId) 取源 → currentDoc.findItem("字段").setValue(...),不做新文档;详见 common-script-collection → view-jump-copy-data。
- 生成后启动流程:在 doCreate(subDoc) 后接 docProcess.doStartFlowOrUpdate(subDoc, params, user)。
- 条件过滤:循环里按 data.getItemValueAsString("状态") === "已审核" 决定是否生成。
常见坑:
- _selects 末尾有空字符:视图选择框若末尾有空格/空字符,IN (...) 会查不到——剪裁后再拼。
- 去重不可省:用户重复点按钮会重复生成子表,必须用 countBySQL 检查 parent + 业务主键 是否已存在。
- replace(";", "','") 只替换首个:JS 字符串 String.prototype.replace 第一个参数为字符串时仅替换首个匹配;要全替换用正则 /;/g。
- Java List 遍历:queryBySQL 返回的 Collection 不要用 for...of(GraalVM 下对 Java 集合的行为不稳定),用 .iterator() 最稳(GraalVM 差异「Java List .size() vs JS 数组」)。
批量删除文档¶
业务目标:在视图里勾选多条记录,一次性删除;或按 SQL 条件批量清理过期/作废单据。
写在哪儿:视图 → 操作按钮(Label ACTIVITY:BEFORE 前置校验 / ACTIVITY:AFTER 后置执行)。
触发时机与上下文:用户点击操作按钮;可用 getParameter("_selects")、getCurrentDocument()。
返回值契约:前置返回 false/非空串阻断;后置通常无返回值,或返回统计串。
示例代码(后置:按选中 ID 批量删除):
(function () {
var selectIds = getParameter("_selects");
if (selectIds == null || selectIds.trim().length === 0) {
return;
}
var ids = selectIds.split(";"); // JS 字符串 split,返回 JS 数组,用 .length
var docProcess = getDocumentProcess();
var removed = 0;
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
if (id == null || id.trim().length === 0) {
continue;
}
var doc = docProcess.doView(id);
if (doc != null) {
docProcess.doRemove(doc.getId());
removed++;
}
}
// 如需在前端提示,可写日志或返回串
})()
代码说明:
- getDocumentProcess().doView(id) 取文档对象;doRemove(id) 删除(参数是文档 ID 字符串,不是文档对象)。
- selectIds.split(";") 返回的是 JS 数组,用 .length 和 arr[i];与 queryBySQL 返回的 Java Collection 不同(GraalVM 差异「Java List .size() vs JS 数组」)。
- 按条件批量删(不依赖选中):queryBySQL("SELECT id FROM tlk_xxx WHERE ...") → 循环 doView + doRemove;详见 common-script-collection → delete-document-info。
变体与扩展:
- 前置权限校验:在 ACTIVITY:BEFORE 里检查用户角色 / 文档状态,返回非空串阻断。
- 逻辑删除:把 doRemove 换成 updateByDSName(ds, "UPDATE tlk_xxx SET item_状态='已作废' WHERE id IN (...)")。
- 级联删子表:删主表前先用 SELECT id FROM tlk_子表 WHERE parent = '...' 拿到子表 ID 集合并先删。
常见坑:
- 删正在流程中的单据:会破坏流程实例,必须先校验 getStateInt() 或 getStateLabel(),流程未结束的不要删。
- doRemove 不可恢复:建议先备份或做逻辑删除。
- split 后的数组判空:ids[i] 可能是空串(末尾分号导致),continue 跳过。
取网格视图数据转 JSON / 导出¶
业务目标:按视图名取出视图数据,转换为 JSON(供前端 / API 消费)或 CSV / HTML(导出)。
写在哪儿:计算脚本 / API 响应脚本(.api,Label API:RESPONSE)。
触发时机与上下文:API 调用或字段渲染时;可用 getApplication()、getParameter(...)。
返回值契约:JSON 串 / CSV 串 / HTML 串,由调用方消费。
示例代码(按视图名取数据 → 转 JSON 数组):
(function () {
var viewName = "订单列表";
var viewProcess = getViewProcess();
var view = viewProcess.doViewByViewName(viewName, getApplication());
if (view == null) {
return "[]";
}
var sql = view.getSQL(); // 取视图设计器里的 SQL(已含 domainid 等条件)
var datas = queryBySQL(sql);
var rows = [];
if (datas != null && datas.size() > 0) {
for (var it = datas.iterator(); it.hasNext(); ) {
var d = it.next();
rows.push({
id: d.getId(),
订单号: d.getItemValueAsString("订单号"),
金额: d.getItemValueAsDouble("金额")
});
}
}
return JSON.stringify(rows);
})()
代码说明:
- getViewProcess().doViewByViewName(name, application) / doView(viewId):取视图对象;view.getSQL():视图设计器配置的查询 SQL。
- 拿到 SQL 后用 queryBySQL 取数,遍历 Collection 转 JS 数组(注意是 Java List,用 .size() 与 .iterator(),GraalVM 差异「Java List .size() vs JS 数组」)。
- 分页、统计、导出 CSV/HTML、按 getParameter("filterValue") 动态过滤等多种变体见 common-script-collection → get-grid-view-on-page。
变体与扩展:
- CSV 导出:循环拼接 field1,field2,field3\n,注意字段含逗号时要加引号转义。
- HTML 表格:拼 <table>...<tr>...<td>...</td></tr>...</table>,作为计算脚本回填到只读字段展示。
- 分页:循环里用下标 index 控制 startIndex ≤ index < endIndex,构造 { total, pageIndex, pageSize, data: [] }。
- 动态过滤:sql += " AND item_" + filterField + " = '" + filterValue + "'"(注意 SQL 注入风险,filterField 用白名单)。
常见坑:
- 视图名大小写:doViewByViewName 严格匹配,写错大小写返回 null。
- view.getSQL() 拼接用户输入:直接 sql += " AND item_xxx = '" + filterValue + "'" 有注入风险——filterValue 必须校验/转义。
- 大结果集:视图数据量大时一次性 queryBySQL 会拖垮内存;务必分页或 LIMIT。
本章常见坑汇总¶
| # | 坑 | 正确做法 | 详见 |
|---|---|---|---|
| 1 | 把 queryBySQL / queryByDSName 返回的 Collection 当 JS 数组(.length / forEach / [i]) |
用 .size() 取长度、.iterator() 遍历、.get(i) 取元素 |
GraalVM 差异「Java List .size() vs JS 数组」 |
| 2 | SQL 表名漏 tlk_ 前缀、字段漏 item_ 前缀 |
业务表统一 tlk_<表单名>、业务字段 item_<字段名> |
「SQL 查询与聚合统计」/「SQL 批量更新表单字段」 |
| 3 | 平台表查询漏 DOMAINID 导致跨域读 |
平台表必带 DOMAINID = '...';业务表也建议加 domainid |
「SQL 查询与聚合统计」/「记录存在判断与计数」 |
| 4 | SQL 拼接用户输入导致注入 | 优先用 DQL 过滤或文档对象 setValue;必须拼 SQL 时白名单校验 |
「SQL 批量更新表单字段」/「取网格视图数据转 JSON / 导出」 |
| 5 | 校验脚本返回 true/false |
校验契约是字符串:"" 通过、非空串失败 |
「记录存在判断与计数」/ 返回值契约一览 |
| 6 | updateByDSName 后视图不刷新 / 不触发联动 |
SQL 直改不触发文档事件;需要联动用 findItem().setValue |
「SQL 批量更新表单字段」 |
| 7 | 视图 _selects 末尾有空字符 / 只替换首个分号 |
剪裁后用 replace(/;/g, "','") 全替换 |
「批量生成子表 / 复制字段」 |
| 8 | doStartFlow 启动人不在首节点 |
启动人必须是首节点处理人;_flowid 取自流程定义 |
「自动创建文档并启动流程」 |
| 9 | 删除流程未结束的单据破坏流程实例 | 删前校验 getStateLabel / getStateInt |
「批量删除文档」 |
| 10 | getItemValueAsDouble 在 SUM 无结果时返回 null 参与运算 |
|| 0 兜底再算 |
「SQL 查询与聚合统计」 |
通用 GraalVM 写法差异(
.equals→===、.length()→.length、java.util.X→Java.type(...))见全书锚点 GraalVM 差异。
相关章节¶
- 校验场景(含子表非空、查重组合校验):02-validation.md
- 视图过滤(DQL/SQL/PROCEDURE 三种模式):09-view-filter-and-column.md
- 流程相关批量(流程实例查询、终止、回退):06-flow-history-intervention.md
- 定时任务里使用
queryByDSName(无用户上下文):13-task-api-widget.md - JSON 解析 / 跨脚本缓存 / 表单名 ↔ ID 互转等工具函数:appendix-toolbox.md