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{}{ "license_key": lic.LicenseKey, // 签名字段:客户端凭响应重建验签串,必须自带 // Logan.Li "expires_at": lic.EndDate.Format("2006-01-02 15:04:05"), "days_left": utils.CalculateDaysLeft(lic.EndDate), "max_devices": lic.MaxActivations, "status": lic.Status, "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 { resp := &dto.LicenseVerifyResponse{Valid: false, Message: "无效的 License"} s.signVerifyResponse(resp) return resp, nil } if req.ProductID > 0 && lic.ProductID != req.ProductID { resp := &dto.LicenseVerifyResponse{Valid: false, Message: "License 与产品不匹配"} s.signVerifyResponse(resp) return resp, nil } // 吊销/暂停是滥用处置,立即生效、无宽限 // Logan.Li switch lic.Status { case model.LicenseStatusRevoked: resp := &dto.LicenseVerifyResponse{Valid: false, Status: lic.Status, Message: "License 已被吊销"} s.signVerifyResponse(resp) return resp, nil case model.LicenseStatusSuspended: resp := &dto.LicenseVerifyResponse{Valid: false, Status: lic.Status, Message: "License 已被暂停"} s.signVerifyResponse(resp) return resp, nil } // 商业过期:宽限期内放行并提示续费,超出宽限才拒绝 // Logan.Li if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) { if lic.Status != model.LicenseStatusExpired { lic.Status = model.LicenseStatusExpired s.db.Save(&lic) } if inGrace, graceDays := s.inGraceInfo(lic); inGrace { resp := &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"), MaxDevices: lic.MaxActivations, PlanType: inferPlanType(lic), Message: "License 已过期,处于宽限期,请尽快续费", InGrace: true, GraceDaysLeft: graceDays, } s.signVerifyResponse(resp) return resp, nil } resp := &dto.LicenseVerifyResponse{Valid: false, Status: lic.Status, Message: "License 已过期"} s.signVerifyResponse(resp) return resp, nil } resp := &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: "验证成功", } s.signVerifyResponse(resp) return resp, nil } // Activate 客户端激活 License(事务 + 条件占座,消除 TOCTOU); // 新设备激活时下发设备激活令牌,并对响应签名。 // Logan.Li func (s *LicenseService) Activate(req dto.LicenseActivateRequest, clientIP string) (map[string]interface{}, error) { var payload map[string]interface{} var newToken string 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 } // 新设备占座前校验 24 小时换座上限,抑制序列号共享轮换 // Logan.Li if err := s.checkDailyActivationLimit(tx, lic.ID); err != nil { return err } 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("已达到最大激活数量限制") } newToken = generateActivationToken() if err := tx.Create(&model.Activation{ LicenseID: lic.ID, DeviceID: req.DeviceID, DeviceName: req.DeviceName, DeviceInfo: req.DeviceInfo, IP: clientIP, Status: "active", IsOnline: true, Token: newToken, 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 } if newToken != "" { // 令牌仅在本机首次占座时下发一次;客户端须持久保存用于后续解绑 // Logan.Li payload["activation_token"] = newToken } s.signPayloadMap(payload, req.LicenseKey) 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") } var act model.Activation hasRow := tx.Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID). First(&act).Error == nil // 存量记录 Token 为空(升级前激活),保持旧协议放行以兼容老客户端 // Logan.Li if hasRow && act.Token != "" && s.guard != nil && s.guard.deactivateRequireToken { if req.ActivationToken == "" || req.ActivationToken != act.Token { return errors.New("解绑校验失败:请在对应设备的工具内解绑,或联系管理员处理") } } 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) { // 宽限期内心跳照常放行,超出宽限才中断 // Logan.Li if inGrace, graceDays := s.inGraceInfo(lic); inGrace { payload := licenseClientPayload(lic) payload["in_grace"] = true payload["grace_days_left"] = graceDays payload["message"] = "License 已过期,处于宽限期,请尽快续费" s.signPayloadMap(payload, req.LicenseKey) return payload, nil } 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}) payload := licenseClientPayload(lic) s.signPayloadMap(payload, req.LicenseKey) return payload, 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 }