package license import ( "backend/internal/dto" "backend/internal/model" customersvc "backend/internal/service/customer" "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 失败") } _ = customersvc.AttachProduct(s.db, req.CustomerID, req.ProductID) 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 !isDeletableLicense(lic) { return nil, errors.New("仅已吊销或已过期的 License 可删除") } if err := s.db.Delete(&model.License{}, id).Error; err != nil { return nil, errors.New("删除 License 失败") } return &lic, nil } // BatchDeleteResult 批量删除结果 // Logan.Li type BatchDeleteResult struct { Deleted int `json:"deleted"` Skipped int `json:"skipped"` Failed int `json:"failed"` SkippedIDs []uint `json:"skipped_ids,omitempty"` } // BatchDelete 批量删除:仅删已吊销/已过期,其余跳过 // Logan.Li func (s *LicenseService) BatchDelete(ids []uint) (*BatchDeleteResult, error) { result := &BatchDeleteResult{} if len(ids) == 0 { return result, nil } seen := map[uint]struct{}{} for _, id := range ids { if id == 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} var lic model.License if err := s.db.First(&lic, id).Error; err != nil { result.Failed++ continue } if !isDeletableLicense(lic) { result.Skipped++ result.SkippedIDs = append(result.SkippedIDs, id) continue } if err := s.db.Delete(&model.License{}, id).Error; err != nil { result.Failed++ continue } result.Deleted++ } return result, nil } // isDeletableLicense 仅吊销或到期可物理清理(软删) // Logan.Li func isDeletableLicense(lic model.License) bool { if lic.Status == model.LicenseStatusRevoked || lic.Status == model.LicenseStatusExpired { return true } return utils.IsLicenseExpired(lic.EndDate) } // 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 := "" customerEmail := "" if l.Customer.ID > 0 { customerName = l.Customer.Name if !strings.HasSuffix(l.Customer.Email, "@local.invalid") { customerEmail = l.Customer.Email } } 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, CustomerEmail: customerEmail, 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) }