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 ) // Init 初始化数据库连接 func Init() { initMySQL() initRedis() } // initMySQL 初始化MySQL连接 func initMySQL() { // 尝试连接MySQL 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, }) // 如果MySQL连接失败,创建一个SQLite数据库用于开发 if err != nil { log.Println("Failed to connect to MySQL, using SQLite database for development:", err.Error()) log.Println("Using file database for development") dbPath := "verification_platform.db" absPath, err := filepath.Abs(dbPath) if err != nil { log.Fatal("Failed to get absolute path:", err.Error()) } wd, err := os.Getwd() if err != nil { log.Fatal("Failed to get working directory:", err.Error()) } log.Printf("Working directory: %s, basename: %s\n", wd, filepath.Base(wd)) if filepath.Base(wd) == "cmd" { dbPath = filepath.Join(filepath.Dir(wd), "verification_platform.db") absPath = dbPath } DB, err = gorm.Open(sqlite.Open(absPath), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { log.Fatal("Failed to create SQLite database:", err.Error()) } log.Printf("Database file path: %s\n", absPath) } // 设置连接池(仅对非内存数据库有效) 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) } // 检查applications表结构 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) } // 自动迁移数据库表 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.Captcha{}, &model.ApiUsage{}, &model.StorageUsage{}, &model.UsageAlertRecord{}, &model.Notification{}, &model.RiskControlRule{}, &model.EmailVerifyCode{}, &model.AppSMTPConfig{}, &model.EmailTemplate{}, &model.VersionFile{}, &model.CloudVariableRecord{}, ) if 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 developerCount int64 DB.Model(&model.User{}).Where("role = ?", "developer").Count(&developerCount) if developerCount > 0 { log.Printf("Migrating %d developer users to admin role...", developerCount) DB.Model(&model.User{}).Where("role = ?", "developer").Update("role", "admin") } // 清理没有关联应用的云端常量和变量 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{}) } // 初始化文档分类和文档 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(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(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(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") } }