Files
opsx-licence/backend/internal/service/license/client_service.go
T
Logan.LiandCursor bac72e6945 fix: 按评审报告止血支付漏洞并加固授权链路
废除浏览器回跳自助结算,通用 webhook 强制密钥鉴权,Waffo 支持验签与未知事件忽略;重构订单状态机/激活占座/SQLite WAL,并补备份、readyz、限流与前端导出/登录页安全修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 21:26:58 +08:00

226 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}