#!/usr/bin/env bash # 国内期货监控系统 - Ubuntu 一键部署 / 更新 # root 用户 | 目录 /opt/qihuo | 端口 6600 | PM2 # # 已内置修复(避免重复踩坑): # - vnpy_ctp 编译:build-essential python3-dev pkg-config # - CTP 登录崩溃:zh_CN.GB18030 + zh_CN.UTF-8 locale # - 时区:Asia/Shanghai(与 SimNow 交易时段一致) # - SimNow 前置:自动探测可用线路并写入 .env # - PM2 环境变量:LANG/LC_ALL(见 ecosystem.config.cjs) # - .env 缺项补全:SIMNOW_ENV、CTP_AUTO_RECONNECT set -euo pipefail APP_DIR="/opt/qihuo" REPO_URL="https://git.bz121.com/dekun/qihuo.git" SERVICE_NAME="qihuo" # SimNow 前置候选(按优先级;部署时自动 nc 探测) SIMNOW_FRONTS=( "182.254.243.31:30001:30011" "180.168.146.187:10201:10211" "180.168.146.187:10130:10131" "218.202.237.33:10203:10213" ) if [ "$(id -u)" -ne 0 ]; then echo "请使用 root 用户运行: sudo bash deploy.sh" exit 1 fi probe_tcp() { local host="$1" port="$2" if command -v nc &>/dev/null; then nc -z -w 3 "$host" "$port" &>/dev/null return $? fi timeout 3 bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null } ensure_locale_gen() { local pat="$1" if [ -f /etc/locale.gen ] && grep -q "^# ${pat}" /etc/locale.gen; then sed -i "s/^# ${pat}/${pat}/" /etc/locale.gen fi } ensure_env_key() { local file="$1" key="$2" val="$3" if [ ! -f "$file" ]; then return fi if grep -q "^${key}=" "$file"; then return fi echo "${key}=${val}" >>"$file" echo " .env 补全: ${key}=${val}" } pick_simnow_front() { local host td_port md_port for entry in "${SIMNOW_FRONTS[@]}"; do IFS=: read -r host td_port md_port <<<"$entry" if probe_tcp "$host" "$td_port" && probe_tcp "$host" "$md_port"; then echo "tcp://${host}:${td_port}|tcp://${host}:${md_port}" return 0 fi echo " SimNow 前置不可达: ${host} ${td_port}/${md_port}" >&2 done return 1 } update_simnow_front_in_env() { local env_file="$1" local picked td md picked="$(pick_simnow_front)" || { echo "警告: 未能探测到可用 SimNow 前置,请手动编辑 ${env_file}(见 docs/SIMNOW.md)" return 1 } td="${picked%%|*}" md="${picked##*|}" echo "==> SimNow 可用前置: TD=${td} MD=${md}" if grep -q "^SIMNOW_TD_ADDRESS=" "$env_file"; then sed -i "s|^SIMNOW_TD_ADDRESS=.*|SIMNOW_TD_ADDRESS=${td}|" "$env_file" sed -i "s|^SIMNOW_MD_ADDRESS=.*|SIMNOW_MD_ADDRESS=${md}|" "$env_file" else echo "SIMNOW_TD_ADDRESS=${td}" >>"$env_file" echo "SIMNOW_MD_ADDRESS=${md}" >>"$env_file" fi } echo "==> 检查系统依赖..." export DEBIAN_FRONTEND=noninteractive apt-get update -qq need_install() { if ! command -v "$1" &>/dev/null; then apt-get install -y "$2" fi } need_install python3 python3 need_install python3-venv python3-venv need_install git git echo "==> 安装 vnpy_ctp 编译依赖..." apt-get install -y build-essential python3-dev pkg-config locales netcat-openbsd echo "==> 配置时区 Asia/Shanghai..." if command -v timedatectl &>/dev/null; then timedatectl set-timezone Asia/Shanghai || true fi echo "==> 配置 CTP 所需 locale(zh_CN.GB18030 等)..." ensure_locale_gen "zh_CN.GB18030 GB18030" ensure_locale_gen "zh_CN.UTF-8 UTF-8" ensure_locale_gen "en_US.UTF-8 UTF-8" locale-gen zh_CN.GB18030 zh_CN.UTF-8 en_US.UTF-8 2>/dev/null || locale-gen update-locale LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8 2>/dev/null || true export LANG=zh_CN.UTF-8 export LC_ALL=zh_CN.UTF-8 if ! locale -a 2>/dev/null | grep -qi gb18030; then echo "错误: zh_CN.GB18030 未生成,CTP 连接后会崩溃" exit 1 fi echo " locale OK: $(locale -a 2>/dev/null | grep -i gb18030 | head -1)" if ! command -v pm2 &>/dev/null; then echo "==> 安装 PM2..." if ! command -v npm &>/dev/null; then need_install nodejs nodejs need_install npm npm fi npm install -g pm2 fi echo "==> 准备应用目录 ${APP_DIR}..." mkdir -p "$(dirname "$APP_DIR")" if [ -d "$APP_DIR/.git" ]; then echo "==> 更新已有仓库..." cd "$APP_DIR" git pull origin main || git pull origin master || true else if [ -d "$APP_DIR" ] && [ "$(ls -A "$APP_DIR" 2>/dev/null)" ]; then echo "目录 ${APP_DIR} 已存在且非 git 仓库,请手动处理后重试" exit 1 fi git clone "$REPO_URL" "$APP_DIR" cd "$APP_DIR" fi echo "==> Python 虚拟环境与依赖..." if [ ! -d "$APP_DIR/venv" ]; then python3 -m venv "$APP_DIR/venv" fi # shellcheck disable=SC1091 source "$APP_DIR/venv/bin/activate" pip install --upgrade pip -q pip install -r "$APP_DIR/requirements.txt" python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')" echo "==> 配置 .env..." if [ ! -f "$APP_DIR/.env" ]; then cp "$APP_DIR/.env.example" "$APP_DIR/.env" RAND_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") sed -i "s/change-this-to-a-random-secret-key/${RAND_KEY}/" "$APP_DIR/.env" echo " 已生成 .env,请编辑 SIMNOW_USER / ADMIN_PASSWORD" fi ensure_env_key "$APP_DIR/.env" "SIMNOW_ENV" "实盘" ensure_env_key "$APP_DIR/.env" "CTP_AUTO_RECONNECT" "true" ensure_env_key "$APP_DIR/.env" "SIMNOW_BROKER_ID" "9999" ensure_env_key "$APP_DIR/.env" "SIMNOW_APP_ID" "simnow_client_test" ensure_env_key "$APP_DIR/.env" "SIMNOW_AUTH_CODE" "0000000000000000" update_simnow_front_in_env "$APP_DIR/.env" || true mkdir -p "$APP_DIR/logs" echo "==> 验证 CTP 环境..." if grep -q "^SIMNOW_USER=.\+" "$APP_DIR/.env" 2>/dev/null && \ grep -q "^SIMNOW_PASSWORD=.\+" "$APP_DIR/.env" 2>/dev/null; then set +e python "$APP_DIR/scripts/test_simnow.py" CTP_TEST=$? set -e if [ "$CTP_TEST" -ne 0 ]; then echo "警告: SimNow 连接测试未通过,请检查 .env 账号与网络(见 docs/SIMNOW.md)" else echo " SimNow CTP 连接测试通过" fi else echo " 跳过 CTP 测试(未配置 SIMNOW_USER / SIMNOW_PASSWORD)" fi echo "==> PM2 启动/重启服务..." cd "$APP_DIR" if pm2 describe "$SERVICE_NAME" &>/dev/null; then pm2 restart ecosystem.config.cjs --update-env else pm2 start ecosystem.config.cjs fi pm2 save echo "" echo "==========================================" echo " 部署完成" echo " 目录: ${APP_DIR}" echo " 时区: $(date +%Z) $(date '+%Y-%m-%d %H:%M:%S')" echo " 端口: 6600" echo " 访问: http://$(hostname -I 2>/dev/null | awk '{print $1}'):6600" echo " 日志: pm2 logs ${SERVICE_NAME}" echo " SimNow 注册: docs/SIMNOW.md" echo " 开机自启: pm2 startup && pm2 save" echo "=========================================="