Files
opsx-licence/backend/internal/controller/license/license_controller.go
T
Logan.Li e0cef06234 refactor: 错误消息脱敏 + 后端 CI
- 新增 utils.InternalMsg:内部错误详情只进服务端日志,对外统一脱敏
  消息,不再向客户端泄露 GORM/SQL 实现细节;九个 controller 的
  err.Error() 出口全部切换
- 新增 .github/workflows/ci.yml:go build/vet/test(backend,go 1.25)
2026-11-01 02:03:26 +08:00

267 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package license
import (
"backend/config"
"backend/internal/dto"
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
offline *licenseservice.OfflineIssuer
db *gorm.DB
}
// NewLicenseController 创建控制器实例 // Logan.Li
func NewLicenseController(db *gorm.DB, cfg *config.Config) *LicenseController {
return &LicenseController{
svc: licenseservice.NewLicenseService(db, cfg),
offline: licenseservice.NewOfflineIssuer(db, cfg),
db: db,
}
}
// parseListQuery 从请求解析筛选参数 // Logan.Li
func parseListQuery(c *gin.Context) licenseservice.ListQuery {
q := licenseservice.ListQuery{
Keyword: c.Query("keyword"),
Status: c.Query("status"),
Channel: c.Query("channel"),
}
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 dto.LicenseCreate
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": utils.InternalMsg(err)})
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})
}
// IssueOffline 签发离线许可证序列号(最长 365 天,绑定 device_id) // Logan.Li
func (ctrl *LicenseController) IssueOffline(c *gin.Context) {
var req dto.OfflineLicenseCreate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
return
}
result, err := ctrl.offline.Issue(req)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": utils.InternalMsg(err)})
return
}
utils.LogAction(ctrl.db, c, "create_offline", "license", result.LicenseID, result.Serial, "", "",
"签发离线证 device="+result.DeviceID)
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "离线许可证已生成", "data": result})
}
// OfflinePublicKey 返回当前离线验签公钥(供运维核对桌面嵌入值) // Logan.Li
func (ctrl *LicenseController) OfflinePublicKey(c *gin.Context) {
pub := ""
if ctrl.offline != nil {
pub = ctrl.offline.PublicKeyHex()
}
if pub == "" {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": "未配置 OFFLINE_LICENSE_SEED"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"public_key_hex": pub}})
}
// 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": utils.InternalMsg(err)})
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": utils.InternalMsg(err)})
return
}
utils.LogAction(ctrl.db, c, "delete", "license", lic.ID, lic.LicenseKey, "", "", "删除 License")
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功"})
}
// BatchDelete 批量删除:非吊销/未到期自动跳过 // Logan.Li
func (ctrl *LicenseController) BatchDelete(c *gin.Context) {
var req dto.LicenseBatchDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: 请提供 ids"})
return
}
result, err := ctrl.svc.BatchDelete(req.IDs)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": utils.InternalMsg(err)})
return
}
utils.LogAction(ctrl.db, c, "batch_delete", "license", 0, "", "", "",
"批量删除 License deleted="+strconv.Itoa(result.Deleted)+" skipped="+strconv.Itoa(result.Skipped))
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "批量删除完成",
"data": dto.LicenseBatchDeleteResponse{
Deleted: result.Deleted,
Skipped: result.Skipped,
Failed: result.Failed,
SkippedIDs: result.SkippedIDs,
},
})
}
// 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": utils.InternalMsg(err)})
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": utils.InternalMsg(err)})
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": utils.InternalMsg(err)})
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": utils.InternalMsg(err)})
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": utils.InternalMsg(err)})
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": utils.InternalMsg(err)})
return
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=licenses.csv")
c.String(http.StatusOK, csv)
}