跳转至

定时任务、API 与 Widget

一句话定位:本章解决"在没有用户/文档上下文的位置跑脚本"——Quartz 定时任务、对外 HTTP API、首页 Widget 三类挂载点的契约与典型写法。它们的共同点是:触发者不是表单或视图刷新,所以 getCurrentDocument() / getWebUser() 等表单上下文 API 要么不可用、要么可能为空,必须改用 getApplication()queryByDSNamegetParameter 等"无上下文"API。

本章重点(务必记牢):

  1. 定时任务无用户、无文档上下文——getWebUser()getCurrentDocument() 都不可用;只能用 getApplication()queryByDSName / updateByDSName 等。
  2. TASK:TERMINATE 返回 true 会删除调度且不再执行——这是不可逆动作,误返 true 会让后续永远跑不起来。
  3. API 在 publicgetWebUser() 可能为空——别假设一定有当前用户;需要身份就用非 public 入口或在脚本里判空兜底。
  4. Widget 内容脚本 type=iscript 返回 HTML、type=page 返回 URL——同一属性 actionContent,由 Widget 类型决定平台怎么消费你的返回值。

跨章约定:默认包装为 IIFE (function () { ... return ...; })();字符串比较用 ===/!==;JS 字符串长度用 s.length(属性),Java 集合用 .size().iterator();新建 Java 类用 Java.type('java.util.X')。详见 GraalVM 差异

场景清单

场景 落点 / Label 主要素材
定时任务主脚本(无用户上下文,副作用) .tasktaskScriptTASK:SCRIPT timer-task、task
定时任务终止脚本(满足条件停调度) .taskterminateScriptTASK:TERMINATE task
API 响应脚本(ParamsTable/_content/可能空 WebUser) .apiresponseScriptAPI:RESPONSE api
Widget 内容脚本(iscript→HTML / page→URL) .widgetactionContentWIDGET:CONTENT widget
Widget 可见范围脚本 .widgetuserScriptauthMode=2),WIDGET:USERSCRIPT widget

定时任务主脚本(无用户上下文,副作用)

业务目标:每天凌晨把"已发布且超过 30 天"的公告自动归档;每小时把"待同步"状态的行push到下游系统;每周一统计上周 KPI 写入看板表——这类**周期性、批量、无需用户点按钮**的数据维护,挂在 Quartz 定时任务上自动执行。

写在哪儿:基础功能 → 定时任务 → 选中 .task → 脚本(属性 taskScript,Label TASK:SCRIPT)。挂载点说明见 where-to-use/basic-function/timer-task

触发时机与上下文:由 Quartz 按 .task 文件配置的 cron / 间隔触发,主脚本每次调度执行一次。没有业务文档、没有当前用户——可用环境与典型 API:

  • getApplication() —— 取当前应用 id(→ system-functions)。
  • queryByDSName(dsName, sql) / updateByDSName(dsName, sql) —— 跨数据源查询/更新(→ database API)。
  • queryBySQL(sql) / updateBySQL(sql) / countBySQL(sql) —— 默认数据源查询/更新/计数(→ curdb API)。
  • getDocProcess(applicationId) / getDocumentProcess(applicationId) —— 取文档业务过程对象做建/删文档(→ system-functions)。
  • ⚠️ getWebUser() / getCurrentDocument() 不可用,调用会返回 null 或抛错。

返回值契约无要求——平台不消费主脚本的 return,副作用即可(可写库、调外部接口、println 写日志)。一般省略 return

示例代码

// 每天凌晨把"已发布且超过 30 天未活动"的公告状态改为已归档
(function () {
  var ds = "默认数据源"; // 与平台数据源配置名一致
  var sql = "update tlk_公告 "
          + " set item_状态 = '03' " // 03=已归档
          + " where item_状态 = '02' " // 02=已发布
          + " and datediff(day, item_发布时间, now()) > 30";
  updateByDSName(ds, sql);

  // 同时按部门汇总本周新增公告数,写入看板表
  var statsSql = "select item_发布部门, count(1) as cnt "
          + " from tlk_公告 "
          + " where item_状态 in ('02','03') "
          + " and yearweek(item_发布时间) = yearweek(now()) "
          + " group by item_发布部门";
  var rows = queryByDSName(ds, statsSql);
  if (rows != null) {
    for (var it = rows.iterator(); it.hasNext();) {
      var row = it.next();
      var dept = row.get("item_发布部门");
      var cnt = row.get("cnt");
      // 用 upsert 维护看板表
      updateByDSName(ds,
        "update tlk_公告看板 set item_本周数 = " + cnt
        + " where item_部门 = '" + dept + "'");
    }
  }
})()

