Skip to content

Backend Guide

one-ea edited this page Apr 25, 2026 · 2 revisions

⚙️ 后端指南

Hono + Cloudflare Workers + Drizzle ORM。


目录结构

server/
└── src/
    ├── index.ts                # 主入口,60+ 路由
    ├── db/
    │   ├── schema.ts           # SQLite/D1/Turso schema
    │   └── schema-pg.ts        # PostgreSQL schema (镜像)
    ├── migrations/             # Drizzle 生成的 SQL 迁移
    │   ├── 0001_init.sql
    │   ├── 0002_add_categories.sql
    │   └── ...
    ├── seeds/                  # 测试数据生成器
    ├── storage/
    │   ├── interfaces.ts       # IDatabase / IObjectStorage
    │   ├── factory.ts          # 按 env 实例化
    │   ├── db/
    │   │   ├── d1.ts
    │   │   ├── turso.ts
    │   │   └── postgres.ts
    │   └── object/
    │       ├── r2.ts
    │       └── s3.ts
    ├── middleware/             # JWT / 限流 / CORS
    ├── lib/                    # Markdown 净化 / Webhook / Reaction
    └── routes/                 # 大型路由模块拆分(如 importers)

路由规范

命名规则

前缀 用途 鉴权 缓存
/api/* (GET) 公开数据读取 边缘缓存
/api/* (POST) 公开写入(评论/反应) 限流
/api/admin/* 后台管理 JWT 必须
/api/auth/* 登录/登出/token 刷新 限流
/cdn/* R2 静态资源 1 年强缓存
/rss.xml RSS 订阅 5 分钟边缘缓存

中间件顺序

app.use('*', corsMiddleware);
app.use('*', securityHeadersMiddleware);
app.use('*', loggerMiddleware);
app.use('/api/admin/*', jwtMiddleware);
app.use('/api/auth/login', rateLimitMiddleware(5, '15m'));

新增 API 端点

公开 GET

app.get('/api/tags/:name', async (c) => {
  const db = await createDatabase(c.env);
  const posts = await db.listPostsByTag(c.req.param('name'));

  c.header('Cache-Control', 'public, max-age=15, s-maxage=60, stale-while-revalidate=30');
  return c.json({ posts });
});

后台写入

app.post('/api/admin/tags', async (c) => {
  // JWT 中间件已自动验证
  const body = await c.req.json();
  const db = await createDatabase(c.env);
  const tag = await db.createTag(body);

  // 触发 Webhook 异步通知
  c.executionCtx.waitUntil(
    triggerWebhook(c.env, 'tag_created', { tag })
  );

  return c.json(tag, 201);
});

Drizzle ORM 三端同步

⚠️ 铁律: 任何 schema 变更必须同时更新 D1/Turso/PostgreSQL 三端。

工作流

# 1. 修改 server/src/db/schema.ts
vim server/src/db/schema.ts

# 2. 生成迁移 SQL
cd server
npm run db:generate
# 输出: server/migrations/000X_xxx.sql

# 3. 人工审核 SQL
#    检查项:
#    - 新增列必须有 DEFAULT 值
#    - 外键 ON DELETE/ON UPDATE 模式明确
#    - 禁止 sql.raw() 拼接,必须参数化

# 4. 同步 schema-pg.ts (镜像 schema.ts 改动)

# 5. 同步三个适配器实现
#    - storage/db/d1.ts
#    - storage/db/turso.ts
#    - storage/db/postgres.ts

# 6. 应用本地迁移
npx wrangler d1 migrations apply monolith-db --local

# 7. 测试通过后应用生产
npx wrangler d1 migrations apply monolith-db --remote

drizzle-sync-check skill 会在编辑 schema.ts 时自动触发同步检查。

查询模式

只选必要列,避免 select().from()

// ❌ 全表扫
const posts = await db.select().from(postsTable);

// ✅ 投影
const posts = await db
  .select({ id: postsTable.id, title: postsTable.title })
  .from(postsTable)
  .limit(20);

并发查询用 db.batch([...])

const [posts, categories] = await db.batch([
  db.select().from(postsTable).limit(10),
  db.select().from(categoriesTable),
]);

R2 文件上传

app.post('/api/admin/media/upload', async (c) => {
  const file = await c.req.formData().then(fd => fd.get('file') as File);
  const key = `uploads/${Date.now()}-${crypto.randomUUID()}.${ext(file.name)}`;

  const storage = createObjectStorage(c.env);
  await storage.put(key, await file.arrayBuffer(), {
    httpMetadata: {
      contentType: file.type,
      cacheControl: 'public, max-age=31536000, immutable',
    },
  });

  return c.json({ url: `/cdn/${key}` });
});

Webhook 通知

WEBHOOK_URLS 环境变量配置后,关键事件会异步触发 POST:

import { triggerWebhook } from './lib/webhook';

c.executionCtx.waitUntil(
  triggerWebhook(c.env, 'post_published', {
    slug: post.slug,
    title: post.title,
    publishedAt: post.publishedAt,
  })
);

支持事件: post_created, post_updated, post_deleted, post_published, comment_created


Cron 定时任务

server/wrangler.toml:

[triggers]
crons = ["* * * * *"]  # 每分钟

server/src/index.ts 暴露 scheduled handler:

export default {
  fetch: app.fetch,
  async scheduled(_event, env, ctx) {
    ctx.waitUntil(publishScheduledPosts(env));
  },
};

publishScheduledPosts 检查 posts.publishAt <= now() AND status = 'scheduled',自动晋升为 published 并触发 webhook。


Reaction 防刷

/api/posts/:slug/reactions 接收用户反应:

const fingerprint = await sha256(
  `${c.req.header('cf-connecting-ip')}:${ua}:${env.REACTION_SALT}`
);
// 用 fingerprint 作为唯一索引,避免同人重复点

REACTION_SALT 必须配置,否则匿名指纹可被预测推算。


安全要点

风险 防护
SQL 注入 Drizzle 参数化,禁用 sql.raw()
XSS DOMPurify 双层净化(前后端)
CSRF 严格 SameSite + JWT Bearer
SSRF 导入器 fetch 前校验目标 IP,禁内网段
暴力破解 /api/auth/login 限流 5 次/15 分钟
Token 泄漏 JWT 过期 8h + 单设备登录

详见 安全设计


性能要点

并发 IO

// ❌ 串行 await
const post = await db.getPost(slug);
const comments = await db.getComments(post.id);

// ✅ Promise.all
const [post, related] = await Promise.all([
  db.getPost(slug),
  db.getRelatedPosts(slug),
]);

边缘缓存

公开 GET 默认注入:

Cache-Control: public, max-age=15, s-maxage=60, stale-while-revalidate=30

禁用 Node 模块

Workers 不支持 fs/path/crypto,必须用 Web API:

// ❌ const crypto = require('crypto');
// ✅
const buf = new TextEncoder().encode(input);
const hash = await crypto.subtle.digest('SHA-256', buf);

延伸阅读

Clone this wiki locally