feat: 通用授权后台、离线证、Waffo 支付页与仪表盘加强

支持多产品计划展示、LICOFF 离线签发、支付独立配置与调试,以及分析看板;同步更新 README 与集成文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-12 20:25:46 +08:00
1 parent 0966375076
commit 8cc2c83f9d
41 files changed
+3177 -332

No files matched your search

+176
View File
@@ -0,0 +1,176 @@
package utils
import (
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
const (
// OfflineLicensePrefix 离线序列号前缀(与在线 LIC- 区分) // Logan.Li
OfflineLicensePrefix = "LICOFF-1."
// OfflineMaxDurationDays 离线证最长有效期(禁止永久离线证) // Logan.Li
OfflineMaxDurationDays = 365
)
// OfflineLicenseClaims 离线许可证声明(自包含,桌面本地验签) // Logan.Li
type OfflineLicenseClaims struct {
V int `json:"v"`
ProductCode string `json:"pid"`
DeviceID string `json:"did"`
Email string `json:"email,omitempty"`
PlanType string `json:"pt"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
MaxDevices int `json:"md"`
CustomerName string `json:"cn,omitempty"`
}
// OfflineKeyPair 离线签发密钥对 // Logan.Li
type OfflineKeyPair struct {
Private ed25519.PrivateKey
Public ed25519.PublicKey
}
// DeriveOfflineKeyPair 从 32 字节 seed(hex)派生 Ed25519 密钥对 // Logan.Li
func DeriveOfflineKeyPair(seedHex string) (*OfflineKeyPair, error) {
seedHex = strings.TrimSpace(seedHex)
if seedHex == "" {
return nil, errors.New("离线签发 seed 为空")
}
raw, err := hex.DecodeString(seedHex)
if err != nil {
return nil, fmt.Errorf("离线签发 seed 不是合法 hex: %w", err)
}
if len(raw) != ed25519.SeedSize {
// 允许任意长度输入,统一 hash 成 32 字节 // Logan.Li
sum := sha256.Sum256(raw)
raw = sum[:]
}
priv := ed25519.NewKeyFromSeed(raw)
pub := priv.Public().(ed25519.PublicKey)
return &OfflineKeyPair{Private: priv, Public: pub}, nil
}
// OfflinePublicKeyHex 返回公钥 hex(供桌面端嵌入) // Logan.Li
func (k *OfflineKeyPair) OfflinePublicKeyHex() string {
return hex.EncodeToString(k.Public)
}
// IsOfflineLicenseKey 判断是否为离线序列号 // Logan.Li
func IsOfflineLicenseKey(key string) bool {
return strings.HasPrefix(strings.TrimSpace(key), "LICOFF-")
}
// SignOfflineLicense 签发离线序列号:LICOFF-1.<payload_b64url>.<sig_b64url> // Logan.Li
func SignOfflineLicense(kp *OfflineKeyPair, claims OfflineLicenseClaims) (string, error) {
if kp == nil || len(kp.Private) == 0 {
return "", errors.New("离线签发密钥未配置")
}
if err := validateOfflineClaims(claims, true); err != nil {
return "", err
}
claims.V = 1
payload, err := json.Marshal(claims)
if err != nil {
return "", err
}
payloadB64 := base64.RawURLEncoding.EncodeToString(payload)
sig := ed25519.Sign(kp.Private, []byte(payloadB64))
sigB64 := base64.RawURLEncoding.EncodeToString(sig)
return OfflineLicensePrefix + payloadB64 + "." + sigB64, nil
}
// VerifyOfflineLicense 验签并解析离线序列号(不校验设备与过期,由调用方决定) // Logan.Li
func VerifyOfflineLicense(publicKey ed25519.PublicKey, serial string) (*OfflineLicenseClaims, error) {
serial = strings.TrimSpace(serial)
if !strings.HasPrefix(serial, OfflineLicensePrefix) {
return nil, errors.New("不是合法的离线序列号(需 LICOFF-1. 前缀)")
}
body := strings.TrimPrefix(serial, OfflineLicensePrefix)
parts := strings.Split(body, ".")
if len(parts) != 2 {
return nil, errors.New("离线序列号格式错误")
}
payloadB64, sigB64 := parts[0], parts[1]
sig, err := base64.RawURLEncoding.DecodeString(sigB64)
if err != nil {
return nil, errors.New("离线序列号签名无法解码")
}
if len(publicKey) != ed25519.PublicKeySize {
return nil, errors.New("离线验签公钥无效")
}
if !ed25519.Verify(publicKey, []byte(payloadB64), sig) {
return nil, errors.New("离线序列号签名无效")
}
payload, err := base64.RawURLEncoding.DecodeString(payloadB64)
if err != nil {
return nil, errors.New("离线序列号载荷无法解码")
}
var claims OfflineLicenseClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, errors.New("离线序列号载荷无法解析")
}
if err := validateOfflineClaims(claims, false); err != nil {
return nil, err
}
return &claims, nil
}
// ParseOfflinePublicKeyHex 解析公钥 hex // Logan.Li
func ParseOfflinePublicKeyHex(hexKey string) (ed25519.PublicKey, error) {
raw, err := hex.DecodeString(strings.TrimSpace(hexKey))
if err != nil {
return nil, err
}
if len(raw) != ed25519.PublicKeySize {
return nil, errors.New("公钥长度错误")
}
return ed25519.PublicKey(raw), nil
}
func validateOfflineClaims(c OfflineLicenseClaims, issuing bool) error {
if strings.TrimSpace(c.DeviceID) == "" {
return errors.New("device_id 不能为空")
}
pt := strings.TrimSpace(c.PlanType)
if pt != "trial" && pt != "yearly" {
return errors.New("离线证仅允许 trial / yearly,禁止永久")
}
if c.ExpiresAt <= 0 || c.IssuedAt <= 0 {
return errors.New("签发/到期时间无效")
}
if c.ExpiresAt <= c.IssuedAt {
return errors.New("到期时间必须晚于签发时间")
}
maxSpan := int64(OfflineMaxDurationDays) * 24 * 3600
if c.ExpiresAt-c.IssuedAt > maxSpan+3600 { // 容许 1h 时钟
return errors.New("离线证有效期不得超过 365 天")
}
if issuing {
now := time.Now().Unix()
if c.ExpiresAt <= now {
return errors.New("到期时间已过")
}
if c.ExpiresAt-now > maxSpan+3600 {
return errors.New("离线证有效期不得超过 365 天")
}
}
if c.MaxDevices <= 0 {
c.MaxDevices = 1
}
if c.MaxDevices > 1 {
// 离线证绑定单设备,强制 1 // Logan.Li
return errors.New("离线证仅支持 1 台设备")
}
if strings.TrimSpace(c.ProductCode) == "" {
return errors.New("product_code 不能为空")
}
return nil
}
+75
View File
@@ -0,0 +1,75 @@
package utils
import (
"testing"
"time"
)
func TestOfflineLicenseSignAndVerify(t *testing.T) {
kp, err := DeriveOfflineKeyPair("319de107090c6fd0aae9b7db343376dd5a22ccfb24b631ee67144141b1d32000")
if err != nil {
t.Fatal(err)
}
now := time.Now().Unix()
claims := OfflineLicenseClaims{
ProductCode: "opsxshell",
DeviceID: "dev-1",
Email: "a@b.com",
PlanType: "yearly",
IssuedAt: now,
ExpiresAt: now + 86400*30,
MaxDevices: 1,
}
serial, err := SignOfflineLicense(kp, claims)
if err != nil {
t.Fatal(err)
}
if !IsOfflineLicenseKey(serial) {
t.Fatalf("prefix: %s", serial)
}
got, err := VerifyOfflineLicense(kp.Public, serial)
if err != nil {
t.Fatal(err)
}
if got.DeviceID != "dev-1" || got.PlanType != "yearly" {
t.Fatalf("claims mismatch: %+v", got)
}
}
func TestOfflineLicenseRejectsPerpetual(t *testing.T) {
kp, err := DeriveOfflineKeyPair("319de107090c6fd0aae9b7db343376dd5a22ccfb24b631ee67144141b1d32000")
if err != nil {
t.Fatal(err)
}
now := time.Now().Unix()
_, err = SignOfflineLicense(kp, OfflineLicenseClaims{
ProductCode: "opsxshell",
DeviceID: "dev-1",
PlanType: "perpetual",
IssuedAt: now,
ExpiresAt: now + 86400,
MaxDevices: 1,
})
if err == nil {
t.Fatal("expected perpetual reject")
}
}
func TestOfflineLicenseRejectsOver365(t *testing.T) {
kp, err := DeriveOfflineKeyPair("319de107090c6fd0aae9b7db343376dd5a22ccfb24b631ee67144141b1d32000")
if err != nil {
t.Fatal(err)
}
now := time.Now().Unix()
_, err = SignOfflineLicense(kp, OfflineLicenseClaims{
ProductCode: "opsxshell",
DeviceID: "dev-1",
PlanType: "yearly",
IssuedAt: now,
ExpiresAt: now + 86400*400,
MaxDevices: 1,
})
if err == nil {
t.Fatal("expected over-365 reject")
}
}