package database

// 重要提示：请始终从 backend/ 目录运行服务
// 正确的运行方式：
//   cd d:\Code\verify\verification-platform\backend
//   go run cmd/main.go
//
// 数据库文件路径基于可执行文件所在目录计算，确保无论从哪个目录运行都使用同一个数据库文件。

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"time"
	"verification-platform-backend/internal/config"
	"verification-platform-backend/internal/model"

	"github.com/glebarez/sqlite"
	"github.com/go-redis/redis/v8"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

var (
	DB  *gorm.DB
	RDB *redis.Client
)

func Init() {
	initDatabase()
	initRedis()
}

func initDatabase() {
	dbType := config.GetString("database.type")
	if dbType == "" {
		dbType = "auto"
	}

	switch dbType {
	case "sqlite":
		initSQLite()
	case "mysql":
		initMySQLExplicit()
	default:
		initAutoDetect()
	}
}

func initSQLite() {
	dataDir := config.GetDataDir()
	if err := os.MkdirAll(dataDir, 0755); err != nil {
		log.Fatal("Failed to create data directory:", err.Error())
	}

	dbPath := filepath.Join(dataDir, "verification_platform.db")

	var errOpen error
	DB, errOpen = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
		Logger:                                   logger.Default.LogMode(logger.Silent),
		DisableForeignKeyConstraintWhenMigrating: true,
	})
	if errOpen != nil {
		log.Fatal("Failed to create SQLite database:", errOpen.Error())
	}
	log.Printf("Using SQLite database: %s\n", dbPath)

	if sqlDB, err := DB.DB(); err == nil {
		sqlDB.SetMaxIdleConns(10)
		sqlDB.SetMaxOpenConns(100)
		sqlDB.SetConnMaxLifetime(time.Hour)
	}

	runMigrations()
}

func initMySQLExplicit() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
		config.GetString("database.username"),
		config.GetString("database.password"),
		config.GetString("database.host"),
		config.GetString("database.port"),
		config.GetString("database.name"),
		config.GetString("database.charset"),
	)

	var err error
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger:                                   logger.Default.LogMode(logger.Silent),
		DisableForeignKeyConstraintWhenMigrating: true,
	})
	if err != nil {
		log.Fatal("Failed to connect to MySQL:", err.Error())
	}
	log.Printf("Connected to MySQL: %s@%s:%s/%s\n",
		config.GetString("database.username"),
		config.GetString("database.host"),
		config.GetString("database.port"),
		config.GetString("database.name"))

	if sqlDB, err := DB.DB(); err == nil {
		sqlDB.SetMaxIdleConns(config.GetInt("database.max_idle_conns"))
		sqlDB.SetMaxOpenConns(config.GetInt("database.max_open_conns"))
		sqlDB.SetConnMaxLifetime(time.Hour)
	}

	runMigrations()
}

func initAutoDetect() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
		config.GetString("database.username"),
		config.GetString("database.password"),
		config.GetString("database.host"),
		config.GetString("database.port"),
		config.GetString("database.name"),
		config.GetString("database.charset"),
	)

	var err error
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger:                                   logger.Default.LogMode(logger.Silent),
		DisableForeignKeyConstraintWhenMigrating: true,
	})

	if err != nil {
		log.Println("Failed to connect to MySQL, using SQLite database:", err.Error())

		dataDir := config.GetDataDir()
		dbPath := filepath.Join(dataDir, "verification_platform.db")

		DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
			Logger:                                   logger.Default.LogMode(logger.Silent),
			DisableForeignKeyConstraintWhenMigrating: true,
		})
		if err != nil {
			log.Fatal("Failed to create SQLite database:", err.Error())
		}
		log.Printf("Using SQLite database: %s\n", dbPath)
	}

	if sqlDB, err := DB.DB(); err == nil {
		sqlDB.SetMaxIdleConns(config.GetInt("database.max_idle_conns"))
		sqlDB.SetMaxOpenConns(config.GetInt("database.max_open_conns"))
		sqlDB.SetConnMaxLifetime(time.Hour)
	}

	runMigrations()
}

func runMigrations() {
	var columns []struct {
		CID        int
		Name       string
		Type       string
		NotNull    int
		DefaultVal interface{}
		PK         int
	}
	DB.Raw("PRAGMA table_info(applications)").Scan(&columns)
	log.Println("Applications table columns:")
	for _, col := range columns {
		log.Printf("  - %s (%s)\n", col.Name, col.Type)
	}

	// 检查app_users表结构
	var appUserColumns []struct {
		CID        int
		Name       string
		Type       string
		NotNull    int
		DefaultVal interface{}
		PK         int
	}
	DB.Raw("PRAGMA table_info(app_users)").Scan(&appUserColumns)
	log.Println("AppUsers table columns:")
	for _, col := range appUserColumns {
		log.Printf("  - %s (%s)\n", col.Name, col.Type)
	}

	// 自动迁移数据库表
	if err := DB.AutoMigrate(
		&model.User{},
		&model.AppUser{},
		&model.UserLevel{},
		&model.Application{},
		&model.Version{},
		&model.CardType{},
		&model.Card{},
		&model.AgentApplication{},
		&model.AgentApplicationCardType{},
		&model.AgentApplicationRequest{},
		&model.Order{},
		&model.Log{},
		&model.RechargeRecord{},
		&model.ConsumptionRecord{},
		&model.Ticket{},
		&model.TicketReply{},
		&model.UserVariable{},
		&model.CloudConstant{},
		&model.CloudVariable{},
		&model.IPBlacklist{},
		&model.DeviceBlacklist{},
		&model.AbnormalBehavior{},
		&model.DocCategory{},
		&model.Doc{},
		&model.Package{},
		&model.PackagePermission{},
		&model.Setting{},
		&model.Announcement{},
		&model.UserPackage{},
		&model.UserDevice{},
		&model.UserIP{},
		&model.DeviceSession{},
		&model.PackHistory{},
		&model.WebhookConfig{},
		&model.WebhookLog{},
		&model.ExtensionAPIKey{},
		&model.DynamicCode{},
		&model.PaymentChannel{},
		&model.EmailConfig{},
		&model.SmsConfig{},
		&model.StorageConfig{},
		&model.Captcha{},
		&model.ApiUsage{},
		&model.StorageUsage{},
		&model.UsageAlertRecord{},
		&model.Notification{},
		&model.RiskControlRule{},
		&model.EmailVerifyCode{},
		&model.SmsVerifyCode{},
		&model.AppSMTPConfig{},
		&model.EmailTemplate{},
		&model.VersionFile{},
		&model.CloudVariableRecord{},
	); err != nil {
		log.Println("Failed to migrate database:", err.Error())
	}

	// 手动迁移：检查并添加 risk_control_rules 表的 user_id 列
	var riskControlColumns []struct {
		Name string
	}
	DB.Raw("PRAGMA table_info(risk_control_rules)").Scan(&riskControlColumns)
	hasUserID := false
	for _, col := range riskControlColumns {
		if col.Name == "user_id" {
			hasUserID = true
			break
		}
	}
	if !hasUserID {
		log.Println("Adding user_id column to risk_control_rules table...")
		if err := DB.Exec("ALTER TABLE risk_control_rules ADD COLUMN user_id INTEGER").Error; err != nil {
			log.Println("Failed to add user_id column:", err.Error())
		} else {
			log.Println("user_id column added successfully")
		}
	}

	// 修复 application_id 列的 NOT NULL 约束（SQLite需要重建表）
	var riskControlTableInfo []struct {
		CID        int
		Name       string
		Type       string
		NotNull    int
		DefaultVal interface{}
		PK         int
	}
	DB.Raw("PRAGMA table_info(risk_control_rules)").Scan(&riskControlTableInfo)
	for _, col := range riskControlTableInfo {
		if col.Name == "application_id" && col.NotNull == 1 {
			log.Println("Fixing application_id NOT NULL constraint...")
			// 创建临时表
			DB.Exec(`CREATE TABLE risk_control_rules_new (
				id INTEGER PRIMARY KEY,
				user_id INTEGER,
				application_id INTEGER,
				type TEXT NOT NULL,
				value TEXT NOT NULL,
				reason TEXT,
				status TEXT DEFAULT 'active',
				expires_at DATETIME,
				created_at DATETIME,
				updated_at DATETIME,
				deleted_at DATETIME
			)`)
			// 复制数据
			DB.Exec(`INSERT INTO risk_control_rules_new SELECT * FROM risk_control_rules`)
			// 删除旧表
			DB.Exec(`DROP TABLE risk_control_rules`)
			// 重命名新表
			DB.Exec(`ALTER TABLE risk_control_rules_new RENAME TO risk_control_rules`)
			// 重建索引
			DB.Exec(`CREATE INDEX IF NOT EXISTS idx_risk_control_rules_user_id ON risk_control_rules(user_id)`)
			DB.Exec(`CREATE INDEX IF NOT EXISTS idx_risk_control_rules_application_id ON risk_control_rules(application_id)`)
			DB.Exec(`CREATE INDEX IF NOT EXISTS idx_risk_control_rules_deleted_at ON risk_control_rules(deleted_at)`)
			log.Println("application_id NOT NULL constraint fixed")
			break
		}
	}

	// 手动迁移：检查并添加 packages 表的 currency 列
	var packageColumns []struct {
		Name string
	}
	DB.Raw("PRAGMA table_info(packages)").Scan(&packageColumns)
	hasCurrency := false
	for _, col := range packageColumns {
		if col.Name == "currency" {
			hasCurrency = true
			break
		}
	}
	if !hasCurrency {
		log.Println("Adding currency column to packages table...")
		if err := DB.Exec("ALTER TABLE packages ADD COLUMN currency TEXT DEFAULT 'CNY'").Error; err != nil {
			log.Println("Failed to add currency column:", err.Error())
		} else {
			log.Println("currency column added successfully")
		}
	}

	// 初始化数据
	initData()

	// 初始化工单数据（独立于initData，确保总是被调用）
	initTicketData()

	// 初始化文档分类和文档
	initDocData()
}

