跳转至

投稿:CSDN / InfoQ - 技术文章(开发者视角)

发布平台:CSDN(https://blog.csdn.net/ )或 InfoQ(https://www.infoq.cn/ ) 建议专栏:架构 / 云计算 / 物联网 / 安全 标签:Cloudflare Workers / Hono / React 19 / D1 / TypeScript / 应急管理


用 Cloudflare 全栈开发一个合规的应急演练评估平台:技术方案与踩坑实录

适合读者:全栈开发者、云原生架构师、对 Cloudflare Workers 生态感兴趣的工程师 技术栈:Cloudflare Workers + Hono + React 19 + D1 + R2 + Queues + Tailwind 背景:GB/T 46792-2025 将在 2026-07-01 实施,演练评估数字化是一个真实的市场需求


一、为什么选 Cloudflare Workers?

开发 ToB SaaS,最头疼的不是写代码,而是部署、运维、成本

传统方式:买服务器 → 搭 CI/CD → 配置 Nginx → 买域名 → 配 SSL → 买数据库 → 配备份

用 Cloudflare Workers:

Workers      # 运行时(全球 300+ 节点)
Hono         # 轻量 API 框架(类 Express,但快 10 倍)
D1           # SQLite 边缘数据库(全球复制,低延迟)
R2           # 对象存储(存放报告 PDF / 演练视频证据)
Queues       # 消息队列(报告生成、邮件通知异步处理)
Tailwind     # 前端样式
React 19     # 前端框架

成本:每月 10 美元以内的免费额度可以支持中小规模 SaaS 初期运行。


二、系统架构

2.1 分层设计

用户浏览器 / 微信
  React 19 SPA(静态资源,托管在 Cloudflare Pages)
       ↓ HTTPS
  Cloudflare Workers(API 网关,JWT 鉴权)
  ┌────┴────┐
  │         │
 Hono API  Queues(异步任务)
  │         │
 D1 DB     R2(文件存储)

2.2 数据模型(D1 Schema,SQLite)

核心表:

-- 租户/机构
CREATE TABLE tenants (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  plan TEXT DEFAULT 'free',   -- free | pro | enterprise
  created_at INTEGER
);

-- 演练评估(核心实体)
CREATE TABLE drills (
  id TEXT PRIMARY KEY,
  tenant_id TEXT REFERENCES tenants(id),
  name TEXT,
  type TEXT,                  -- fire | flood | earthquake | ...
  date INTEGER,
  procedure_mode TEXT,        -- complete | simplified
  status TEXT DEFAULT 'draft',-- draft | in_progress | completed
  created_at INTEGER
);

-- 评估报告(最终产出)
CREATE TABLE reports (
  id TEXT PRIMARY KEY,
  drill_id TEXT REFERENCES drills(id),
  data TEXT,                  -- JSON,存储 10 模块报告内容
  pdf_url TEXT,
  signed_at INTEGER,
  created_at INTEGER
);

-- 整改跟踪
CREATE TABLE corrections (
  id TEXT PRIMARY KEY,
  report_id TEXT REFERENCES reports(id),
  issue TEXT,
  responsible TEXT,
  deadline INTEGER,
  status TEXT DEFAULT 'open', -- open | in_progress | closed
  closed_at INTEGER
);

-- 匿名会话(简化评估入口)
CREATE TABLE anonymous_sessions (
  token TEXT PRIMARY KEY,
  data TEXT,
  expires_at INTEGER
);

2.3 API 设计(Hono)

// app.ts
const app = new Hono();

// 匿名快速评估(简化程序)
app.post('/api/v1/anonymous/session', createAnonymousSession);
app.post('/api/v1/anonymous/report', generateAnonymousReport);

// 完整评估(JWT 鉴权)
app.post('/api/v1/drill', authMiddleware, createDrill);
app.get('/api/v1/drill/:id', authMiddleware, getDrill);
app.post('/api/v1/drill/:id/report', authMiddleware, generateReport);

// 报告管理
app.get('/api/v1/report/:id', authMiddleware, getReport);
app.get('/api/v1/report/:id/export/pdf', authMiddleware, exportPDF);

// 整改跟踪
app.get('/api/v1/report/:id/corrections', authMiddleware, listCorrections);
app.patch('/api/v1/correction/:id', authMiddleware, updateCorrection);

三、AI-Chat 向导的技术实现

AI-Chat 是本产品的核心交互创新,背后是一套状态机 + LLM 调用 + Schema 校验的设计:

3.1 对话状态机

Session
  ├── stage: "A_scene_selection"   → 选择演练场景
  ├── stage: "B_basic_info"        → 采集基础信息
  ├── stage: "C_quantitative"      → 量化指标采集
  ├── stage: "D_issues_highlights" → 问题与亮点
  └── stage: "E_conclusion"        → 生成结论

3.2 Schema 驱动的字段映射

// evaluation-report-schema.json(单一真相源)
const schema = {
  "report_meta": { "fields": ["org_name", "drill_date", ...] },
  "quantitative_result": { "fields": ["info_report_time", "evacuation_time", ...] }
};

// 对话阶段 → 字段采集顺序
const stageFieldMap = {
  "A_scene_selection": ["drill_type", "drill_scale"],
  "B_basic_info": ["org_name", "drill_date", "drill_purpose"],
  "C_quantitative": ["info_report_time", "evacuation_time", "evacuation_rate"],
  "D_issues_highlights": ["issues[]", "highlights[]"],
  "E_conclusion": ["conclusion", "grade"]
};

3.3 LLM Prompt 设计(系统提示词)

const SYSTEM_PROMPT = `你是应急演练评估助手,基于 GB/T 46792-2025《突发事件应急演练评估指南》引导用户完成评估数据采集。

当前会话阶段:{current_stage}
已采集字段:{collected_fields}
缺失必填字段:{required_fields}

要求:
1. 用自然语言提问,一次只问 1 个问题
2. 收到用户回答后,提取并存储对应字段
3. 不要重复询问已采集的信息
4. 如果用户信息不完整,使用"未提供"并说明对报告的影响
5. 保持对话简洁专业,贴近应急管理从业者的语言习惯
6. 当前采集数据:{partial_data}
7. 输出格式:回答文本 + 提取的 JSON 字段

国标关键条款参考:
- 5.4.2.2 定量指标优先
- 6.5.4 报告要素
- 附录E 报告体例10模块`;

四、踩坑实录

坑 1:D1 的 SQLite 语法限制

D1 是 SQLite,不是 PostgreSQL。JSON 存储要习惯用 json_extract() 而不是 ->> 操作符:

-- ✅ 正确
SELECT json_extract(data, '$.quantitative.info_report_time') FROM reports;

-- ❌ 错误(不是 PostgreSQL)
SELECT data->>'quantitative.info_report_time' FROM reports;

坑 2:Queues 的消息体大小限制

Cloudflare Queues 每条消息最大 256KB。报告 JSON 超过这个限制时,需要先存 R2,再把 R2 URL 发到队列:

// 大报告先存 R2
const objectKey = `reports/${reportId}.json`;
await env.R2_BUCKET.put(objectKey, JSON.stringify(reportData));

// 队列只发 R2 路径
await env.QUEUE.send({
  reportId,
  r2Key: objectKey,
  type: 'generate_pdf'
});

坑 3:JWT Secret 不能写死在代码里

Wrangler 的 .dev.vars 只在本地生效,生产环境必须用 wrangler secret put

# 本地
wrangler dev --local

# 生产(Cloudflare Dashboard 配置)
npx wrangler secret put JWT_SECRET

五、性能数据

指标 数据
P50 响应时间 28ms(Workers 全球边缘节点)
P99 响应时间 142ms
D1 查询延迟 8-15ms(边缘读取)
月均费用(初期) < $10(含 D1 5GB + R2 10GB + Workers 10M 请求)

六、可以参考的开源项目


本文采用 CC BY-SA 4.0 许可。