代码说明

  • updateByDSName(dsName, sql) —— 指定数据源执行 update/insert/delete(→ database API);同源也可用 updateBySQL(sql)(→ curdb API)。
  • queryByDSName(dsName, sql) —— 返回 java.util.Collection必须用 .iterator() 遍历(→ database API)。
  • getApplication() —— 取应用 id;如果脚本需要建/查文档而非裸 SQL,可 var dp = getDocProcess(getApplication()); 再调 dp.doQuery(...) / doUpdate(...)(→ system-functions)。
  • 表前缀 tlk_<表单名>、字段前缀 item_<字段名> 是平台默认建表约定——SQL 里要带这些前缀,不是表单设计器里的中文别名。

变体与扩展

  • 跨系统同步:在主脚本里调 var resp = post(url, payload); 推送数据到下游(→ http-functions);记录成功/失败条数到日志表。
  • 自动建文档并启动流程:用 getDocProcess(getApplication()) + doCreate(formName, params) 建单,再调流程 API doStartFlow(...)(详见数据查询、统计与文档批量操作 08-data-query-and-doc-ops 的"自动建文档并启动流程"场景)。
  • 发提醒邮件/IM:搭配 mail-functions / message-functions 给管理员发执行报告。
  • 分布式锁/防重入:长任务可在专用表里加 running 标记,主脚本开头判标记,避免上一轮没跑完时本轮又启动。

常见坑

  • getWebUser() 在定时任务里不可用——这里没有 HTTP 会话。若复用了表单脚本里的代码段(例如直接 var uid = getWebUser().getId();),上线后定时任务一跑就 NPE。改用配置项(应用 id、固定管理员 id)或从 getApplication() 派生(→ GraalVM 差异 「Java List .size() vs JS 数组」 也提醒:Java 集合遍历用 .iterator(),别当 JS 数组)。
  • getCurrentDocument() 也是空的——定时任务没有"当前文档"。需要查文档请显式 queryByDSName / getDocProcess().doQuery(...)
  • 副作用不会自动落库——findItem().setValue(...) 这类依赖当前文档的写法在这里不适用;改用 updateByDSName(sql)getDocumentProcess().doUpdate(doc) 显式持久化。
  • SQL 注意 DOMAINID 与表前缀——平台业务表多数要按域隔离,跨域查询带 DOMAINID 过滤;表名 tlk_xxx、字段 item_xxx 别写错。
  • 数据源名大小写敏感——queryByDSName 第一参数必须与「数据源配置」名完全一致,含大小写。
  • 长时间阻塞——定时任务里同步调外部 HTTP 容易拖死调度线程,建议设超时或改异步。

定时任务终止脚本(满足条件停调度)

业务目标:给一个"截止日期"或"达到次数"的硬约束,到期后让这个调度**自动停掉自己**——例如每天同步外部数据的任务,在外部系统下线后自动停止;或"提前 7 天提醒审批"任务在文档归档后停止。

写在哪儿:基础功能 → 定时任务 → 选中 .task → 终止脚本(属性 terminateScript,Label TASK:TERMINATE)。挂载点说明见 where-to-use/basic-function/timer-task

触发时机与上下文每次执行主脚本之前**触发一次。可用环境同主脚本——getApplication()queryByDSName 等;同样**无 getWebUser() / getCurrentDocument()

返回值契约Boolean

返回值 含义
true 删除该调度,且本次不执行主脚本,后续也不再触发——不可逆
false / 其它类型 不终止,继续执行主脚本。
null / 空 视为不终止,跳过判断继续执行。

示例代码

