┌─────────────┐
│ 客户端 │ (Mobile/Web)
└──────┬──────┘
│ REST API (HTTPS)
▼
┌─────────────────────────────────┐
│ Cloudflare Worker (fetch) │
│ ┌──────────────────────────┐ │
│ │ API Router & Controllers │ │
│ └────────┬─────────────────┘ │
│ │ │
│ ┌────────▼─────────────────┐ │
│ │ Service Layer │ │
│ │ - Schedule Calculator │ │
│ │ - Lunar Converter │ │
│ │ - Validator │ │
│ └────────┬─────────────────┘ │
│ │ │
│ ┌────────▼─────────────────┐ │
│ │ D1 Database (SQLite) │ │
│ └──────────────────────────┘ │
└─────────────────────────────────┘
▲
│ Cron Trigger (every minute)
│
┌──────┴──────────────────────────┐
│ Cloudflare Worker (scheduled) │
│ - Query due reminders │
│ - Send Telegram notifications │
│ - Update next_trigger_at │
└──────────────────────────────────┘
系统支持以下 5 种提醒类型:
-
once(一次性提醒)
- 在指定的日期时间触发一次
- 支持 ISO 8601 格式或 Unix 时间戳
-
daily(每日提醒)
- 每天在指定时间触发
- 支持每隔 N 天触发
- 可设置结束日期
-
weekly(每周提醒)
- 每周的指定星期几触发
- 支持多个星期选择(0=周日, 6=周六)
- 支持每隔 N 周触发
- 可设置结束日期
-
monthly(每月提醒)
- 每月指定日期触发(1-31号)
- 支持每隔 N 个月触发
- 自动处理月份日期不存在的情况(如2月30日将使用该月最后一天)
- 可设置结束日期
-
yearly(每年提醒)
- 每年指定月份和日期触发(如每年6月28日)
- 支持每隔 N 年触发
- 自动处理日期不存在的情况(如闰年2月29日在平年将使用2月28日)
- 可设置结束日期
-
lunar(农历提醒)
- 支持农历日期触发(如农历八月十五)
- 支持闰月精确匹配
- 支持一次性农历提醒(
repeat: false) - 支持每年重复农历提醒(
repeat: true,默认)
- 时区处理: 所有时间在数据库中存储为 UTC Unix 秒,客户端可指定 IANA 时区
- 农历算法: 实现简化版农历转公历算法(支持 2000-2100 年),基于预计算农历数据
- 并发控制: 使用乐观锁(version 字段)防止 Cron 重复触发
- 幂等性: 支持 Idempotency-Key 防止重复创建
- 扩展性: schedule_config 使用 JSON 存储,便于扩展新类型
- 农历算法采用预计算数据表(2000-2100),不支持更早/更晚年份
- 闰月处理:默认闰月与正常月份同等对待,可通过
leapMonth: true精确指定 - Cron 最小粒度 1 分钟,精确度 ±30 秒
- 单次 Cron 处理最多 50 条提醒(防止超时)
- Node.js 18+
- npm 或 pnpm
- Cloudflare 账号
- Wrangler CLI:
npm install -g wrangler
# 初始化项目目录
mkdir reminder && cd reminder
# 复制所有源码文件到对应目录
# 目录结构:
# /src
# ├── index.js
# ├── api.router.js
# ├── controllers/
# ├── services/
# └── utils/
# /schema.sql
# /wrangler.toml
# /openapi.yamlnpm init -y
npm install -D wranglerwrangler login# 创建数据库
wrangler d1 create reminder_db
# 输出示例:
# ✅ Successfully created DB 'reminder_db'
#
# [[d1_databases]]
# binding = "DB"
# database_name = "reminder_db"
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# 复制 database_id 到 wrangler.toml重要:将输出的 database_id 填入 wrangler.toml 文件中(最新 cli 工具会自动填写到 wrangler.toml 文件中)。
# 执行 schema.sql (在本地执行)
wrangler d1 execute reminder_db --file=./schema.sql
# 在远程执行
wrangler d1 execute reminder_db --remote --file=./schema.sql
# 验证表创建 (在本地执行)
wrangler d1 execute reminder_db --command="SELECT name FROM sqlite_master WHERE type='table';"
# 在远程执行
wrangler d1 execute reminder_db --remote --command="SELECT name FROM sqlite_master WHERE type='table';"# 部署到 Cloudflare
wrangler deploy
# 输出示例:
# ✅ Deployment complete
# https://reminder.your-subdomain.workers.dev# 设置 Telegram Bot Token
wrangler secret put TELEGRAM_BOT_TOKEN
# 输入您的 Telegram Bot Token (从 @BotFather 获取)
# 可选:设置 Webhook Secret
wrangler secret put WEBHOOK_SECRET
# 输入一个随机字符串# 创建一个测试用户
wrangler d1 execute reminder_db --command="
INSERT OR IGNORE INTO users (user_id, api_token, created_at)
VALUES ('my_user', 'my_secret_token_abc123', unixepoch());
"
# 或者使用更安全的随机 token
wrangler d1 execute reminder_db --command="
INSERT OR IGNORE INTO users (user_id, api_token, created_at)
VALUES ('my_user', '$(openssl rand -hex 32)', unixepoch());
"注意:保存生成的 api_token,它将用于 API 认证。
# 健康检查
curl https://reminder.your-subdomain.workers.dev/health
# 应返回:
# {"success":true,"data":{"status":"healthy","service":"reminder"}}# 创建一个提醒
curl -X POST https://reminder.your-subdomain.workers.dev/api/reminders \
-H "Authorization: Bearer my_secret_token_abc123" \
-H "Content-Type: application/json" \
-d '{
"content": "测试提醒",
"chat_id": "YOUR_TELEGRAM_CHAT_ID",
"schedule_type": "daily",
"schedule_config": {
"time": "09:00"
},
"timezone": "Asia/Shanghai",
"preview": 3
}'- 与您的 Bot 对话,发送任意消息
- 访问:
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates - 在返回的 JSON 中找到
chat.id
# 添加自定义域名
wrangler domains add api.yourdomain.com
# 更新 DNS 记录(按照提示操作)# 查看实时日志
wrangler tail
# 查看 Cron 执行日志
wrangler tail --format=json | grep "Cron triggered"# 导出数据库备份
wrangler d1 export reminder_db --output=backup.sql
# 查询统计信息
wrangler d1 execute reminder_db --command="
SELECT status, COUNT(*) as count
FROM reminders
GROUP BY status;
"
# 清理测试数据
wrangler d1 execute reminder_db --command="
DELETE FROM reminders WHERE user_id = 'test_user';
"# 修改代码后重新部署
wrangler deploy
# Cron 会自动更新,无需额外配置A: 检查 Cron Triggers 配置:
wrangler deployments list
# 确认 cron schedule 已配置A:
- 数据库存储 UTC 时间
- 客户端传入 IANA 时区
- 检查
src/utils/time.js中的时区映射
A: 检查 wrangler.jsonc 中的 d1_databases 的 binding 值是否为 DB
CREATE INDEX idx_next_trigger ON reminders(next_trigger_at, status);
CREATE INDEX idx_user_id ON reminders(user_id);在 src/services/cron.service.js 中调整 batchSize。
在 wrangler.toml 中配置 rate limit binding。
- 定期轮换 API Tokens
- 使用强随机 Token:
openssl rand -hex 32 - 限制 CORS 来源:修改
src/index.js中的 CORS 配置 - 监控异常访问:使用 Cloudflare Analytics
- 备份数据库:定期执行
wrangler d1 export
- D1 数据库已创建并初始化
- Secrets 已设置(TELEGRAM_BOT_TOKEN)
- 测试用户已创建
- Cron Triggers 配置正确(每分钟)
- API 健康检查通过
- Telegram 通知测试成功
- 自定义域名已配置(可选)
- 监控和告警已设置
- 数据库备份策略已制定
# 本地开发(需要配置本地 D1)
wrangler dev
# 本地测试 Cron
wrangler dev --test-scheduled部署完成! 🎉
下一步:查看客户端示例进行集成。
# examples/curl-examples.sh
#!/bin/bash
# 配置
API_URL="https://reminder.your-subdomain.workers.dev/api"
API_TOKEN="your_api_token_here"
# 1. 创建每日提醒
echo "=== Creating daily reminder ==="
curl -X POST "$API_URL/reminders" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-daily-$(date +%s)" \
-d '{
"content": "每天喝8杯水",
"chat_id": "123456789",
"schedule_type": "daily",
"schedule_config": {
"time": "09:00",
"every_n_days": 1
},
"timezone": "Asia/Shanghai",
"preview": 3
}' | jq
# 2. 创建每周提醒
echo -e "\n=== Creating weekly reminder ==="
curl -X POST "$API_URL/reminders" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "周末给妈妈打电话",
"chat_id": "123456789",
"schedule_type": "weekly",
"schedule_config": {
"time": "20:00",
"weekdays": [0, 6]
},
"timezone": "Asia/Shanghai",
"preview": 4
}' | jq
# 3. 创建每月提醒(每月15号)
echo -e "\n=== Creating monthly reminder ==="
curl -X POST "$API_URL/reminders" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "每月还信用卡",
"chat_id": "123456789",
"schedule_type": "monthly",
"schedule_config": {
"day_of_month": 15,
"time": "10:00",
"every_n_months": 1
},
"timezone": "Asia/Shanghai",
"preview": 3
}' | jq
# 4. 创建每年提醒(每年6月28日)
echo -e "\n=== Creating yearly reminder ==="
curl -X POST "$API_URL/reminders" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "生日快乐!",
"chat_id": "123456789",
"schedule_type": "yearly",
"schedule_config": {
"month": 6,
"day": 28,
"time": "09:00",
"every_n_years": 1
},
"timezone": "Asia/Shanghai",
"preview": 3
}' | jq
# 5. 创建农历一次性提醒(今年中秋节)
echo -e "\n=== Creating lunar once reminder ==="
curl -X POST "$API_URL/reminders" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "今年中秋节快乐!",
"chat_id": "123456789",
"schedule_type": "lunar",
"schedule_config": {
"lunarMonth": 8,
"lunarDay": 15,
"time": "10:00",
"repeat": false
},
"timezone": "Asia/Shanghai",
"preview": 1
}' | jq
# 6. 创建农历重复提醒(每年中秋节)
echo -e "\n=== Creating lunar repeat reminder ==="
curl -X POST "$API_URL/reminders" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "中秋节快乐!记得买月饼",
"chat_id": "123456789",
"schedule_type": "lunar",
"schedule_config": {
"lunarMonth": 8,
"lunarDay": 15,
"time": "10:00",
"repeat": true
},
"timezone": "Asia/Shanghai",
"preview": 2
}' | jq
# 7. 获取提醒列表
echo -e "\n=== Listing reminders ==="
curl -X GET "$API_URL/reminders?status=active&limit=10&page=1" \
-H "Authorization: Bearer $API_TOKEN" | jq
# 8. 获取单个提醒
echo -e "\n=== Getting reminder details ==="
REMINDER_ID=1
curl -X GET "$API_URL/reminders/$REMINDER_ID" \
-H "Authorization: Bearer $API_TOKEN" | jq
# 9. 更新提醒
echo -e "\n=== Updating reminder ==="
curl -X PUT "$API_URL/reminders/$REMINDER_ID" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "更新后的提醒内容",
"status": "active",
"preview": 2
}' | jq
# 10. 测试触发
echo -e "\n=== Testing trigger ==="
curl -X POST "$API_URL/reminders/$REMINDER_ID/test-trigger" \
-H "Authorization: Bearer $API_TOKEN" | jq
# 11. 删除提醒
echo -e "\n=== Deleting reminder ==="
curl -X DELETE "$API_URL/reminders/$REMINDER_ID" \
-H "Authorization: Bearer $API_TOKEN" | jq
# 12. 批量创建
echo -e "\n=== Bulk creating reminders ==="
curl -X POST "$API_URL/reminders/bulk" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reminders": [
{
"content": "提醒1",
"schedule_type": "daily",
"schedule_config": {"time": "08:00"},
"timezone": "Asia/Shanghai"
},
{
"content": "提醒2",
"schedule_type": "daily",
"schedule_config": {"time": "12:00"},
"timezone": "Asia/Shanghai"
}
]
}' | jq
echo -e "\n=== Done ==="// examples/javascript-client.js
// JavaScript/TypeScript 客户端示例
class ReminderClient {
constructor(apiUrl, apiToken) {
this.apiUrl = apiUrl;
this.apiToken = apiToken;
}
async request(method, path, body = null, headers = {}) {
const url = `${this.apiUrl}${path}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json',
...headers,
},
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error?.message || 'Request failed');
}
return data.data;
}
// 创建提醒
async createReminder(reminder, idempotencyKey = null) {
const headers = idempotencyKey
? { 'Idempotency-Key': idempotencyKey }
: {};
return await this.request('POST', '/reminders', reminder, headers);
}
// 获取列表
async listReminders(params = {}) {
const query = new URLSearchParams(params).toString();
return await this.request('GET', `/reminders?${query}`);
}
// 获取单个
async getReminder(id) {
return await this.request('GET', `/reminders/${id}`);
}
// 更新
async updateReminder(id, updates) {
return await this.request('PUT', `/reminders/${id}`, updates);
}
// 删除
async deleteReminder(id) {
return await this.request('DELETE', `/reminders/${id}`);
}
// 测试触发
async testTrigger(id) {
return await this.request('POST', `/reminders/${id}/test-trigger`);
}
// 批量创建
async bulkCreate(reminders) {
return await this.request('POST', '/reminders/bulk', { reminders });
}
}
// 使用示例
(async () => {
const client = new ReminderClient(
'https://reminder.your-subdomain.workers.dev/api',
'your_api_token'
);
try {
// 创建每日提醒
const dailyReminder = await client.createReminder({
content: '每天9点喝水',
chat_id: '123456789',
schedule_type: 'daily',
schedule_config: {
time: '09:00',
every_n_days: 1,
},
timezone: 'Asia/Shanghai',
preview: 3,
});
console.log('Created reminder:', dailyReminder);
// 获取列表
const reminders = await client.listReminders({
status: 'active',
limit: 20,
page: 1,
});
console.log('Reminders:', reminders);
// 测试触发
const testResult = await client.testTrigger(dailyReminder.id);
console.log('Test result:', testResult);
} catch (error) {
console.error('Error:', error.message);
}
})();// examples/flutter_client.dart
// Flutter/Dart 客户端示例
import 'dart:convert';
import 'package:http/http.dart' as http;
class ReminderClient {
final String apiUrl;
final String apiToken;
ReminderClient(this.apiUrl, this.apiToken);
Future<Map<String, dynamic>> _request(
String method,
String path, {
Map<String, dynamic>? body,
Map<String, String>? headers,
}) async {
final url = Uri.parse('$apiUrl$path');
final requestHeaders = {
'Authorization': 'Bearer $apiToken',
'Content-Type': 'application/json',
...?headers,
};
http.Response response;
switch (method) {
case 'GET':
response = await http.get(url, headers: requestHeaders);
break;
case 'POST':
response = await http.post(
url,
headers: requestHeaders,
body: body != null ? json.encode(body) : null,
);
break;
case 'PUT':
response = await http.put(
url,
headers: requestHeaders,
body: body != null ? json.encode(body) : null,
);
break;
case 'DELETE':
response = await http.delete(url, headers: requestHeaders);
break;
default:
throw Exception('Unsupported method: $method');
}
final data = json.decode(response.body);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception(data['error']?['message'] ?? 'Request failed');
}
return data['data'];
}
// 创建提醒
Future<Map<String, dynamic>> createReminder(
Map<String, dynamic> reminder, {
String? idempotencyKey,
}) async {
final headers = idempotencyKey != null
? {'Idempotency-Key': idempotencyKey}
: null;
return await _request('POST', '/reminders',
body: reminder, headers: headers);
}
// 获取列表
Future<Map<String, dynamic>> listReminders({
String status = 'active',
int limit = 20,
int page = 1,
}) async {
final query = 'status=$status&limit=$limit&page=$page';
return await _request('GET', '/reminders?$query');
}
// 获取单个
Future<Map<String, dynamic>> getReminder(int id) async {
return await _request('GET', '/reminders/$id');
}
// 更新
Future<Map<String, dynamic>> updateReminder(
int id,
Map<String, dynamic> updates,
) async {
return await _request('PUT', '/reminders/$id', body: updates);
}
// 删除
Future<Map<String, dynamic>> deleteReminder(int id) async {
return await _request('DELETE', '/reminders/$id');
}
// 测试触发
Future<Map<String, dynamic>> testTrigger(int id) async {
return await _request('POST', '/reminders/$id/test-trigger');
}
}
// 使用示例
void main() async {
final client = ReminderClient(
'https://reminder.your-subdomain.workers.dev/api',
'your_api_token',
);
try {
// 创建每周提醒
final weeklyReminder = await client.createReminder({
'content': '周末给家人打电话',
'chat_id': '123456789',
'schedule_type': 'weekly',
'schedule_config': {
'time': '20:00',
'weekdays': [0, 6], // Sunday and Saturday
},
'timezone': 'Asia/Shanghai',
'preview': 4,
});
print('Created reminder: ${weeklyReminder['id']}');
// 获取列表
final reminders = await client.listReminders(
status: 'active',
limit: 10,
);
print('Total reminders: ${reminders['meta']['total']}');
// 测试触发
final testResult = await client.testTrigger(weeklyReminder['id']);
print('Test result: ${testResult['test_result']['success']}');
} catch (e) {
print('Error: $e');
}
}// examples/react-native-client.ts
// React Native/TypeScript 客户端示例
import axios, { AxiosInstance } from 'axios';
interface ReminderInput {
content: string;
chat_id?: string;
schedule_type: 'once' | 'daily' | 'weekly' | 'lunar';
schedule_config: any;
timezone?: string;
preview?: number;
}
interface ReminderResponse {
id: number;
content: string;
schedule_type: string;
next_trigger_at: number;
next_trigger_at_iso: string;
status: string;
preview?: Array<{ unix: number; iso: string }>;
}
class ReminderAPI {
private client: AxiosInstance;
constructor(apiUrl: string, apiToken: string) {
this.client = axios.create({
baseURL: apiUrl,
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
});
}
async createReminder(
reminder: ReminderInput,
idempotencyKey?: string
): Promise<ReminderResponse> {
const headers = idempotencyKey
? { 'Idempotency-Key': idempotencyKey }
: {};
const response = await this.client.post('/reminders', reminder, { headers });
return response.data.data;
}
async listReminders(params: {
status?: string;
limit?: number;
page?: number;
} = {}): Promise<{ items: ReminderResponse[]; meta: any }> {
const response = await this.client.get('/reminders', { params });
return response.data.data;
}
async getReminder(id: number): Promise<ReminderResponse> {
const response = await this.client.get(`/reminders/${id}`);
return response.data.data;
}
async updateReminder(
id: number,
updates: Partial<ReminderInput>
): Promise<ReminderResponse> {
const response = await this.client.put(`/reminders/${id}`, updates);
return response.data.data;
}
async deleteReminder(id: number): Promise<void> {
await this.client.delete(`/reminders/${id}`);
}
async testTrigger(id: number): Promise<any> {
const response = await this.client.post(`/reminders/${id}/test-trigger`);
return response.data.data;
}
}
// React Native 组件示例
import React, { useState, useEffect } from 'react';
import { View, Text, Button, FlatList } from 'react-native';
const ReminderScreen: React.FC = () => {
const [reminders, setReminders] = useState<ReminderResponse[]>([]);
const [loading, setLoading] = useState(false);
const api = new ReminderAPI(
'https://reminder.your-subdomain.workers.dev/api',
'your_api_token'
);
useEffect(() => {
loadReminders();
}, []);
const loadReminders = async () => {
setLoading(true);
try {
const data = await api.listReminders({ status: 'active' });
setReminders(data.items);
} catch (error) {
console.error('Failed to load reminders:', error);
} finally {
setLoading(false);
}
};
const createDailyReminder = async () => {
try {
await api.createReminder({
content: '每天提醒喝水',
schedule_type: 'daily',
schedule_config: {
time: '09:00',
},
timezone: 'Asia/Shanghai',
preview: 3,
});
loadReminders();
} catch (error) {
console.error('Failed to create reminder:', error);
}
};
return (
<View>
<Button title="创建提醒" onPress={createDailyReminder} />
<FlatList
data={reminders}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View>
<Text>{item.content}</Text>
<Text>下次提醒: {item.next_trigger_at_iso}</Text>
</View>
)}
refreshing={loading}
onRefresh={loadReminders}
/>
</View>
);
};
export default ReminderScreen;选择:使用预计算数据表(2000-2100年)
原因:
- 完整的农历计算算法复杂且占用空间
- 预计算数据表仅约 100 个整数(<1KB)
- 覆盖绝大多数实际使用场景
边际情况:
- 2000年之前或2100年之后的农历日期将返回 null
- 闰月处理:默认将闰月视为独立月份,需要
leapMonth: true精确匹配
选择:简化的时区偏移映射
原因:
- Cloudflare Workers 环境对完整时区库支持有限
- 硬编码常用时区减少依赖
限制:
- 不支持夏令时自动调整
- 仅支持预定义的常用时区
- 生产环境建议扩展映射表或使用轻量时区库
选择:乐观锁(version 字段)
原因:
- D1/SQLite 不支持 SELECT FOR UPDATE
- 版本号更新具有原子性
风险:
- 高并发下可能出现版本冲突
- 通过限制 Cron 批处理大小(50条)降低风险
选择:每分钟触发
原因:
- Cloudflare Cron Triggers 最小粒度为 1 分钟
- 平衡精确度和资源消耗
精确度:±30秒
本实现提供了一个完整、可部署、生产可用的 Cloudflare Workers 定时备忘录系统,包括:
- ✅ 完整的 REST API(CRUD + 预览 + 测试触发)
- ✅ 支持 4 种调度类型(once/daily/weekly/lunar)
- ✅ Cron 自动触发和 Telegram 通知
- ✅ 乐观锁并发控制
- ✅ 幂等性支持
- ✅ 完整的 OpenAPI 规范
- ✅ 单元测试覆盖
- ✅ 多语言客户端示例
- ✅ 详细的部署文档
所有代码均可直接在 Cloudflare Workers 环境中部署运行。