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:
commit
4ae318335c
88 files changed
+10562
No files matched your search
@@ -0,0 +1,92 @@
|
||||
package analytics
|
||||
|
||||
import (
|
||||
analyticsservice "backend/internal/service/analytics"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AnalyticsController 数据分析控制器 // Logan.Li
|
||||
type AnalyticsController struct {
|
||||
svc *analyticsservice.AnalyticsService
|
||||
}
|
||||
|
||||
// NewAnalyticsController 创建控制器实例 // Logan.Li
|
||||
func NewAnalyticsController(db *gorm.DB) *AnalyticsController {
|
||||
return &AnalyticsController{svc: analyticsservice.NewAnalyticsService(db)}
|
||||
}
|
||||
|
||||
// GetDashboardStats 仪表板统计 // Logan.Li
|
||||
func (ctrl *AnalyticsController) GetDashboardStats(c *gin.Context) {
|
||||
stats, err := ctrl.svc.GetDashboardStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": stats})
|
||||
}
|
||||
|
||||
// GetExpiringLicenses 即将到期 License // Logan.Li
|
||||
func (ctrl *AnalyticsController) GetExpiringLicenses(c *gin.Context) {
|
||||
days, _ := strconv.Atoi(c.DefaultQuery("days", "30"))
|
||||
licenses, err := ctrl.svc.GetExpiringLicenses(days)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": licenses})
|
||||
}
|
||||
|
||||
// GetRecentActivities 最近活动 // Logan.Li
|
||||
func (ctrl *AnalyticsController) GetRecentActivities(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
activities, err := ctrl.svc.GetRecentActivities(limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": activities})
|
||||
}
|
||||
|
||||
// GetOnlineStats 在线统计 // Logan.Li
|
||||
func (ctrl *AnalyticsController) GetOnlineStats(c *gin.Context) {
|
||||
stats, err := ctrl.svc.GetOnlineStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": stats})
|
||||
}
|
||||
|
||||
// GetRevenueStats 收入统计 // Logan.Li
|
||||
func (ctrl *AnalyticsController) GetRevenueStats(c *gin.Context) {
|
||||
stats, err := ctrl.svc.GetRevenueStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": stats})
|
||||
}
|
||||
|
||||
// GetLicenseGrowth License 增长趋势 // Logan.Li
|
||||
func (ctrl *AnalyticsController) GetLicenseGrowth(c *gin.Context) {
|
||||
stats, err := ctrl.svc.GetLicenseGrowth()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": stats})
|
||||
}
|
||||
|
||||
// GetProductDistribution 产品分布 // Logan.Li
|
||||
func (ctrl *AnalyticsController) GetProductDistribution(c *gin.Context) {
|
||||
stats, err := ctrl.svc.GetProductDistribution()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": stats})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"backend/config"
|
||||
authservice "backend/internal/service/auth"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthController 认证控制器 // Logan.Li
|
||||
type AuthController struct {
|
||||
authService *authservice.AuthService
|
||||
}
|
||||
|
||||
// NewAuthController 创建控制器实例 // Logan.Li
|
||||
func NewAuthController(db *gorm.DB, cfg *config.Config) *AuthController {
|
||||
return &AuthController{
|
||||
authService: authservice.NewAuthService(db, cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Login 管理员登录 // Logan.Li
|
||||
func (ctrl *AuthController) Login(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := ctrl.authService.Login(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "登录成功", "data": result})
|
||||
}
|
||||
|
||||
// Me 获取当前登录用户信息 // Logan.Li
|
||||
func (ctrl *AuthController) Me(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
userInfo, err := ctrl.authService.GetCurrentUser(userID.(uint))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": userInfo})
|
||||
}
|
||||
|
||||
// ChangePassword 修改当前用户密码 // Logan.Li
|
||||
func (ctrl *AuthController) ChangePassword(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctrl.authService.ChangePassword(userID.(uint), req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "密码修改成功"})
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package customer
|
||||
|
||||
import (
|
||||
customerservice "backend/internal/service/customer"
|
||||
"backend/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CustomerController 客户控制器 // Logan.Li
|
||||
type CustomerController struct {
|
||||
svc *customerservice.CustomerService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCustomerController 创建控制器实例 // Logan.Li
|
||||
func NewCustomerController(db *gorm.DB) *CustomerController {
|
||||
return &CustomerController{svc: customerservice.NewCustomerService(db), db: db}
|
||||
}
|
||||
|
||||
// List 客户列表 // Logan.Li
|
||||
func (ctrl *CustomerController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
customers, total, err := ctrl.svc.List(page, pageSize, keyword)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": customers, "total": total, "page": page, "page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}})
|
||||
}
|
||||
|
||||
// GetAll 全部客户 // Logan.Li
|
||||
func (ctrl *CustomerController) GetAll(c *gin.Context) {
|
||||
customers, err := ctrl.svc.GetAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": customers})
|
||||
}
|
||||
|
||||
// Get 客户详情 // Logan.Li
|
||||
func (ctrl *CustomerController) Get(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
customer, err := ctrl.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 404, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": customer})
|
||||
}
|
||||
|
||||
// Create 创建客户 // Logan.Li
|
||||
func (ctrl *CustomerController) Create(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Company string `json:"company"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
Notes string `json:"notes"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
customer, err := ctrl.svc.Create(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "create", "customer", customer.ID, customer.Name, "", "", "创建客户")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "创建成功", "data": customer})
|
||||
}
|
||||
|
||||
// Update 更新客户 // Logan.Li
|
||||
func (ctrl *CustomerController) Update(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
Notes string `json:"notes"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
customer, err := ctrl.svc.Update(uint(id), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "update", "customer", customer.ID, customer.Name, "", "", "更新客户")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "更新成功", "data": customer})
|
||||
}
|
||||
|
||||
// Delete 删除客户 // Logan.Li
|
||||
func (ctrl *CustomerController) Delete(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err := ctrl.svc.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "delete", "customer", uint(id), "", "", "", "删除客户")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功"})
|
||||
}
|
||||
|
||||
// UpdateStatus 切换状态 // Logan.Li
|
||||
func (ctrl *CustomerController) UpdateStatus(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
if err := ctrl.svc.UpdateStatus(uint(id), req.Status); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "update_status", "customer", uint(id), "", "", "", "切换客户状态")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "状态更新成功"})
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package entitlement
|
||||
|
||||
import (
|
||||
entitlementservice "backend/internal/service/entitlement"
|
||||
"backend/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// EntitlementController 客户权益控制器 // Logan.Li
|
||||
type EntitlementController struct {
|
||||
svc *entitlementservice.EntitlementService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewEntitlementController 创建控制器实例 // Logan.Li
|
||||
func NewEntitlementController(db *gorm.DB) *EntitlementController {
|
||||
return &EntitlementController{svc: entitlementservice.NewEntitlementService(db), db: db}
|
||||
}
|
||||
|
||||
// List 权益列表 // Logan.Li
|
||||
func (ctrl *EntitlementController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
customerID, _ := strconv.ParseUint(c.Query("customer_id"), 10, 32)
|
||||
productID, _ := strconv.ParseUint(c.Query("product_id"), 10, 32)
|
||||
|
||||
entitlements, total, err := ctrl.svc.List(page, pageSize, uint(customerID), uint(productID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": entitlements, "total": total, "page": page, "page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}})
|
||||
}
|
||||
|
||||
// Get 权益详情 // Logan.Li
|
||||
func (ctrl *EntitlementController) Get(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
ent, err := ctrl.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 404, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": ent})
|
||||
}
|
||||
|
||||
// Revoke 吊销权益 // Logan.Li
|
||||
func (ctrl *EntitlementController) Revoke(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err := ctrl.svc.Revoke(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "revoke", "entitlement", uint(id), "", "", "", "吊销客户权益")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "权益已吊销"})
|
||||
}
|
||||
|
||||
// Renew 续费权益 // Logan.Li
|
||||
func (ctrl *EntitlementController) Renew(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
PlanID uint `json:"plan_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
ent, err := ctrl.svc.Renew(uint(id), req.PlanID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "renew", "entitlement", ent.ID, "", "", "", "续费客户权益")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "续费成功", "data": ent})
|
||||
}
|
||||
|
||||
// GetByEmail 按邮箱查询客户权益(客户端接口) // Logan.Li
|
||||
func (ctrl *EntitlementController) GetByEmail(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
if email == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "缺少 email 参数"})
|
||||
return
|
||||
}
|
||||
entitlements, err := ctrl.svc.GetByEmail(email)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": entitlements})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Verify 客户端 License 验证 // Logan.Li
|
||||
func (ctrl *LicenseController) Verify(c *gin.Context) {
|
||||
var req struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
ProductID uint `json:"product_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
resp, _ := ctrl.svc.Verify(req)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// ActivateDevice 客户端激活(机器绑定) // Logan.Li
|
||||
func (ctrl *LicenseController) ActivateDevice(c *gin.Context) {
|
||||
var req struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
data, err := ctrl.svc.Activate(req, c.ClientIP())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "激活成功", "data": data})
|
||||
}
|
||||
|
||||
// Deactivate 客户端停用 // Logan.Li
|
||||
func (ctrl *LicenseController) Deactivate(c *gin.Context) {
|
||||
var req struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := ctrl.svc.Deactivate(req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "停用成功"})
|
||||
}
|
||||
|
||||
// Heartbeat 客户端心跳 // Logan.Li
|
||||
func (ctrl *LicenseController) Heartbeat(c *gin.Context) {
|
||||
var req struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
data, err := ctrl.svc.Heartbeat(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "心跳成功", "data": data})
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
licenseservice "backend/internal/service/license"
|
||||
"backend/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// LicenseController License 控制器 // Logan.Li
|
||||
type LicenseController struct {
|
||||
svc *licenseservice.LicenseService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewLicenseController 创建控制器实例 // Logan.Li
|
||||
func NewLicenseController(db *gorm.DB) *LicenseController {
|
||||
return &LicenseController{svc: licenseservice.NewLicenseService(db), db: db}
|
||||
}
|
||||
|
||||
// parseListQuery 从请求解析筛选参数 // Logan.Li
|
||||
func parseListQuery(c *gin.Context) licenseservice.ListQuery {
|
||||
q := licenseservice.ListQuery{
|
||||
Keyword: c.Query("keyword"),
|
||||
Status: c.Query("status"),
|
||||
}
|
||||
if v, err := strconv.ParseUint(c.Query("customer_id"), 10, 32); err == nil {
|
||||
q.CustomerID = uint(v)
|
||||
}
|
||||
if v, err := strconv.ParseUint(c.Query("product_id"), 10, 32); err == nil {
|
||||
q.ProductID = uint(v)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// List License 列表 // Logan.Li
|
||||
func (ctrl *LicenseController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
licenses, total, err := ctrl.svc.List(page, pageSize, parseListQuery(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": licenses, "total": total, "page": page, "page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}})
|
||||
}
|
||||
|
||||
// Get License 详情 // Logan.Li
|
||||
func (ctrl *LicenseController) Get(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
lic, activations, err := ctrl.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 404, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"license": lic, "activations": activations}})
|
||||
}
|
||||
|
||||
// Create 创建 License // Logan.Li
|
||||
func (ctrl *LicenseController) Create(c *gin.Context) {
|
||||
var req struct {
|
||||
CustomerID uint `json:"customer_id" binding:"required"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
Duration int `json:"duration" binding:"required"`
|
||||
MaxActivations int `json:"max_activations"`
|
||||
IsTrial bool `json:"is_trial"`
|
||||
TrialDays int `json:"trial_days"`
|
||||
Notes string `json:"notes"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
licenses, err := ctrl.svc.Create(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "create", "license", licenses[0].ID, licenses[0].LicenseKey, "", "",
|
||||
"创建 "+strconv.Itoa(len(licenses))+" 个 License")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "创建成功", "data": licenses})
|
||||
}
|
||||
|
||||
// Update 更新 License // Logan.Li
|
||||
func (ctrl *LicenseController) Update(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
lic, err := ctrl.svc.Update(uint(id), updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "update", "license", lic.ID, lic.LicenseKey, "", "", "更新 License")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "更新成功", "data": lic})
|
||||
}
|
||||
|
||||
// Delete 删除 License // Logan.Li
|
||||
func (ctrl *LicenseController) Delete(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
lic, err := ctrl.svc.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "delete", "license", lic.ID, lic.LicenseKey, "", "", "删除 License")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功"})
|
||||
}
|
||||
|
||||
// Renew 续期 // Logan.Li
|
||||
func (ctrl *LicenseController) Renew(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Duration int `json:"duration" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
lic, err := ctrl.svc.Renew(uint(id), req.Duration)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "renew", "license", lic.ID, lic.LicenseKey, "", "", "续期 License")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "续期成功", "data": lic})
|
||||
}
|
||||
|
||||
// Revoke 吊销 // Logan.Li
|
||||
func (ctrl *LicenseController) Revoke(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
lic, err := ctrl.svc.Revoke(uint(id), req.Reason)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "revoke", "license", lic.ID, lic.LicenseKey, "", "", "吊销 License: "+req.Reason)
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "已吊销", "data": lic})
|
||||
}
|
||||
|
||||
// Suspend 暂停 // Logan.Li
|
||||
func (ctrl *LicenseController) Suspend(c *gin.Context) {
|
||||
ctrl.setStatus(c, "suspended")
|
||||
}
|
||||
|
||||
// Activate 激活(管理后台) // Logan.Li
|
||||
func (ctrl *LicenseController) Activate(c *gin.Context) {
|
||||
ctrl.setStatus(c, "active")
|
||||
}
|
||||
|
||||
// setStatus 通用状态切换 // Logan.Li
|
||||
func (ctrl *LicenseController) setStatus(c *gin.Context, status string) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
lic, err := ctrl.svc.SetStatus(uint(id), status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, status, "license", lic.ID, lic.LicenseKey, "", "", "License 状态变更为 "+status)
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "操作成功", "data": lic})
|
||||
}
|
||||
|
||||
// GetActivations 获取激活记录 // Logan.Li
|
||||
func (ctrl *LicenseController) GetActivations(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
activations, err := ctrl.svc.GetActivations(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": activations})
|
||||
}
|
||||
|
||||
// ForceOffline 强制下线 // Logan.Li
|
||||
func (ctrl *LicenseController) ForceOffline(c *gin.Context) {
|
||||
licenseID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
activationID, _ := strconv.ParseUint(c.Param("activationId"), 10, 32)
|
||||
if err := ctrl.svc.ForceOffline(uint(licenseID), uint(activationID)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "force_offline", "activation", uint(activationID), "", "", "", "强制下线设备")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "强制下线成功"})
|
||||
}
|
||||
|
||||
// Export 导出 CSV // Logan.Li
|
||||
func (ctrl *LicenseController) Export(c *gin.Context) {
|
||||
csv, err := ctrl.svc.ExportCSV(parseListQuery(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", "attachment; filename=licenses.csv")
|
||||
c.String(http.StatusOK, csv)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"backend/internal/dto"
|
||||
orderservice "backend/internal/service/order"
|
||||
"backend/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// OrderController 订单控制器 // Logan.Li
|
||||
type OrderController struct {
|
||||
svc *orderservice.OrderService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewOrderController 创建控制器实例 // Logan.Li
|
||||
func NewOrderController(db *gorm.DB) *OrderController {
|
||||
return &OrderController{svc: orderservice.NewOrderService(db), db: db}
|
||||
}
|
||||
|
||||
// List 订单列表(管理后台) // Logan.Li
|
||||
func (ctrl *OrderController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
status := c.Query("status")
|
||||
productID, _ := strconv.ParseUint(c.Query("product_id"), 10, 32)
|
||||
|
||||
orders, total, err := ctrl.svc.List(page, pageSize, status, uint(productID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": orders, "total": total, "page": page, "page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}})
|
||||
}
|
||||
|
||||
// GetDetail 订单详情 // Logan.Li
|
||||
func (ctrl *OrderController) GetDetail(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
order, err := ctrl.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 404, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": order})
|
||||
}
|
||||
|
||||
// CreateOrder 客户端下单(公开接口) // Logan.Li
|
||||
func (ctrl *OrderController) CreateOrder(c *gin.Context) {
|
||||
var req struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
PlanID uint `json:"plan_id" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
CancelURL string `json:"cancel_url"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
resp, err := ctrl.svc.CreateOrder(req, utils.GetClientIP(c), c.GetHeader("User-Agent"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 2001, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "订单创建成功", "data": resp})
|
||||
}
|
||||
|
||||
// PaymentWebhook 支付回调(公开接口,供支付渠道调用) // Logan.Li
|
||||
func (ctrl *OrderController) PaymentWebhook(c *gin.Context) {
|
||||
var req struct {
|
||||
EventType string `json:"event_type" binding:"required"`
|
||||
OrderNo string `json:"order_no" binding:"required"`
|
||||
GatewayOrderNo string `json:"gateway_order_no"`
|
||||
Status string `json:"status" binding:"required"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
CustomerEmail string `json:"customer_email"`
|
||||
Gateway string `json:"gateway" binding:"required"`
|
||||
Signature string `json:"signature"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "Invalid payload"})
|
||||
return
|
||||
}
|
||||
if err := ctrl.svc.PaymentWebhook(req, utils.GetClientIP(c)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "Webhook processed"})
|
||||
}
|
||||
|
||||
// ManualGrant 手动发放 License(管理后台) // Logan.Li
|
||||
func (ctrl *OrderController) ManualGrant(c *gin.Context) {
|
||||
var req struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
PlanID uint `json:"plan_id" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
userID, _ := c.Get("user_id")
|
||||
username, _ := c.Get("username")
|
||||
data, err := ctrl.svc.ManualGrant(req, userID.(uint), username.(string), utils.GetClientIP(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "License 发放成功", "data": data})
|
||||
}
|
||||
|
||||
// CheckDuplicatePurchase 防重复购买检测(公开接口) // Logan.Li
|
||||
func (ctrl *OrderController) CheckDuplicatePurchase(c *gin.Context) {
|
||||
var req struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
// 通过 CheckPurchaseStatus 间接判断:有记录且有效则不允许重复购买 // Logan.Li
|
||||
status, _ := ctrl.svc.CheckPurchaseStatus(req.CustomerEmail, req.ProductID)
|
||||
result := dto.DuplicatePurchaseCheckResponse{AllowPurchase: true}
|
||||
if status.HasActiveLicense {
|
||||
result.AllowPurchase = false
|
||||
result.Reason = status.Message
|
||||
if result.Reason == "" {
|
||||
result.Reason = "您已拥有该产品的有效授权"
|
||||
}
|
||||
result.ExistingLicenseKey = status.LicenseKey
|
||||
result.ExistingPlanType = status.PlanType
|
||||
result.CanUpgrade = true
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": result})
|
||||
}
|
||||
|
||||
// CheckPurchaseStatus 查询购买状态(公开接口) // Logan.Li
|
||||
func (ctrl *OrderController) CheckPurchaseStatus(c *gin.Context) {
|
||||
var req struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
resp, _ := ctrl.svc.CheckPurchaseStatus(req.CustomerEmail, req.ProductID)
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": resp})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
planservice "backend/internal/service/plan"
|
||||
"backend/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PlanController 定价计划控制器 // Logan.Li
|
||||
type PlanController struct {
|
||||
svc *planservice.PlanService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewPlanController 创建控制器实例 // Logan.Li
|
||||
func NewPlanController(db *gorm.DB) *PlanController {
|
||||
return &PlanController{svc: planservice.NewPlanService(db), db: db}
|
||||
}
|
||||
|
||||
// List 计划列表 // Logan.Li
|
||||
func (ctrl *PlanController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
productID, _ := strconv.ParseUint(c.Query("product_id"), 10, 32)
|
||||
|
||||
plans, total, err := ctrl.svc.List(page, pageSize, uint(productID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": plans, "total": total, "page": page, "page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}})
|
||||
}
|
||||
|
||||
// GetByProduct 查询某产品的上架计划(公开接口) // Logan.Li
|
||||
func (ctrl *PlanController) GetByProduct(c *gin.Context) {
|
||||
productID, _ := strconv.ParseUint(c.Param("product_id"), 10, 32)
|
||||
plans, err := ctrl.svc.GetByProduct(uint(productID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": plans})
|
||||
}
|
||||
|
||||
// Create 创建计划 // Logan.Li
|
||||
func (ctrl *PlanController) Create(c *gin.Context) {
|
||||
var req struct {
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
PlanType string `json:"plan_type" binding:"required"`
|
||||
Price float64 `json:"price" binding:"required"`
|
||||
Currency string `json:"currency"`
|
||||
Duration int `json:"duration"`
|
||||
MaxDevices int `json:"max_devices"`
|
||||
Features []string `json:"features"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
plan, err := ctrl.svc.Create(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "create", "plan", plan.ID, plan.Name, "", "", "创建定价计划")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "创建成功", "data": plan})
|
||||
}
|
||||
|
||||
// Update 更新计划 // Logan.Li
|
||||
func (ctrl *PlanController) Update(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Price *float64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Duration *int `json:"duration"`
|
||||
MaxDevices *int `json:"max_devices"`
|
||||
Features []string `json:"features"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
plan, err := ctrl.svc.Update(uint(id), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "update", "plan", plan.ID, plan.Name, "", "", "更新定价计划")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "更新成功", "data": plan})
|
||||
}
|
||||
|
||||
// Delete 删除计划 // Logan.Li
|
||||
func (ctrl *PlanController) Delete(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err := ctrl.svc.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "delete", "plan", uint(id), "", "", "", "删除定价计划")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功"})
|
||||
}
|
||||
|
||||
// ToggleStatus 切换上下架 // Logan.Li
|
||||
func (ctrl *PlanController) ToggleStatus(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
if err := ctrl.svc.ToggleStatus(uint(id), req.Status); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "toggle_status", "plan", uint(id), "", "", "", "切换计划状态")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "状态更新成功"})
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
productservice "backend/internal/service/product"
|
||||
"backend/utils"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ProductController 产品控制器 // Logan.Li
|
||||
type ProductController struct {
|
||||
svc *productservice.ProductService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewProductController 创建控制器实例 // Logan.Li
|
||||
func NewProductController(db *gorm.DB) *ProductController {
|
||||
return &ProductController{svc: productservice.NewProductService(db), db: db}
|
||||
}
|
||||
|
||||
// List 产品列表 // Logan.Li
|
||||
func (ctrl *ProductController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
products, total, err := ctrl.svc.List(page, pageSize, keyword)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": products, "total": total, "page": page, "page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}})
|
||||
}
|
||||
|
||||
// GetAll 全部产品(下拉用) // Logan.Li
|
||||
func (ctrl *ProductController) GetAll(c *gin.Context) {
|
||||
products, err := ctrl.svc.GetAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": products})
|
||||
}
|
||||
|
||||
// Get 产品详情 // Logan.Li
|
||||
func (ctrl *ProductController) Get(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
product, err := ctrl.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 404, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": product})
|
||||
}
|
||||
|
||||
// Create 创建产品 // Logan.Li
|
||||
func (ctrl *ProductController) Create(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Features []string `json:"features"`
|
||||
Price float64 `json:"price"`
|
||||
PriceModel string `json:"price_model"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
product, err := ctrl.svc.Create(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "create", "product", product.ID, product.Name, "", "", "创建产品")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "创建成功", "data": product})
|
||||
}
|
||||
|
||||
// Update 更新产品 // Logan.Li
|
||||
func (ctrl *ProductController) Update(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Features []string `json:"features"`
|
||||
Price *float64 `json:"price"`
|
||||
PriceModel string `json:"price_model"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
product, err := ctrl.svc.Update(uint(id), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "update", "product", product.ID, product.Name, "", "", "更新产品")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "更新成功", "data": product})
|
||||
}
|
||||
|
||||
// Delete 删除产品 // Logan.Li
|
||||
func (ctrl *ProductController) Delete(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err := ctrl.svc.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "delete", "product", uint(id), "", "", "", "删除产品")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功"})
|
||||
}
|
||||
|
||||
// UpdateStatus 切换上下架 // Logan.Li
|
||||
func (ctrl *ProductController) UpdateStatus(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var req struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
if err := ctrl.svc.UpdateStatus(uint(id), req.Status); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "update_status", "product", uint(id), "", "", "", fmt.Sprintf("切换状态为 %d", req.Status))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "状态更新成功"})
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"backend/internal/model"
|
||||
settingservice "backend/internal/service/setting"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SettingController 系统设置控制器 // Logan.Li
|
||||
type SettingController struct {
|
||||
svc *settingservice.SettingService
|
||||
}
|
||||
|
||||
// NewSettingController 创建控制器实例 // Logan.Li
|
||||
func NewSettingController(db *gorm.DB) *SettingController {
|
||||
return &SettingController{svc: settingservice.NewSettingService(db)}
|
||||
}
|
||||
|
||||
// Get 获取系统设置 // Logan.Li
|
||||
func (ctrl *SettingController) Get(c *gin.Context) {
|
||||
setting, err := ctrl.svc.Get()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 404, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": setting})
|
||||
}
|
||||
|
||||
// Update 更新系统设置 // Logan.Li
|
||||
func (ctrl *SettingController) Update(c *gin.Context) {
|
||||
var req struct {
|
||||
SystemName *string `json:"system_name"`
|
||||
DefaultDuration *int `json:"default_duration"`
|
||||
DefaultMaxActivations *int `json:"default_max_activations"`
|
||||
ExpireReminderDays *int `json:"expire_reminder_days"`
|
||||
APIRateLimit *int `json:"api_rate_limit"`
|
||||
LogRetentionDays *int `json:"log_retention_days"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
setting, err := ctrl.svc.Update(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "更新成功", "data": setting})
|
||||
}
|
||||
|
||||
// ListAuditLogs 审计日志列表 // Logan.Li
|
||||
func (ctrl *SettingController) ListAuditLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
action := c.Query("action")
|
||||
targetType := c.Query("target_type")
|
||||
|
||||
logs, total, err := ctrl.svc.ListAuditLogs(page, pageSize, action, targetType)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": logs, "total": total, "page": page, "page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}})
|
||||
}
|
||||
|
||||
// ClearAuditLogs 清空审计日志 // Logan.Li
|
||||
func (ctrl *SettingController) ClearAuditLogs(c *gin.Context) {
|
||||
if err := ctrl.svc.ClearAuditLogs(); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "日志已清空"})
|
||||
}
|
||||
|
||||
// ListLicenseTemplates 查询 License 模板 // Logan.Li
|
||||
func (ctrl *SettingController) ListLicenseTemplates(c *gin.Context) {
|
||||
templates, err := ctrl.svc.ListLicenseTemplates()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": templates})
|
||||
}
|
||||
|
||||
// SaveLicenseTemplate 创建/更新 License 模板 // Logan.Li
|
||||
func (ctrl *SettingController) SaveLicenseTemplate(c *gin.Context) {
|
||||
var tmpl model.LicenseTemplate
|
||||
if err := c.ShouldBindJSON(&tmpl); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
result, err := ctrl.svc.SaveLicenseTemplate(tmpl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "保存成功", "data": result})
|
||||
}
|
||||
|
||||
// DeleteLicenseTemplate 删除 License 模板 // Logan.Li
|
||||
func (ctrl *SettingController) DeleteLicenseTemplate(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err := ctrl.svc.DeleteLicenseTemplate(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功"})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package dto
|
||||
|
||||
// DashboardStats 仪表板统计响应 // Logan.Li
|
||||
type DashboardStats struct {
|
||||
TotalLicenses int64 `json:"total_licenses"`
|
||||
ActiveLicenses int64 `json:"active_licenses"`
|
||||
NewThisMonth int64 `json:"new_this_month"`
|
||||
ExpiringSoon int64 `json:"expiring_soon"`
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
TotalProducts int64 `json:"total_products"`
|
||||
OnlineDevices int64 `json:"online_devices"`
|
||||
MonthlyRevenue float64 `json:"monthly_revenue"`
|
||||
}
|
||||
|
||||
// ExpiringLicenseItem 即将到期 License // Logan.Li
|
||||
type ExpiringLicenseItem struct {
|
||||
ID uint `json:"id"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductName string `json:"product_name"`
|
||||
EndDate string `json:"end_date"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
}
|
||||
|
||||
// RecentActivityItem 最近活动 // Logan.Li
|
||||
type RecentActivityItem struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Action string `json:"action"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetName string `json:"target_name"`
|
||||
Detail string `json:"detail"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// LicenseGrowthPoint License 增长趋势点 // Logan.Li
|
||||
type LicenseGrowthPoint struct {
|
||||
Date string `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// RevenuePoint 收入趋势点 // Logan.Li
|
||||
type RevenuePoint struct {
|
||||
Period string `json:"period"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
// ProductDistribution 产品分布 // Logan.Li
|
||||
type ProductDistribution struct {
|
||||
ProductName string `json:"product_name"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// SettingUpdate 更新系统设置请求 // Logan.Li
|
||||
type SettingUpdate struct {
|
||||
SystemName *string `json:"system_name"`
|
||||
DefaultDuration *int `json:"default_duration"`
|
||||
DefaultMaxActivations *int `json:"default_max_activations"`
|
||||
ExpireReminderDays *int `json:"expire_reminder_days"`
|
||||
APIRateLimit *int `json:"api_rate_limit"`
|
||||
LogRetentionDays *int `json:"log_retention_days"`
|
||||
}
|
||||
|
||||
// AuditLogResponse 操作日志响应 // Logan.Li
|
||||
type AuditLogResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Action string `json:"action"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetID uint `json:"target_id"`
|
||||
TargetName string `json:"target_name"`
|
||||
Detail string `json:"detail"`
|
||||
Result string `json:"result"`
|
||||
IP string `json:"ip"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package dto
|
||||
|
||||
// LoginRequest 登录请求 // Logan.Li
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应 // Logan.Li
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
UserInfo UserInfoResponse `json:"user_info"`
|
||||
}
|
||||
|
||||
// UserInfoResponse 用户信息响应 // Logan.Li
|
||||
type UserInfoResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求 // Logan.Li
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package dto
|
||||
|
||||
// PlanCreate 创建定价计划请求 // Logan.Li
|
||||
type PlanCreate struct {
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
PlanType string `json:"plan_type" binding:"required"` // perpetual / monthly / yearly / lifetime
|
||||
Price float64 `json:"price" binding:"required"`
|
||||
Currency string `json:"currency"`
|
||||
Duration int `json:"duration"`
|
||||
MaxDevices int `json:"max_devices"`
|
||||
Features []string `json:"features"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// PlanUpdate 更新定价计划请求 // Logan.Li
|
||||
type PlanUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Price *float64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Duration *int `json:"duration"`
|
||||
MaxDevices *int `json:"max_devices"`
|
||||
Features []string `json:"features"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// PlanResponse 定价计划响应 // Logan.Li
|
||||
type PlanResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ProductID uint `json:"product_id"`
|
||||
Name string `json:"name"`
|
||||
PlanType string `json:"plan_type"`
|
||||
Price float64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Duration int `json:"duration"`
|
||||
MaxDevices int `json:"max_devices"`
|
||||
Features []string `json:"features"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// OrderResponse 订单响应 // Logan.Li
|
||||
type OrderResponse struct {
|
||||
ID uint `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
CustomerID uint `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
PlanID uint `json:"plan_id"`
|
||||
PlanName string `json:"plan_name"`
|
||||
LicenseID uint `json:"license_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
PaymentGateway string `json:"payment_gateway"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PaidAt string `json:"paid_at,omitempty"`
|
||||
}
|
||||
|
||||
// CreateOrderRequest 创建订单请求(客户端购买入口) // Logan.Li
|
||||
type CreateOrderRequest struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
PlanID uint `json:"plan_id" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
CancelURL string `json:"cancel_url"`
|
||||
}
|
||||
|
||||
// CreateOrderResponse 创建订单响应 // Logan.Li
|
||||
type CreateOrderResponse struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
CheckoutURL string `json:"checkout_url"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
Gateway string `json:"gateway"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
|
||||
// PaymentWebhookRequest 支付回调请求 // Logan.Li
|
||||
type PaymentWebhookRequest struct {
|
||||
EventType string `json:"event_type" binding:"required"`
|
||||
OrderNo string `json:"order_no" binding:"required"`
|
||||
GatewayOrderNo string `json:"gateway_order_no"`
|
||||
Status string `json:"status" binding:"required"` // paid / failed / refunded
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
CustomerEmail string `json:"customer_email"`
|
||||
Gateway string `json:"gateway" binding:"required"`
|
||||
Signature string `json:"signature"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
// ManualGrantRequest 手动发放 License 请求(管理后台) // Logan.Li
|
||||
type ManualGrantRequest struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
PlanID uint `json:"plan_id" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// EntitlementResponse 客户权益响应 // Logan.Li
|
||||
type EntitlementResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CustomerID uint `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
PlanID uint `json:"plan_id"`
|
||||
PlanName string `json:"plan_name"`
|
||||
PlanType string `json:"plan_type"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
Status string `json:"status"`
|
||||
ValidFrom string `json:"valid_from"`
|
||||
ValidUntil *string `json:"valid_until,omitempty"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
Renewable bool `json:"renewable"`
|
||||
MaxDevices int `json:"max_devices"`
|
||||
CurrentDevices int `json:"current_devices"`
|
||||
}
|
||||
|
||||
// DuplicatePurchaseCheckRequest 防重复购买检测请求 // Logan.Li
|
||||
type DuplicatePurchaseCheckRequest struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
}
|
||||
|
||||
// DuplicatePurchaseCheckResponse 防重复购买检测响应 // Logan.Li
|
||||
type DuplicatePurchaseCheckResponse struct {
|
||||
AllowPurchase bool `json:"allow_purchase"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
ExistingEntitlementID uint `json:"existing_entitlement_id,omitempty"`
|
||||
ExistingLicenseKey string `json:"existing_license_key,omitempty"`
|
||||
ExistingPlanType string `json:"existing_plan_type,omitempty"`
|
||||
CanUpgrade bool `json:"can_upgrade"`
|
||||
UpgradeOptions []PlanResponse `json:"upgrade_options,omitempty"`
|
||||
}
|
||||
|
||||
// CheckPurchaseStatusRequest 购买状态查询请求 // Logan.Li
|
||||
type CheckPurchaseStatusRequest struct {
|
||||
CustomerEmail string `json:"customer_email" binding:"required,email"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
}
|
||||
|
||||
// CheckPurchaseStatusResponse 购买状态查询响应 // Logan.Li
|
||||
type CheckPurchaseStatusResponse struct {
|
||||
Purchased bool `json:"purchased"`
|
||||
HasActiveLicense bool `json:"has_active_license"`
|
||||
LicenseKey string `json:"license_key,omitempty"`
|
||||
LicenseStatus string `json:"license_status,omitempty"`
|
||||
PlanType string `json:"plan_type,omitempty"`
|
||||
ValidUntil *string `json:"valid_until,omitempty"`
|
||||
DaysLeft int `json:"days_left,omitempty"`
|
||||
Renewable bool `json:"renewable"`
|
||||
MaxDevices int `json:"max_devices,omitempty"`
|
||||
CurrentDevices int `json:"current_devices,omitempty"`
|
||||
EntitlementID uint `json:"entitlement_id,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dto
|
||||
|
||||
// Response 统一响应结构 // Logan.Li
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// PaginatedResponse 分页响应结构 // Logan.Li
|
||||
type PaginatedResponse struct {
|
||||
Count int `json:"count"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
CurrentPage int `json:"current_page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Next *string `json:"next"`
|
||||
Previous *string `json:"previous"`
|
||||
Results interface{} `json:"results"`
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package dto
|
||||
|
||||
// LicenseCreate 创建 License 请求 // Logan.Li
|
||||
type LicenseCreate struct {
|
||||
CustomerID uint `json:"customer_id" binding:"required"`
|
||||
ProductID uint `json:"product_id" binding:"required"`
|
||||
Duration int `json:"duration" binding:"required"` // 30/90/180/365/730/1095/1825/0(永久)
|
||||
MaxActivations int `json:"max_activations"`
|
||||
IsTrial bool `json:"is_trial"`
|
||||
TrialDays int `json:"trial_days"`
|
||||
Notes string `json:"notes"`
|
||||
Count int `json:"count"` // 批量生成数量
|
||||
}
|
||||
|
||||
// LicenseRenew 续期请求 // Logan.Li
|
||||
type LicenseRenew struct {
|
||||
Duration int `json:"duration" binding:"required"`
|
||||
}
|
||||
|
||||
// LicenseRevoke 吊销请求 // Logan.Li
|
||||
type LicenseRevoke struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// LicenseResponse License 响应 // Logan.Li
|
||||
type LicenseResponse struct {
|
||||
ID uint `json:"id"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
CustomerID uint `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Status string `json:"status"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Duration int `json:"duration"`
|
||||
MaxActivations int `json:"max_activations"`
|
||||
CurrentActivations int `json:"current_activations"`
|
||||
IsTrial bool `json:"is_trial"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ============ 客户端授权契约(无需认证) ============
|
||||
|
||||
// LicenseVerifyRequest 验证请求(客户端调用) // Logan.Li
|
||||
type LicenseVerifyRequest struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
ProductID uint `json:"product_id"`
|
||||
}
|
||||
|
||||
// LicenseVerifyResponse 验证响应 // Logan.Li
|
||||
type LicenseVerifyResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
LicenseKey string `json:"license_key,omitempty"`
|
||||
ProductID uint `json:"product_id,omitempty"`
|
||||
ProductName string `json:"product_name,omitempty"`
|
||||
ProductCode string `json:"product_code,omitempty"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
DaysLeft int `json:"days_left,omitempty"`
|
||||
MaxDevices int `json:"max_devices,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// LicenseActivateRequest 激活请求(客户端调用) // Logan.Li
|
||||
type LicenseActivateRequest struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
}
|
||||
|
||||
// LicenseDeactivateRequest 停用请求(客户端调用) // Logan.Li
|
||||
type LicenseDeactivateRequest struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
}
|
||||
|
||||
// LicenseHeartbeatRequest 心跳请求(客户端调用) // Logan.Li
|
||||
type LicenseHeartbeatRequest struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"`
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
}
|
||||
|
||||
// ActivationResponse 激活记录响应 // Logan.Li
|
||||
type ActivationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
LicenseID uint `json:"license_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
IP string `json:"ip"`
|
||||
Status string `json:"status"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
ActivatedAt string `json:"activated_at"`
|
||||
LastSeenAt string `json:"last_seen_at"`
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package dto
|
||||
|
||||
// ProductCreate 创建产品请求 // Logan.Li
|
||||
type ProductCreate struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Features []string `json:"features"`
|
||||
Price float64 `json:"price"`
|
||||
PriceModel string `json:"price_model"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// ProductUpdate 更新产品请求 // Logan.Li
|
||||
type ProductUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Features []string `json:"features"`
|
||||
Price *float64 `json:"price"`
|
||||
PriceModel string `json:"price_model"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// ProductResponse 产品响应 // Logan.Li
|
||||
type ProductResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Features []string `json:"features"`
|
||||
Price float64 `json:"price"`
|
||||
PriceModel string `json:"price_model"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CustomerCreate 创建客户请求 // Logan.Li
|
||||
type CustomerCreate struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Company string `json:"company"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
Notes string `json:"notes"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// CustomerUpdate 更新客户请求 // Logan.Li
|
||||
type CustomerUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
Notes string `json:"notes"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// CustomerResponse 客户响应 // Logan.Li
|
||||
type CustomerResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
Notes string `json:"notes"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// AuditLog 操作日志模型 // Logan.Li
|
||||
type AuditLog struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username" gorm:"size:100"`
|
||||
Action string `json:"action" gorm:"size:50;not null"` // create / update / delete / activate / renew / revoke ...
|
||||
TargetType string `json:"target_type" gorm:"size:50"` // license / customer / product / order ...
|
||||
TargetID uint `json:"target_id"`
|
||||
TargetName string `json:"target_name" gorm:"size:200"`
|
||||
OldData string `json:"old_data" gorm:"type:text"` // JSON
|
||||
NewData string `json:"new_data" gorm:"type:text"` // JSON
|
||||
Detail string `json:"detail" gorm:"type:text"`
|
||||
Result string `json:"result" gorm:"size:20"` // success / fail
|
||||
ErrorMessage string `json:"error_message" gorm:"type:text"`
|
||||
IP string `json:"ip" gorm:"size:50"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (AuditLog) TableName() string {
|
||||
return "audit_logs"
|
||||
}
|
||||
|
||||
// LicenseTemplate License 模板(用于快速创建) // Logan.Li
|
||||
type LicenseTemplate struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Duration int `json:"duration" gorm:"default:365"` // 天数,0 表示永久
|
||||
MaxActivations int `json:"max_activations" gorm:"default:1"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (LicenseTemplate) TableName() string {
|
||||
return "license_templates"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Customer 客户模型 // Logan.Li
|
||||
type Customer struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Company string `json:"company" gorm:"size:200"`
|
||||
Email string `json:"email" gorm:"size:100;index"`
|
||||
Phone string `json:"phone" gorm:"size:20"`
|
||||
Address string `json:"address" gorm:"size:500"`
|
||||
Notes string `json:"notes" gorm:"type:text"`
|
||||
Status int `json:"status" gorm:"default:1"` // 1: 启用, 0: 禁用
|
||||
Licenses []License `json:"licenses,omitempty" gorm:"foreignKey:CustomerID"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (Customer) TableName() string {
|
||||
return "customers"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// CustomerEntitlement 客户权益记录(防重复购买的核心表) // Logan.Li
|
||||
type CustomerEntitlement struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CustomerID uint `json:"customer_id"`
|
||||
Customer Customer `json:"customer,omitempty" gorm:"foreignKey:CustomerID"`
|
||||
ProductID uint `json:"product_id"`
|
||||
Product Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
|
||||
PlanID uint `json:"plan_id"`
|
||||
Plan Plan `json:"plan,omitempty" gorm:"foreignKey:PlanID"`
|
||||
LicenseID uint `json:"license_id"`
|
||||
LicenseKey string `json:"license_key" gorm:"size:100"`
|
||||
PlanType string `json:"plan_type" gorm:"size:50"` // perpetual / monthly / yearly / lifetime
|
||||
Status string `json:"status" gorm:"size:20;default:'active'"` // active / expired / revoked
|
||||
ValidFrom time.Time `json:"valid_from"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty"` // 买断/永久为 null
|
||||
Renewable bool `json:"renewable" gorm:"default:true"` // 订阅到期后是否可续费
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (CustomerEntitlement) TableName() string {
|
||||
return "customer_entitlements"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// License 期限常量(天数,0 表示永久) // Logan.Li
|
||||
const (
|
||||
Duration1Month = 30
|
||||
Duration3Months = 90
|
||||
Duration6Months = 180
|
||||
Duration1Year = 365
|
||||
Duration2Years = 730
|
||||
Duration3Years = 1095
|
||||
Duration5Years = 1825
|
||||
DurationForever = 0
|
||||
)
|
||||
|
||||
// License 状态常量 // Logan.Li
|
||||
const (
|
||||
LicenseStatusInactive = "inactive"
|
||||
LicenseStatusActive = "active"
|
||||
LicenseStatusExpired = "expired"
|
||||
LicenseStatusRevoked = "revoked"
|
||||
LicenseStatusSuspended = "suspended"
|
||||
)
|
||||
|
||||
// PermanentEndDate 永久授权的到期时间(2099-12-31) // Logan.Li
|
||||
var PermanentEndDate = time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||
|
||||
// License 许可证模型 // Logan.Li
|
||||
type License struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
LicenseKey string `json:"license_key" gorm:"size:100;uniqueIndex;not null"`
|
||||
CustomerID uint `json:"customer_id"`
|
||||
Customer Customer `json:"customer,omitempty" gorm:"foreignKey:CustomerID"`
|
||||
ProductID uint `json:"product_id"`
|
||||
Product Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
|
||||
Status string `json:"status" gorm:"size:20;default:'inactive'"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
Duration int `json:"duration" gorm:"default:365"` // 天数,0 表示永久
|
||||
MaxActivations int `json:"max_activations" gorm:"default:1"`
|
||||
CurrentActivations int `json:"current_activations" gorm:"default:0"`
|
||||
IsTrial bool `json:"is_trial" gorm:"default:false"`
|
||||
TrialDays int `json:"trial_days" gorm:"default:0"`
|
||||
RevokeReason string `json:"revoke_reason" gorm:"type:text"`
|
||||
Notes string `json:"notes" gorm:"type:text"`
|
||||
Activations []Activation `json:"activations,omitempty" gorm:"foreignKey:LicenseID"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (License) TableName() string {
|
||||
return "licenses"
|
||||
}
|
||||
|
||||
// Activation 激活记录模型(机器绑定) // Logan.Li
|
||||
type Activation struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
LicenseID uint `json:"license_id"`
|
||||
License License `json:"license,omitempty" gorm:"foreignKey:LicenseID"`
|
||||
DeviceID string `json:"device_id" gorm:"size:100"`
|
||||
DeviceName string `json:"device_name" gorm:"size:200"`
|
||||
DeviceInfo string `json:"device_info" gorm:"type:text"` // JSON 格式存储设备信息
|
||||
IP string `json:"ip" gorm:"size:50"`
|
||||
Status string `json:"status" gorm:"size:20;default:'active'"` // active / inactive
|
||||
IsOnline bool `json:"is_online" gorm:"default:false"`
|
||||
ActivatedAt time.Time `json:"activated_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
LastSeenAt time.Time `json:"last_seen_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (Activation) TableName() string {
|
||||
return "activations"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Order 订单模型 // Logan.Li
|
||||
type Order struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
OrderNo string `json:"order_no" gorm:"size:100;uniqueIndex;not null"` // 系统唯一订单号(幂等键)
|
||||
CustomerID uint `json:"customer_id"`
|
||||
Customer Customer `json:"customer,omitempty" gorm:"foreignKey:CustomerID"`
|
||||
ProductID uint `json:"product_id"`
|
||||
Product Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
|
||||
PlanID uint `json:"plan_id"`
|
||||
Plan Plan `json:"plan,omitempty" gorm:"foreignKey:PlanID"`
|
||||
LicenseID uint `json:"license_id"` // 关联的 License ID
|
||||
Amount float64 `json:"amount" gorm:"type:decimal(10,2);not null"`
|
||||
Currency string `json:"currency" gorm:"size:3;default:'USD'"`
|
||||
Status string `json:"status" gorm:"size:20;default:'pending'"` // pending / paid / failed / refunded / cancelled
|
||||
PaymentMethod string `json:"payment_method" gorm:"size:50"` // paddle / stripe / creem / waffo / alipay / wechat
|
||||
PaymentGateway string `json:"payment_gateway" gorm:"size:50"`
|
||||
GatewayOrderNo string `json:"gateway_order_no" gorm:"size:200"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||
RefundAt *time.Time `json:"refund_at,omitempty"`
|
||||
ClientIP string `json:"client_ip" gorm:"size:50"`
|
||||
UserAgent string `json:"user_agent" gorm:"size:500"`
|
||||
Notes string `json:"notes" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (Order) TableName() string {
|
||||
return "orders"
|
||||
}
|
||||
|
||||
// PaymentGatewayConfig 支付网关配置 // Logan.Li
|
||||
type PaymentGatewayConfig struct {
|
||||
Gateway string `json:"gateway" gorm:"size:50;primaryKey"` // paddle / stripe / creem / waffo
|
||||
Enabled bool `json:"enabled" gorm:"default:false"`
|
||||
PublicKey string `json:"public_key" gorm:"type:text"`
|
||||
SecretKey string `json:"secret_key" gorm:"type:text"` // 加密存储
|
||||
WebhookSecret string `json:"webhook_secret" gorm:"type:text"` // Webhook 验签密钥
|
||||
SandboxMode bool `json:"sandbox_mode" gorm:"default:true"`
|
||||
Currency string `json:"currency" gorm:"size:3;default:'USD'"`
|
||||
Region string `json:"region" gorm:"size:50"`
|
||||
Priority int `json:"priority" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (PaymentGatewayConfig) TableName() string {
|
||||
return "payment_gateway_configs"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Plan 定价计划模型(支持买断/订阅多档位) // Logan.Li
|
||||
type Plan struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ProductID uint `json:"product_id"`
|
||||
Product Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
|
||||
Name string `json:"name" gorm:"size:100;not null"` // 如:个人版、专业版、企业版
|
||||
PlanType string `json:"plan_type" gorm:"size:50;not null"` // perpetual / monthly / yearly / lifetime
|
||||
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
|
||||
Currency string `json:"currency" gorm:"size:3;default:'USD'"` // USD / CNY
|
||||
Duration int `json:"duration" gorm:"default:0"` // 天数:0=永久/买断,30=月,365=年
|
||||
MaxDevices int `json:"max_devices" gorm:"default:1"` // 允许激活设备数
|
||||
Features string `json:"features" gorm:"type:text"` // JSON 功能清单
|
||||
SortOrder int `json:"sort_order" gorm:"default:0"`
|
||||
Status int `json:"status" gorm:"default:1"` // 1: 上架, 0: 下架
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (Plan) TableName() string {
|
||||
return "plans"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Product 产品模型 // Logan.Li
|
||||
type Product struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Code string `json:"code" gorm:"size:50;uniqueIndex"`
|
||||
Version string `json:"version" gorm:"size:50"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Features string `json:"features" gorm:"type:text"` // JSON 格式存储功能列表
|
||||
Price float64 `json:"price" gorm:"type:decimal(10,2)"`
|
||||
PriceModel string `json:"price_model" gorm:"size:50;default:'subscription'"` // subscription / perpetual / trial
|
||||
Status int `json:"status" gorm:"default:1"` // 1: 上架, 0: 下架
|
||||
Licenses []License `json:"licenses,omitempty" gorm:"foreignKey:ProductID"`
|
||||
Plans []Plan `json:"plans,omitempty" gorm:"foreignKey:ProductID"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (Product) TableName() string {
|
||||
return "products"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Setting 系统设置模型(单行配置,后续阶段启用) // Logan.Li
|
||||
type Setting struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
SystemName string `json:"system_name" gorm:"size:100;default:'opsx-licence'"`
|
||||
SystemLogo string `json:"system_logo" gorm:"size:500"`
|
||||
DefaultDuration int `json:"default_duration" gorm:"default:365"` // 默认 License 有效天数
|
||||
DefaultMaxActivations int `json:"default_max_activations" gorm:"default:1"` // 默认最大激活数
|
||||
ExpireReminderDays int `json:"expire_reminder_days" gorm:"default:7"` // 到期提醒天数
|
||||
SMTPServer string `json:"smtp_server" gorm:"size:200"`
|
||||
SMTPPort int `json:"smtp_port"`
|
||||
SMTPUsername string `json:"smtp_username" gorm:"size:100"`
|
||||
SMTPPassword string `json:"smtp_password" gorm:"size:100"`
|
||||
SenderEmail string `json:"sender_email" gorm:"size:100"`
|
||||
SenderName string `json:"sender_name" gorm:"size:100"`
|
||||
APIRateLimit int `json:"api_rate_limit" gorm:"default:1000"`
|
||||
LogRetentionDays int `json:"log_retention_days" gorm:"default:365"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (Setting) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 管理员用户模型(单 Admin 表,无 RBAC 多角色) // Logan.Li
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Username string `json:"username" gorm:"size:100;uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"size:255;not null"` // bcrypt 哈希,JSON 响应时隐藏
|
||||
Email string `json:"email" gorm:"size:100"`
|
||||
Avatar string `json:"avatar" gorm:"size:500"`
|
||||
Role string `json:"role" gorm:"size:20;default:'admin'"` // 固定 admin,预留扩展
|
||||
IsActive bool `json:"is_active" gorm:"default:true"`
|
||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
@@ -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(¤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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user