// 当"项目结束日期"已过,则停止后续调度
(function () {
  var ds = "默认数据源";
  var sql = "select item_结束时间 from tlk_项目配置 where id = 'config-001'";
  var row = findBySQL(sql);
  if (row == null) {
    return false; // 配置不存在时不要终止,避免误删调度
  }
  var endTime = row.getItemValueAsString("结束时间");
  if (endTime == null || endTime === "") {
    return false;
  }
  // 字符串日期比较:用时间戳
  var end = new Date(endTime.replace(/-/g, "/")).getTime();
  var now = new Date().getTime();
  return now > end; // 当前已晚于结束时间 -> true -> 删除调度
})()

代码说明

  • findBySQL(sql) —— 单行查询(→ curdb API);返回 null 时务必判空再取字段。
  • 日期比较:JS Datenew Date(str) 解析、.getTime() 取毫秒戳(→ date-functions)。
  • return 必须是布尔——返回字符串 "true" 不会被平台解析为终止,会被当作"其它类型 → 不终止"。

变体与扩展

  • 按次数终止:维护一个计数表,每次终止脚本里 update tlk_计数 set cnt = cnt + 1,再 select cnt 比对上限。
  • 按外部信号终止:调用外部接口(→ http-functions)查"是否还需同步",返回的 JSON 字段决定是否终止。
  • 配合主脚本"软退出":终止脚本返回 false,但主脚本里自己判条件后早返回——比硬终止更可逆。

常见坑

  • ⚠️ return true 不可逆——一旦返回 true,调度被**删除**,下次想恢复得手工重建调度。判断条件写错(例如把 > 写成 >=)会提前误删。强烈建议:调试期把终止脚本临时返回 false,验证主脚本稳定后再开 true
  • return "true" 不等于 return true——平台按 Boolean 解析;字符串、数字、对象都被视作"不终止"。
  • 同主脚本一样无用户上下文——别在终止脚本里调 getWebUser() / getCurrentDocument()(同 定时任务主脚本常见坑)。
  • 判空顺序findBySQL 返回 null 时直接 row.getItemValueAsString(...) 会 NPE,导致终止脚本抛错——平台通常按"未终止"处理,但这会掩盖你的判断逻辑,建议先判空。
  • 字符串日期格式差异——new Date("2026-08-04") 在不同 JDK/GraalVM 版本下解析行为略有差异;保险写法 new Date("2026/08/04") 或用 format(...)(→ date-functions)做中间格式化。

API 响应脚本(ParamsTable/_content/可能空 WebUser)

业务目标:自定义对外 HTTP 接口——给前端单页、第三方系统、小程序返回动态数据;按 path 参数、query、body 计算业务结果并以 JSON/HTML/纯文本回写响应体。

写在哪儿:API 中心 → 选中 .api → 响应脚本(属性 responseScript,Label API:RESPONSE)。素材见 agent-skills-usage/skills/iscript-usage/api

触发时机与上下文:HTTP 请求命中该 API(按 method + path 匹配)时触发。可用环境:

  • getParameter("name") —— 取 path 变量、query 参数、或 body 关键字段;getParameter("_content") 取原始请求体字符串(→ system-functions)。
  • getApplication() —— 取应用 id(→ system-functions)。
  • queryByDSName / queryBySQL / updateByDSName 等(→ database API)。
  • ⚠️ getWebUser() 取决于 API 是否标记为 public——public 时**可能为 null**;非 public 时平台会先做身份校验。
  • getCurrentDocument() 通常为空——API 调用没有"当前文档"概念。

返回值契约:写入响应体的内容(String 为主,也可为其它可序列化对象)。null → 返回 200 空响应体。

示例代码

按 path 参数取部门,返回部门信息 JSON:

(function () {
  var deptId = getParameter("departmentId"); // path: /depts/{departmentId}
  if (deptId == null || deptId === "") {
    // 失败响应:返回 4xx 风格的 JSON 文本
    return "{\"code\":400,\"msg\":\"departmentId required\"}";
  }
  var sql = "select id, item_部门名称 as name, item_部门代码 as code "
          + " from tlk_部门 where id = '" + deptId + "'";
  var row = findBySQL(sql);
  if (row == null) {
    return "{\"code\":404,\"msg\":\"not found\"}";
  }
  // 手工拼 JSON(生产建议用 JSONObject,避免引号/转义错误)
  var name = row.getItemValueAsString("name");
  var code = row.getItemValueAsString("code");
  return "{\"code\":0,\"data\":{\"id\":\"" + deptId + "\","
        + "\"name\":\"" + name + "\","
        + "\"code\":\"" + code + "\"}}";
})()