// initData 初始化数据库中的基础数据
func initData() {
	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)

	// 修复现有的授权记录，将 is_received 设置为 true
	DB.Model(&model.AgentApplication{}).Where("is_received = ?", false).Update("is_received", true)

	// 确保管理员用户存在
	var adminCount int64
	DB.Model(&model.User{}).Where("username = ?", "admin").Count(&adminCount)
	if adminCount == 0 {
		log.Println("Creating admin user...")
		adminEmail := "admin@example.com"
		adminUser := model.User{
			Username: "admin",
			Email:    &adminEmail,
			Password: string(hashedPassword),
			Role:     "admin",
			Status:   "active",
		}
		DB.Create(&adminUser)
	}

	// 确保开发者用户存在
	var devCount int64
	DB.Model(&model.User{}).Where("username = ?", "dev").Count(&devCount)
	if devCount == 0 {
		log.Println("Creating dev user...")
		devEmail := "dev@example.com"
		devUser := model.User{
			Username: "dev",
			Email:    &devEmail,
			Password: string(hashedPassword),
			Role:     "admin",
			Status:   "active",
		}
		DB.Create(&devUser)
	}

	// 确保代理用户存在
	var agentCount int64
	DB.Model(&model.User{}).Where("username = ?", "agent").Count(&agentCount)
	if agentCount == 0 {
		log.Println("Creating agent user...")
		agentEmail := "agent@example.com"
		agentUser := model.User{
			Username: "agent",
			Email:    &agentEmail,
			Password: string(hashedPassword),
			Role:     "agent",
			Status:   "active",
		}
		DB.Create(&agentUser)
	}

	// 迁移：将 developer 角色统一为 admin
	var devRoleCount int64
	DB.Model(&model.User{}).Where("role = ?", "developer").Count(&devRoleCount)
	if devRoleCount > 0 {
		log.Printf("Migrating %d developer users to admin role...", devRoleCount)
		DB.Model(&model.User{}).Where("role = ?", "developer").Update("role", "admin")
	}

	// 迁移：将 developer_id 列名改为 admin_id
	if DB.Migrator().HasColumn(&model.AgentApplication{}, "developer_id") {
		log.Println("Migrating agent_applications.developer_id to admin_id...")
		DB.Exec("ALTER TABLE agent_applications RENAME COLUMN developer_id TO admin_id")
	}
	if DB.Migrator().HasColumn(&model.AgentApplicationRequest{}, "developer_id") {
		log.Println("Migrating agent_application_requests.developer_id to admin_id...")
		DB.Exec("ALTER TABLE agent_application_requests RENAME COLUMN developer_id TO admin_id")
	}

	// 迁移：将 write_permission 默认值从 developer 改为 admin
	DB.Exec("UPDATE cloud_variables SET write_permission = 'admin' WHERE write_permission = 'developer'")

	// 清理没有关联应用的云端常量和变量
	var orphanConstants int64
	DB.Model(&model.CloudConstant{}).Where("app_id IS NULL").Count(&orphanConstants)
	if orphanConstants > 0 {
		log.Printf("Cleaning up %d orphan cloud constants...", orphanConstants)
		DB.Where("app_id IS NULL").Delete(&model.CloudConstant{})
	}

	var orphanVariables int64
	DB.Model(&model.CloudVariable{}).Where("app_id IS NULL").Count(&orphanVariables)
	if orphanVariables > 0 {
		log.Printf("Cleaning up %d orphan cloud variables...", orphanVariables)
		DB.Where("app_id IS NULL").Delete(&model.CloudVariable{})
	}

	var localStorageCount int64
	DB.Model(&model.StorageConfig{}).Where("type = ?", "local").Count(&localStorageCount)
	if localStorageCount == 0 {
		log.Println("Creating default local storage config...")
		defaultStorage := model.StorageConfig{
			Name:      "本地存储",
			Type:      "local",
			IsDefault: true,
			Status:    "active",
			Remark:    "系统默认本地存储",
		}
		DB.Create(&defaultStorage)
	} else {
		DB.Model(&model.StorageConfig{}).Where("type = ? AND (status = '' OR status IS NULL)", "local").Update("status", "active")
	}

	var bepusdtCount int64
	DB.Model(&model.PaymentChannel{}).Where("type = ?", "bepusdt").Count(&bepusdtCount)
	if bepusdtCount == 0 {
		log.Println("Creating BEPUSDT example payment channel...")
		bepusdtChannel := model.PaymentChannel{
			Name:   "USDT支付",
			Type:   "bepusdt",
			Icon:   "",
			Config: `{"api_url": "http://your-bepusdt-server:8080", "token": "your-api-token-here"}`,
			Sort:   1,
			Status: "inactive",
			Remark: "BEPUSDT示例配置，请修改api_url和token后启用",
		}
		DB.Create(&bepusdtChannel)
	}

	initDocData()
}

