- 数据模型新增: title(必填), description(可选), status(draft/published) - 新增 API: POST /scripts/:id/publish, GET /api/market (搜索+分页+runtime过滤) - 前端首页重构: 选语言 → CodeMirror 编辑器(8种语言语法高亮) → 标题/描述 → 草稿/发布 - 新增 /market 页面: 浏览已发布脚本, 搜索+过滤+分页 - 详情页新增: 发布按钮(草稿→市场), title/description 展示 - Shell 类运行时显示 source 命令(继承环境变量) - backend GetSourceCommand 支持 bash/zsh/sh/fish 四种 shell 格式 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
205 lines
4.3 KiB
Go
205 lines
4.3 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"gitea.kmux.cn/zhilv/scriptforge/internal/config"
|
|
"gitea.kmux.cn/zhilv/scriptforge/internal/idgen"
|
|
"gitea.kmux.cn/zhilv/scriptforge/internal/model"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidRuntime = errors.New("invalid runtime")
|
|
ErrNotFound = errors.New("script not found or expired")
|
|
ErrAlreadyPublished = errors.New("script already published")
|
|
)
|
|
|
|
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
|
|
Runtime string
|
|
ExpiresIn string
|
|
Publish bool
|
|
}
|
|
|
|
type CreateResult struct {
|
|
Script model.Script
|
|
AdminToken string
|
|
Command string
|
|
SourceCmd string
|
|
RawURL string
|
|
}
|
|
|
|
func (s *ScriptService) Create(input CreateInput, scheme, host string) (*CreateResult, error) {
|
|
if !config.IsValidRuntime(input.Runtime) {
|
|
return nil, ErrInvalidRuntime
|
|
}
|
|
|
|
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,
|
|
Runtime: input.Runtime,
|
|
AdminToken: adminToken,
|
|
Status: status,
|
|
ExpiresAt: time.Now().Add(d),
|
|
CreatedAt: time.Now(),
|
|
PublishedAt: publishedAt,
|
|
}
|
|
|
|
if err := s.db.Create(&script).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rawURL := scheme + "://" + host + "/raw/" + id
|
|
command := "curl " + rawURL + " | " + input.Runtime
|
|
sourceCmd := config.GetSourceCommand(rawURL, input.Runtime)
|
|
|
|
return &CreateResult{
|
|
Script: script,
|
|
AdminToken: adminToken,
|
|
Command: command,
|
|
SourceCmd: sourceCmd,
|
|
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) 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
|
|
Runtime string
|
|
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.Runtime != "" && config.IsValidRuntime(q.Runtime) {
|
|
query = query.Where("runtime = ?", q.Runtime)
|
|
}
|
|
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
|
|
} |