diff --git a/README.md b/README.md index 7975490..6ad5c36 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,14 @@ 需要 Node.js 22、uv、Python 3.12、PostgreSQL 17。所有示例从仓库根目录执行。 +快速启动本地页面(迁移、管理员初始化、前端构建、API 与导入 worker 一条命令完成,健康检查通过后自动打开 `http://127.0.0.1:8000`;`Ctrl-C` 或 `scripts/dev_up.sh --stop` 停止,`--no-open` 跳过自动打开浏览器): + +```bash +scripts/dev_up.sh +``` + +未设置 `DATABASE_URL` 时该脚本使用 SQLite 轻量开发库 `var/db.sqlite3`,仅限页面测试与开发回归;金额聚合与财务验收需按下方配置 PostgreSQL。手动分步流程如下: + ```bash uv sync --frozen --python 3.12 npm ci --registry=https://registry.npmjs.org diff --git a/scripts/dev_up.sh b/scripts/dev_up.sh new file mode 100755 index 0000000..f8d763d --- /dev/null +++ b/scripts/dev_up.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# 一键启动本地开发环境:迁移 → 初始化管理员 → 构建前端 → 启动 API+worker → 打开页面。 +# 默认使用 SQLite 轻量开发库(var/db.sqlite3);设置 DATABASE_URL 则使用 PostgreSQL。 +# Ctrl-C 或 --stop 停止本脚本启动的进程。 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +PORT="${PORT:-8000}" +SERVER_PID_FILE="var/dev-server.pid" +WORKER_PID_FILE="var/dev-worker.pid" +SERVER_LOG="var/dev-server.log" +WORKER_LOG="var/dev-worker.log" + +die() { echo "错误:$*" >&2; exit 1; } + +detect_python() { + if [ -x ".venv/bin/python" ]; then + PY=".venv/bin/python" + elif command -v uv >/dev/null 2>&1; then + PY="uv run python" + else + die "未找到 .venv/bin/python 或 uv,请先安装依赖(uv sync)" + fi +} + +read_pidfile() { [ -f "$1" ] && cat "$1" || true; } + +stop_all() { + local stopped=0 pid + for file in "$SERVER_PID_FILE" "$WORKER_PID_FILE"; do + pid="$(read_pidfile "$file")" + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + stopped=1 + fi + rm -f "$file" + done + [ "$stopped" = 1 ] && echo "已停止本地开发服务" || echo "没有正在运行的开发服务" +} + +health_ok() { curl -fsS --max-time 2 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; } + +open_page() { + local url="http://127.0.0.1:$PORT/" + if command -v open >/dev/null 2>&1; then open "$url" + elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 || true + fi +} + +case "${1:-}" in + --stop) + stop_all + exit 0 + ;; + "" | --no-open) ;; + *) die "未知参数:$1(支持 --stop / --no-open)" ;; +esac + +detect_python +mkdir -p var + +if health_ok; then + echo "已在 $PORT 端口运行,直接打开页面" + [ "${1:-}" != "--no-open" ] && open_page + exit 0 +fi + +if [ -z "${DATABASE_URL:-}" ]; then + echo "未设置 DATABASE_URL,使用 SQLite 轻量开发库 var/db.sqlite3(仅限页面测试与开发回归)" +fi + +echo "执行数据库迁移…" +$PY manage.py migrate --no-input >/dev/null + +if [ ! -f var/local-access.txt ]; then + echo "初始化本地管理员…" + PYTHONPATH="$ROOT" $PY scripts/init_local.py +fi + +if [ ! -d frontend/dist ]; then + echo "构建前端(首次)…" + npm run build >/dev/null +fi + +echo "启动 worker(日志 ${WORKER_LOG})…" +nohup $PY manage.py import_worker >"$WORKER_LOG" 2>&1 & +echo $! >"$WORKER_PID_FILE" + +echo "启动 API 服务…" +nohup $PY manage.py runserver "127.0.0.1:$PORT" --noreload >"$SERVER_LOG" 2>&1 & +echo $! >"$SERVER_PID_FILE" + +cleanup() { + stop_all >/dev/null 2>&1 || true +} +trap cleanup INT TERM + +for _ in $(seq 1 30); do + if health_ok; then + break + fi + if [ -n "$(read_pidfile "$SERVER_PID_FILE")" ] && ! kill -0 "$(read_pidfile "$SERVER_PID_FILE")" 2>/dev/null; then + tail -20 "$SERVER_LOG" >&2 + die "API 服务启动失败(完整日志:${SERVER_LOG})" + fi + sleep 1 +done +health_ok || { tail -20 "$SERVER_LOG" >&2; die "健康检查超时(日志:${SERVER_LOG})"; } + +echo +echo "本地开发环境已就绪:http://127.0.0.1:$PORT" +[ -f var/local-access.txt ] && echo "登录凭据:var/local-access.txt(用户名 admin,权限 0600)" +if [ -z "${DATABASE_URL:-}" ]; then + echo "提示:当前为 SQLite 轻量模式;高精度金额聚合与生产验收请按 README 配置 PostgreSQL。" +fi +echo "日志:${SERVER_LOG} / ${WORKER_LOG} · Ctrl-C 或 scripts/dev_up.sh --stop 停止" +echo + +[ "${1:-}" != "--no-open" ] && open_page + +wait "$(read_pidfile "$SERVER_PID_FILE")" || true +cleanup