diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 4111d07..425978c 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -18,7 +18,8 @@ jobs: - name: debian build_command: build remote_port: 22 - # school 暂时停用,需要时恢复(并先按 docs/migrate-to-bun.md 切换那台服务器) + # school 暂时停用,需要时恢复。那台还没从 Postgres 迁到 SQLite:迁移脚本、webpreview-db 服务和 + # 步骤文档已删除,从提交 cdeb7c1 里找回(git show cdeb7c1:docs/migrate-to-bun.md) # - name: school # build_command: build:staging # remote_port: 8822 diff --git a/CLAUDE.md b/CLAUDE.md index 2959dd8..5637921 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1 - `src/routes/` — `account`, `tasks` (tutorial / challenge / creation), `files` (task assets + image upload), `submission`, `showcase` (mounted under `/submission/showcase`), `prompt` - `src/services/` — `score.ts` (rating weights super×0.5, admin×0.3, normal×0.2, then Bayesian smoothing with C=3 and zones), `gradebook.ts`, `llm.ts` (OpenAI-compatible streaming via fetch; DeepSeek default, ARK for doubao models), `classifier.ts` (Bloom-level L1–L6 prompt tagging, runs in background), `cascade.ts` (Django's CASCADE deletes, done by hand), `pyround.ts` (`pyRound`/`pySum` reproduce Python's `round()` and `sum()` so scores match the old system bit-for-bit) - `src/ws.ts` — `/ws/prompt//?token=` (code generation chat) and `/ws/guidance//?token=...` (prompt coaching); messages per connection are processed serially; on disconnect the pending user message is deleted and the LLM request aborted - - `scripts/` — one-off CLIs: `migrate-from-postgres.ts`, `create-superuser.ts`, `classify-prompts.ts`, `recalculate-scores.ts` + - `scripts/` — one-off CLIs: `create-superuser.ts`, `classify-prompts.ts`, `recalculate-scores.ts` - Roles: `super`, `admin`, `normal`. Task model is multi-table: `task_task` + one of `task_tutorial` / `task_challenge` / `task_creation` (`task_ptr_id`) - **`client/`** — Vue 3 + TypeScript SPA built with **Vite**; Bun is the package manager and runtime (no Node/npm) @@ -31,7 +31,7 @@ The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1 - `src/pages/`, `src/components/` — user management uses `components/dashboard/UserFormModal.vue` (there is no Django Admin anymore) - **`Caddyfile`** — serves SPA from `build/` and `/media`, proxies `/api*` and `/ws*` to the backend -- **`docker-compose.yaml`** — `webpreview-web` (Caddy) and `webpreview-api` (Bun); `webpreview-db` (Postgres) exists only under the `migrate` profile for the one-time data migration +- **`docker-compose.yaml`** — `webpreview-web` (Caddy) and `webpreview-api` (Bun); the API reads the repo-root `.env` via `env_file` (`server/.env` is only for local dev). The one-time Postgres → SQLite migration (script, `webpreview-db` service, `docs/migrate-to-bun.md`) was removed after commit `cdeb7c1`; restore it from there if another server still needs the cutover - **`.github/workflows/deploy.yaml`** — on push to `main`: build client with Bun, rsync to server `build/`, then `git pull && docker compose up -d --build` on the server ## Development Commands @@ -72,6 +72,7 @@ Frontend env vars used at build time: `PUBLIC_BASE_URL`, `PUBLIC_WS_URL`, `PUBLI docker compose up -d --build docker compose run --rm webpreview-api bun scripts/create-superuser.ts ``` +Back up production by copying `sqlite/` (or `sqlite3 sqlite/db.sqlite3 ".backup backup.sqlite3"`). ## Key Technical Details diff --git a/docker-compose.yaml b/docker-compose.yaml index bc64ce3..f4ac624 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -23,16 +23,3 @@ services: - ./sqlite:/app/data - ./media:/app/media restart: unless-stopped - - # 只在从旧版 Postgres 迁移数据时使用: - # docker compose --profile migrate up -d webpreview-db - # docker compose run --rm webpreview-api bun scripts/migrate-from-postgres.ts - webpreview-db: - image: postgres:17-alpine - container_name: webpreview-db - profiles: ["migrate"] - volumes: - - ./data:/var/lib/postgresql/data - env_file: - - .env - restart: "no" diff --git a/docs/migrate-to-bun.md b/docs/migrate-to-bun.md deleted file mode 100644 index 9ae7757..0000000 --- a/docs/migrate-to-bun.md +++ /dev/null @@ -1,75 +0,0 @@ -# 服务器切换到 Bun 后端(一次性) - -后端从 Django + Postgres + Redis 换成了 Bun + SQLite,前后端也合并成了一个仓库 -(`git.xuyue.cc:xuyue/web.git` / `github.com/yuetsh/web`)。 -每台服务器(debian、school)都要按下面的步骤做一次,之后 push 到 `main` 就会自动部署。 - -切换时所有人需要重新登录一次(旧的登录会话在 Redis 里,不迁移)。密码不受影响。 - -## 0. GitHub 仓库设置 - -在 `yuetsh/web` 的 Settings → Secrets and variables → Actions 里添加和旧仓库一样的 `KEY`、`HOST`。 - -## 1. 备份 - -```bash -cd /root/WebPreview -docker compose exec webpreview-db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > /root/webpreview-$(date +%F).sql -``` - -`data/`(Postgres 数据目录)在整个过程中不会被修改,出问题可以随时回退。 - -## 2. 把目录变成新仓库的 clone - -```bash -cd /root/WebPreview -git init -b main -git remote add origin git@git.xuyue.cc:xuyue/web.git -git fetch origin -git reset origin/main # 只建立 git 记录,不改动现有文件 -git checkout -- . # 用仓库里的 docker-compose.yaml、Caddyfile 等覆盖旧文件 -mv api /root/webpreview-api-old # 旧的 Django 后端,确认没问题后可删 -git status # 应该是干净的(.env、data/、build/、media/ 都被忽略) -``` - -## 3. 迁移数据(Postgres → SQLite) - -找一个没人用的时间做,先停掉旧后端,避免迁移期间有新数据写进 Postgres: - -```bash -docker stop webpreview-api -docker compose --profile migrate up -d webpreview-db # 旧库本来就在运行的话不会有变化 -docker compose build webpreview-api -docker compose run --rm --no-deps webpreview-api bun scripts/migrate-from-postgres.ts -``` - -脚本会逐表打印行数并做外键检查,最后输出 `导入完成` 才算成功。数据库文件在 `./sqlite/db.sqlite3`。 -`.env` 里原有的 `POSTGRES_*` 变量就是脚本要用的,不需要改。 - -## 4. 启动新服务 - -```bash -docker compose up -d --remove-orphans # 启动 Caddy + Bun,顺带删掉不再需要的 Redis 容器 -docker compose --profile migrate stop webpreview-db -docker stats --no-stream # webpreview-api 应该在几十 MB -``` - -打开网站登录确认一下。之后就可以 `docker compose --profile migrate rm webpreview-db`, -`data/` 目录建议留一段时间再删(属主是容器里的 postgres 用户,需要 root 才能删)。 - -## 回退 - -```bash -docker compose down -mv /root/webpreview-api-old api -git show 47d4679:docker-compose.yaml > docker-compose.yaml -git show 47d4679:Caddyfile > Caddyfile -docker compose up -d --build -``` - -## 以后的日常操作 - -- 部署:push 到 `main`,GitHub Actions 会构建前端并在服务器上 `git pull && docker compose up -d --build` -- 新建超级管理员:`docker compose exec webpreview-api bun scripts/create-superuser.ts <用户名> <密码>` -- 补标注提示词层级:`docker compose exec webpreview-api bun scripts/classify-prompts.ts` -- 备份:复制 `sqlite/` 目录即可(或 `sqlite3 sqlite/db.sqlite3 ".backup backup.sqlite3"`) diff --git a/server/scripts/migrate-from-postgres.ts b/server/scripts/migrate-from-postgres.ts deleted file mode 100644 index b5c78a8..0000000 --- a/server/scripts/migrate-from-postgres.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * 把旧 Django 项目的 Postgres 数据导入 SQLite(一次性迁移)。 - * - * POSTGRES_HOST=... POSTGRES_DB=... POSTGRES_USER=... POSTGRES_PASSWORD=... \ - * bun scripts/migrate-from-postgres.ts [--force] - * - * 目标库为 DB_PATH(默认 data/db.sqlite3)。目标库已有数据时需要 --force(会先清空)。 - * 会话不迁移,所有人需要重新登录一次。 - */ -import { SQL } from "bun" -import { config } from "../src/config" -import { db } from "../src/db" - -// 按外键依赖顺序 -const TABLES = [ - "account_user", - "task_task", - "task_tutorial", - "task_challenge", - "task_creation", - "task_taskasset", - "submission_submission", - "submission_rating", - "submission_award", - "submission_submissionaward", - "prompt_conversation", - "prompt_message", -] - -const force = process.argv.includes("--force") -const env = process.env -for (const key of ["POSTGRES_HOST", "POSTGRES_DB", "POSTGRES_USER", "POSTGRES_PASSWORD"]) { - if (!env[key]) { - console.error(`缺少环境变量 ${key}`) - process.exit(1) - } -} - -const pg = new SQL({ - hostname: env.POSTGRES_HOST, - port: Number(env.POSTGRES_PORT ?? 5432), - database: env.POSTGRES_DB, - username: env.POSTGRES_USER, - password: env.POSTGRES_PASSWORD, -}) - -const existing = db.query(`SELECT COUNT(*) AS n FROM account_user`).get() as { n: number } -if (existing.n > 0 && !force) { - console.error(`目标库 ${config.dbPath} 已有数据,加 --force 会清空后重新导入`) - process.exit(1) -} - -const quote = (name: string) => `"${name.replaceAll('"', '""')}"` - -// 按 Postgres 的列类型转换成 Django 在 SQLite 中的存储格式 -function selectExpr(column: string, pgType: string) { - const c = quote(column) - switch (pgType) { - case "timestamp with time zone": - return `to_char(${c} AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS.US') AS ${c}` - case "timestamp without time zone": - return `to_char(${c}, 'YYYY-MM-DD HH24:MI:SS.US') AS ${c}` - case "uuid": - return `replace(${c}::text, '-', '') AS ${c}` - case "boolean": - return `${c}::int AS ${c}` - case "bigint": - return `${c}::text AS ${c}` - default: - return c - } -} - -const counts: Record = {} -const rowsByTable: Record[]> = {} - -for (const table of TABLES) { - const sqliteColumns = (db.query(`PRAGMA table_info(${quote(table)})`).all() as { name: string }[]).map( - (c) => c.name, - ) - const pgColumns = await pg` - SELECT column_name, data_type FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = ${table}` - if (!pgColumns.length) throw new Error(`Postgres 中没有表 ${table}`) - const pgTypes = new Map(pgColumns.map((c: any) => [c.column_name, c.data_type])) - const missing = sqliteColumns.filter((c) => !pgTypes.has(c)) - if (missing.length) throw new Error(`Postgres 表 ${table} 缺少列:${missing.join(", ")}`) - - const select = sqliteColumns.map((c) => selectExpr(c, pgTypes.get(c)!)).join(", ") - const rows: Record[] = await pg.unsafe(`SELECT ${select} FROM ${quote(table)}`) - for (const row of rows) { - for (const c of sqliteColumns) { - if (pgTypes.get(c) === "bigint" && row[c] !== null) row[c] = Number(row[c]) - } - } - rowsByTable[table] = rows - console.log(`读取 ${table}: ${rows.length} 行`) -} -await pg.close() - -db.transaction(() => { - db.run("PRAGMA defer_foreign_keys = ON") - for (const table of [...TABLES].reverse()) db.run(`DELETE FROM ${quote(table)}`) - db.run(`DELETE FROM auth_refresh_token`) - for (const table of TABLES) { - const rows = rowsByTable[table]! - if (!rows.length) { - counts[table] = 0 - continue - } - const columns = Object.keys(rows[0]!) - const insert = db.prepare( - `INSERT INTO ${quote(table)} (${columns.map(quote).join(", ")}) - VALUES (${columns.map((c) => "$" + c).join(", ")})`, - ) - for (const row of rows) insert.run(row as any) - counts[table] = rows.length - } -})() - -// 校验 -let ok = true -for (const table of TABLES) { - const n = (db.query(`SELECT COUNT(*) AS n FROM ${quote(table)}`).get() as { n: number }).n - if (n !== counts[table]) ok = false - console.log(`${n === counts[table] ? "✓" : "✗"} ${table}: ${n}`) -} -const fk = db.query(`PRAGMA foreign_key_check`).all() -if (fk.length) { - ok = false - console.error("外键检查失败:", fk.slice(0, 10)) -} -console.log(ok ? `\n导入完成:${config.dbPath}` : "\n导入有问题,请检查上面的输出") -process.exit(ok ? 0 : 1)