@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.a87aa13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/charts/SKILL.md +4 -0
  3. package/steering/design-html/skills/pptx-style-extract/SKILL.md +58 -22
  4. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
  5. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +18 -12
  6. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
  7. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +1768 -241
  8. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +325 -22
  9. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -1
  10. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +379 -156
  11. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +6 -3
  12. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
  13. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +16 -10
  14. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
  15. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  16. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +62 -0
  17. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +378 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +98 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +168 -0
  21. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
  22. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
  23. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
  24. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
  25. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
  26. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
  27. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
  28. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
  29. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
  30. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
  31. package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
  32. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +255 -0
  33. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
  34. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
  35. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
  36. package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
  37. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
  38. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
  39. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
  40. package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
  41. package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
  42. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  43. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
  44. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
  45. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
  46. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
  47. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
  48. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
  49. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
  50. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
  51. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
  52. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
  53. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
  54. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
  55. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
  56. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
  57. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
@@ -1,91 +0,0 @@
1
- # 权限管理 (Permission)
2
-
3
- > 开放平台文档(Markdown 版):<https://open.larkoffice.com/document/server-docs/docs/permission/overview>
4
- .md
5
-
6
- 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中管理飞书云文档的协作者权限。
7
-
8
- ## 所需权限
9
-
10
- | 权限标识 | 说明 |
11
- |----------|------|
12
- | `drive:permission` | 管理文档/文件的协作者权限 |
13
-
14
- > **敏感操作警告**:权限管理涉及文档访问控制,添加/移除协作者会直接影响用户的文档可见性。操作前请确认目标对象和权限级别。
15
-
16
- ## 列出协作者
17
-
18
- ```typescript
19
- const res = await client.drive.permissionMember.list({
20
- path: { token: 'ABC123' },
21
- params: { type: 'docx' },
22
- });
23
- const members = res.data?.items ?? [];
24
- // members: [{member_type, member_id, perm, name}, ...]
25
- ```
26
-
27
- ## 添加协作者
28
-
29
- ```typescript
30
- const res = await client.drive.permissionMember.create({
31
- path: { token: 'ABC123' },
32
- params: { type: 'docx', need_notification: false },
33
- data: {
34
- member_type: 'email',
35
- member_id: 'user@example.com',
36
- perm: 'edit',
37
- },
38
- });
39
- ```
40
-
41
- ## 移除协作者
42
-
43
- ```typescript
44
- await client.drive.permissionMember.delete({
45
- path: { token: 'ABC123', member_id: 'user@example.com' },
46
- params: { type: 'docx', member_type: 'email' },
47
- });
48
- ```
49
-
50
- ## Token 类型参考
51
-
52
- | 类型 | 说明 |
53
- |------|------|
54
- | `doc` | 旧版文档 |
55
- | `docx` | 新版文档 |
56
- | `sheet` | 电子表格 |
57
- | `bitable` | 多维表格 |
58
- | `folder` | 文件夹 |
59
- | `file` | 上传的文件 |
60
- | `wiki` | 知识库节点 |
61
- | `mindnote` | 思维导图 |
62
-
63
- ## 成员类型参考
64
-
65
- | 类型 | 说明 |
66
- |------|------|
67
- | `email` | 邮箱地址 |
68
- | `openid` | 用户 open_id |
69
- | `userid` | 用户 user_id |
70
- | `unionid` | 用户 union_id |
71
- | `openchat` | 群聊 open_id |
72
- | `opendepartmentid` | 部门 open_id |
73
- | `groupid` | 用户组 ID |
74
- | `wikispaceid` | 知识空间 ID |
75
-
76
- ## 权限级别参考
77
-
78
- | 权限值 | 说明 |
79
- |--------|------|
80
- | `view` | 仅查看 |
81
- | `edit` | 可编辑 |
82
- | `full_access` | 完全访问(可管理权限) |
83
-
84
- ## Common Mistakes
85
-
86
- | 错误 | 正确做法 |
87
- |------|----------|
88
- | token 类型与文件实际类型不匹配 | `type` 参数必须与文件实际类型一致(docx/sheet/bitable 等) |
89
- | 用 wiki URL 的 token 直接操作权限 | Wiki 节点需用 `wiki` 类型,或先获取 `obj_token` 用对应类型 |
90
- | 添加协作者时成员不存在 | 确认 member_id 正确,email 需要是飞书注册邮箱 |
91
- | 移除自身的 full_access 权限 | 文档至少需要一个管理员,避免移除最后一个 full_access 成员 |
@@ -1,165 +0,0 @@
1
- # 知识库 (Wiki)
2
-
3
- > 开放平台文档(Markdown 版):<https://open.larkoffice.com/document/server-docs/docs/wiki-v2/wiki-overview>
4
- .md
5
-
6
- 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中操作飞书知识库。
7
-
8
- ## 所需权限
9
-
10
- | 权限标识 | 说明 |
11
- |----------|------|
12
- | `wiki:wiki` | 读写知识库 |
13
- | `wiki:wiki:readonly` | 只读知识库 |
14
-
15
- > 机器人需被添加为知识空间成员才能访问:知识空间 → 设置 → 成员管理 → 添加机器人。
16
-
17
- ## 从 URL 提取 Token
18
-
19
- URL 格式:`https://xxx.feishu.cn/wiki/{token}`
20
-
21
- ```typescript
22
- const url = 'https://xxx.feishu.cn/wiki/ABC123def';
23
- const token = new URL(url).pathname.split('/wiki/')[1]; // 'ABC123def'
24
- ```
25
-
26
- ## 列出知识空间
27
-
28
- ```typescript
29
- const res = await client.wiki.space.list({});
30
- const spaces = res.data?.items ?? [];
31
- // spaces: [{space_id, name, description, visibility}, ...]
32
- ```
33
-
34
- > 如果返回空列表,说明机器人未被添加到任何知识空间。
35
-
36
- ## 列出节点
37
-
38
- ```typescript
39
- // 列出空间根节点
40
- const res = await client.wiki.spaceNode.list({
41
- path: { space_id: '7xxx' },
42
- });
43
- const nodes = res.data?.items ?? [];
44
- // nodes: [{node_token, obj_token, obj_type, title, has_child}, ...]
45
-
46
- // 列出子节点
47
- const childRes = await client.wiki.spaceNode.list({
48
- path: { space_id: '7xxx' },
49
- params: { parent_node_token: 'wikcnXXX' },
50
- });
51
- ```
52
-
53
- ## 获取节点详情
54
-
55
- ```typescript
56
- const res = await client.wiki.space.getNode({
57
- params: { token: 'ABC123def' }, // 从 URL 提取的 token
58
- });
59
- const node = res.data?.node;
60
- // node: {node_token, space_id, obj_token, obj_type, title, parent_node_token, has_child, creator}
61
- ```
62
-
63
- > **关键**:返回的 `obj_token` 是实际文档/表格的 token,需用它来调用 docx/bitable 等 API。
64
-
65
- ## 创建节点
66
-
67
- ```typescript
68
- const res = await client.wiki.spaceNode.create({
69
- path: { space_id: '7xxx' },
70
- data: {
71
- obj_type: 'docx', // 节点类型
72
- node_type: 'origin', // 固定值
73
- title: '新页面',
74
- // parent_node_token: 'wikcnXXX', // 可选:父节点
75
- },
76
- });
77
- const node = res.data?.node;
78
- // node: {node_token, obj_token, obj_type, title}
79
- ```
80
-
81
- ### obj_type 取值
82
-
83
- | 值 | 说明 |
84
- |------|------|
85
- | `docx` | 新版文档(默认) |
86
- | `doc` | 旧版文档 |
87
- | `sheet` | 电子表格 |
88
- | `bitable` | 多维表格 |
89
- | `mindnote` | 思维导图 |
90
- | `file` | 文件 |
91
- | `slides` | 幻灯片 |
92
-
93
- ## 移动节点
94
-
95
- ```typescript
96
- await client.wiki.spaceNode.move({
97
- path: { space_id: '7xxx', node_token: 'wikcnXXX' },
98
- data: {
99
- target_space_id: '7yyy', // 目标空间(不传则同空间内移动)
100
- target_parent_token: 'wikcnYYY', // 目标父节点
101
- },
102
- });
103
- ```
104
-
105
- ## 重命名节点
106
-
107
- ```typescript
108
- await client.wiki.spaceNode.updateTitle({
109
- path: { space_id: '7xxx', node_token: 'wikcnXXX' },
110
- data: { title: '新标题' },
111
- });
112
- ```
113
-
114
- ## Wiki-Doc 工作流(关键)
115
-
116
- 知识库页面的内容读写必须通过 docx API,流程:
117
-
118
- ```typescript
119
- // 1. 获取节点详情 → 拿到 obj_token
120
- const nodeRes = await client.wiki.space.getNode({
121
- params: { token: wikiToken },
122
- });
123
- const objToken = nodeRes.data?.node?.obj_token;
124
-
125
- // 2. 用 obj_token 作为 doc_token 读取文档
126
- const contentRes = await client.docx.document.rawContent({
127
- path: { document_id: objToken },
128
- });
129
-
130
- // 3. 用 obj_token 作为 doc_token 写入文档
131
- const convertRes = await client.docx.document.convert({
132
- data: { content_type: 'markdown', content: '# 新内容\n\n正文...' },
133
- });
134
- await client.docx.documentBlockChildren.create({
135
- path: { document_id: objToken, block_id: objToken },
136
- data: { children: convertRes.data?.blocks ?? [] },
137
- });
138
- ```
139
-
140
- > **重要**:不要用 `node_token` 或 URL 中的 `token` 直接调用 docx API,必须用 `getNode()` 返回的 `obj_token`。
141
-
142
- ## 搜索不可用
143
-
144
- Wiki API 不提供搜索功能。获取内容需通过以下方式:
145
-
146
- - 通过 `spaceNode.list()` 浏览节点树
147
- - 通过 `space.getNode()` + URL 中的 token 直接查询
148
-
149
- ## 知识库访问设置
150
-
151
- 机器人需要被添加为知识空间成员才能访问:
152
-
153
- 1. 打开知识空间 → 设置 → 成员管理
154
- 2. 添加机器人应用
155
- 3. 参考:<https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa>
156
-
157
- ## Common Mistakes
158
-
159
- | 错误 | 正确做法 |
160
- |------|----------|
161
- | 用 wiki URL 中的 token 直接调用 docx API | 必须先 `getNode()` 获取 `obj_token`,再用 `obj_token` 调用 docx API |
162
- | 列出空间返回空但实际有内容 | 机器人未被添加为空间成员 |
163
- | 尝试搜索知识库内容 | Wiki API 不支持搜索,只能通过 `list` 浏览或 `getNode` 查询 |
164
- | 创建节点忘记传 `node_type: 'origin'` | `node_type` 是必填字段,值固定为 `'origin'` |
165
- | 混淆 `node_token` 和 `obj_token` | `node_token` 是知识库节点标识,`obj_token` 是实际文档标识 |
@@ -1,301 +0,0 @@
1
- # 触发器入参类型与代码示例
2
-
3
- 本 reference 承载 nestjs-react-fullstack 触发器 handler 的入参类型定义、完整代码示例与常见实现场景。先读主 [trigger-guide](../SKILL.md) 了解目录结构、绑定约束与配置要求。
4
-
5
- ## 触发器类型与入参
6
-
7
- 触发器类型(`triggerType`)有三种:
8
-
9
- - `record_change`:记录变更触发器,**有入参**
10
- - `cron`:定时触发器,**无入参**
11
- - `webhook`:Webhook 触发器,**有入参**
12
-
13
- ```typescript
14
- // 有入参触发器的入参类型(triggerType = 'record_change' 时)
15
- interface TaskHandlerArgs {
16
- attributes: {
17
- trigger: string;
18
- triggerID?: string;
19
- triggerType: 'record_change' | 'cron' | 'webhook';
20
- instanceID: string;
21
- startAt?: number;
22
- };
23
- content: {
24
- input: string; // JSON 字符串,根据 triggerType 解析为对应类型
25
- };
26
- }
27
-
28
- // record_change:input 解析后的数据结构
29
- interface DataChangeEventInput {
30
- id: string;
31
- tenant_id: number;
32
- workspace: string;
33
- branch: string;
34
- app: string;
35
- table: string;
36
- type: 'INSERT' | 'UPDATE' | 'DELETE';
37
- timestamp: number;
38
- before?: Record<string, unknown>; // DELETE 时有值,其他情况可能为空
39
- after?: Record<string, unknown>; // INSERT/UPDATE 时有值,其他情况可能为空
40
- msg_id: string;
41
- }
42
-
43
- // webhook:input 解析后的数据结构
44
- interface WebhookEvent {
45
- method: 'GET' | 'POST';
46
- url: string; // 完整 URL(含查询参数)
47
- host: string; // 不含查询参数的 URL
48
- path: string; // 路径部分
49
- query: Record<string, string[]>; // URL 查询参数,值为字符串数组
50
- headers: Record<string, string[]>; // 请求头,值为字符串数组
51
- body: string; // 请求体,JSON 字符串
52
- meta: {
53
- timestamp: number; // 请求时间戳(秒)
54
- traceID: string; // 追踪 ID
55
- env: 'development' | 'online'; // 环境:开发/线上
56
- };
57
- }
58
- ```
59
-
60
- `DataChangeEventInput.type` 只定义 `INSERT`、`UPDATE`、`DELETE`,不包含 `UPSERT`。
61
-
62
- ## 代码示例
63
-
64
- 根据触发器创建后确定的任务名字(应用内唯一),编写并绑定到对应的方法上。使用模板已有的 `@lark-apaas/fullstack-nestjs-core` 聚合入口导入 `Automation` / `BindTrigger`,不要求项目再感知底层 trigger 包。具体代码示例如下:
65
-
66
- ```typescript
67
- // 文件名:demo.automation.ts
68
- import { Logger } from '@nestjs/common';
69
- // 必须导入
70
- import { Automation, BindTrigger } from '@lark-apaas/fullstack-nestjs-core';
71
-
72
- /**
73
- * 示例自动化任务服务
74
- * 使用 @Automation 装饰器标记 class,@BindTrigger 绑定具体 function
75
- */
76
- @Automation()
77
- export class DemoAutomationTasksService {
78
- // 务必使用 logger 打印日志
79
- private readonly logger = new Logger(DemoAutomationTasksService.name);
80
-
81
- @BindTrigger('triggerName1')
82
- // 任务对应具体的实现
83
- async helloWorld() {
84
- this.logger.log('执行 Hello World 任务');
85
- // do logic
86
- // return logic result or throw Error
87
- }
88
-
89
- @BindTrigger('triggerName2')
90
- // 任务对应具体的实现
91
- async sendNotification() {
92
- this.logger.log('开始发送通知');
93
- // do logic
94
- this.logger.log('通知发送完成');
95
- // return logic result or throw Error
96
- }
97
-
98
- @BindTrigger('recordChangeTrigger')
99
- // 记录变更任务(triggerType = 'record_change')
100
- async handleDataChange(event: TaskHandlerArgs) {
101
- // 1. 校验并解析 input
102
- const input = event.content.input;
103
- if (typeof input !== 'string') {
104
- this.logger.error('input 类型错误');
105
- return;
106
- }
107
-
108
- let eventData: DataChangeEventInput;
109
- try {
110
- eventData = JSON.parse(input);
111
- } catch (error) {
112
- this.logger.error('JSON 解析失败', error);
113
- return;
114
- }
115
-
116
- // 2. 根据操作类型获取数据:INSERT/UPDATE 用 after,DELETE 用 before
117
- const record = eventData.after || eventData.before;
118
- if (!record) {
119
- this.logger.error('记录数据为空');
120
- return;
121
- }
122
- this.logger.log(`处理 ${eventData.type} 事件,记录ID: ${record.id}`);
123
- // do logic
124
- // return logic result or throw Error
125
- }
126
-
127
- @BindTrigger('webhookTrigger')
128
- // Webhook 任务(triggerType = 'webhook')
129
- async handleWebhook(event: TaskHandlerArgs) {
130
- // 1. 校验并解析 input
131
- const input = event.content.input;
132
- if (typeof input !== 'string') {
133
- this.logger.error('input 类型错误');
134
- return;
135
- }
136
-
137
- let webhookEvent: WebhookEvent;
138
- try {
139
- webhookEvent = JSON.parse(input);
140
- } catch (error) {
141
- this.logger.error('JSON 解析失败', error);
142
- return;
143
- }
144
-
145
- // 2. 获取请求信息
146
- const { method, path, query, headers, body } = webhookEvent;
147
- this.logger.log(`处理 Webhook 请求:${method} ${path}`);
148
-
149
- // 3. 按需解析 body(body 本身也是 JSON 字符串)
150
- // const bodyData = JSON.parse(body);
151
-
152
- // do logic
153
- // return logic result or throw Error
154
- }
155
- }
156
- ```
157
-
158
- ## 技术实现路径参考
159
-
160
- 以下是一些常见需求的推荐实现路径,帮助你在平台能力限制下找到合理的技术方案。
161
-
162
- ### 场景一:用户需要管理页面控制定时任务的启停
163
-
164
- 平台侧不支持通过 API 动态启停触发器。推荐方案:**平台定时触发器始终保持开启,在任务执行时查询数据库中的开关状态,决定是否真正执行业务逻辑。**
165
-
166
- 实现步骤:
167
-
168
- 1. 在数据库中建一张配置表(或复用已有配置表),存储任务开关状态
169
- 2. 前端管理页面提供开关操作,修改数据库中的状态
170
- 3. 定时任务触发时,先查询开关状态,关闭则直接跳过
171
-
172
- ```typescript
173
- @Automation()
174
- export class ReportAutomationService {
175
- private readonly logger = new Logger(ReportAutomationService.name);
176
-
177
- constructor(private readonly configService: ConfigService) {}
178
-
179
- @BindTrigger('dailyReportTrigger')
180
- async generateDailyReport() {
181
- // 1. 先查询任务开关状态
182
- const config = await this.configService.getTaskConfig('dailyReport');
183
- if (!config?.enabled) {
184
- this.logger.log('每日报告任务已被管理员关闭,跳过执行');
185
- return;
186
- }
187
-
188
- // 2. 开关开启,执行实际业务逻辑
189
- this.logger.log('开始生成每日报告');
190
- // do logic
191
- }
192
- }
193
- ```
194
-
195
- ### 场景二:定时任务需要将结果通知给特定用户
196
-
197
- 自动化任务执行时无法获取当前用户上下文。推荐方案:**在数据库中预存需要通知的用户 ID,任务执行时从数据库查询目标用户,再调用飞书插件发送通知。**
198
-
199
- ```typescript
200
- @Automation()
201
- export class NotifyAutomationService {
202
- private readonly logger = new Logger(NotifyAutomationService.name);
203
-
204
- constructor(
205
- private readonly userConfigService: UserConfigService,
206
- ) {}
207
-
208
- @BindTrigger('weeklyDigestTrigger')
209
- async sendWeeklyDigest() {
210
- // 1. 从数据库查询订阅了周报的用户列表
211
- const subscribers = await this.userConfigService.getSubscribers('weeklyDigest');
212
- if (!subscribers.length) {
213
- this.logger.log('无订阅用户,跳过发送');
214
- return;
215
- }
216
-
217
- // 2. 生成周报内容
218
- const reportContent = await this.buildWeeklyReport();
219
-
220
- // 3. 逐个发送通知
221
- for (const user of subscribers) {
222
- // 调用插件发送飞书消息
223
- this.logger.log(`已发送周报给用户: ${user.userId}`);
224
- }
225
- }
226
- }
227
- ```
228
-
229
- ### 场景三:记录变更触发器需要做防抖/去重
230
-
231
- 高频数据变更场景下,同一条记录可能短时间内触发多次。推荐方案:**利用数据库记录最近一次处理时间戳,对比 event 时间戳进行去重。**
232
-
233
- ```typescript
234
- @BindTrigger('orderStatusChange')
235
- async handleOrderChange(event: TaskHandlerArgs) {
236
- const eventData: DataChangeEventInput = JSON.parse(event.content.input);
237
- const record = eventData.after;
238
- if (!record) return;
239
-
240
- const orderId = record.id as string;
241
-
242
- // 查询上次处理时间,跳过短时间内的重复事件
243
- const lastProcessed = await this.orderService.getLastProcessedTime(orderId);
244
- if (lastProcessed && eventData.timestamp - lastProcessed < 5000) {
245
- this.logger.log(`订单 ${orderId} 短时间内重复触发,跳过`);
246
- return;
247
- }
248
-
249
- // 记录本次处理时间并执行业务逻辑
250
- await this.orderService.updateLastProcessedTime(orderId, eventData.timestamp);
251
- this.logger.log(`处理订单状态变更: ${orderId}`);
252
- // do logic
253
- }
254
- ```
255
-
256
- ### 场景四:用户需要自定义定时任务的触发时间
257
-
258
- 平台侧的 cron 表达式在触发器创建后无法由用户动态修改。推荐方案:**平台设置一个固定的高频定时器(如每 30 分钟执行一次),在任务执行时从数据库读取用户配置的触发时间,判断当前是否命中再决定是否执行。**
259
-
260
- 实现步骤:
261
-
262
- 1. 平台侧创建一个每 30 分钟执行的 cron 触发器(最小粒度)
263
- 2. 数据库中存储用户配置的期望执行时间(如 `"09:00"`、`"每周一 14:00"` 等)
264
- 3. 前端管理页面提供时间配置界面,用户可随时修改
265
- 4. 每次触发时,读取配置并判断当前时间是否匹配,不匹配则跳过
266
-
267
- ```typescript
268
- @Automation()
269
- export class ScheduleAutomationService {
270
- private readonly logger = new Logger(ScheduleAutomationService.name);
271
-
272
- constructor(private readonly scheduleConfigService: ScheduleConfigService) {}
273
-
274
- @BindTrigger('fixedIntervalTrigger') // 平台侧固定每 30 分钟触发
275
- async checkAndExecuteTasks() {
276
- // 1. 查询所有用户配置的定时任务
277
- const tasks = await this.scheduleConfigService.getAllActiveTasks();
278
-
279
- const now = new Date();
280
- for (const task of tasks) {
281
- // 2. 判断当前时间是否命中用户配置的执行时间
282
- if (!this.isTimeMatched(now, task.scheduledTime)) {
283
- continue;
284
- }
285
-
286
- // 3. 命中则执行对应业务逻辑
287
- this.logger.log(`执行任务: ${task.name}, 配置时间: ${task.scheduledTime}`);
288
- await this.executeTask(task);
289
- }
290
- }
291
-
292
- private isTimeMatched(now: Date, scheduledTime: string): boolean {
293
- // 将当前时间取到半小时精度,与用户配置的时间比较
294
- // 例如 scheduledTime = "09:00",当前 08:46~09:15 之间的某次触发即命中
295
- const [hour, minute] = scheduledTime.split(':').map(Number);
296
- return now.getHours() === hour && now.getMinutes() === minute;
297
- }
298
- }
299
- ```
300
-
301
- > 注意:由于平台最小调度间隔为 30 分钟,用户可配置的时间精度也应限制为 30 分钟的整数倍(如 `09:00`、`09:30`),前端做好校验提示。