mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 12:00:59 +08:00
merge fix
This commit is contained in:
@@ -1,228 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Fail fast and normalize working directory
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# 内测码生成脚本
|
||||
# 生成6位不重复的内测码并写入 beta_codes.txt
|
||||
|
||||
BETA_CODES_FILE="beta_codes.txt"
|
||||
COUNT=1
|
||||
LIST_ONLY=false
|
||||
CODE_LENGTH=6
|
||||
|
||||
# 字符集(避免易混淆字符:0/O, 1/I/l)
|
||||
CHARSET="23456789abcdefghjkmnpqrstuvwxyz"
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
cat << EOF
|
||||
用法: $0 [选项]
|
||||
|
||||
选项:
|
||||
-c COUNT 生成内测码数量 (默认: 1)
|
||||
-l 列出现有内测码
|
||||
-f FILE 内测码文件路径 (默认: beta_codes.txt)
|
||||
-h 显示此帮助信息
|
||||
|
||||
示例:
|
||||
$0 -c 10 # 生成10个内测码
|
||||
$0 -l # 列出现有内测码
|
||||
$0 -f custom.txt -c 5 # 在自定义文件中生成5个内测码
|
||||
EOF
|
||||
}
|
||||
|
||||
# 生成随机内测码
|
||||
generate_beta_code() {
|
||||
local length="$1"
|
||||
local charset="$2"
|
||||
local code=""
|
||||
|
||||
for ((i=0; i<length; i++)); do
|
||||
local random_index=$((RANDOM % ${#charset}))
|
||||
code+="${charset:$random_index:1}"
|
||||
done
|
||||
|
||||
echo "$code"
|
||||
}
|
||||
|
||||
# 读取现有内测码
|
||||
read_existing_codes() {
|
||||
local file="$1"
|
||||
if [ -f "$file" ]; then
|
||||
grep -v '^$' "$file" 2>/dev/null | tr -d ' \t' | grep -v '^#' || true
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查内测码是否已存在
|
||||
code_exists() {
|
||||
local code="$1"
|
||||
local file="$2"
|
||||
if [ -f "$file" ]; then
|
||||
grep -Fxq "$code" "$file" 2>/dev/null
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 添加内测码到文件
|
||||
add_code_to_file() {
|
||||
local code="$1"
|
||||
local file="$2"
|
||||
echo "$code" >> "$file"
|
||||
}
|
||||
|
||||
# 验证内测码格式
|
||||
validate_code() {
|
||||
local code="$1"
|
||||
# 检查长度
|
||||
if [ ${#code} -ne $CODE_LENGTH ]; then
|
||||
return 1
|
||||
fi
|
||||
# 检查字符是否都在允许的字符集中
|
||||
if [[ ! "$code" =~ ^[$CHARSET]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# 去重并排序内测码
|
||||
dedupe_and_sort_codes() {
|
||||
local file="$1"
|
||||
if [ -f "$file" ]; then
|
||||
# 过滤空行和注释,去重并排序
|
||||
grep -v '^$' "$file" | grep -v '^#' | sort -u > "${file}.tmp" && mv "${file}.tmp" "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# 解析命令行参数
|
||||
while getopts "c:lf:h" opt; do
|
||||
case $opt in
|
||||
c)
|
||||
COUNT="$OPTARG"
|
||||
if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [ "$COUNT" -lt 1 ]; then
|
||||
echo "错误: count 必须是正整数" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
l)
|
||||
LIST_ONLY=true
|
||||
;;
|
||||
f)
|
||||
BETA_CODES_FILE="$OPTARG"
|
||||
;;
|
||||
h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
echo "无效选项: -$OPTARG" >&2
|
||||
echo "使用 -h 查看帮助信息" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 如果是列出现有内测码
|
||||
if [ "$LIST_ONLY" = true ]; then
|
||||
if [ -f "$BETA_CODES_FILE" ]; then
|
||||
existing_codes=$(read_existing_codes "$BETA_CODES_FILE")
|
||||
if [ -z "$existing_codes" ]; then
|
||||
echo "内测码列表为空"
|
||||
else
|
||||
count=$(echo "$existing_codes" | wc -l | tr -d ' ')
|
||||
echo "当前内测码 ($count 个):"
|
||||
echo "$existing_codes" | nl -w3 -s'. '
|
||||
fi
|
||||
else
|
||||
echo "内测码文件不存在: $BETA_CODES_FILE"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 读取现有内测码
|
||||
existing_codes=$(read_existing_codes "$BETA_CODES_FILE")
|
||||
|
||||
# 生成新内测码
|
||||
new_codes=()
|
||||
max_attempts=1000 # 防止无限循环
|
||||
|
||||
echo "正在生成 $COUNT 个内测码..."
|
||||
|
||||
for ((i=1; i<=COUNT; i++)); do
|
||||
attempts=0
|
||||
while [ $attempts -lt $max_attempts ]; do
|
||||
code=$(generate_beta_code $CODE_LENGTH "$CHARSET")
|
||||
|
||||
# 验证格式
|
||||
if ! validate_code "$code"; then
|
||||
((attempts++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查是否已存在
|
||||
if code_exists "$code" "$BETA_CODES_FILE"; then
|
||||
((attempts++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查是否与本次生成的重复
|
||||
duplicate=false
|
||||
for existing_code in "${new_codes[@]}"; do
|
||||
if [ "$code" = "$existing_code" ]; then
|
||||
duplicate=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$duplicate" = false ]; then
|
||||
new_codes+=("$code")
|
||||
break
|
||||
fi
|
||||
|
||||
((attempts++))
|
||||
done
|
||||
|
||||
if [ $attempts -eq $max_attempts ]; then
|
||||
echo "警告: 生成第 $i 个内测码时达到最大尝试次数,可能字符空间不足" >&2
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查是否成功生成了内测码
|
||||
if [ ${#new_codes[@]} -eq 0 ]; then
|
||||
echo "未能生成任何新的内测码"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 添加到文件
|
||||
for code in "${new_codes[@]}"; do
|
||||
add_code_to_file "$code" "$BETA_CODES_FILE"
|
||||
done
|
||||
|
||||
# 去重并排序
|
||||
dedupe_and_sort_codes "$BETA_CODES_FILE"
|
||||
|
||||
echo "成功生成 ${#new_codes[@]} 个内测码:"
|
||||
printf ' %s\n' "${new_codes[@]}"
|
||||
echo
|
||||
echo "内测码文件: $BETA_CODES_FILE"
|
||||
|
||||
# 显示当前总数
|
||||
if [ -f "$BETA_CODES_FILE" ]; then
|
||||
total_count=$(read_existing_codes "$BETA_CODES_FILE" | wc -l | tr -d ' ')
|
||||
echo "当前内测码总计: $total_count 个"
|
||||
fi
|
||||
|
||||
# 显示文件头部信息(如果是新文件)
|
||||
if [ ! -s "$BETA_CODES_FILE" ] || [ $(wc -l < "$BETA_CODES_FILE") -eq ${#new_codes[@]} ]; then
|
||||
echo
|
||||
echo "内测码规则:"
|
||||
echo "- 长度: $CODE_LENGTH 位"
|
||||
echo "- 字符集: 数字 2-9, 小写字母 a-z (排除 0,1,i,l,o 避免混淆)"
|
||||
echo "- 每个内测码唯一且不重复"
|
||||
fi
|
||||
@@ -1,76 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
keysDir := "keys"
|
||||
if err := os.MkdirAll(keysDir, 0700); err != nil {
|
||||
fmt.Printf("创建keys目录失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
privateKeyPath := filepath.Join(keysDir, "rsa_private.key")
|
||||
publicKeyPath := filepath.Join(keysDir, "rsa_private.key.pub")
|
||||
|
||||
if _, err := os.Stat(privateKeyPath); err == nil {
|
||||
fmt.Println("RSA密钥对已存在:")
|
||||
fmt.Printf(" 私钥: %s\n", privateKeyPath)
|
||||
fmt.Printf(" 公钥: %s\n", publicKeyPath)
|
||||
|
||||
publicKeyPEM, err := ioutil.ReadFile(publicKeyPath)
|
||||
if err == nil {
|
||||
fmt.Println("\n公钥内容:")
|
||||
fmt.Println(string(publicKeyPEM))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("生成新的RSA密钥对...")
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
fmt.Printf("生成RSA密钥失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
})
|
||||
|
||||
if err := ioutil.WriteFile(privateKeyPath, privateKeyPEM, 0600); err != nil {
|
||||
fmt.Printf("保存私钥失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
fmt.Printf("编码公钥失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyDER,
|
||||
})
|
||||
|
||||
if err := ioutil.WriteFile(publicKeyPath, publicKeyPEM, 0644); err != nil {
|
||||
fmt.Printf("保存公钥失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✓ RSA密钥对生成成功!")
|
||||
fmt.Printf(" 私钥: %s\n", privateKeyPath)
|
||||
fmt.Printf(" 公钥: %s\n", publicKeyPath)
|
||||
fmt.Println("\n公钥内容(可用于前端配置):")
|
||||
fmt.Println(string(publicKeyPEM))
|
||||
fmt.Println("\n注意: 请妥善保管私钥文件,不要提交到版本控制系统中!")
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
echo "🎟️ 导入 beta_codes.txt 到 PostgreSQL"
|
||||
|
||||
if [ ! -f "beta_codes.txt" ]; then
|
||||
echo "❌ 找不到 beta_codes.txt 文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v "docker-compose" &> /dev/null; then
|
||||
DOCKER_CMD="docker-compose"
|
||||
elif command -v "docker" &> /dev/null && docker compose version &> /dev/null; then
|
||||
DOCKER_CMD="docker compose"
|
||||
else
|
||||
echo "❌ 错误:找不到 docker-compose 或 docker compose 命令"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ENV_FILE=".env"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "📁 加载 .env 配置..."
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
else
|
||||
echo "⚠️ 未找到 .env 文件,使用默认数据库配置"
|
||||
fi
|
||||
|
||||
POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
||||
POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
POSTGRES_DB=${POSTGRES_DB:-nofx}
|
||||
POSTGRES_USER=${POSTGRES_USER:-nofx}
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
|
||||
POSTGRES_SERVICE=${POSTGRES_SERVICE:-postgres}
|
||||
POSTGRES_CONTAINER_NAME=${POSTGRES_CONTAINER_NAME:-nofx-postgres}
|
||||
|
||||
POSTGRES_CONTAINER=$($DOCKER_CMD ps -q "$POSTGRES_SERVICE" 2>/dev/null || true)
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
POSTGRES_CONTAINER=$(docker ps -q --filter "name=$POSTGRES_CONTAINER_NAME" | head -n 1)
|
||||
fi
|
||||
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
echo "❌ 找不到 PostgreSQL 容器 (${POSTGRES_SERVICE}/${POSTGRES_CONTAINER_NAME})"
|
||||
echo "💡 请确认数据库服务已启动"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PG_ENV_ARGS=()
|
||||
if [ -n "$POSTGRES_PASSWORD" ]; then
|
||||
PG_ENV_ARGS=(--env "PGPASSWORD=$POSTGRES_PASSWORD")
|
||||
fi
|
||||
|
||||
SQL_PAYLOAD=$(python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
codes = []
|
||||
for line in Path('beta_codes.txt').read_text(encoding='utf-8').splitlines():
|
||||
code = line.strip()
|
||||
if code and not code.startswith('#'):
|
||||
codes.append(f"('{code}')")
|
||||
|
||||
if codes:
|
||||
values = ",\n".join(codes)
|
||||
print(f"INSERT INTO beta_codes (code) VALUES\n{values}\nON CONFLICT (code) DO NOTHING;")
|
||||
PY
|
||||
)
|
||||
|
||||
if [ -z "$SQL_PAYLOAD" ]; then
|
||||
echo "⚠️ beta_codes.txt 中没有有效的内测码,已跳过导入"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TOTAL_CODES=$(grep -vc '^\s*$' beta_codes.txt || true)
|
||||
echo "📊 检测到 $TOTAL_CODES 条内测码记录"
|
||||
|
||||
echo "🔄 导入到数据库..."
|
||||
printf '%s\n' "$SQL_PAYLOAD" | docker exec -i "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" \
|
||||
psql -v ON_ERROR_STOP=1 --pset pager=off -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
||||
|
||||
echo "✅ 导入完成(重复的已跳过)"
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔧 同步默认用户与基础配置"
|
||||
echo "==============================="
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# 检测 Docker Compose 命令
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker-compose"
|
||||
elif docker compose version &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker compose"
|
||||
else
|
||||
echo "❌ 无法找到 docker-compose 或 docker compose 命令"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📋 使用命令: $DOCKER_COMPOSE_CMD"
|
||||
|
||||
# 加载 .env 配置
|
||||
ENV_FILE=".env"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "📁 加载 .env ..."
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
else
|
||||
echo "⚠️ 未找到 .env,使用默认数据库配置"
|
||||
fi
|
||||
|
||||
POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
||||
POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
POSTGRES_DB=${POSTGRES_DB:-nofx}
|
||||
POSTGRES_USER=${POSTGRES_USER:-nofx}
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
|
||||
POSTGRES_SERVICE=${POSTGRES_SERVICE:-postgres}
|
||||
POSTGRES_CONTAINER_NAME=${POSTGRES_CONTAINER_NAME:-nofx-postgres}
|
||||
|
||||
# 查找 PostgreSQL 容器
|
||||
POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q "$POSTGRES_SERVICE" 2>/dev/null || true)
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
POSTGRES_CONTAINER=$(docker ps -q --filter "name=$POSTGRES_CONTAINER_NAME" | head -n 1)
|
||||
fi
|
||||
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
echo "❌ 未找到 PostgreSQL 容器 (${POSTGRES_SERVICE}/${POSTGRES_CONTAINER_NAME})"
|
||||
echo "💡 请先启动数据库容器: $DOCKER_COMPOSE_CMD up -d postgres"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PG_ENV_ARGS=()
|
||||
if [ -n "$POSTGRES_PASSWORD" ]; then
|
||||
PG_ENV_ARGS=(-e "PGPASSWORD=$POSTGRES_PASSWORD")
|
||||
fi
|
||||
|
||||
echo "🔌 检查数据库连接..."
|
||||
if ! docker exec "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" > /dev/null 2>&1; then
|
||||
echo "❌ 无法连接到 PostgreSQL,请确认容器和凭据"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
read -p "确认写入默认账号和基础配置? (y/N): " confirm
|
||||
if [[ $confirm != [yY] ]]; then
|
||||
echo "ℹ️ 已取消操作"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🚀 执行初始化 SQL..."
|
||||
if docker exec -i "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" \
|
||||
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'SQL'
|
||||
-- 确保 traders 表存在 custom_coins 字段
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'traders' AND column_name = 'custom_coins'
|
||||
) THEN
|
||||
ALTER TABLE traders ADD COLUMN custom_coins TEXT DEFAULT '';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- 创建 default 用户
|
||||
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at)
|
||||
VALUES ('default', 'default@localhost', '', '', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET email = EXCLUDED.email,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
-- 默认 AI 模型配置
|
||||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES
|
||||
('deepseek', 'default', 'DeepSeek', 'deepseek', false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('qwen', 'default', 'Qwen', 'qwen', false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET user_id = EXCLUDED.user_id,
|
||||
name = EXCLUDED.name,
|
||||
provider = EXCLUDED.provider,
|
||||
enabled = EXCLUDED.enabled,
|
||||
api_key = EXCLUDED.api_key,
|
||||
custom_api_url = EXCLUDED.custom_api_url,
|
||||
custom_model_name = EXCLUDED.custom_model_name,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
-- 默认交易所配置
|
||||
INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet,
|
||||
hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key,
|
||||
created_at, updated_at) VALUES
|
||||
('binance', 'default', 'Binance Futures', 'binance', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('aster', 'default', 'Aster DEX', 'aster', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id, user_id) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
type = EXCLUDED.type,
|
||||
enabled = EXCLUDED.enabled,
|
||||
api_key = EXCLUDED.api_key,
|
||||
secret_key = EXCLUDED.secret_key,
|
||||
testnet = EXCLUDED.testnet,
|
||||
hyperliquid_wallet_addr = EXCLUDED.hyperliquid_wallet_addr,
|
||||
aster_user = EXCLUDED.aster_user,
|
||||
aster_signer = EXCLUDED.aster_signer,
|
||||
aster_private_key = EXCLUDED.aster_private_key,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
-- 默认系统配置(不存在时写入)
|
||||
INSERT INTO system_config (key, value) VALUES
|
||||
('beta_mode', 'false'),
|
||||
('api_server_port', '8080'),
|
||||
('use_default_coins', 'true'),
|
||||
('default_coins', '["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]'),
|
||||
('max_daily_loss', '10.0'),
|
||||
('max_drawdown', '20.0'),
|
||||
('stop_trading_minutes', '60'),
|
||||
('btc_eth_leverage', '5'),
|
||||
('altcoin_leverage', '5'),
|
||||
('jwt_secret', '')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- 输出校验信息
|
||||
SELECT 'default_user' AS item, COUNT(*) AS count FROM users WHERE id = 'default'
|
||||
UNION ALL
|
||||
SELECT 'default_ai_models', COUNT(*) FROM ai_models WHERE user_id = 'default'
|
||||
UNION ALL
|
||||
SELECT 'default_exchanges', COUNT(*) FROM exchanges WHERE user_id = 'default';
|
||||
SQL
|
||||
then
|
||||
echo
|
||||
echo "✅ 默认数据写入完成"
|
||||
else
|
||||
echo
|
||||
echo "❌ 数据写入失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 操作完成"
|
||||
@@ -1,367 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/crypto"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
privateKeyPath := flag.String("key", "keys/rsa_private.key", "RSA 私钥路径")
|
||||
dryRun := flag.Bool("dry-run", false, "仅检查需要迁移的数据,不写入数据库")
|
||||
flag.Parse()
|
||||
|
||||
// 尝试加载 .env 文件(从项目根目录运行时)
|
||||
envPaths := []string{
|
||||
".env", // 项目根目录
|
||||
}
|
||||
envLoaded := false
|
||||
for _, envPath := range envPaths {
|
||||
if err := loadEnvFile(envPath); err == nil {
|
||||
log.Printf("成功加载 .env 文件: %s", envPath)
|
||||
envLoaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !envLoaded {
|
||||
log.Printf("警告: 未找到 .env 文件,请确保在项目根目录存在 .env 文件")
|
||||
log.Printf("尝试的路径: %v", envPaths)
|
||||
}
|
||||
|
||||
// 确保环境变量已设置
|
||||
if os.Getenv("DATA_ENCRYPTION_KEY") == "" {
|
||||
log.Fatalf("迁移失败: DATA_ENCRYPTION_KEY 环境变量未设置")
|
||||
}
|
||||
|
||||
if err := run(*privateKeyPath, *dryRun); err != nil {
|
||||
log.Fatalf("迁移失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(privateKeyPath string, dryRun bool) error {
|
||||
log.SetFlags(0)
|
||||
|
||||
// 尝试多个可能的私钥路径(从项目根目录运行时)
|
||||
keyPaths := []string{
|
||||
privateKeyPath, // 用户指定的路径
|
||||
"keys/rsa_private.key", // 项目根目录的 keys 文件夹
|
||||
}
|
||||
|
||||
var finalKeyPath string
|
||||
for _, path := range keyPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
finalKeyPath = path
|
||||
log.Printf("找到私钥文件: %s", path)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if finalKeyPath == "" {
|
||||
finalKeyPath = privateKeyPath // 使用默认路径,让 crypto 服务生成新密钥
|
||||
log.Printf("警告: 私钥文件不存在,将使用路径: %s, 系统将尝试生成新密钥", finalKeyPath)
|
||||
}
|
||||
|
||||
cryptoService, err := crypto.NewCryptoService(finalKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化加密服务失败: %w", err)
|
||||
}
|
||||
|
||||
db, err := openPostgres()
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接数据库失败: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
log.Printf("开始迁移 AI 模型密钥 (dry-run=%v)", dryRun)
|
||||
if err := migrateAIModels(db, cryptoService, dryRun); err != nil {
|
||||
return fmt.Errorf("迁移 AI 模型失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("开始迁移交易所密钥 (dry-run=%v)", dryRun)
|
||||
if err := migrateExchanges(db, cryptoService, dryRun); err != nil {
|
||||
return fmt.Errorf("迁移交易所失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 敏感数据迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
func openPostgres() (*sql.DB, error) {
|
||||
host := getEnv("POSTGRES_HOST", "localhost")
|
||||
// 如果是 Docker 服务名,替换为 localhost
|
||||
if host == "postgres" {
|
||||
host = "localhost"
|
||||
}
|
||||
port := getEnv("POSTGRES_PORT", "5432")
|
||||
dbname := getEnv("POSTGRES_DB", "nofx")
|
||||
user := getEnv("POSTGRES_USER", "nofx")
|
||||
password := getEnv("POSTGRES_PASSWORD", "nofx123456")
|
||||
|
||||
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, user, password, dbname)
|
||||
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(5)
|
||||
db.SetMaxIdleConns(2)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateAIModels(db *sql.DB, cryptoService *crypto.CryptoService, dryRun bool) error {
|
||||
type record struct {
|
||||
ID string
|
||||
UserID string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id, user_id, COALESCE(api_key, '')
|
||||
FROM ai_models
|
||||
WHERE COALESCE(deleted, FALSE) = FALSE
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []record
|
||||
for rows.Next() {
|
||||
var r record
|
||||
if err := rows.Scan(&r.ID, &r.UserID, &r.APIKey); err != nil {
|
||||
return err
|
||||
}
|
||||
records = append(records, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var updated int
|
||||
for _, r := range records {
|
||||
if r.APIKey == "" || cryptoService.IsEncryptedStorageValue(r.APIKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
encrypted, err := cryptoService.EncryptForStorage(r.APIKey, r.UserID, r.ID, "api_key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密 AI 模型 %s (%s) 失败: %w", r.ID, r.UserID, err)
|
||||
}
|
||||
|
||||
updated++
|
||||
if dryRun {
|
||||
log.Printf("[DRY-RUN] AI 模型 %s (%s) 将被加密", r.ID, r.UserID)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := db.Exec(`
|
||||
UPDATE ai_models
|
||||
SET api_key = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2 AND user_id = $3
|
||||
`, encrypted, r.ID, r.UserID); err != nil {
|
||||
return fmt.Errorf("更新 AI 模型 %s (%s) 失败: %w", r.ID, r.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("AI 模型处理完成,需更新 %d 条记录", updated)
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateExchanges(db *sql.DB, cryptoService *crypto.CryptoService, dryRun bool) error {
|
||||
type record struct {
|
||||
ID string
|
||||
UserID string
|
||||
APIKey string
|
||||
SecretKey string
|
||||
HyperliquidWallet string
|
||||
AsterUser string
|
||||
AsterSigner string
|
||||
AsterPrivateKey string
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id, user_id,
|
||||
COALESCE(api_key, '') AS api_key,
|
||||
COALESCE(secret_key, '') AS secret_key,
|
||||
COALESCE(hyperliquid_wallet_addr, '') AS hyperliquid_wallet_addr,
|
||||
COALESCE(aster_user, '') AS aster_user,
|
||||
COALESCE(aster_signer, '') AS aster_signer,
|
||||
COALESCE(aster_private_key, '') AS aster_private_key
|
||||
FROM exchanges
|
||||
WHERE COALESCE(deleted, FALSE) = FALSE
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []record
|
||||
for rows.Next() {
|
||||
var r record
|
||||
if err := rows.Scan(
|
||||
&r.ID, &r.UserID,
|
||||
&r.APIKey, &r.SecretKey,
|
||||
&r.HyperliquidWallet,
|
||||
&r.AsterUser, &r.AsterSigner, &r.AsterPrivateKey,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
records = append(records, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var updated int
|
||||
for _, r := range records {
|
||||
newAPIKey := r.APIKey
|
||||
newSecretKey := r.SecretKey
|
||||
newHyper := r.HyperliquidWallet
|
||||
newAsterUser := r.AsterUser
|
||||
newAsterSigner := r.AsterSigner
|
||||
newAsterPrivate := r.AsterPrivateKey
|
||||
|
||||
changed := false
|
||||
|
||||
if r.APIKey != "" && !cryptoService.IsEncryptedStorageValue(r.APIKey) {
|
||||
enc, err := cryptoService.EncryptForStorage(r.APIKey, r.UserID, r.ID, "api_key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密交易所 API Key 失败: %s (%s): %w", r.ID, r.UserID, err)
|
||||
}
|
||||
newAPIKey = enc
|
||||
changed = true
|
||||
}
|
||||
if r.SecretKey != "" && !cryptoService.IsEncryptedStorageValue(r.SecretKey) {
|
||||
enc, err := cryptoService.EncryptForStorage(r.SecretKey, r.UserID, r.ID, "secret_key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密交易所 Secret Key 失败: %s (%s): %w", r.ID, r.UserID, err)
|
||||
}
|
||||
newSecretKey = enc
|
||||
changed = true
|
||||
}
|
||||
if r.HyperliquidWallet != "" && !cryptoService.IsEncryptedStorageValue(r.HyperliquidWallet) {
|
||||
enc, err := cryptoService.EncryptForStorage(r.HyperliquidWallet, r.UserID, r.ID, "hyperliquid_wallet_addr")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密 Hyperliquid 地址失败: %s (%s): %w", r.ID, r.UserID, err)
|
||||
}
|
||||
newHyper = enc
|
||||
changed = true
|
||||
}
|
||||
if r.AsterUser != "" && !cryptoService.IsEncryptedStorageValue(r.AsterUser) {
|
||||
enc, err := cryptoService.EncryptForStorage(r.AsterUser, r.UserID, r.ID, "aster_user")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密 Aster 用户失败: %s (%s): %w", r.ID, r.UserID, err)
|
||||
}
|
||||
newAsterUser = enc
|
||||
changed = true
|
||||
}
|
||||
if r.AsterSigner != "" && !cryptoService.IsEncryptedStorageValue(r.AsterSigner) {
|
||||
enc, err := cryptoService.EncryptForStorage(r.AsterSigner, r.UserID, r.ID, "aster_signer")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密 Aster Signer 失败: %s (%s): %w", r.ID, r.UserID, err)
|
||||
}
|
||||
newAsterSigner = enc
|
||||
changed = true
|
||||
}
|
||||
if r.AsterPrivateKey != "" && !cryptoService.IsEncryptedStorageValue(r.AsterPrivateKey) {
|
||||
enc, err := cryptoService.EncryptForStorage(r.AsterPrivateKey, r.UserID, r.ID, "aster_private_key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密 Aster 私钥失败: %s (%s): %w", r.ID, r.UserID, err)
|
||||
}
|
||||
newAsterPrivate = enc
|
||||
changed = true
|
||||
}
|
||||
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
updated++
|
||||
if dryRun {
|
||||
log.Printf("[DRY-RUN] 交易所 %s (%s) 将被加密", r.ID, r.UserID)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := db.Exec(`
|
||||
UPDATE exchanges
|
||||
SET api_key = $1,
|
||||
secret_key = $2,
|
||||
hyperliquid_wallet_addr = $3,
|
||||
aster_user = $4,
|
||||
aster_signer = $5,
|
||||
aster_private_key = $6,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7 AND user_id = $8
|
||||
`, newAPIKey, newSecretKey, newHyper, newAsterUser, newAsterSigner, newAsterPrivate, r.ID, r.UserID); err != nil {
|
||||
return fmt.Errorf("更新交易所 %s (%s) 失败: %w", r.ID, r.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("交易所处理完成,需更新 %d 条记录", updated)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func loadEnvFile(filename string) error {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
return fmt.Errorf("文件不存在: %s", filename)
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法打开文件: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 逐行读取
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// 跳过空行和注释行
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析 KEY=VALUE 格式
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
// 只有当环境变量不存在时才设置
|
||||
if os.Getenv(key) == "" {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
476
scripts/start.sh
476
scripts/start.sh
@@ -1,476 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# NOFX AI Trading System - Docker Quick Start Script
|
||||
# Usage: ./start.sh [command]
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
set -e
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Color Definitions
|
||||
# ------------------------------------------------------------------------
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Utility Functions: Colored Output
|
||||
# ------------------------------------------------------------------------
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Detection: Docker Compose Command (Backward Compatible)
|
||||
# ------------------------------------------------------------------------
|
||||
detect_compose_cmd() {
|
||||
if command -v docker compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
print_error "Docker Compose 未安装!请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
print_info "使用 Docker Compose 命令: $COMPOSE_CMD"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Docker Installation
|
||||
# ------------------------------------------------------------------------
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker 未安装!请先安装 Docker: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_compose_cmd
|
||||
print_success "Docker 和 Docker Compose 已安装"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Environment File (.env)
|
||||
# ------------------------------------------------------------------------
|
||||
check_env() {
|
||||
if [ ! -f ".env" ]; then
|
||||
print_warning ".env 不存在,从模板复制..."
|
||||
cp .env.example .env
|
||||
print_info "✓ 已使用默认环境变量创建 .env"
|
||||
print_info "💡 如需修改端口等设置,可编辑 .env 文件"
|
||||
fi
|
||||
print_success "环境变量文件存在"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Encryption Environment (RSA Keys + Data Encryption Key)
|
||||
# ------------------------------------------------------------------------
|
||||
check_encryption() {
|
||||
local need_setup=false
|
||||
|
||||
print_info "检查加密环境..."
|
||||
|
||||
# 检查RSA密钥对
|
||||
if [ ! -f "secrets/rsa_key" ] || [ ! -f "secrets/rsa_key.pub" ]; then
|
||||
print_warning "RSA密钥对不存在"
|
||||
need_setup=true
|
||||
fi
|
||||
|
||||
# 检查数据加密密钥
|
||||
if [ ! -f ".env" ] || ! grep -q "^DATA_ENCRYPTION_KEY=" .env; then
|
||||
print_warning "数据加密密钥未配置"
|
||||
need_setup=true
|
||||
fi
|
||||
|
||||
# 检查JWT认证密钥
|
||||
if [ ! -f ".env" ] || ! grep -q "^JWT_SECRET=" .env; then
|
||||
print_warning "JWT认证密钥未配置"
|
||||
need_setup=true
|
||||
fi
|
||||
|
||||
# 如果需要设置加密环境,直接自动设置
|
||||
if [ "$need_setup" = "true" ]; then
|
||||
print_info "🔐 检测到加密环境未配置,正在自动设置..."
|
||||
print_info "加密环境用于保护敏感数据(API密钥、私钥等)"
|
||||
echo ""
|
||||
|
||||
# 检查加密设置脚本是否存在
|
||||
if [ -f "scripts/setup_encryption.sh" ]; then
|
||||
print_info "加密系统将保护: API密钥、私钥、Hyperliquid代理钱包"
|
||||
echo ""
|
||||
|
||||
# 自动运行加密设置脚本
|
||||
echo -e "Y\nn\nn" | bash scripts/setup_encryption.sh
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
print_success "🔐 加密环境设置完成!"
|
||||
print_info " • RSA-2048密钥对已生成"
|
||||
print_info " • AES-256数据加密密钥已配置"
|
||||
print_info " • JWT认证密钥已配置"
|
||||
print_info " • 所有敏感数据现在都受加密保护"
|
||||
echo ""
|
||||
else
|
||||
print_error "加密环境设置失败"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "加密设置脚本不存在: scripts/setup_encryption.sh"
|
||||
print_info "请手动运行: ./scripts/setup_encryption.sh"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_success "🔐 加密环境已配置"
|
||||
print_info " • RSA密钥对: secrets/rsa_key + secrets/rsa_key.pub"
|
||||
print_info " • 数据加密密钥: .env (DATA_ENCRYPTION_KEY)"
|
||||
print_info " • JWT认证密钥: .env (JWT_SECRET)"
|
||||
print_info " • 加密算法: RSA-OAEP-2048 + AES-256-GCM + HS256"
|
||||
print_info " • 保护数据: API密钥、私钥、Hyperliquid代理钱包、用户认证"
|
||||
|
||||
# 验证密钥文件权限
|
||||
if [ -f "secrets/rsa_key" ]; then
|
||||
local perm=$(stat -f "%A" "secrets/rsa_key" 2>/dev/null || stat -c "%a" "secrets/rsa_key" 2>/dev/null)
|
||||
if [ "$perm" != "600" ]; then
|
||||
print_warning "修复RSA私钥权限..."
|
||||
chmod 600 secrets/rsa_key
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f ".env" ]; then
|
||||
local perm=$(stat -f "%A" ".env" 2>/dev/null || stat -c "%a" ".env" 2>/dev/null)
|
||||
if [ "$perm" != "600" ]; then
|
||||
print_warning "修复环境文件权限..."
|
||||
chmod 600 .env
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Configuration File (config.json) - BASIC SETTINGS ONLY
|
||||
# ------------------------------------------------------------------------
|
||||
check_config() {
|
||||
if [ ! -f "config.json" ]; then
|
||||
print_warning "config.json 不存在,从模板复制..."
|
||||
cp config.json.example config.json
|
||||
print_info "✓ 已使用默认配置创建 config.json"
|
||||
print_info "💡 如需修改基础设置(杠杆大小、开仓币种、管理员模式、JWT密钥等),可编辑 config.json"
|
||||
print_info "💡 模型/交易所/交易员配置请使用Web界面"
|
||||
fi
|
||||
print_success "配置文件存在"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Beta Code File (beta_codes.txt)
|
||||
# ------------------------------------------------------------------------
|
||||
check_beta_codes_file() {
|
||||
local beta_file="beta_codes.txt"
|
||||
|
||||
if [ -d "$beta_file" ]; then
|
||||
print_warning "beta_codes.txt 是目录,正在删除后重建文件..."
|
||||
rm -rf "$beta_file"
|
||||
touch "$beta_file"
|
||||
chmod 600 "$beta_file"
|
||||
print_info "✓ 已重新创建 beta_codes.txt(权限: 600)"
|
||||
elif [ ! -f "$beta_file" ]; then
|
||||
print_warning "beta_codes.txt 不存在,正在创建空文件..."
|
||||
touch "$beta_file"
|
||||
chmod 600 "$beta_file"
|
||||
print_info "✓ 已创建空的内测码文件(权限: 600)"
|
||||
else
|
||||
print_success "内测码文件存在"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Utility: Read Environment Variables
|
||||
# ------------------------------------------------------------------------
|
||||
read_env_vars() {
|
||||
if [ -f ".env" ]; then
|
||||
# 读取端口配置,设置默认值
|
||||
NOFX_FRONTEND_PORT=$(grep "^NOFX_FRONTEND_PORT=" .env 2>/dev/null | cut -d'=' -f2 || echo "3000")
|
||||
NOFX_BACKEND_PORT=$(grep "^NOFX_BACKEND_PORT=" .env 2>/dev/null | cut -d'=' -f2 || echo "8080")
|
||||
|
||||
# 去除可能的引号和空格
|
||||
NOFX_FRONTEND_PORT=$(echo "$NOFX_FRONTEND_PORT" | tr -d '"'"'" | tr -d ' ')
|
||||
NOFX_BACKEND_PORT=$(echo "$NOFX_BACKEND_PORT" | tr -d '"'"'" | tr -d ' ')
|
||||
|
||||
# 如果为空则使用默认值
|
||||
NOFX_FRONTEND_PORT=${NOFX_FRONTEND_PORT:-3000}
|
||||
NOFX_BACKEND_PORT=${NOFX_BACKEND_PORT:-8080}
|
||||
else
|
||||
# 如果.env不存在,使用默认端口
|
||||
NOFX_FRONTEND_PORT=3000
|
||||
NOFX_BACKEND_PORT=8080
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Database File (config.db)
|
||||
# ------------------------------------------------------------------------
|
||||
check_database() {
|
||||
if [ -d "config.db" ]; then
|
||||
# 如果存在的是目录,删除它
|
||||
print_warning "config.db 是目录而非文件,正在删除目录..."
|
||||
rm -rf config.db
|
||||
print_info "✓ 已删除目录,现在创建文件..."
|
||||
install -m 600 /dev/null config.db
|
||||
print_success "✓ 已创建空数据库文件(权限: 600),系统将在启动时初始化"
|
||||
elif [ ! -f "config.db" ]; then
|
||||
# 如果不存在文件,创建它
|
||||
print_warning "数据库文件不存在,创建空数据库文件..."
|
||||
# 创建空文件以避免Docker创建目录(使用安全权限600)
|
||||
install -m 600 /dev/null config.db
|
||||
print_info "✓ 已创建空数据库文件(权限: 600),系统将在启动时初始化"
|
||||
else
|
||||
# 文件存在
|
||||
print_success "数据库文件存在"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Build: Frontend (Node.js Based)
|
||||
# ------------------------------------------------------------------------
|
||||
# build_frontend() {
|
||||
# print_info "检查前端构建环境..."
|
||||
|
||||
# if ! command -v node &> /dev/null; then
|
||||
# print_error "Node.js 未安装!请先安装 Node.js"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# if ! command -v npm &> /dev/null; then
|
||||
# print_error "npm 未安装!请先安装 npm"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# print_info "正在构建前端..."
|
||||
# cd web
|
||||
|
||||
# print_info "安装 Node.js 依赖..."
|
||||
# npm install
|
||||
|
||||
# print_info "构建前端应用..."
|
||||
# npm run build
|
||||
|
||||
# cd ..
|
||||
# print_success "前端构建完成"
|
||||
# }
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Service Management: Start
|
||||
# ------------------------------------------------------------------------
|
||||
start() {
|
||||
print_info "正在启动 NOFX AI Trading System..."
|
||||
|
||||
# 读取环境变量
|
||||
read_env_vars
|
||||
|
||||
# 确保必要的文件和目录存在(修复 Docker volume 挂载问题)
|
||||
if [ ! -f "config.db" ]; then
|
||||
print_info "创建数据库文件..."
|
||||
touch config.db
|
||||
install -m 600 /dev/null config.db
|
||||
fi
|
||||
if [ ! -d "decision_logs" ]; then
|
||||
print_info "创建日志目录..."
|
||||
install -m 700 -d decision_logs
|
||||
fi
|
||||
|
||||
# Auto-build frontend if missing or forced
|
||||
# if [ ! -d "web/dist" ] || [ "$1" == "--build" ]; then
|
||||
# build_frontend
|
||||
# fi
|
||||
|
||||
# Rebuild images if flag set
|
||||
if [ "$1" == "--build" ]; then
|
||||
print_info "重新构建镜像..."
|
||||
$COMPOSE_CMD up -d --build
|
||||
else
|
||||
print_info "启动容器..."
|
||||
$COMPOSE_CMD up -d
|
||||
fi
|
||||
|
||||
print_success "服务已启动!"
|
||||
print_info "Web 界面: http://localhost:${NOFX_FRONTEND_PORT}"
|
||||
print_info "API 端点: http://localhost:${NOFX_BACKEND_PORT}"
|
||||
print_info ""
|
||||
print_info "查看日志: ./start.sh logs"
|
||||
print_info "停止服务: ./start.sh stop"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Service Management: Stop
|
||||
# ------------------------------------------------------------------------
|
||||
stop() {
|
||||
print_info "正在停止服务..."
|
||||
$COMPOSE_CMD stop
|
||||
print_success "服务已停止"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Service Management: Restart
|
||||
# ------------------------------------------------------------------------
|
||||
restart() {
|
||||
print_info "正在重启服务..."
|
||||
$COMPOSE_CMD restart
|
||||
print_success "服务已重启"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Monitoring: Logs
|
||||
# ------------------------------------------------------------------------
|
||||
logs() {
|
||||
if [ -z "$2" ]; then
|
||||
$COMPOSE_CMD logs -f
|
||||
else
|
||||
$COMPOSE_CMD logs -f "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Monitoring: Status
|
||||
# ------------------------------------------------------------------------
|
||||
status() {
|
||||
# 读取环境变量
|
||||
read_env_vars
|
||||
|
||||
print_info "服务状态:"
|
||||
$COMPOSE_CMD ps
|
||||
echo ""
|
||||
print_info "健康检查:"
|
||||
curl -s "http://localhost:${NOFX_BACKEND_PORT}/api/health" | jq '.' || echo "后端未响应"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Maintenance: Clean (Destructive)
|
||||
# ------------------------------------------------------------------------
|
||||
clean() {
|
||||
print_warning "这将删除所有容器和数据!"
|
||||
read -p "确认删除?(yes/no): " confirm
|
||||
if [ "$confirm" == "yes" ]; then
|
||||
print_info "正在清理..."
|
||||
$COMPOSE_CMD down -v
|
||||
print_success "清理完成"
|
||||
else
|
||||
print_info "已取消"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Maintenance: Update
|
||||
# ------------------------------------------------------------------------
|
||||
update() {
|
||||
print_info "正在更新..."
|
||||
git pull
|
||||
$COMPOSE_CMD up -d --build
|
||||
print_success "更新完成"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Encryption: Manual Setup
|
||||
# ------------------------------------------------------------------------
|
||||
setup_encryption_manual() {
|
||||
print_info "🔐 手动设置加密环境"
|
||||
|
||||
if [ -f "scripts/setup_encryption.sh" ]; then
|
||||
bash scripts/setup_encryption.sh
|
||||
else
|
||||
print_error "加密设置脚本不存在: scripts/setup_encryption.sh"
|
||||
print_info "请确保项目文件完整"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Help: Usage Information
|
||||
# ------------------------------------------------------------------------
|
||||
show_help() {
|
||||
echo "NOFX AI Trading System - Docker 管理脚本"
|
||||
echo ""
|
||||
echo "用法: ./start.sh [command] [options]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " start [--build] 启动服务(可选:重新构建)"
|
||||
echo " stop 停止服务"
|
||||
echo " restart 重启服务"
|
||||
echo " logs [service] 查看日志(可选:指定服务名 backend/frontend)"
|
||||
echo " status 查看服务状态"
|
||||
echo " clean 清理所有容器和数据"
|
||||
echo " update 更新代码并重启"
|
||||
echo " setup-encryption 设置加密环境(RSA密钥+数据加密)"
|
||||
echo " help 显示此帮助信息"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " ./start.sh start --build # 构建并启动"
|
||||
echo " ./start.sh logs backend # 查看后端日志"
|
||||
echo " ./start.sh status # 查看状态"
|
||||
echo " ./start.sh setup-encryption # 手动设置加密环境"
|
||||
echo ""
|
||||
echo "🔐 关于加密:"
|
||||
echo " 系统自动检测加密环境,首次运行时会自动设置"
|
||||
echo " 手动设置: ./scripts/setup_encryption.sh"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Main: Command Dispatcher
|
||||
# ------------------------------------------------------------------------
|
||||
main() {
|
||||
check_docker
|
||||
|
||||
case "${1:-start}" in
|
||||
start)
|
||||
check_env
|
||||
check_encryption
|
||||
check_config
|
||||
check_beta_codes_file
|
||||
check_database
|
||||
start "$2"
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
logs)
|
||||
logs "$@"
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
clean)
|
||||
clean
|
||||
;;
|
||||
update)
|
||||
update
|
||||
;;
|
||||
setup-encryption)
|
||||
setup_encryption_manual
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
print_error "未知命令: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Execute Main
|
||||
main "$@"
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 保证从仓库根目录运行
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# PostgreSQL数据查看工具
|
||||
echo "🔍 PostgreSQL 数据查看工具"
|
||||
echo "=========================="
|
||||
|
||||
# 检测Docker Compose命令
|
||||
DOCKER_COMPOSE_CMD=""
|
||||
if command -v "docker-compose" &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker-compose"
|
||||
elif command -v "docker" &> /dev/null && docker compose version &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker compose"
|
||||
else
|
||||
echo "❌ 错误:找不到 docker-compose 或 docker compose 命令"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 加载数据库配置
|
||||
ENV_FILE=".env"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "📁 加载 .env 配置..."
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
else
|
||||
echo "⚠️ 未找到 .env 文件,使用默认数据库配置"
|
||||
fi
|
||||
|
||||
POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
||||
POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
POSTGRES_DB=${POSTGRES_DB:-nofx}
|
||||
POSTGRES_USER=${POSTGRES_USER:-nofx}
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
|
||||
POSTGRES_SERVICE=${POSTGRES_SERVICE:-postgres}
|
||||
POSTGRES_CONTAINER_NAME=${POSTGRES_CONTAINER_NAME:-nofx-postgres}
|
||||
|
||||
# 获取 PostgreSQL 容器 ID
|
||||
POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q "$POSTGRES_SERVICE" 2>/dev/null || true)
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
POSTGRES_CONTAINER=$(docker ps -q --filter "name=$POSTGRES_CONTAINER_NAME" | head -n 1)
|
||||
fi
|
||||
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
echo "❌ 找不到 PostgreSQL 容器 (${POSTGRES_SERVICE}/${POSTGRES_CONTAINER_NAME})"
|
||||
echo "💡 请确认数据库服务已启动"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PG_ENV_ARGS=()
|
||||
if [ -n "$POSTGRES_PASSWORD" ]; then
|
||||
PG_ENV_ARGS=(--env "PGPASSWORD=$POSTGRES_PASSWORD")
|
||||
fi
|
||||
|
||||
run_psql() {
|
||||
local sql="$1"
|
||||
docker exec -i "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" \
|
||||
psql -v ON_ERROR_STOP=1 --pset pager=off -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "$sql"
|
||||
}
|
||||
|
||||
echo "📋 数据库容器: $POSTGRES_CONTAINER"
|
||||
echo "📋 连接参数: $POSTGRES_HOST:${POSTGRES_PORT}/$POSTGRES_DB (user: $POSTGRES_USER)"
|
||||
|
||||
echo "📊 数据库概览:"
|
||||
run_psql "SELECT relname AS \"表名\", n_live_tup AS \"记录数\" FROM pg_stat_user_tables WHERE n_live_tup > 0 ORDER BY relname;"
|
||||
|
||||
echo -e "\n🤖 AI模型配置:"
|
||||
run_psql "SELECT id, name, provider, enabled, CASE WHEN api_key != '' THEN '已配置' ELSE '未配置' END AS api_key_status FROM ai_models ORDER BY id;"
|
||||
|
||||
echo -e "\n🏢 交易所配置:"
|
||||
run_psql "SELECT id, name, type, enabled, CASE WHEN api_key != '' THEN '已配置' ELSE '未配置' END AS api_key_status FROM exchanges ORDER BY id;"
|
||||
|
||||
echo -e "\n⚙️ 关键系统配置:"
|
||||
run_psql "SELECT key, CASE WHEN LENGTH(value) > 50 THEN LEFT(value, 50) || '...' ELSE value END AS value FROM system_config WHERE key IN ('beta_mode', 'api_server_port', 'default_coins', 'jwt_secret') ORDER BY key;"
|
||||
|
||||
echo -e "\n🎟️ 内测码统计:"
|
||||
run_psql "SELECT CASE WHEN used THEN '已使用' ELSE '未使用' END AS status, COUNT(*) AS count FROM beta_codes GROUP BY used ORDER BY used;"
|
||||
|
||||
echo -e "\n👥 用户信息:"
|
||||
run_psql "SELECT id, email, otp_verified, created_at FROM users ORDER BY created_at;"
|
||||
Reference in New Issue
Block a user