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) }