|
|
|
|
@ -0,0 +1,246 @@
|
|
|
|
|
#!/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="finance-ai-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
|