package service import ( "crypto/hmac" "crypto/sha256" "encoding/base32" "errors" "fmt" "log" "strings" "time" "gorm.io/gorm" "github.com/wangjia/jiu/backend/config" "github.com/wangjia/jiu/backend/internal/model" "github.com/wangjia/jiu/backend/internal/util" ) var ( ErrLicenseNotFound = errors.New("license not found") ErrLicenseInactive = errors.New("license is inactive") ErrLicenseExpired = errors.New("license has expired") ErrDeviceLimitExceed = errors.New("device limit reached — deactivate another device first") ) type LicenseService struct { db *gorm.DB } func NewLicenseService(db *gorm.DB) *LicenseService { return &LicenseService{db: db} } // GenerateKey 生成许可证激活码 // 格式:HMAC-SHA256(shopID+licenseType+expiry, secret) → base32, 每5字符加'-' func GenerateKey(shopID uint64, licenseType string, expiresAt *time.Time) string { payload := fmt.Sprintf("%d:%s", shopID, licenseType) if expiresAt != nil { payload += ":" + expiresAt.Format("20060102") } mac := hmac.New(sha256.New, []byte(config.C.License.HMACSecret)) mac.Write([]byte(payload)) raw := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(mac.Sum(nil)) // 截取前20字符,分4段,每段5字符 raw = strings.ToUpper(raw)[:20] return fmt.Sprintf("%s-%s-%s-%s", raw[0:5], raw[5:10], raw[10:15], raw[15:20]) } // Activate 激活许可证并绑定设备到 license_devices 表。 // 若该设备已绑定,则更新 last_seen_at(幂等)。 // 若是新设备,则校验是否超出 max_devices 上限。 func (s *LicenseService) Activate(shopID uint64, licenseKey, deviceID, deviceName, platform string) (*model.License, error) { var lic model.License if err := s.db.Where("license_key = ? AND shop_id = ?", licenseKey, shopID).First(&lic).Error; err != nil { return nil, ErrLicenseNotFound } if !lic.IsActive { return nil, ErrLicenseInactive } if lic.ExpiresAt != nil && time.Now().After(*lic.ExpiresAt) { return nil, ErrLicenseExpired } var existing model.LicenseDevice err := s.db.Where("license_id = ? AND device_id = ?", lic.ID, deviceID).First(&existing).Error if err == nil { // Device already bound — just touch last_seen_at (handled by autoUpdateTime) if err := s.db.Model(&existing).Update("device_name", deviceName).Error; err != nil { return nil, err } return &lic, nil } // New device — enforce max_devices var count int64 if err := s.db.Model(&model.LicenseDevice{}).Where("license_id = ?", lic.ID).Count(&count).Error; err != nil { return nil, err } if int(count) >= lic.MaxDevices { return nil, ErrDeviceLimitExceed } dev := model.LicenseDevice{ LicenseID: lic.ID, ShopID: shopID, DeviceID: deviceID, DeviceName: deviceName, Platform: platform, } if err := s.db.Create(&dev).Error; err != nil { return nil, err } return &lic, nil } // Verify 验证设备许可证(客户端启动时调用)。 // 通过 license_devices 表查找设备,再加载对应的许可证做有效性检查。 func (s *LicenseService) Verify(shopID uint64, deviceID string) (*model.License, error) { var dev model.LicenseDevice if err := s.db.Where("shop_id = ? AND device_id = ?", shopID, deviceID).First(&dev).Error; err != nil { return nil, ErrLicenseNotFound } var lic model.License if err := s.db.Where("id = ? AND shop_id = ? AND is_active = 1", dev.LicenseID, shopID).First(&lic).Error; err != nil { return nil, ErrLicenseNotFound } if lic.ExpiresAt != nil && time.Now().After(*lic.ExpiresAt) { return nil, ErrLicenseExpired } return &lic, nil } // ShopInfo 返回门店当前授权信息(取最新一条有效许可证) func (s *LicenseService) ShopInfo(shopID uint64) (*model.License, error) { var lic model.License if err := s.db.Where("shop_id = ? AND is_active = 1", shopID). Order("id DESC").First(&lic).Error; err != nil { return nil, ErrLicenseNotFound } return &lic, nil } // CountDevices 返回指定 license 下已绑定设备数。 func (s *LicenseService) CountDevices(licenseID uint64) (int64, error) { var count int64 err := s.db.Model(&model.LicenseDevice{}).Where("license_id = ?", licenseID).Count(&count).Error return count, err } // ListDevices 列出许可证下所有已绑定设备。 func (s *LicenseService) ListDevices(shopID uint64) ([]model.LicenseDevice, error) { var devs []model.LicenseDevice if err := s.db.Where("shop_id = ?", shopID).Order("activated_at DESC").Find(&devs).Error; err != nil { return nil, err } return devs, nil } // Deactivate 解绑设备(从 license_devices 删除该条记录)。 func (s *LicenseService) Deactivate(shopID uint64, deviceID string) error { return s.db.Where("shop_id = ? AND device_id = ?", shopID, deviceID). Delete(&model.LicenseDevice{}).Error } // createTrialLicense 在注册事务中为新门店签发 30 天 trial license。 // 私钥未配置时 Fatal,防止生产环境静默跳过导致新注册门店无 license。 func createTrialLicense(tx *gorm.DB, shopID uint64) { privKey := config.C.License.Ed25519PrivateKey if privKey == "" { log.Fatalf("[license] Ed25519 private key not configured — cannot issue trial for shop %d; set License.Ed25519PrivateKey in config", shopID) } expiresAt := time.Now().Add(30 * 24 * time.Hour) expiresUnix := expiresAt.Unix() payload := util.LicensePayload{ ShopID: shopID, Type: "trial", IssuedAt: time.Now().Unix(), ExpiresAt: &expiresUnix, MaxDevices: 1, } token, err := util.IssueLicenseToken(payload, privKey) if err != nil { log.Printf("[license] failed to issue trial token for shop %d: %v", shopID, err) return } lic := model.License{ ShopID: shopID, LicenseKey: token, Type: "trial", ExpiresAt: &expiresAt, IsActive: true, MaxDevices: 1, } if err := tx.Create(&lic).Error; err != nil { log.Printf("[license] failed to create trial license for shop %d: %v", shopID, err) } }