diff --git a/api/server.go b/api/server.go index ad16470a..a3c18431 100644 --- a/api/server.go +++ b/api/server.go @@ -166,8 +166,13 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { altcoinLeverage = val } + // 获取内测模式配置 + betaModeStr, _ := s.database.GetSystemConfig("beta_mode") + betaMode := betaModeStr == "true" + c.JSON(http.StatusOK, gin.H{ "admin_mode": auth.IsAdminMode(), + "beta_mode": betaMode, "default_coins": defaultCoins, "btc_eth_leverage": btcEthLeverage, "altcoin_leverage": altcoinLeverage, @@ -1168,6 +1173,7 @@ func (s *Server) handleRegister(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` + BetaCode string `json:"beta_code"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -1175,6 +1181,27 @@ func (s *Server) handleRegister(c *gin.Context) { return } + // 检查是否开启了内测模式 + betaModeStr, _ := s.database.GetSystemConfig("beta_mode") + if betaModeStr == "true" { + // 内测模式下必须提供有效的内测码 + if req.BetaCode == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "内测期间,注册需要提供内测码"}) + return + } + + // 验证内测码 + isValid, err := s.database.ValidateBetaCode(req.BetaCode) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "验证内测码失败"}) + return + } + if !isValid { + c.JSON(http.StatusBadRequest, gin.H{"error": "内测码无效或已被使用"}) + return + } + } + // 检查邮箱是否已存在 _, err := s.database.GetUserByEmail(req.Email) if err == nil { @@ -1212,6 +1239,18 @@ func (s *Server) handleRegister(c *gin.Context) { return } + // 如果是内测模式,标记内测码为已使用 + betaModeStr2, _ := s.database.GetSystemConfig("beta_mode") + if betaModeStr2 == "true" && req.BetaCode != "" { + err := s.database.UseBetaCode(req.BetaCode, req.Email) + if err != nil { + log.Printf("⚠️ 标记内测码为已使用失败: %v", err) + // 这里不返回错误,因为用户已经创建成功 + } else { + log.Printf("✓ 内测码 %s 已被用户 %s 使用", req.BetaCode, req.Email) + } + } + // 返回OTP设置信息 qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email) c.JSON(http.StatusOK, gin.H{ diff --git a/config/database.go b/config/database.go index c5eef755..686f9346 100644 --- a/config/database.go +++ b/config/database.go @@ -6,6 +6,7 @@ import ( "encoding/base32" "fmt" "log" + "os" "strings" "time" @@ -125,6 +126,15 @@ func (d *Database) createTables() error { updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, + // 内测码表 + `CREATE TABLE IF NOT EXISTS 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 + )`, + // 触发器:自动更新 updated_at `CREATE TRIGGER IF NOT EXISTS update_users_updated_at AFTER UPDATE ON users @@ -246,6 +256,7 @@ func (d *Database) initDefaultData() error { // 初始化系统配置 - 创建所有字段,设置默认值,后续由config.json同步更新 systemConfigs := map[string]string{ "admin_mode": "true", // 默认开启管理员模式,便于首次使用 + "beta_mode": "false", // 默认关闭内测模式 "api_server_port": "8080", // 默认API端口 "use_default_coins": "true", // 默认使用内置币种列表 "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) @@ -943,4 +954,106 @@ func (d *Database) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) // Close 关闭数据库连接 func (d *Database) Close() error { return d.db.Close() +} + +// LoadBetaCodesFromFile 从文件加载内测码到数据库 +func (d *Database) LoadBetaCodesFromFile(filePath string) error { + // 读取文件内容 + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("读取内测码文件失败: %w", err) + } + + // 按行分割内测码 + lines := strings.Split(string(content), "\n") + var codes []string + for _, line := range lines { + code := strings.TrimSpace(line) + if code != "" && !strings.HasPrefix(code, "#") { + codes = append(codes, code) + } + } + + // 批量插入内测码 + tx, err := d.db.Begin() + if err != nil { + return fmt.Errorf("开始事务失败: %w", err) + } + defer tx.Rollback() + + stmt, err := tx.Prepare(`INSERT OR IGNORE INTO beta_codes (code) VALUES (?)`) + if err != nil { + return fmt.Errorf("准备语句失败: %w", err) + } + defer stmt.Close() + + insertedCount := 0 + for _, code := range codes { + result, err := stmt.Exec(code) + if err != nil { + log.Printf("插入内测码 %s 失败: %v", code, err) + continue + } + + if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 { + insertedCount++ + } + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("提交事务失败: %w", err) + } + + log.Printf("✅ 成功加载 %d 个内测码到数据库 (总计 %d 个)", insertedCount, len(codes)) + return nil +} + +// ValidateBetaCode 验证内测码是否有效且未使用 +func (d *Database) ValidateBetaCode(code string) (bool, error) { + var used bool + err := d.db.QueryRow(`SELECT used FROM beta_codes WHERE code = ?`, code).Scan(&used) + if err != nil { + if err == sql.ErrNoRows { + return false, nil // 内测码不存在 + } + return false, err + } + return !used, nil // 内测码存在且未使用 +} + +// UseBetaCode 使用内测码(标记为已使用) +func (d *Database) UseBetaCode(code, userEmail string) error { + result, err := d.db.Exec(` + UPDATE beta_codes SET used = 1, used_by = ?, used_at = CURRENT_TIMESTAMP + WHERE code = ? AND used = 0 + `, userEmail, code) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return fmt.Errorf("内测码无效或已被使用") + } + + return nil +} + +// GetBetaCodeStats 获取内测码统计信息 +func (d *Database) GetBetaCodeStats() (total, used int, err error) { + err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes`).Scan(&total) + if err != nil { + return 0, 0, err + } + + err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes WHERE used = 1`).Scan(&used) + if err != nil { + return 0, 0, err + } + + return total, used, nil } \ No newline at end of file diff --git a/generate_beta_code.sh b/generate_beta_code.sh new file mode 100755 index 00000000..cee2ca3a --- /dev/null +++ b/generate_beta_code.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# 内测码生成脚本 +# 生成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/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 \ No newline at end of file diff --git a/main.go b/main.go index 1d9631a9..69fa8064 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ 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"` DefaultCoins []string `json:"default_coins"` @@ -62,6 +63,7 @@ func syncConfigToDatabase(database *config.Database) error { // 同步各配置项到数据库 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), "coin_pool_api_url": configFile.CoinPoolAPIURL, @@ -105,6 +107,41 @@ func syncConfigToDatabase(database *config.Database) error { return nil } +// loadBetaCodesToDatabase 加载内测码文件到数据库 +func loadBetaCodesToDatabase(database *config.Database) error { + betaCodeFile := "beta_codes.txt" + + // 检查内测码文件是否存在 + if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) { + log.Printf("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile) + return nil + } + + // 获取文件信息 + fileInfo, err := os.Stat(betaCodeFile) + if err != nil { + return fmt.Errorf("获取内测码文件信息失败: %w", err) + } + + log.Printf("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024) + + // 加载内测码到数据库 + err = database.LoadBetaCodesFromFile(betaCodeFile) + if err != nil { + return fmt.Errorf("加载内测码失败: %w", err) + } + + // 显示统计信息 + total, used, err := database.GetBetaCodeStats() + if err != nil { + log.Printf("⚠️ 获取内测码统计失败: %v", err) + } else { + log.Printf("✅ 内测码加载完成: 总计 %d 个,已使用 %d 个,剩余 %d 个", total, used, total-used) + } + + return nil +} + func main() { fmt.Println("╔════════════════════════════════════════════════════════════╗") fmt.Println("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║") @@ -129,6 +166,11 @@ func main() { log.Printf("⚠️ 同步config.json到数据库失败: %v", err) } + // 加载内测码到数据库 + if err := loadBetaCodesToDatabase(database); err != nil { + log.Printf("⚠️ 加载内测码到数据库失败: %v", err) + } + // 获取系统配置 useDefaultCoinsStr, _ := database.GetSystemConfig("use_default_coins") useDefaultCoins := useDefaultCoinsStr == "true" diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 01691aac..c6f23175 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -1,7 +1,8 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; +import { getSystemConfig } from '../lib/config'; export function RegisterPage() { const { language } = useLanguage(); @@ -10,12 +11,23 @@ export function RegisterPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [betaCode, setBetaCode] = useState(''); const [otpCode, setOtpCode] = useState(''); const [userID, setUserID] = useState(''); const [otpSecret, setOtpSecret] = useState(''); const [qrCodeURL, setQrCodeURL] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [betaMode, setBetaMode] = useState(false); + + useEffect(() => { + // 获取系统配置,检查是否开启内测模式 + getSystemConfig().then(config => { + setBetaMode(config.beta_mode || false); + }).catch(err => { + console.error('Failed to fetch system config:', err); + }); + }, []); const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); @@ -31,9 +43,14 @@ export function RegisterPage() { return; } + if (betaMode && !betaCode.trim()) { + setError('内测期间,注册需要提供内测码'); + return; + } + setLoading(true); - const result = await register(email, password); + const result = await register(email, password, betaCode.trim() || undefined); if (result.success && result.userID) { setUserID(result.userID); @@ -137,6 +154,27 @@ export function RegisterPage() { /> + {betaMode && ( +
+ + setBetaCode(e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase())} + className="w-full px-3 py-2 rounded font-mono" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + placeholder="请输入6位内测码" + maxLength={6} + required={betaMode} + /> +

+ 内测码由6位字母数字组成,区分大小写 +

+
+ )} + {error && (
{error} @@ -145,7 +183,7 @@ export function RegisterPage() {