基于 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
344 lines
10 KiB
Go
344 lines
10 KiB
Go
package license
|
|
|
|
import (
|
|
"backend/internal/dto"
|
|
"backend/internal/model"
|
|
"backend/utils"
|
|
"encoding/json"
|
|
"errors"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// LicenseService License 业务逻辑层 // Logan.Li
|
|
type LicenseService struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewLicenseService 创建 service 实例 // Logan.Li
|
|
func NewLicenseService(db *gorm.DB) *LicenseService {
|
|
return &LicenseService{db: db}
|
|
}
|
|
|
|
// ListQuery 列表查询参数 // Logan.Li
|
|
type ListQuery struct {
|
|
Keyword string
|
|
Status string
|
|
CustomerID uint
|
|
ProductID uint
|
|
}
|
|
|
|
// List 分页查询 License // Logan.Li
|
|
func (s *LicenseService) List(page, pageSize int, q ListQuery) ([]dto.LicenseResponse, int64, error) {
|
|
var licenses []model.License
|
|
var total int64
|
|
|
|
query := s.db.Model(&model.License{}).Preload("Customer").Preload("Product")
|
|
if q.Keyword != "" {
|
|
query = query.Where("license_key LIKE ?", "%"+q.Keyword+"%")
|
|
}
|
|
if q.Status != "" {
|
|
query = query.Where("status = ?", q.Status)
|
|
}
|
|
if q.CustomerID > 0 {
|
|
query = query.Where("customer_id = ?", q.CustomerID)
|
|
}
|
|
if q.ProductID > 0 {
|
|
query = query.Where("product_id = ?", q.ProductID)
|
|
}
|
|
|
|
query.Count(&total)
|
|
if err := query.Order("created_at DESC").
|
|
Offset((page - 1) * pageSize).Limit(pageSize).
|
|
Find(&licenses).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
results := make([]dto.LicenseResponse, 0, len(licenses))
|
|
for _, l := range licenses {
|
|
results = append(results, toLicenseResponse(l))
|
|
}
|
|
return results, total, nil
|
|
}
|
|
|
|
// GetByID 获取 License 详情(含激活记录) // Logan.Li
|
|
func (s *LicenseService) GetByID(id uint) (*dto.LicenseResponse, []dto.ActivationResponse, error) {
|
|
var lic model.License
|
|
if err := s.db.Preload("Customer").Preload("Product").Preload("Activations").
|
|
First(&lic, id).Error; err != nil {
|
|
return nil, nil, errors.New("License 不存在")
|
|
}
|
|
|
|
resp := toLicenseResponse(lic)
|
|
activations := make([]dto.ActivationResponse, 0, len(lic.Activations))
|
|
for _, a := range lic.Activations {
|
|
activations = append(activations, toActivationResponse(a))
|
|
}
|
|
return &resp, activations, nil
|
|
}
|
|
|
|
// Create 创建 License(支持批量) // Logan.Li
|
|
func (s *LicenseService) Create(req dto.LicenseCreate) ([]dto.LicenseResponse, error) {
|
|
// 校验客户与产品 // Logan.Li
|
|
var customer model.Customer
|
|
if err := s.db.First(&customer, req.CustomerID).Error; err != nil {
|
|
return nil, errors.New("客户不存在")
|
|
}
|
|
var product model.Product
|
|
if err := s.db.First(&product, req.ProductID).Error; err != nil {
|
|
return nil, errors.New("产品不存在")
|
|
}
|
|
|
|
if req.MaxActivations <= 0 {
|
|
req.MaxActivations = 1
|
|
}
|
|
if req.Count <= 0 {
|
|
req.Count = 1
|
|
}
|
|
if req.Count > 100 {
|
|
req.Count = 100
|
|
}
|
|
|
|
// 计算到期日期 // Logan.Li
|
|
var endDate time.Time
|
|
if req.Duration == model.DurationForever {
|
|
endDate = model.PermanentEndDate
|
|
} else {
|
|
endDate = time.Now().AddDate(0, 0, req.Duration)
|
|
}
|
|
|
|
licenses := make([]model.License, 0, req.Count)
|
|
for i := 0; i < req.Count; i++ {
|
|
licenses = append(licenses, model.License{
|
|
LicenseKey: utils.GenerateLicenseKey(),
|
|
CustomerID: req.CustomerID,
|
|
ProductID: req.ProductID,
|
|
Status: model.LicenseStatusInactive,
|
|
StartDate: time.Now(),
|
|
EndDate: endDate,
|
|
Duration: req.Duration,
|
|
MaxActivations: req.MaxActivations,
|
|
IsTrial: req.IsTrial,
|
|
TrialDays: req.TrialDays,
|
|
Notes: req.Notes,
|
|
})
|
|
}
|
|
|
|
if err := s.db.Create(&licenses).Error; err != nil {
|
|
return nil, errors.New("创建 License 失败")
|
|
}
|
|
|
|
results := make([]dto.LicenseResponse, 0, len(licenses))
|
|
for _, l := range licenses {
|
|
l.Customer = customer
|
|
l.Product = product
|
|
results = append(results, toLicenseResponse(l))
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// Update 更新 License // Logan.Li
|
|
func (s *LicenseService) Update(id uint, updates map[string]interface{}) (*dto.LicenseResponse, error) {
|
|
var lic model.License
|
|
if err := s.db.First(&lic, id).Error; err != nil {
|
|
return nil, errors.New("License 不存在")
|
|
}
|
|
if err := s.db.Model(&lic).Updates(updates).Error; err != nil {
|
|
return nil, errors.New("更新 License 失败")
|
|
}
|
|
s.db.Preload("Customer").Preload("Product").First(&lic, id)
|
|
resp := toLicenseResponse(lic)
|
|
return &resp, nil
|
|
}
|
|
|
|
// Delete 删除 License(软删除) // Logan.Li
|
|
func (s *LicenseService) Delete(id uint) (*model.License, error) {
|
|
var lic model.License
|
|
if err := s.db.First(&lic, id).Error; err != nil {
|
|
return nil, errors.New("License 不存在")
|
|
}
|
|
if err := s.db.Delete(&model.License{}, id).Error; err != nil {
|
|
return nil, errors.New("删除 License 失败")
|
|
}
|
|
return &lic, nil
|
|
}
|
|
|
|
// Renew 续期 // Logan.Li
|
|
func (s *LicenseService) Renew(id uint, duration int) (*dto.LicenseResponse, error) {
|
|
var lic model.License
|
|
if err := s.db.First(&lic, id).Error; err != nil {
|
|
return nil, errors.New("License 不存在")
|
|
}
|
|
|
|
var newEndDate time.Time
|
|
if duration == model.DurationForever {
|
|
newEndDate = model.PermanentEndDate
|
|
} else {
|
|
// 续期在原到期日基础上叠加(若已过期则从当前起算) // Logan.Li
|
|
base := lic.EndDate
|
|
if utils.IsLicenseExpired(lic.EndDate) {
|
|
base = time.Now()
|
|
}
|
|
newEndDate = base.AddDate(0, 0, duration)
|
|
}
|
|
|
|
lic.EndDate = newEndDate
|
|
lic.Duration = duration
|
|
lic.Status = model.LicenseStatusActive
|
|
if err := s.db.Save(&lic).Error; err != nil {
|
|
return nil, errors.New("续期失败")
|
|
}
|
|
s.db.Preload("Customer").Preload("Product").First(&lic, id)
|
|
resp := toLicenseResponse(lic)
|
|
return &resp, nil
|
|
}
|
|
|
|
// Revoke 吊销(同时停用所有激活) // Logan.Li
|
|
func (s *LicenseService) Revoke(id uint, reason string) (*dto.LicenseResponse, error) {
|
|
var lic model.License
|
|
if err := s.db.First(&lic, id).Error; err != nil {
|
|
return nil, errors.New("License 不存在")
|
|
}
|
|
lic.Status = model.LicenseStatusRevoked
|
|
lic.RevokeReason = reason
|
|
if err := s.db.Save(&lic).Error; err != nil {
|
|
return nil, errors.New("吊销失败")
|
|
}
|
|
// 级联停用所有激活记录 // Logan.Li
|
|
s.db.Model(&model.Activation{}).Where("license_id = ?", id).Update("status", "inactive")
|
|
s.db.Preload("Customer").Preload("Product").First(&lic, id)
|
|
resp := toLicenseResponse(lic)
|
|
return &resp, nil
|
|
}
|
|
|
|
// SetStatus 设置状态(暂停/激活) // Logan.Li
|
|
func (s *LicenseService) SetStatus(id uint, status string) (*dto.LicenseResponse, error) {
|
|
var lic model.License
|
|
if err := s.db.First(&lic, id).Error; err != nil {
|
|
return nil, errors.New("License 不存在")
|
|
}
|
|
lic.Status = status
|
|
if err := s.db.Save(&lic).Error; err != nil {
|
|
return nil, errors.New("状态更新失败")
|
|
}
|
|
s.db.Preload("Customer").Preload("Product").First(&lic, id)
|
|
resp := toLicenseResponse(lic)
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetActivations 获取激活记录 // Logan.Li
|
|
func (s *LicenseService) GetActivations(licenseID uint) ([]dto.ActivationResponse, error) {
|
|
var activations []model.Activation
|
|
if err := s.db.Where("license_id = ?", licenseID).Order("created_at DESC").Find(&activations).Error; err != nil {
|
|
return nil, errors.New("获取激活记录失败")
|
|
}
|
|
results := make([]dto.ActivationResponse, 0, len(activations))
|
|
for _, a := range activations {
|
|
results = append(results, toActivationResponse(a))
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// ForceOffline 强制下线设备 // Logan.Li
|
|
func (s *LicenseService) ForceOffline(licenseID, activationID uint) error {
|
|
result := s.db.Model(&model.Activation{}).
|
|
Where("id = ? AND license_id = ?", activationID, licenseID).
|
|
Updates(map[string]interface{}{"status": "inactive", "is_online": false})
|
|
if result.Error != nil {
|
|
return errors.New("强制下线失败")
|
|
}
|
|
if result.RowsAffected > 0 {
|
|
var lic model.License
|
|
s.db.First(&lic, licenseID)
|
|
if lic.CurrentActivations > 0 {
|
|
lic.CurrentActivations--
|
|
s.db.Save(&lic)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ExportCSV 导出 License 列表为 CSV // Logan.Li
|
|
func (s *LicenseService) ExportCSV(q ListQuery) (string, error) {
|
|
var licenses []model.License
|
|
query := s.db.Model(&model.License{}).Preload("Customer").Preload("Product")
|
|
if q.Status != "" {
|
|
query = query.Where("status = ?", q.Status)
|
|
}
|
|
if q.CustomerID > 0 {
|
|
query = query.Where("customer_id = ?", q.CustomerID)
|
|
}
|
|
if q.ProductID > 0 {
|
|
query = query.Where("product_id = ?", q.ProductID)
|
|
}
|
|
if err := query.Find(&licenses).Error; err != nil {
|
|
return "", errors.New("导出失败")
|
|
}
|
|
|
|
csv := "License Key,产品,客户,状态,到期时间,激活数/最大激活数,创建时间\n"
|
|
for _, l := range licenses {
|
|
csv += l.LicenseKey + ","
|
|
csv += l.Product.Name + ","
|
|
csv += l.Customer.Name + ","
|
|
csv += l.Status + ","
|
|
csv += l.EndDate.Format("2006-01-02") + ","
|
|
csv += strconv.Itoa(l.CurrentActivations) + "/" + strconv.Itoa(l.MaxActivations) + ","
|
|
csv += l.CreatedAt.Format("2006-01-02 15:04:05") + "\n"
|
|
}
|
|
return csv, nil
|
|
}
|
|
|
|
// toLicenseResponse 模型转响应 DTO // Logan.Li
|
|
func toLicenseResponse(l model.License) dto.LicenseResponse {
|
|
customerName := ""
|
|
if l.Customer.ID > 0 {
|
|
customerName = l.Customer.Name
|
|
}
|
|
productName := ""
|
|
if l.Product.ID > 0 {
|
|
productName = l.Product.Name
|
|
}
|
|
return dto.LicenseResponse{
|
|
ID: l.ID,
|
|
LicenseKey: l.LicenseKey,
|
|
CustomerID: l.CustomerID,
|
|
CustomerName: customerName,
|
|
ProductID: l.ProductID,
|
|
ProductName: productName,
|
|
Status: l.Status,
|
|
StartDate: l.StartDate.Format("2006-01-02 15:04:05"),
|
|
EndDate: l.EndDate.Format("2006-01-02 15:04:05"),
|
|
Duration: l.Duration,
|
|
MaxActivations: l.MaxActivations,
|
|
CurrentActivations: l.CurrentActivations,
|
|
IsTrial: l.IsTrial,
|
|
DaysLeft: utils.CalculateDaysLeft(l.EndDate),
|
|
Notes: l.Notes,
|
|
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
}
|
|
}
|
|
|
|
// toActivationResponse 模型转响应 DTO // Logan.Li
|
|
func toActivationResponse(a model.Activation) dto.ActivationResponse {
|
|
return dto.ActivationResponse{
|
|
ID: a.ID,
|
|
LicenseID: a.LicenseID,
|
|
DeviceID: a.DeviceID,
|
|
DeviceName: a.DeviceName,
|
|
DeviceInfo: a.DeviceInfo,
|
|
IP: a.IP,
|
|
Status: a.Status,
|
|
IsOnline: a.IsOnline,
|
|
ActivatedAt: a.ActivatedAt.Format("2006-01-02 15:04:05"),
|
|
LastSeenAt: a.LastSeenAt.Format("2006-01-02 15:04:05"),
|
|
}
|
|
}
|
|
|
|
// MarshalForAudit 序列化为 JSON 用于审计日志 // Logan.Li
|
|
func MarshalForAudit(v interface{}) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|