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

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

383 lines
11 KiB
Go

package license
import (
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
// LicenseService License 业务逻辑层 // Logan.Li
type LicenseService struct {
db *gorm.DB
}
// NewLicenseService 创建 service 实例 // Logan.Li
func NewLicenseService(db *gorm.DB) *LicenseService {
return &LicenseService{db: db}
}
// ListQuery 列表查询参数 // Logan.Li
type ListQuery struct {
Keyword string
Status string
CustomerID uint
ProductID uint
Channel string // online | offline | ""(全部)
}
// List 分页查询 License // Logan.Li
func (s *LicenseService) List(page, pageSize int, q ListQuery) ([]dto.LicenseResponse, int64, error) {
var licenses []model.License
var total int64
query := s.db.Model(&model.License{}).Preload("Customer").Preload("Product").Preload("Activations")
if q.Keyword != "" {
query = query.Where("license_key LIKE ?", "%"+q.Keyword+"%")
}
if q.Status != "" {
query = query.Where("status = ?", q.Status)
}
if q.CustomerID > 0 {
query = query.Where("customer_id = ?", q.CustomerID)
}
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").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&licenses).Error; err != nil {
return nil, 0, err
}
results := make([]dto.LicenseResponse, 0, len(licenses))
for _, l := range licenses {
results = append(results, toLicenseResponse(l))
}
return results, total, nil
}
// GetByID 获取 License 详情(含激活记录) // Logan.Li
func (s *LicenseService) GetByID(id uint) (*dto.LicenseResponse, []dto.ActivationResponse, error) {
var lic model.License
if err := s.db.Preload("Customer").Preload("Product").Preload("Activations").
First(&lic, id).Error; err != nil {
return nil, nil, errors.New("License 不存在")
}
resp := toLicenseResponse(lic)
activations := make([]dto.ActivationResponse, 0, len(lic.Activations))
for _, a := range lic.Activations {
activations = append(activations, toActivationResponse(a))
}
return &resp, activations, nil
}
// Create 创建 License(支持批量) // Logan.Li
func (s *LicenseService) Create(req dto.LicenseCreate) ([]dto.LicenseResponse, error) {
// 校验客户与产品 // Logan.Li
var customer model.Customer
if err := s.db.First(&customer, req.CustomerID).Error; err != nil {
return nil, errors.New("客户不存在")
}
var product model.Product
if err := s.db.First(&product, req.ProductID).Error; err != nil {
return nil, errors.New("产品不存在")
}
if req.MaxActivations <= 0 {
req.MaxActivations = 1
}
if req.Count <= 0 {
req.Count = 1
}
if req.Count > 100 {
req.Count = 100
}
// 计算到期日期 // Logan.Li
var endDate time.Time
if req.Duration == model.DurationForever {
endDate = model.PermanentEndDate
} else {
endDate = time.Now().AddDate(0, 0, req.Duration)
}
licenses := make([]model.License, 0, req.Count)
for i := 0; i < req.Count; i++ {
licenses = append(licenses, model.License{
LicenseKey: utils.GenerateLicenseKey(),
CustomerID: req.CustomerID,
ProductID: req.ProductID,
Status: model.LicenseStatusInactive,
StartDate: time.Now(),
EndDate: endDate,
Duration: req.Duration,
MaxActivations: req.MaxActivations,
IsTrial: req.IsTrial,
TrialDays: req.TrialDays,
Notes: req.Notes,
})
}
if err := s.db.Create(&licenses).Error; err != nil {
return nil, errors.New("创建 License 失败")
}
results := make([]dto.LicenseResponse, 0, len(licenses))
for _, l := range licenses {
l.Customer = customer
l.Product = product
results = append(results, toLicenseResponse(l))
}
return results, nil
}
// Update 更新 License // Logan.Li
func (s *LicenseService) Update(id uint, updates map[string]interface{}) (*dto.LicenseResponse, error) {
var lic model.License
if err := s.db.First(&lic, id).Error; err != nil {
return nil, errors.New("License 不存在")
}
if err := s.db.Model(&lic).Updates(updates).Error; err != nil {
return nil, errors.New("更新 License 失败")
}
s.db.Preload("Customer").Preload("Product").First(&lic, id)
resp := toLicenseResponse(lic)
return &resp, nil
}
// Delete 删除 License(软删除) // Logan.Li
func (s *LicenseService) Delete(id uint) (*model.License, error) {
var lic model.License
if err := s.db.First(&lic, id).Error; err != nil {
return nil, errors.New("License 不存在")
}
if err := s.db.Delete(&model.License{}, id).Error; err != nil {
return nil, errors.New("删除 License 失败")
}
return &lic, nil
}
// 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 {
newEndDate = model.PermanentEndDate
} else {
// 续期在原到期日基础上叠加(若已过期则从当前起算) // Logan.Li
base := lic.EndDate
if utils.IsLicenseExpired(lic.EndDate) {
base = time.Now()
}
newEndDate = base.AddDate(0, 0, duration)
}
lic.EndDate = newEndDate
lic.Duration = duration
lic.Status = model.LicenseStatusActive
if err := s.db.Save(&lic).Error; err != nil {
return nil, errors.New("续期失败")
}
s.db.Preload("Customer").Preload("Product").First(&lic, id)
resp := toLicenseResponse(lic)
return &resp, nil
}
// Revoke 吊销(同时停用所有激活) // Logan.Li
func (s *LicenseService) Revoke(id uint, reason string) (*dto.LicenseResponse, error) {
var lic model.License
if err := s.db.First(&lic, id).Error; err != nil {
return nil, errors.New("License 不存在")
}
lic.Status = model.LicenseStatusRevoked
lic.RevokeReason = reason
if err := s.db.Save(&lic).Error; err != nil {
return nil, errors.New("吊销失败")
}
// 级联停用所有激活记录 // Logan.Li
s.db.Model(&model.Activation{}).Where("license_id = ?", id).Update("status", "inactive")
s.db.Preload("Customer").Preload("Product").First(&lic, id)
resp := toLicenseResponse(lic)
return &resp, nil
}
// SetStatus 设置状态(暂停/激活) // Logan.Li
func (s *LicenseService) SetStatus(id uint, status string) (*dto.LicenseResponse, error) {
var lic model.License
if err := s.db.First(&lic, id).Error; err != nil {
return nil, errors.New("License 不存在")
}
lic.Status = status
if err := s.db.Save(&lic).Error; err != nil {
return nil, errors.New("状态更新失败")
}
s.db.Preload("Customer").Preload("Product").First(&lic, id)
resp := toLicenseResponse(lic)
return &resp, nil
}
// GetActivations 获取激活记录 // Logan.Li
func (s *LicenseService) GetActivations(licenseID uint) ([]dto.ActivationResponse, error) {
var activations []model.Activation
if err := s.db.Where("license_id = ?", licenseID).Order("created_at DESC").Find(&activations).Error; err != nil {
return nil, errors.New("获取激活记录失败")
}
results := make([]dto.ActivationResponse, 0, len(activations))
for _, a := range activations {
results = append(results, toActivationResponse(a))
}
return results, nil
}
// ForceOffline 强制下线设备 // Logan.Li
func (s *LicenseService) ForceOffline(licenseID, activationID uint) error {
result := s.db.Model(&model.Activation{}).
Where("id = ? AND license_id = ?", activationID, licenseID).
Updates(map[string]interface{}{"status": "inactive", "is_online": false})
if result.Error != nil {
return errors.New("强制下线失败")
}
if result.RowsAffected > 0 {
var lic model.License
s.db.First(&lic, licenseID)
if lic.CurrentActivations > 0 {
lic.CurrentActivations--
s.db.Save(&lic)
}
}
return nil
}
// ExportCSV 导出 License 列表为 CSV // Logan.Li
func (s *LicenseService) ExportCSV(q ListQuery) (string, error) {
var licenses []model.License
query := s.db.Model(&model.License{}).Preload("Customer").Preload("Product")
if q.Status != "" {
query = query.Where("status = ?", q.Status)
}
if q.CustomerID > 0 {
query = query.Where("customer_id = ?", q.CustomerID)
}
if q.ProductID > 0 {
query = query.Where("product_id = ?", q.ProductID)
}
if err := query.Find(&licenses).Error; err != nil {
return "", errors.New("导出失败")
}
csv := "License Key,产品,客户,状态,到期时间,激活数/最大激活数,创建时间\n"
for _, l := range licenses {
csv += l.LicenseKey + ","
csv += l.Product.Name + ","
csv += l.Customer.Name + ","
csv += l.Status + ","
csv += l.EndDate.Format("2006-01-02") + ","
csv += strconv.Itoa(l.CurrentActivations) + "/" + strconv.Itoa(l.MaxActivations) + ","
csv += l.CreatedAt.Format("2006-01-02 15:04:05") + "\n"
}
return csv, nil
}
// toLicenseResponse 模型转响应 DTO // Logan.Li
func toLicenseResponse(l model.License) dto.LicenseResponse {
customerName := ""
if l.Customer.ID > 0 {
customerName = l.Customer.Name
}
productName := ""
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,
ProductName: productName,
Status: l.Status,
StartDate: l.StartDate.Format("2006-01-02 15:04:05"),
EndDate: l.EndDate.Format("2006-01-02 15:04:05"),
Duration: l.Duration,
MaxActivations: l.MaxActivations,
CurrentActivations: l.CurrentActivations,
IsTrial: l.IsTrial,
DaysLeft: utils.CalculateDaysLeft(l.EndDate),
Notes: l.Notes,
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
// 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{
ID: a.ID,
LicenseID: a.LicenseID,
DeviceID: a.DeviceID,
DeviceName: a.DeviceName,
DeviceInfo: a.DeviceInfo,
IP: a.IP,
Status: a.Status,
IsOnline: a.IsOnline,
ActivatedAt: a.ActivatedAt.Format("2006-01-02 15:04:05"),
LastSeenAt: a.LastSeenAt.Format("2006-01-02 15:04:05"),
}
}
// MarshalForAudit 序列化为 JSON 用于审计日志 // Logan.Li
func MarshalForAudit(v interface{}) string {
b, _ := json.Marshal(v)
return string(b)
}