Files
scriptforge/backend/internal/service/script.go
zhilv 5414c9c865 feat: 分类/变体体系 + 用户认证 + 管理后台
- 运行时分类体系:Shell/Python/JavaScript/Ruby/PHP 各含变体
- 用户注册/登录(JWT + bcrypt),首个注册用户为管理员
- 管理后台 /admin 动态管理分类和变体
- 脚本市场支持按分类筛选
- CodeMirror 语言模式根据分类名称自动切换
- 结果页展示该分类下所有变体的运行命令
- source 命令变体用于 Shell 类继承环境变量

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:02:20 +08:00

256 lines
5.9 KiB
Go

package service
import (
"errors"
"fmt"
"time"
"gorm.io/gorm"
"gitea.kmux.cn/zhilv/scriptforge/internal/idgen"
"gitea.kmux.cn/zhilv/scriptforge/internal/model"
)
var (
ErrNotFound = errors.New("script not found or expired")
ErrAlreadyPublished = errors.New("script already published")
ErrInvalidCategory = errors.New("invalid category")
)
type ScriptService struct {
db *gorm.DB
}
func NewScriptService(db *gorm.DB) *ScriptService {
return &ScriptService{db: db}
}
type CreateInput struct {
Title string
Description string
Content string
CategoryID uint
ExpiresIn string
Publish bool
UserID uint
}
type CreateResult struct {
Script model.Script
AdminToken string
RawURL string
}
func (s *ScriptService) Create(input CreateInput, scheme, host string) (*CreateResult, error) {
var cat model.RuntimeCategory
if err := s.db.First(&cat, input.CategoryID).Error; err != nil {
return nil, ErrInvalidCategory
}
var d time.Duration
switch input.ExpiresIn {
case "1h":
d = 1 * time.Hour
case "24h":
d = 24 * time.Hour
case "7d":
d = 7 * 24 * time.Hour
case "30d":
d = 30 * 24 * time.Hour
default:
d = 24 * time.Hour
}
id, err := idgen.GenerateID(8)
if err != nil {
return nil, err
}
adminToken, err := idgen.GenerateToken()
if err != nil {
return nil, err
}
status := "draft"
var publishedAt *time.Time
if input.Publish {
status = "published"
now := time.Now()
publishedAt = &now
}
script := model.Script{
ID: id,
Title: input.Title,
Description: input.Description,
Content: input.Content,
CategoryID: input.CategoryID,
UserID: input.UserID,
AdminToken: adminToken,
Status: status,
ExpiresAt: time.Now().Add(d),
PublishedAt: publishedAt,
}
if err := s.db.Create(&script).Error; err != nil {
return nil, err
}
rawURL := scheme + "://" + host + "/raw/" + id
return &CreateResult{
Script: script,
AdminToken: adminToken,
RawURL: rawURL,
}, nil
}
func (s *ScriptService) GetByID(id string) (*model.Script, error) {
var script model.Script
err := s.db.Where("id = ? AND expires_at > ?", id, time.Now()).First(&script).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &script, err
}
func (s *ScriptService) GetCategoryByID(id uint) (*model.RuntimeCategory, error) {
var cat model.RuntimeCategory
err := s.db.Preload("Variants").First(&cat, id).Error
return &cat, err
}
func (s *ScriptService) GetVariants(categoryID uint) ([]model.RuntimeVariant, error) {
var variants []model.RuntimeVariant
err := s.db.Where("category_id = ?", categoryID).Order("sort_order ASC").Find(&variants).Error
return variants, err
}
func (s *ScriptService) GetDefaultVariant(categoryID uint) (*model.RuntimeVariant, error) {
var v model.RuntimeVariant
err := s.db.Where("category_id = ? AND is_default = true", categoryID).First(&v).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
err = s.db.Where("category_id = ?", categoryID).Order("sort_order ASC").First(&v).Error
}
return &v, err
}
func (s *ScriptService) Publish(id, token string) (*model.Script, error) {
var script model.Script
err := s.db.Where("id = ? AND admin_token = ? AND expires_at > ?", id, token, time.Now()).First(&script).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
if script.Status == "published" {
return nil, ErrAlreadyPublished
}
now := time.Now()
script.Status = "published"
script.PublishedAt = &now
if err := s.db.Save(&script).Error; err != nil {
return nil, err
}
return &script, nil
}
type MarketQuery struct {
Page int
PerPage int
CategoryID uint
Search string
}
type MarketResult struct {
Items []model.Script
Total int64
Page int
PerPage int
}
func (s *ScriptService) ListMarket(q MarketQuery) (*MarketResult, error) {
if q.Page < 1 {
q.Page = 1
}
if q.PerPage < 1 || q.PerPage > 50 {
q.PerPage = 20
}
query := s.db.Model(&model.Script{}).
Where("status = ? AND expires_at > ?", "published", time.Now())
if q.CategoryID > 0 {
query = query.Where("category_id = ?", q.CategoryID)
}
if q.Search != "" {
search := "%" + q.Search + "%"
query = query.Where("title LIKE ? OR description LIKE ?", search, search)
}
var total int64
query.Count(&total)
var scripts []model.Script
err := query.Order("published_at DESC").
Offset((q.Page - 1) * q.PerPage).
Limit(q.PerPage).
Find(&scripts).Error
return &MarketResult{
Items: scripts,
Total: total,
Page: q.Page,
PerPage: q.PerPage,
}, err
}
func (s *ScriptService) Delete(id, token string) error {
result := s.db.Where("id = ? AND admin_token = ?", id, token).Delete(&model.Script{})
if result.RowsAffected == 0 {
return ErrNotFound
}
return result.Error
}
func (s *ScriptService) CleanupExpired() (int64, error) {
result := s.db.Where("expires_at <= ?", time.Now()).Delete(&model.Script{})
return result.RowsAffected, result.Error
}
func (s *ScriptService) ListCategories() ([]model.RuntimeCategory, error) {
var cats []model.RuntimeCategory
err := s.db.Preload("Variants").Order("sort_order ASC").Find(&cats).Error
return cats, err
}
func (s *ScriptService) CreateCategory(cat *model.RuntimeCategory) error {
return s.db.Create(cat).Error
}
func (s *ScriptService) UpdateCategory(cat *model.RuntimeCategory) error {
return s.db.Save(cat).Error
}
func (s *ScriptService) DeleteCategory(id uint) error {
s.db.Where("category_id = ?", id).Delete(&model.RuntimeVariant{})
return s.db.Delete(&model.RuntimeCategory{}, id).Error
}
func (s *ScriptService) CreateVariant(v *model.RuntimeVariant) error {
return s.db.Create(v).Error
}
func (s *ScriptService) UpdateVariant(v *model.RuntimeVariant) error {
return s.db.Save(v).Error
}
func (s *ScriptService) DeleteVariant(id uint) error {
return s.db.Delete(&model.RuntimeVariant{}, id).Error
}
func FormatCommand(template, url string) string {
return fmt.Sprintf(template, url)
}