Files
verify/backend/internal/database/database.go
T

714 lines
58 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
// 清理没有关联应用的云端常量和变量
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<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")
}
}