读 body 后落库(POST/PUT 场景):

(function () {
  var body = getParameter("_content"); // 原始 body 字符串
  if (body == null || body === "") {
    return "{\"ok\":false,\"msg\":\"empty body\"}";
  }
  var JSONObject = Java.type('org.json.JSONObject');
  var json = new JSONObject(body);
  var title = json.getString("title");
  var amount = json.getDouble("amount");

  updateByDSName("默认数据源",
    "insert into tlk_外部单据 (id, item_标题, item_金额, domainid) values ("
    + "'" + $CURDOC.util.getUUID() + "', "
    + "'" + title.replace(/'/g, "''") + "', "
    + amount + ", "
    + "'" + getApplication() + "')");

  return "{\"ok\":true}";
})()

代码说明

  • getParameter("name") —— 取请求参数(path / query / _content)(→ system-functions)。
  • findBySQL(sql) / updateByDSName(ds, sql) —— 查询 / 更新(→ curdb API / database API)。
  • Java.type('org.json.JSONObject') —— GraalVM 推荐 Java 类引用方式(→ GraalVM 差异 「取 Java 类:用 Java.type(...),」);用 getString / getDouble 取字段。
  • 返回 null → 200 空体;返回非空字符串 → 原样写响应体;可在 API 配置里指定 Content-Type(JSON / HTML / 纯文本)。

变体与扩展

  • 鉴权:API 不开 public 时,平台先做身份校验,脚本里再用 getWebUser() 取调用者;也可读请求头 Authorization 自己实现 token 校验。
  • 大列表分页:取 getParameter("page") / getParameter("size") 拼 SQL limit/offset
  • 调用其它服务:用 http-functions 转发请求或聚合上游数据后统一返回。
  • 文件上传/下载:通过 _content(base64 或 multipart 原文)解析后落附件;下载直接返回字节流(参考 pdf / ocr 等 API)。

常见坑

  • ⚠️ publicgetWebUser() 可能为空——若脚本依赖用户身份(如"按当前用户过滤数据"),要么别把 API 设为 public,要么在脚本里**先判空**:var u = getWebUser(); if (u == null) { return "{\"code\":401,\"msg\":\"no user\"}"; }
  • JSON 手拼易错——引号、反斜杠、Unicode 转义都容易出错;推荐用 Java.type('org.json.JSONObject')JSONArray 构造,再 .toString() 返回(→ [GraalVM 差异 「字符串长度:用 .length 属性,禁用 .le」](getting-started.md#graalvm-差异):JSONArray.length()` 仍是方法,别改成属性)。
  • SQL 注入——直接拼用户输入到 SQL 高危;至少 replace(/'/g, "''") 转义,更稳的做法是用参数化查询或先做白名单校验。
  • HTTP 状态码——脚本本身只能写响应体;想返回 4xx/5xx 状态码要靠 API 配置层或返回约定字段让前端判。
  • getCurrentDocument() 通常为空——别假设有"当前文档";查文档用 queryBySQL / getDocProcess(getApplication()).doQuery(...)(→ system-functions)。
  • 大请求体——_content 是完整 body 字符串,大文件上传会占内存;超阈值改用专用附件接口。

Widget 内容脚本(iscript→HTML / page→URL)

业务目标:在系统首页 Widget 卡片里显示动态内容——例如"我的待办数"、"今日 KPI 小卡片"、"部门公告速览";或把卡片做成一个外链入口,点进去跳到指定页面。

写在哪儿:首页 Widget → 选中 .widget → 内容脚本(属性 actionContent,**Widget 类型**决定返回怎么消费,Label WIDGET:CONTENT)。挂载点说明见 where-to-use/basic-function/widget。素材见 agent-skills-usage/skills/iscript-usage/widget

触发时机与上下文:Widget 渲染(首页打开或刷新)时触发。可用环境:

  • getWebUser() —— 当前登录用户(Widget 在登录会话下渲染,通常不为空,但仍建议判空)。
  • getApplication()queryBySQL / queryByDSNamegetParameter 等。
  • 与定时任务不同,Widget 里有用户上下文,可安全使用 getWebUser().getId() / .getName()

返回值契约:取决于 Widget 类型(在 .widget 配置里选):

Widget 类型 返回值形态 平台处理
type=iscript String HTML 片段 直接作为卡片内容渲染(等价于一块 innerHTML)
type=page String URL 卡片变成 iframe/跳转入口,按 URL 加载页面

示例代码

type=iscript —— 返回"今日待办数"HTML 卡片:

(function () {
  var u = getWebUser();
  if (u == null) {
    return "<div class='muted'>未登录</div>";
  }
  var uid = u.getId();
  var sql = "select count(1) as cnt from tlk_待办 "
          + " where item_处理人 = '" + uid + "' and item_状态 = '待处理'";
  var row = findBySQL(sql);
  var cnt = row != null ? row.getItemValueAsString("cnt") : "0";
  var color = cnt === "0" ? "#9aa0a6" : "#e6a23c";
  return "<div style='padding:12px;'>"
       + "<div style='font-size:12px;color:#909399;'>我的待办</div>"
       + "<div style='font-size:28px;color:" + color + ";font-weight:600;'>"
       + cnt + "</div>"
       + "</div>";
})()

type=page —— 返回一个外部链接:

(function () {
  // 直接返回相对或绝对 URL,平台按页面类型加载
  return "/portal/my-todo-list";
})()

代码说明

  • getWebUser().getId() / .getName() —— 取当前用户(→ curruser API)。
  • findBySQL(sql) —— 单值查询(→ curdb API)。
  • HTML 字符串拼接时注意转义——业务数据里的 <>&' 容易破坏 DOM,可用 replace(/[<>&']/g, ...) 转义或用 Packages.cn.myapps.common.util.Security 类(旧示例常见)。
  • type=page 模式下脚本可以非常简单(直接 return "/xxx"),主要价值在于"动态算 URL"——例如按角色返回不同入口。

变体与扩展

  • 多块卡片组合:返回多段 <div> 拼成的 dashboard 风格 HTML,配合内联 <style> 做布局。
  • 图表嵌入:返回一个 <iframe> 指向平台的图表页(图表本身用报表、图表与可视化 12-report-and-chartCHART:SCRIPT)。
  • 动态 URL 参数return "/portal/report?id=" + getApplication(); 把当前应用 id 带给目标页。
  • 缓存优化:Widget 高频渲染,复杂 SQL 可考虑附录 A:工具函数与通用技巧 appendix-toolbox 描述的"跨脚本缓存"私有空间写法。

常见坑

  • ⚠️ type=iscript vs type=page 返回类型必须对得上——把 URL 当 HTML 返回(或反过来),前端会按 HTML/URL 解析得到一团乱码或 404。挂脚本前先确认 .widget 的类型字段。
  • HTML 字符串拼接易错——内联样式里 ' 与 JS 字符串引号冲突,可改用 " 包 HTML、' 包属性,或用模板字符串 `(GraalVM 支持)。
  • 避免 XSS——把用户姓名等动态值拼进 HTML 前,至少做 <>&"' 五种字符转义。
  • SQL 性能:Widget 每次开首页都跑——大表查询要加索引、限制结果集大小,避免首页打开卡顿。
  • getWebUser() 兜底判空:登录态异常或被嵌到无登录 iframe 时可能为 null,写法上参考示例先判空。

Widget 可见范围脚本

业务目标:让同一个 Widget 卡片**只对部分用户可见**——例如"管理员后台入口"卡片仅显示给系统管理员角色;"分公司销售看板"卡片只显示给对应分公司成员。

写在哪儿:首页 Widget → 选中 .widget → 用户脚本(属性 userScript需要把 Widget 的授权模式设为 authMode=2 才会生效,Label WIDGET:USERSCRIPT)。素材见 agent-skills-usage/skills/iscript-usage/widget

触发时机与上下文:平台计算 Widget 对谁可见时触发。可用环境同 Widget 内容脚本getWebUser()getApplication()、组织架构 API(→ usercenter API)。

返回值契约:返回**用户 id 的集合**或 UserVO 对象的集合——集合中**包含当前用户 id** 时该用户能看到此 Widget;否则隐藏。

返回类型 平台处理
Collection<String>(用户 id 字符串集合) 当前用户 id 在集合中 → 可见
Collection<UserVO>(用户对象集合) 平台提取 id 后比对
null / 空 视平台版本为"对所有人不可见"或"对所有人可见",不要依赖此默认行为,显式返回集合

示例代码

按角色返回可见用户集合("系统管理员"角色的用户能看到此 Widget):

(function () {
  var roleId = "<角色ID>"; // 系统管理员角色 id(设计器或 getRoleIdByName 取得)
  // 按角色取用户集合(java.util.Collection<UserVO>),可直接返回
  var users = getUsersByRoleId(roleId);
  return users; // 平台会从中提取用户 id 比对
})()

直接按用户 id 列表返回(用 Java.type 新建 ArrayList):

(function () {
  var ArrayList = Java.type('java.util.ArrayList');
  var list = new ArrayList();
  var u = getWebUser();
  if (u != null) {
    list.add(u.getId()); // 至少对当前用户可见
  }
  // 加上其它指定用户
  list.add("用户A的id");
  list.add("用户B的id");
  return list;
})()

代码说明

  • getUsersByRoleId(roleId) —— 按角色取用户对象集合(→ usercenter API);也可用 getUsersByDptId(dptId) 按部门取。
  • Java.type('java.util.ArrayList') —— GraalVM 推荐 Java 类引用方式,不要裸写 new java.util.ArrayList()(→ GraalVM 差异 「取 Java 类:用 Java.type(...),」)。
  • 平台对返回集合的元素类型容错——String(id)、UserVO、甚至 User 都可以,会自动提取 id 比对。

变体与扩展

  • 按部门可见getUsersByDptId(deptId) 替换 getUsersByRoleId(→ usercenter API)。
  • 多角色合并:取多个角色用户集合后,用临时 HashMap 按 id 去重再合并(参考附录 A:工具函数与通用技巧 appendix-toolbox 的字符串/ID 列表去重技巧)。
  • 按业务条件动态判定:例如"年度活跃用户才可见"——先 queryBySQL 取活跃用户 id 列表,再装进 ArrayList 返回。
  • 当前用户特别加白名单:脚本开头无条件 list.add(getWebUser().getId()),保证至少自己能看见,便于调试。

常见坑

  • ⚠️ 必须 authMode=2——Widget 授权模式若保持默认(01),脚本写了也不生效。挂脚本前先在 .widget 设计器里把授权模式切到 2(脚本授权)。
  • ⚠️ 不要裸写 new java.util.ArrayList()——GraalVM 下应使用 Java.type('java.util.ArrayList')(旧示例里 new java.util.ArrayList() 的写法在新引擎上可能报错,详见 GraalVM 差异 「取 Java 类:用 Java.type(...),」)。
  • getUsersByRoleId 返回的是 Java Collection——不要用 .length 取长度,用 .size();遍历用 .iterator()(→ GraalVM 差异 「Java List .size() vs JS 数组」)。
  • 返回 null 行为不确定——部分版本按"全员可见"处理,部分按"全员不可见"。生产脚本请**显式返回非空集合**,不要靠默认。
  • 性能:脚本在每次渲染首页时执行;如果取角色用户列表耗时,可缓存结果(参考 appendix-toolbox 的跨脚本缓存写法)。
  • 返回集合元素类型——尽量统一为用户 id 字符串,避免在 UserVO/User 混排时被某些版本的平台解析出错。

章末小结

  • 定时任务TASK:SCRIPT / TASK:TERMINATE)是无用户、无文档上下文的"纯调度"环境——只用 getApplication() + queryByDSName / updateByDSName;终止脚本 return true 不可逆。
  • API 响应脚本API:RESPONSE)通过 getParameter 取 path / query / _contentpublic 模式下 getWebUser() 可能为空。
  • WidgetWIDGET:CONTENT / WIDGET:USERSCRIPT)在登录会话下渲染,getWebUser() 可用;内容脚本按 type=iscript(HTML)或 type=page(URL)返回;可见范围脚本要求 authMode=2 才生效。

下一步:函数签名细节见 iscript-guid/functionsiscript-guid/api;引擎差异回到前言 GraalVM 差异