package license import ( "backend/internal/dto" "backend/internal/model" "backend/utils" "errors" "time" "gorm.io/gorm" ) func inferPlanType(lic model.License) string { if lic.IsTrial { return "trial" } if lic.Duration <= 0 || lic.EndDate.Equal(model.PermanentEndDate) || lic.EndDate.Year() >= 2099 { return "perpetual" } // 先判月度,再判体验/包年,避免 monthly 成为死代码 // Logan.Li if lic.Duration <= 31 { return "monthly" } if lic.Duration <= 180 { return "trial" } return "yearly" } func licenseClientPayload(lic model.License) map[string]interface{} { return map[string]interface{}{ "expires_at": lic.EndDate.Format("2006-01-02 15:04:05"), "days_left": utils.CalculateDaysLeft(lic.EndDate), "max_devices": lic.MaxActivations, "plan_type": inferPlanType(lic), } } // Verify 客户端 License 验证 // Logan.Li func (s *LicenseService) Verify(req dto.LicenseVerifyRequest) (*dto.LicenseVerifyResponse, error) { var lic model.License if err := s.db.Preload("Product").Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil { return &dto.LicenseVerifyResponse{Valid: false, Message: "无效的 License"}, nil } if req.ProductID > 0 && lic.ProductID != req.ProductID { return &dto.LicenseVerifyResponse{Valid: false, Message: "License 与产品不匹配"}, nil } switch lic.Status { case model.LicenseStatusRevoked: return &dto.LicenseVerifyResponse{Valid: false, Message: "License 已被吊销"}, nil case model.LicenseStatusSuspended: return &dto.LicenseVerifyResponse{Valid: false, Message: "License 已被暂停"}, nil case model.LicenseStatusExpired: return &dto.LicenseVerifyResponse{Valid: false, Message: "License 已过期"}, nil } if utils.IsLicenseExpired(lic.EndDate) { lic.Status = model.LicenseStatusExpired s.db.Save(&lic) return &dto.LicenseVerifyResponse{Valid: false, Message: "License 已过期"}, nil } return &dto.LicenseVerifyResponse{ Valid: true, LicenseKey: lic.LicenseKey, ProductID: lic.ProductID, ProductName: lic.Product.Name, ProductCode: lic.Product.Code, Features: utils.ParseFeatures(lic.Product.Features), Status: lic.Status, ExpiresAt: lic.EndDate.Format("2006-01-02 15:04:05"), DaysLeft: utils.CalculateDaysLeft(lic.EndDate), MaxDevices: lic.MaxActivations, PlanType: inferPlanType(lic), Message: "验证成功", }, nil } // Activate 客户端激活 License(事务 + 条件占座,消除 TOCTOU) // Logan.Li func (s *LicenseService) Activate(req dto.LicenseActivateRequest, clientIP string) (map[string]interface{}, error) { var payload map[string]interface{} err := s.db.Transaction(func(tx *gorm.DB) error { var lic model.License if err := tx.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil { return errors.New("无效的 License") } if lic.Status == model.LicenseStatusRevoked || lic.Status == model.LicenseStatusSuspended { return errors.New("License 已被吊销或暂停") } if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) { return errors.New("License 已过期") } now := time.Now() var existing model.Activation if err := tx.Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID). First(&existing).Error; err == nil { updates := map[string]interface{}{ "last_seen_at": now, "is_online": true, "device_name": req.DeviceName, "device_info": req.DeviceInfo, "ip": clientIP, } if existing.Status != "active" { updates["status"] = "active" res := tx.Model(&model.License{}). Where("id = ? AND current_activations < max_activations", lic.ID). Update("current_activations", gorm.Expr("current_activations + 1")) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return errors.New("已达到最大激活数量限制") } } if err := tx.Model(&existing).Updates(updates).Error; err != nil { return err } if err := tx.First(&lic, lic.ID).Error; err != nil { return err } payload = licenseClientPayload(lic) return nil } res := tx.Model(&model.License{}). Where("id = ? AND current_activations < max_activations", lic.ID). Updates(map[string]interface{}{ "current_activations": gorm.Expr("current_activations + 1"), "status": model.LicenseStatusActive, }) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return errors.New("已达到最大激活数量限制") } if err := tx.Create(&model.Activation{ LicenseID: lic.ID, DeviceID: req.DeviceID, DeviceName: req.DeviceName, DeviceInfo: req.DeviceInfo, IP: clientIP, Status: "active", IsOnline: true, ActivatedAt: now, LastSeenAt: now, }).Error; err != nil { return errors.New("激活失败") } if err := tx.First(&lic, lic.ID).Error; err != nil { return err } payload = licenseClientPayload(lic) return nil }) if err != nil { return nil, err } return payload, nil } // Deactivate 客户端停用(仅 active→inactive 才减席位) // Logan.Li func (s *LicenseService) Deactivate(req dto.LicenseDeactivateRequest) error { return s.db.Transaction(func(tx *gorm.DB) error { var lic model.License if err := tx.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil { return errors.New("无效的 License") } res := tx.Model(&model.Activation{}). Where("license_id = ? AND device_id = ? AND status = ?", lic.ID, req.DeviceID, "active"). Updates(map[string]interface{}{"status": "inactive", "is_online": false}) if res.Error != nil { return res.Error } if res.RowsAffected > 0 { return tx.Model(&model.License{}). Where("id = ? AND current_activations > 0", lic.ID). Update("current_activations", gorm.Expr("current_activations - 1")).Error } return nil }) } // Heartbeat 客户端心跳 // Logan.Li func (s *LicenseService) Heartbeat(req dto.LicenseHeartbeatRequest) (map[string]interface{}, error) { var lic model.License if err := s.db.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil { return nil, errors.New("无效的 License") } if lic.Status == model.LicenseStatusRevoked || lic.Status == model.LicenseStatusSuspended { return nil, errors.New("License 已被吊销或暂停") } if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) { return nil, errors.New("License 已过期") } s.db.Model(&model.Activation{}). Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID). Updates(map[string]interface{}{"last_seen_at": time.Now(), "is_online": true}) return licenseClientPayload(lic), nil } // ListDevicesByKey returns activations for a license key (client self-service). // Logan.Li func (s *LicenseService) ListDevicesByKey(licenseKey string) ([]dto.ActivationResponse, int, error) { var lic model.License if err := s.db.Where("license_key = ?", licenseKey).First(&lic).Error; err != nil { return nil, 0, errors.New("无效的 License") } var activations []model.Activation if err := s.db.Where("license_id = ? AND status = ?", lic.ID, "active").Order("activated_at DESC").Find(&activations).Error; err != nil { return nil, 0, errors.New("获取设备列表失败") } results := make([]dto.ActivationResponse, 0, len(activations)) for _, a := range activations { results = append(results, toActivationResponse(a)) } return results, lic.MaxActivations, nil }