23dff69c62
- 新增 license_codes 码池表 + model.LicenseCode;licenses 加 tier 档位列 - LicenseService.Redeem:单事务 FOR UPDATE 校验码未用 → 时长叠加(可叠加,0=永久) → 写 type/tier/max_devices → 绑设备(超限整笔回滚) → 标记已用 → 即时失效 phase 缓存 路由仍 POST /license/activate,客户端零破坏 - util.GenerateRedeemCode/NormalizeCode:JIUKU-XXXX-XXXX 短码(crypto/rand) - cmd/gencode:平台批量生成兑换码并落库;删除 cmd/issue、cmd/genkey - 退役 ed25519 + HMAC:删 util/license_key、GenerateKey、License 全部 config 字段 及生产启动私钥校验;trial 改直接建行(无需私钥、去 Fatal) - tier 档位钩子默认 standard,分档消费模式后续设计 - 测试:Redeem 全场景(叠加/过期重置/永久/一码一次/无效/设备上限回滚) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
2.9 KiB
Go
99 lines
2.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/wangjia/jiu/backend/internal/middleware"
|
|
"github.com/wangjia/jiu/backend/internal/service"
|
|
"github.com/wangjia/jiu/backend/internal/util"
|
|
)
|
|
|
|
type LicenseHandler struct {
|
|
svc *service.LicenseService
|
|
}
|
|
|
|
func NewLicenseHandler(svc *service.LicenseService) *LicenseHandler {
|
|
return &LicenseHandler{svc: svc}
|
|
}
|
|
|
|
// Activate POST /api/v1/license/activate — 兑换激活码(时长券),把时长叠加到门店授权。
|
|
// 路由名保留 activate 以兼容客户端;内部走 Redeem 逻辑。
|
|
func (h *LicenseHandler) Activate(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
var req struct {
|
|
LicenseKey string `json:"license_key" binding:"required"` // 承载激活码(短码)
|
|
DeviceID string `json:"device_id" binding:"required"`
|
|
DeviceName string `json:"device_name"`
|
|
Platform string `json:"platform"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
lic, err := h.svc.Redeem(shopID, req.LicenseKey, req.DeviceID, req.DeviceName, req.Platform)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
util.RespondSuccess(c, lic)
|
|
}
|
|
|
|
// Verify GET /api/v1/license/verify
|
|
func (h *LicenseHandler) Verify(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
deviceID := c.Query("device_id")
|
|
if deviceID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "device_id required"})
|
|
return
|
|
}
|
|
|
|
lic, err := h.svc.Verify(shopID, deviceID)
|
|
if err != nil {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
util.RespondSuccess(c, lic)
|
|
}
|
|
|
|
// Info GET /api/v1/license/info — 当前门店授权概况
|
|
func (h *LicenseHandler) Info(c *gin.Context) {
|
|
view, err := h.svc.ShopInfoView(middleware.GetShopID(c))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
// view 为 nil 表示无有效授权 → data:null(与原行为一致)。
|
|
util.RespondSuccess(c, view)
|
|
}
|
|
|
|
// Devices GET /api/v1/license/devices — 已绑定设备列表
|
|
func (h *LicenseHandler) Devices(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
devs, err := h.svc.ListDevices(shopID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
util.RespondSuccess(c, devs)
|
|
}
|
|
|
|
// Deactivate POST /api/v1/license/deactivate
|
|
func (h *LicenseHandler) Deactivate(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
var req struct {
|
|
DeviceID string `json:"device_id" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Deactivate(shopID, req.DeviceID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "deactivated"})
|
|
}
|