Files
opsx-licence/backend/internal/service/entitlement/entitlement_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

211 lines
5.8 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 entitlement
import (
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"strings"
"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 按邮箱查询客户权益(客户端接口;不回传完整 License Key) // 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 {
resp := s.toResponse(e)
resp.LicenseKey = maskLicenseKey(resp.LicenseKey)
results = append(results, resp)
}
return results, nil
}
// maskLicenseKey 脱敏展示,避免公开接口泄露完整密钥 // Logan.Li
func maskLicenseKey(key string) string {
key = strings.TrimSpace(key)
if len(key) <= 8 {
if key == "" {
return ""
}
return "****"
}
return key[:4] + "****" + key[len(key)-4:]
}
// 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
}