一个简单的 TypeScript + SQLite 服务:外部系统通过 webhook 推送全量工单/合同快照,服务保存每一次完整 payload,记录本次与上一次 active 数据的差异,并提供动态 ICS 订阅地址。
POST /webhook/contracts或POST /webhook/tickets接收外部应用推送的全量工单/合同信息。- SQLite 保存当前 active 数据、每一次完整 webhook 快照、每一次新增/修改/删除日志。
GET /contracts.ics每次请求都实时读取 SQLite 最新 active 数据生成 ICS。GET /sync-logs查看最近同步差异。GET /health健康检查。
- Node.js 22.5 或更高版本。
- npm。
本项目使用 Node 自带的 node:sqlite,不依赖第三方 SQLite 驱动。
npm install
npm run init-dbnpm run serve默认监听:
http://127.0.0.1:3000
管理员订阅地址:
http://127.0.0.1:3000/contracts.ics
生产部署建议至少配置 WEBHOOK_SECRET。如果 ICS 订阅地址不应该公开,也配置 CALENDAR_TOKEN。
每次 webhook 请求表示一次完整快照。也就是说,如果某个旧工单没有出现在本次 payload 中,系统会认为它已被删除,并从 ICS 中移除。
curl -X POST "http://127.0.0.1:3000/webhook/contracts?source=test-system" \
-H "content-type: application/json" \
-H "x-webhook-secret: your-webhook-secret" \
-d '{
"tickets": [
{
"id": "ticket-001",
"userName": "张三",
"expiresAt": "2026-05-01T10:00:00+08:00",
"description": "A 产品 120 件,年度服务合同",
"reminderDaysBefore": 1
},
{
"id": "ticket-002",
"userName": "李四",
"expiresAt": "2026-05-10T18:00:00+08:00",
"description": "B 产品 80 件,季度续约合同",
"reminderDaysBefore": 3
}
]
}'响应会包含本次变化统计和明细:
{
"ok": true,
"snapshotId": "1",
"itemCount": 2,
"added": 2,
"modified": 0,
"deleted": 0
}支持的外层数组字段名:
contractsticketsitemsdata
也可以直接 POST 数组。
为了避免误删,空数组会被拒绝。如果确实需要清空所有 active 工单,建议先增加专门的确认接口,而不是复用普通同步 webhook。
单条数据支持这些字段别名:
- ID:
id,contractId,ticketId,workOrderId - 用户名:
userName,user_name,username,user,customerName - 到期时间:
expiresAt,expires_at,expireTime,expire_time,expiredAt,dueAt,due_at - 简述:
description,summary,brief,remark,content - 提前提醒天数:
reminderDaysBefore,reminder_days_before,remindDaysBefore,alarmDaysBefore
curl "http://127.0.0.1:3000/sync-logs?limit=10"日志中会记录:
added: 本次新增的工单。modified: 本次发生字段变化的工单,包含before和after。deleted: 本次快照缺失、因此被标记删除的旧工单。
contracts: 当前工单/合同数据。只有 status = 'active' 会出现在 ICS 中。
webhook_snapshots: 每一次 webhook 的完整原始 payload。
sync_logs: 每一次 webhook 与上一份 active 数据比较后的新增、修改、删除日志。
可以通过环境变量调整:
DATABASE_PATH=./data/contracts.sqlite \
PORT=3000 \
HOST=127.0.0.1 \
CALENDAR_NAME="合同到期提醒" \
WEBHOOK_SECRET="change-me" \
CALENDAR_TOKEN="calendar-token" \
ADMIN_TOKEN="admin-token" \
CORS_ORIGIN="https://example.com" \
MAX_BODY_BYTES=5242880 \
npm run serve安全相关配置:
WEBHOOK_SECRET: 设置后,webhook 请求必须携带x-webhook-secret或Authorization: Bearer <token>。CALENDAR_TOKEN: 设置后,订阅 ICS 时必须携带?token=<token>、x-calendar-token或 Bearer token。ADMIN_TOKEN: 设置后,访问/sync-logs时必须携带?token=<token>、x-admin-token或 Bearer token。CORS_ORIGIN: 默认不返回 CORS 头。只有确实需要浏览器跨域调用时再配置具体来源。MAX_BODY_BYTES: webhook 请求体最大字节数,默认5242880。
允许其他机器访问时,可以显式监听所有网卡:
npm start -- serve --host 0.0.0.0 --port 8080如果配置了 CALENDAR_TOKEN,订阅地址类似:
http://127.0.0.1:3000/contracts.ics?token=calendar-token
构建镜像:
docker build -t contract-expiry-ics:latest .运行容器:
docker run -d \
--name contract-expiry-ics \
-p 3000:3000 \
-v contract-expiry-ics-data:/app/data \
-e WEBHOOK_SECRET="change-me" \
-e CALENDAR_TOKEN="calendar-token" \
-e ADMIN_TOKEN="admin-token" \
contract-expiry-ics:latest容器默认配置:
- 监听
0.0.0.0:3000。 - SQLite 数据库路径为
/app/data/contracts.sqlite。 /app/data已声明为持久化卷。- 运行阶段使用非 root 用户。
npm run build
npm test
npm run init-db
npm run serve测试用示例数据:
npm run seed-sample静态生成 ICS 仍保留为调试命令,部署订阅时不需要定时生成文件:
npm run generate当前数据库访问集中在 src/contracts.ts。后续如果换成 MySQL、PostgreSQL 或其他 SQL 数据库,主要替换 ContractRepository,HTTP 接口和 ICS 生成逻辑不需要大改。