-
Notifications
You must be signed in to change notification settings - Fork 56
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'));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);
});
⚠️ 铁律: 任何 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 --remotedrizzle-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),
]);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_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。
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。
/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 + 单设备登录 |
详见 安全设计。
// ❌ 串行 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
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);Monolith · 边缘原生全栈博客 · MIT License · 文档以 main 分支 代码为准