feat: 通用授权后台、离线证、Waffo 支付页与仪表盘加强

支持多产品计划展示、LICOFF 离线签发、支付独立配置与调试,以及分析看板;同步更新 README 与集成文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-12 20:25:46 +08:00
1 parent 0966375076
commit 8cc2c83f9d
41 files changed
+3177 -332

No files matched your search

@@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"gorm.io/gorm"
@@ -28,6 +29,7 @@ type ListQuery struct {
Status string
CustomerID uint
ProductID uint
Channel string // online | offline | ""(全部)
}
// List 分页查询 License // Logan.Li
@@ -35,7 +37,7 @@ func (s *LicenseService) List(page, pageSize int, q ListQuery) ([]dto.LicenseRes
var licenses []model.License
var total int64
query := s.db.Model(&model.License{}).Preload("Customer").Preload("Product")
query := s.db.Model(&model.License{}).Preload("Customer").Preload("Product").Preload("Activations")
if q.Keyword != "" {
query = query.Where("license_key LIKE ?", "%"+q.Keyword+"%")
}
@@ -48,6 +50,12 @@ func (s *LicenseService) List(page, pageSize int, q ListQuery) ([]dto.LicenseRes
if q.ProductID > 0 {
query = query.Where("product_id = ?", q.ProductID)
}
switch strings.TrimSpace(q.Channel) {
case "offline":
query = query.Where("license_key LIKE ?", "LICOFF-%")
case "online":
query = query.Where("license_key NOT LIKE ?", "LICOFF-%")
}
query.Count(&total)
if err := query.Order("created_at DESC").
@@ -165,12 +173,15 @@ func (s *LicenseService) Delete(id uint) (*model.License, error) {
return &lic, nil
}
// Renew 续期 // Logan.Li
// Renew 续期(仅在线证;离线证签名自包含,到期后应重新签发) // Logan.Li
func (s *LicenseService) Renew(id uint, duration int) (*dto.LicenseResponse, error) {
var lic model.License
if err := s.db.First(&lic, id).Error; err != nil {
return nil, errors.New("License 不存在")
}
if utils.IsOfflineLicenseKey(lic.LicenseKey) {
return nil, errors.New("离线许可证不可续期:到期后请重新签发新序列号(绑定同一设备 ID)")
}
var newEndDate time.Time
if duration == model.DurationForever {
@@ -300,9 +311,25 @@ func toLicenseResponse(l model.License) dto.LicenseResponse {
if l.Product.ID > 0 {
productName = l.Product.Name
}
offline := utils.IsOfflineLicenseKey(l.LicenseKey)
channel := "online"
if offline {
channel = "offline"
}
deviceID := ""
for _, a := range l.Activations {
if strings.TrimSpace(a.DeviceID) != "" {
deviceID = a.DeviceID
break
}
}
return dto.LicenseResponse{
ID: l.ID,
LicenseKey: l.LicenseKey,
LicenseKeyShort: shortLicenseKey(l.LicenseKey),
Channel: channel,
IsOffline: offline,
DeviceID: deviceID,
CustomerID: l.CustomerID,
CustomerName: customerName,
ProductID: l.ProductID,
@@ -320,6 +347,18 @@ func toLicenseResponse(l model.License) dto.LicenseResponse {
}
}
// shortLicenseKey 表格短展示:在线保留全文;离线保留前缀+尾部。 // Logan.Li
func shortLicenseKey(key string) string {
key = strings.TrimSpace(key)
if !utils.IsOfflineLicenseKey(key) {
return key
}
if len(key) <= 28 {
return key
}
return key[:14] + "…" + key[len(key)-8:]
}
// toActivationResponse 模型转响应 DTO // Logan.Li
func toActivationResponse(a model.Activation) dto.ActivationResponse {
return dto.ActivationResponse{