Files
opsx-licence/backend/database/seed.go
T
Logan.LiandCursor 8cc2c83f9d feat: 通用授权后台、离线证、Waffo 支付页与仪表盘加强
支持多产品计划展示、LICOFF 离线签发、支付独立配置与调试,以及分析看板;同步更新 README 与集成文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 20:25:46 +08:00

309 lines
8.0 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
import (
"backend/internal/model"
"backend/utils"
"log"
"time"
"gorm.io/gorm"
)
const (
defaultAdminUsername = "admin"
defaultAdminPassword = "admin"
)
// SeedDefaults 初始化默认数据:管理员账号 + 系统设置 + 示例业务数据 // Logan.Li
func SeedDefaults(db *gorm.DB) error {
if err := seedAdmin(db); err != nil {
return err
}
if err := seedSetting(db); err != nil {
return err
}
if err := seedSampleData(db); err != nil {
return err
}
if err := ensureOpsXShellPlanPricing(db); err != nil {
return err
}
if err := ensureWaffoPaymentGateway(db); err != nil {
return err
}
return nil
}
// seedAdmin 初始化默认管理员账号(仅在不存在时创建) // Logan.Li
func seedAdmin(db *gorm.DB) error {
var count int64
db.Model(&model.User{}).Where("username = ?", defaultAdminUsername).Count(&count)
if count > 0 {
return nil
}
admin := model.User{
Username: defaultAdminUsername,
Password: utils.HashPassword(defaultAdminPassword),
Email: "admin@opsx-licence.local",
Role: "admin",
IsActive: true,
}
if err := db.Create(&admin).Error; err != nil {
return err
}
log.Printf("[初始化] 已创建默认管理员账号: %s / %s(请尽快修改密码)", defaultAdminUsername, defaultAdminPassword)
return nil
}
// seedSetting 初始化默认系统设置(仅创建一次) // Logan.Li
func seedSetting(db *gorm.DB) error {
var count int64
db.Model(&model.Setting{}).Count(&count)
if count > 0 {
return nil
}
setting := model.Setting{
SystemName: "opsx-licence",
DefaultDuration: 365,
DefaultMaxActivations: 1,
ExpireReminderDays: 7,
APIRateLimit: 1000,
LogRetentionDays: 365,
}
if err := db.Create(&setting).Error; err != nil {
return err
}
log.Println("[初始化] 已创建默认系统设置")
return nil
}
// seedSampleData 初始化 OpsXShell 产品 / 计划 / Waffo 网关,便于桌面端联调 // Logan.Li
func seedSampleData(db *gorm.DB) error {
var productCount int64
db.Model(&model.Product{}).Count(&productCount)
if productCount > 0 {
return nil
}
product := model.Product{
Name: "OpsXShell",
Code: "opsxshell",
Version: "2.0.0",
Description: "OpsXShell Pro — SSH / SFTP / RDP / VNC / AI",
Features: `["ssh","sftp","rdp","vnc","ai","portForward","proxy","serial","batchExec"]`,
Price: 199.00,
PriceModel: "perpetual",
Status: 1,
}
if err := db.Create(&product).Error; err != nil {
return err
}
customer := model.Customer{
Name: "Demo",
Company: "OpsX",
Email: "demo@opsxshell.local",
Status: 1,
}
if err := db.Create(&customer).Error; err != nil {
return err
}
plans := []model.Plan{
{
ProductID: product.ID,
Name: "体验",
PlanType: "trial",
Price: 0,
Currency: "CNY",
Duration: 180,
MaxDevices: 1,
Features: `["专业功能","体验","1 设备"]`,
SortOrder: 0,
Status: 1,
},
{
ProductID: product.ID,
Name: "包年",
PlanType: "yearly",
Price: 99,
Currency: "CNY",
Duration: 365,
MaxDevices: 2,
Features: `["专业功能","包年","2 设备"]`,
SortOrder: 1,
Status: 1,
},
{
ProductID: product.ID,
Name: "永久",
PlanType: "perpetual",
Price: 199,
Currency: "CNY",
Duration: 0,
MaxDevices: 5,
Features: `["专业功能","永久授权","5 设备"]`,
SortOrder: 2,
Status: 1,
},
}
for i := range plans {
if err := db.Create(&plans[i]).Error; err != nil {
return err
}
}
now := time.Now()
license := model.License{
LicenseKey: utils.GenerateLicenseKey(),
CustomerID: customer.ID,
ProductID: product.ID,
Status: model.LicenseStatusActive,
StartDate: now,
EndDate: model.PermanentEndDate,
Duration: 0,
MaxActivations: 5,
Notes: "开发示例授权",
}
if err := db.Create(&license).Error; err != nil {
return err
}
gateway := model.PaymentGatewayConfig{
Gateway: "waffo",
Enabled: true,
SandboxMode: true,
Currency: "USD",
Priority: 1,
}
if err := db.Create(&gateway).Error; err != nil {
return err
}
log.Printf("[初始化] OpsXShell 产品已就绪 (code=opsxshell),示例 License: %s", license.LicenseKey)
return nil
}
// ensureOpsXShellPlanPricing ensures opsxshell product exists and syncs plan price/seats. // Logan.Li
func ensureOpsXShellPlanPricing(db *gorm.DB) error {
var product model.Product
err := db.Where("code = ?", "opsxshell").First(&product).Error
if err != nil {
// Prefer renaming legacy demo product so existing plans/licenses stay attached.
if renameErr := db.Where("code = ?", "opsx-tools").First(&product).Error; renameErr == nil {
product.Code = "opsxshell"
product.Name = "OpsXShell"
product.Description = "OpsXShell Pro — SSH / SFTP / RDP / VNC / AI"
product.Features = `["ssh","sftp","rdp","vnc","ai","portForward","proxy","serial","batchExec"]`
product.Price = 199
product.PriceModel = "perpetual"
product.Status = 1
if err := db.Save(&product).Error; err != nil {
return err
}
log.Println("[初始化] 已将 opsx-tools 迁移为 opsxshell")
} else {
product = model.Product{
Name: "OpsXShell",
Code: "opsxshell",
Version: "2.0.0",
Description: "OpsXShell Pro — SSH / SFTP / RDP / VNC / AI",
Features: `["ssh","sftp","rdp","vnc","ai","portForward","proxy","serial","batchExec"]`,
Price: 199,
PriceModel: "perpetual",
Status: 1,
}
if err := db.Create(&product).Error; err != nil {
return err
}
log.Println("[初始化] 已创建 opsxshell 产品")
}
}
type planSpec struct {
planType string
name string
price float64
currency string
duration int
maxDevices int
features string
sortOrder int
}
updates := []planSpec{
{"trial", "体验", 0, "CNY", 180, 1, "[\"专业功能\",\"体验\",\"1 设备\"]", 0},
{"yearly", "包年", 99, "CNY", 365, 2, "[\"专业功能\",\"包年\",\"2 设备\"]", 1},
{"perpetual", "永久", 199, "CNY", 0, 5, "[\"专业功能\",\"永久授权\",\"5 设备\"]", 2},
}
for _, u := range updates {
var plan model.Plan
err := db.Where("product_id = ? AND plan_type = ?", product.ID, u.planType).First(&plan).Error
if err != nil {
plan = model.Plan{
ProductID: product.ID,
Name: u.name,
PlanType: u.planType,
Price: u.price,
Currency: u.currency,
Duration: u.duration,
MaxDevices: u.maxDevices,
Features: u.features,
SortOrder: u.sortOrder,
Status: 1,
}
if err := db.Create(&plan).Error; err != nil {
return err
}
continue
}
plan.Name = u.name
plan.Price = u.price
plan.Currency = u.currency
plan.Duration = u.duration
plan.MaxDevices = u.maxDevices
plan.Features = u.features
plan.SortOrder = u.sortOrder
plan.Status = 1
if err := db.Save(&plan).Error; err != nil {
return err
}
}
log.Printf("[初始化] OpsXShell 套餐已同步: 包年¥99/2台, 永久¥199/5台, 体验180天/1台")
return nil
}
// ensureWaffoPaymentGateway makes pancake.waffo.ai the preferred checkout gateway. // Logan.Li
func ensureWaffoPaymentGateway(db *gorm.DB) error {
var existing model.PaymentGatewayConfig
err := db.Where("gateway = ?", "waffo").First(&existing).Error
if err != nil {
gw := model.PaymentGatewayConfig{
Gateway: "waffo",
Enabled: true,
SandboxMode: true,
Currency: "CNY",
Priority: 0,
}
if err := db.Create(&gw).Error; err != nil {
return err
}
} else {
existing.Enabled = true
existing.SandboxMode = true
existing.Currency = "CNY"
existing.Priority = 0
if err := db.Save(&existing).Error; err != nil {
return err
}
}
// Lower priority for other enabled gateways (ASC pick prefers Waffo).
if err := db.Model(&model.PaymentGatewayConfig{}).
Where("gateway <> ? AND enabled = ?", "waffo", true).
Update("priority", 100).Error; err != nil {
return err
}
log.Println("[初始化] 支付网关已优先 Waffo(包年/永久均为 onetime)")
return nil
}