跳转至

数据查询、统计与文档批量操作

本章面向**业务数据层**:从一次 SQL 查询、一条聚合统计,到批量改字段、自动建文档、批量删单等"数据侧"操作。前置阅读 getting-started.md(特别是 GraalVM 差异 一节,平台返回的 queryBySQL/queryByDSName 结果集是 Java Collection,遍历用 .iterator()、长度用 .size(),不要混用 JS 数组写法)。

场景清单

场景 主要落点 关键 API / 素材
SQL 查询与聚合统计(标杆) 值脚本 / 计算脚本 / 报表 queryBySQLfindBySQL、SQL SUM/COUNT/AVG
记录存在判断与计数 校验脚本 / 值脚本 countBySQLfindBySQL
跨数据源查询 值脚本 / 定时任务 queryByDSNamecountByDSName
通过 HTTP/API 取数 API 响应 / 值脚本 / 计算 httpGet/httpPost、外部接口
SQL 批量更新表单字段 操作后置 updateByDSName
自动创建文档并启动流程 操作后置 / 定时 docProcess.doNewdoCreatedoStartFlow
批量生成子表 / 复制字段 视图确认 / 操作后置 / 表单按钮 getRelateDocumentDOC.buileDocument
批量删除文档 视图操作(前/后置) docProcess.doViewdoRemove
取网格视图数据转 JSON / 导出 计算脚本 / 其它 getViewProcessview.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-functionssum-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,做算术前 || 0null。 - 表名前缀漏写:业务表必须 tlk_ 开头、业务字段必须 item_ 开头;平台表(如 t_usert_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。 - 仅判断存在性时 countBySQLqueryBySQL 更高效(不传输字段数据)。 - 若既要判存在又要取记录详情,用 findBySQL(sql)null;详见 common-script-collection → check-record-exists-or-count

变体与扩展: - 多字段查重AND 串接更多 item_xxx 条件。 - 按 ID 判存在SELECT id FROM tlk_xxx WHERE id = '...' + findBySQLnull。 - 分类计数:分别 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):按数据源名查询,返回 CollectioncountByDSName / 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 = '...'")insertByDSNamedeleteByDSName。 - 事务:多步写入用 beginTransaction → 各 updateByDSNamecommitTransaction,异常时 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 → dociscript-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——详见源文档。 - 批量新建:循环调用,但注意事务/性能;大批量建议改放定时任务。

常见坑: - 启动人不在首节点:调用 doStartFlowOrUpdateuser 必须是该流程首节点的处理人,否则流程引擎报错。 - _flowid 必填:流程 ID 在平台系统表(流程定义)里查,不是表单 ID,也不是节点 ID。 - 字段类型与 addXxxItem 匹配:日期字段必须 addDateItem、数值字段必须 addDoubleItem,类型不匹配会静默失败。 - newDoc.setId(...) 慎用:除非有特殊业务主键约束,**不要**自行设置 ID;让引擎用 Sequence 生成。


批量生成子表 / 复制字段

业务目标:从视图选中的若干条记录(或当前表单的某字段集合),批量生成目标表单的子表记录;或将选中源文档的字段复制到当前文档。

写在哪儿: - 视图 → 操作按钮「确认脚本 / 跳转脚本」(Label ACTIVITY:BEFOREACTIVITY: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-scriptview-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 数组,用 .lengtharr[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 getItemValueAsDoubleSUM 无结果时返回 null 参与运算 || 0 兜底再算 「SQL 查询与聚合统计」

通用 GraalVM 写法差异(.equals===.length().lengthjava.util.XJava.type(...))见全书锚点 GraalVM 差异

相关章节