growork 1.0.0

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.
@@ -0,0 +1,1236 @@
1
+ # iOS 横幅推送与站内信方案
2
+
3
+ ## 整体架构
4
+
5
+ ### 系统分层架构
6
+
7
+
8
+ ```plaintext
9
+ flowchart TB
10
+ subgraph Client[客户端]
11
+ iOS[iOS APP]
12
+ Admin[管理后台]
13
+ end
14
+
15
+ subgraph Backend[bloomo-core 后端服务]
16
+ API[REST API]
17
+ Scheduler[定时调度器]
18
+ Service[站内信服务]
19
+ Cache[缓存服务]
20
+ Async[异步推送]
21
+ end
22
+
23
+ subgraph Data[数据存储]
24
+ DB[(PostgreSQL)]
25
+ Redis[(Redis)]
26
+ end
27
+
28
+ FCM[FCM 推送服务]
29
+
30
+ Client --> API
31
+ API --> Service
32
+ Scheduler --> Service
33
+ Service --> Cache
34
+ Cache --> Redis
35
+ Service --> DB
36
+ Service --> Async
37
+ Async --> FCM
38
+ FCM -.->|推送通知| iOS
39
+ ```
40
+
41
+ ### 消息发送时序图
42
+
43
+
44
+
45
+ ```plaintext
46
+ sequenceDiagram
47
+ autonumber
48
+ participant Admin as 管理后台
49
+ participant API as Admin API
50
+ participant TemplateService as 模板服务
51
+ participant TaskService as 任务服务
52
+ participant DB as PostgreSQL
53
+
54
+ Note over Admin, DB: 1. 创建消息模板
55
+ Admin->>API: POST /admin/inbox/templates
56
+ API->>TemplateService: createTemplate(dto)
57
+ TemplateService->>DB: INSERT inbox_message_templates
58
+ DB-->>TemplateService: template_id
59
+ TemplateService-->>API: 模板创建成功
60
+ API-->>Admin: 返回 template_id
61
+
62
+ Note over Admin, DB: 2. 创建发送任务
63
+ Admin->>API: POST /admin/inbox/tasks
64
+ API->>TaskService: createTask(dto)
65
+ TaskService->>DB: INSERT inbox_message_tasks
66
+ DB-->>TaskService: task_id
67
+ TaskService-->>API: 任务创建成功
68
+ API-->>Admin: 返回 task_id
69
+ ```
70
+
71
+ ### 定时任务执行时序图
72
+
73
+
74
+
75
+ ```plaintext
76
+ sequenceDiagram
77
+ autonumber
78
+ participant Scheduler as 定时调度器
79
+ participant Redis as Redis缓存
80
+ participant DB as 数据库
81
+ participant Service as 站内信服务
82
+ participant FCM as FCM推送
83
+
84
+ Note over Scheduler, FCM: 每小时整点执行
85
+
86
+ Scheduler->>DB: 查询活跃任务
87
+ DB-->>Scheduler: 任务列表
88
+
89
+ Scheduler->>Redis: 获取时区列表 + 已执行时区
90
+ Redis-->>Scheduler: 时区数据
91
+
92
+ loop 遍历待执行时区
93
+ Scheduler->>Scheduler: 窗口匹配(过去60分钟)
94
+
95
+ alt 匹配成功且未执行
96
+ Scheduler->>Redis: 获取该时区用户ID
97
+ Redis-->>Scheduler: 用户ID列表
98
+
99
+ Scheduler->>DB: 分批查询用户详情
100
+ DB-->>Scheduler: 用户列表
101
+
102
+ Scheduler->>Service: 发送消息
103
+ Service->>DB: 保存站内信记录
104
+
105
+ opt 启用推送
106
+ Service->>FCM: 异步批量推送
107
+ end
108
+
109
+ Scheduler->>Redis: 标记已执行
110
+ Scheduler->>DB: 保存执行记录
111
+ end
112
+ end
113
+ ```
114
+
115
+ ### 用户查看站内信时序图
116
+
117
+
118
+ ```plaintext
119
+ sequenceDiagram
120
+ autonumber
121
+ participant iOS as iOS APP
122
+ participant API as User API
123
+ participant Service as 站内信服务
124
+ participant DB as PostgreSQL
125
+
126
+ Note over iOS, DB: 获取消息列表(含 isRead、isExpired 字段)
127
+ iOS->>API: GET /inbox/messages
128
+ API->>Service: getMessages(userId, pageable)
129
+ Service->>DB: SELECT(包括过期消息)
130
+ DB-->>Service: Page of Message
131
+ Service-->>API: 返回列表(含 isExpired 标识)
132
+ API-->>iOS: 消息列表
133
+ iOS->>iOS: 前端计算未读数(isRead=false 的数量)
134
+ iOS->>iOS: 过期消息置灰不可点击
135
+
136
+ Note over iOS, DB: 标记已读
137
+ iOS->>API: PUT /inbox/messages/{id}/read
138
+ API->>Service: markAsRead(userId, messageId)
139
+ Service->>DB: UPDATE SET is_read = true
140
+ Service-->>API: success
141
+ API-->>iOS: 200 OK
142
+ ```
143
+
144
+ ### 缓存预热时序图
145
+
146
+
147
+ ```plaintext
148
+ sequenceDiagram
149
+ autonumber
150
+ participant App as 应用启动
151
+ participant CacheWarmer as 缓存预热组件
152
+ participant CacheService as 缓存服务
153
+ participant Redis as Redis
154
+ participant UserRepo as 用户Repository
155
+ participant DB as PostgreSQL
156
+
157
+ Note over App, DB: 应用启动时预热缓存
158
+ App->>CacheWarmer: ApplicationRunner.run()
159
+ CacheWarmer->>CacheService: warmUpCache()
160
+
161
+ CacheService->>Redis: 清除旧缓存 DEL inbox:tz:users:*
162
+ CacheService->>Redis: 清除旧缓存 DEL inbox:timezones
163
+
164
+ CacheService->>UserRepo: 查询所有用户ID和时区
165
+ UserRepo->>DB: SELECT id, timezone FROM users
166
+ DB-->>UserRepo: List of [userId, timezone]
167
+ UserRepo-->>CacheService: List of [userId, timezone]
168
+
169
+ loop 按时区分组
170
+ CacheService->>Redis: SADD inbox:tz:users:{timezone} userId
171
+ CacheService->>Redis: SADD inbox:timezones timezone
172
+ end
173
+
174
+ CacheService-->>CacheWarmer: 预热完成
175
+ CacheWarmer-->>App: 启动继续
176
+
177
+ Note over App, DB: 每日凌晨3点刷新缓存
178
+ CacheWarmer->>CacheService: warmUpCache()
179
+ Note right of CacheService: 重复上述流程
180
+ ```
181
+
182
+ ### FCM 推送流程时序图
183
+
184
+
185
+ ```plaintext
186
+ sequenceDiagram
187
+ autonumber
188
+ participant iOS as iOS APP
189
+ participant FirebaseSDK as Firebase SDK
190
+ participant API as User API
191
+ participant UserService as 用户服务
192
+ participant CacheService as 缓存服务
193
+ participant Redis as Redis
194
+ participant DB as PostgreSQL
195
+
196
+ Note over iOS, DB: APP 启动时通过登录接口上报 FCM Token 和时区
197
+ iOS->>FirebaseSDK: 获取 FCM Token
198
+ FirebaseSDK-->>iOS: fcm_token
199
+ iOS->>API: POST /user/login 包含 fcmToken、timezone 字段
200
+ API->>UserService: login 或 updateClientInfo
201
+ UserService->>DB: UPDATE users SET fcm_token, timezone
202
+ DB-->>UserService: success
203
+
204
+ alt 时区变更
205
+ UserService->>CacheService: moveUserTimezone(userId, old, new)
206
+ CacheService->>Redis: SREM inbox:tz:users:{old} userId
207
+ CacheService->>Redis: SADD inbox:tz:users:{new} userId
208
+ CacheService->>Redis: SADD inbox:timezones {new}
209
+ end
210
+
211
+ UserService-->>API: success
212
+ API-->>iOS: 200 OK
213
+ ```
214
+
215
+ ## 数据库设计
216
+
217
+ ### 站内信模板表 `inbox_message_templates`
218
+
219
+ 存储站内信模板配置,支持灵活的内容定义。
220
+
221
+ | 字段 | 类型 | 说明 |
222
+ | --- | --- | --- |
223
+ | id | BIGSERIAL | 主键 |
224
+ | template_code | VARCHAR(64) | 模板编码(唯一标识) |
225
+ | title | VARCHAR(256) | 标题(支持变量占位符) |
226
+ | content | TEXT | 内容(支持富文本/Markdown) |
227
+ | content_type | VARCHAR(20) | 内容类型:TEXT / RICH_TEXT / TEMPLATE |
228
+ | image_url | VARCHAR(512) | 可选图片 URL |
229
+ | action_type | VARCHAR(32) | 点击动作类型:NONE / DEEP_LINK / WEB_URL |
230
+ | action_url | VARCHAR(512) | 动作链接 |
231
+
232
+ | variables | JSONB | 模板变量定义(如 `{"username": "string"}`) |
233
+
234
+ | status | SMALLINT | 状态:0-禁用 1-启用 |
235
+
236
+ | create_time | TIMESTAMPTZ | 创建时间 |
237
+
238
+ | update_time | TIMESTAMPTZ | 更新时间 |
239
+
240
+ ### 站内信任务表 `inbox_message_tasks`
241
+
242
+ 存储定时/重复通知任务配置。支持固定模板或从 Remote Config 随机选择模板。
243
+
244
+ | 字段 | 类型 | 说明 |
245
+ | --- | --- | --- |
246
+ | id | BIGSERIAL | 主键 |
247
+ | task_name | VARCHAR(128) | 任务名称 |
248
+ | template_id | BIGINT | 关联模板 ID(必填,定义消息格式和变量占位符) |
249
+
250
+ | variable_values | JSONB | 固定模板变量值(如 `{"title": "Hello"}`),与 remote_config_key 二选一 |
251
+
252
+ | remote_config_key | VARCHAR(128) | Remote Config 中的 key(存储文案列表,每天随机选一组),与 variable_values 二选一 |
253
+
254
+ | target_type | VARCHAR(20) | 目标类型:ALL / USER_GROUP / INDIVIDUAL |
255
+
256
+ | target_user_ids | TEXT[] | 目标用户 ID 列表(INDIVIDUAL 时使用) |
257
+
258
+ | target_conditions | JSONB | 目标用户条件(USER_GROUP 时使用,如 `{"is_vip": true}`) |
259
+
260
+ | inbox_enabled | BOOLEAN | 是否存储到站内信列表(默认 true) |
261
+
262
+ | push_enabled | BOOLEAN | 是否发送推送(默认 false) |
263
+
264
+ | silent_push | BOOLEAN | 是否静默推送(默认 false,仅发送 data 用于 APP 内弹窗,pushEnabled=true 时生效) |
265
+
266
+ | push_title | VARCHAR(128) | 推送标题(可选,默认用模板标题) |
267
+
268
+ | push_body | VARCHAR(256) | 推送内容摘要(可选,默认截取模板内容) |
269
+
270
+ | schedule_type | VARCHAR(20) | 调度类型:IMMEDIATE / SCHEDULED / CRON |
271
+
272
+ | scheduled_date | DATE | 定时发送日期(SCHEDULED 类型,用户当地日期) |
273
+
274
+ | scheduled_local_time | TIME | 定时发送时间(SCHEDULED 类型,用户当地时间) |
275
+
276
+ | cron_expression | VARCHAR(64) | Cron 表达式(CRON 类型,按用户时区执行) |
277
+
278
+ | start_date | DATE | 任务生效开始日期 |
279
+
280
+ | end_date | DATE | 任务生效结束日期(可选) |
281
+
282
+ | valid_hours | INT | 消息有效时长(小时,从发送时算起,为空则永不过期) |
283
+
284
+ | priority | SMALLINT | 消息优先级:0=普通, 1=重要, 2=紧急(默认 0) |
285
+
286
+ | is_pinned | BOOLEAN | 是否置顶(默认 false) |
287
+
288
+ | status | SMALLINT | 状态:0-禁用 1-启用 2-已完成 |
289
+
290
+ | create_time | TIMESTAMPTZ | 创建时间 |
291
+
292
+ | update_time | TIMESTAMPTZ | 更新时间 |
293
+
294
+ ### 任务执行记录表 `inbox_task_executions`
295
+
296
+ 追踪每个任务按时区的执行状态,用于支持按用户时区分批发送。
297
+
298
+ | 字段 | 类型 | 说明 |
299
+ | --- | --- | --- |
300
+ | id | BIGSERIAL | 主键 |
301
+ | task_id | BIGINT | 任务 ID |
302
+ | timezone | VARCHAR(64) | 时区(如 Asia/Shanghai) |
303
+ | execute_date | DATE | 执行日期 |
304
+ | execute_time | TIMESTAMPTZ | 实际执行时间 |
305
+ | user_count | INT | 发送用户数 |
306
+ | push_count | INT | 推送数量 |
307
+ | status | VARCHAR(20) | 状态:SUCCESS / FAILED / PARTIAL |
308
+ | error_message | TEXT | 错误信息(失败时) |
309
+ | create_time | TIMESTAMPTZ | 创建时间 |
310
+
311
+ **索引**:
312
+
313
+ - `UNIQUE (task_id, timezone, execute_date)` 防止同一时区同一天重复执行
314
+ - `(task_id, execute_date)` 用于查询当天已选中的模板(确保同一天所有用户收到相同消息)
315
+ ### 用户站内信表 `user_inbox_messages`
316
+
317
+ 存储发送给每个用户的站内信记录。
318
+
319
+ | 字段 | 类型 | 说明 |
320
+ | --- | --- | --- |
321
+ | id | BIGSERIAL | 主键 |
322
+ | user_id | BIGINT | 用户 ID |
323
+ | task_id | BIGINT | 来源任务 ID(可选) |
324
+ | title | VARCHAR(256) | 标题 |
325
+ | content | TEXT | 内容 |
326
+ | content_type | VARCHAR(20) | 内容类型 |
327
+ | image_url | VARCHAR(512) | 图片 URL |
328
+ | action_type | VARCHAR(32) | 点击动作类型 |
329
+ | action_url | VARCHAR(512) | 动作链接 |
330
+ | priority | SMALLINT | 消息优先级:0=普通, 1=重要, 2=紧急(从任务继承) |
331
+ | is_pinned | BOOLEAN | 是否置顶(从任务继承) |
332
+ | is_read | BOOLEAN | 是否已读 |
333
+ | read_time | TIMESTAMPTZ | 阅读时间 |
334
+ | push_sent | BOOLEAN | 是否已发送推送 |
335
+ | push_sent_time | TIMESTAMPTZ | 推送发送时间 |
336
+ | expire_time | TIMESTAMPTZ | 过期时间(发送时根据任务配置计算) |
337
+ | is_deleted | BOOLEAN | 是否已删除(软删除,默认 false) |
338
+ | delete_time | TIMESTAMPTZ | 删除时间 |
339
+ | create_time | TIMESTAMPTZ | 创建时间 |
340
+
341
+ ### User 表新增字段
342
+
343
+ 在现有 `users` 表中新增 FCM Token 相关字段(单设备模式):
344
+
345
+ | 字段 | 类型 | 说明 |
346
+ | --- | --- | --- |
347
+ | fcm_token | VARCHAR(512) | FCM 推送 Token |
348
+ | fcm_token_update_time | TIMESTAMPTZ | Token 更新时间 |
349
+
350
+ ## Redis Key 清单
351
+
352
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_HssoHe]
353
+
354
+ ### Key 用途说明
355
+
356
+ ### 1. inbox:timezones
357
+
358
+ - 用途:定时任务遍历所有时区,判断是否需要发送消息
359
+ - 维护:应用启动预热 + 每日凌晨 3 点刷新
360
+ ### 2. inbox:tz:users:{timezone}
361
+
362
+ - 用途:快速获取某时区的所有用户,避免数据库查询
363
+ - 维护:应用启动预热 + 用户登录实时更新
364
+ ### 3. inbox:exec:{taskId}:{date}
365
+
366
+ - 用途:防止同一任务在同一时区同一天重复执行
367
+ - 维护:任务执行后写入; 48h TTL 自动清理
368
+ ### 4. inbox:content:{taskId}:{date}
369
+
370
+ - 用途:Remote Config 随机文案场景,确保同一天所有用户看到相同文案
371
+ - 维护:首次选中时写入; 48h TTL 自动清理
372
+
373
+ ## Firebase Remote Config 随机文案配置
374
+
375
+ 当任务配置了 `remote_config_key` 时,系统会从 Firebase Remote Config 读取**文案变量列表**,每天随机选择一组变量值填充到模板中发送(当天所有用户收到相同消息)。
376
+
377
+ ### 变量来源选择逻辑
378
+
379
+ ```plaintext
380
+ if (task.remoteConfigKey != null) {
381
+ // 从 Remote Config 每天随机选一组变量
382
+ variables = selectRandomFromRemoteConfig(remoteConfigKey);
383
+ } else {
384
+ // 使用固定变量
385
+ variables = task.variableValues;
386
+ }
387
+ ```
388
+
389
+ ### 设计思路
390
+
391
+ ```plaintext
392
+ ┌─────────────────────────────────────────────────────────────┐
393
+ │ 数据库模板 (template_id) │
394
+ │ ┌─────────────────────────────────────────────────────┐ │
395
+ │ │ title: "{{title}}" │ │
396
+ │ │ content: "{{content}}" │ │
397
+ │ │ actionUrl: "bloomo://reading/daily" │ │
398
+ │ └─────────────────────────────────────────────────────┘ │
399
+ └─────────────────────────────────────────────────────────────┘
400
+
401
+ ▼ 填充变量
402
+ ┌─────────────────────────────────────────────────────────────┐
403
+ │ Remote Config 文案列表 (每日随机选一组) │
404
+ │ ┌─────────────────────────────────────────────────────┐ │
405
+ │ │ [0] title: "Trust Your Intuition" │ │
406
+ │ │ content: "Today, trust your inner voice..." │ │
407
+ │ ├─────────────────────────────────────────────────────┤ │
408
+ │ │ [1] title: "Embrace New Beginnings" │ │
409
+ │ │ content: "A fresh start awaits you..." │ │
410
+ │ ├─────────────────────────────────────────────────────┤ │
411
+ │ │ [2] title: "Love is All Around" │ │
412
+ │ │ content: "The universe is sending love..." │ │
413
+ │ └─────────────────────────────────────────────────────┘ │
414
+ └─────────────────────────────────────────────────────────────┘
415
+ ```
416
+
417
+ ### Remote Config 配置格式
418
+
419
+ 在 Firebase Console 的 Remote Config 中配置一个 JSON 参数,Key 与任务的 `remote_config_key` 对应:
420
+
421
+ **Key**: `daily_inspiration_contents`
422
+
423
+ **Value**:
424
+
425
+ ```json
426
+ [
427
+ {
428
+ "title": "Trust Your Intuition",
429
+ "content": "Today, the cards suggest you trust your inner voice. Let your intuition guide your decisions.",
430
+ "pushTitle": "Daily Inspiration",
431
+ "pushBody": "Trust your intuition today"
432
+ },
433
+ {
434
+ "title": "Embrace New Beginnings",
435
+ "content": "A fresh start awaits you. Be open to new opportunities and let go of what no longer serves you.",
436
+ "pushTitle": "Daily Inspiration",
437
+ "pushBody": "Embrace new beginnings"
438
+ },
439
+ {
440
+ "title": "Love is All Around",
441
+ "content": "The universe is sending you love today. Open your heart to receive and give love freely.",
442
+ "pushTitle": "Daily Inspiration",
443
+ "pushBody": "Love is all around you"
444
+ }
445
+ ]
446
+ ```
447
+
448
+ ### 配置字段说明
449
+
450
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_Nfp8ec]
451
+
452
+ ## 推送类型完整对照表
453
+
454
+ ### 后端配置 vs 推送效果
455
+
456
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_6MomZZ]
457
+
458
+ > *客户端需自行实现前台弹窗逻辑*
459
+
460
+ ### APP 状态 vs 用户体验
461
+
462
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_DJ6gjN]
463
+
464
+ ### 使用场景推荐
465
+
466
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_xWFS2o]
467
+
468
+ ### FCM 消息结构对比
469
+
470
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_q0LTHT]
471
+
472
+
473
+
474
+ ## 核心服务设计
475
+
476
+ ### FCM 推送 `FcmPushClient`
477
+
478
+ #### 横幅推送(Notification + Data)
479
+
480
+ 使用 Firebase Admin SDK 的 `FirebaseMessaging` 类:
481
+
482
+ #### 静默推送(仅 Data,用于 APP 内弹窗)
483
+
484
+ 静默推送不携带 `notification`,只发送 `data`。客户端在 APP 前台时自行展示弹窗,APP 不在前台时忽略。
485
+
486
+ #### 推送模式对比
487
+
488
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_XAH36h]
489
+
490
+ ### 站内信服务 `InboxMessageService`
491
+
492
+ ### 通知任务调度器 `InboxMessageTaskScheduler`
493
+
494
+ **按用户时区发送**:SCHEDULED 和 CRON 类型的任务都按用户时区分批发送,确保用户在本地时间收到通知。
495
+
496
+ ## API 设计
497
+
498
+ ### 管理端 API(Open API 认证)
499
+
500
+ | 方法 | 路径 | 说明 |
501
+ | --- | --- | --- |
502
+ | POST | /admin/inbox/templates | 创建消息模板 |
503
+ | PUT | /admin/inbox/templates/{id} | 更新消息模板 |
504
+ | GET | /admin/inbox/templates | 查询模板列表 |
505
+ | POST | /admin/inbox/tasks | 创建发送任务 |
506
+ | PUT | /admin/inbox/tasks/{id} | 更新任务 |
507
+ | GET | /admin/inbox/tasks | 查询任务列表 |
508
+ | POST | /admin/inbox/send | 即时发送消息(创建 IMMEDIATE 类型任务) |
509
+
510
+ ### 用户端 API(Firebase 认证)
511
+
512
+ | 方法 | 路径 | 说明 |
513
+ | --- | --- | --- |
514
+ | GET | /inbox/messages | 获取站内信列表(分页) |
515
+ | PUT | /inbox/messages/{id}/read | 标记已读 |
516
+ | PUT | /inbox/messages/read-all | 全部标记已读 |
517
+ | DELETE | /inbox/messages/{id} | 删除消息 |
518
+ | - | (通过现有登录接口上报 fcmToken,与 appInstanceId 一致) | - |
519
+
520
+ ## 用户体验
521
+
522
+ 勿扰时段只对 **Banner 推送** 生效,**静默推送(站内弹窗)** 保持正常发送。
523
+
524
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_TRiRk4]
525
+
526
+ **场景示例**:
527
+
528
+ ```plaintext
529
+ 凌晨 2:00 发送 Banner 推送:
530
+ ├── 站内信 → 存入数据库 ✅
531
+ ├── 推送 → 只更新角标(不弹系统通知)
532
+ └── 用户第二天看到 App 图标有红点 → 打开查看
533
+
534
+ 凌晨 2:00 发送静默推送(站内弹窗):
535
+ ├── 站内信 → 存入数据库 ✅
536
+ ├── 推送 → 正常发送静默推送
537
+ └── 用户在 App 内 → 显示站内弹窗
538
+ 用户在后台/锁屏 → 不显示(符合预期)
539
+ ```
540
+
541
+ ## iOS 客户端配置说明
542
+
543
+ ### 推送配置链路
544
+
545
+
546
+ ```plaintext
547
+ flowchart LR
548
+ Backend[后端 FCM API] --> FCM[Firebase Cloud Messaging]
549
+ FCM --> APNs[Apple Push Notification]
550
+ APNs --> iOS[iOS 设备]
551
+ ```
552
+
553
+ 要使推送正常工作,需要 **Firebase 配置** 和 **iOS 客户端配置** 两部分。
554
+
555
+ ### Firebase Console 配置(一次性)
556
+
557
+ | 步骤 | 操作 |
558
+ | --- | --- |
559
+ | 1 | 登录 Firebase Console,进入项目设置 |
560
+ | 2 | 点击 Cloud Messaging 标签页 |
561
+
562
+ | 3 | 在 Apple app configuration 中上传 **APNs Auth Key**(推荐)或 APNs 证书 |
563
+
564
+ APNs Auth Key 获取方式:
565
+
566
+ 1. 登录 Apple Developer 后台
567
+ 1. 进入 Certificates, Identifiers & Profiles → Keys
568
+ 1. 创建新 Key,勾选 Apple Push Notifications service (APNs)
569
+ 1. 下载 .p8 文件,上传到 Firebase
570
+ ### iOS 客户端配置
571
+
572
+ #### 2.1 处理静默推送(APP 内弹窗)
573
+
574
+ 静默推送只发送 data,不带 notification。客户端需要自行判断并展示弹窗。
575
+
576
+ #### 2.2 静默推送与横幅推送的区别
577
+
578
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_QzIaaW]
579
+
580
+ ### 配置清单
581
+
582
+ | 项目 | 负责方 | 状态 |
583
+ | --- | --- | --- |
584
+ | 上传 APNs Key 到 Firebase | iOS 开发者 | 待完成 |
585
+ | Xcode 启用 Push Notifications | iOS 开发者 | 待完成 |
586
+ | 集成 Firebase/Messaging SDK | iOS 开发者 | 待完成 |
587
+ | 获取并上报 FCM Token | iOS 开发者 | 待完成 |
588
+ | 实现站内信列表 UI | iOS 开发者 | 待完成 |
589
+ | 后端 FCM 推送实现 | 后端开发者 | 待完成 |
590
+
591
+ ## 操作配置说明
592
+
593
+ ### 模板配置(Template)
594
+
595
+ 模板定义消息的格式和内容结构,是创建任务的前提。
596
+
597
+ #### 基本信息
598
+
599
+ **templateCode**(必填)
600
+
601
+ - 模板的唯一标识符,用于区分不同模板
602
+ - 建议使用 snake_case 命名,如 `daily_reminder`、`christmas_2026`
603
+ - 创建后不可修改
604
+ **title**(必填)
605
+
606
+ - 消息标题,支持变量占位符 `{{变量名}}`
607
+ - 示例:`Hello {{username}}` 或固定文本 `Daily Tarot Reading`
608
+ **content**(必填)
609
+
610
+ - 消息正文内容
611
+ - 支持变量占位符 `{{变量名}}`
612
+ - 根据 contentType 可以是纯文本或富文本
613
+ **contentType**(必填)
614
+
615
+ - `TEXT` - 纯文本,适用于简短通知
616
+ - `RICH_TEXT` - 富文本,支持 HTML 格式,适用于图文消息
617
+ #### 交互配置
618
+
619
+ **actionType**
620
+
621
+ - `NONE` - 无点击动作(默认)
622
+ - `DEEP_LINK` - 深度链接,点击后跳转到 APP 内指定页面
623
+ - `WEB_URL` - 网页链接,点击后打开外部浏览器
624
+ **actionUrl**
625
+
626
+ - actionType 为 `DEEP_LINK` 时:填写 APP 深度链接,如 `bloomo://reading/daily`
627
+ - actionType 为 `WEB_URL` 时:填写完整网址,如 `https://bloomo.com/promo`
628
+ **imageUrl**
629
+
630
+ - 可选的消息图片 URL
631
+ - 适用于节日祝福、活动通知等需要配图的场景
632
+ #### 变量定义
633
+
634
+ **variables**
635
+
636
+ - JSON 对象,定义模板中使用的变量及其类型
637
+ - 用于文档说明和校验,非强制
638
+ - 示例:`{"username": "string", "date": "string"}`
639
+ ### 任务配置(Task)
640
+
641
+ 任务定义消息的发送策略,包括发送对象、发送时间、推送方式等。
642
+
643
+ #### 基本信息
644
+
645
+ **taskName**(必填)
646
+
647
+ - 任务名称,用于后台管理识别
648
+ - 建议包含日期或用途,如 `Daily Reading Reminder`、`Christmas 2026 Greeting`
649
+ **templateId**(必填)
650
+
651
+ - 关联的模板 ID
652
+ - 必须是已存在且启用的模板
653
+ #### 变量值配置
654
+
655
+ 以下两个字段**二选一**:
656
+
657
+ **variableValues**
658
+
659
+ - 固定的变量值,JSON 对象
660
+ - 用于填充模板中的 `{{变量名}}` 占位符
661
+ - 示例:`{"title": "Today's Reading", "content": "Your daily inspiration..."}`
662
+ **remoteConfigKey**
663
+
664
+ - Firebase Remote Config 中的参数 Key
665
+ - 系统每天从该 Key 对应的文案列表中随机选择一组变量值
666
+ - 适用于每日灵感、随机祝福等需要变化的场景
667
+ - 示例:`daily_inspiration_contents`
668
+ #### 目标配置
669
+
670
+ **targetType**(必填)
671
+
672
+ - `ALL` - 发送给所有用户
673
+ - `USER_GROUP` - 发送给符合条件的用户群组
674
+ - `INDIVIDUAL` - 发送给指定的用户列表
675
+ **targetUserIds**
676
+
677
+ - targetType 为 `INDIVIDUAL` 时必填
678
+ - 用户 UID 数组
679
+ - 示例:`["uid_123", "uid_456"]`
680
+ **targetConditions**
681
+
682
+ - targetType 为 `USER_GROUP` 时使用
683
+ - JSON 对象,定义筛选条件
684
+ - 示例:`{"is_vip": true}` 或 `{"country": "US"}`
685
+ #### 站内信配置
686
+
687
+ **inboxEnabled**
688
+
689
+ - 是否将消息存储到站内信列表
690
+ - 默认 `true`
691
+ - 设为 `false` 时消息只发推送不存储
692
+ #### 推送配置
693
+
694
+ **pushEnabled**
695
+
696
+ - 是否发送 FCM 推送
697
+ - 默认 `false`
698
+ - 设为 `true` 时会向用户设备发送推送通知
699
+ **silentPush**
700
+
701
+ - 是否静默推送(仅在 pushEnabled=true 时生效)
702
+ - 默认 `false`(横幅推送)
703
+ - 设为 `true` 时发送静默推送,用于 APP 内弹窗
704
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_53y29u]
705
+
706
+ **pushTitle**
707
+
708
+ - 推送通知的标题
709
+ - 不填则使用模板 title(变量替换后)
710
+ - 可被 Remote Config 中的 `pushTitle` 字段覆盖
711
+ **pushBody**
712
+
713
+ - 推送通知的内容摘要
714
+ - 不填则截取模板 content 前 100 字符
715
+ - 可被 Remote Config 中的 `pushBody` 字段覆盖
716
+ #### 调度配置
717
+
718
+ **scheduleType**(必填)
719
+
720
+ - `IMMEDIATE` - 立即发送,任务创建后马上执行
721
+ - `SCHEDULED` - 定时一次性发送,在指定日期时间发送
722
+ - `CRON` - 按 Cron 表达式重复发送
723
+ **scheduledDate** + **scheduledLocalTime**
724
+
725
+ - scheduleType 为 `SCHEDULED` 时必填
726
+ - `scheduledDate`:发送日期,格式 `2026-01-28`
727
+ - `scheduledLocalTime`:发送时间,格式 `09:00:00`
728
+ - 按用户时区执行:东京用户在东京时间收到,纽约用户在纽约时间收到
729
+ **cronExpression**
730
+
731
+ - scheduleType 为 `CRON` 时必填
732
+ - 标准 6 位 Cron 表达式:`秒 分 时 日 月 周`
733
+ - 按用户时区执行
734
+ 常用表达式:
735
+
736
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_ZsXfIg]
737
+
738
+ **startDate**
739
+
740
+ - 任务生效开始日期
741
+ - CRON 类型任务在此日期之前不会执行
742
+ **endDate**
743
+
744
+ - 任务生效结束日期(可选)
745
+ - 不填则永久有效
746
+ - CRON 类型任务在此日期之后不再执行
747
+ #### 消息有效期
748
+
749
+ **validHours**
750
+
751
+ - 消息有效时长(小时),从发送时刻算起
752
+ - 不填则消息永不过期
753
+ - 示例:`24` 表示 24 小时后消息过期,用户无法查看
754
+
755
+ **priority**
756
+
757
+ - 消息优先级,控制消息在列表中的排序位置
758
+ - 取值范围:
759
+ - `0`:普通(默认)
760
+ - `1`:重要
761
+ - `2`:紧急
762
+ - 优先级高的消息排在前面
763
+ - 不填则默认为 `0`
764
+ **isPinned**
765
+
766
+ - 是否将消息置顶显示
767
+ - 取值:`true` / `false`(默认)
768
+ - 置顶消息始终显示在列表最前面,多条置顶消息按优先级和时间排序
769
+ - 适用于重要公告、紧急通知等需要用户优先看到的场景
770
+ **排序规则**
771
+
772
+ 用户站内信列表的排序规则为:
773
+
774
+ 1. 置顶消息(`isPinned=true`)优先
775
+ 1. 同等置顶状态下,按优先级(`priority`)降序
776
+ 1. 同等优先级下,按创建时间(`createTime`)降序
777
+ ```plaintext
778
+ 置顶紧急 > 置顶重要 > 置顶普通 > 非置顶紧急 > 非置顶重要 > 非置顶普通
779
+ ```
780
+
781
+ ### 配置组合示例
782
+
783
+ #### 每日定时推送(横幅通知)
784
+
785
+ ```json
786
+ {
787
+ "taskName": "Daily Reading Reminder",
788
+ "templateId": 1,
789
+ "targetType": "ALL",
790
+ "inboxEnabled": true,
791
+ "pushEnabled": true,
792
+ "silentPush": false,
793
+ "scheduleType": "CRON",
794
+ "cronExpression": "0 0 9 * * ?",
795
+ "startDate": "2026-01-28",
796
+ "validHours": 24,
797
+ "priority": 0,
798
+ "isPinned": false
799
+ }
800
+ ```
801
+
802
+ #### 紧急公告(置顶 + 高优先级)
803
+
804
+ ```json
805
+ {
806
+ "taskName": "Urgent Announcement",
807
+ "templateId": 3,
808
+ "targetType": "ALL",
809
+ "inboxEnabled": true,
810
+ "pushEnabled": true,
811
+ "scheduleType": "IMMEDIATE",
812
+ "validHours": 48,
813
+ "priority": 2,
814
+ "isPinned": true
815
+ }
816
+ ```
817
+
818
+ #### APP 内实时弹窗(静默推送)
819
+
820
+ ```json
821
+ {
822
+ "taskName": "Reading Complete Alert",
823
+ "templateId": 2,
824
+ "targetType": "INDIVIDUAL",
825
+ "targetUserIds": ["user_123"],
826
+ "inboxEnabled": true,
827
+ "pushEnabled": true,
828
+ "silentPush": true,
829
+ "scheduleType": "IMMEDIATE"
830
+ }
831
+ ```
832
+
833
+ #### 仅站内信(无推送)
834
+
835
+ ```json
836
+ {
837
+ "taskName": "System Announcement",
838
+ "templateId": 3,
839
+ "targetType": "ALL",
840
+ "inboxEnabled": true,
841
+ "pushEnabled": false,
842
+ "scheduleType": "IMMEDIATE"
843
+ }
844
+ ```
845
+
846
+ #### 仅推送(不存站内信)
847
+
848
+ ```json
849
+ {
850
+ "taskName": "Flash Sale",
851
+ "templateId": 4,
852
+ "targetType": "ALL",
853
+ "inboxEnabled": false,
854
+ "pushEnabled": true,
855
+ "silentPush": false,
856
+ "scheduleType": "IMMEDIATE"
857
+ }
858
+ ```
859
+
860
+ ## 使用指南
861
+
862
+ ### 场景一:发送即时通知(如紧急公告)
863
+
864
+ **步骤 1:创建消息模板**
865
+
866
+ ```http
867
+ POST /admin/inbox/templates
868
+ Content-Type: application/json
869
+
870
+ {
871
+ "templateCode": "urgent_notice",
872
+ "title": "Important Notice",
873
+ "content": "Dear user, we will perform system maintenance on {{date}}. Please save your work in advance.",
874
+ "contentType": "TEXT",
875
+ "actionType": "NONE",
876
+ "variables": {
877
+ "date": "string"
878
+ }
879
+ }
880
+ ```
881
+
882
+ **响应:**
883
+
884
+ ```json
885
+ {
886
+ "code": 200,
887
+ "data": {
888
+ "id": 1,
889
+ "templateCode": "urgent_notice"
890
+ }
891
+ }
892
+ ```
893
+
894
+ **步骤 2:创建即时发送任务**
895
+
896
+ ```http
897
+ POST /admin/inbox/tasks
898
+ Content-Type: application/json
899
+
900
+ {
901
+ "taskName": "System Maintenance Notice 2026-01-28",
902
+ "templateId": 1,
903
+ "variableValues": {
904
+ "date": "January 28, 2026 10:00 PM UTC"
905
+ },
906
+ "targetType": "ALL",
907
+ "inboxEnabled": true,
908
+ "pushEnabled": true,
909
+ "pushTitle": "System Maintenance",
910
+ "pushBody": "We will perform maintenance tonight",
911
+ "scheduleType": "IMMEDIATE",
912
+ "validHours": 24,
913
+ "priority": 2,
914
+ "isPinned": true
915
+ }
916
+ ```
917
+
918
+ **响应:**
919
+
920
+ ```json
921
+ {
922
+ "code": 200,
923
+ "data": {
924
+ "id": 1,
925
+ "taskName": "System Maintenance Notice 2026-01-28",
926
+ "status": 1
927
+ }
928
+ }
929
+ ```
930
+
931
+ 系统会立即开始发送消息给所有用户。
932
+
933
+ ### 场景二:配置每日提醒(如签到提醒)
934
+
935
+ **步骤 1:创建模板**
936
+
937
+ ```http
938
+ POST /admin/inbox/templates
939
+ Content-Type: application/json
940
+
941
+ {
942
+ "templateCode": "daily_reminder",
943
+ "title": "Daily Tarot Reading",
944
+ "content": "Start your day with a tarot reading! See what the cards have in store for you today.",
945
+ "contentType": "TEXT",
946
+ "actionType": "DEEP_LINK",
947
+ "actionUrl": "bloomo://reading/daily"
948
+ }
949
+ ```
950
+
951
+ **步骤 2:创建每日定时任务**
952
+
953
+ ```http
954
+ POST /admin/inbox/tasks
955
+ Content-Type: application/json
956
+
957
+ {
958
+ "taskName": "Daily Reading Reminder",
959
+ "templateId": 2,
960
+ "targetType": "ALL",
961
+ "pushEnabled": true,
962
+ "scheduleType": "CRON",
963
+ "cronExpression": "0 0 9 * * ?",
964
+ "startDate": "2026-01-28",
965
+ "validHours": 24
966
+ }
967
+ ```
968
+
969
+ 说明:
970
+
971
+ - `cronExpression`: `0 0 9 * * ?` 表示每天 9:00
972
+ - 系统会按用户时区发送,东京用户在东京时间 9:00 收到,纽约用户在纽约时间 9:00 收到
973
+ - `validHours`: 24 小时后消息过期
974
+ ### 场景三:配置节日通知(如圣诞祝福)
975
+
976
+ **步骤 1:创建节日模板**
977
+
978
+ ```http
979
+ POST /admin/inbox/templates
980
+ Content-Type: application/json
981
+
982
+ {
983
+ "templateCode": "christmas_2026",
984
+ "title": "Merry Christmas!",
985
+ "content": "Wishing you a magical Christmas filled with love and joy! Here's a special holiday reading just for you.",
986
+ "contentType": "RICH_TEXT",
987
+ "imageUrl": "https://cdn.bloomo.com/images/christmas-2026.png",
988
+ "actionType": "DEEP_LINK",
989
+ "actionUrl": "bloomo://reading/holiday/christmas"
990
+ }
991
+ ```
992
+
993
+ **步骤 2:创建每年定时任务**
994
+
995
+ ```http
996
+ POST /admin/inbox/tasks
997
+ Content-Type: application/json
998
+
999
+ {
1000
+ "taskName": "Christmas Blessing",
1001
+ "templateId": 3,
1002
+ "targetType": "ALL",
1003
+ "pushEnabled": true,
1004
+ "scheduleType": "CRON",
1005
+ "cronExpression": "0 0 9 25 12 ?",
1006
+ "startDate": "2026-12-25"
1007
+ }
1008
+ ```
1009
+
1010
+ 说明:
1011
+
1012
+ - `cronExpression`: `0 0 9 25 12 ?` 表示每年 12 月 25 日 9:00
1013
+ - 任务会每年自动触发
1014
+ ### 场景四:纯 Push 消息(不存储站内信)
1015
+
1016
+ 适用于短时提醒、促销弹窗等不需要保存历史的场景:
1017
+
1018
+ ```http
1019
+ POST /admin/inbox/tasks
1020
+ Content-Type: application/json
1021
+
1022
+ {
1023
+ "taskName": "Flash Sale Reminder",
1024
+ "templateId": 10,
1025
+ "targetType": "ALL",
1026
+ "inboxEnabled": false,
1027
+ "pushEnabled": true,
1028
+ "pushTitle": "Flash Sale!",
1029
+ "pushBody": "50% off for the next 2 hours!",
1030
+ "scheduleType": "IMMEDIATE"
1031
+ }
1032
+ ```
1033
+
1034
+ 说明:
1035
+
1036
+ - `inboxEnabled: false` - 不存储到站内信列表
1037
+ - `pushEnabled: true` - 只发送横幅推送
1038
+ - 用户收到推送后点击进入 APP,但站内信列表中没有这条消息
1039
+ ### 场景四-B:APP 内弹窗通知(静默推送)
1040
+
1041
+ 适用于用户正在使用 APP 时的实时提醒,如新消息到达、状态变更等:
1042
+
1043
+ ```http
1044
+ POST /admin/inbox/tasks
1045
+ Content-Type: application/json
1046
+
1047
+ {
1048
+ "taskName": "New Reading Available",
1049
+ "templateId": 11,
1050
+ "targetType": "INDIVIDUAL",
1051
+ "targetUserIds": ["user_123"],
1052
+ "inboxEnabled": true,
1053
+ "pushEnabled": true,
1054
+ "silentPush": true,
1055
+ "pushTitle": "Your reading is ready!",
1056
+ "pushBody": "Tap to view your personalized tarot reading",
1057
+ "scheduleType": "IMMEDIATE"
1058
+ }
1059
+ ```
1060
+
1061
+ 说明:
1062
+
1063
+ - `pushEnabled: true` + `silentPush: true` - 发送静默推送
1064
+ - 静默推送只发送 data,不显示系统通知栏
1065
+ - 客户端在 APP 前台时自行展示弹窗
1066
+ - APP 不在前台时忽略(不会打扰用户)
1067
+ **推送模式对比:**
1068
+
1069
+ [嵌入表格: PjaBsB4aAh4jPZtVSl2lraKJgBg_uHbqDO]
1070
+
1071
+ ### 场景五:针对特定用户发送消息
1072
+
1073
+ **发送给 VIP 用户:**
1074
+
1075
+ ```http
1076
+ POST /admin/inbox/tasks
1077
+ Content-Type: application/json
1078
+
1079
+ {
1080
+ "taskName": "VIP Exclusive Offer",
1081
+ "templateId": 4,
1082
+ "targetType": "USER_GROUP",
1083
+ "targetConditions": {
1084
+ "is_vip": true
1085
+ },
1086
+ "inboxEnabled": true,
1087
+ "pushEnabled": true,
1088
+ "scheduleType": "IMMEDIATE"
1089
+ }
1090
+ ```
1091
+
1092
+ **发送给指定用户:**
1093
+
1094
+ ```http
1095
+ POST /admin/inbox/tasks
1096
+ Content-Type: application/json
1097
+
1098
+ {
1099
+ "taskName": "Personal Welcome Message",
1100
+ "templateId": 5,
1101
+ "targetType": "INDIVIDUAL",
1102
+ "targetUserIds": ["user_123", "user_456"],
1103
+ "pushEnabled": false,
1104
+ "scheduleType": "IMMEDIATE"
1105
+ }
1106
+ ```
1107
+
1108
+ ### 场景六:每日随机文案推送(基于 Remote Config)
1109
+
1110
+ 适用于每日灵感、每日运势等需要随机变化的消息场景。使用同一个模板格式,每天从 Remote Config 配置的文案列表中随机选择一组变量值填充,所有用户当天收到相同的消息。
1111
+
1112
+ **步骤 1:创建带变量的模板**
1113
+
1114
+ 创建一个包含变量占位符的模板:
1115
+
1116
+ ```http
1117
+ POST /admin/inbox/templates
1118
+ Content-Type: application/json
1119
+
1120
+ {
1121
+ "templateCode": "daily_inspiration",
1122
+ "title": "{{title}}",
1123
+ "content": "{{content}}",
1124
+ "contentType": "TEXT",
1125
+ "actionType": "DEEP_LINK",
1126
+ "actionUrl": "bloomo://reading/daily",
1127
+ "variables": {
1128
+ "title": "string",
1129
+ "content": "string"
1130
+ }
1131
+ }
1132
+ ```
1133
+
1134
+ **响应:**
1135
+
1136
+ ```json
1137
+ {
1138
+ "code": 200,
1139
+ "data": {
1140
+ "id": 10,
1141
+ "templateCode": "daily_inspiration"
1142
+ }
1143
+ }
1144
+ ```
1145
+
1146
+ **步骤 2:在 Firebase Remote Config 配置文案列表**
1147
+
1148
+ 在 Firebase Console 中创建 Remote Config 参数:
1149
+
1150
+ - **Key**: `daily_inspiration_contents`
1151
+ - **Value**:
1152
+ ```json
1153
+ [
1154
+ {
1155
+ "title": "Trust Your Intuition",
1156
+ "content": "Today, the cards suggest you trust your inner voice. Let your intuition guide your decisions.",
1157
+ "pushTitle": "Daily Inspiration",
1158
+ "pushBody": "Trust your intuition today"
1159
+ },
1160
+ {
1161
+ "title": "Embrace New Beginnings",
1162
+ "content": "A fresh start awaits you. Be open to new opportunities and let go of what no longer serves you.",
1163
+ "pushTitle": "Daily Inspiration",
1164
+ "pushBody": "A fresh start awaits"
1165
+ },
1166
+ {
1167
+ "title": "Love is All Around",
1168
+ "content": "The universe is sending you love today. Open your heart to receive and give love freely.",
1169
+ "pushTitle": "Daily Inspiration",
1170
+ "pushBody": "Love is all around you"
1171
+ }
1172
+ ]
1173
+ ```
1174
+
1175
+ **步骤 3:创建每日随机推送任务**
1176
+
1177
+ ```http
1178
+ POST /admin/inbox/tasks
1179
+ Content-Type: application/json
1180
+
1181
+ {
1182
+ "taskName": "Daily Inspiration - Random",
1183
+ "templateId": 10,
1184
+ "remoteConfigKey": "daily_inspiration_contents",
1185
+ "targetType": "ALL",
1186
+ "inboxEnabled": true,
1187
+ "pushEnabled": true,
1188
+ "scheduleType": "CRON",
1189
+ "cronExpression": "0 0 9 * * ?",
1190
+ "startDate": "2026-01-28",
1191
+ "validHours": 24
1192
+ }
1193
+ ```
1194
+
1195
+ 说明:
1196
+
1197
+ - `templateId: 10` - 指定使用的模板(定义格式和变量占位符)
1198
+ - `remoteConfigKey` - 指向 Firebase Remote Config 中的文案列表 Key(有此字段则从远程随机选文案)
1199
+ - 每天 9:00(用户当地时间)执行时,系统会:
1200
+ 1. 检查 Redis 缓存中当天是否已选择过文案(`inbox:content:{taskId}:{date}`)
1201
+ 1. 如果没有,从 Remote Config 读取文案列表并随机选择一组
1202
+ 1. 将选中的索引缓存到 Redis(48h 过期)
1203
+ 1. 使用选中的变量值填充模板,发送给所有用户
1204
+ 1. 当天所有时区的用户都会收到相同内容的消息
1205
+ ### Cron 表达式快速参考
1206
+
1207
+ | 场景 | Cron 表达式 | 说明 |
1208
+ | --- | --- | --- |
1209
+
1210
+ | 每天 9:00 | `0 0 9 * * ?` | 每日定时提醒 |
1211
+
1212
+ | 每周一 10:00 | `0 0 10 ? * MON` | 周报推送 |
1213
+
1214
+ | 每月 1 号 9:00 | `0 0 9 1 * ?` | 月初提醒 |
1215
+
1216
+ | 每年 1/1 0:00 | `0 0 0 1 1 ?` | 新年祝福 |
1217
+
1218
+ | 每年 12/25 9:00 | `0 0 9 25 12 ?` | 圣诞祝福 |
1219
+
1220
+ | 每周一三五 9:00 | `0 0 9 ? * MON,WED,FRI` | 隔日提醒 |
1221
+
1222
+ | 每 2 小时 | `0 0 */2 * * ?` | 定期检查 |
1223
+
1224
+ **Cron 格式说明:**
1225
+
1226
+ ```plaintext
1227
+ 秒 分 时 日 月 周
1228
+ 0 0 9 * * ?
1229
+ │ │ │ │ │ └── 周(? 表示不指定)
1230
+ │ │ │ │ └───── 月(* 表示每月)
1231
+ │ │ │ └──────── 日(* 表示每日)
1232
+ │ │ └─────────── 时(9 表示 9 点)
1233
+ │ └────────────── 分(0 表示 0 分)
1234
+ └───────────────── 秒(0 表示 0 秒)
1235
+ ```
1236
+