基于 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
460 lines
14 KiB
Go
460 lines
14 KiB
Go
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(¤tDevices)
|
|
}
|
|
|
|
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
|
|
}
|