296 lines
10 KiB
Go
296 lines
10 KiB
Go
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
|
|
}
|