func initDocData() {
	var categoryCount int64
	DB.Model(&model.DocCategory{}).Count(&categoryCount)
	
	if categoryCount > 0 {
		log.Println("Document data already exists, skipping initialization")
		return
	}
	
	log.Println("Initializing document data...")

	categories := []model.DocCategory{
		{Name: "快速开始", NameEn: "Quick Start", Slug: "quickstart", Description: "5分钟内完成集成", DescriptionEn: "Complete integration in 5 minutes", Icon: "zap", Sort: 1},
		{Name: "API文档", NameEn: "API Documentation", Slug: "api", Description: "完整的API接口说明", DescriptionEn: "Complete API reference", Icon: "code", Sort: 2},
		{Name: "示例代码", NameEn: "Code Examples", Slug: "examples", Description: "常见使用场景示例", DescriptionEn: "Common use case examples", Icon: "terminal", Sort: 3},
		{Name: "常见问题", NameEn: "FAQ", Slug: "faq", Description: "解答常见疑问", DescriptionEn: "Frequently asked questions", Icon: "help-circle", Sort: 4},
	}

	for i := range categories {
		DB.Create(&categories[i])
	}

	var quickstartCat, apiCat, examplesCat, faqCat model.DocCategory
	DB.Where("slug = ?", "quickstart").First(&quickstartCat)
	DB.Where("slug = ?", "api").First(&apiCat)
	DB.Where("slug = ?", "examples").First(&examplesCat)
	DB.Where("slug = ?", "faq").First(&faqCat)

	docs := []struct {
		CategoryID *uint
		Title      string
		TitleEn    string
		Slug       string
		Content    string
		ContentEn  string
		Summary    string
		SummaryEn  string
		Sort       int
	}{
		{
			CategoryID: &apiCat.ID,
			Title:      "用户注册",
			TitleEn:    "User Registration",
			Slug:       "api-register",
			Content:    "# 用户注册\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/register\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| password | string | 是 | 密码 |\n| device_id | string | 是 | 设备指纹，设备唯一标识 |\n| device_name | string | 否 | 设备名称，如\"我的电脑\"，不传则使用device_id |\n| email | string | 条件必填 | 邮箱地址，启用邮箱验证时必填 |\n| email_code | string | 条件必填 | 邮箱验证码，启用强制邮箱验证时必填 |\n| device_type | string | 否 | 设备类型：android/ios/windows/mac/linux/web，不传则自动识别 |\n| instance_id | string | 否 | 实例ID，用于多开识别，不传则使用device_id |\n\n### 请求示例\n```json\n{\n  \"username\": \"user123\",\n  \"password\": \"password123\",\n  \"device_id\": \"abc123def456\",\n  \"device_name\": \"我的电脑\",\n  \"email\": \"user@example.com\",\n  \"email_code\": \"123456\",\n  \"device_type\": \"windows\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"注册成功\",\n  \"data\": {\n    \"user_id\": 123\n  }\n}\n```\n\n### 说明\n- device_id 是设备指纹，用于唯一标识设备\n- device_name 是设备的友好名称，用于显示\n- device_type 不传时，系统会根据 User-Agent 自动识别\n- 若应用启用了强制邮箱验证，则 email 和 email_code 为必填参数\n\n### 错误响应\n```json\n{\n  \"code\": 400,\n  \"message\": \"用户名已存在\"\n}\n```\n\n### 邮箱验证错误\n```json\n{\n  \"code\": 400,\n  \"message\": \"验证码错误或已过期\"\n}\n```",
			ContentEn: "# User Registration\n\n### Endpoint\n```\nPOST /api/v1/app/:appKey/register\n```\n\n### Request Parameters\n| Parameter | Type | Required | Description |\n|--------|------|------|------|\n| username | string | Yes | Username |\n| password | string | Yes | Password |\n| device_id | string | Yes | Device fingerprint, unique device identifier |\n| device_name | string | No | Device name, e.g. \"My Computer\", defaults to device_id |\n| email | string | Conditional | Email address, required when email verification is enabled |\n| email_code | string | Conditional | Email verification code, required when mandatory email verification is enabled |\n| device_type | string | No | Device type: android/ios/windows/mac/linux/web, auto-detected if not provided |\n| instance_id | string | No | Instance ID for multi-instance detection, defaults to device_id |\n\n### Request Example\n```json\n{\n  \"username\": \"user123\",\n  \"password\": \"password123\",\n  \"device_id\": \"abc123def456\",\n  \"device_name\": \"My Computer\",\n  \"email\": \"user@example.com\",\n  \"email_code\": \"123456\",\n  \"device_type\": \"windows\"\n}\n```\n\n### Response Example\n```json\n{\n  \"code\": 200,\n  \"message\": \"Registration successful\",\n  \"data\": {\n    \"user_id\": 123\n  }\n}\n```\n\n### Notes\n- device_id is the device fingerprint used to uniquely identify the device\n- device_name is a friendly name for the device display\n- device_type is auto-detected from User-Agent if not provided\n- If mandatory email verification is enabled, email and email_code are required\n\n### Error Response\n```json\n{\n  \"code\": 400,\n  \"message\": \"Username already exists\"\n}\n```\n\n### Email Verification Error\n```json\n{\n  \"code\": 400,\n  \"message\": \"Invalid or expired verification code\"\n}\n```",
			Summary:    "用户注册说明",
			SummaryEn:  "User registration instructions",
			Sort:       1,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "发送邮箱验证码",
			TitleEn:    "Send Email Verification Code",
			Slug:       "api-send-email-code",
			Content:    "# 发送邮箱验证码\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/send-email-code\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| email | string | 是 | 邮箱地址 |\n| purpose | string | 否 | 用途：register(注册)、reset_password(重置密码)、change_email(更换邮箱)，默认register |\n\n### 请求示例\n```json\n{\n  \"email\": \"user@example.com\",\n  \"purpose\": \"register\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"success\",\n  \"data\": {\n    \"message\": \"验证码已发送\"\n  }\n}\n```\n\n### 说明\n- 验证码有效期为15分钟\n- 若应用未启用邮箱验证，会返回错误\n\n### 错误响应\n```json\n{\n  \"code\": 400,\n  \"message\": \"该应用未启用邮箱验证\"\n}\n```",
			ContentEn: "# Send Email Verification Code\n\n### Endpoint\n```\nPOST /api/v1/app/:appKey/send-email-code\n```\n\n### Request Parameters\n| Parameter | Type | Required | Description |\n|--------|------|------|------|\n| email | string | Yes | Email address |\n| purpose | string | No | Purpose: register, reset_password, change_email, defaults to register |\n\n### Request Example\n```json\n{\n  \"email\": \"user@example.com\",\n  \"purpose\": \"register\"\n}\n```\n\n### Response Example\n```json\n{\n  \"code\": 200,\n  \"message\": \"success\",\n  \"data\": {\n    \"message\": \"Verification code sent\"\n  }\n}\n```\n\n### Notes\n- Verification code is valid for 15 minutes\n- Returns error if email verification is not enabled for the application\n\n### Error Response\n```json\n{\n  \"code\": 400,\n  \"message\": \"Email verification is not enabled for this application\"\n}\n```",
			Summary:    "发送邮箱验证码说明",
			SummaryEn:  "Send email verification code instructions",
			Sort:       2,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "用户登录",
			TitleEn:    "User Login",
			Slug:       "api-login",
			Content:    "# 用户登录\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/login\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| password | string | 是 | 密码 |\n| device_id | string | 是 | 设备指纹，设备唯一标识 |\n| device_name | string | 否 | 设备名称，如\"我的电脑\"，不传则使用device_id |\n| device_type | string | 否 | 设备类型：android/ios/windows/mac/linux/web，不传则自动识别 |\n| instance_id | string | 否 | 实例ID，用于多开识别，不传则使用device_id |\n\n### 请求示例\n```json\n{\n  \"username\": \"user123\",\n  \"password\": \"password123\",\n  \"device_id\": \"abc123def456\",\n  \"device_name\": \"我的电脑\",\n  \"device_type\": \"windows\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"登录成功\",\n  \"data\": {\n    \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n    \"user_id\": 123\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| token | string | JWT令牌，用于后续认证 |\n| user_id | number | 用户ID |\n\n### 说明\n- device_id 是设备指纹，用于唯一标识设备\n- device_name 是设备的友好名称，用于显示\n- device_type 不传时，系统会根据 User-Agent 自动识别\n- 若设备绑定数量已达上限，会返回错误并显示已绑定设备列表\n\n### 错误响应\n```json\n{\n  \"code\": 401,\n  \"message\": \"用户名或密码错误\"\n}\n```\n\n### 设备上限错误响应\n```json\n{\n  \"code\": 403,\n  \"message\": \"设备绑定数量已达上限，请解绑后再试\",\n  \"data\": {\n    \"error_code\": \"DEVICE_LIMIT_EXCEEDED\",\n    \"max_devices\": 3,\n    \"device_count\": 3,\n    \"devices\": [\n      {\n        \"id\": 1,\n        \"device_id\": \"abc123def456\",\n        \"device_name\": \"我的电脑\",\n        \"device_type\": \"windows\",\n        \"online_count\": 1,\n        \"created_at\": \"2024-03-04T12:00:00Z\"\n      }\n    ]\n  }\n}\n```",
			ContentEn: "# User Login\n\n### Endpoint\n```\nPOST /api/v1/app/:appKey/login\n```\n\n### Request Parameters\n| Parameter | Type | Required | Description |\n|--------|------|------|------|\n| username | string | Yes | Username |\n| password | string | Yes | Password |\n| device_id | string | Yes | Device fingerprint, unique device identifier |\n| device_name | string | No | Device name, e.g. \"My Computer\", defaults to device_id |\n| device_type | string | No | Device type: android/ios/windows/mac/linux/web, auto-detected if not provided |\n| instance_id | string | No | Instance ID for multi-instance detection, defaults to device_id |\n\n### Request Example\n```json\n{\n  \"username\": \"user123\",\n  \"password\": \"password123\",\n  \"device_id\": \"abc123def456\",\n  \"device_name\": \"My Computer\",\n  \"device_type\": \"windows\"\n}\n```\n\n### Response Example\n```json\n{\n  \"code\": 200,\n  \"message\": \"Login successful\",\n  \"data\": {\n    \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n    \"user_id\": 123\n  }\n}\n```\n\n### Response Fields\n| Field | Type | Description |\n|--------|------|------|\n| token | string | JWT token for subsequent authentication |\n| user_id | number | User ID |\n\n### Notes\n- device_id is the device fingerprint used to uniquely identify the device\n- device_name is a friendly name for the device display\n- device_type is auto-detected from User-Agent if not provided\n- Returns error with bound device list if device limit is exceeded\n\n### Error Response\n```json\n{\n  \"code\": 401,\n  \"message\": \"Invalid username or password\"\n}\n```\n\n### Device Limit Exceeded Response\n```json\n{\n  \"code\": 403,\n  \"message\": \"Device limit exceeded, please unbind a device first\",\n  \"data\": {\n    \"error_code\": \"DEVICE_LIMIT_EXCEEDED\",\n    \"max_devices\": 3,\n    \"device_count\": 3,\n    \"devices\": [\n      {\n        \"id\": 1,\n        \"device_id\": \"abc123def456\",\n        \"device_name\": \"My Computer\",\n        \"device_type\": \"windows\",\n        \"online_count\": 1,\n        \"created_at\": \"2024-03-04T12:00:00Z\"\n      }\n    ]\n  }\n}\n```",
			Summary:    "用户登录说明",
			SummaryEn:  "User login instructions",
			Sort:       3,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "心跳验证",
			Slug:       "api-heartbeat",
			Content:    "# 心跳验证\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/heartbeat\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| device_id | string | 是 | 设备ID |\n\n### 请求示例\n```json\n{\n  \"device_id\": \"device_001\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"success\"\n}\n```\n\n### 说明\n- 保持用户在线状态\n- 建议每30秒调用一次\n- 超过一定时间未调用心跳，用户将被视为离线\n- 需要在请求头中携带JWT token",
			Summary:    "心跳验证说明",
			Sort:       4,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "卡密充值",
			Slug:       "api-recharge",
			Content:    "# 卡密充值\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/recharge\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户账号 |\n| card_key | string | 是 | 卡密 |\n\n### 请求示例\n```json\n{\n  \"username\": \"user123\",\n  \"card_key\": \"VIP123456\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"充值成功\",\n  \"data\": {\n    \"value\": 30\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| value | number | 充值值（根据卡密类型不同，含义不同）|\n\n### 错误响应\n```json\n{\n  \"code\": 400,\n  \"message\": \"卡密无效或已过期\"\n}\n```",
			Summary:    "卡密充值说明",
			Sort:       5,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取绑定设备列表",
			Slug:       "api-devices",
			Content:    "# 获取绑定设备列表\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/devices\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"devices\": [\n      {\n        \"device_id\": \"device_001\",\n        \"device_name\": \"我的电脑\",\n        \"bind_time\": \"2024-03-04T12:00:00Z\",\n        \"last_active\": \"2024-03-04T13:00:00Z\"\n      }\n    ]\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| device_id | string | 设备ID |\n| device_name | string | 设备名称 |\n| bind_time | string | 绑定时间 |\n| last_active | string | 最后活跃时间 |",
			Summary:    "获取绑定设备列表说明",
			Sort:       6,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "解绑设备",
			Slug:       "api-unbind-device",
			Content:    "# 解绑设备\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/unbind-device\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| device_id | string | 是 | 设备ID |\n\n### 请求示例\n```json\n{\n  \"device_id\": \"device_001\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"解绑成功\"\n}\n```\n\n### 错误响应\n```json\n{\n  \"code\": 400,\n  \"message\": \"设备不存在或未绑定\"\n}\n```",
			Summary:    "解绑设备说明",
			Sort:       7,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "通过认证解绑设备",
			Slug:       "api-unbind-device-with-auth",
			Content:    "# 通过认证解绑设备\n\n当用户因设备数量达到上限无法登录时，可以使用此接口通过用户名和密码验证来解绑设备。\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/unbind-device-with-auth\n```\n\n### 说明\n此接口不需要 Authorization header，使用用户名和密码进行身份验证。\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| password | string | 是 | 密码 |\n| device_id | string | 是 | 要解绑的设备ID |\n\n### 请求示例\n```json\n{\n  \"username\": \"user123\",\n  \"password\": \"password123\",\n  \"device_id\": \"device_001\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"message\": \"解绑成功\"\n  }\n}\n```\n\n### 错误响应\n\n**参数错误**\n```json\n{\n  \"code\": 400,\n  \"message\": \"用户名、密码和设备ID不能为空\"\n}\n```\n\n**密码错误**\n```json\n{\n  \"code\": 401,\n  \"message\": \"密码错误\"\n}\n```\n\n**用户不存在**\n```json\n{\n  \"code\": 404,\n  \"message\": \"用户不存在\"\n}\n```\n\n**设备不存在**\n```json\n{\n  \"code\": 404,\n  \"message\": \"设备不存在\"\n}\n```\n\n**解绑失败**\n```json\n{\n  \"code\": 500,\n  \"message\": \"解绑设备失败\"\n}\n```\n\n### 使用场景\n1. 用户登录时收到\"设备数量已达上限\"的错误\n2. 前端引导用户使用用户名+密码验证方式解绑设备\n3. 用户输入用户名、密码和要解绑的设备ID\n4. 调用此接口完成解绑\n5. 解绑成功后，用户可以正常登录",
			Summary:    "通过认证解绑设备说明",
			Sort:       8,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "重置密码（邮箱验证码）",
			Slug:       "api-reset-password",
			Content:    "# 重置密码（邮箱验证码）\n\n当用户忘记密码时，可以通过邮箱验证码重置密码。\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/reset-password\n```\n\n### 说明\n此接口不需要 Authorization header，需要先调用发送邮箱验证码接口获取验证码。\n\n### 前提条件\n- 应用已启用密码重置功能\n- 用户套餐支持密码重置功能\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| email | string | 是 | 邮箱地址 |\n| code | string | 是 | 邮箱验证码 |\n| password | string | 是 | 新密码（至少6位） |\n\n### 请求示例\n```json\n{\n  \"email\": \"user@example.com\",\n  \"code\": \"123456\",\n  \"password\": \"newpassword123\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"message\": \"密码重置成功\"\n  }\n}\n```\n\n### 错误响应\n\n**应用未启用密码重置**\n```json\n{\n  \"code\": 400,\n  \"message\": \"该应用未启用密码重置功能\"\n}\n```\n\n**套餐不支持**\n```json\n{\n  \"code\": 403,\n  \"message\": \"当前套餐不支持密码重置功能\"\n}\n```\n\n**验证码无效**\n```json\n{\n  \"code\": 400,\n  \"message\": \"验证码无效或已过期\"\n}\n```\n\n**验证码已过期**\n```json\n{\n  \"code\": 400,\n  \"message\": \"验证码已过期\"\n}\n```\n\n**用户不存在**\n```json\n{\n  \"code\": 404,\n  \"message\": \"用户不存在\"\n}\n```\n\n### 使用流程\n1. 调用 `POST /api/v1/app/:appKey/send-email-code` 发送验证码，purpose 设为 `reset_password`\n2. 用户收到验证码后调用此接口重置密码",
			Summary:    "通过邮箱验证码重置密码",
			Sort:       9,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "修改密码",
			Slug:       "api-change-password",
			Content:    "# 修改密码\n\n通过用户名和原密码验证来修改密码。\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/change-password\n```\n\n### 说明\n此接口不需要 Authorization header，使用用户名和原密码进行身份验证。\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| old_password | string | 是 | 原密码 |\n| new_password | string | 是 | 新密码（至少6位） |\n\n### 请求示例\n```json\n{\n  \"username\": \"user123\",\n  \"old_password\": \"oldpassword123\",\n  \"new_password\": \"newpassword123\"\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"message\": \"密码修改成功\"\n  }\n}\n```\n\n### 错误响应\n\n**参数错误**\n```json\n{\n  \"code\": 400,\n  \"message\": \"参数错误\"\n}\n```\n\n**用户不存在**\n```json\n{\n  \"code\": 404,\n  \"message\": \"用户不存在\"\n}\n```\n\n**原密码错误**\n```json\n{\n  \"code\": 400,\n  \"message\": \"原密码错误\"\n}\n```\n\n### 使用场景\n- 用户记得原密码时修改密码\n- 无需邮箱验证的密码修改场景",
			Summary:    "修改密码",
			Sort:       10,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取设备数量",
			Slug:       "api-device-count",
			Content:    "# 获取设备数量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/device-count\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"count\": 3,\n    \"max_devices\": 5\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| count | number | 当前绑定设备数量 |\n| max_devices | number | 最大允许设备数量 |",
			Summary:    "获取设备数量说明",
			Sort:       11,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取应用信息",
			Slug:       "api-app-info",
			Content:    "# 获取应用信息\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/info\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"app_id\": 1,\n    \"name\": \"我的应用\",\n    \"description\": \"游戏验证应用\",\n    \"status\": \"active\"\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| app_id | number | 应用ID |\n| name | string | 应用名称 |\n| description | string | 应用描述 |\n| status | string | 应用状态 |",
			Summary:    "获取应用信息说明",
			Sort:       12,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取用户账户信息",
			Slug:       "api-account",
			Content:    "# 获取用户账户信息\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/account\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"user_id\": 123,\n    \"username\": \"user123\",\n    \"expire_time\": \"2024-04-04T00:00:00Z\",\n    \"remaining_days\": 30\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| user_id | number | 用户ID |\n| username | string | 用户名 |\n| expire_time | string | 过期时间 |\n| remaining_days | number | 剩余天数 |",
			Summary:    "获取用户账户信息说明",
			Sort:       13,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取应用常量",
			Slug:       "api-constants",
			Content:    "# 获取应用常量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/constants\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"常量名称\",\n      \"key\": \"CONSTANT_KEY\",\n      \"value\": \"常量值\",\n      \"var_type\": \"string\",\n      \"description\": \"常量描述\"\n    }\n  ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 常量ID |\n| name | string | 常量名称 |\n| key | string | 常量键 |\n| value | string | 常量值 |\n| var_type | string | 变量类型 |\n| description | string | 描述 |",
			Summary:    "获取应用常量说明",
			Sort:       14,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取指定常量",
			Slug:       "api-constant",
			Content:    "# 获取指定常量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/constants/:key\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 路径参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| key | string | 是 | 常量key |\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"id\": 1,\n    \"name\": \"常量名称\",\n    \"key\": \"CONSTANT_KEY\",\n    \"value\": \"常量值\",\n    \"var_type\": \"string\",\n    \"description\": \"常量描述\"\n  }\n}\n```\n\n### 错误响应\n```json\n{\n  \"code\": 404,\n  \"message\": \"常量不存在\"\n}\n```",
			Summary:    "获取指定常量说明",
			Sort:       15,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取应用变量",
			Slug:       "api-variables",
			Content:    "# 获取应用变量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/variables\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"变量名称\",\n      \"key\": \"VARIABLE_KEY\",\n      \"default_value\": \"默认值\",\n      \"var_type\": \"string\",\n      \"scope\": \"user\",\n      \"description\": \"变量描述\"\n    }\n  ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 变量ID |\n| name | string | 变量名称 |\n| key | string | 变量键 |\n| default_value | string | 默认值 |\n| var_type | string | 变量类型 |\n| scope | string | 作用域 |\n| description | string | 描述 |",
			Summary:    "获取应用变量说明",
			Sort:       16,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取指定变量",
			Slug:       "api-variable",
			Content:    "# 获取指定变量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/variables/:key\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 路径参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| key | string | 是 | 变量key |\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"id\": 1,\n    \"name\": \"变量名称\",\n    \"key\": \"VARIABLE_KEY\",\n    \"default_value\": \"默认值\",\n    \"var_type\": \"string\",\n    \"scope\": \"user\",\n    \"description\": \"变量描述\"\n  }\n}\n```\n\n### 错误响应\n```json\n{\n  \"code\": 404,\n  \"message\": \"变量不存在\"\n}\n```",
			Summary:    "获取指定变量说明",
			Sort:       17,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "更新用户变量",
			Slug:       "api-update-variables",
			Content:    "# 更新用户变量\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/variables\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| variables | object | 是 | 变量键值对 |\n\n### 请求示例\n```json\n{\n  \"variables\": {\n    \"nickname\": \"新昵称\",\n    \"level\": 10\n  }\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"更新成功\"\n}\n```\n\n### 说明\n- 只能更新作用域为user的变量\n- 变量值类型需要与定义的类型匹配",
			Summary:    "更新用户变量说明",
			Sort:       18,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "调用云端函数",
			Slug:       "api-call-function",
			Content:    "# 调用云端函数\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/call-function\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| function_name | string | 是 | 云端函数名称 |\n| params | object | 否 | 传递给云端函数的参数 |\n\n### 请求示例\n```json\n{\n  \"function_name\": \"calculatePrice\",\n  \"params\": {\n    \"x\": 10,\n    \"y\": 20\n  }\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"操作成功\",\n  \"data\": {\n    \"result\": 30,\n    \"execution_time\": 0.001234\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| result | any | 云端函数执行结果 |\n| execution_time | float | 执行时间（秒） |\n\n### 错误响应\n```json\n{\n  \"code\": 400,\n  \"message\": \"代码执行错误: ReferenceError: x is not defined\"\n}\n```\n\n---\n\n## 云端函数编写指南\n\n云端函数使用 JavaScript 语法编写，在服务端安全执行。\n\n### 内置变量\n\n| 变量名 | 类型 | 说明 |\n|--------|------|------|\n| app | object | 当前应用信息 |\n| user | object | 当前登录用户信息（需传入user_id） |\n| params | object | 自定义传入的参数 |\n\n### 示例1：简单计算\n\n```javascript\n// 云端函数代码\nvar x = params.x || 0;\nvar y = params.y || 0;\nreturn x + y;\n```\n\n### 示例2：条件判断\n\n```javascript\n// 根据用户等级返回折扣\nvar level = user.level || 1;\nvar discount = 1.0;\n\nif (level >= 5) {\n  discount = 0.7;\n} else if (level >= 3) {\n  discount = 0.8;\n} else if (level >= 2) {\n  discount = 0.9;\n}\n\nreturn {\n  discount: discount,\n  message: \"您的等级为\" + level + \"，享受\" + (discount * 10) + \"折优惠\"\n};\n```\n\n### 示例3：返回应用信息\n\n```javascript\n// 返回应用配置信息\nreturn {\n  app_name: app.name,\n  billing_type: app.billing_type,\n  max_devices: app.max_devices,\n  status: app.status\n};\n```\n\n### 示例4：用户状态检查\n\n```javascript\n// 检查用户状态\nif (!user) {\n  return { error: \"用户未登录\" };\n}\n\nreturn {\n  username: user.username,\n  status: user.status,\n  balance: user.balance,\n  is_vip: user.balance > 0 || user.balance === -1\n};\n```\n\n### 示例5：复杂业务逻辑\n\n```javascript\n// 积分计算\nvar basePoints = params.basePoints || 100;\nvar multiplier = 1;\n\n// VIP用户双倍积分\nif (user && user.balance > 0) {\n  multiplier = 2;\n}\n\n// 节假日加成（示例）\nvar today = new Date();\nvar dayOfWeek = today.getDay();\nif (dayOfWeek === 0 || dayOfWeek === 6) {\n  multiplier += 0.5;\n}\n\nvar finalPoints = Math.floor(basePoints * multiplier);\n\nreturn {\n  base_points: basePoints,\n  multiplier: multiplier,\n  final_points: finalPoints,\n  reason: multiplier > 2 ? \"VIP周末加成\" : (multiplier > 1 ? \"VIP加成\" : \"基础积分\")\n};\n```\n\n### 注意事项\n\n1. 云端函数必须返回一个值（可以是任意类型）\n2. 函数执行超时时间为5秒\n3. 不支持网络请求和文件系统操作\n4. 请确保代码语法正确，保存前会进行语法检查",
			Summary:    "调用云端函数说明",
			Sort:       19,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "检查更新",
			Slug:       "api-check-update",
			Content:    "# 检查更新\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/check-update\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| version | string | 否 | 当前客户端版本号 |\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"has_update\": true,\n    \"latest_version\": \"2.0.0\",\n    \"download_url\": \"https://example.com/download/v2.0.0\",\n    \"update_notes\": \"修复了若干bug\",\n    \"update_strategy\": \"optional\",\n    \"update_method\": \"manual\"\n  }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| has_update | boolean | 是否有更新 |\n| latest_version | string | 最新版本号 |\n| download_url | string | 下载地址 |\n| update_notes | string | 更新说明 |\n| update_strategy | string | 更新策略：`optional`(可选更新) 或 `force`(强制更新) |\n| update_method | string | 更新方式：`auto`(自动更新) 或 `manual`(手动更新) |",
			Summary:    "检查更新说明",
			Sort:       20,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取公告",
			Slug:       "api-announcements",
			Content:    "# 获取公告\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/announcements\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": [\n    {\n      \"id\": 1,\n      \"title\": \"系统维护通知\",\n      \"content\": \"系统将于今晚进行维护...\",\n      \"created_at\": \"2024-03-04T10:00:00Z\"\n    }\n  ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 公告ID |\n| title | string | 公告标题 |\n| content | string | 公告内容 |\n| created_at | string | 创建时间 |",
			Summary:    "获取公告说明",
			Sort:       21,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "获取在线实例列表",
			Slug:       "api-instances",
			Content:    "# 获取在线实例列表\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/instances\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| user_id | number | 是 | 用户ID |\n\n### 请求示例\n```json\n{\n  \"user_id\": 123\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"data\": [\n    {\n      \"id\": 1,\n      \"instance_id\": \"instance_001\",\n      \"device_id\": \"device_001\",\n      \"device_name\": \"我的电脑\",\n      \"is_online\": true,\n      \"last_heartbeat\": \"2024-03-04T13:00:00Z\",\n      \"created_at\": \"2024-03-04T12:00:00Z\"\n    }\n  ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 实例ID |\n| instance_id | string | 实例标识 |\n| device_id | string | 设备ID |\n| device_name | string | 设备名称 |\n| is_online | boolean | 是否在线 |\n| last_heartbeat | string | 最后心跳时间 |\n| created_at | string | 创建时间 |",
			Summary:    "获取在线实例列表说明",
			Sort:       22,
		},
		{
			CategoryID: &apiCat.ID,
			Title:      "强制离线实例",
			Slug:       "api-force-offline",
			Content:    "# 强制离线实例\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/instances/:instance_id/offline\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 路径参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| instance_id | string | 是 | 实例标识 |\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| user_id | number | 是 | 用户ID |\n\n### 请求示例\n```json\n{\n  \"user_id\": 123\n}\n```\n\n### 响应示例\n```json\n{\n  \"code\": 200,\n  \"message\": \"已强制离线\"\n}\n```\n\n### 错误响应\n```json\n{\n  \"code\": 404,\n  \"message\": \"实例不存在\"\n}\n```",
			Summary:    "强制离线实例说明",
			Sort:       23,
		},
		{
			CategoryID: &examplesCat.ID,
			Title:      "软件验证示例",
			Slug:       "example-software",
			Content:    "# 软件验证示例\n\n## 完整验证流程\n\n### 1. 用户输入用户名和密码\n\n```javascript\nconst username = document.getElementById('username').value;\nconst password = document.getElementById('password').value;\n```\n\n### 2. 获取设备ID\n\n```javascript\nfunction getDeviceId() {\n  let deviceId = localStorage.getItem('device_id');\n  if (!deviceId) {\n    deviceId = 'device_' + Math.random().toString(36).substr(2, 9);\n    localStorage.setItem('device_id', deviceId);\n  }\n  return deviceId;\n}\n\nconst deviceId = getDeviceId();\n```\n\n### 3. 调用登录接口\n\n```javascript\nconst response = await fetch(`/api/v1/app/${appKey}/login`, {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    username,\n    password,\n    device_id: deviceId\n  })\n});\n\nconst result = await response.json();\n\nif (result.code !== 200) {\n  alert('登录失败: ' + result.message);\n  return;\n}\n```\n\n### 4. 保存token\n\n```javascript\nlocalStorage.setItem('token', result.data.token);\nlocalStorage.setItem('expire_time', result.data.expire_time);\n```\n\n### 5. 启动心跳\n\n```javascript\nsetInterval(async () => {\n  await fetch(`/api/v1/app/${appKey}/heartbeat`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'Authorization': `Bearer ${localStorage.getItem('token')}`\n    },\n    body: JSON.stringify({\n      device_id: deviceId\n    })\n  });\n}, 30000);\n```\n\n### 6. 进入主程序\n\n```javascript\nstartApplication();\n```\n\n## 完整示例代码\n\n```javascript\n// 配置\nconst appKey = 'your_app_key';\n\n// 获取设备ID\nfunction getDeviceId() {\n  let deviceId = localStorage.getItem('device_id');\n  if (!deviceId) {\n    deviceId = 'device_' + Math.random().toString(36).substr(2, 9);\n    localStorage.setItem('device_id', deviceId);\n  }\n  return deviceId;\n}\n\n// 登录函数\nasync function login() {\n  const username = document.getElementById('username').value;\n  const password = document.getElementById('password').value;\n  const deviceId = getDeviceId();\n\n  try {\n    const response = await fetch(`/api/v1/app/${appKey}/login`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({\n        username,\n        password,\n        device_id: deviceId\n      })\n    });\n\n    const result = await response.json();\n\n    if (result.code !== 200) {\n      alert('登录失败: ' + result.message);\n      return false;\n    }\n\n    // 保存token\n    localStorage.setItem('token', result.data.token);\n    localStorage.setItem('expire_time', result.data.expire_time);\n    localStorage.setItem('user_id', result.data.user_id);\n\n    // 启动心跳\n    startHeartbeat();\n\n    // 进入主程序\n    startApplication();\n\n    return true;\n  } catch (error) {\n    console.error('登录错误:', error);\n    alert('登录失败，请检查网络连接');\n    return false;\n  }\n}\n\n// 心跳函数\nfunction startHeartbeat() {\n  setInterval(async () => {\n    const token = localStorage.getItem('token');\n    if (!token) return;\n\n    try {\n      await fetch(`/api/v1/app/${appKey}/heartbeat`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'Authorization': `Bearer ${token}`\n        },\n        body: JSON.stringify({\n          device_id: getDeviceId()\n        })\n      });\n    } catch (error) {\n      console.error('心跳错误:', error);\n    }\n  }, 30000);\n}\n\n// 检查登录状态\nfunction checkLoginStatus() {\n  const token = localStorage.getItem('token');\n  const expireTime = localStorage.getItem('expire_time');\n\n  if (!token || !expireTime) {\n    return false;\n  }\n\n  const now = new Date();\n  const expire = new Date(expireTime);\n\n  return now < expire;\n}\n\n// 主程序入口\nfunction startApplication() {\n  console.log('启动主程序...');\n  // 在这里添加你的主程序逻辑\n}\n\n// 页面加载时检查登录状态\nwindow.onload = function() {\n  if (checkLoginStatus()) {\n    startHeartbeat();\n    startApplication();\n  } else {\n    // 显示登录界面\n    document.getElementById('login-form').style.display = 'block';\n  }\n};\n```",
			Summary:    "软件登录验证完整示例",
			Sort:       1,
		},
		{
			CategoryID: &examplesCat.ID,
			Title:      "游戏验证示例",
			Slug:       "example-game",
			Content:    "# 游戏验证示例\n\n## Unity集成\n\n### 1. 调用登录接口\n\n```csharp\nusing UnityEngine;\nusing UnityEngine.Networking;\n\npublic class AuthManager : MonoBehaviour {\n    private string appKey = \"your_app_key\";\n    private string token;\n    private string deviceId;\n    \n    void Start() {\n        deviceId = SystemInfo.deviceUniqueIdentifier;\n    }\n    \n    public async void Login(string username, string password) {\n        string url = $\"https://api.example.com/api/v1/app/{appKey}/login\";\n        \n        string jsonData = $\"{{\\\"username\\\": \\\"{username}\\\", \\\"password\\\": \\\"{password}\\\", \\\"device_id\\\": \\\"{deviceId}\\\"}}\";\n        \n        using (UnityWebRequest request = UnityWebRequest.Post(url, jsonData)) {\n            request.SetRequestHeader(\"Content-Type\", \"application/json\");\n            \n            yield return request.SendWebRequest();\n            \n            if (request.result == UnityWebRequest.Result.Success) {\n                string response = request.downloadHandler.text;\n                var result = JsonUtility.FromJson<LoginResponse>(response);\n                \n                if (result.code == 200) {\n                    token = result.data.token;\n                    PlayerPrefs.SetString(\"token\", token);\n                    \n                    // 启动心跳\n                    StartCoroutine(HeartbeatCoroutine());\n                    \n                    // 进入游戏\n                    LoadMainScene();\n                } else {\n                    Debug.LogError(\"登录失败: \" + result.message);\n                }\n            } else {\n                Debug.LogError(\"请求失败: \" + request.error);\n            }\n        }\n    }\n    \n    IEnumerator HeartbeatCoroutine() {\n        while (true) {\n            yield return new WaitForSeconds(30f);\n            SendHeartbeat();\n        }\n    }\n    \n    async void SendHeartbeat() {\n        string url = $\"https://api.example.com/api/v1/app/{appKey}/heartbeat\";\n        \n        string jsonData = $\"{{\\\"device_id\\\": \\\"{deviceId}\\\"}}\";\n        \n        using (UnityWebRequest request = UnityWebRequest.Post(url, jsonData)) {\n            request.SetRequestHeader(\"Content-Type\", \"application/json\");\n            request.SetRequestHeader(\"Authorization\", $\"Bearer {token}\");\n            \n            yield return request.SendWebRequest();\n            \n            if (request.result != UnityWebRequest.Result.Success) {\n                Debug.LogError(\"心跳失败: \" + request.error);\n            }\n        }\n    }\n    \n    void LoadMainScene() {\n        UnityEngine.SceneManagement.SceneManager.LoadScene(\"MainScene\");\n    }\n}\n\n[System.Serializable]\npublic class LoginResponse {\n    public int code;\n    public string message;\n    public LoginData data;\n}\n\n[System.Serializable]\npublic class LoginData {\n    public string token;\n    public string expire_time;\n    public int user_id;\n    public string username;\n}\n```\n\n### 2. 卡密充值\n\n```csharp\npublic async void Recharge(string username, string cardKey) {\n    string url = $\"https://api.example.com/api/v1/app/{appKey}/recharge\";\n    \n    string jsonData = $\"{{\\\"username\\\": \\\"{username}\\\", \\\"card_key\\\": \\\"{cardKey}\\\"}}\";\n    \n    using (UnityWebRequest request = UnityWebRequest.Post(url, jsonData)) {\n        request.SetRequestHeader(\"Content-Type\", \"application/json\");\n        \n        yield return request.SendWebRequest();\n        \n        if (request.result == UnityWebRequest.Result.Success) {\n            string response = request.downloadHandler.text;\n            var result = JsonUtility.FromJson<RechargeResponse>(response);\n            \n            if (result.code == 200) {\n                Debug.Log(\"充值成功，充值值: \" + result.data.value);\n            } else {\n                Debug.LogError(\"充值失败: \" + result.message);\n            }\n        } else {\n            Debug.LogError(\"请求失败: \" + request.error);\n        }\n    }\n}\n\n[System.Serializable]\npublic class RechargeResponse {\n    public int code;\n    public string message;\n    public RechargeData data;\n}\n\n[System.Serializable]\npublic class RechargeData {\n    public int value;\n}\n```\n\n### 3. 检查更新\n\n```csharp\npublic async void CheckUpdate(string currentVersion) {\n    string url = $\"https://api.example.com/api/v1/app/{appKey}/check-update?version={currentVersion}\";\n    \n    using (UnityWebRequest request = UnityWebRequest.Get(url)) {\n        request.SetRequestHeader(\"Authorization\", $\"Bearer {token}\");\n        \n        yield return request.SendWebRequest();\n        \n        if (request.result == UnityWebRequest.Result.Success) {\n            string response = request.downloadHandler.text;\n            var result = JsonUtility.FromJson<UpdateResponse>(response);\n            \n            if (result.code == 200 && result.data.has_update) {\n                Debug.Log(\"发现新版本: \" + result.data.latest_version);\n                Debug.Log(\"更新说明: \" + result.data.update_notes);\n                \n                if (result.data.update_strategy == \"force\") {\n                    // 强制更新\n                    ShowUpdateDialog(true);\n                } else {\n                    // 可选更新\n                    ShowUpdateDialog(false);\n                }\n            }\n        } else {\n            Debug.LogError(\"检查更新失败: \" + request.error);\n        }\n    }\n}\n\n[System.Serializable]\npublic class UpdateResponse {\n    public int code;\n    public string message;\n    public UpdateData data;\n}\n\n[System.Serializable]\npublic class UpdateData {\n    public bool has_update;\n    public string latest_version;\n    public string download_url;\n    public string update_notes;\n    public string update_strategy;\n    public string update_method;\n}\n```",
			Summary:    "Unity游戏集成示例",
			Sort:       2,
		},
		{
			CategoryID: &faqCat.ID,
			Title:      "如何获取API密钥？",
			Slug:       "faq-api-key",
			Content:    "# 如何获取API密钥？\n\n## 步骤\n\n1. 登录开发者后台\n2. 进入\"应用管理\"页面\n3. 创建新应用或选择已有应用\n4. 在应用详情页可以看到：\n   - **AppID**：应用唯一标识\n   - **AppKey**：应用密钥\n   - **SecretKey**：加密密钥\n\n## 注意事项\n\n- AppKey只在创建时显示一次，请及时保存\n- 如需重置AppKey，点击\"重置密钥\"按钮\n- 重置后旧密钥立即失效\n- SecretKey用于服务端验证响应，不要在客户端使用",
			Summary:    "获取API密钥的详细步骤",
			Sort:       1,
		},
		{
			CategoryID: &faqCat.ID,
			Title:      "卡密验证失败怎么办？",
			Slug:       "faq-card-fail",
			Content:    "# 卡密验证失败怎么办？\n\n## 常见原因\n\n1. **卡密已过期**\n   - 检查卡密的有效期\n   - 过期的卡密无法使用\n\n2. **卡密已被使用**\n   - 单次使用的卡密只能验证一次\n   - 已使用的卡密无法再次验证\n\n3. **设备绑定错误**\n   - 检查设备ID是否正确\n   - 确认应用是否启用了设备绑定\n\n4. **应用配置错误**\n   - 检查AppID和AppKey是否正确\n   - 确认应用状态是否正常\n\n## 解决方法\n\n1. 联系应用客服\n2. 重新获取卡密\n3. 检查设备网络连接\n4. 查看应用日志获取详细错误信息",
			Summary:    "卡密验证失败的常见原因和解决方法",
			Sort:       2,
		},
		{
			CategoryID: &faqCat.ID,
			Title:      "如何实现代理授权？",
			Slug:       "faq-agent",
			Content:    "# 如何实现代理授权？\n\n## 什么是代理授权？\n\n代理授权允许开发者将自己的应用授权给其他开发者，被授权的开发者可以生成卡密并销售。\n\n## 授权流程\n\n1. **申请授权**\n   - 被授权方向授权方提交申请\n   - 输入应用ID\n   - 等待授权方批准\n\n2. **批准授权**\n   - 授权方查看申请\n   - 批准申请后设置卡密类型权限\n   - 设置代理佣金和折扣\n\n3. **生成卡密**\n   - 被授权方选择被授权的应用\n   - 选择有权限的卡密类型\n   - 生成卡密并销售\n\n4. **结算佣金**\n   - 销售收入按比例结算\n   - 被授权方获得佣金收入\n\n## 权限管理\n\n- 授权方可以随时修改卡密类型权限\n- 可以暂停或终止授权\n- 可以查看代理的销售数据",
			Summary:    "代理授权的实现流程和权限管理",
			Sort:       3,
		},
	}

	for i := range docs {
		doc := model.Doc{
			CategoryID: docs[i].CategoryID,
			Title:      docs[i].Title,
			TitleEn:    docs[i].TitleEn,
			Slug:       docs[i].Slug,
			Content:    docs[i].Content,
			ContentEn:  docs[i].ContentEn,
			Summary:    docs[i].Summary,
			SummaryEn:  docs[i].SummaryEn,
			Sort:       docs[i].Sort,
			Status:     "published",
		}
		DB.Create(&doc)
	}

	log.Println("Document data initialized successfully")
}

// initRedis 初始化Redis连接
func initRedis() {
	RDB = redis.NewClient(&redis.Options{
		Addr:     fmt.Sprintf("%s:%s", config.GetString("redis.host"), config.GetString("redis.port")),
		Password: config.GetString("redis.password"),
		DB:       config.GetInt("redis.db"),
	})

	// 测试连接
	ctx := context.Background()
	_, err := RDB.Ping(ctx).Result()
	if err != nil {
		log.Println("Failed to connect to Redis:", err.Error())
		// 不再panic，只记录错误
		RDB = nil
	}
}

// initTicketData 初始化工单数据
func initTicketData() {
	var ticketCount int64
	DB.Model(&model.Ticket{}).Count(&ticketCount)

	if ticketCount == 0 {
		log.Println("Initializing ticket data...")

		// 确保开发者用户存在
		var devUser model.User
		if err := DB.Where("username = ?", "dev").First(&devUser).Error; err != nil {
			log.Println("Creating dev user for tickets...")
			devEmail := "dev@example.com"
			hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
			devUser = model.User{
				Username: "dev",
				Email:    &devEmail,
				Password: string(hashedPassword),
				Role:     "admin",
				Status:   "active",
			}
			DB.Create(&devUser)
		}

		// 创建示例工单
		tickets := []model.Ticket{
			{
				UserID:   devUser.ID,
				Title:    "登录接口返回500错误",
				Content:  "在使用登录接口时，服务器返回500错误，请帮忙查看。",
				Category: "技术问题",
				Priority: "high",
				Status:   "pending",
			},
			{
				UserID:   devUser.ID,
				Title:    "卡密生成功能咨询",
				Content:  "请问如何批量生成卡密？是否有相关API接口？",
				Category: "功能咨询",
				Priority: "medium",
				Status:   "resolved",
			},
		}

		for i := range tickets {
			DB.Create(&tickets[i])
		}

		log.Println("Ticket data initialized successfully")
	}
}
