- 新增 utils.InternalMsg:内部错误详情只进服务端日志,对外统一脱敏 消息,不再向客户端泄露 GORM/SQL 实现细节;九个 controller 的 err.Error() 出口全部切换 - 新增 .github/workflows/ci.yml:go build/vet/test(backend,go 1.25)
267 lines
9.3 KiB
Go
267 lines
9.3 KiB
Go
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)
|
||
}
|