Files
opsx-licence/backend/internal/service/license/client_service.go
T
Logan.Li 4ae318335c feat: 完成 opsx-licence 许可证管理平台全栈实现
基于 opsx-web-lite 框架(Go+Gin+Vue3+Element Plus)构建许可证管理服务器,
业务域参考 Licence 服务器 Web 版。

【后端 - 4层架构】
- 认证:单 Admin 表 + JWT(HS256)
- 核心授权域:Product / Customer / License / Activation
  · License 生成/验证/激活/机器绑定/心跳/续期/吊销
  · 客户端契约接口:/api/v1/license/{verify,activate,deactivate,heartbeat}
- 定价订单:Plan / Order / 支付网关配置
- 防重复购买:CustomerEntitlement + 幂等支付 webhook + 手动发放
- 分析审计:Dashboard 统计 / Analytics / AuditLog / Setting 管理

【前端】
- Dashboard(ECharts 图表)、产品、客户、License、计划、订单、权益、日志、设置
- 9 个完整 CRUD 业务页面 + 登录/布局

【部署】
- SQLite + 单二进制(//go:embed 嵌入前端)
- 开发:./run.sh start(前端5181/后端9001)
- 生产:./run.sh build && ./run.sh binary start
- 默认账号:admin/admin

Author: Logan.Li
2026-08-07 15:02:00 +08:00

159 lines
5.1 KiB
Go

package license
import (
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"time"
)
// 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
}
// 校验产品归属 // Logan.Li
if req.ProductID > 0 && lic.ProductID != req.ProductID {
return &dto.LicenseVerifyResponse{Valid: false, Message: "License 与产品不匹配"}, nil
}
// 状态检查 // Logan.Li
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
}
// 过期检查(惰性更新状态) // Logan.Li
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,
Message: "验证成功",
}, nil
}
// Activate 客户端激活 License(机器绑定) // Logan.Li
func (s *LicenseService) Activate(req dto.LicenseActivateRequest, clientIP string) (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 {
return nil, errors.New("License 已被吊销")
}
if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) {
return nil, errors.New("License 已过期")
}
// 同设备重复激活视为重新激活,不消耗额度 // Logan.Li
var existing model.Activation
if err := s.db.Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID).First(&existing).Error; err == nil {
existing.LastSeenAt = time.Now()
existing.IsOnline = true
existing.DeviceName = req.DeviceName
existing.DeviceInfo = req.DeviceInfo
existing.IP = clientIP
s.db.Save(&existing)
return map[string]interface{}{
"expires_at": lic.EndDate.Format("2006-01-02 15:04:05"),
"days_left": utils.CalculateDaysLeft(lic.EndDate),
}, nil
}
// 检查激活额度 // Logan.Li
if lic.CurrentActivations >= lic.MaxActivations {
return nil, errors.New("已达到最大激活数量限制")
}
// 创建激活记录 // Logan.Li
activation := model.Activation{
LicenseID: lic.ID,
DeviceID: req.DeviceID,
DeviceName: req.DeviceName,
DeviceInfo: req.DeviceInfo,
IP: clientIP,
Status: "active",
IsOnline: true,
ActivatedAt: time.Now(),
LastSeenAt: time.Now(),
}
if err := s.db.Create(&activation).Error; err != nil {
return nil, errors.New("激活失败")
}
lic.CurrentActivations++
if lic.Status == model.LicenseStatusInactive {
lic.Status = model.LicenseStatusActive
}
s.db.Save(&lic)
return map[string]interface{}{
"expires_at": lic.EndDate.Format("2006-01-02 15:04:05"),
"days_left": utils.CalculateDaysLeft(lic.EndDate),
}, nil
}
// Deactivate 客户端停用 // Logan.Li
func (s *LicenseService) Deactivate(req dto.LicenseDeactivateRequest) error {
var lic model.License
if err := s.db.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil {
return errors.New("无效的 License")
}
result := s.db.Model(&model.Activation{}).
Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID).
Update("status", "inactive")
if result.RowsAffected > 0 && lic.CurrentActivations > 0 {
lic.CurrentActivations--
s.db.Save(&lic)
}
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 {
return nil, errors.New("License 已被吊销")
}
if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) {
return nil, errors.New("License 已过期")
}
// 更新心跳时间与在线状态 // Logan.Li
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 map[string]interface{}{
"expires_at": lic.EndDate.Format("2006-01-02 15:04:05"),
"days_left": utils.CalculateDaysLeft(lic.EndDate),
}, nil
}