feat: 对齐 Waffo 包年/永久结账与客户端席位 API
同步 OpsXShell 档位定价与 Waffo 商品映射,补齐 checkout/webhook/设备列表契约,便于桌面端购买与满席释放。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4ae318335c
commit
95405de119
14 files changed
+958
-155
No files matched your search
+180
-26
@@ -25,6 +25,12 @@ func SeedDefaults(db *gorm.DB) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -73,7 +79,7 @@ func seedSetting(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedSampleData 初始化示例业务数据(产品/客户/计划/支付网关),便于开发联调 // Logan.Li
|
||||
// seedSampleData 初始化 OpsXShell 产品 / 计划 / Waffo 网关,便于桌面端联调 // Logan.Li
|
||||
func seedSampleData(db *gorm.DB) error {
|
||||
var productCount int64
|
||||
db.Model(&model.Product{}).Count(&productCount)
|
||||
@@ -81,38 +87,67 @@ func seedSampleData(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 示例产品 // Logan.Li
|
||||
product := model.Product{
|
||||
Name: "OpsX Tools Suite",
|
||||
Code: "opsx-tools",
|
||||
Version: "1.0.0",
|
||||
Description: "OpsX 工具集 - 一站式运维工具箱",
|
||||
Features: `["CLI","GUI","Web Console","API"]`,
|
||||
Price: 99.00,
|
||||
PriceModel: "subscription",
|
||||
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
|
||||
}
|
||||
|
||||
// 示例客户 // Logan.Li
|
||||
customer := model.Customer{
|
||||
Name: "示例客户",
|
||||
Company: "Acme Inc.",
|
||||
Email: "demo@acme.com",
|
||||
Phone: "13800000000",
|
||||
Name: "Demo",
|
||||
Company: "OpsX",
|
||||
Email: "demo@opsxshell.local",
|
||||
Status: 1,
|
||||
}
|
||||
if err := db.Create(&customer).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 示例定价计划(买断 + 月订阅 + 年订阅) // Logan.Li
|
||||
plans := []model.Plan{
|
||||
{ProductID: product.ID, Name: "个人永久版", PlanType: "perpetual", Price: 199, Currency: "USD", Duration: 0, MaxDevices: 1, Features: `["全部功能","永久使用","1 设备"]`, SortOrder: 1, Status: 1},
|
||||
{ProductID: product.ID, Name: "专业版(月)", PlanType: "monthly", Price: 19, Currency: "USD", Duration: 30, MaxDevices: 3, Features: `["全部功能","按月订阅","3 设备"]`, SortOrder: 2, Status: 1},
|
||||
{ProductID: product.ID, Name: "企业版(年)", PlanType: "yearly", Price: 199, Currency: "USD", Duration: 365, MaxDevices: 10, Features: `["全部功能","按年订阅优惠","10 设备","优先支持"]`, SortOrder: 3, Status: 1},
|
||||
{
|
||||
ProductID: product.ID,
|
||||
Name: "Yearly",
|
||||
PlanType: "yearly",
|
||||
Price: 99,
|
||||
Currency: "CNY",
|
||||
Duration: 365,
|
||||
MaxDevices: 2,
|
||||
Features: `["专业功能","包年","2 设备"]`,
|
||||
SortOrder: 1,
|
||||
Status: 1,
|
||||
},
|
||||
{
|
||||
ProductID: product.ID,
|
||||
Name: "Perpetual",
|
||||
PlanType: "perpetual",
|
||||
Price: 199,
|
||||
Currency: "CNY",
|
||||
Duration: 0,
|
||||
MaxDevices: 5,
|
||||
Features: `["专业功能","永久授权","5 设备"]`,
|
||||
SortOrder: 2,
|
||||
Status: 1,
|
||||
},
|
||||
{
|
||||
ProductID: product.ID,
|
||||
Name: "Trial 180d",
|
||||
PlanType: "trial",
|
||||
Price: 0,
|
||||
Currency: "USD",
|
||||
Duration: 180,
|
||||
MaxDevices: 1,
|
||||
Features: `["专业功能","半年体验","1 设备"]`,
|
||||
SortOrder: 0,
|
||||
Status: 1,
|
||||
},
|
||||
}
|
||||
for i := range plans {
|
||||
if err := db.Create(&plans[i]).Error; err != nil {
|
||||
@@ -120,7 +155,6 @@ func seedSampleData(db *gorm.DB) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 示例 License // Logan.Li
|
||||
now := time.Now()
|
||||
license := model.License{
|
||||
LicenseKey: utils.GenerateLicenseKey(),
|
||||
@@ -128,18 +162,17 @@ func seedSampleData(db *gorm.DB) error {
|
||||
ProductID: product.ID,
|
||||
Status: model.LicenseStatusActive,
|
||||
StartDate: now,
|
||||
EndDate: now.AddDate(0, 0, 365),
|
||||
Duration: 365,
|
||||
MaxActivations: 3,
|
||||
Notes: "示例授权,初始化生成",
|
||||
EndDate: model.PermanentEndDate,
|
||||
Duration: 0,
|
||||
MaxActivations: 5,
|
||||
Notes: "开发示例授权",
|
||||
}
|
||||
if err := db.Create(&license).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 示例支付网关配置(沙箱) // Logan.Li
|
||||
gateway := model.PaymentGatewayConfig{
|
||||
Gateway: "creem",
|
||||
Gateway: "waffo",
|
||||
Enabled: true,
|
||||
SandboxMode: true,
|
||||
Currency: "USD",
|
||||
@@ -149,6 +182,127 @@ func seedSampleData(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("[初始化] 已创建示例产品、客户、计划、License、支付网关")
|
||||
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{
|
||||
{"yearly", "Yearly", 99, "CNY", 365, 2, "[\"专业功能\",\"包年\",\"2 设备\"]", 1},
|
||||
{"perpetual", "Perpetual", 199, "CNY", 0, 5, "[\"专业功能\",\"永久授权\",\"5 设备\"]", 2},
|
||||
{"trial", "Trial 180d", 0, "CNY", 180, 1, "[\"专业功能\",\"半年体验\",\"1 设备\"]", 0},
|
||||
}
|
||||
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(包年 subscription / 永久 onetime)")
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user