定时任务、API 与 Widget¶
一句话定位:本章解决"在没有用户/文档上下文的位置跑脚本"——Quartz 定时任务、对外 HTTP API、首页 Widget 三类挂载点的契约与典型写法。它们的共同点是:触发者不是表单或视图刷新,所以
getCurrentDocument()/getWebUser()等表单上下文 API 要么不可用、要么可能为空,必须改用getApplication()、queryByDSName、getParameter等"无上下文"API。
本章重点(务必记牢):
- 定时任务无用户、无文档上下文——
getWebUser()与getCurrentDocument()都不可用;只能用getApplication()、queryByDSName/updateByDSName等。 TASK:TERMINATE返回true会删除调度且不再执行——这是不可逆动作,误返true会让后续永远跑不起来。- API 在
public时getWebUser()可能为空——别假设一定有当前用户;需要身份就用非 public 入口或在脚本里判空兜底。 - 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 | 主要素材 |
|---|---|---|
| 定时任务主脚本(无用户上下文,副作用) | .task → taskScript,TASK:SCRIPT |
timer-task、task |
| 定时任务终止脚本(满足条件停调度) | .task → terminateScript,TASK:TERMINATE |
task |
| API 响应脚本(ParamsTable/_content/可能空 WebUser) | .api → responseScript,API:RESPONSE |
api |
| Widget 内容脚本(iscript→HTML / page→URL) | .widget → actionContent,WIDGET:CONTENT |
widget |
| Widget 可见范围脚本 | .widget → userScript(authMode=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)建单,再调流程 APIdoStartFlow(...)(详见数据查询、统计与文档批量操作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
Date用new 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")拼 SQLlimit/offset。 - 调用其它服务:用
http-functions转发请求或聚合上游数据后统一返回。 - 文件上传/下载:通过
_content(base64 或 multipart 原文)解析后落附件;下载直接返回字节流(参考pdf/ocr等 API)。
常见坑:
- ⚠️
public时getWebUser()可能为空——若脚本依赖用户身份(如"按当前用户过滤数据"),要么别把 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/queryByDSName、getParameter等。- 与定时任务不同,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 —— 返回一个外部链接:
代码说明:
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-chart的CHART:SCRIPT)。 - 动态 URL 参数:
return "/portal/report?id=" + getApplication();把当前应用 id 带给目标页。 - 缓存优化:Widget 高频渲染,复杂 SQL 可考虑附录 A:工具函数与通用技巧
appendix-toolbox描述的"跨脚本缓存"私有空间写法。
常见坑:
- ⚠️
type=iscriptvstype=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 授权模式若保持默认(0或1),脚本写了也不生效。挂脚本前先在.widget设计器里把授权模式切到2(脚本授权)。 - ⚠️ 不要裸写
new java.util.ArrayList()——GraalVM 下应使用Java.type('java.util.ArrayList')(旧示例里new java.util.ArrayList()的写法在新引擎上可能报错,详见 GraalVM 差异 「取 Java 类:用Java.type(...),」)。 getUsersByRoleId返回的是 JavaCollection——不要用.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 /_content;public模式下getWebUser()可能为空。 - Widget(
WIDGET:CONTENT/WIDGET:USERSCRIPT)在登录会话下渲染,getWebUser()可用;内容脚本按type=iscript(HTML)或type=page(URL)返回;可见范围脚本要求authMode=2才生效。
下一步:函数签名细节见
iscript-guid/functions与iscript-guid/api;引擎差异回到前言 GraalVM 差异。