Files
YG_FT/start.sh
2026-07-23 11:09:04 +08:00

382 lines
9.7 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="${ROOT_DIR}/backend"
FRONTEND_DIR="${ROOT_DIR}/frontend"
BACKEND_PORT=17861
FRONTEND_PORT=16801
BACKEND_PYTHON=""
BACKEND_PID=""
FRONTEND_PID=""
SHUTTING_DOWN=0
CHECK_ONLY=0
DATABASE_URL_SOURCE=""
info() {
printf '[启动] %s\n' "$*"
}
warn() {
printf '[警告] %s\n' "$*" >&2
}
fail() {
printf '[错误] %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
用法bash ./start.sh [--check] [--help]
--check 仅检查运行环境和端口,不启动服务
--help 显示帮助
脚本会继承当前终端的环境变量,例如:
DATABASE_HOST='db.example.com' bash ./start.sh
EOF
}
parse_args() {
if [ "$#" -gt 1 ]; then
usage >&2
exit 2
fi
case "${1:-}" in
"") ;;
--check) CHECK_ONLY=1 ;;
--help|-h)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
}
select_backend_python() {
if [ -x "${BACKEND_DIR}/.venv/bin/python" ]; then
BACKEND_PYTHON="${BACKEND_DIR}/.venv/bin/python"
elif command -v python3 >/dev/null 2>&1; then
BACKEND_PYTHON="$(command -v python3)"
warn "未找到 backend/.venv将使用 ${BACKEND_PYTHON}"
else
fail "未找到 Python 3。请先创建 backend/.venv 并安装后端依赖。"
fi
}
load_database_url() {
local env_file="${ROOT_DIR}/docker/app/.env"
local value=""
if ! value="$(DATABASE_CONFIG_FILE="${env_file}" "${BACKEND_PYTHON}" -c '
import ipaddress
import os
from pathlib import Path
from urllib.parse import quote
def read_env(path: str) -> dict[str, str]:
values: dict[str, str] = {}
env_path = Path(path)
if not env_path.is_file():
return values
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"\x27", "\""}:
value = value[1:-1]
values[key.strip()] = value
return values
file_values = read_env(os.environ["DATABASE_CONFIG_FILE"])
def setting(*names: str, default: str = "") -> str:
for name in names:
env_value = os.environ.get(name, "").strip()
if env_value:
return env_value
for name in names:
file_value = file_values.get(name, "").strip()
if file_value:
return file_value
return default
shell_url = os.environ.get("DATABASE_URL", "").strip()
shell_host = os.environ.get("DATABASE_HOST", "").strip()
file_host = file_values.get("DATABASE_HOST", "").strip()
file_url = file_values.get("DATABASE_URL", "").strip()
candidate = shell_url or shell_host or file_host or file_url
if not candidate:
raise SystemExit(0)
if "://" in candidate:
print(candidate)
raise SystemExit(0)
host = candidate.strip().rstrip("/")
if not host or any(character in host for character in "/@"):
raise SystemExit(1)
port_text = setting("DATABASE_PORT", default="5432")
try:
port = int(port_text)
except ValueError:
raise SystemExit(1)
if not 1 <= port <= 65535:
raise SystemExit(1)
username = setting("DATABASE_USER", "POSTGRES_USER", default="yg_ft")
password = setting("DATABASE_PASSWORD", "POSTGRES_PASSWORD", default="change_me")
database = setting("DATABASE_NAME", "POSTGRES_DB", default="yg_ft")
if not username or not database:
raise SystemExit(1)
try:
parsed_host = ipaddress.ip_address(host)
url_host = f"[{host}]" if parsed_host.version == 6 else host
except ValueError:
url_host = host
credentials = quote(username, safe="")
if password:
credentials += ":" + quote(password, safe="")
encoded_database = quote(database, safe="")
print(f"postgresql+psycopg://{credentials}@{url_host}:{port}/{encoded_database}")
' 2>/dev/null)"; then
fail "数据库分项配置无效,请检查 DATABASE_HOST 和 DATABASE_PORT。"
fi
if [ -z "${value}" ]; then
return
fi
if [ -n "${DATABASE_URL:-}" ] || [ -n "${DATABASE_HOST:-}" ]; then
DATABASE_URL_SOURCE="当前终端环境变量"
elif [ -f "${env_file}" ] && grep -Eq '^[[:space:]]*DATABASE_HOST=' "${env_file}"; then
DATABASE_URL_SOURCE="docker/app/.env 的 DATABASE_HOST"
else
DATABASE_URL_SOURCE="docker/app/.env 的 DATABASE_URL"
fi
export DATABASE_URL="${value}"
}
validate_database_url() {
if [ -z "${DATABASE_URL:-}" ]; then
return
fi
case "${DATABASE_URL}" in
*"@postgres:"*|*"//postgres:"*)
fail "DATABASE_URL 使用了 Docker 服务名 postgres本地启动请填写真实数据库主机名或 IP。"
;;
esac
if ! "${BACKEND_PYTHON}" -c '
import os
from urllib.parse import urlsplit
try:
parsed = urlsplit(os.environ["DATABASE_URL"])
port = parsed.port or 5432
except (KeyError, ValueError):
raise SystemExit(1)
valid = (
parsed.scheme in {"postgresql", "postgresql+psycopg"}
and bool(parsed.hostname)
and bool(parsed.path.strip("/"))
and 1 <= port <= 65535
)
raise SystemExit(0 if valid else 1)
'; then
fail "${DATABASE_URL_SOURCE:-DATABASE_URL} 中的数据库连接串格式无效。格式应为postgresql+psycopg://用户名:密码@主机:端口/数据库名"
fi
info "已使用 ${DATABASE_URL_SOURCE:-DATABASE_URL} 中的数据库连接配置(敏感信息已隐藏)。"
}
check_backend_dependencies() {
if ! "${BACKEND_PYTHON}" -c \
'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)'; then
fail "后端要求 Python >= 3.12,当前解释器为 $(${BACKEND_PYTHON} --version 2>&1)"
fi
if ! "${BACKEND_PYTHON}" -c \
'import alembic, dotenv, fastapi, httpx, jwt, multipart, passlib, psycopg, pydantic, redis, sqlalchemy, uvicorn' \
>/dev/null 2>&1; then
fail "后端依赖不完整。请执行:${BACKEND_PYTHON} -m pip install -r backend/requirements.txt"
fi
}
check_frontend_dependencies() {
command -v node >/dev/null 2>&1 || fail "未找到 Node.js。"
command -v npm >/dev/null 2>&1 || fail "未找到 npm。"
if [ ! -x "${FRONTEND_DIR}/node_modules/.bin/vite" ]; then
fail "前端依赖未安装。请执行cd frontend && npm ci"
fi
if ! npm --prefix "${FRONTEND_DIR}" ls --depth=0 >/dev/null 2>&1; then
fail "前端依赖状态异常。请执行cd frontend && npm ci"
fi
}
check_port() {
local port="$1"
local service_name="$2"
if command -v lsof >/dev/null 2>&1 && \
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
fail "${service_name}端口 ${port} 已被占用,请先停止占用该端口的进程。"
fi
}
warn_if_default_database_is_unavailable() {
if [ -n "${DATABASE_URL:-}" ] || ! command -v lsof >/dev/null 2>&1; then
return
fi
if ! lsof -nP -iTCP:15432 -sTCP:LISTEN >/dev/null 2>&1; then
warn "未检测到默认 PostgreSQL 端口 15432后端可启动但健康检查和业务接口可能失败。"
fi
}
preflight() {
[ -d "${BACKEND_DIR}" ] || fail "缺少后端目录:${BACKEND_DIR}"
[ -d "${FRONTEND_DIR}" ] || fail "缺少前端目录:${FRONTEND_DIR}"
[ -f "${FRONTEND_DIR}/package-lock.json" ] || fail "缺少 frontend/package-lock.json"
select_backend_python
load_database_url
validate_database_url
check_backend_dependencies
check_frontend_dependencies
check_port "${BACKEND_PORT}" "后端"
check_port "${FRONTEND_PORT}" "前端"
warn_if_default_database_is_unavailable
info "环境检查通过:$(${BACKEND_PYTHON} --version 2>&1)Node $(node --version)"
}
terminate_process_tree() {
local pid="$1"
local child=""
if [ -z "${pid}" ] || ! kill -0 "${pid}" 2>/dev/null; then
return
fi
if command -v pgrep >/dev/null 2>&1; then
for child in $(pgrep -P "${pid}" 2>/dev/null || true); do
terminate_process_tree "${child}"
done
fi
kill -TERM "${pid}" 2>/dev/null || true
}
cleanup() {
local exit_code="${1:-0}"
if [ "${SHUTTING_DOWN}" -eq 1 ]; then
return
fi
SHUTTING_DOWN=1
trap - EXIT INT TERM
if [ -n "${BACKEND_PID}" ] || [ -n "${FRONTEND_PID}" ]; then
info "正在停止前端和后端服务……"
fi
terminate_process_tree "${FRONTEND_PID}"
terminate_process_tree "${BACKEND_PID}"
[ -z "${FRONTEND_PID}" ] || wait "${FRONTEND_PID}" 2>/dev/null || true
[ -z "${BACKEND_PID}" ] || wait "${BACKEND_PID}" 2>/dev/null || true
exit "${exit_code}"
}
start_backend() {
info "启动后端http://127.0.0.1:${BACKEND_PORT}/modelTF/health"
(
cd "${BACKEND_DIR}"
exec "${BACKEND_PYTHON}" -m uvicorn app.main:app \
--reload \
--host 127.0.0.1 \
--port "${BACKEND_PORT}"
) &
BACKEND_PID=$!
}
start_frontend() {
info "启动前端http://localhost:${FRONTEND_PORT}"
(
cd "${FRONTEND_DIR}"
exec npm run dev -- --strictPort
) &
FRONTEND_PID=$!
}
wait_for_services() {
local exit_code=0
while :; do
if ! kill -0 "${BACKEND_PID}" 2>/dev/null; then
if wait "${BACKEND_PID}"; then
exit_code=0
else
exit_code=$?
fi
warn "后端服务已退出(状态码 ${exit_code})。"
[ "${exit_code}" -ne 0 ] || exit_code=1
return "${exit_code}"
fi
if ! kill -0 "${FRONTEND_PID}" 2>/dev/null; then
if wait "${FRONTEND_PID}"; then
exit_code=0
else
exit_code=$?
fi
warn "前端服务已退出(状态码 ${exit_code})。"
[ "${exit_code}" -ne 0 ] || exit_code=1
return "${exit_code}"
fi
sleep 1
done
}
main() {
parse_args "$@"
preflight
if [ "${CHECK_ONLY}" -eq 1 ]; then
info "前后端均可启动。"
return
fi
trap 'cleanup $?' EXIT
trap 'cleanup 130' INT
trap 'cleanup 143' TERM
start_backend
start_frontend
info "前后端已启动,按 Ctrl+C 同时停止。"
wait_for_services
}
main "$@"