chore: 数据已迁到 SQLite,删掉 Postgres 迁移脚本、webpreview-db 服务和迁移文档
Deploy / deploy (build, debian, 22) (push) Canceled after 0s

school 服务器以后要切换时,从 cdeb7c1 里找回(deploy.yaml 里写了)。
备份方法挪到 CLAUDE.md。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 21:36:33 +08:00
1 parent cdeb7c18ef
commit dc91b9b5db
5 files changed
+5 -225

No files matched your search

+2 -1
View File
@@ -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
+3 -2
View File
@@ -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/<task_id>/?token=<access_token>` (code generation chat) and `/ws/guidance/<task_id>/?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 <username> <password>
```
Back up production by copying `sqlite/` (or `sqlite3 sqlite/db.sqlite3 ".backup backup.sqlite3"`).
## Key Technical Details
-13
View File
@@ -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"
-75
View File
@@ -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"`)
-134
View File
@@ -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<string, number> = {}
const rowsByTable: Record<string, Record<string, unknown>[]> = {}
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<string, string>(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<string, unknown>[] = 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)