247 lines
8.5 KiB
Bash
247 lines
8.5 KiB
Bash
#!/usr/bin/env bash
|
||
# 财务大模型智能问答客服系统 — 后端一键落地脚本
|
||
#
|
||
# 前置:
|
||
# - Ubuntu 24.04,Python 3.12、postgresql、redis-server、nginx 已通过 apt 安装
|
||
# - 代码已放在 /home/ubuntu/finai/{backend,frontend,miniapp}
|
||
# - 用 ubuntu 账号执行本脚本;脚本内需要 root 权限的步骤会自己 sudo
|
||
#
|
||
# 用法:
|
||
# bash /home/ubuntu/finai/deploy/setup-backend.sh
|
||
# 可选环境变量:
|
||
# DOMAIN=finai.ctc-zc.com 站点域名(写进 ALLOWED_ORIGINS)
|
||
# DB_NAME=finai PG 库名 = 用户名
|
||
# DB_PASS=<留空自动生成> PG 用户密码
|
||
# FORCE_ENV=1 即使 .env 已存在也覆盖重写
|
||
#
|
||
# 幂等性:
|
||
# venv、依赖、PG 用户/库、.env、systemd unit 均检查后再动作,重复执行安全。
|
||
|
||
set -Eeuo pipefail
|
||
|
||
# ─── 可调参数 ──────────────────────────────────────────────
|
||
FINAI_HOME="/home/ubuntu/finai"
|
||
BACKEND_DIR="${FINAI_HOME}/backend"
|
||
STORAGE_DIR="${BACKEND_DIR}/storage"
|
||
VENV_DIR="${BACKEND_DIR}/.venv"
|
||
ENV_FILE="${BACKEND_DIR}/.env"
|
||
|
||
DOMAIN="${DOMAIN:-finai.ctc-zc.com}"
|
||
DB_NAME="${DB_NAME:-finai}"
|
||
DB_USER="${DB_NAME}"
|
||
DB_PASS="${DB_PASS:-}"
|
||
FORCE_ENV="${FORCE_ENV:-0}"
|
||
|
||
SERVICE_NAME="finai-backend"
|
||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||
|
||
# ─── 工具函数 ──────────────────────────────────────────────
|
||
c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||
c_yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
|
||
c_red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
|
||
step() { printf '\n\033[36m▶ %s\033[0m\n' "$*"; }
|
||
|
||
require_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || {
|
||
c_red "缺少命令:$1,请先 apt 安装对应包"; exit 1;
|
||
}
|
||
}
|
||
|
||
# ─── 0. 前置检查 ───────────────────────────────────────────
|
||
step "0. 前置检查"
|
||
|
||
if [[ "${EUID}" -eq 0 ]]; then
|
||
c_red "请用 ubuntu 账号(非 root)运行,脚本内部会按需 sudo。"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ "$(whoami)" != "ubuntu" ]]; then
|
||
c_yellow "当前用户是 $(whoami)(非 ubuntu),若你确认要用当前用户可继续,5 秒内 Ctrl+C 中止…"
|
||
sleep 5
|
||
fi
|
||
|
||
require_cmd python3.12
|
||
require_cmd psql
|
||
require_cmd systemctl
|
||
require_cmd openssl
|
||
require_cmd curl
|
||
|
||
if [[ ! -d "${BACKEND_DIR}" ]]; then
|
||
c_red "找不到 ${BACKEND_DIR},请先把代码上传到 ${FINAI_HOME}/backend"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -f "${BACKEND_DIR}/pyproject.toml" ]]; then
|
||
c_red "${BACKEND_DIR}/pyproject.toml 不存在,代码目录不完整"
|
||
exit 1
|
||
fi
|
||
|
||
sudo chown -R ubuntu:ubuntu "${FINAI_HOME}"
|
||
|
||
# ─── 1. Python 虚拟环境 + 依赖 ─────────────────────────────
|
||
step "1. 虚拟环境 & 依赖"
|
||
|
||
if [[ ! -x "${VENV_DIR}/bin/python" ]]; then
|
||
python3.12 -m venv "${VENV_DIR}"
|
||
c_green "venv 已创建:${VENV_DIR}"
|
||
else
|
||
c_yellow "venv 已存在,跳过创建"
|
||
fi
|
||
|
||
# shellcheck disable=SC1091
|
||
source "${VENV_DIR}/bin/activate"
|
||
pip install --upgrade pip >/dev/null
|
||
pip install . >/dev/null
|
||
c_green "依赖安装完成"
|
||
|
||
python - <<'PY'
|
||
import fastapi, sqlalchemy, pydantic_settings, httpx
|
||
from tencentcloud.lke.v20231130 import lke_client # noqa: F401
|
||
print(f" fastapi={fastapi.__version__} sqlalchemy={sqlalchemy.__version__}")
|
||
PY
|
||
|
||
mkdir -p "${STORAGE_DIR}"
|
||
|
||
# ─── 2. PostgreSQL 库 & 用户 ───────────────────────────────
|
||
step "2. PostgreSQL 建库建用户"
|
||
|
||
sudo systemctl is-active --quiet postgresql || {
|
||
c_red "postgresql 服务未运行,先 sudo systemctl start postgresql"
|
||
exit 1
|
||
}
|
||
|
||
user_exists=$(sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" || true)
|
||
db_exists=$(sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" || true)
|
||
|
||
if [[ "${user_exists}" != "1" ]]; then
|
||
if [[ -z "${DB_PASS}" ]]; then
|
||
DB_PASS="$(openssl rand -hex 16)"
|
||
c_yellow "自动生成 DB_PASS = ${DB_PASS} ← 保存好"
|
||
fi
|
||
sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL
|
||
CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';
|
||
SQL
|
||
c_green "PG 用户 ${DB_USER} 已创建"
|
||
else
|
||
c_yellow "PG 用户 ${DB_USER} 已存在"
|
||
if [[ -z "${DB_PASS}" ]]; then
|
||
if [[ -f "${ENV_FILE}" ]] && grep -q '^DATABASE_URL=' "${ENV_FILE}"; then
|
||
DB_PASS="$(sed -nE 's|^DATABASE_URL=postgresql\+psycopg://[^:]+:([^@]+)@.*|\1|p' "${ENV_FILE}")"
|
||
c_yellow "从旧 .env 读回 DB_PASS = ${DB_PASS}"
|
||
else
|
||
DB_PASS="$(openssl rand -hex 16)"
|
||
c_yellow "旧用户密码未知,改写为新 DB_PASS = ${DB_PASS}"
|
||
sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL
|
||
ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';
|
||
SQL
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [[ "${db_exists}" != "1" ]]; then
|
||
sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL
|
||
CREATE DATABASE ${DB_NAME} OWNER ${DB_USER} ENCODING 'UTF8';
|
||
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
|
||
SQL
|
||
c_green "PG 数据库 ${DB_NAME} 已创建"
|
||
else
|
||
c_yellow "PG 数据库 ${DB_NAME} 已存在"
|
||
fi
|
||
|
||
PGPASSWORD="${DB_PASS}" psql -h 127.0.0.1 -U "${DB_USER}" -d "${DB_NAME}" -c '\conninfo' \
|
||
|| { c_red "PG 连接失败,请检查 pg_hba.conf 是否放行 127.0.0.1/32 scram-sha-256"; exit 1; }
|
||
|
||
# ─── 3. 生成 .env ──────────────────────────────────────────
|
||
step "3. 生成 .env"
|
||
|
||
if [[ -f "${ENV_FILE}" && "${FORCE_ENV}" != "1" ]]; then
|
||
c_yellow ".env 已存在,保留不改。要重写请用 FORCE_ENV=1 重跑"
|
||
else
|
||
JWT_KEY="$(openssl rand -hex 32)"
|
||
cat > "${ENV_FILE}" <<EOF
|
||
ENVIRONMENT=production
|
||
DATABASE_URL=postgresql+psycopg://${DB_USER}:${DB_PASS}@127.0.0.1:5432/${DB_NAME}
|
||
JWT_SECRET_KEY=${JWT_KEY}
|
||
JWT_EXPIRE_MINUTES=480
|
||
ALLOWED_ORIGINS=https://${DOMAIN},http://${DOMAIN}
|
||
|
||
SMS_PROVIDER=mock
|
||
ADP_PROVIDER=mock
|
||
STORAGE_PROVIDER=local
|
||
LOCAL_STORAGE_DIR=${STORAGE_DIR}
|
||
|
||
# 接真实腾讯云 ADP 时把 mock 换 tencent,并填以下:
|
||
# ADP_BOT_APP_KEY=xxx
|
||
# ADP_KNOWLEDGE_BIZ_ID=xxx
|
||
# TENCENT_SECRET_ID=xxx
|
||
# TENCENT_SECRET_KEY=xxx
|
||
# TENCENT_REGION=ap-guangzhou
|
||
EOF
|
||
chmod 600 "${ENV_FILE}"
|
||
c_green ".env 已写入 ${ENV_FILE}"
|
||
fi
|
||
|
||
# ─── 4. systemd 单元 ──────────────────────────────────────
|
||
step "4. 部署 systemd 单元 ${SERVICE_NAME}"
|
||
|
||
sudo tee "${SERVICE_FILE}" >/dev/null <<EOF
|
||
[Unit]
|
||
Description=Finance AI Backend (FastAPI)
|
||
After=network.target postgresql.service redis-server.service
|
||
Wants=postgresql.service redis-server.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=ubuntu
|
||
Group=ubuntu
|
||
WorkingDirectory=${BACKEND_DIR}
|
||
EnvironmentFile=${ENV_FILE}
|
||
ExecStart=${VENV_DIR}/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2 --proxy-headers
|
||
Restart=always
|
||
RestartSec=3
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
ProtectSystem=full
|
||
ProtectHome=false
|
||
ReadWritePaths=${BACKEND_DIR}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable "${SERVICE_NAME}" >/dev/null
|
||
sudo systemctl restart "${SERVICE_NAME}"
|
||
sleep 2
|
||
sudo systemctl --no-pager status "${SERVICE_NAME}" | head -n 12 || true
|
||
|
||
# ─── 5. 健康检查 ──────────────────────────────────────────
|
||
step "5. 健康检查"
|
||
|
||
for i in 1 2 3 4 5; do
|
||
if curl -fsS http://127.0.0.1:8000/health >/dev/null; then
|
||
c_green "GET /health OK"
|
||
break
|
||
fi
|
||
if [[ "$i" == "5" ]]; then
|
||
c_red "5 次探测均失败,跑 journalctl -u ${SERVICE_NAME} -n 100 --no-pager 排查"
|
||
exit 1
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
# ─── 6. 总结 ──────────────────────────────────────────────
|
||
step "6. 完成 ✔"
|
||
|
||
cat <<EOF
|
||
|
||
后端已上线:http://127.0.0.1:8000 (仅本机可达,供 Nginx 反代)
|
||
默认管理员: 手机号 13800000000 验证码 123456 (mock 模式)
|
||
数据库: ${DB_NAME} / ${DB_USER} / ${DB_PASS}
|
||
JWT 密钥: 保存在 ${ENV_FILE}(chmod 600)
|
||
systemd: sudo systemctl status ${SERVICE_NAME}
|
||
日志: journalctl -u ${SERVICE_NAME} -f
|
||
|
||
下一步:按 run.md §4 构建并发布前端到 Nginx;HTTPS 见 §4.4(certbot -d ${DOMAIN})。
|
||
EOF
|