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
This commit is contained in:
Logan.Li committed 2026-08-07 15:02:00 +08:00
commit 4ae318335c
88 files changed
+10562

No files matched your search

@@ -0,0 +1,167 @@
package analytics
import (
"backend/internal/dto"
"backend/internal/model"
"time"
"gorm.io/gorm"
)
// AnalyticsService 数据分析业务逻辑层 // Logan.Li
type AnalyticsService struct {
db *gorm.DB
}
// NewAnalyticsService 创建 service 实例 // Logan.Li
func NewAnalyticsService(db *gorm.DB) *AnalyticsService {
return &AnalyticsService{db: db}
}
// GetDashboardStats 仪表板统计 // Logan.Li
func (s *AnalyticsService) GetDashboardStats() (*dto.DashboardStats, error) {
var stats dto.DashboardStats
s.db.Model(&model.License{}).Count(&stats.TotalLicenses)
s.db.Model(&model.License{}).Where("status = ?", model.LicenseStatusActive).Count(&stats.ActiveLicenses)
// 本月新增 // Logan.Li
now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
s.db.Model(&model.License{}).Where("created_at >= ?", monthStart).Count(&stats.NewThisMonth)
// 即将到期(30 天内) // Logan.Li
thirtyDaysLater := now.AddDate(0, 0, 30)
s.db.Model(&model.License{}).
Where("status = ? AND end_date > ? AND end_date < ?", model.LicenseStatusActive, now, thirtyDaysLater).
Count(&stats.ExpiringSoon)
s.db.Model(&model.Customer{}).Count(&stats.TotalCustomers)
s.db.Model(&model.Product{}).Count(&stats.TotalProducts)
// 在线设备(5 分钟内有心跳) // Logan.Li
fiveMinAgo := now.Add(-5 * time.Minute)
s.db.Model(&model.Activation{}).Where("is_online = ? AND last_seen_at >= ?", true, fiveMinAgo).Count(&stats.OnlineDevices)
// 本月收入 // Logan.Li
s.db.Model(&model.Order{}).
Where("status = ? AND paid_at >= ?", "paid", monthStart).
Select("COALESCE(SUM(amount), 0)").
Scan(&stats.MonthlyRevenue)
return &stats, nil
}
// GetExpiringLicenses 即将到期 License 列表 // Logan.Li
func (s *AnalyticsService) GetExpiringLicenses(days int) ([]dto.ExpiringLicenseItem, error) {
if days <= 0 {
days = 30
}
now := time.Now()
deadline := now.AddDate(0, 0, days)
var licenses []model.License
s.db.Preload("Customer").Preload("Product").
Where("status = ? AND end_date > ? AND end_date < ?", model.LicenseStatusActive, now, deadline).
Order("end_date ASC").Limit(20).Find(&licenses)
results := make([]dto.ExpiringLicenseItem, 0, len(licenses))
for _, l := range licenses {
results = append(results, dto.ExpiringLicenseItem{
ID: l.ID,
LicenseKey: l.LicenseKey,
CustomerName: l.Customer.Name,
ProductName: l.Product.Name,
EndDate: l.EndDate.Format("2006-01-02"),
DaysLeft: int(l.EndDate.Sub(now).Hours() / 24),
})
}
return results, nil
}
// GetRecentActivities 最近操作活动 // Logan.Li
func (s *AnalyticsService) GetRecentActivities(limit int) ([]dto.RecentActivityItem, error) {
if limit <= 0 {
limit = 10
}
var logs []model.AuditLog
s.db.Order("created_at DESC").Limit(limit).Find(&logs)
results := make([]dto.RecentActivityItem, 0, len(logs))
for _, l := range logs {
results = append(results, dto.RecentActivityItem{
ID: l.ID,
Username: l.Username,
Action: l.Action,
TargetType: l.TargetType,
TargetName: l.TargetName,
Detail: l.Detail,
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
return results, nil
}
// GetOnlineStats 在线统计 // Logan.Li
func (s *AnalyticsService) GetOnlineStats() (map[string]interface{}, error) {
now := time.Now()
fiveMinAgo := now.Add(-5 * time.Minute)
var onlineDevices int64
var totalActivations int64
s.db.Model(&model.Activation{}).Where("is_online = ? AND last_seen_at >= ?", true, fiveMinAgo).Count(&onlineDevices)
s.db.Model(&model.Activation{}).Where("status = ?", "active").Count(&totalActivations)
return map[string]interface{}{
"online_devices": onlineDevices,
"total_activations": totalActivations,
"offline_devices": totalActivations - onlineDevices,
}, nil
}
// GetRevenueStats 收入统计(近 6 个月) // Logan.Li
func (s *AnalyticsService) GetRevenueStats() ([]dto.RevenuePoint, error) {
now := time.Now()
results := make([]dto.RevenuePoint, 0, 6)
for i := 5; i >= 0; i-- {
monthStart := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, now.Location())
nextMonth := monthStart.AddDate(0, 1, 0)
var amount float64
s.db.Model(&model.Order{}).
Where("status = ? AND created_at >= ? AND created_at < ?", "paid", monthStart, nextMonth).
Select("COALESCE(SUM(amount), 0)").
Scan(&amount)
results = append(results, dto.RevenuePoint{
Period: monthStart.Format("2006-01"),
Amount: amount,
})
}
return results, nil
}
// GetLicenseGrowth License 增长趋势(近 6 个月) // Logan.Li
func (s *AnalyticsService) GetLicenseGrowth() ([]dto.LicenseGrowthPoint, error) {
now := time.Now()
results := make([]dto.LicenseGrowthPoint, 0, 6)
for i := 5; i >= 0; i-- {
monthEnd := time.Date(now.Year(), now.Month()-time.Month(i)+1, 0, 23, 59, 59, 0, now.Location())
var count int64
s.db.Model(&model.License{}).Where("created_at <= ?", monthEnd).Count(&count)
results = append(results, dto.LicenseGrowthPoint{
Date: monthEnd.Format("2006-01"),
Count: count,
})
}
return results, nil
}
// GetProductDistribution 产品分布 // Logan.Li
func (s *AnalyticsService) GetProductDistribution() ([]dto.ProductDistribution, error) {
var results []dto.ProductDistribution
s.db.Model(&model.License{}).
Select("products.name as product_name, count(licenses.id) as count").
Joins("LEFT JOIN products ON products.id = licenses.product_id").
Group("licenses.product_id").
Scan(&results)
return results, nil
}
@@ -0,0 +1,91 @@
package auth
import (
"backend/config"
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"time"
"gorm.io/gorm"
)
// AuthService 认证业务逻辑层 // Logan.Li
type AuthService struct {
db *gorm.DB
cfg *config.Config
}
// NewAuthService 创建 service 实例 // Logan.Li
func NewAuthService(db *gorm.DB, cfg *config.Config) *AuthService {
return &AuthService{db: db, cfg: cfg}
}
// Login 校验账号密码并签发 JWT // Logan.Li
func (s *AuthService) Login(req dto.LoginRequest) (*dto.LoginResponse, error) {
var user model.User
if err := s.db.Where("username = ?", req.Username).First(&user).Error; err != nil {
return nil, errors.New("用户名或密码错误")
}
if !user.IsActive {
return nil, errors.New("账号已被禁用")
}
if !utils.CheckPassword(req.Password, user.Password) {
return nil, errors.New("用户名或密码错误")
}
// 更新最后登录时间 // Logan.Li
now := time.Now()
s.db.Model(&user).Update("last_login", now)
// 签发 JWT // Logan.Li
token, err := utils.GenerateToken(user.ID, user.Username, user.Role, s.cfg.JWTSecret)
if err != nil {
return nil, errors.New("令牌生成失败")
}
return &dto.LoginResponse{
Token: token,
UserInfo: dto.UserInfoResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Avatar: user.Avatar,
Role: user.Role,
},
}, nil
}
// GetCurrentUser 根据 ID 获取当前用户信息 // Logan.Li
func (s *AuthService) GetCurrentUser(userID uint) (*dto.UserInfoResponse, error) {
var user model.User
if err := s.db.First(&user, userID).Error; err != nil {
return nil, errors.New("用户不存在")
}
return &dto.UserInfoResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Avatar: user.Avatar,
Role: user.Role,
}, nil
}
// ChangePassword 修改当前用户密码 // Logan.Li
func (s *AuthService) ChangePassword(userID uint, req dto.ChangePasswordRequest) error {
var user model.User
if err := s.db.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
if !utils.CheckPassword(req.OldPassword, user.Password) {
return errors.New("原密码错误")
}
user.Password = utils.HashPassword(req.NewPassword)
return s.db.Save(&user).Error
}
@@ -0,0 +1,146 @@
package customer
import (
"backend/internal/dto"
"backend/internal/model"
"errors"
"gorm.io/gorm"
)
// CustomerService 客户业务逻辑层 // Logan.Li
type CustomerService struct {
db *gorm.DB
}
// NewCustomerService 创建 service 实例 // Logan.Li
func NewCustomerService(db *gorm.DB) *CustomerService {
return &CustomerService{db: db}
}
// List 分页查询客户 // Logan.Li
func (s *CustomerService) List(page, pageSize int, keyword string) ([]dto.CustomerResponse, int64, error) {
var customers []model.Customer
var total int64
query := s.db.Model(&model.Customer{})
if keyword != "" {
query = query.Where("name LIKE ? OR email LIKE ? OR company LIKE ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}
query.Count(&total)
if err := query.Order("created_at DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&customers).Error; err != nil {
return nil, 0, err
}
results := make([]dto.CustomerResponse, 0, len(customers))
for _, c := range customers {
results = append(results, toCustomerResponse(c))
}
return results, total, nil
}
// GetAll 获取全部客户(下拉选项用) // Logan.Li
func (s *CustomerService) GetAll() ([]dto.CustomerResponse, error) {
var customers []model.Customer
if err := s.db.Where("status = ?", 1).Order("created_at DESC").Find(&customers).Error; err != nil {
return nil, err
}
results := make([]dto.CustomerResponse, 0, len(customers))
for _, c := range customers {
results = append(results, toCustomerResponse(c))
}
return results, nil
}
// GetByID 根据 ID 获取客户 // Logan.Li
func (s *CustomerService) GetByID(id uint) (*dto.CustomerResponse, error) {
var customer model.Customer
if err := s.db.First(&customer, id).Error; err != nil {
return nil, errors.New("客户不存在")
}
resp := toCustomerResponse(customer)
return &resp, nil
}
// Create 创建客户 // Logan.Li
func (s *CustomerService) Create(req dto.CustomerCreate) (*dto.CustomerResponse, error) {
status := 1
if req.Status != nil {
status = *req.Status
}
customer := model.Customer{
Name: req.Name,
Company: req.Company,
Email: req.Email,
Phone: req.Phone,
Address: req.Address,
Notes: req.Notes,
Status: status,
}
if err := s.db.Create(&customer).Error; err != nil {
return nil, errors.New("创建客户失败")
}
resp := toCustomerResponse(customer)
return &resp, nil
}
// Update 更新客户 // Logan.Li
func (s *CustomerService) Update(id uint, req dto.CustomerUpdate) (*dto.CustomerResponse, error) {
var customer model.Customer
if err := s.db.First(&customer, id).Error; err != nil {
return nil, errors.New("客户不存在")
}
updates := map[string]interface{}{
"name": req.Name,
"company": req.Company,
"email": req.Email,
"phone": req.Phone,
"address": req.Address,
"notes": req.Notes,
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := s.db.Model(&customer).Updates(updates).Error; err != nil {
return nil, errors.New("更新客户失败")
}
s.db.First(&customer, id)
resp := toCustomerResponse(customer)
return &resp, nil
}
// Delete 删除客户(软删除) // Logan.Li
func (s *CustomerService) Delete(id uint) error {
var count int64
s.db.Model(&model.License{}).Where("customer_id = ?", id).Count(&count)
if count > 0 {
return errors.New("该客户下存在 License,无法删除")
}
return s.db.Delete(&model.Customer{}, id).Error
}
// UpdateStatus 切换客户状态 // Logan.Li
func (s *CustomerService) UpdateStatus(id uint, status int) error {
return s.db.Model(&model.Customer{}).Where("id = ?", id).Update("status", status).Error
}
// toCustomerResponse 模型转响应 DTO // Logan.Li
func toCustomerResponse(c model.Customer) dto.CustomerResponse {
return dto.CustomerResponse{
ID: c.ID,
Name: c.Name,
Company: c.Company,
Email: c.Email,
Phone: c.Phone,
Address: c.Address,
Notes: c.Notes,
Status: c.Status,
CreatedAt: c.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: c.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}
@@ -0,0 +1,195 @@
package entitlement
import (
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"time"
"gorm.io/gorm"
)
// EntitlementService 客户权益业务逻辑层 // Logan.Li
type EntitlementService struct {
db *gorm.DB
}
// NewEntitlementService 创建 service 实例 // Logan.Li
func NewEntitlementService(db *gorm.DB) *EntitlementService {
return &EntitlementService{db: db}
}
// List 分页/筛选查询权益 // Logan.Li
func (s *EntitlementService) List(page, pageSize int, customerID, productID uint) ([]dto.EntitlementResponse, int64, error) {
var entitlements []model.CustomerEntitlement
var total int64
query := s.db.Model(&model.CustomerEntitlement{}).Preload("Customer").Preload("Product").Preload("Plan")
if customerID > 0 {
query = query.Where("customer_id = ?", customerID)
}
if productID > 0 {
query = query.Where("product_id = ?", productID)
}
query.Count(&total)
if err := query.Order("created_at DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&entitlements).Error; err != nil {
return nil, 0, err
}
results := make([]dto.EntitlementResponse, 0, len(entitlements))
for _, e := range entitlements {
results = append(results, s.toResponse(e))
}
return results, total, nil
}
// GetByID 获取权益详情 // Logan.Li
func (s *EntitlementService) GetByID(id uint) (*dto.EntitlementResponse, error) {
var ent model.CustomerEntitlement
if err := s.db.Preload("Customer").Preload("Product").Preload("Plan").First(&ent, id).Error; err != nil {
return nil, errors.New("权益记录不存在")
}
resp := s.toResponse(ent)
return &resp, nil
}
// Revoke 吊销权益 // Logan.Li
func (s *EntitlementService) Revoke(id uint) error {
var ent model.CustomerEntitlement
if err := s.db.First(&ent, id).Error; err != nil {
return errors.New("权益记录不存在")
}
return s.db.Transaction(func(tx *gorm.DB) error {
ent.Status = "revoked"
if err := tx.Save(&ent).Error; err != nil {
return err
}
// 同步吊销关联 License // Logan.Li
if ent.LicenseID > 0 {
tx.Model(&model.License{}).Where("id = ?", ent.LicenseID).
Updates(map[string]interface{}{"status": model.LicenseStatusRevoked, "revoke_reason": "权益被吊销"})
}
return nil
})
}
// Renew 续费订阅(按计划延长有效期) // Logan.Li
func (s *EntitlementService) Renew(id, planID uint) (*dto.EntitlementResponse, error) {
var ent model.CustomerEntitlement
if err := s.db.First(&ent, id).Error; err != nil {
return nil, errors.New("权益记录不存在")
}
var plan model.Plan
if err := s.db.First(&plan, planID).Error; err != nil {
return nil, errors.New("定价计划不存在")
}
now := time.Now()
var validUntil *time.Time
if plan.Duration == 0 {
validUntil = nil // 永久
} else {
var base time.Time
if ent.ValidUntil != nil && ent.ValidUntil.After(now) {
base = *ent.ValidUntil
} else {
base = now
}
end := base.AddDate(0, 0, plan.Duration)
validUntil = &end
}
ent.Status = "active"
ent.ValidFrom = now
ent.ValidUntil = validUntil
ent.PlanID = planID
ent.PlanType = plan.PlanType
if err := s.db.Save(&ent).Error; err != nil {
return nil, errors.New("续费失败")
}
// 同步延长关联 License 有效期 // Logan.Li
if ent.LicenseID > 0 {
var lic model.License
s.db.First(&lic, ent.LicenseID)
if plan.Duration == 0 {
lic.EndDate = model.PermanentEndDate
} else {
base := lic.EndDate
if utils.IsLicenseExpired(lic.EndDate) {
base = now
}
lic.EndDate = base.AddDate(0, 0, plan.Duration)
}
lic.Status = model.LicenseStatusActive
s.db.Save(&lic)
}
s.db.Preload("Customer").Preload("Product").Preload("Plan").First(&ent, id)
resp := s.toResponse(ent)
return &resp, nil
}
// GetByEmail 按邮箱查询客户权益(客户端接口) // Logan.Li
func (s *EntitlementService) GetByEmail(email string) ([]dto.EntitlementResponse, error) {
var customer model.Customer
if err := s.db.Where("email = ?", email).First(&customer).Error; err != nil {
return []dto.EntitlementResponse{}, nil
}
var entitlements []model.CustomerEntitlement
s.db.Where("customer_id = ?", customer.ID).
Preload("Product").Preload("Plan").
Find(&entitlements)
results := make([]dto.EntitlementResponse, 0, len(entitlements))
for _, e := range entitlements {
results = append(results, s.toResponse(e))
}
return results, nil
}
// toResponse 模型转响应 DTO // Logan.Li
func (s *EntitlementService) toResponse(e model.CustomerEntitlement) dto.EntitlementResponse {
resp := dto.EntitlementResponse{
ID: e.ID,
CustomerID: e.CustomerID,
ProductID: e.ProductID,
PlanID: e.PlanID,
PlanType: e.PlanType,
LicenseKey: e.LicenseKey,
Status: e.Status,
ValidFrom: e.ValidFrom.Format("2006-01-02 15:04:05"),
Renewable: e.Renewable,
}
if e.Customer.ID > 0 {
resp.CustomerName = e.Customer.Name
}
if e.Product.ID > 0 {
resp.ProductName = e.Product.Name
}
if e.Plan.ID > 0 {
resp.PlanName = e.Plan.Name
}
if e.ValidUntil != nil {
s := e.ValidUntil.Format("2006-01-02 15:04:05")
resp.ValidUntil = &s
resp.DaysLeft = utils.CalculateDaysLeft(*e.ValidUntil)
} else {
resp.DaysLeft = 9999 // 永久
}
// 查设备数 // Logan.Li
if e.LicenseID > 0 {
var lic model.License
s.db.First(&lic, e.LicenseID)
resp.MaxDevices = lic.MaxActivations
var count int64
s.db.Model(&model.Activation{}).Where("license_id = ?", e.LicenseID).Count(&count)
resp.CurrentDevices = int(count)
}
return resp
}
@@ -0,0 +1,158 @@
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
}
@@ -0,0 +1,343 @@
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)
}
@@ -0,0 +1,459 @@
package order
import (
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"fmt"
"time"
"gorm.io/gorm"
)
// OrderService 订单业务逻辑层 // Logan.Li
type OrderService struct {
db *gorm.DB
}
// NewOrderService 创建 service 实例 // Logan.Li
func NewOrderService(db *gorm.DB) *OrderService {
return &OrderService{db: db}
}
// List 分页查询订单 // Logan.Li
func (s *OrderService) List(page, pageSize int, status string, productID uint) ([]dto.OrderResponse, int64, error) {
var orders []model.Order
var total int64
query := s.db.Model(&model.Order{}).Preload("Customer").Preload("Product").Preload("Plan")
if status != "" {
query = query.Where("status = ?", status)
}
if productID > 0 {
query = query.Where("product_id = ?", productID)
}
query.Count(&total)
if err := query.Order("created_at DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&orders).Error; err != nil {
return nil, 0, err
}
results := make([]dto.OrderResponse, 0, len(orders))
for _, o := range orders {
results = append(results, toOrderResponse(o))
}
return results, total, nil
}
// GetByID 获取订单详情 // Logan.Li
func (s *OrderService) GetByID(id uint) (*dto.OrderResponse, error) {
var order model.Order
if err := s.db.Preload("Customer").Preload("Product").Preload("Plan").First(&order, id).Error; err != nil {
return nil, errors.New("订单不存在")
}
resp := toOrderResponse(order)
return &resp, nil
}
// CreateOrder 客户端下单(含防重复购买检查) // Logan.Li
func (s *OrderService) CreateOrder(req dto.CreateOrderRequest, clientIP, userAgent string) (*dto.CreateOrderResponse, error) {
// 1. 查找或创建客户 // Logan.Li
var customer model.Customer
if err := s.db.Where("email = ?", req.CustomerEmail).First(&customer).Error; err != nil {
name := req.CustomerName
if name == "" {
name = req.CustomerEmail
}
customer = model.Customer{Name: name, Email: req.CustomerEmail, Status: 1}
if err := s.db.Create(&customer).Error; err != nil {
return nil, errors.New("创建客户失败")
}
}
// 2. 校验产品与计划 // Logan.Li
var product model.Product
if err := s.db.First(&product, req.ProductID).Error; err != nil {
return nil, errors.New("产品不存在")
}
var plan model.Plan
if err := s.db.First(&plan, req.PlanID).Error; err != nil {
return nil, errors.New("定价计划不存在")
}
// 3. 防重复购买检查 // Logan.Li
checkResult := s.CheckDuplicatePurchase(customer.ID, req.ProductID, plan.PlanType)
if !checkResult.AllowPurchase {
return nil, fmt.Errorf("重复购买拦截: %s", checkResult.Reason)
}
// 4. 创建订单 // Logan.Li
order := model.Order{
OrderNo: utils.GenerateOrderNo(),
CustomerID: customer.ID,
ProductID: req.ProductID,
PlanID: req.PlanID,
Amount: plan.Price,
Currency: plan.Currency,
Status: "pending",
ClientIP: clientIP,
UserAgent: userAgent,
}
if err := s.db.Create(&order).Error; err != nil {
return nil, errors.New("创建订单失败")
}
// 5. 选取可用支付渠道 // Logan.Li
var gateway model.PaymentGatewayConfig
if err := s.db.Where("enabled = ?", true).Order("priority ASC").First(&gateway).Error; err != nil {
return nil, errors.New("暂无可用的支付渠道")
}
// 6. 生成收银台地址(占位) // Logan.Li
checkoutURL := fmt.Sprintf("https://checkout.%s.com/?order_no=%s", gateway.Gateway, order.OrderNo)
return &dto.CreateOrderResponse{
OrderNo: order.OrderNo,
CheckoutURL: checkoutURL,
Amount: plan.Price,
Currency: plan.Currency,
Status: "pending",
Gateway: gateway.Gateway,
ExpiresAt: time.Now().Add(30 * time.Minute).Format("2006-01-02 15:04:05"),
}, nil
}
// PaymentWebhook 支付回调(幂等 + 事务) // Logan.Li
func (s *OrderService) PaymentWebhook(req dto.PaymentWebhookRequest, clientIP string) error {
// 幂等检查 // Logan.Li
var order model.Order
if err := s.db.Where("order_no = ?", req.OrderNo).First(&order).Error; err != nil {
return errors.New("订单不存在")
}
if order.Status == "paid" || order.Status == "refunded" {
return nil // 已处理,幂等返回
}
// 事务处理 // Logan.Li
return s.db.Transaction(func(tx *gorm.DB) error {
now := time.Now()
switch req.Status {
case "paid", "completed":
// 更新订单 // Logan.Li
order.Status = "paid"
order.GatewayOrderNo = req.GatewayOrderNo
order.PaymentGateway = req.Gateway
order.PaymentMethod = req.Gateway
order.PaidAt = &now
if err := tx.Save(&order).Error; err != nil {
return err
}
// 发放 License // Logan.Li
var plan model.Plan
tx.First(&plan, order.PlanID)
var validUntil *time.Time
var endDate time.Time
if plan.Duration == 0 {
endDate = model.PermanentEndDate
} else {
endDate = now.AddDate(0, 0, plan.Duration)
validUntil = &endDate
}
lic := model.License{
LicenseKey: utils.GenerateLicenseKey(),
CustomerID: order.CustomerID,
ProductID: order.ProductID,
Status: model.LicenseStatusActive,
StartDate: now,
EndDate: endDate,
Duration: plan.Duration,
MaxActivations: plan.MaxDevices,
}
if err := tx.Create(&lic).Error; err != nil {
return err
}
order.LicenseID = lic.ID
tx.Save(&order)
// 更新权益记录(防重复购买核心) // Logan.Li
var ent model.CustomerEntitlement
entResult := tx.Where("customer_id = ? AND product_id = ?", order.CustomerID, order.ProductID).First(&ent)
renewable := plan.PlanType != "perpetual" && plan.PlanType != "lifetime"
if entResult.Error != nil {
ent = model.CustomerEntitlement{
CustomerID: order.CustomerID,
ProductID: order.ProductID,
PlanID: order.PlanID,
LicenseID: lic.ID,
LicenseKey: lic.LicenseKey,
PlanType: plan.PlanType,
Status: "active",
ValidFrom: now,
ValidUntil: validUntil,
Renewable: renewable,
}
tx.Create(&ent)
} else {
ent.PlanID = order.PlanID
ent.LicenseID = lic.ID
ent.LicenseKey = lic.LicenseKey
ent.PlanType = plan.PlanType
ent.Status = "active"
ent.ValidFrom = now
ent.ValidUntil = validUntil
ent.Renewable = renewable
tx.Save(&ent)
}
utils.LogSystemAction(tx, "payment_completed", "order", order.ID, order.OrderNo,
fmt.Sprintf("订单支付成功,已发放 License: %s", lic.LicenseKey), clientIP)
case "failed", "cancelled":
order.Status = req.Status
if err := tx.Save(&order).Error; err != nil {
return err
}
case "refunded":
order.Status = "refunded"
order.RefundAt = &now
if err := tx.Save(&order).Error; err != nil {
return err
}
// 吊销关联 License // Logan.Li
if order.LicenseID > 0 {
tx.Model(&model.License{}).Where("id = ?", order.LicenseID).
Updates(map[string]interface{}{"status": model.LicenseStatusRevoked, "revoke_reason": "订单退款"})
tx.Model(&model.CustomerEntitlement{}).Where("license_id = ?", order.LicenseID).
Update("status", "revoked")
}
}
return nil
})
}
// ManualGrant 手动发放 License(管理后台) // Logan.Li
func (s *OrderService) ManualGrant(req dto.ManualGrantRequest, operatorID uint, operatorName, clientIP string) (map[string]interface{}, error) {
// 查找或创建客户 // Logan.Li
var customer model.Customer
if err := s.db.Where("email = ?", req.CustomerEmail).First(&customer).Error; err != nil {
name := req.CustomerName
if name == "" {
name = req.CustomerEmail
}
customer = model.Customer{Name: name, Email: req.CustomerEmail, Status: 1}
if err := s.db.Create(&customer).Error; err != nil {
return nil, errors.New("创建客户失败")
}
}
var plan model.Plan
if err := s.db.First(&plan, req.PlanID).Error; err != nil {
return nil, errors.New("定价计划不存在")
}
now := time.Now()
var endDate time.Time
var validUntil *time.Time
if plan.Duration == 0 {
endDate = model.PermanentEndDate
} else {
endDate = now.AddDate(0, 0, plan.Duration)
validUntil = &endDate
}
lic := model.License{
LicenseKey: utils.GenerateLicenseKey(),
CustomerID: customer.ID,
ProductID: req.ProductID,
Status: model.LicenseStatusActive,
StartDate: now,
EndDate: endDate,
Duration: plan.Duration,
MaxActivations: plan.MaxDevices,
Notes: req.Notes,
}
if err := s.db.Create(&lic).Error; err != nil {
return nil, errors.New("创建 License 失败")
}
// 创建/更新权益记录 // Logan.Li
renewable := plan.PlanType != "perpetual" && plan.PlanType != "lifetime"
var ent model.CustomerEntitlement
if err := s.db.Where("customer_id = ? AND product_id = ?", customer.ID, req.ProductID).First(&ent).Error; err != nil {
ent = model.CustomerEntitlement{
CustomerID: customer.ID, ProductID: req.ProductID, PlanID: req.PlanID,
LicenseID: lic.ID, LicenseKey: lic.LicenseKey, PlanType: plan.PlanType,
Status: "active", ValidFrom: now, ValidUntil: validUntil, Renewable: renewable,
}
s.db.Create(&ent)
} else {
ent.PlanID = req.PlanID
ent.LicenseID = lic.ID
ent.LicenseKey = lic.LicenseKey
ent.PlanType = plan.PlanType
ent.Status = "active"
ent.ValidFrom = now
ent.ValidUntil = validUntil
ent.Renewable = renewable
s.db.Save(&ent)
}
utils.LogSystemAction(s.db, "manual_grant", "license", lic.ID, lic.LicenseKey,
fmt.Sprintf("手动发放 License: %s 给客户 %s", lic.LicenseKey, req.CustomerEmail), clientIP)
return map[string]interface{}{
"license_key": lic.LicenseKey,
"license_id": lic.ID,
"customer_id": customer.ID,
}, nil
}
// CheckDuplicatePurchase 防重复购买检测 // Logan.Li
func (s *OrderService) CheckDuplicatePurchase(customerID, productID uint, newPlanType string) dto.DuplicatePurchaseCheckResponse {
var entitlements []model.CustomerEntitlement
s.db.Where("customer_id = ? AND product_id = ?", customerID, productID).Find(&entitlements)
if len(entitlements) == 0 {
return dto.DuplicatePurchaseCheckResponse{AllowPurchase: true}
}
// 已有有效的永久/买断授权:禁止重复购买,但可升级 // Logan.Li
for _, ent := range entitlements {
if ent.Status != "active" {
continue
}
switch ent.PlanType {
case "perpetual", "lifetime":
return dto.DuplicatePurchaseCheckResponse{
AllowPurchase: false,
Reason: "您已拥有该产品的永久授权,无需重复购买",
ExistingEntitlementID: ent.ID,
ExistingLicenseKey: ent.LicenseKey,
ExistingPlanType: ent.PlanType,
CanUpgrade: true,
}
case "monthly", "yearly":
// 订阅可续费 // Logan.Li
if ent.Renewable && (newPlanType == "monthly" || newPlanType == "yearly") {
return dto.DuplicatePurchaseCheckResponse{
AllowPurchase: true,
Reason: "您有有效的订阅,将为您续费",
ExistingEntitlementID: ent.ID,
ExistingLicenseKey: ent.LicenseKey,
ExistingPlanType: ent.PlanType,
CanUpgrade: true,
}
}
}
}
// 已过期但可续费 // Logan.Li
for _, ent := range entitlements {
if ent.Status == "expired" && ent.Renewable {
return dto.DuplicatePurchaseCheckResponse{
AllowPurchase: true,
Reason: "您的订阅已过期,可以续费恢复",
ExistingEntitlementID: ent.ID,
ExistingLicenseKey: ent.LicenseKey,
ExistingPlanType: ent.PlanType,
CanUpgrade: true,
}
}
}
return dto.DuplicatePurchaseCheckResponse{AllowPurchase: true}
}
// CheckPurchaseStatus 查询购买状态 // Logan.Li
func (s *OrderService) CheckPurchaseStatus(customerEmail string, productID uint) (*dto.CheckPurchaseStatusResponse, error) {
var customer model.Customer
if err := s.db.Where("email = ?", customerEmail).First(&customer).Error; err != nil {
return &dto.CheckPurchaseStatusResponse{
Purchased: false, HasActiveLicense: false, Message: "未找到购买记录",
}, nil
}
var ent model.CustomerEntitlement
if err := s.db.Where("customer_id = ? AND product_id = ?", customer.ID, productID).
Preload("Plan").First(&ent).Error; err != nil {
return &dto.CheckPurchaseStatusResponse{
Purchased: false, HasActiveLicense: false, Message: "未购买该产品",
}, nil
}
daysLeft := 0
if ent.ValidUntil != nil {
daysLeft = utils.CalculateDaysLeft(*ent.ValidUntil)
}
// 查询 License 设备数 // Logan.Li
var lic model.License
var currentDevices int64
licenseStatus := ""
maxDevices := 0
if ent.LicenseID > 0 {
s.db.First(&lic, ent.LicenseID)
licenseStatus = lic.Status
maxDevices = lic.MaxActivations
s.db.Model(&model.Activation{}).Where("license_id = ?", lic.ID).Count(&currentDevices)
}
isActive := ent.Status == "active" && (ent.ValidUntil == nil || daysLeft > 0)
var validUntilStr *string
if ent.ValidUntil != nil {
s := ent.ValidUntil.Format("2006-01-02")
validUntilStr = &s
}
return &dto.CheckPurchaseStatusResponse{
Purchased: true,
HasActiveLicense: isActive,
LicenseKey: ent.LicenseKey,
LicenseStatus: licenseStatus,
PlanType: ent.PlanType,
ValidUntil: validUntilStr,
DaysLeft: daysLeft,
Renewable: ent.Renewable,
MaxDevices: maxDevices,
CurrentDevices: int(currentDevices),
EntitlementID: ent.ID,
}, nil
}
// toOrderResponse 模型转响应 DTO // Logan.Li
func toOrderResponse(o model.Order) dto.OrderResponse {
resp := dto.OrderResponse{
ID: o.ID,
OrderNo: o.OrderNo,
CustomerID: o.CustomerID,
ProductID: o.ProductID,
PlanID: o.PlanID,
LicenseID: o.LicenseID,
Amount: o.Amount,
Currency: o.Currency,
Status: o.Status,
PaymentGateway: o.PaymentGateway,
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
}
if o.Customer.ID > 0 {
resp.CustomerName = o.Customer.Name
}
if o.Product.ID > 0 {
resp.ProductName = o.Product.Name
}
if o.Plan.ID > 0 {
resp.PlanName = o.Plan.Name
}
if o.PaidAt != nil {
resp.PaidAt = o.PaidAt.Format("2006-01-02 15:04:05")
}
return resp
}
@@ -0,0 +1,161 @@
package plan
import (
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"gorm.io/gorm"
)
// PlanService 定价计划业务逻辑层 // Logan.Li
type PlanService struct {
db *gorm.DB
}
// NewPlanService 创建 service 实例 // Logan.Li
func NewPlanService(db *gorm.DB) *PlanService {
return &PlanService{db: db}
}
// List 分页查询定价计划 // Logan.Li
func (s *PlanService) List(page, pageSize int, productID uint) ([]dto.PlanResponse, int64, error) {
var plans []model.Plan
var total int64
query := s.db.Model(&model.Plan{}).Preload("Product")
if productID > 0 {
query = query.Where("product_id = ?", productID)
}
query.Count(&total)
if err := query.Order("sort_order ASC, created_at DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&plans).Error; err != nil {
return nil, 0, err
}
results := make([]dto.PlanResponse, 0, len(plans))
for _, p := range plans {
results = append(results, toPlanResponse(p))
}
return results, total, nil
}
// GetByProduct 查询某产品的上架计划(公开接口) // Logan.Li
func (s *PlanService) GetByProduct(productID uint) ([]dto.PlanResponse, error) {
var plans []model.Plan
if err := s.db.Where("product_id = ? AND status = ?", productID, 1).
Order("sort_order ASC").Find(&plans).Error; err != nil {
return nil, errors.New("获取定价计划失败")
}
results := make([]dto.PlanResponse, 0, len(plans))
for _, p := range plans {
results = append(results, toPlanResponse(p))
}
return results, nil
}
// Create 创建定价计划 // Logan.Li
func (s *PlanService) Create(req dto.PlanCreate) (*dto.PlanResponse, error) {
var product model.Product
if err := s.db.First(&product, req.ProductID).Error; err != nil {
return nil, errors.New("产品不存在")
}
status := 1
if req.Status != nil {
status = *req.Status
}
currency := req.Currency
if currency == "" {
currency = "USD"
}
plan := model.Plan{
ProductID: req.ProductID,
Name: req.Name,
PlanType: req.PlanType,
Price: req.Price,
Currency: currency,
Duration: req.Duration,
MaxDevices: req.MaxDevices,
Features: utils.FormatFeatures(req.Features),
SortOrder: req.SortOrder,
Status: status,
}
if err := s.db.Create(&plan).Error; err != nil {
return nil, errors.New("创建定价计划失败")
}
resp := toPlanResponse(plan)
return &resp, nil
}
// Update 更新定价计划 // Logan.Li
func (s *PlanService) Update(id uint, req dto.PlanUpdate) (*dto.PlanResponse, error) {
var plan model.Plan
if err := s.db.First(&plan, id).Error; err != nil {
return nil, errors.New("定价计划不存在")
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Price != nil {
updates["price"] = *req.Price
}
if req.Currency != "" {
updates["currency"] = req.Currency
}
if req.Duration != nil {
updates["duration"] = *req.Duration
}
if req.MaxDevices != nil {
updates["max_devices"] = *req.MaxDevices
}
if req.Features != nil {
updates["features"] = utils.FormatFeatures(req.Features)
}
if req.SortOrder != nil {
updates["sort_order"] = *req.SortOrder
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := s.db.Model(&plan).Updates(updates).Error; err != nil {
return nil, errors.New("更新定价计划失败")
}
s.db.First(&plan, id)
resp := toPlanResponse(plan)
return &resp, nil
}
// Delete 删除定价计划 // Logan.Li
func (s *PlanService) Delete(id uint) error {
return s.db.Delete(&model.Plan{}, id).Error
}
// ToggleStatus 切换上下架状态 // Logan.Li
func (s *PlanService) ToggleStatus(id uint, status int) error {
return s.db.Model(&model.Plan{}).Where("id = ?", id).Update("status", status).Error
}
// toPlanResponse 模型转响应 DTO // Logan.Li
func toPlanResponse(p model.Plan) dto.PlanResponse {
return dto.PlanResponse{
ID: p.ID,
ProductID: p.ProductID,
Name: p.Name,
PlanType: p.PlanType,
Price: p.Price,
Currency: p.Currency,
Duration: p.Duration,
MaxDevices: p.MaxDevices,
Features: utils.ParseFeatures(p.Features),
SortOrder: p.SortOrder,
Status: p.Status,
}
}
@@ -0,0 +1,171 @@
package product
import (
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"gorm.io/gorm"
)
// ProductService 产品业务逻辑层 // Logan.Li
type ProductService struct {
db *gorm.DB
}
// NewProductService 创建 service 实例 // Logan.Li
func NewProductService(db *gorm.DB) *ProductService {
return &ProductService{db: db}
}
// List 分页查询产品 // Logan.Li
func (s *ProductService) List(page, pageSize int, keyword string) ([]dto.ProductResponse, int64, error) {
var products []model.Product
var total int64
query := s.db.Model(&model.Product{})
if keyword != "" {
query = query.Where("name LIKE ? OR code LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
query.Count(&total)
if err := query.Order("created_at DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&products).Error; err != nil {
return nil, 0, err
}
results := make([]dto.ProductResponse, 0, len(products))
for _, p := range products {
results = append(results, toProductResponse(p))
}
return results, total, nil
}
// GetAll 获取全部产品(下拉选项用) // Logan.Li
func (s *ProductService) GetAll() ([]dto.ProductResponse, error) {
var products []model.Product
if err := s.db.Where("status = ?", 1).Order("created_at DESC").Find(&products).Error; err != nil {
return nil, err
}
results := make([]dto.ProductResponse, 0, len(products))
for _, p := range products {
results = append(results, toProductResponse(p))
}
return results, nil
}
// GetByID 根据 ID 获取产品 // Logan.Li
func (s *ProductService) GetByID(id uint) (*dto.ProductResponse, error) {
var product model.Product
if err := s.db.First(&product, id).Error; err != nil {
return nil, errors.New("产品不存在")
}
resp := toProductResponse(product)
return &resp, nil
}
// Create 创建产品 // Logan.Li
func (s *ProductService) Create(req dto.ProductCreate) (*dto.ProductResponse, error) {
// 检查 code 唯一性 // Logan.Li
var count int64
s.db.Model(&model.Product{}).Where("code = ?", req.Code).Count(&count)
if count > 0 {
return nil, errors.New("产品编码已存在")
}
status := 1
if req.Status != nil {
status = *req.Status
}
priceModel := req.PriceModel
if priceModel == "" {
priceModel = "subscription"
}
product := model.Product{
Name: req.Name,
Code: req.Code,
Version: req.Version,
Description: req.Description,
Features: utils.FormatFeatures(req.Features),
Price: req.Price,
PriceModel: priceModel,
Status: status,
}
if err := s.db.Create(&product).Error; err != nil {
return nil, errors.New("创建产品失败")
}
resp := toProductResponse(product)
return &resp, nil
}
// Update 更新产品 // Logan.Li
func (s *ProductService) Update(id uint, req dto.ProductUpdate) (*dto.ProductResponse, error) {
var product model.Product
if err := s.db.First(&product, id).Error; err != nil {
return nil, errors.New("产品不存在")
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Version != "" {
updates["version"] = req.Version
}
updates["description"] = req.Description
if req.Features != nil {
updates["features"] = utils.FormatFeatures(req.Features)
}
if req.Price != nil {
updates["price"] = *req.Price
}
if req.PriceModel != "" {
updates["price_model"] = req.PriceModel
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := s.db.Model(&product).Updates(updates).Error; err != nil {
return nil, errors.New("更新产品失败")
}
// 重新查询获取最新数据 // Logan.Li
s.db.First(&product, id)
resp := toProductResponse(product)
return &resp, nil
}
// Delete 删除产品(软删除) // Logan.Li
func (s *ProductService) Delete(id uint) error {
var count int64
s.db.Model(&model.License{}).Where("product_id = ?", id).Count(&count)
if count > 0 {
return errors.New("该产品下存在 License,无法删除")
}
return s.db.Delete(&model.Product{}, id).Error
}
// UpdateStatus 切换产品上下架状态 // Logan.Li
func (s *ProductService) UpdateStatus(id uint, status int) error {
return s.db.Model(&model.Product{}).Where("id = ?", id).Update("status", status).Error
}
// toProductResponse 模型转响应 DTO // Logan.Li
func toProductResponse(p model.Product) dto.ProductResponse {
return dto.ProductResponse{
ID: p.ID,
Name: p.Name,
Code: p.Code,
Version: p.Version,
Description: p.Description,
Features: utils.ParseFeatures(p.Features),
Price: p.Price,
PriceModel: p.PriceModel,
Status: p.Status,
CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: p.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}
@@ -0,0 +1,140 @@
package setting
import (
"backend/internal/dto"
"backend/internal/model"
"errors"
"gorm.io/gorm"
)
// SettingService 系统设置业务逻辑层 // Logan.Li
type SettingService struct {
db *gorm.DB
}
// NewSettingService 创建 service 实例 // Logan.Li
func NewSettingService(db *gorm.DB) *SettingService {
return &SettingService{db: db}
}
// Get 获取系统设置(单行) // Logan.Li
func (s *SettingService) Get() (*model.Setting, error) {
var setting model.Setting
if err := s.db.First(&setting).Error; err != nil {
return nil, errors.New("系统设置不存在")
}
return &setting, nil
}
// Update 更新系统设置 // Logan.Li
func (s *SettingService) Update(req dto.SettingUpdate) (*model.Setting, error) {
var setting model.Setting
if err := s.db.First(&setting).Error; err != nil {
return nil, errors.New("系统设置不存在")
}
updates := map[string]interface{}{}
if req.SystemName != nil {
updates["system_name"] = *req.SystemName
}
if req.DefaultDuration != nil {
updates["default_duration"] = *req.DefaultDuration
}
if req.DefaultMaxActivations != nil {
updates["default_max_activations"] = *req.DefaultMaxActivations
}
if req.ExpireReminderDays != nil {
updates["expire_reminder_days"] = *req.ExpireReminderDays
}
if req.APIRateLimit != nil {
updates["api_rate_limit"] = *req.APIRateLimit
}
if req.LogRetentionDays != nil {
updates["log_retention_days"] = *req.LogRetentionDays
}
if len(updates) > 0 {
if err := s.db.Model(&setting).Updates(updates).Error; err != nil {
return nil, errors.New("更新系统设置失败")
}
}
s.db.First(&setting)
return &setting, nil
}
// ============ 审计日志 ============
// ListAuditLogs 分页查询审计日志 // Logan.Li
func (s *SettingService) ListAuditLogs(page, pageSize int, action, targetType string) ([]dto.AuditLogResponse, int64, error) {
var logs []model.AuditLog
var total int64
query := s.db.Model(&model.AuditLog{})
if action != "" {
query = query.Where("action = ?", action)
}
if targetType != "" {
query = query.Where("target_type = ?", targetType)
}
query.Count(&total)
if err := query.Order("created_at DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&logs).Error; err != nil {
return nil, 0, err
}
results := make([]dto.AuditLogResponse, 0, len(logs))
for _, l := range logs {
results = append(results, dto.AuditLogResponse{
ID: l.ID,
Username: l.Username,
Action: l.Action,
TargetType: l.TargetType,
TargetID: l.TargetID,
TargetName: l.TargetName,
Detail: l.Detail,
Result: l.Result,
IP: l.IP,
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
return results, total, nil
}
// ClearAuditLogs 清空审计日志 // Logan.Li
func (s *SettingService) ClearAuditLogs() error {
return s.db.Exec("DELETE FROM audit_logs").Error
}
// ============ License 模板 ============
// ListLicenseTemplates 查询 License 模板 // Logan.Li
func (s *SettingService) ListLicenseTemplates() ([]model.LicenseTemplate, error) {
var templates []model.LicenseTemplate
s.db.Order("created_at DESC").Find(&templates)
return templates, nil
}
// SaveLicenseTemplate 创建/更新 License 模板 // Logan.Li
func (s *SettingService) SaveLicenseTemplate(tmpl model.LicenseTemplate) (*model.LicenseTemplate, error) {
if tmpl.Name == "" {
return nil, errors.New("模板名称不能为空")
}
if tmpl.ID > 0 {
if err := s.db.Save(&tmpl).Error; err != nil {
return nil, errors.New("更新模板失败")
}
} else {
if err := s.db.Create(&tmpl).Error; err != nil {
return nil, errors.New("创建模板失败")
}
}
return &tmpl, nil
}
// DeleteLicenseTemplate 删除 License 模板 // Logan.Li
func (s *SettingService) DeleteLicenseTemplate(id uint) error {
return s.db.Delete(&model.LicenseTemplate{}, id).Error
}