Add beta mode

This commit is contained in:
icy
2025-11-02 02:31:19 +08:00
parent 3c56f25f86
commit 266481caee
6 changed files with 464 additions and 6 deletions

View File

@@ -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{

View File

@@ -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
}

221
generate_beta_code.sh Executable file
View File

@@ -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<length; i++)); do
local random_index=$((RANDOM % ${#charset}))
code+="${charset:$random_index:1}"
done
echo "$code"
}
# 读取现有内测码
read_existing_codes() {
local file="$1"
if [ -f "$file" ]; then
grep -v '^$' "$file" 2>/dev/null | tr -d ' \t' | grep -v '^#' || true
fi
}
# 检查内测码是否已存在
code_exists() {
local code="$1"
local file="$2"
if [ -f "$file" ]; then
grep -Fxq "$code" "$file" 2>/dev/null
else
return 1
fi
}
# 添加内测码到文件
add_code_to_file() {
local code="$1"
local file="$2"
echo "$code" >> "$file"
}
# 验证内测码格式
validate_code() {
local code="$1"
# 检查长度
if [ ${#code} -ne $CODE_LENGTH ]; then
return 1
fi
# 检查字符是否都在允许的字符集中
if [[ ! "$code" =~ ^[$CHARSET]+$ ]]; then
return 1
fi
return 0
}
# 去重并排序内测码
dedupe_and_sort_codes() {
local file="$1"
if [ -f "$file" ]; then
# 过滤空行和注释,去重并排序
grep -v '^$' "$file" | grep -v '^#' | sort -u > "${file}.tmp" && mv "${file}.tmp" "$file"
fi
}
# 解析命令行参数
while getopts "c:lf:h" opt; do
case $opt in
c)
COUNT="$OPTARG"
if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [ "$COUNT" -lt 1 ]; then
echo "错误: count 必须是正整数" >&2
exit 1
fi
;;
l)
LIST_ONLY=true
;;
f)
BETA_CODES_FILE="$OPTARG"
;;
h)
show_help
exit 0
;;
\?)
echo "无效选项: -$OPTARG" >&2
echo "使用 -h 查看帮助信息" >&2
exit 1
;;
esac
done
# 如果是列出现有内测码
if [ "$LIST_ONLY" = true ]; then
if [ -f "$BETA_CODES_FILE" ]; then
existing_codes=$(read_existing_codes "$BETA_CODES_FILE")
if [ -z "$existing_codes" ]; then
echo "内测码列表为空"
else
count=$(echo "$existing_codes" | wc -l | tr -d ' ')
echo "当前内测码 ($count 个):"
echo "$existing_codes" | nl -w3 -s'. '
fi
else
echo "内测码文件不存在: $BETA_CODES_FILE"
fi
exit 0
fi
# 读取现有内测码
existing_codes=$(read_existing_codes "$BETA_CODES_FILE")
# 生成新内测码
new_codes=()
max_attempts=1000 # 防止无限循环
echo "正在生成 $COUNT 个内测码..."
for ((i=1; i<=COUNT; i++)); do
attempts=0
while [ $attempts -lt $max_attempts ]; do
code=$(generate_beta_code $CODE_LENGTH "$CHARSET")
# 验证格式
if ! validate_code "$code"; then
((attempts++))
continue
fi
# 检查是否已存在
if code_exists "$code" "$BETA_CODES_FILE"; then
((attempts++))
continue
fi
# 检查是否与本次生成的重复
duplicate=false
for existing_code in "${new_codes[@]}"; do
if [ "$code" = "$existing_code" ]; then
duplicate=true
break
fi
done
if [ "$duplicate" = false ]; then
new_codes+=("$code")
break
fi
((attempts++))
done
if [ $attempts -eq $max_attempts ]; then
echo "警告: 生成第 $i 个内测码时达到最大尝试次数,可能字符空间不足" >&2
break
fi
done
# 检查是否成功生成了内测码
if [ ${#new_codes[@]} -eq 0 ]; then
echo "未能生成任何新的内测码"
exit 1
fi
# 添加到文件
for code in "${new_codes[@]}"; do
add_code_to_file "$code" "$BETA_CODES_FILE"
done
# 去重并排序
dedupe_and_sort_codes "$BETA_CODES_FILE"
echo "成功生成 ${#new_codes[@]} 个内测码:"
printf ' %s\n' "${new_codes[@]}"
echo
echo "内测码文件: $BETA_CODES_FILE"
# 显示当前总数
if [ -f "$BETA_CODES_FILE" ]; then
total_count=$(read_existing_codes "$BETA_CODES_FILE" | wc -l | tr -d ' ')
echo "当前内测码总计: $total_count"
fi
# 显示文件头部信息(如果是新文件)
if [ ! -s "$BETA_CODES_FILE" ] || [ $(wc -l < "$BETA_CODES_FILE") -eq ${#new_codes[@]} ]; then
echo
echo "内测码规则:"
echo "- 长度: $CODE_LENGTH"
echo "- 字符集: 数字 2-9, 小写字母 a-z (排除 0,1,i,l,o 避免混淆)"
echo "- 每个内测码唯一且不重复"
fi

42
main.go
View File

@@ -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"

View File

@@ -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() {
/>
</div>
{betaMode && (
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
*
</label>
<input
type="text"
value={betaCode}
onChange={(e) => 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}
/>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
6
</p>
</div>
)}
{error && (
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
{error}
@@ -145,7 +183,7 @@ export function RegisterPage() {
<button
type="submit"
disabled={loading}
disabled={loading || (betaMode && !betaCode.trim())}
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }}
>

View File

@@ -10,7 +10,7 @@ interface AuthContextType {
user: User | null;
token: string | null;
login: (email: string, password: string) => Promise<{ success: boolean; message?: string; userID?: string; requiresOTP?: boolean }>;
register: (email: string, password: string) => Promise<{ success: boolean; message?: string; userID?: string; otpSecret?: string; qrCodeURL?: string }>;
register: (email: string, password: string, betaCode?: string) => Promise<{ success: boolean; message?: string; userID?: string; otpSecret?: string; qrCodeURL?: string }>;
verifyOTP: (userID: string, otpCode: string) => Promise<{ success: boolean; message?: string }>;
completeRegistration: (userID: string, otpCode: string) => Promise<{ success: boolean; message?: string }>;
logout: () => void;
@@ -89,14 +89,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return { success: false, message: '未知错误' };
};
const register = async (email: string, password: string) => {
const register = async (email: string, password: string, betaCode?: string) => {
try {
const requestBody: { email: string; password: string; beta_code?: string } = { email, password };
if (betaCode) {
requestBody.beta_code = betaCode;
}
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
body: JSON.stringify(requestBody),
});
const data = await response.json();