mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2025-12-06 13:54:41 +08:00
refactor: drop sqlite fallback and admin mode
This commit is contained in:
@@ -180,7 +180,6 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) {
|
||||
betaMode := betaModeStr == "true"
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"admin_mode": auth.IsAdminMode(),
|
||||
"beta_mode": betaMode,
|
||||
"default_coins": defaultCoins,
|
||||
"btc_eth_leverage": btcEthLeverage,
|
||||
@@ -1459,14 +1458,6 @@ func (s *Server) handlePerformance(c *gin.Context) {
|
||||
// authMiddleware JWT认证中间件
|
||||
func (s *Server) authMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 如果是管理员模式,直接使用admin用户
|
||||
if auth.IsAdminMode() {
|
||||
c.Set("user_id", "admin")
|
||||
c.Set("email", "admin@localhost")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"})
|
||||
|
||||
13
auth/auth.go
13
auth/auth.go
@@ -14,9 +14,6 @@ import (
|
||||
// JWTSecret JWT密钥,将从配置中动态设置
|
||||
var JWTSecret []byte
|
||||
|
||||
// AdminMode 管理员模式标志
|
||||
var AdminMode bool = false
|
||||
|
||||
// OTPIssuer OTP发行者名称
|
||||
const OTPIssuer = "nofxAI"
|
||||
|
||||
@@ -25,16 +22,6 @@ func SetJWTSecret(secret string) {
|
||||
JWTSecret = []byte(secret)
|
||||
}
|
||||
|
||||
// SetAdminMode 设置管理员模式
|
||||
func SetAdminMode(enabled bool) {
|
||||
AdminMode = enabled
|
||||
}
|
||||
|
||||
// IsAdminMode 检查是否为管理员模式
|
||||
func IsAdminMode() bool {
|
||||
return AdminMode
|
||||
}
|
||||
|
||||
// Claims JWT声明
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"admin_mode": true,
|
||||
"beta_mode": false,
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
@@ -24,4 +23,4 @@
|
||||
"log": {
|
||||
"level": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1117
config/database.go
1117
config/database.go
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,11 @@ func NewPostgreSQLDatabase() (*PostgreSQLDatabase, error) {
|
||||
database := &PostgreSQLDatabase{db: db}
|
||||
log.Printf("✅ PostgreSQL数据库连接成功")
|
||||
|
||||
// 初始化默认数据
|
||||
if err := database.initDefaultData(); err != nil {
|
||||
return nil, fmt.Errorf("初始化默认数据失败: %w", err)
|
||||
}
|
||||
|
||||
return database, nil
|
||||
}
|
||||
|
||||
@@ -72,32 +77,6 @@ func (d *PostgreSQLDatabase) CreateUser(user *User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// EnsureAdminUser 确保admin用户存在(用于管理员模式)
|
||||
func (d *PostgreSQLDatabase) EnsureAdminUser() error {
|
||||
// 检查admin用户是否已存在
|
||||
var count int
|
||||
err := d.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果已存在,直接返回
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建admin用户(密码为空,因为管理员模式下不需要密码)
|
||||
adminUser := &User{
|
||||
ID: "admin",
|
||||
Email: "admin@localhost",
|
||||
PasswordHash: "", // 管理员模式下不使用密码
|
||||
OTPSecret: "",
|
||||
OTPVerified: true,
|
||||
}
|
||||
|
||||
return d.CreateUser(adminUser)
|
||||
}
|
||||
|
||||
// GetUserByEmail 通过邮箱获取用户
|
||||
func (d *PostgreSQLDatabase) GetUserByEmail(email string) (*User, error) {
|
||||
var user User
|
||||
@@ -701,7 +680,67 @@ func (d *PostgreSQLDatabase) GetBetaCodeStats() (total, used int, err error) {
|
||||
return total, used, nil
|
||||
}
|
||||
|
||||
// initDefaultData 初始化默认数据(AI模型和交易所)
|
||||
func (d *PostgreSQLDatabase) initDefaultData() error {
|
||||
// 确保traders表存在custom_coins列,防止旧环境缺少字段
|
||||
if _, err := d.db.Exec(`ALTER TABLE traders ADD COLUMN IF NOT EXISTS custom_coins TEXT DEFAULT ''`); err != nil {
|
||||
return fmt.Errorf("添加custom_coins列失败: %w", err)
|
||||
}
|
||||
|
||||
// 首先创建default用户(如果不存在)
|
||||
_, err := d.db.Exec(`
|
||||
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified)
|
||||
VALUES ('default', 'default@localhost', '', '', true)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建default用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 初始化AI模型(使用default用户)
|
||||
aiModels := []struct {
|
||||
id, name, provider string
|
||||
}{
|
||||
{"deepseek", "DeepSeek", "deepseek"},
|
||||
{"qwen", "Qwen", "qwen"},
|
||||
}
|
||||
|
||||
for _, model := range aiModels {
|
||||
_, err := d.db.Exec(`
|
||||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at)
|
||||
VALUES ($1, 'default', $2, $3, false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, model.id, model.name, model.provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化AI模型失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化交易所(使用default用户)
|
||||
exchanges := []struct {
|
||||
id, name, typ string
|
||||
}{
|
||||
{"binance", "Binance Futures", "binance"},
|
||||
{"hyperliquid", "Hyperliquid", "hyperliquid"},
|
||||
{"aster", "Aster DEX", "aster"},
|
||||
}
|
||||
|
||||
for _, exchange := range exchanges {
|
||||
_, err := d.db.Exec(`
|
||||
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 ($1, 'default', $2, $3, false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id, user_id) DO NOTHING
|
||||
`, exchange.id, exchange.name, exchange.typ)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化交易所失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
func (d *PostgreSQLDatabase) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ CREATE TABLE IF NOT EXISTS traders (
|
||||
override_base_prompt BOOLEAN DEFAULT FALSE,
|
||||
system_prompt_template TEXT DEFAULT 'default',
|
||||
is_cross_margin BOOLEAN DEFAULT TRUE,
|
||||
custom_coins TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -133,6 +134,11 @@ CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config
|
||||
|
||||
-- 插入默认数据
|
||||
|
||||
-- 创建default用户(如果不存在)
|
||||
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) VALUES
|
||||
('default', 'default@localhost', '', '', TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 初始化AI模型(使用default用户)
|
||||
INSERT INTO ai_models (id, user_id, name, provider, enabled) VALUES
|
||||
('deepseek', 'default', 'DeepSeek', 'deepseek', FALSE),
|
||||
@@ -148,7 +154,6 @@ ON CONFLICT (id, user_id) DO NOTHING;
|
||||
|
||||
-- 初始化系统配置
|
||||
INSERT INTO system_config (key, value) VALUES
|
||||
('admin_mode', 'true'),
|
||||
('beta_mode', 'false'),
|
||||
('api_server_port', '8080'),
|
||||
('use_default_coins', 'true'),
|
||||
@@ -166,4 +171,4 @@ CREATE INDEX IF NOT EXISTS idx_ai_models_user_id ON ai_models(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exchanges_user_id ON exchanges(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_traders_user_id ON traders(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_traders_running ON traders(is_running);
|
||||
CREATE INDEX IF NOT EXISTS idx_beta_codes_used ON beta_codes(used);
|
||||
CREATE INDEX IF NOT EXISTS idx_beta_codes_used ON beta_codes(used);
|
||||
|
||||
@@ -105,4 +105,4 @@ networks:
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
redis_data:
|
||||
|
||||
@@ -93,7 +93,7 @@ nofx/
|
||||
| `github.com/gin-gonic/gin` | HTTP API framework | v1.9+ |
|
||||
| `github.com/adshao/go-binance/v2` | Binance API client | v2.4+ |
|
||||
| `github.com/markcheno/go-talib` | Technical indicators (TA-Lib) | Latest |
|
||||
| `github.com/mattn/go-sqlite3` | SQLite database driver | v1.14+ |
|
||||
| `github.com/lib/pq` | PostgreSQL database driver | v1.10+ |
|
||||
| `github.com/golang-jwt/jwt/v5` | JWT authentication | v5.0+ |
|
||||
| `github.com/pquerna/otp` | 2FA/TOTP support | v1.4+ |
|
||||
| `golang.org/x/crypto` | Password hashing (bcrypt) | Latest |
|
||||
|
||||
@@ -93,7 +93,7 @@ nofx/
|
||||
| `github.com/gin-gonic/gin` | HTTP API 框架 | v1.9+ |
|
||||
| `github.com/adshao/go-binance/v2` | Binance API 客户端 | v2.4+ |
|
||||
| `github.com/markcheno/go-talib` | 技术指标(TA-Lib) | 最新 |
|
||||
| `github.com/mattn/go-sqlite3` | SQLite 数据库驱动 | v1.14+ |
|
||||
| `github.com/lib/pq` | PostgreSQL 数据库驱动 | v1.10+ |
|
||||
| `github.com/golang-jwt/jwt/v5` | JWT 认证 | v5.0+ |
|
||||
| `github.com/pquerna/otp` | 2FA/TOTP 支持 | v1.4+ |
|
||||
| `golang.org/x/crypto` | 密码哈希(bcrypt) | 最新 |
|
||||
@@ -282,7 +282,6 @@ GET /api/decisions/latest # 最近决策
|
||||
- 基于 JWT token 的认证
|
||||
- 使用 TOTP 的 2FA(Google Authenticator)
|
||||
- Bcrypt 密码哈希
|
||||
- 管理员模式(简化的单用户模式)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -152,18 +152,17 @@ Yes, to some extent. NOFX provides historical performance feedback in each decis
|
||||
## Data & Privacy
|
||||
|
||||
### Where is my data stored?
|
||||
All data is stored **locally** on your machine in SQLite databases:
|
||||
- `config.db` - Trader configurations
|
||||
- `trading.db` - Trade history
|
||||
All data is stored **locally** in PostgreSQL (Docker volume `postgres_data`) plus:
|
||||
- `decision_logs/` - AI decision records
|
||||
|
||||
### Is my API key secure?
|
||||
API keys are stored in local databases. Never share your databases or `.env` files. We recommend using API keys with IP whitelist restrictions.
|
||||
|
||||
### Can I export my trading history?
|
||||
Yes! Trading data is in SQLite format. You can query it directly:
|
||||
Yes! Use `pg_dump` or `psql` to export data:
|
||||
```bash
|
||||
sqlite3 trading.db "SELECT * FROM trades;"
|
||||
docker compose exec postgres \
|
||||
psql -U nofx -d nofx -c "SELECT * FROM trades;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -152,18 +152,17 @@ docker compose up -d
|
||||
## 数据与隐私
|
||||
|
||||
### 我的数据存储在哪里?
|
||||
所有数据都**本地存储**在您的机器上,使用 SQLite 数据库:
|
||||
- `config.db` - 交易员配置
|
||||
- `trading.db` - 交易历史
|
||||
所有数据都**本地存储**在 PostgreSQL(Docker 卷 `postgres_data`)中,另有:
|
||||
- `decision_logs/` - AI 决策记录
|
||||
|
||||
### API 密钥安全吗?
|
||||
API 密钥存储在本地数据库中。永远不要分享您的数据库或 `.env` 文件。我们建议使用带 IP 白名单限制的 API 密钥。
|
||||
|
||||
### 可以导出交易历史吗?
|
||||
可以!交易数据是 SQLite 格式。您可以直接查询:
|
||||
可以!使用 `pg_dump` 或 `psql` 导出数据:
|
||||
```bash
|
||||
sqlite3 trading.db "SELECT * FROM trades;"
|
||||
docker compose exec postgres \
|
||||
psql -U nofx -d nofx -c "SELECT * FROM trades;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
9
go.mod
9
go.mod
@@ -11,12 +11,10 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sonirico/go-hyperliquid v0.17.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
modernc.org/sqlite v1.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -31,7 +29,6 @@ require (
|
||||
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
||||
github.com/elastic/go-windows v1.0.2 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
|
||||
@@ -55,13 +52,11 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sonirico/vago v0.9.0 // indirect
|
||||
@@ -77,7 +72,6 @@ require (
|
||||
go.elastic.co/fastjson v1.5.1 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
@@ -86,7 +80,4 @@ require (
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
38
go.sum
38
go.sum
@@ -32,8 +32,6 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q=
|
||||
github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=
|
||||
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
|
||||
@@ -84,8 +82,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
@@ -126,8 +122,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
@@ -136,8 +130,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
@@ -154,8 +146,6 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -215,8 +205,6 @@ golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
@@ -246,29 +234,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
|
||||
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 从SQLite导入default用户和系统默认数据到PostgreSQL
|
||||
echo "🔧 从SQLite导入default用户和系统默认数据"
|
||||
echo "========================================"
|
||||
|
||||
# 检查SQLite数据库文件
|
||||
SQLITE_DB="config.db"
|
||||
if [ ! -f "$SQLITE_DB" ]; then
|
||||
echo "❌ 错误:找不到 SQLite 数据库文件 $SQLITE_DB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检测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
|
||||
|
||||
echo "📋 使用命令: $DOCKER_COMPOSE_CMD"
|
||||
|
||||
# 分析SQLite中的default用户数据
|
||||
echo "📊 分析SQLite中的default用户数据..."
|
||||
AI_MODEL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM ai_models WHERE user_id = 'default';" 2>/dev/null || echo "0")
|
||||
EXCHANGE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchanges WHERE user_id = 'default';" 2>/dev/null || echo "0")
|
||||
|
||||
echo " 🤖 AI模型: $AI_MODEL_COUNT 个"
|
||||
echo " 🏢 交易所: $EXCHANGE_COUNT 个"
|
||||
|
||||
if [ "$AI_MODEL_COUNT" -eq 0 ] && [ "$EXCHANGE_COUNT" -eq 0 ]; then
|
||||
echo "⚠️ SQLite中没有default用户的数据,将跳过导入"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 生成导入脚本
|
||||
IMPORT_SQL="import_default_data.sql"
|
||||
|
||||
cat > $IMPORT_SQL << EOL
|
||||
-- 从SQLite导入default用户和系统默认数据
|
||||
-- 生成时间: $(date)
|
||||
|
||||
-- 设置时区
|
||||
SET timezone = 'Asia/Shanghai';
|
||||
|
||||
-- 1. 创建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 NOTHING;
|
||||
|
||||
EOL
|
||||
|
||||
# 导出AI模型数据
|
||||
if [ "$AI_MODEL_COUNT" -gt 0 ]; then
|
||||
echo "🤖 导出AI模型数据..."
|
||||
echo "-- AI模型数据 ($AI_MODEL_COUNT 条记录)" >> $IMPORT_SQL
|
||||
echo "INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES" >> $IMPORT_SQL
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || quote(name) || ', ' || quote(provider) || ', ' ||
|
||||
CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(custom_api_url, '')) || ', ' ||
|
||||
quote(COALESCE(custom_model_name, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),'
|
||||
FROM ai_models WHERE user_id = 'default';" | sed '$ s/,$//' >> $IMPORT_SQL
|
||||
|
||||
echo "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 = EXCLUDED.updated_at;" >> $IMPORT_SQL
|
||||
echo "" >> $IMPORT_SQL
|
||||
fi
|
||||
|
||||
# 导出交易所数据
|
||||
if [ "$EXCHANGE_COUNT" -gt 0 ]; then
|
||||
echo "🏢 导出交易所数据..."
|
||||
echo "-- 交易所数据 ($EXCHANGE_COUNT 条记录)" >> $IMPORT_SQL
|
||||
echo "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" >> $IMPORT_SQL
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || quote(name) || ', ' || quote(type) || ', ' ||
|
||||
CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(secret_key, '')) || ', ' ||
|
||||
CASE WHEN testnet = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(hyperliquid_wallet_addr, '')) || ', ' ||
|
||||
quote(COALESCE(aster_user, '')) || ', ' || quote(COALESCE(aster_signer, '')) || ', ' || quote(COALESCE(aster_private_key, '')) || ', ' ||
|
||||
quote(created_at) || ', ' || quote(updated_at) || '),'
|
||||
FROM exchanges WHERE user_id = 'default';" | sed '$ s/,$//' >> $IMPORT_SQL
|
||||
|
||||
echo "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 = EXCLUDED.updated_at;" >> $IMPORT_SQL
|
||||
echo "" >> $IMPORT_SQL
|
||||
fi
|
||||
|
||||
# 添加验证查询
|
||||
cat >> $IMPORT_SQL << 'EOL'
|
||||
-- 验证导入结果
|
||||
SELECT '=== 导入完成验证 ===' as status;
|
||||
SELECT 'default用户' as item, COUNT(*) as count FROM users WHERE id = 'default'
|
||||
UNION ALL SELECT 'AI模型', COUNT(*) FROM ai_models WHERE user_id = 'default'
|
||||
UNION ALL SELECT '交易所', COUNT(*) FROM exchanges WHERE user_id = 'default';
|
||||
EOL
|
||||
|
||||
echo "✅ 生成导入脚本: $IMPORT_SQL"
|
||||
|
||||
# 确认执行
|
||||
echo
|
||||
echo "⚠️ 准备导入default用户和系统默认数据,包括:"
|
||||
echo " 1. default用户账户"
|
||||
echo " 2. $AI_MODEL_COUNT 个AI模型"
|
||||
echo " 3. $EXCHANGE_COUNT 个交易所"
|
||||
echo
|
||||
read -p "确认执行导入? (y/N): " confirm
|
||||
|
||||
if [[ $confirm != [yY] ]]; then
|
||||
echo "ℹ️ 已取消导入"
|
||||
echo "手动执行命令: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -f /tmp/$IMPORT_SQL"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 检查PostgreSQL连接
|
||||
echo "🔌 检查数据库连接..."
|
||||
if ! $DOCKER_COMPOSE_CMD exec postgres pg_isready -U nofx > /dev/null 2>&1; then
|
||||
echo "❌ PostgreSQL连接失败,请确保服务正在运行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 复制SQL脚本到容器
|
||||
echo "📦 复制导入脚本到容器..."
|
||||
POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q postgres)
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
echo "❌ 找不到PostgreSQL容器"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker cp $IMPORT_SQL ${POSTGRES_CONTAINER}:/tmp/$IMPORT_SQL
|
||||
|
||||
# 执行导入
|
||||
echo "🔄 执行数据导入..."
|
||||
if $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -f /tmp/$IMPORT_SQL; then
|
||||
echo
|
||||
echo "✅ default用户和系统默认数据导入成功!"
|
||||
echo
|
||||
echo "📋 现在可以访问以下接口:"
|
||||
echo " - GET /api/supported-models ($AI_MODEL_COUNT 个AI模型)"
|
||||
echo " - GET /api/supported-exchanges ($EXCHANGE_COUNT 个交易所)"
|
||||
echo
|
||||
echo "🧹 清理导入文件..."
|
||||
rm -f $IMPORT_SQL
|
||||
$DOCKER_COMPOSE_CMD exec postgres rm -f /tmp/$IMPORT_SQL
|
||||
else
|
||||
echo "❌ 数据导入失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 导入完成!"
|
||||
27
main.go
27
main.go
@@ -25,7 +25,6 @@ type LeverageConfig struct {
|
||||
|
||||
// ConfigFile 配置文件结构,只包含需要同步到数据库的字段
|
||||
type ConfigFile struct {
|
||||
AdminMode bool `json:"admin_mode"`
|
||||
BetaMode bool `json:"beta_mode"`
|
||||
APIServerPort int `json:"api_server_port"`
|
||||
UseDefaultCoins bool `json:"use_default_coins"`
|
||||
@@ -74,7 +73,6 @@ func syncConfigToDatabase(database config.DatabaseInterface, configFile *ConfigF
|
||||
|
||||
// 同步各配置项到数据库
|
||||
configs := map[string]string{
|
||||
"admin_mode": fmt.Sprintf("%t", configFile.AdminMode),
|
||||
"beta_mode": fmt.Sprintf("%t", configFile.BetaMode),
|
||||
"api_server_port": strconv.Itoa(configFile.APIServerPort),
|
||||
"use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins),
|
||||
@@ -160,20 +158,14 @@ func main() {
|
||||
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
||||
fmt.Println()
|
||||
|
||||
// 初始化数据库配置
|
||||
dbPath := "config.db"
|
||||
if len(os.Args) > 1 {
|
||||
dbPath = os.Args[1]
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
configFile, err := loadConfigFile()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 读取config.json失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("📋 初始化配置数据库: %s", dbPath)
|
||||
database, err := config.NewDatabase(dbPath)
|
||||
log.Printf("📋 初始化配置数据库 (PostgreSQL)")
|
||||
database, err := config.NewDatabase()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 初始化数据库失败: %v", err)
|
||||
}
|
||||
@@ -194,10 +186,6 @@ func main() {
|
||||
useDefaultCoins := useDefaultCoinsStr == "true"
|
||||
apiPortStr, _ := database.GetSystemConfig("api_server_port")
|
||||
|
||||
// 获取管理员模式配置
|
||||
adminModeStr, _ := database.GetSystemConfig("admin_mode")
|
||||
adminMode := adminModeStr != "false" // 默认为true
|
||||
|
||||
// 设置JWT密钥
|
||||
jwtSecret, _ := database.GetSystemConfig("jwt_secret")
|
||||
if jwtSecret == "" {
|
||||
@@ -206,17 +194,6 @@ func main() {
|
||||
}
|
||||
auth.SetJWTSecret(jwtSecret)
|
||||
|
||||
// 在管理员模式下,确保admin用户存在
|
||||
if adminMode {
|
||||
err := database.EnsureAdminUser()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 创建admin用户失败: %v", err)
|
||||
} else {
|
||||
log.Printf("✓ 管理员模式已启用,无需登录")
|
||||
}
|
||||
auth.SetAdminMode(true)
|
||||
}
|
||||
|
||||
log.Printf("✓ 配置数据库初始化成功")
|
||||
fmt.Println()
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
||||
// AddTrader 从数据库配置添加trader (移除旧版兼容性)
|
||||
|
||||
// AddTraderFromDB 从数据库配置添加trader
|
||||
func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error {
|
||||
func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database config.DatabaseInterface, userID string) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
-- 实际数据迁移脚本 - 从SQLite迁移到PostgreSQL
|
||||
-- 执行方式: psql -h localhost -p 5433 -U nofx -d nofx -f migrate_actual_data.sql
|
||||
|
||||
-- 首先插入default用户(满足外键约束)
|
||||
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) VALUES
|
||||
('default', 'default@localhost', '', '', true, '2025-11-03 09:09:52', '2025-11-03 09:09:52')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入AI模型数据(转换布尔值:0->false, 1->true)
|
||||
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, '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52'),
|
||||
('qwen', 'default', 'Qwen', 'qwen', false, '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入交易所数据(转换布尔值)
|
||||
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, '', '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52'),
|
||||
('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', false, '', '', false, '', '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52'),
|
||||
('aster', 'default', 'Aster DEX', 'aster', false, '', '', false, '', '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52')
|
||||
ON CONFLICT (id, user_id) DO NOTHING;
|
||||
|
||||
-- 插入系统配置数据
|
||||
INSERT INTO system_config (key, value, updated_at) VALUES
|
||||
('coin_pool_api_url', '', '2025-11-03 09:09:52'),
|
||||
('btc_eth_leverage', '5', '2025-11-03 09:09:52'),
|
||||
('api_server_port', '8080', '2025-11-03 09:09:52'),
|
||||
('oi_top_api_url', '', '2025-11-03 09:09:52'),
|
||||
('stop_trading_minutes', '60', '2025-11-03 09:09:52'),
|
||||
('default_coins', '["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]', '2025-11-03 09:09:52'),
|
||||
('altcoin_leverage', '5', '2025-11-03 09:09:52'),
|
||||
('beta_mode', 'true', '2025-11-03 09:09:52'),
|
||||
('use_default_coins', 'true', '2025-11-03 09:09:52'),
|
||||
('max_daily_loss', '10.0', '2025-11-03 09:09:52'),
|
||||
('jwt_secret', 'Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==', '2025-11-03 09:09:52'),
|
||||
('admin_mode', 'false', '2025-11-03 09:09:52'),
|
||||
('max_drawdown', '20.0', '2025-11-03 09:09:52'),
|
||||
('encryption_public_key', '-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsGHRSFXqR2YFoWMNWC
|
||||
8s0FlVE2KglHjLnm1f+i5yPfuTYkTUbVDu6RZuqLJdvhX+UO0x1XnwFIhZqmEfro
|
||||
8Myr5+RnItl7QGqWWcKry4ZlPHroMwIK50WJt316KUKVUv7wUMMLoUUq7yctI8V/
|
||||
thRX+ZRaErJJU9DWkSqjYOVdc+KwsZnN9WifoYhp6veTKmJ1kJOd6AVtF+KJ/z0R
|
||||
hFarXjaQ89vf/oUgKahS/BUH7P6jpP+L+7z8G650oygp3Pn66eq+ttcUdc20WiBj
|
||||
K5eDBUJUUeNmdesqZXBafhJBhsQyilC0+LgI+3laSkGh3odMdY5Mf9lnke9mfX8E
|
||||
RQIDAQAB
|
||||
-----END PUBLIC KEY-----', '2025-11-03 09:09:52'),
|
||||
('encryption_public_key_version', 'mock-v1', '2025-11-03 09:09:52')
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at;
|
||||
|
||||
-- 插入内测码数据(转换布尔值:0->false, 1->true)
|
||||
INSERT INTO beta_codes (code, used, used_by, used_at, created_at) VALUES
|
||||
('2aw4wm', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('34cvds', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('3f39nc', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('3qmg67', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('5rjp6k', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('65a3e6', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('6hzgpr', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('6wruwb', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('8bdf7a', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('8jxnp5', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('8xp3xq', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('9r5uev', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('adbn7p', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('azm8y4', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('b6tfqu', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('bs32f9', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('ctz8gn', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('d8rmq8', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('dmf2yt', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('dz7e8d', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('e9ptrm', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('f25m8s', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('feuzgb', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('fnd7z7', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('h43s95', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('hgs7gq', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('huhkra', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('mhqch4', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('mqwkau', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('mwfssp', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('na7629', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('pb5c2n', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('q5k6jt', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('qrurb8', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('rssybm', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('s7hbk7', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('sj8rus', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('sxy53c', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('t8fjmk', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('udmqcb', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('um6xu6', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('uzwb4r', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('w2uh55', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('wejxcq', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('wtaama', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('x82qvu', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('ygg4d4', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('yv8hnn', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('z9ywv8', false, '', NULL, '2025-11-03 09:09:52'),
|
||||
('znpa5t', false, '', NULL, '2025-11-03 09:09:52')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- 数据迁移验证查询
|
||||
SELECT 'Migration Summary:' as status;
|
||||
SELECT 'ai_models' as table_name, COUNT(*) as count FROM ai_models
|
||||
UNION ALL
|
||||
SELECT 'exchanges', COUNT(*) FROM exchanges
|
||||
UNION ALL
|
||||
SELECT 'system_config', COUNT(*) FROM system_config
|
||||
UNION ALL
|
||||
SELECT 'beta_codes', COUNT(*) FROM beta_codes;
|
||||
|
||||
-- 显示当前配置
|
||||
SELECT 'Current System Config:' as status;
|
||||
SELECT key, value FROM system_config ORDER BY key;
|
||||
@@ -1,49 +0,0 @@
|
||||
-- PostgreSQL数据迁移脚本
|
||||
-- 从SQLite导出的数据转换为PostgreSQL格式
|
||||
|
||||
-- 注意:这个脚本需要根据实际的SQLite导出数据进行调整
|
||||
-- 主要差异:
|
||||
-- 1. SQLite的AUTOINCREMENT -> PostgreSQL的SERIAL
|
||||
-- 2. 布尔值:SQLite的0/1 -> PostgreSQL的false/true
|
||||
-- 3. 日期时间格式可能需要调整
|
||||
-- 4. 主键冲突处理:使用ON CONFLICT
|
||||
|
||||
-- 如果有实际数据,请在这里添加INSERT语句
|
||||
-- 例如:
|
||||
|
||||
-- 插入用户数据(如果有)
|
||||
-- INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at)
|
||||
-- VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入AI模型配置(如果有自定义)
|
||||
-- INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at)
|
||||
-- VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入交易所配置(如果有自定义)
|
||||
-- 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 (...) ON CONFLICT (id, user_id) DO NOTHING;
|
||||
|
||||
-- 插入交易员配置(如果有)
|
||||
-- INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin, created_at, updated_at)
|
||||
-- VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入系统配置(如果有自定义)
|
||||
-- INSERT INTO system_config (key, value, updated_at)
|
||||
-- VALUES (...) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
|
||||
|
||||
-- 插入内测码(如果有)
|
||||
-- INSERT INTO beta_codes (code, used, used_by, used_at, created_at)
|
||||
-- VALUES (...) ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- 数据迁移完成后的验证查询
|
||||
-- SELECT 'users' as table_name, COUNT(*) as count FROM users
|
||||
-- UNION ALL
|
||||
-- SELECT 'ai_models', COUNT(*) FROM ai_models
|
||||
-- UNION ALL
|
||||
-- SELECT 'exchanges', COUNT(*) FROM exchanges
|
||||
-- UNION ALL
|
||||
-- SELECT 'traders', COUNT(*) FROM traders
|
||||
-- UNION ALL
|
||||
-- SELECT 'system_config', COUNT(*) FROM system_config
|
||||
-- UNION ALL
|
||||
-- SELECT 'beta_codes', COUNT(*) FROM beta_codes;
|
||||
@@ -1,330 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 生产环境 SQLite -> PostgreSQL 数据迁移脚本
|
||||
# 真实数据迁移工具 - 支持完整数据导出和迁移
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🚀 生产环境数据迁移工具 SQLite → PostgreSQL ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo
|
||||
|
||||
# 检查必要文件
|
||||
SQLITE_DB="config.db"
|
||||
if [ ! -f "$SQLITE_DB" ]; then
|
||||
echo -e "${RED}❌ 错误:找不到 SQLite 数据库文件 $SQLITE_DB${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检测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 -e "${RED}❌ 错误:找不到 docker-compose 或 docker compose 命令${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}📋 使用命令: ${DOCKER_COMPOSE_CMD}${NC}"
|
||||
|
||||
# 分析当前SQLite数据
|
||||
echo -e "\n${BLUE}📊 分析当前SQLite数据库...${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# 获取数据统计
|
||||
USER_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM users;" 2>/dev/null || echo "0")
|
||||
AI_MODEL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM ai_models;" 2>/dev/null || echo "0")
|
||||
EXCHANGE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchanges;" 2>/dev/null || echo "0")
|
||||
TRADER_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM traders;" 2>/dev/null || echo "0")
|
||||
CONFIG_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM system_config;" 2>/dev/null || echo "0")
|
||||
BETA_CODE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM beta_codes;" 2>/dev/null || echo "0")
|
||||
|
||||
echo "📈 数据库表统计:"
|
||||
echo " 👥 用户 (users): $USER_COUNT"
|
||||
echo " 🤖 AI模型 (ai_models): $AI_MODEL_COUNT"
|
||||
echo " 🏢 交易所 (exchanges): $EXCHANGE_COUNT"
|
||||
echo " 🔧 交易员 (traders): $TRADER_COUNT"
|
||||
echo " ⚙️ 系统配置 (system_config): $CONFIG_COUNT"
|
||||
echo " 🎟️ 内测码 (beta_codes): $BETA_CODE_COUNT"
|
||||
|
||||
# 检查是否有exchange_secrets表
|
||||
if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='exchange_secrets';" | grep -q exchange_secrets; then
|
||||
SECRET_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchange_secrets;" 2>/dev/null || echo "0")
|
||||
echo " 🔐 交易所密钥 (exchange_secrets): $SECRET_COUNT"
|
||||
fi
|
||||
|
||||
# 检查是否有user_signal_sources表
|
||||
if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='user_signal_sources';" | grep -q user_signal_sources; then
|
||||
SIGNAL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM user_signal_sources;" 2>/dev/null || echo "0")
|
||||
echo " 📡 用户信号源 (user_signal_sources): $SIGNAL_COUNT"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# 生成迁移时间戳
|
||||
TIMESTAMP=$(date '+%Y-%m-%d_%H-%M-%S')
|
||||
MIGRATION_FILE="migrate_production_${TIMESTAMP}.sql"
|
||||
|
||||
echo -e "${BLUE}📤 生成数据迁移脚本: $MIGRATION_FILE${NC}"
|
||||
|
||||
# 生成SQL迁移脚本
|
||||
cat > $MIGRATION_FILE << EOL
|
||||
-- 生产环境数据迁移脚本
|
||||
-- 从SQLite自动导出生成
|
||||
-- 执行时间: $(date)
|
||||
|
||||
-- 设置时区
|
||||
SET timezone = 'Asia/Shanghai';
|
||||
|
||||
EOL
|
||||
|
||||
# 导出用户数据
|
||||
if [ "$USER_COUNT" -gt 0 ]; then
|
||||
echo -e "${CYAN}👥 导出用户数据...${NC}"
|
||||
echo "-- 用户数据 ($USER_COUNT 条记录)" >> $MIGRATION_FILE
|
||||
echo "INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) VALUES" >> $MIGRATION_FILE
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(COALESCE(email, '')) || ', ' || quote(COALESCE(password_hash, '')) || ', ' || quote(COALESCE(otp_secret, '')) || ', ' ||
|
||||
CASE WHEN otp_verified = 1 THEN 'true' ELSE 'false' END || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),'
|
||||
FROM users;" | sed '$ s/,$//' >> $MIGRATION_FILE
|
||||
|
||||
echo "ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, password_hash = EXCLUDED.password_hash, otp_secret = EXCLUDED.otp_secret, otp_verified = EXCLUDED.otp_verified, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE
|
||||
echo "" >> $MIGRATION_FILE
|
||||
fi
|
||||
|
||||
# 导出AI模型数据
|
||||
if [ "$AI_MODEL_COUNT" -gt 0 ]; then
|
||||
echo -e "${CYAN}🤖 导出AI模型数据...${NC}"
|
||||
echo "-- AI模型数据 ($AI_MODEL_COUNT 条记录)" >> $MIGRATION_FILE
|
||||
echo "INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES" >> $MIGRATION_FILE
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' ||
|
||||
quote(name) || ', ' || quote(provider) || ', ' ||
|
||||
CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(api_key) || ', ' || quote(COALESCE(custom_api_url, '')) || ', ' ||
|
||||
quote(COALESCE(custom_model_name, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),'
|
||||
FROM ai_models WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default'
|
||||
AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE
|
||||
|
||||
echo "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 = EXCLUDED.updated_at;" >> $MIGRATION_FILE
|
||||
echo "" >> $MIGRATION_FILE
|
||||
fi
|
||||
|
||||
# 导出交易所数据
|
||||
if [ "$EXCHANGE_COUNT" -gt 0 ]; then
|
||||
echo -e "${CYAN}🏢 导出交易所数据...${NC}"
|
||||
echo "-- 交易所数据 ($EXCHANGE_COUNT 条记录)" >> $MIGRATION_FILE
|
||||
echo "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" >> $MIGRATION_FILE
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' ||
|
||||
quote(name) || ', ' || quote(type) || ', ' ||
|
||||
CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(secret_key, '')) || ', ' ||
|
||||
CASE WHEN testnet = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(hyperliquid_wallet_addr, '')) || ', ' ||
|
||||
quote(COALESCE(aster_user, '')) || ', ' || quote(COALESCE(aster_signer, '')) || ', ' || quote(COALESCE(aster_private_key, '')) || ', ' ||
|
||||
quote(created_at) || ', ' || quote(updated_at) || '),'
|
||||
FROM exchanges WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default'
|
||||
AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE
|
||||
|
||||
echo "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 = EXCLUDED.updated_at;" >> $MIGRATION_FILE
|
||||
echo "" >> $MIGRATION_FILE
|
||||
fi
|
||||
|
||||
# 导出交易员数据
|
||||
if [ "$TRADER_COUNT" -gt 0 ]; then
|
||||
echo -e "${CYAN}🔧 导出交易员数据...${NC}"
|
||||
echo "-- 交易员数据 ($TRADER_COUNT 条记录)" >> $MIGRATION_FILE
|
||||
echo "INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin, created_at, updated_at) VALUES" >> $MIGRATION_FILE
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' ||
|
||||
quote(name) || ', ' || quote(ai_model_id) || ', ' ||
|
||||
quote(exchange_id) || ', ' || initial_balance || ', ' || scan_interval_minutes || ', ' ||
|
||||
CASE WHEN is_running = 1 THEN 'true' ELSE 'false' END || ', ' || btc_eth_leverage || ', ' || altcoin_leverage || ', ' ||
|
||||
quote(COALESCE(trading_symbols, '')) || ', ' ||
|
||||
CASE WHEN use_coin_pool = 1 THEN 'true' ELSE 'false' END || ', ' || CASE WHEN use_oi_top = 1 THEN 'true' ELSE 'false' END || ', ' ||
|
||||
quote(COALESCE(custom_prompt, '')) || ', ' || CASE WHEN override_base_prompt = 1 THEN 'true' ELSE 'false' END || ', ' ||
|
||||
quote(COALESCE(system_prompt_template, 'default')) || ', ' || CASE WHEN is_cross_margin = 1 THEN 'true' ELSE 'false' END || ', ' ||
|
||||
quote(created_at) || ', ' || quote(updated_at) || '),'
|
||||
FROM traders WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default'
|
||||
AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE
|
||||
|
||||
echo "ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, ai_model_id = EXCLUDED.ai_model_id, exchange_id = EXCLUDED.exchange_id, initial_balance = EXCLUDED.initial_balance, scan_interval_minutes = EXCLUDED.scan_interval_minutes, is_running = EXCLUDED.is_running, btc_eth_leverage = EXCLUDED.btc_eth_leverage, altcoin_leverage = EXCLUDED.altcoin_leverage, trading_symbols = EXCLUDED.trading_symbols, use_coin_pool = EXCLUDED.use_coin_pool, use_oi_top = EXCLUDED.use_oi_top, custom_prompt = EXCLUDED.custom_prompt, override_base_prompt = EXCLUDED.override_base_prompt, system_prompt_template = EXCLUDED.system_prompt_template, is_cross_margin = EXCLUDED.is_cross_margin, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE
|
||||
echo "" >> $MIGRATION_FILE
|
||||
fi
|
||||
|
||||
# 导出系统配置
|
||||
if [ "$CONFIG_COUNT" -gt 0 ]; then
|
||||
echo -e "${CYAN}⚙️ 导出系统配置...${NC}"
|
||||
echo "-- 系统配置数据 ($CONFIG_COUNT 条记录)" >> $MIGRATION_FILE
|
||||
echo "INSERT INTO system_config (key, value, updated_at) VALUES" >> $MIGRATION_FILE
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(key) || ', ' || quote(value) || ', ' || quote(updated_at) || '),'
|
||||
FROM system_config;" | sed '$ s/,$//' >> $MIGRATION_FILE
|
||||
|
||||
echo "ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE
|
||||
echo "" >> $MIGRATION_FILE
|
||||
fi
|
||||
|
||||
# 导出内测码数据
|
||||
if [ "$BETA_CODE_COUNT" -gt 0 ]; then
|
||||
echo -e "${CYAN}🎟️ 导出内测码数据...${NC}"
|
||||
echo "-- 内测码数据 ($BETA_CODE_COUNT 条记录)" >> $MIGRATION_FILE
|
||||
echo "INSERT INTO beta_codes (code, used, used_by, used_at, created_at) VALUES" >> $MIGRATION_FILE
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(code) || ', ' || CASE WHEN used = 1 THEN 'true' ELSE 'false' END || ', ' ||
|
||||
quote(COALESCE(used_by, '')) || ', ' || CASE WHEN used_at IS NULL THEN 'NULL' ELSE quote(used_at) END || ', ' ||
|
||||
quote(created_at) || '),'
|
||||
FROM beta_codes;" | sed '$ s/,$//' >> $MIGRATION_FILE
|
||||
|
||||
echo "ON CONFLICT (code) DO UPDATE SET used = EXCLUDED.used, used_by = EXCLUDED.used_by, used_at = EXCLUDED.used_at;" >> $MIGRATION_FILE
|
||||
echo "" >> $MIGRATION_FILE
|
||||
fi
|
||||
|
||||
# 导出用户信号源(如果存在)
|
||||
if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='user_signal_sources';" | grep -q user_signal_sources; then
|
||||
SIGNAL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM user_signal_sources;" 2>/dev/null || echo "0")
|
||||
if [ "$SIGNAL_COUNT" -gt 0 ]; then
|
||||
echo -e "${CYAN}📡 导出用户信号源数据...${NC}"
|
||||
echo "-- 用户信号源数据 ($SIGNAL_COUNT 条记录)" >> $MIGRATION_FILE
|
||||
echo "INSERT INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, created_at, updated_at) VALUES" >> $MIGRATION_FILE
|
||||
|
||||
sqlite3 $SQLITE_DB "SELECT '(' || quote(user_id) || ', ' ||
|
||||
quote(COALESCE(coin_pool_url, '')) || ', ' ||
|
||||
quote(COALESCE(oi_top_url, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),'
|
||||
FROM user_signal_sources WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default'
|
||||
AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE
|
||||
|
||||
echo "ON CONFLICT (user_id) DO UPDATE SET coin_pool_url = EXCLUDED.coin_pool_url, oi_top_url = EXCLUDED.oi_top_url, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE
|
||||
echo "" >> $MIGRATION_FILE
|
||||
fi
|
||||
fi
|
||||
|
||||
# 添加迁移验证查询
|
||||
cat >> $MIGRATION_FILE << 'EOL'
|
||||
-- 迁移验证查询
|
||||
SELECT '=== 数据迁移完成验证 ===' as status;
|
||||
SELECT 'users' as table_name, COUNT(*) as record_count FROM users
|
||||
UNION ALL SELECT 'ai_models', COUNT(*) FROM ai_models
|
||||
UNION ALL SELECT 'exchanges', COUNT(*) FROM exchanges
|
||||
UNION ALL SELECT 'traders', COUNT(*) FROM traders
|
||||
UNION ALL SELECT 'system_config', COUNT(*) FROM system_config
|
||||
UNION ALL SELECT 'beta_codes', COUNT(*) FROM beta_codes
|
||||
UNION ALL SELECT 'user_signal_sources', COUNT(*) FROM user_signal_sources
|
||||
ORDER BY table_name;
|
||||
|
||||
-- 显示关键配置
|
||||
SELECT '=== 关键系统配置 ===' as info;
|
||||
SELECT key,
|
||||
CASE WHEN LENGTH(value) > 50 THEN LEFT(value, 50) || '...' ELSE value END as value
|
||||
FROM system_config
|
||||
WHERE key IN ('admin_mode', 'beta_mode', 'api_server_port', 'default_coins', 'jwt_secret')
|
||||
ORDER BY key;
|
||||
EOL
|
||||
|
||||
echo -e "${GREEN}✅ 迁移脚本生成完成: $MIGRATION_FILE${NC}"
|
||||
|
||||
# 确认是否执行迁移
|
||||
echo
|
||||
echo -e "${YELLOW}⚠️ 准备执行数据迁移,这将:${NC}"
|
||||
echo " 1. 停止现有服务"
|
||||
echo " 2. 启动PostgreSQL和Redis"
|
||||
echo " 3. 执行数据迁移"
|
||||
echo " 4. 验证迁移结果"
|
||||
echo
|
||||
read -p "确认执行迁移? (y/N): " confirm
|
||||
|
||||
if [[ $confirm != [yY] ]]; then
|
||||
echo -e "${BLUE}ℹ️ 迁移脚本已生成,可稍后手动执行${NC}"
|
||||
echo "手动执行命令: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -f /tmp/$MIGRATION_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 执行迁移
|
||||
echo -e "\n${BLUE}🚀 开始执行数据迁移...${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# 停止现有服务
|
||||
echo -e "${YELLOW}🛑 停止现有服务...${NC}"
|
||||
$DOCKER_COMPOSE_CMD down 2>/dev/null || true
|
||||
|
||||
# 启动PostgreSQL和Redis
|
||||
echo -e "${YELLOW}🚀 启动PostgreSQL和Redis服务...${NC}"
|
||||
$DOCKER_COMPOSE_CMD up postgres redis -d
|
||||
|
||||
# 等待服务启动
|
||||
echo -e "${YELLOW}⏳ 等待服务启动...${NC}"
|
||||
sleep 15
|
||||
|
||||
# 检查PostgreSQL连接
|
||||
echo -e "${BLUE}🔌 测试数据库连接...${NC}"
|
||||
max_retries=15
|
||||
retry_count=0
|
||||
|
||||
while [ $retry_count -lt $max_retries ]; do
|
||||
if $DOCKER_COMPOSE_CMD exec postgres pg_isready -U nofx > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ PostgreSQL连接正常${NC}"
|
||||
break
|
||||
else
|
||||
retry_count=$((retry_count + 1))
|
||||
echo -e "${YELLOW}⏳ 等待PostgreSQL启动... (${retry_count}/${max_retries})${NC}"
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
echo -e "${RED}❌ 无法连接到PostgreSQL,请检查服务状态${NC}"
|
||||
$DOCKER_COMPOSE_CMD logs postgres
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 复制迁移脚本到容器
|
||||
echo -e "${BLUE}📦 复制迁移脚本到容器...${NC}"
|
||||
POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q postgres)
|
||||
if [ -z "$POSTGRES_CONTAINER" ]; then
|
||||
echo -e "${RED}❌ 找不到PostgreSQL容器${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker cp $MIGRATION_FILE ${POSTGRES_CONTAINER}:/tmp/$MIGRATION_FILE
|
||||
|
||||
# 验证文件复制成功
|
||||
if ! $DOCKER_COMPOSE_CMD exec postgres test -f /tmp/$MIGRATION_FILE; then
|
||||
echo -e "${RED}❌ 迁移脚本复制失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 执行数据迁移
|
||||
echo -e "${BLUE}🔄 执行数据迁移...${NC}"
|
||||
if $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -f /tmp/$MIGRATION_FILE; then
|
||||
echo -e "${GREEN}✅ 数据迁移成功!${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ 数据迁移失败${NC}"
|
||||
echo "查看错误日志: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -c \"SELECT version();\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}🎉 生产环境数据迁移完成!${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo -e "${BLUE}📋 后续步骤:${NC}"
|
||||
echo -e "1. 备份原SQLite: ${YELLOW}mv config.db config.db.backup.$(date +%Y%m%d)${NC}"
|
||||
echo -e "2. 启动完整应用: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}"
|
||||
echo -e "3. 验证功能: 访问 ${YELLOW}http://localhost:3000${NC}"
|
||||
echo -e "4. 删除迁移文件: ${YELLOW}rm $MIGRATION_FILE${NC}"
|
||||
echo
|
||||
echo -e "${BLUE}🔧 如需回滚:${NC}"
|
||||
echo -e "1. 停止服务: ${YELLOW}$DOCKER_COMPOSE_CMD down${NC}"
|
||||
echo -e "2. 恢复SQLite: ${YELLOW}mv config.db.backup.$(date +%Y%m%d) config.db${NC}"
|
||||
echo -e "3. 删除环境变量或编辑 .env 文件注释掉 POSTGRES_HOST"
|
||||
echo -e "4. 重启: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}"
|
||||
echo
|
||||
echo -e "${GREEN}🚀 PostgreSQL生产环境迁移成功!${NC}"
|
||||
160
scripts/import_default_patch.sh
Executable file
160
scripts/import_default_patch.sh
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/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,207 +0,0 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE ai_models (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL DEFAULT 'default',
|
||||
name TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
enabled BOOLEAN DEFAULT 0,
|
||||
api_key TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, custom_api_url TEXT DEFAULT '', custom_model_name TEXT DEFAULT '',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO ai_models VALUES('deepseek','default','DeepSeek','deepseek',0,'','2025-11-03 09:09:52','2025-11-03 09:09:52','','');
|
||||
INSERT INTO ai_models VALUES('qwen','default','Qwen','qwen',0,'','2025-11-03 09:09:52','2025-11-03 09:09:52','','');
|
||||
CREATE TABLE exchange_secrets (
|
||||
exchange_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
credential_type TEXT NOT NULL,
|
||||
ciphertext BLOB NOT NULL,
|
||||
nonce BLOB NOT NULL,
|
||||
kms_ciphertext BLOB NOT NULL,
|
||||
kms_key_version TEXT NOT NULL,
|
||||
public_key_version TEXT NOT NULL,
|
||||
algorithm TEXT NOT NULL,
|
||||
aad BLOB NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (exchange_id, user_id, credential_type),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE user_signal_sources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
coin_pool_url TEXT DEFAULT '',
|
||||
oi_top_url TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
CREATE TABLE traders (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL DEFAULT 'default',
|
||||
name TEXT NOT NULL,
|
||||
ai_model_id TEXT NOT NULL,
|
||||
exchange_id TEXT NOT NULL,
|
||||
initial_balance REAL NOT NULL,
|
||||
scan_interval_minutes INTEGER DEFAULT 3,
|
||||
is_running BOOLEAN DEFAULT 0,
|
||||
btc_eth_leverage INTEGER DEFAULT 5,
|
||||
altcoin_leverage INTEGER DEFAULT 5,
|
||||
trading_symbols TEXT DEFAULT '',
|
||||
use_coin_pool BOOLEAN DEFAULT 0,
|
||||
use_oi_top BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, custom_prompt TEXT DEFAULT '', override_base_prompt BOOLEAN DEFAULT 0, is_cross_margin BOOLEAN DEFAULT 1, use_default_coins BOOLEAN DEFAULT 1, custom_coins TEXT DEFAULT '', system_prompt_template TEXT DEFAULT 'default',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ai_model_id) REFERENCES ai_models(id),
|
||||
FOREIGN KEY (exchange_id) REFERENCES exchanges(id)
|
||||
);
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
otp_secret TEXT,
|
||||
otp_verified BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE system_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO system_config VALUES('coin_pool_api_url','','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('btc_eth_leverage','5','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('api_server_port','8080','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('oi_top_api_url','','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('stop_trading_minutes','60','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('default_coins','["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('altcoin_leverage','5','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('beta_mode','true','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('use_default_coins','true','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('max_daily_loss','10.0','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('jwt_secret','Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('admin_mode','false','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('max_drawdown','20.0','2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('encryption_public_key',unistr('-----BEGIN PUBLIC KEY-----\u000aMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsGHRSFXqR2YFoWMNWC\u000a8s0FlVE2KglHjLnm1f+i5yPfuTYkTUbVDu6RZuqLJdvhX+UO0x1XnwFIhZqmEfro\u000a8Myr5+RnItl7QGqWWcKry4ZlPHroMwIK50WJt316KUKVUv7wUMMLoUUq7yctI8V/\u000athRX+ZRaErJJU9DWkSqjYOVdc+KwsZnN9WifoYhp6veTKmJ1kJOd6AVtF+KJ/z0R\u000ahFarXjaQ89vf/oUgKahS/BUH7P6jpP+L+7z8G650oygp3Pn66eq+ttcUdc20WiBj\u000aK5eDBUJUUeNmdesqZXBafhJBhsQyilC0+LgI+3laSkGh3odMdY5Mf9lnke9mfX8E\u000aRQIDAQAB\u000a-----END PUBLIC KEY-----'),'2025-11-03 09:09:52');
|
||||
INSERT INTO system_config VALUES('encryption_public_key_version','mock-v1','2025-11-03 09:09:52');
|
||||
CREATE TABLE beta_codes (
|
||||
code TEXT PRIMARY KEY,
|
||||
used BOOLEAN DEFAULT 0,
|
||||
used_by TEXT DEFAULT '',
|
||||
used_at DATETIME DEFAULT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO beta_codes VALUES('2aw4wm',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('34cvds',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('3f39nc',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('3qmg67',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('5rjp6k',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('65a3e6',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('6hzgpr',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('6wruwb',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('8bdf7a',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('8jxnp5',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('8xp3xq',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('9r5uev',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('adbn7p',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('azm8y4',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('b6tfqu',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('bs32f9',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('ctz8gn',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('d8rmq8',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('dmf2yt',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('dz7e8d',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('e9ptrm',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('f25m8s',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('feuzgb',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('fnd7z7',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('h43s95',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('hgs7gq',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('huhkra',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('mhqch4',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('mqwkau',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('mwfssp',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('na7629',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('pb5c2n',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('q5k6jt',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('qrurb8',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('rssybm',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('s7hbk7',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('sj8rus',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('sxy53c',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('t8fjmk',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('udmqcb',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('um6xu6',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('uzwb4r',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('w2uh55',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('wejxcq',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('wtaama',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('x82qvu',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('ygg4d4',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('yv8hnn',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('z9ywv8',0,'',NULL,'2025-11-03 09:09:52');
|
||||
INSERT INTO beta_codes VALUES('znpa5t',0,'',NULL,'2025-11-03 09:09:52');
|
||||
CREATE TABLE IF NOT EXISTS "exchanges" (
|
||||
id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL DEFAULT 'default',
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
enabled BOOLEAN DEFAULT 0,
|
||||
api_key TEXT DEFAULT '',
|
||||
secret_key TEXT DEFAULT '',
|
||||
testnet BOOLEAN DEFAULT 0,
|
||||
hyperliquid_wallet_addr TEXT DEFAULT '',
|
||||
aster_user TEXT DEFAULT '',
|
||||
aster_signer TEXT DEFAULT '',
|
||||
aster_private_key TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO exchanges VALUES('binance','default','Binance Futures','binance',0,'','',0,'','','','','2025-11-03 09:09:52','2025-11-03 09:09:52');
|
||||
INSERT INTO exchanges VALUES('hyperliquid','default','Hyperliquid','hyperliquid',0,'','',0,'','','','','2025-11-03 09:09:52','2025-11-03 09:09:52');
|
||||
INSERT INTO exchanges VALUES('aster','default','Aster DEX','aster',0,'','',0,'','','','','2025-11-03 09:09:52','2025-11-03 09:09:52');
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
AFTER UPDATE ON users
|
||||
BEGIN
|
||||
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
CREATE TRIGGER update_ai_models_updated_at
|
||||
AFTER UPDATE ON ai_models
|
||||
BEGIN
|
||||
UPDATE ai_models SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
CREATE TRIGGER update_exchange_secrets_updated_at
|
||||
AFTER UPDATE ON exchange_secrets
|
||||
BEGIN
|
||||
UPDATE exchange_secrets
|
||||
SET updated_at = CURRENT_TIMESTAMP
|
||||
WHERE exchange_id = NEW.exchange_id AND user_id = NEW.user_id AND credential_type = NEW.credential_type;
|
||||
END;
|
||||
CREATE TRIGGER update_traders_updated_at
|
||||
AFTER UPDATE ON traders
|
||||
BEGIN
|
||||
UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
CREATE TRIGGER update_user_signal_sources_updated_at
|
||||
AFTER UPDATE ON user_signal_sources
|
||||
BEGIN
|
||||
UPDATE user_signal_sources SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
CREATE TRIGGER update_system_config_updated_at
|
||||
AFTER UPDATE ON system_config
|
||||
BEGIN
|
||||
UPDATE system_config SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;
|
||||
END;
|
||||
CREATE TRIGGER update_exchanges_updated_at
|
||||
AFTER UPDATE ON exchanges
|
||||
BEGIN
|
||||
UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = NEW.id AND user_id = NEW.user_id;
|
||||
END;
|
||||
COMMIT;
|
||||
@@ -12,7 +12,6 @@ import AILearning from './components/AILearning'
|
||||
import { LanguageProvider, useLanguage } from './contexts/LanguageContext'
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||
import { t, type Language } from './i18n/translations'
|
||||
import { useSystemConfig } from './hooks/useSystemConfig'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import type {
|
||||
SystemStatus,
|
||||
@@ -42,7 +41,7 @@ function getModelDisplayName(modelId: string): string {
|
||||
function App() {
|
||||
const { language, setLanguage } = useLanguage()
|
||||
const { user, token, logout, isLoading } = useAuth()
|
||||
const { config: systemConfig, loading: configLoading } = useSystemConfig()
|
||||
const configLoading = false
|
||||
const [route, setRoute] = useState(window.location.pathname)
|
||||
|
||||
// 从URL路径读取初始页面状态(支持刷新保持页面)
|
||||
@@ -243,7 +242,6 @@ function App() {
|
||||
onLanguageChange={setLanguage}
|
||||
user={user}
|
||||
onLogout={logout}
|
||||
isAdminMode={systemConfig?.admin_mode}
|
||||
onPageChange={(page) => {
|
||||
console.log('Competition page onPageChange called with:', page)
|
||||
console.log('Current route:', route, 'Current page:', currentPage)
|
||||
@@ -286,7 +284,7 @@ function App() {
|
||||
}
|
||||
|
||||
// Show main app for authenticated users on other routes
|
||||
if (!systemConfig?.admin_mode && (!user || !token)) {
|
||||
if (!user || !token) {
|
||||
// Default to landing page when not authenticated and no specific route
|
||||
return <LandingPage />
|
||||
}
|
||||
@@ -303,7 +301,6 @@ function App() {
|
||||
onLanguageChange={setLanguage}
|
||||
user={user}
|
||||
onLogout={logout}
|
||||
isAdminMode={systemConfig?.admin_mode}
|
||||
onPageChange={(page) => {
|
||||
console.log('Main app onPageChange called with:', page)
|
||||
|
||||
|
||||
@@ -104,8 +104,8 @@ export default function AboutSection({ language }: AboutSectionProps) {
|
||||
lines={[
|
||||
'$ git clone https://github.com/tinkle-community/nofx.git',
|
||||
'$ cd nofx',
|
||||
'$ chmod +x start.sh',
|
||||
'$ ./start.sh start --build',
|
||||
'$ chmod +x scripts/start.sh',
|
||||
'$ ./scripts/start.sh start --build',
|
||||
t('startupMessages1', language),
|
||||
t('startupMessages2', language),
|
||||
t('startupMessages3', language),
|
||||
|
||||
@@ -12,7 +12,6 @@ interface HeaderBarProps {
|
||||
onLanguageChange?: (lang: Language) => void
|
||||
user?: { email: string } | null
|
||||
onLogout?: () => void
|
||||
isAdminMode?: boolean
|
||||
onPageChange?: (page: string) => void
|
||||
}
|
||||
|
||||
@@ -24,7 +23,6 @@ export default function HeaderBar({
|
||||
onLanguageChange,
|
||||
user,
|
||||
onLogout,
|
||||
isAdminMode = false,
|
||||
onPageChange,
|
||||
}: HeaderBarProps) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
@@ -363,7 +361,7 @@ export default function HeaderBar({
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
{!isAdminMode && onLogout && (
|
||||
{onLogout && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onLogout()
|
||||
@@ -763,7 +761,7 @@ export default function HeaderBar({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isAdminMode && onLogout && (
|
||||
{onLogout && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onLogout()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||
import { getSystemConfig } from '../lib/config'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
@@ -49,37 +48,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 先检查是否为管理员模式(使用带缓存的系统配置获取)
|
||||
getSystemConfig()
|
||||
.then((data) => {
|
||||
if (data.admin_mode) {
|
||||
// 管理员模式下,模拟admin用户
|
||||
setUser({ id: 'admin', email: 'admin@localhost' })
|
||||
setToken('admin-mode')
|
||||
} else {
|
||||
// 非管理员模式,检查本地存储中是否有token
|
||||
const savedToken = localStorage.getItem('auth_token')
|
||||
const savedUser = localStorage.getItem('auth_user')
|
||||
const savedToken = localStorage.getItem('auth_token')
|
||||
const savedUser = localStorage.getItem('auth_user')
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
setToken(savedToken)
|
||||
setUser(JSON.parse(savedUser))
|
||||
}
|
||||
}
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to fetch system config:', err)
|
||||
// 发生错误时,继续检查本地存储
|
||||
const savedToken = localStorage.getItem('auth_token')
|
||||
const savedUser = localStorage.getItem('auth_user')
|
||||
if (savedToken && savedUser) {
|
||||
setToken(savedToken)
|
||||
setUser(JSON.parse(savedUser))
|
||||
}
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
setToken(savedToken)
|
||||
setUser(JSON.parse(savedUser))
|
||||
}
|
||||
setIsLoading(false)
|
||||
})
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
|
||||
@@ -11,7 +11,6 @@ export const translations = {
|
||||
competition: 'Competition',
|
||||
running: 'RUNNING',
|
||||
stopped: 'STOPPED',
|
||||
adminMode: 'Admin Mode',
|
||||
logout: 'Logout',
|
||||
switchTrader: 'Switch Trader:',
|
||||
view: 'View',
|
||||
@@ -504,7 +503,6 @@ export const translations = {
|
||||
competition: '竞赛',
|
||||
running: '运行中',
|
||||
stopped: '已停止',
|
||||
adminMode: '管理员模式',
|
||||
logout: '退出',
|
||||
switchTrader: '切换交易员:',
|
||||
view: '查看',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export interface SystemConfig {
|
||||
admin_mode: boolean
|
||||
beta_mode: boolean
|
||||
default_coins?: string[]
|
||||
btc_eth_leverage?: number
|
||||
altcoin_leverage?: number
|
||||
}
|
||||
|
||||
let configPromise: Promise<SystemConfig> | null = null
|
||||
|
||||
Reference in New Issue
Block a user