Skip to content

feassh/reminder

Repository files navigation

Reminder System - 部署指南

系统设计说明

1.1 架构概述

┌─────────────┐
│   客户端    │ (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        │
└──────────────────────────────────┘

1.1.1 支持的提醒类型

系统支持以下 5 种提醒类型:

  1. once(一次性提醒)

    • 在指定的日期时间触发一次
    • 支持 ISO 8601 格式或 Unix 时间戳
  2. daily(每日提醒)

    • 每天在指定时间触发
    • 支持每隔 N 天触发
    • 可设置结束日期
  3. weekly(每周提醒)

    • 每周的指定星期几触发
    • 支持多个星期选择(0=周日, 6=周六)
    • 支持每隔 N 周触发
    • 可设置结束日期
  4. monthly(每月提醒)

    • 每月指定日期触发(1-31号)
    • 支持每隔 N 个月触发
    • 自动处理月份日期不存在的情况(如2月30日将使用该月最后一天)
    • 可设置结束日期
  5. yearly(每年提醒)

    • 每年指定月份和日期触发(如每年6月28日)
    • 支持每隔 N 年触发
    • 自动处理日期不存在的情况(如闰年2月29日在平年将使用2月28日)
    • 可设置结束日期
  6. lunar(农历提醒)

    • 支持农历日期触发(如农历八月十五)
    • 支持闰月精确匹配
    • 支持一次性农历提醒repeat: false
    • 支持每年重复农历提醒repeat: true,默认)

1.2 核心设计决策

  • 时区处理: 所有时间在数据库中存储为 UTC Unix 秒,客户端可指定 IANA 时区
  • 农历算法: 实现简化版农历转公历算法(支持 2000-2100 年),基于预计算农历数据
  • 并发控制: 使用乐观锁(version 字段)防止 Cron 重复触发
  • 幂等性: 支持 Idempotency-Key 防止重复创建
  • 扩展性: schedule_config 使用 JSON 存储,便于扩展新类型

1.3 限制与权衡

  • 农历算法采用预计算数据表(2000-2100),不支持更早/更晚年份
  • 闰月处理:默认闰月与正常月份同等对待,可通过 leapMonth: true 精确指定
  • Cron 最小粒度 1 分钟,精确度 ±30 秒
  • 单次 Cron 处理最多 50 条提醒(防止超时)

前置要求

  • Node.js 18+
  • npm 或 pnpm
  • Cloudflare 账号
  • Wrangler CLI: npm install -g wrangler

1. 克隆/准备项目

# 初始化项目目录
mkdir reminder && cd reminder

# 复制所有源码文件到对应目录
# 目录结构:
# /src
#   ├── index.js
#   ├── api.router.js
#   ├── controllers/
#   ├── services/
#   └── utils/
# /schema.sql
# /wrangler.toml
# /openapi.yaml

2. 安装依赖

npm init -y
npm install -D wrangler

3. 登录 Cloudflare

wrangler login

4. 创建 D1 数据库

# 创建数据库
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 文件中)。

5. 初始化数据库 Schema

# 执行 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';"

6. 部署 Worker

# 部署到 Cloudflare
wrangler deploy

# 输出示例:
# ✅ Deployment complete
# https://reminder.your-subdomain.workers.dev

7. 设置 Secrets

# 设置 Telegram Bot Token
wrangler secret put TELEGRAM_BOT_TOKEN
# 输入您的 Telegram Bot Token (从 @BotFather 获取)

# 可选:设置 Webhook Secret
wrangler secret put WEBHOOK_SECRET
# 输入一个随机字符串

8. 创建测试用户

# 创建一个测试用户
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 认证。

9. 验证部署

# 健康检查
curl https://reminder.your-subdomain.workers.dev/health

# 应返回:
# {"success":true,"data":{"status":"healthy","service":"reminder"}}

10. 测试 API

# 创建一个提醒
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
	}'

11. 获取 Telegram Chat ID

  • 与您的 Bot 对话,发送任意消息
  • 访问:https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
  • 在返回的 JSON 中找到 chat.id

12. 配置自定义域名(可选)

# 添加自定义域名
wrangler domains add api.yourdomain.com

# 更新 DNS 记录(按照提示操作)

13. 监控与日志

# 查看实时日志
wrangler tail

# 查看 Cron 执行日志
wrangler tail --format=json | grep "Cron triggered"

14. 数据库维护

# 导出数据库备份
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';
"

15. 更新代码

# 修改代码后重新部署
wrangler deploy

# Cron 会自动更新,无需额外配置

常见问题

Q: Cron 没有触发?

A: 检查 Cron Triggers 配置:

wrangler deployments list
# 确认 cron schedule 已配置

Q: 时区不正确?

A:

  • 数据库存储 UTC 时间
  • 客户端传入 IANA 时区
  • 检查 src/utils/time.js 中的时区映射

Q: workers 无法访问 D1 数据库?

A: 检查 wrangler.jsonc 中的 d1_databasesbinding 值是否为 DB

性能优化

1. 添加数据库索引(已在 schema.sql 中)

CREATE INDEX idx_next_trigger ON reminders(next_trigger_at, status);
CREATE INDEX idx_user_id ON reminders(user_id);

2. 限制 Cron 批处理大小

src/services/cron.service.js 中调整 batchSize

3. 启用 Rate Limiting

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

部署完成! 🎉

下一步:查看客户端示例进行集成。


八、客户端示例

8.1 示例:curl

# 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 ==="

8.2 示例:JavaScript (Fetch)

// 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);
  }
})();

8.3 示例:Flutter/Dart

// 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');
  }
}

8.4 示例:React Native

// 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;

九、系统设计权衡说明

9.1 农历算法简化

选择:使用预计算数据表(2000-2100年)

原因

  • 完整的农历计算算法复杂且占用空间
  • 预计算数据表仅约 100 个整数(<1KB)
  • 覆盖绝大多数实际使用场景

边际情况

  • 2000年之前或2100年之后的农历日期将返回 null
  • 闰月处理:默认将闰月视为独立月份,需要 leapMonth: true 精确匹配

9.2 时区处理

选择:简化的时区偏移映射

原因

  • Cloudflare Workers 环境对完整时区库支持有限
  • 硬编码常用时区减少依赖

限制

  • 不支持夏令时自动调整
  • 仅支持预定义的常用时区
  • 生产环境建议扩展映射表或使用轻量时区库

9.3 并发控制

选择:乐观锁(version 字段)

原因

  • D1/SQLite 不支持 SELECT FOR UPDATE
  • 版本号更新具有原子性

风险

  • 高并发下可能出现版本冲突
  • 通过限制 Cron 批处理大小(50条)降低风险

9.4 Cron 频率

选择:每分钟触发

原因

  • Cloudflare Cron Triggers 最小粒度为 1 分钟
  • 平衡精确度和资源消耗

精确度:±30秒


总结

本实现提供了一个完整、可部署、生产可用的 Cloudflare Workers 定时备忘录系统,包括:

  • ✅ 完整的 REST API(CRUD + 预览 + 测试触发)
  • ✅ 支持 4 种调度类型(once/daily/weekly/lunar)
  • ✅ Cron 自动触发和 Telegram 通知
  • ✅ 乐观锁并发控制
  • ✅ 幂等性支持
  • ✅ 完整的 OpenAPI 规范
  • ✅ 单元测试覆盖
  • ✅ 多语言客户端示例
  • ✅ 详细的部署文档

所有代码均可直接在 Cloudflare Workers 环境中部署运行。

About

A Reminder System Built with Cloudflare Workers and D1.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors