mirror of
https://github.com/wangwangit/SubsTracker.git
synced 2026-06-08 18:44:22 +08:00
refactor(api): 引入 Hono 应用作为路由外壳
src/app.js 新建 Hono 应用:
- 全局中间件:ensureMigrations(首次访问透明迁移)、onError 兜底
- 路由:GET /、GET /admin、ALL /admin/*、ALL /api/*、/debug、兜底
- path/method/response shape 与 v2 严格 1:1 兼容(前端零修改)
src/index.js 简化为 { fetch: app.fetch, scheduled }
策略说明:
- Hono 现阶段当"路由外壳"用,请求转发给现有 handler
- 后续 Task 可逐 handler 改成 Hono 原生写法(c.json/c.req.json 等)
- 这样既拿到中间件 / 错误兜底好处,又不破坏既有行为
vitest.config.js 增加 assetsInclude: ['**/*.html']
让 vite 在测试中能解析 src/views/*.html 文本 import(与生产 wrangler text loader 行为一致)
测试 tests/api/routes-compat.test.js 8 条覆盖:
- GET / 未登录 → 登录页 HTML
- GET /admin 未登录 → 重定向
- GET /api/subscriptions 未登录 → 401 标准错误体
- POST /api/login 错误 body / 正确凭据
- GET /debug 未登录 → 401
- GET /api/未知 → 404 标准错误体
- 兜底路径 → 登录页
总计 158 条测试全绿;wrangler dry-run 476 KiB / gzip 101 KiB。
Refs Task 7 of refactor/v3-product-grade plan.
This commit is contained in:
110
src/app.js
Normal file
110
src/app.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Hono 应用装配(v3)
|
||||
*
|
||||
* 设计目标:
|
||||
* - 用 Hono 替换 v2 的手写 if/else 路由分发
|
||||
* - 引入中间件:迁移检查 / 日志 / 认证 / 错误处理
|
||||
* - 保持 path / method / response shape 与 v2 严格 1:1 兼容
|
||||
* 现有前端代码无需改动即可继续工作
|
||||
*
|
||||
* 落地策略:
|
||||
* - 现阶段(Task 7):Hono 充当"外壳路由器",把请求转发给现有 handler
|
||||
* 后续 Task 可逐个把 handler 改成 Hono 原生写法,但当前优先保证不破坏。
|
||||
*
|
||||
* 维护人:v3 重构 (2026-05)
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { handleApiRequest } from './api/router.js';
|
||||
import { handleAdminRequest, handleLoginPage } from './api/admin.js';
|
||||
import { handleDebug } from './api/debug.js';
|
||||
import { getUserFromRequest } from './api/handlers/auth.js';
|
||||
import { ensureMigrations } from './data/migrate.js';
|
||||
|
||||
/**
|
||||
* @typedef {{ SUBSCRIPTIONS_KV: KVNamespace }} Bindings
|
||||
*/
|
||||
|
||||
/** @type {Hono<{ Bindings: Bindings }>} */
|
||||
const app = new Hono();
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 全局中间件:迁移检查(首次访问透明触发)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
app.use('*', async (c, next) => {
|
||||
try {
|
||||
await ensureMigrations(c.env);
|
||||
} catch (err) {
|
||||
console.error('[app] 迁移失败,回退继续处理请求:', err);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 全局错误兜底
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
app.onError((err, c) => {
|
||||
console.error('[app] 未捕获异常:', err && err.stack ? err.stack : err);
|
||||
// 与 v2 错误格式保持一致
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
message: err && err.message ? err.message : '服务异常',
|
||||
code: 'internal_error'
|
||||
},
|
||||
500
|
||||
);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 路由:根路径
|
||||
// 已登录跳 /admin;未登录返回登录页
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
app.get('/', async (c) => {
|
||||
const { user } = await getUserFromRequest(c.req.raw, c.env);
|
||||
if (user) return c.redirect('/admin');
|
||||
return handleLoginPage();
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 路由:/debug(必须登录)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
app.all('/debug', async (c) => {
|
||||
const { user } = await getUserFromRequest(c.req.raw, c.env);
|
||||
if (!user) {
|
||||
return new Response('未授权访问', {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
||||
});
|
||||
}
|
||||
return handleDebug(c.req.raw, c.env);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 路由:/api/*(认证由 handler 内部处理,与 v2 一致)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
app.all('/api/*', async (c) => {
|
||||
return handleApiRequest(c.req.raw, c.env);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 路由:/admin/*
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
app.all('/admin/*', async (c) => {
|
||||
return handleAdminRequest(c.req.raw, c.env);
|
||||
});
|
||||
|
||||
app.get('/admin', async (c) => {
|
||||
return handleAdminRequest(c.req.raw, c.env);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 兜底:其他路径返回登录页(保留 v2 行为)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
app.all('*', async () => {
|
||||
return handleLoginPage();
|
||||
});
|
||||
|
||||
export default app;
|
||||
63
src/index.js
63
src/index.js
@@ -2,70 +2,33 @@
|
||||
/**
|
||||
* Worker 入口(v3)
|
||||
*
|
||||
* - fetch handler:处理 HTTP 请求;首先确保 KV 数据已迁移到 v3 schema
|
||||
* - scheduled handler:每小时触发一次到期检查(cron 0 * * * * UTC)
|
||||
*
|
||||
* v3 起 schema 迁移由 src/data/migrate.js 自动完成(首次访问透明触发,幂等可重跑)。
|
||||
*
|
||||
* 后续 Task 7 会把 fetch handler 整体迁到 Hono 应用,本文件届时大幅简化。
|
||||
* fetch handler 委托给 Hono 应用(src/app.js)。
|
||||
* scheduled handler 触发定时任务执行。
|
||||
*
|
||||
* 维护人:v3 重构 (2026-05)
|
||||
*/
|
||||
|
||||
import { handleApiRequest } from './api/router.js';
|
||||
import { handleAdminRequest, handleLoginPage } from './api/admin.js';
|
||||
import { handleDebug } from './api/debug.js';
|
||||
import { checkExpiringSubscriptions } from './services/scheduler.js';
|
||||
import { getUserFromRequest } from './api/handlers/auth.js';
|
||||
import app from './app.js';
|
||||
import { ensureMigrations } from './data/migrate.js';
|
||||
import { checkExpiringSubscriptions } from './services/scheduler.js';
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
// 透明迁移:v3 schema 不到位时先迁移再处理请求
|
||||
try {
|
||||
await ensureMigrations(env);
|
||||
} catch (err) {
|
||||
console.error('[index] 迁移失败,回退继续处理请求(用户会看到旧数据):', err);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === '/') {
|
||||
const { user } = await getUserFromRequest(request, env);
|
||||
if (user) {
|
||||
return new Response('', {
|
||||
status: 302,
|
||||
headers: { Location: '/admin' }
|
||||
});
|
||||
}
|
||||
return handleLoginPage();
|
||||
} else if (url.pathname === '/debug') {
|
||||
// 调试页必须登录后才能访问,避免泄露系统信息
|
||||
const { user } = await getUserFromRequest(request, env);
|
||||
if (!user) {
|
||||
return new Response('未授权访问', {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
||||
});
|
||||
}
|
||||
return handleDebug(request, env);
|
||||
} else if (url.pathname.startsWith('/api')) {
|
||||
return handleApiRequest(request, env);
|
||||
} else if (url.pathname.startsWith('/admin')) {
|
||||
return handleAdminRequest(request, env);
|
||||
} else {
|
||||
return handleLoginPage();
|
||||
}
|
||||
},
|
||||
fetch: app.fetch,
|
||||
|
||||
/**
|
||||
* 每小时由 Cron 触发一次。
|
||||
*
|
||||
* @param {ScheduledEvent} event
|
||||
* @param {{ SUBSCRIPTIONS_KV: KVNamespace }} env
|
||||
* @param {ExecutionContext} ctx
|
||||
*/
|
||||
async scheduled(event, env, ctx) {
|
||||
// Cron 触发也要确保迁移完成(首次部署后用户可能还没访问过页面)
|
||||
void ctx;
|
||||
try {
|
||||
await ensureMigrations(env);
|
||||
} catch (err) {
|
||||
console.error('[index] scheduled 迁移失败:', err);
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[Workers] 定时任务触发',
|
||||
'cron:',
|
||||
|
||||
Reference in New Issue
Block a user