Go(fun)+SQLite backend, Vue 3 frontend. Modules: dashboard, pay gateway (Epay-compatible + CDK stock), license/card system (per-app appid, domain binding), AI memory bank with built-in MCP server (/mcp, FTS5 search, signed read links), server SSH monitor + terminal + SFTP, proxy pool with protocol-level alive checks, DB manager (mysql/pg/sqlite + SQL autocomplete + import/export), disk share, vault (AES-GCM + TOTP), bookmarks, dev tools. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
204 lines
6.1 KiB
Go
204 lines
6.1 KiB
Go
// license.go 授权系统管理:项目(appid/secret)+ 激活卡密(绑域名、按时长计时)。
|
||
// 对外校验走 /v1/license,仅供其他项目客户端调用。
|
||
package service
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"database/sql"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/cyi-cc/fun"
|
||
|
||
"github.com/cyi-cc/cyi-box/backend/internal/db"
|
||
"github.com/cyi-cc/cyi-box/backend/internal/dto"
|
||
"github.com/cyi-cc/cyi-box/backend/internal/store"
|
||
)
|
||
|
||
// LicenseSvc 授权管理(仅管理员)。
|
||
type LicenseSvc struct {
|
||
fun.Ctx
|
||
Store *store.Store
|
||
}
|
||
|
||
const (
|
||
licenseUnused = 0 // 未使用
|
||
licenseActive = 1 // 已激活(有效期内)
|
||
licenseExpired = 2 // 已过期
|
||
)
|
||
|
||
// ---- 项目管理 ----
|
||
|
||
func (s *LicenseSvc) Apps() (dto.LicenseAppsView, error) {
|
||
rows, err := s.Store.Q().ListLicenseApps(context.Background())
|
||
if err != nil {
|
||
return dto.LicenseAppsView{}, err
|
||
}
|
||
items := make([]dto.LicenseAppView, 0, len(rows))
|
||
for _, r := range rows {
|
||
items = append(items, dto.LicenseAppView{
|
||
Id: r.ID, Appid: r.Appid, Name: r.Name,
|
||
Enabled: r.Enabled, CardCount: r.CardCount, CreatedAt: r.CreatedAt,
|
||
})
|
||
}
|
||
return dto.LicenseAppsView{Items: items}, nil
|
||
}
|
||
|
||
// SaveApp id=0 新建(自动生成 appid+secret);否则改名/开关/重置 secret。
|
||
func (s *LicenseSvc) SaveApp(d dto.LicenseAppSaveDto) (dto.LicenseAppView, error) {
|
||
ctx := context.Background()
|
||
if d.Id == 0 {
|
||
name := strings.TrimSpace(d.Name)
|
||
if name == "" || len(name) > 50 {
|
||
return dto.LicenseAppView{}, fun.Error(4001, "项目名必填且不超过 50 字")
|
||
}
|
||
appid := "app-" + randLowerHex(4)
|
||
id, err := s.Store.Q().CreateLicenseApp(ctx, db.CreateLicenseAppParams{
|
||
Appid: appid, Secret: "", Name: name, CreatedAt: time.Now().Unix(),
|
||
})
|
||
if err != nil {
|
||
return dto.LicenseAppView{}, fmt.Errorf("创建项目失败: %w", err)
|
||
}
|
||
return dto.LicenseAppView{Id: id, Appid: appid, Name: name, Enabled: 1}, nil
|
||
}
|
||
name := strings.TrimSpace(d.Name)
|
||
if name == "" || len(name) > 50 {
|
||
return dto.LicenseAppView{}, fun.Error(4001, "项目名必填且不超过 50 字")
|
||
}
|
||
enabled := int64(1)
|
||
if d.Enabled != nil {
|
||
enabled = *d.Enabled
|
||
}
|
||
if err := s.Store.Q().UpdateLicenseApp(ctx, db.UpdateLicenseAppParams{
|
||
Name: name, Enabled: enabled, ID: d.Id,
|
||
}); err != nil {
|
||
return dto.LicenseAppView{}, err
|
||
}
|
||
return dto.LicenseAppView{Id: d.Id, Name: name, Enabled: enabled}, nil
|
||
}
|
||
|
||
func (s *LicenseSvc) DeleteApp(d dto.LicenseIdDto) (dto.OkView, error) {
|
||
if err := s.Store.Q().DeleteLicenseApp(context.Background(), d.Id); err != nil {
|
||
return dto.OkView{}, err
|
||
}
|
||
return dto.OkView{Message: "已删除"}, nil
|
||
}
|
||
|
||
// ---- 卡密管理 ----
|
||
|
||
func licenseStatus(c db.LicenseCard, now int64) int64 {
|
||
if c.ActivatedAt == 0 {
|
||
return licenseUnused
|
||
}
|
||
if c.ExpiresAt > 0 && c.ExpiresAt <= now { // expires_at=0 表示永久
|
||
return licenseExpired
|
||
}
|
||
return licenseActive
|
||
}
|
||
|
||
func licenseView(c db.LicenseCard, now int64) dto.LicenseCardView {
|
||
return dto.LicenseCardView{
|
||
Id: c.ID, Card: c.Card, Hours: c.Hours, Domain: c.Domain,
|
||
Status: licenseStatus(c, now),
|
||
ActivatedAt: c.ActivatedAt, ExpiresAt: c.ExpiresAt, CreatedAt: c.CreatedAt,
|
||
}
|
||
}
|
||
|
||
func (s *LicenseSvc) List(d dto.LicenseListDto) (dto.LicensePageView, error) {
|
||
kw := sql.NullString{}
|
||
if d.Kw != nil {
|
||
kw = sql.NullString{String: strings.TrimSpace(*d.Kw), Valid: true}
|
||
}
|
||
page := max(d.Page, 1)
|
||
size := min(max(d.PageSize, 1), 200)
|
||
ctx := context.Background()
|
||
total, err := s.Store.Q().CountLicenseCards(ctx, db.CountLicenseCardsParams{
|
||
AppID: d.AppId, Column2: kw.String, Column3: kw, Column4: kw,
|
||
})
|
||
if err != nil {
|
||
return dto.LicensePageView{}, err
|
||
}
|
||
rows, err := s.Store.Q().ListLicenseCards(ctx, db.ListLicenseCardsParams{
|
||
AppID: d.AppId, Column2: kw.String, Column3: kw, Column4: kw,
|
||
Limit: size, Offset: (page - 1) * size,
|
||
})
|
||
if err != nil {
|
||
return dto.LicensePageView{}, err
|
||
}
|
||
now := time.Now().Unix()
|
||
items := make([]dto.LicenseCardView, 0, len(rows))
|
||
for _, r := range rows {
|
||
items = append(items, licenseView(r, now))
|
||
}
|
||
return dto.LicensePageView{Total: total, Items: items}, nil
|
||
}
|
||
|
||
func (s *LicenseSvc) Stats(d dto.LicenseStatsDto) (dto.LicenseStatsView, error) {
|
||
now := time.Now().Unix()
|
||
r, err := s.Store.Q().LicenseCardStats(context.Background(), db.LicenseCardStatsParams{
|
||
ExpiresAt: now, ExpiresAt_2: now, AppID: d.AppId,
|
||
})
|
||
if err != nil {
|
||
return dto.LicenseStatsView{}, err
|
||
}
|
||
return dto.LicenseStatsView{Total: r.Total, Unused: anyI64(r.Unused), Active: anyI64(r.Active), Expired: anyI64(r.Expired)}, nil
|
||
}
|
||
|
||
// Generate 批量生成卡密:大写分组格式(XXXX-XXXX-XXXX-XXXX),一次最多 500。
|
||
func (s *LicenseSvc) Generate(d dto.LicenseGenDto) (dto.LicenseGenView, error) {
|
||
if d.AppId <= 0 {
|
||
return dto.LicenseGenView{}, fun.Error(4001, "请先选择项目")
|
||
}
|
||
count := min(max(d.Count, 1), 500)
|
||
hours := min(max(d.Hours, 0), 24*3650) // 0 = 永久
|
||
ctx := context.Background()
|
||
now := time.Now().Unix()
|
||
cards := make([]string, 0, count)
|
||
for i := int64(0); i < count; i++ {
|
||
card := genLicenseCard()
|
||
if err := s.Store.Q().CreateLicenseCard(ctx, db.CreateLicenseCardParams{
|
||
AppID: d.AppId, Card: card, Hours: hours, CreatedAt: now,
|
||
}); err != nil {
|
||
return dto.LicenseGenView{}, fmt.Errorf("生成卡密失败(%d/%d): %w", i, count, err)
|
||
}
|
||
cards = append(cards, card)
|
||
}
|
||
return dto.LicenseGenView{Cards: cards}, nil
|
||
}
|
||
|
||
func (s *LicenseSvc) Delete(d dto.LicenseIdDto) (dto.OkView, error) {
|
||
if err := s.Store.Q().DeleteLicenseCard(context.Background(), d.Id); err != nil {
|
||
return dto.OkView{}, err
|
||
}
|
||
return dto.OkView{Message: "已删除"}, nil
|
||
}
|
||
|
||
// genLicenseCard XXXX-XXXX-XXXX-XXXX 大写卡密,排除易混淆字符(0/O、1/I/L)。
|
||
func genLicenseCard() string {
|
||
const chars = "23456789ABCDEFGHJKMNPQRSTUVWXYZ"
|
||
var b [16]byte
|
||
if _, err := rand.Read(b[:]); err != nil {
|
||
panic(err)
|
||
}
|
||
var sb strings.Builder
|
||
for i, v := range b {
|
||
if i > 0 && i%4 == 0 {
|
||
sb.WriteByte('-')
|
||
}
|
||
sb.WriteByte(chars[int(v)%len(chars)])
|
||
}
|
||
return sb.String()
|
||
}
|
||
|
||
// randLowerHex n 字节随机小写 hex。
|
||
func randLowerHex(n int) string {
|
||
b := make([]byte, n)
|
||
if _, err := rand.Read(b); err != nil {
|
||
panic(err)
|
||
}
|
||
return hex.EncodeToString(b)
|
||
}
|