package license import ( "backend/config" "backend/internal/dto" "backend/internal/model" "backend/utils" "errors" "fmt" "strings" "time" "gorm.io/gorm" ) // OfflineIssuer 离线许可证签发(绑定 device_id,最长 365 天) // Logan.Li type OfflineIssuer struct { db *gorm.DB kp *utils.OfflineKeyPair } // NewOfflineIssuer 创建离线签发器;seed 为空时返回可用实例但 Issue 会失败 // Logan.Li func NewOfflineIssuer(db *gorm.DB, cfg *config.Config) *OfflineIssuer { issuer := &OfflineIssuer{db: db} if cfg == nil || strings.TrimSpace(cfg.OfflineLicenseSeed) == "" { return issuer } kp, err := utils.DeriveOfflineKeyPair(cfg.OfflineLicenseSeed) if err == nil { issuer.kp = kp } return issuer } // PublicKeyHex 返回当前公钥 hex(桌面嵌入对照) // Logan.Li func (s *OfflineIssuer) PublicKeyHex() string { if s == nil || s.kp == nil { return "" } return s.kp.OfflinePublicKeyHex() } // Issue 签发离线序列号并落库审计记录 // Logan.Li func (s *OfflineIssuer) Issue(req dto.OfflineLicenseCreate) (*dto.OfflineLicenseResponse, error) { if s == nil || s.kp == nil { return nil, errors.New("未配置 OFFLINE_LICENSE_SEED,无法签发离线证") } deviceID := strings.TrimSpace(req.DeviceID) if deviceID == "" { return nil, errors.New("device_id 不能为空") } planType := strings.TrimSpace(req.PlanType) if planType == "" { planType = "yearly" } if planType != "trial" && planType != "yearly" { return nil, errors.New("离线证仅允许体验(trial)或包年(yearly),禁止永久") } duration := req.DurationDays if duration <= 0 { duration = utils.OfflineMaxDurationDays } if duration > utils.OfflineMaxDurationDays { return nil, errors.New("离线证有效期不得超过 365 天") } if planType == "trial" && duration > 180 { duration = 180 } productCode := strings.TrimSpace(req.ProductCode) if productCode == "" { productCode = "opsxshell" } var product model.Product if err := s.db.Where("code = ?", productCode).First(&product).Error; err != nil { return nil, errors.New("产品不存在: " + productCode) } var customer model.Customer if req.CustomerID > 0 { if err := s.db.First(&customer, req.CustomerID).Error; err != nil { return nil, errors.New("客户不存在") } } else { var err error customer, err = s.resolveOrCreateCustomer(req) if err != nil { return nil, err } } now := time.Now().UTC() exp := now.AddDate(0, 0, duration) claimEmail := strings.TrimSpace(req.Email) if claimEmail == "" && customer.Email != "" && !strings.HasSuffix(customer.Email, "@local.invalid") { claimEmail = customer.Email } claims := utils.OfflineLicenseClaims{ V: 1, ProductCode: product.Code, DeviceID: deviceID, Email: claimEmail, PlanType: planType, IssuedAt: now.Unix(), ExpiresAt: exp.Unix(), MaxDevices: 1, CustomerName: customer.Name, } serial, err := utils.SignOfflineLicense(s.kp, claims) if err != nil { return nil, err } notes := strings.TrimSpace(req.Notes) if notes == "" { notes = "离线许可证" } notes = notes + " | device=" + deviceID + " | plan=" + planType lic := model.License{ LicenseKey: serial, CustomerID: customer.ID, ProductID: product.ID, Status: model.LicenseStatusActive, StartDate: now, EndDate: exp, Duration: duration, MaxActivations: 1, CurrentActivations: 1, IsTrial: planType == "trial", TrialDays: duration, Notes: notes, } if err := s.db.Create(&lic).Error; err != nil { return nil, errors.New("保存离线 License 失败") } activation := model.Activation{ LicenseID: lic.ID, DeviceID: deviceID, DeviceName: "offline", Status: "active", IsOnline: false, ActivatedAt: now, ExpiresAt: &exp, LastSeenAt: now, } _ = s.db.Create(&activation).Error return &dto.OfflineLicenseResponse{ Serial: serial, DeviceID: deviceID, Email: claims.Email, PlanType: planType, ProductCode: product.Code, IssuedAt: now.Format(time.RFC3339), ExpiresAt: exp.Format(time.RFC3339), Duration: duration, MaxDevices: 1, LicenseID: lic.ID, PublicKey: s.kp.OfflinePublicKeyHex(), }, nil } // resolveOrCreateCustomer 解析或创建离线签发客户;禁止重复插入空邮箱(uniqueIndex) // Logan.Li func (s *OfflineIssuer) resolveOrCreateCustomer(req dto.OfflineLicenseCreate) (model.Customer, error) { email := strings.TrimSpace(req.Email) name := strings.TrimSpace(req.CustomerName) if name == "" { name = "离线客户" } var customer model.Customer if email != "" { if err := s.db.Where("email = ?", email).First(&customer).Error; err == nil { return customer, nil } customer = model.Customer{Name: name, Email: email, Status: 1} if err := s.db.Create(&customer).Error; err != nil { // 并发下可能已被创建,再查一次 // Logan.Li if err2 := s.db.Where("email = ?", email).First(&customer).Error; err2 == nil { return customer, nil } return customer, errors.New("创建客户失败") } return customer, nil } // 无邮箱:优先复用同名客户,避免空邮箱触发 UNIQUE 冲突 // Logan.Li if err := s.db.Where("name = ?", name).Order("id ASC").First(&customer).Error; err == nil { return customer, nil } placeholder := fmt.Sprintf("offline+%d@local.invalid", time.Now().UnixNano()) customer = model.Customer{Name: name, Email: placeholder, Status: 1} if err := s.db.Create(&customer).Error; err != nil { return customer, errors.New("创建客户失败") } return customer, nil }