f7f92c9853
- 配置文件和SQLite数据库存储在/app/data - Docker volume持久化数据 - 无需预设环境变量,安装向导自动配置
821 lines
62 KiB
Go
821 lines
62 KiB
Go
package database
|
||
|
||
// 重要提示:请始终从 backend/ 目录运行服务
|
||
// 正确的运行方式:
|
||
// cd d:\Code\verify\verification-platform\backend
|
||
// go run cmd/main.go
|
||
//
|
||
// 数据库文件路径基于可执行文件所在目录计算,确保无论从哪个目录运行都使用同一个数据库文件。
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"path/filepath"
|
||
"time"
|
||
"verification-platform-backend/internal/config"
|
||
"verification-platform-backend/internal/model"
|
||
|
||
"github.com/glebarez/sqlite"
|
||
"github.com/go-redis/redis/v8"
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/driver/mysql"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/logger"
|
||
)
|
||
|
||
var (
|
||
DB *gorm.DB
|
||
RDB *redis.Client
|
||
)
|
||
|
||
func Init() {
|
||
initDatabase()
|
||
initRedis()
|
||
}
|
||
|
||
func initDatabase() {
|
||
dbType := config.GetString("database.type")
|
||
if dbType == "" {
|
||
dbType = "auto"
|
||
}
|
||
|
||
switch dbType {
|
||
case "sqlite":
|
||
initSQLite()
|
||
case "mysql":
|
||
initMySQLExplicit()
|
||
default:
|
||
initAutoDetect()
|
||
}
|
||
}
|
||
|
||
func initSQLite() {
|
||
dataDir := config.GetDataDir()
|
||
dbPath := filepath.Join(dataDir, "verification_platform.db")
|
||
|
||
var errOpen error
|
||
DB, errOpen = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Silent),
|
||
DisableForeignKeyConstraintWhenMigrating: true,
|
||
})
|
||
if errOpen != nil {
|
||
log.Fatal("Failed to create SQLite database:", errOpen.Error())
|
||
}
|
||
log.Printf("Using SQLite database: %s\n", dbPath)
|
||
|
||
if sqlDB, err := DB.DB(); err == nil {
|
||
sqlDB.SetMaxIdleConns(10)
|
||
sqlDB.SetMaxOpenConns(100)
|
||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||
}
|
||
|
||
runMigrations()
|
||
}
|
||
|
||
func initMySQLExplicit() {
|
||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
|
||
config.GetString("database.username"),
|
||
config.GetString("database.password"),
|
||
config.GetString("database.host"),
|
||
config.GetString("database.port"),
|
||
config.GetString("database.name"),
|
||
config.GetString("database.charset"),
|
||
)
|
||
|
||
var err error
|
||
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Silent),
|
||
DisableForeignKeyConstraintWhenMigrating: true,
|
||
})
|
||
if err != nil {
|
||
log.Fatal("Failed to connect to MySQL:", err.Error())
|
||
}
|
||
log.Printf("Connected to MySQL: %s@%s:%s/%s\n",
|
||
config.GetString("database.username"),
|
||
config.GetString("database.host"),
|
||
config.GetString("database.port"),
|
||
config.GetString("database.name"))
|
||
|
||
if sqlDB, err := DB.DB(); err == nil {
|
||
sqlDB.SetMaxIdleConns(config.GetInt("database.max_idle_conns"))
|
||
sqlDB.SetMaxOpenConns(config.GetInt("database.max_open_conns"))
|
||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||
}
|
||
|
||
runMigrations()
|
||
}
|
||
|
||
func initAutoDetect() {
|
||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
|
||
config.GetString("database.username"),
|
||
config.GetString("database.password"),
|
||
config.GetString("database.host"),
|
||
config.GetString("database.port"),
|
||
config.GetString("database.name"),
|
||
config.GetString("database.charset"),
|
||
)
|
||
|
||
var err error
|
||
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Silent),
|
||
DisableForeignKeyConstraintWhenMigrating: true,
|
||
})
|
||
|
||
if err != nil {
|
||
log.Println("Failed to connect to MySQL, using SQLite database:", err.Error())
|
||
|
||
dataDir := config.GetDataDir()
|
||
dbPath := filepath.Join(dataDir, "verification_platform.db")
|
||
|
||
DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Silent),
|
||
DisableForeignKeyConstraintWhenMigrating: true,
|
||
})
|
||
if err != nil {
|
||
log.Fatal("Failed to create SQLite database:", err.Error())
|
||
}
|
||
log.Printf("Using SQLite database: %s\n", dbPath)
|
||
}
|
||
|
||
if sqlDB, err := DB.DB(); err == nil {
|
||
sqlDB.SetMaxIdleConns(config.GetInt("database.max_idle_conns"))
|
||
sqlDB.SetMaxOpenConns(config.GetInt("database.max_open_conns"))
|
||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||
}
|
||
|
||
runMigrations()
|
||
}
|
||
|
||
func runMigrations() {
|
||
var columns []struct {
|
||
CID int
|
||
Name string
|
||
Type string
|
||
NotNull int
|
||
DefaultVal interface{}
|
||
PK int
|
||
}
|
||
DB.Raw("PRAGMA table_info(applications)").Scan(&columns)
|
||
log.Println("Applications table columns:")
|
||
for _, col := range columns {
|
||
log.Printf(" - %s (%s)\n", col.Name, col.Type)
|
||
}
|
||
|
||
// 检查app_users表结构
|
||
var appUserColumns []struct {
|
||
CID int
|
||
Name string
|
||
Type string
|
||
NotNull int
|
||
DefaultVal interface{}
|
||
PK int
|
||
}
|
||
DB.Raw("PRAGMA table_info(app_users)").Scan(&appUserColumns)
|
||
log.Println("AppUsers table columns:")
|
||
for _, col := range appUserColumns {
|
||
log.Printf(" - %s (%s)\n", col.Name, col.Type)
|
||
}
|
||
|
||
// 自动迁移数据库表
|
||
if err := DB.AutoMigrate(
|
||
&model.User{},
|
||
&model.AppUser{},
|
||
&model.UserLevel{},
|
||
&model.Application{},
|
||
&model.Version{},
|
||
&model.CardType{},
|
||
&model.Card{},
|
||
&model.AgentApplication{},
|
||
&model.AgentApplicationCardType{},
|
||
&model.AgentApplicationRequest{},
|
||
&model.Order{},
|
||
&model.Log{},
|
||
&model.RechargeRecord{},
|
||
&model.ConsumptionRecord{},
|
||
&model.Ticket{},
|
||
&model.TicketReply{},
|
||
&model.UserVariable{},
|
||
&model.CloudConstant{},
|
||
&model.CloudVariable{},
|
||
&model.IPBlacklist{},
|
||
&model.DeviceBlacklist{},
|
||
&model.AbnormalBehavior{},
|
||
&model.DocCategory{},
|
||
&model.Doc{},
|
||
&model.Package{},
|
||
&model.PackagePermission{},
|
||
&model.Setting{},
|
||
&model.Announcement{},
|
||
&model.UserPackage{},
|
||
&model.UserDevice{},
|
||
&model.UserIP{},
|
||
&model.DeviceSession{},
|
||
&model.PackHistory{},
|
||
&model.WebhookConfig{},
|
||
&model.WebhookLog{},
|
||
&model.ExtensionAPIKey{},
|
||
&model.DynamicCode{},
|
||
&model.PaymentChannel{},
|
||
&model.EmailConfig{},
|
||
&model.SmsConfig{},
|
||
&model.StorageConfig{},
|
||
&model.Captcha{},
|
||
&model.ApiUsage{},
|
||
&model.StorageUsage{},
|
||
&model.UsageAlertRecord{},
|
||
&model.Notification{},
|
||
&model.RiskControlRule{},
|
||
&model.EmailVerifyCode{},
|
||
&model.SmsVerifyCode{},
|
||
&model.AppSMTPConfig{},
|
||
&model.EmailTemplate{},
|
||
&model.VersionFile{},
|
||
&model.CloudVariableRecord{},
|
||
); err != nil {
|
||
log.Println("Failed to migrate database:", err.Error())
|
||
}
|
||
|
||
// 手动迁移:检查并添加 risk_control_rules 表的 user_id 列
|
||
var riskControlColumns []struct {
|
||
Name string
|
||
}
|
||
DB.Raw("PRAGMA table_info(risk_control_rules)").Scan(&riskControlColumns)
|
||
hasUserID := false
|
||
for _, col := range riskControlColumns {
|
||
if col.Name == "user_id" {
|
||
hasUserID = true
|
||
break
|
||
}
|
||
}
|
||
if !hasUserID {
|
||
log.Println("Adding user_id column to risk_control_rules table...")
|
||
if err := DB.Exec("ALTER TABLE risk_control_rules ADD COLUMN user_id INTEGER").Error; err != nil {
|
||
log.Println("Failed to add user_id column:", err.Error())
|
||
} else {
|
||
log.Println("user_id column added successfully")
|
||
}
|
||
}
|
||
|
||
// 修复 application_id 列的 NOT NULL 约束(SQLite需要重建表)
|
||
var riskControlTableInfo []struct {
|
||
CID int
|
||
Name string
|
||
Type string
|
||
NotNull int
|
||
DefaultVal interface{}
|
||
PK int
|
||
}
|
||
DB.Raw("PRAGMA table_info(risk_control_rules)").Scan(&riskControlTableInfo)
|
||
for _, col := range riskControlTableInfo {
|
||
if col.Name == "application_id" && col.NotNull == 1 {
|
||
log.Println("Fixing application_id NOT NULL constraint...")
|
||
// 创建临时表
|
||
DB.Exec(`CREATE TABLE risk_control_rules_new (
|
||
id INTEGER PRIMARY KEY,
|
||
user_id INTEGER,
|
||
application_id INTEGER,
|
||
type TEXT NOT NULL,
|
||
value TEXT NOT NULL,
|
||
reason TEXT,
|
||
status TEXT DEFAULT 'active',
|
||
expires_at DATETIME,
|
||
created_at DATETIME,
|
||
updated_at DATETIME,
|
||
deleted_at DATETIME
|
||
)`)
|
||
// 复制数据
|
||
DB.Exec(`INSERT INTO risk_control_rules_new SELECT * FROM risk_control_rules`)
|
||
// 删除旧表
|
||
DB.Exec(`DROP TABLE risk_control_rules`)
|
||
// 重命名新表
|
||
DB.Exec(`ALTER TABLE risk_control_rules_new RENAME TO risk_control_rules`)
|
||
// 重建索引
|
||
DB.Exec(`CREATE INDEX IF NOT EXISTS idx_risk_control_rules_user_id ON risk_control_rules(user_id)`)
|
||
DB.Exec(`CREATE INDEX IF NOT EXISTS idx_risk_control_rules_application_id ON risk_control_rules(application_id)`)
|
||
DB.Exec(`CREATE INDEX IF NOT EXISTS idx_risk_control_rules_deleted_at ON risk_control_rules(deleted_at)`)
|
||
log.Println("application_id NOT NULL constraint fixed")
|
||
break
|
||
}
|
||
}
|
||
|
||
// 手动迁移:检查并添加 packages 表的 currency 列
|
||
var packageColumns []struct {
|
||
Name string
|
||
}
|
||
DB.Raw("PRAGMA table_info(packages)").Scan(&packageColumns)
|
||
hasCurrency := false
|
||
for _, col := range packageColumns {
|
||
if col.Name == "currency" {
|
||
hasCurrency = true
|
||
break
|
||
}
|
||
}
|
||
if !hasCurrency {
|
||
log.Println("Adding currency column to packages table...")
|
||
if err := DB.Exec("ALTER TABLE packages ADD COLUMN currency TEXT DEFAULT 'CNY'").Error; err != nil {
|
||
log.Println("Failed to add currency column:", err.Error())
|
||
} else {
|
||
log.Println("currency column added successfully")
|
||
}
|
||
}
|
||
|
||
// 初始化数据
|
||
initData()
|
||
|
||
// 初始化工单数据(独立于initData,确保总是被调用)
|
||
initTicketData()
|
||
|
||
// 初始化文档分类和文档
|
||
initDocData()
|
||
}
|
||
|
||
// initData 初始化数据库中的基础数据
|
||
func initData() {
|
||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
|
||
|
||
// 修复现有的授权记录,将 is_received 设置为 true
|
||
DB.Model(&model.AgentApplication{}).Where("is_received = ?", false).Update("is_received", true)
|
||
|
||
// 确保管理员用户存在
|
||
var adminCount int64
|
||
DB.Model(&model.User{}).Where("username = ?", "admin").Count(&adminCount)
|
||
if adminCount == 0 {
|
||
log.Println("Creating admin user...")
|
||
adminEmail := "admin@example.com"
|
||
adminUser := model.User{
|
||
Username: "admin",
|
||
Email: &adminEmail,
|
||
Password: string(hashedPassword),
|
||
Role: "admin",
|
||
Status: "active",
|
||
}
|
||
DB.Create(&adminUser)
|
||
}
|
||
|
||
// 确保开发者用户存在
|
||
var devCount int64
|
||
DB.Model(&model.User{}).Where("username = ?", "dev").Count(&devCount)
|
||
if devCount == 0 {
|
||
log.Println("Creating dev user...")
|
||
devEmail := "dev@example.com"
|
||
devUser := model.User{
|
||
Username: "dev",
|
||
Email: &devEmail,
|
||
Password: string(hashedPassword),
|
||
Role: "admin",
|
||
Status: "active",
|
||
}
|
||
DB.Create(&devUser)
|
||
}
|
||
|
||
// 确保代理用户存在
|
||
var agentCount int64
|
||
DB.Model(&model.User{}).Where("username = ?", "agent").Count(&agentCount)
|
||
if agentCount == 0 {
|
||
log.Println("Creating agent user...")
|
||
agentEmail := "agent@example.com"
|
||
agentUser := model.User{
|
||
Username: "agent",
|
||
Email: &agentEmail,
|
||
Password: string(hashedPassword),
|
||
Role: "agent",
|
||
Status: "active",
|
||
}
|
||
DB.Create(&agentUser)
|
||
}
|
||
|
||
// 迁移:将 developer 角色统一为 admin
|
||
var devRoleCount int64
|
||
DB.Model(&model.User{}).Where("role = ?", "developer").Count(&devRoleCount)
|
||
if devRoleCount > 0 {
|
||
log.Printf("Migrating %d developer users to admin role...", devRoleCount)
|
||
DB.Model(&model.User{}).Where("role = ?", "developer").Update("role", "admin")
|
||
}
|
||
|
||
// 迁移:将 developer_id 列名改为 admin_id
|
||
if DB.Migrator().HasColumn(&model.AgentApplication{}, "developer_id") {
|
||
log.Println("Migrating agent_applications.developer_id to admin_id...")
|
||
DB.Exec("ALTER TABLE agent_applications RENAME COLUMN developer_id TO admin_id")
|
||
}
|
||
if DB.Migrator().HasColumn(&model.AgentApplicationRequest{}, "developer_id") {
|
||
log.Println("Migrating agent_application_requests.developer_id to admin_id...")
|
||
DB.Exec("ALTER TABLE agent_application_requests RENAME COLUMN developer_id TO admin_id")
|
||
}
|
||
|
||
// 迁移:将 write_permission 默认值从 developer 改为 admin
|
||
DB.Exec("UPDATE cloud_variables SET write_permission = 'admin' WHERE write_permission = 'developer'")
|
||
|
||
// 清理没有关联应用的云端常量和变量
|
||
var orphanConstants int64
|
||
DB.Model(&model.CloudConstant{}).Where("app_id IS NULL").Count(&orphanConstants)
|
||
if orphanConstants > 0 {
|
||
log.Printf("Cleaning up %d orphan cloud constants...", orphanConstants)
|
||
DB.Where("app_id IS NULL").Delete(&model.CloudConstant{})
|
||
}
|
||
|
||
var orphanVariables int64
|
||
DB.Model(&model.CloudVariable{}).Where("app_id IS NULL").Count(&orphanVariables)
|
||
if orphanVariables > 0 {
|
||
log.Printf("Cleaning up %d orphan cloud variables...", orphanVariables)
|
||
DB.Where("app_id IS NULL").Delete(&model.CloudVariable{})
|
||
}
|
||
|
||
var localStorageCount int64
|
||
DB.Model(&model.StorageConfig{}).Where("type = ?", "local").Count(&localStorageCount)
|
||
if localStorageCount == 0 {
|
||
log.Println("Creating default local storage config...")
|
||
defaultStorage := model.StorageConfig{
|
||
Name: "本地存储",
|
||
Type: "local",
|
||
IsDefault: true,
|
||
Status: "active",
|
||
Remark: "系统默认本地存储",
|
||
}
|
||
DB.Create(&defaultStorage)
|
||
} else {
|
||
DB.Model(&model.StorageConfig{}).Where("type = ? AND (status = '' OR status IS NULL)", "local").Update("status", "active")
|
||
}
|
||
|
||
var bepusdtCount int64
|
||
DB.Model(&model.PaymentChannel{}).Where("type = ?", "bepusdt").Count(&bepusdtCount)
|
||
if bepusdtCount == 0 {
|
||
log.Println("Creating BEPUSDT example payment channel...")
|
||
bepusdtChannel := model.PaymentChannel{
|
||
Name: "USDT支付",
|
||
Type: "bepusdt",
|
||
Icon: "",
|
||
Config: `{"api_url": "http://your-bepusdt-server:8080", "token": "your-api-token-here"}`,
|
||
Sort: 1,
|
||
Status: "inactive",
|
||
Remark: "BEPUSDT示例配置,请修改api_url和token后启用",
|
||
}
|
||
DB.Create(&bepusdtChannel)
|
||
}
|
||
|
||
initDocData()
|
||
}
|
||
|
||
func initDocData() {
|
||
var categoryCount int64
|
||
DB.Model(&model.DocCategory{}).Count(&categoryCount)
|
||
|
||
if categoryCount > 0 {
|
||
log.Println("Document data already exists, skipping initialization")
|
||
return
|
||
}
|
||
|
||
log.Println("Initializing document data...")
|
||
|
||
categories := []model.DocCategory{
|
||
{Name: "快速开始", NameEn: "Quick Start", Slug: "quickstart", Description: "5分钟内完成集成", DescriptionEn: "Complete integration in 5 minutes", Icon: "zap", Sort: 1},
|
||
{Name: "API文档", NameEn: "API Documentation", Slug: "api", Description: "完整的API接口说明", DescriptionEn: "Complete API reference", Icon: "code", Sort: 2},
|
||
{Name: "示例代码", NameEn: "Code Examples", Slug: "examples", Description: "常见使用场景示例", DescriptionEn: "Common use case examples", Icon: "terminal", Sort: 3},
|
||
{Name: "常见问题", NameEn: "FAQ", Slug: "faq", Description: "解答常见疑问", DescriptionEn: "Frequently asked questions", Icon: "help-circle", Sort: 4},
|
||
}
|
||
|
||
for i := range categories {
|
||
DB.Create(&categories[i])
|
||
}
|
||
|
||
var quickstartCat, apiCat, examplesCat, faqCat model.DocCategory
|
||
DB.Where("slug = ?", "quickstart").First(&quickstartCat)
|
||
DB.Where("slug = ?", "api").First(&apiCat)
|
||
DB.Where("slug = ?", "examples").First(&examplesCat)
|
||
DB.Where("slug = ?", "faq").First(&faqCat)
|
||
|
||
docs := []struct {
|
||
CategoryID *uint
|
||
Title string
|
||
TitleEn string
|
||
Slug string
|
||
Content string
|
||
ContentEn string
|
||
Summary string
|
||
SummaryEn string
|
||
Sort int
|
||
}{
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "用户注册",
|
||
TitleEn: "User Registration",
|
||
Slug: "api-register",
|
||
Content: "# 用户注册\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/register\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| password | string | 是 | 密码 |\n| device_id | string | 是 | 设备指纹,设备唯一标识 |\n| device_name | string | 否 | 设备名称,如\"我的电脑\",不传则使用device_id |\n| email | string | 条件必填 | 邮箱地址,启用邮箱验证时必填 |\n| email_code | string | 条件必填 | 邮箱验证码,启用强制邮箱验证时必填 |\n| device_type | string | 否 | 设备类型:android/ios/windows/mac/linux/web,不传则自动识别 |\n| instance_id | string | 否 | 实例ID,用于多开识别,不传则使用device_id |\n\n### 请求示例\n```json\n{\n \"username\": \"user123\",\n \"password\": \"password123\",\n \"device_id\": \"abc123def456\",\n \"device_name\": \"我的电脑\",\n \"email\": \"user@example.com\",\n \"email_code\": \"123456\",\n \"device_type\": \"windows\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"注册成功\",\n \"data\": {\n \"user_id\": 123\n }\n}\n```\n\n### 说明\n- device_id 是设备指纹,用于唯一标识设备\n- device_name 是设备的友好名称,用于显示\n- device_type 不传时,系统会根据 User-Agent 自动识别\n- 若应用启用了强制邮箱验证,则 email 和 email_code 为必填参数\n\n### 错误响应\n```json\n{\n \"code\": 400,\n \"message\": \"用户名已存在\"\n}\n```\n\n### 邮箱验证错误\n```json\n{\n \"code\": 400,\n \"message\": \"验证码错误或已过期\"\n}\n```",
|
||
ContentEn: "# User Registration\n\n### Endpoint\n```\nPOST /api/v1/app/:appKey/register\n```\n\n### Request Parameters\n| Parameter | Type | Required | Description |\n|--------|------|------|------|\n| username | string | Yes | Username |\n| password | string | Yes | Password |\n| device_id | string | Yes | Device fingerprint, unique device identifier |\n| device_name | string | No | Device name, e.g. \"My Computer\", defaults to device_id |\n| email | string | Conditional | Email address, required when email verification is enabled |\n| email_code | string | Conditional | Email verification code, required when mandatory email verification is enabled |\n| device_type | string | No | Device type: android/ios/windows/mac/linux/web, auto-detected if not provided |\n| instance_id | string | No | Instance ID for multi-instance detection, defaults to device_id |\n\n### Request Example\n```json\n{\n \"username\": \"user123\",\n \"password\": \"password123\",\n \"device_id\": \"abc123def456\",\n \"device_name\": \"My Computer\",\n \"email\": \"user@example.com\",\n \"email_code\": \"123456\",\n \"device_type\": \"windows\"\n}\n```\n\n### Response Example\n```json\n{\n \"code\": 200,\n \"message\": \"Registration successful\",\n \"data\": {\n \"user_id\": 123\n }\n}\n```\n\n### Notes\n- device_id is the device fingerprint used to uniquely identify the device\n- device_name is a friendly name for the device display\n- device_type is auto-detected from User-Agent if not provided\n- If mandatory email verification is enabled, email and email_code are required\n\n### Error Response\n```json\n{\n \"code\": 400,\n \"message\": \"Username already exists\"\n}\n```\n\n### Email Verification Error\n```json\n{\n \"code\": 400,\n \"message\": \"Invalid or expired verification code\"\n}\n```",
|
||
Summary: "用户注册说明",
|
||
SummaryEn: "User registration instructions",
|
||
Sort: 1,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "发送邮箱验证码",
|
||
TitleEn: "Send Email Verification Code",
|
||
Slug: "api-send-email-code",
|
||
Content: "# 发送邮箱验证码\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/send-email-code\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| email | string | 是 | 邮箱地址 |\n| purpose | string | 否 | 用途:register(注册)、reset_password(重置密码)、change_email(更换邮箱),默认register |\n\n### 请求示例\n```json\n{\n \"email\": \"user@example.com\",\n \"purpose\": \"register\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"success\",\n \"data\": {\n \"message\": \"验证码已发送\"\n }\n}\n```\n\n### 说明\n- 验证码有效期为15分钟\n- 若应用未启用邮箱验证,会返回错误\n\n### 错误响应\n```json\n{\n \"code\": 400,\n \"message\": \"该应用未启用邮箱验证\"\n}\n```",
|
||
ContentEn: "# Send Email Verification Code\n\n### Endpoint\n```\nPOST /api/v1/app/:appKey/send-email-code\n```\n\n### Request Parameters\n| Parameter | Type | Required | Description |\n|--------|------|------|------|\n| email | string | Yes | Email address |\n| purpose | string | No | Purpose: register, reset_password, change_email, defaults to register |\n\n### Request Example\n```json\n{\n \"email\": \"user@example.com\",\n \"purpose\": \"register\"\n}\n```\n\n### Response Example\n```json\n{\n \"code\": 200,\n \"message\": \"success\",\n \"data\": {\n \"message\": \"Verification code sent\"\n }\n}\n```\n\n### Notes\n- Verification code is valid for 15 minutes\n- Returns error if email verification is not enabled for the application\n\n### Error Response\n```json\n{\n \"code\": 400,\n \"message\": \"Email verification is not enabled for this application\"\n}\n```",
|
||
Summary: "发送邮箱验证码说明",
|
||
SummaryEn: "Send email verification code instructions",
|
||
Sort: 2,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "用户登录",
|
||
TitleEn: "User Login",
|
||
Slug: "api-login",
|
||
Content: "# 用户登录\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/login\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| password | string | 是 | 密码 |\n| device_id | string | 是 | 设备指纹,设备唯一标识 |\n| device_name | string | 否 | 设备名称,如\"我的电脑\",不传则使用device_id |\n| device_type | string | 否 | 设备类型:android/ios/windows/mac/linux/web,不传则自动识别 |\n| instance_id | string | 否 | 实例ID,用于多开识别,不传则使用device_id |\n\n### 请求示例\n```json\n{\n \"username\": \"user123\",\n \"password\": \"password123\",\n \"device_id\": \"abc123def456\",\n \"device_name\": \"我的电脑\",\n \"device_type\": \"windows\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"登录成功\",\n \"data\": {\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"user_id\": 123\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| token | string | JWT令牌,用于后续认证 |\n| user_id | number | 用户ID |\n\n### 说明\n- device_id 是设备指纹,用于唯一标识设备\n- device_name 是设备的友好名称,用于显示\n- device_type 不传时,系统会根据 User-Agent 自动识别\n- 若设备绑定数量已达上限,会返回错误并显示已绑定设备列表\n\n### 错误响应\n```json\n{\n \"code\": 401,\n \"message\": \"用户名或密码错误\"\n}\n```\n\n### 设备上限错误响应\n```json\n{\n \"code\": 403,\n \"message\": \"设备绑定数量已达上限,请解绑后再试\",\n \"data\": {\n \"error_code\": \"DEVICE_LIMIT_EXCEEDED\",\n \"max_devices\": 3,\n \"device_count\": 3,\n \"devices\": [\n {\n \"id\": 1,\n \"device_id\": \"abc123def456\",\n \"device_name\": \"我的电脑\",\n \"device_type\": \"windows\",\n \"online_count\": 1,\n \"created_at\": \"2024-03-04T12:00:00Z\"\n }\n ]\n }\n}\n```",
|
||
ContentEn: "# User Login\n\n### Endpoint\n```\nPOST /api/v1/app/:appKey/login\n```\n\n### Request Parameters\n| Parameter | Type | Required | Description |\n|--------|------|------|------|\n| username | string | Yes | Username |\n| password | string | Yes | Password |\n| device_id | string | Yes | Device fingerprint, unique device identifier |\n| device_name | string | No | Device name, e.g. \"My Computer\", defaults to device_id |\n| device_type | string | No | Device type: android/ios/windows/mac/linux/web, auto-detected if not provided |\n| instance_id | string | No | Instance ID for multi-instance detection, defaults to device_id |\n\n### Request Example\n```json\n{\n \"username\": \"user123\",\n \"password\": \"password123\",\n \"device_id\": \"abc123def456\",\n \"device_name\": \"My Computer\",\n \"device_type\": \"windows\"\n}\n```\n\n### Response Example\n```json\n{\n \"code\": 200,\n \"message\": \"Login successful\",\n \"data\": {\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"user_id\": 123\n }\n}\n```\n\n### Response Fields\n| Field | Type | Description |\n|--------|------|------|\n| token | string | JWT token for subsequent authentication |\n| user_id | number | User ID |\n\n### Notes\n- device_id is the device fingerprint used to uniquely identify the device\n- device_name is a friendly name for the device display\n- device_type is auto-detected from User-Agent if not provided\n- Returns error with bound device list if device limit is exceeded\n\n### Error Response\n```json\n{\n \"code\": 401,\n \"message\": \"Invalid username or password\"\n}\n```\n\n### Device Limit Exceeded Response\n```json\n{\n \"code\": 403,\n \"message\": \"Device limit exceeded, please unbind a device first\",\n \"data\": {\n \"error_code\": \"DEVICE_LIMIT_EXCEEDED\",\n \"max_devices\": 3,\n \"device_count\": 3,\n \"devices\": [\n {\n \"id\": 1,\n \"device_id\": \"abc123def456\",\n \"device_name\": \"My Computer\",\n \"device_type\": \"windows\",\n \"online_count\": 1,\n \"created_at\": \"2024-03-04T12:00:00Z\"\n }\n ]\n }\n}\n```",
|
||
Summary: "用户登录说明",
|
||
SummaryEn: "User login instructions",
|
||
Sort: 3,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "心跳验证",
|
||
Slug: "api-heartbeat",
|
||
Content: "# 心跳验证\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/heartbeat\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| device_id | string | 是 | 设备ID |\n\n### 请求示例\n```json\n{\n \"device_id\": \"device_001\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"success\"\n}\n```\n\n### 说明\n- 保持用户在线状态\n- 建议每30秒调用一次\n- 超过一定时间未调用心跳,用户将被视为离线\n- 需要在请求头中携带JWT token",
|
||
Summary: "心跳验证说明",
|
||
Sort: 4,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "卡密充值",
|
||
Slug: "api-recharge",
|
||
Content: "# 卡密充值\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/recharge\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户账号 |\n| card_key | string | 是 | 卡密 |\n\n### 请求示例\n```json\n{\n \"username\": \"user123\",\n \"card_key\": \"VIP123456\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"充值成功\",\n \"data\": {\n \"value\": 30\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| value | number | 充值值(根据卡密类型不同,含义不同)|\n\n### 错误响应\n```json\n{\n \"code\": 400,\n \"message\": \"卡密无效或已过期\"\n}\n```",
|
||
Summary: "卡密充值说明",
|
||
Sort: 5,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取绑定设备列表",
|
||
Slug: "api-devices",
|
||
Content: "# 获取绑定设备列表\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/devices\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"devices\": [\n {\n \"device_id\": \"device_001\",\n \"device_name\": \"我的电脑\",\n \"bind_time\": \"2024-03-04T12:00:00Z\",\n \"last_active\": \"2024-03-04T13:00:00Z\"\n }\n ]\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| device_id | string | 设备ID |\n| device_name | string | 设备名称 |\n| bind_time | string | 绑定时间 |\n| last_active | string | 最后活跃时间 |",
|
||
Summary: "获取绑定设备列表说明",
|
||
Sort: 6,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "解绑设备",
|
||
Slug: "api-unbind-device",
|
||
Content: "# 解绑设备\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/unbind-device\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| device_id | string | 是 | 设备ID |\n\n### 请求示例\n```json\n{\n \"device_id\": \"device_001\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"解绑成功\"\n}\n```\n\n### 错误响应\n```json\n{\n \"code\": 400,\n \"message\": \"设备不存在或未绑定\"\n}\n```",
|
||
Summary: "解绑设备说明",
|
||
Sort: 7,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "通过认证解绑设备",
|
||
Slug: "api-unbind-device-with-auth",
|
||
Content: "# 通过认证解绑设备\n\n当用户因设备数量达到上限无法登录时,可以使用此接口通过用户名和密码验证来解绑设备。\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/unbind-device-with-auth\n```\n\n### 说明\n此接口不需要 Authorization header,使用用户名和密码进行身份验证。\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| password | string | 是 | 密码 |\n| device_id | string | 是 | 要解绑的设备ID |\n\n### 请求示例\n```json\n{\n \"username\": \"user123\",\n \"password\": \"password123\",\n \"device_id\": \"device_001\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"message\": \"解绑成功\"\n }\n}\n```\n\n### 错误响应\n\n**参数错误**\n```json\n{\n \"code\": 400,\n \"message\": \"用户名、密码和设备ID不能为空\"\n}\n```\n\n**密码错误**\n```json\n{\n \"code\": 401,\n \"message\": \"密码错误\"\n}\n```\n\n**用户不存在**\n```json\n{\n \"code\": 404,\n \"message\": \"用户不存在\"\n}\n```\n\n**设备不存在**\n```json\n{\n \"code\": 404,\n \"message\": \"设备不存在\"\n}\n```\n\n**解绑失败**\n```json\n{\n \"code\": 500,\n \"message\": \"解绑设备失败\"\n}\n```\n\n### 使用场景\n1. 用户登录时收到\"设备数量已达上限\"的错误\n2. 前端引导用户使用用户名+密码验证方式解绑设备\n3. 用户输入用户名、密码和要解绑的设备ID\n4. 调用此接口完成解绑\n5. 解绑成功后,用户可以正常登录",
|
||
Summary: "通过认证解绑设备说明",
|
||
Sort: 8,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "重置密码(邮箱验证码)",
|
||
Slug: "api-reset-password",
|
||
Content: "# 重置密码(邮箱验证码)\n\n当用户忘记密码时,可以通过邮箱验证码重置密码。\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/reset-password\n```\n\n### 说明\n此接口不需要 Authorization header,需要先调用发送邮箱验证码接口获取验证码。\n\n### 前提条件\n- 应用已启用密码重置功能\n- 用户套餐支持密码重置功能\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| email | string | 是 | 邮箱地址 |\n| code | string | 是 | 邮箱验证码 |\n| password | string | 是 | 新密码(至少6位) |\n\n### 请求示例\n```json\n{\n \"email\": \"user@example.com\",\n \"code\": \"123456\",\n \"password\": \"newpassword123\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"message\": \"密码重置成功\"\n }\n}\n```\n\n### 错误响应\n\n**应用未启用密码重置**\n```json\n{\n \"code\": 400,\n \"message\": \"该应用未启用密码重置功能\"\n}\n```\n\n**套餐不支持**\n```json\n{\n \"code\": 403,\n \"message\": \"当前套餐不支持密码重置功能\"\n}\n```\n\n**验证码无效**\n```json\n{\n \"code\": 400,\n \"message\": \"验证码无效或已过期\"\n}\n```\n\n**验证码已过期**\n```json\n{\n \"code\": 400,\n \"message\": \"验证码已过期\"\n}\n```\n\n**用户不存在**\n```json\n{\n \"code\": 404,\n \"message\": \"用户不存在\"\n}\n```\n\n### 使用流程\n1. 调用 `POST /api/v1/app/:appKey/send-email-code` 发送验证码,purpose 设为 `reset_password`\n2. 用户收到验证码后调用此接口重置密码",
|
||
Summary: "通过邮箱验证码重置密码",
|
||
Sort: 9,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "修改密码",
|
||
Slug: "api-change-password",
|
||
Content: "# 修改密码\n\n通过用户名和原密码验证来修改密码。\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/change-password\n```\n\n### 说明\n此接口不需要 Authorization header,使用用户名和原密码进行身份验证。\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| username | string | 是 | 用户名 |\n| old_password | string | 是 | 原密码 |\n| new_password | string | 是 | 新密码(至少6位) |\n\n### 请求示例\n```json\n{\n \"username\": \"user123\",\n \"old_password\": \"oldpassword123\",\n \"new_password\": \"newpassword123\"\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"message\": \"密码修改成功\"\n }\n}\n```\n\n### 错误响应\n\n**参数错误**\n```json\n{\n \"code\": 400,\n \"message\": \"参数错误\"\n}\n```\n\n**用户不存在**\n```json\n{\n \"code\": 404,\n \"message\": \"用户不存在\"\n}\n```\n\n**原密码错误**\n```json\n{\n \"code\": 400,\n \"message\": \"原密码错误\"\n}\n```\n\n### 使用场景\n- 用户记得原密码时修改密码\n- 无需邮箱验证的密码修改场景",
|
||
Summary: "修改密码",
|
||
Sort: 10,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取设备数量",
|
||
Slug: "api-device-count",
|
||
Content: "# 获取设备数量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/device-count\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"count\": 3,\n \"max_devices\": 5\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| count | number | 当前绑定设备数量 |\n| max_devices | number | 最大允许设备数量 |",
|
||
Summary: "获取设备数量说明",
|
||
Sort: 11,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取应用信息",
|
||
Slug: "api-app-info",
|
||
Content: "# 获取应用信息\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/info\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"app_id\": 1,\n \"name\": \"我的应用\",\n \"description\": \"游戏验证应用\",\n \"status\": \"active\"\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| app_id | number | 应用ID |\n| name | string | 应用名称 |\n| description | string | 应用描述 |\n| status | string | 应用状态 |",
|
||
Summary: "获取应用信息说明",
|
||
Sort: 12,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取用户账户信息",
|
||
Slug: "api-account",
|
||
Content: "# 获取用户账户信息\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/account\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"user_id\": 123,\n \"username\": \"user123\",\n \"expire_time\": \"2024-04-04T00:00:00Z\",\n \"remaining_days\": 30\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| user_id | number | 用户ID |\n| username | string | 用户名 |\n| expire_time | string | 过期时间 |\n| remaining_days | number | 剩余天数 |",
|
||
Summary: "获取用户账户信息说明",
|
||
Sort: 13,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取应用常量",
|
||
Slug: "api-constants",
|
||
Content: "# 获取应用常量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/constants\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": [\n {\n \"id\": 1,\n \"name\": \"常量名称\",\n \"key\": \"CONSTANT_KEY\",\n \"value\": \"常量值\",\n \"var_type\": \"string\",\n \"description\": \"常量描述\"\n }\n ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 常量ID |\n| name | string | 常量名称 |\n| key | string | 常量键 |\n| value | string | 常量值 |\n| var_type | string | 变量类型 |\n| description | string | 描述 |",
|
||
Summary: "获取应用常量说明",
|
||
Sort: 14,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取指定常量",
|
||
Slug: "api-constant",
|
||
Content: "# 获取指定常量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/constants/:key\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 路径参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| key | string | 是 | 常量key |\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"id\": 1,\n \"name\": \"常量名称\",\n \"key\": \"CONSTANT_KEY\",\n \"value\": \"常量值\",\n \"var_type\": \"string\",\n \"description\": \"常量描述\"\n }\n}\n```\n\n### 错误响应\n```json\n{\n \"code\": 404,\n \"message\": \"常量不存在\"\n}\n```",
|
||
Summary: "获取指定常量说明",
|
||
Sort: 15,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取应用变量",
|
||
Slug: "api-variables",
|
||
Content: "# 获取应用变量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/variables\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": [\n {\n \"id\": 1,\n \"name\": \"变量名称\",\n \"key\": \"VARIABLE_KEY\",\n \"default_value\": \"默认值\",\n \"var_type\": \"string\",\n \"scope\": \"user\",\n \"description\": \"变量描述\"\n }\n ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 变量ID |\n| name | string | 变量名称 |\n| key | string | 变量键 |\n| default_value | string | 默认值 |\n| var_type | string | 变量类型 |\n| scope | string | 作用域 |\n| description | string | 描述 |",
|
||
Summary: "获取应用变量说明",
|
||
Sort: 16,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取指定变量",
|
||
Slug: "api-variable",
|
||
Content: "# 获取指定变量\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/variables/:key\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 路径参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| key | string | 是 | 变量key |\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"id\": 1,\n \"name\": \"变量名称\",\n \"key\": \"VARIABLE_KEY\",\n \"default_value\": \"默认值\",\n \"var_type\": \"string\",\n \"scope\": \"user\",\n \"description\": \"变量描述\"\n }\n}\n```\n\n### 错误响应\n```json\n{\n \"code\": 404,\n \"message\": \"变量不存在\"\n}\n```",
|
||
Summary: "获取指定变量说明",
|
||
Sort: 17,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "更新用户变量",
|
||
Slug: "api-update-variables",
|
||
Content: "# 更新用户变量\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/variables\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| variables | object | 是 | 变量键值对 |\n\n### 请求示例\n```json\n{\n \"variables\": {\n \"nickname\": \"新昵称\",\n \"level\": 10\n }\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"更新成功\"\n}\n```\n\n### 说明\n- 只能更新作用域为user的变量\n- 变量值类型需要与定义的类型匹配",
|
||
Summary: "更新用户变量说明",
|
||
Sort: 18,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "调用云端函数",
|
||
Slug: "api-call-function",
|
||
Content: "# 调用云端函数\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/call-function\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| function_name | string | 是 | 云端函数名称 |\n| params | object | 否 | 传递给云端函数的参数 |\n\n### 请求示例\n```json\n{\n \"function_name\": \"calculatePrice\",\n \"params\": {\n \"x\": 10,\n \"y\": 20\n }\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"操作成功\",\n \"data\": {\n \"result\": 30,\n \"execution_time\": 0.001234\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| result | any | 云端函数执行结果 |\n| execution_time | float | 执行时间(秒) |\n\n### 错误响应\n```json\n{\n \"code\": 400,\n \"message\": \"代码执行错误: ReferenceError: x is not defined\"\n}\n```\n\n---\n\n## 云端函数编写指南\n\n云端函数使用 JavaScript 语法编写,在服务端安全执行。\n\n### 内置变量\n\n| 变量名 | 类型 | 说明 |\n|--------|------|------|\n| app | object | 当前应用信息 |\n| user | object | 当前登录用户信息(需传入user_id) |\n| params | object | 自定义传入的参数 |\n\n### 示例1:简单计算\n\n```javascript\n// 云端函数代码\nvar x = params.x || 0;\nvar y = params.y || 0;\nreturn x + y;\n```\n\n### 示例2:条件判断\n\n```javascript\n// 根据用户等级返回折扣\nvar level = user.level || 1;\nvar discount = 1.0;\n\nif (level >= 5) {\n discount = 0.7;\n} else if (level >= 3) {\n discount = 0.8;\n} else if (level >= 2) {\n discount = 0.9;\n}\n\nreturn {\n discount: discount,\n message: \"您的等级为\" + level + \",享受\" + (discount * 10) + \"折优惠\"\n};\n```\n\n### 示例3:返回应用信息\n\n```javascript\n// 返回应用配置信息\nreturn {\n app_name: app.name,\n billing_type: app.billing_type,\n max_devices: app.max_devices,\n status: app.status\n};\n```\n\n### 示例4:用户状态检查\n\n```javascript\n// 检查用户状态\nif (!user) {\n return { error: \"用户未登录\" };\n}\n\nreturn {\n username: user.username,\n status: user.status,\n balance: user.balance,\n is_vip: user.balance > 0 || user.balance === -1\n};\n```\n\n### 示例5:复杂业务逻辑\n\n```javascript\n// 积分计算\nvar basePoints = params.basePoints || 100;\nvar multiplier = 1;\n\n// VIP用户双倍积分\nif (user && user.balance > 0) {\n multiplier = 2;\n}\n\n// 节假日加成(示例)\nvar today = new Date();\nvar dayOfWeek = today.getDay();\nif (dayOfWeek === 0 || dayOfWeek === 6) {\n multiplier += 0.5;\n}\n\nvar finalPoints = Math.floor(basePoints * multiplier);\n\nreturn {\n base_points: basePoints,\n multiplier: multiplier,\n final_points: finalPoints,\n reason: multiplier > 2 ? \"VIP周末加成\" : (multiplier > 1 ? \"VIP加成\" : \"基础积分\")\n};\n```\n\n### 注意事项\n\n1. 云端函数必须返回一个值(可以是任意类型)\n2. 函数执行超时时间为5秒\n3. 不支持网络请求和文件系统操作\n4. 请确保代码语法正确,保存前会进行语法检查",
|
||
Summary: "调用云端函数说明",
|
||
Sort: 19,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "检查更新",
|
||
Slug: "api-check-update",
|
||
Content: "# 检查更新\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/check-update\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| version | string | 否 | 当前客户端版本号 |\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": {\n \"has_update\": true,\n \"latest_version\": \"2.0.0\",\n \"download_url\": \"https://example.com/download/v2.0.0\",\n \"update_notes\": \"修复了若干bug\",\n \"update_strategy\": \"optional\",\n \"update_method\": \"manual\"\n }\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| has_update | boolean | 是否有更新 |\n| latest_version | string | 最新版本号 |\n| download_url | string | 下载地址 |\n| update_notes | string | 更新说明 |\n| update_strategy | string | 更新策略:`optional`(可选更新) 或 `force`(强制更新) |\n| update_method | string | 更新方式:`auto`(自动更新) 或 `manual`(手动更新) |",
|
||
Summary: "检查更新说明",
|
||
Sort: 20,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取公告",
|
||
Slug: "api-announcements",
|
||
Content: "# 获取公告\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/announcements\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": [\n {\n \"id\": 1,\n \"title\": \"系统维护通知\",\n \"content\": \"系统将于今晚进行维护...\",\n \"created_at\": \"2024-03-04T10:00:00Z\"\n }\n ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 公告ID |\n| title | string | 公告标题 |\n| content | string | 公告内容 |\n| created_at | string | 创建时间 |",
|
||
Summary: "获取公告说明",
|
||
Sort: 21,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "获取在线实例列表",
|
||
Slug: "api-instances",
|
||
Content: "# 获取在线实例列表\n\n### 接口地址\n```\nGET /api/v1/app/:appKey/instances\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| user_id | number | 是 | 用户ID |\n\n### 请求示例\n```json\n{\n \"user_id\": 123\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"data\": [\n {\n \"id\": 1,\n \"instance_id\": \"instance_001\",\n \"device_id\": \"device_001\",\n \"device_name\": \"我的电脑\",\n \"is_online\": true,\n \"last_heartbeat\": \"2024-03-04T13:00:00Z\",\n \"created_at\": \"2024-03-04T12:00:00Z\"\n }\n ]\n}\n```\n\n### 响应字段说明\n| 字段名 | 类型 | 说明 |\n|--------|------|------|\n| id | number | 实例ID |\n| instance_id | string | 实例标识 |\n| device_id | string | 设备ID |\n| device_name | string | 设备名称 |\n| is_online | boolean | 是否在线 |\n| last_heartbeat | string | 最后心跳时间 |\n| created_at | string | 创建时间 |",
|
||
Summary: "获取在线实例列表说明",
|
||
Sort: 22,
|
||
},
|
||
{
|
||
CategoryID: &apiCat.ID,
|
||
Title: "强制离线实例",
|
||
Slug: "api-force-offline",
|
||
Content: "# 强制离线实例\n\n### 接口地址\n```\nPOST /api/v1/app/:appKey/instances/:instance_id/offline\n```\n\n### 请求头\n```\nAuthorization: Bearer {token}\n```\n\n### 路径参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| instance_id | string | 是 | 实例标识 |\n\n### 请求参数\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| user_id | number | 是 | 用户ID |\n\n### 请求示例\n```json\n{\n \"user_id\": 123\n}\n```\n\n### 响应示例\n```json\n{\n \"code\": 200,\n \"message\": \"已强制离线\"\n}\n```\n\n### 错误响应\n```json\n{\n \"code\": 404,\n \"message\": \"实例不存在\"\n}\n```",
|
||
Summary: "强制离线实例说明",
|
||
Sort: 23,
|
||
},
|
||
{
|
||
CategoryID: &examplesCat.ID,
|
||
Title: "软件验证示例",
|
||
Slug: "example-software",
|
||
Content: "# 软件验证示例\n\n## 完整验证流程\n\n### 1. 用户输入用户名和密码\n\n```javascript\nconst username = document.getElementById('username').value;\nconst password = document.getElementById('password').value;\n```\n\n### 2. 获取设备ID\n\n```javascript\nfunction getDeviceId() {\n let deviceId = localStorage.getItem('device_id');\n if (!deviceId) {\n deviceId = 'device_' + Math.random().toString(36).substr(2, 9);\n localStorage.setItem('device_id', deviceId);\n }\n return deviceId;\n}\n\nconst deviceId = getDeviceId();\n```\n\n### 3. 调用登录接口\n\n```javascript\nconst response = await fetch(`/api/v1/app/${appKey}/login`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n username,\n password,\n device_id: deviceId\n })\n});\n\nconst result = await response.json();\n\nif (result.code !== 200) {\n alert('登录失败: ' + result.message);\n return;\n}\n```\n\n### 4. 保存token\n\n```javascript\nlocalStorage.setItem('token', result.data.token);\nlocalStorage.setItem('expire_time', result.data.expire_time);\n```\n\n### 5. 启动心跳\n\n```javascript\nsetInterval(async () => {\n await fetch(`/api/v1/app/${appKey}/heartbeat`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${localStorage.getItem('token')}`\n },\n body: JSON.stringify({\n device_id: deviceId\n })\n });\n}, 30000);\n```\n\n### 6. 进入主程序\n\n```javascript\nstartApplication();\n```\n\n## 完整示例代码\n\n```javascript\n// 配置\nconst appKey = 'your_app_key';\n\n// 获取设备ID\nfunction getDeviceId() {\n let deviceId = localStorage.getItem('device_id');\n if (!deviceId) {\n deviceId = 'device_' + Math.random().toString(36).substr(2, 9);\n localStorage.setItem('device_id', deviceId);\n }\n return deviceId;\n}\n\n// 登录函数\nasync function login() {\n const username = document.getElementById('username').value;\n const password = document.getElementById('password').value;\n const deviceId = getDeviceId();\n\n try {\n const response = await fetch(`/api/v1/app/${appKey}/login`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n username,\n password,\n device_id: deviceId\n })\n });\n\n const result = await response.json();\n\n if (result.code !== 200) {\n alert('登录失败: ' + result.message);\n return false;\n }\n\n // 保存token\n localStorage.setItem('token', result.data.token);\n localStorage.setItem('expire_time', result.data.expire_time);\n localStorage.setItem('user_id', result.data.user_id);\n\n // 启动心跳\n startHeartbeat();\n\n // 进入主程序\n startApplication();\n\n return true;\n } catch (error) {\n console.error('登录错误:', error);\n alert('登录失败,请检查网络连接');\n return false;\n }\n}\n\n// 心跳函数\nfunction startHeartbeat() {\n setInterval(async () => {\n const token = localStorage.getItem('token');\n if (!token) return;\n\n try {\n await fetch(`/api/v1/app/${appKey}/heartbeat`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${token}`\n },\n body: JSON.stringify({\n device_id: getDeviceId()\n })\n });\n } catch (error) {\n console.error('心跳错误:', error);\n }\n }, 30000);\n}\n\n// 检查登录状态\nfunction checkLoginStatus() {\n const token = localStorage.getItem('token');\n const expireTime = localStorage.getItem('expire_time');\n\n if (!token || !expireTime) {\n return false;\n }\n\n const now = new Date();\n const expire = new Date(expireTime);\n\n return now < expire;\n}\n\n// 主程序入口\nfunction startApplication() {\n console.log('启动主程序...');\n // 在这里添加你的主程序逻辑\n}\n\n// 页面加载时检查登录状态\nwindow.onload = function() {\n if (checkLoginStatus()) {\n startHeartbeat();\n startApplication();\n } else {\n // 显示登录界面\n document.getElementById('login-form').style.display = 'block';\n }\n};\n```",
|
||
Summary: "软件登录验证完整示例",
|
||
Sort: 1,
|
||
},
|
||
{
|
||
CategoryID: &examplesCat.ID,
|
||
Title: "游戏验证示例",
|
||
Slug: "example-game",
|
||
Content: "# 游戏验证示例\n\n## Unity集成\n\n### 1. 调用登录接口\n\n```csharp\nusing UnityEngine;\nusing UnityEngine.Networking;\n\npublic class AuthManager : MonoBehaviour {\n private string appKey = \"your_app_key\";\n private string token;\n private string deviceId;\n \n void Start() {\n deviceId = SystemInfo.deviceUniqueIdentifier;\n }\n \n public async void Login(string username, string password) {\n string url = $\"https://api.example.com/api/v1/app/{appKey}/login\";\n \n string jsonData = $\"{{\\\"username\\\": \\\"{username}\\\", \\\"password\\\": \\\"{password}\\\", \\\"device_id\\\": \\\"{deviceId}\\\"}}\";\n \n using (UnityWebRequest request = UnityWebRequest.Post(url, jsonData)) {\n request.SetRequestHeader(\"Content-Type\", \"application/json\");\n \n yield return request.SendWebRequest();\n \n if (request.result == UnityWebRequest.Result.Success) {\n string response = request.downloadHandler.text;\n var result = JsonUtility.FromJson<LoginResponse>(response);\n \n if (result.code == 200) {\n token = result.data.token;\n PlayerPrefs.SetString(\"token\", token);\n \n // 启动心跳\n StartCoroutine(HeartbeatCoroutine());\n \n // 进入游戏\n LoadMainScene();\n } else {\n Debug.LogError(\"登录失败: \" + result.message);\n }\n } else {\n Debug.LogError(\"请求失败: \" + request.error);\n }\n }\n }\n \n IEnumerator HeartbeatCoroutine() {\n while (true) {\n yield return new WaitForSeconds(30f);\n SendHeartbeat();\n }\n }\n \n async void SendHeartbeat() {\n string url = $\"https://api.example.com/api/v1/app/{appKey}/heartbeat\";\n \n string jsonData = $\"{{\\\"device_id\\\": \\\"{deviceId}\\\"}}\";\n \n using (UnityWebRequest request = UnityWebRequest.Post(url, jsonData)) {\n request.SetRequestHeader(\"Content-Type\", \"application/json\");\n request.SetRequestHeader(\"Authorization\", $\"Bearer {token}\");\n \n yield return request.SendWebRequest();\n \n if (request.result != UnityWebRequest.Result.Success) {\n Debug.LogError(\"心跳失败: \" + request.error);\n }\n }\n }\n \n void LoadMainScene() {\n UnityEngine.SceneManagement.SceneManager.LoadScene(\"MainScene\");\n }\n}\n\n[System.Serializable]\npublic class LoginResponse {\n public int code;\n public string message;\n public LoginData data;\n}\n\n[System.Serializable]\npublic class LoginData {\n public string token;\n public string expire_time;\n public int user_id;\n public string username;\n}\n```\n\n### 2. 卡密充值\n\n```csharp\npublic async void Recharge(string username, string cardKey) {\n string url = $\"https://api.example.com/api/v1/app/{appKey}/recharge\";\n \n string jsonData = $\"{{\\\"username\\\": \\\"{username}\\\", \\\"card_key\\\": \\\"{cardKey}\\\"}}\";\n \n using (UnityWebRequest request = UnityWebRequest.Post(url, jsonData)) {\n request.SetRequestHeader(\"Content-Type\", \"application/json\");\n \n yield return request.SendWebRequest();\n \n if (request.result == UnityWebRequest.Result.Success) {\n string response = request.downloadHandler.text;\n var result = JsonUtility.FromJson<RechargeResponse>(response);\n \n if (result.code == 200) {\n Debug.Log(\"充值成功,充值值: \" + result.data.value);\n } else {\n Debug.LogError(\"充值失败: \" + result.message);\n }\n } else {\n Debug.LogError(\"请求失败: \" + request.error);\n }\n }\n}\n\n[System.Serializable]\npublic class RechargeResponse {\n public int code;\n public string message;\n public RechargeData data;\n}\n\n[System.Serializable]\npublic class RechargeData {\n public int value;\n}\n```\n\n### 3. 检查更新\n\n```csharp\npublic async void CheckUpdate(string currentVersion) {\n string url = $\"https://api.example.com/api/v1/app/{appKey}/check-update?version={currentVersion}\";\n \n using (UnityWebRequest request = UnityWebRequest.Get(url)) {\n request.SetRequestHeader(\"Authorization\", $\"Bearer {token}\");\n \n yield return request.SendWebRequest();\n \n if (request.result == UnityWebRequest.Result.Success) {\n string response = request.downloadHandler.text;\n var result = JsonUtility.FromJson<UpdateResponse>(response);\n \n if (result.code == 200 && result.data.has_update) {\n Debug.Log(\"发现新版本: \" + result.data.latest_version);\n Debug.Log(\"更新说明: \" + result.data.update_notes);\n \n if (result.data.update_strategy == \"force\") {\n // 强制更新\n ShowUpdateDialog(true);\n } else {\n // 可选更新\n ShowUpdateDialog(false);\n }\n }\n } else {\n Debug.LogError(\"检查更新失败: \" + request.error);\n }\n }\n}\n\n[System.Serializable]\npublic class UpdateResponse {\n public int code;\n public string message;\n public UpdateData data;\n}\n\n[System.Serializable]\npublic class UpdateData {\n public bool has_update;\n public string latest_version;\n public string download_url;\n public string update_notes;\n public string update_strategy;\n public string update_method;\n}\n```",
|
||
Summary: "Unity游戏集成示例",
|
||
Sort: 2,
|
||
},
|
||
{
|
||
CategoryID: &faqCat.ID,
|
||
Title: "如何获取API密钥?",
|
||
Slug: "faq-api-key",
|
||
Content: "# 如何获取API密钥?\n\n## 步骤\n\n1. 登录开发者后台\n2. 进入\"应用管理\"页面\n3. 创建新应用或选择已有应用\n4. 在应用详情页可以看到:\n - **AppID**:应用唯一标识\n - **AppKey**:应用密钥\n - **SecretKey**:加密密钥\n\n## 注意事项\n\n- AppKey只在创建时显示一次,请及时保存\n- 如需重置AppKey,点击\"重置密钥\"按钮\n- 重置后旧密钥立即失效\n- SecretKey用于服务端验证响应,不要在客户端使用",
|
||
Summary: "获取API密钥的详细步骤",
|
||
Sort: 1,
|
||
},
|
||
{
|
||
CategoryID: &faqCat.ID,
|
||
Title: "卡密验证失败怎么办?",
|
||
Slug: "faq-card-fail",
|
||
Content: "# 卡密验证失败怎么办?\n\n## 常见原因\n\n1. **卡密已过期**\n - 检查卡密的有效期\n - 过期的卡密无法使用\n\n2. **卡密已被使用**\n - 单次使用的卡密只能验证一次\n - 已使用的卡密无法再次验证\n\n3. **设备绑定错误**\n - 检查设备ID是否正确\n - 确认应用是否启用了设备绑定\n\n4. **应用配置错误**\n - 检查AppID和AppKey是否正确\n - 确认应用状态是否正常\n\n## 解决方法\n\n1. 联系应用客服\n2. 重新获取卡密\n3. 检查设备网络连接\n4. 查看应用日志获取详细错误信息",
|
||
Summary: "卡密验证失败的常见原因和解决方法",
|
||
Sort: 2,
|
||
},
|
||
{
|
||
CategoryID: &faqCat.ID,
|
||
Title: "如何实现代理授权?",
|
||
Slug: "faq-agent",
|
||
Content: "# 如何实现代理授权?\n\n## 什么是代理授权?\n\n代理授权允许开发者将自己的应用授权给其他开发者,被授权的开发者可以生成卡密并销售。\n\n## 授权流程\n\n1. **申请授权**\n - 被授权方向授权方提交申请\n - 输入应用ID\n - 等待授权方批准\n\n2. **批准授权**\n - 授权方查看申请\n - 批准申请后设置卡密类型权限\n - 设置代理佣金和折扣\n\n3. **生成卡密**\n - 被授权方选择被授权的应用\n - 选择有权限的卡密类型\n - 生成卡密并销售\n\n4. **结算佣金**\n - 销售收入按比例结算\n - 被授权方获得佣金收入\n\n## 权限管理\n\n- 授权方可以随时修改卡密类型权限\n- 可以暂停或终止授权\n- 可以查看代理的销售数据",
|
||
Summary: "代理授权的实现流程和权限管理",
|
||
Sort: 3,
|
||
},
|
||
}
|
||
|
||
for i := range docs {
|
||
doc := model.Doc{
|
||
CategoryID: docs[i].CategoryID,
|
||
Title: docs[i].Title,
|
||
TitleEn: docs[i].TitleEn,
|
||
Slug: docs[i].Slug,
|
||
Content: docs[i].Content,
|
||
ContentEn: docs[i].ContentEn,
|
||
Summary: docs[i].Summary,
|
||
SummaryEn: docs[i].SummaryEn,
|
||
Sort: docs[i].Sort,
|
||
Status: "published",
|
||
}
|
||
DB.Create(&doc)
|
||
}
|
||
|
||
log.Println("Document data initialized successfully")
|
||
}
|
||
|
||
// initRedis 初始化Redis连接
|
||
func initRedis() {
|
||
RDB = redis.NewClient(&redis.Options{
|
||
Addr: fmt.Sprintf("%s:%s", config.GetString("redis.host"), config.GetString("redis.port")),
|
||
Password: config.GetString("redis.password"),
|
||
DB: config.GetInt("redis.db"),
|
||
})
|
||
|
||
// 测试连接
|
||
ctx := context.Background()
|
||
_, err := RDB.Ping(ctx).Result()
|
||
if err != nil {
|
||
log.Println("Failed to connect to Redis:", err.Error())
|
||
// 不再panic,只记录错误
|
||
RDB = nil
|
||
}
|
||
}
|
||
|
||
// initTicketData 初始化工单数据
|
||
func initTicketData() {
|
||
var ticketCount int64
|
||
DB.Model(&model.Ticket{}).Count(&ticketCount)
|
||
|
||
if ticketCount == 0 {
|
||
log.Println("Initializing ticket data...")
|
||
|
||
// 确保开发者用户存在
|
||
var devUser model.User
|
||
if err := DB.Where("username = ?", "dev").First(&devUser).Error; err != nil {
|
||
log.Println("Creating dev user for tickets...")
|
||
devEmail := "dev@example.com"
|
||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
|
||
devUser = model.User{
|
||
Username: "dev",
|
||
Email: &devEmail,
|
||
Password: string(hashedPassword),
|
||
Role: "admin",
|
||
Status: "active",
|
||
}
|
||
DB.Create(&devUser)
|
||
}
|
||
|
||
// 创建示例工单
|
||
tickets := []model.Ticket{
|
||
{
|
||
UserID: devUser.ID,
|
||
Title: "登录接口返回500错误",
|
||
Content: "在使用登录接口时,服务器返回500错误,请帮忙查看。",
|
||
Category: "技术问题",
|
||
Priority: "high",
|
||
Status: "pending",
|
||
},
|
||
{
|
||
UserID: devUser.ID,
|
||
Title: "卡密生成功能咨询",
|
||
Content: "请问如何批量生成卡密?是否有相关API接口?",
|
||
Category: "功能咨询",
|
||
Priority: "medium",
|
||
Status: "resolved",
|
||
},
|
||
}
|
||
|
||
for i := range tickets {
|
||
DB.Create(&tickets[i])
|
||
}
|
||
|
||
log.Println("Ticket data initialized successfully")
|
||
}
|
||
}
|