109 lines
3.1 KiB
Go
109 lines
3.1 KiB
Go
package projects
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
"senlinai-agent/backend/internal/models"
|
|
)
|
|
|
|
var (
|
|
ErrTagNameRequired = errors.New("tag name is required")
|
|
ErrCronTitleRequired = errors.New("cron plan title is required")
|
|
ErrCronScheduleRequired = errors.New("cron schedule is required")
|
|
)
|
|
|
|
func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronPlanInput) (*models.SaCronPlan, error) {
|
|
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
|
return nil, err
|
|
}
|
|
title := strings.TrimSpace(input.Title)
|
|
if title == "" {
|
|
return nil, ErrCronTitleRequired
|
|
}
|
|
schedule := strings.TrimSpace(input.Schedule)
|
|
if schedule == "" {
|
|
return nil, ErrCronScheduleRequired
|
|
}
|
|
plan := &models.SaCronPlan{
|
|
ProjectID: projectID, CreatedBy: ownerID, Title: title, Schedule: schedule,
|
|
Enabled: input.Enabled, NextRunAt: input.NextRunAt,
|
|
}
|
|
return plan, models.DBService.Create(plan).Error
|
|
}
|
|
|
|
func (s *Service) CreateProjectTag(ownerID uint, projectID uint, name string) (*models.SaTag, error) {
|
|
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.CreateTag(projectID, name)
|
|
}
|
|
|
|
func (s *Service) CreateTag(projectID uint, name string) (*models.SaTag, error) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return nil, ErrTagNameRequired
|
|
}
|
|
var lastErr error
|
|
for attempt := 0; attempt < 5; attempt++ {
|
|
candidate := models.SaTag{ProjectID: projectID, Name: name}
|
|
lastErr = models.DBService.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "project_id"}, {Name: "name"}},
|
|
DoNothing: true,
|
|
}).Create(&candidate).Error
|
|
if lastErr == nil {
|
|
var stored models.SaTag
|
|
lastErr = models.DBService.Where("project_id = ? AND name = ?", projectID, name).First(&stored).Error
|
|
if lastErr == nil {
|
|
return &stored, nil
|
|
}
|
|
}
|
|
if !retryableTagWrite(lastErr) {
|
|
return nil, lastErr
|
|
}
|
|
time.Sleep(time.Duration(attempt+1) * time.Millisecond)
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
func retryableTagWrite(err error) bool {
|
|
if errors.Is(err, gorm.ErrDuplicatedKey) || errors.Is(err, gorm.ErrRecordNotFound) || strings.Contains(strings.ToLower(err.Error()), "database is locked") {
|
|
return true
|
|
}
|
|
var sqlState interface{ SQLState() string }
|
|
if errors.As(err, &sqlState) {
|
|
switch sqlState.SQLState() {
|
|
case "40001", "40P01", "55P03":
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *Service) ListProjectTags(ownerID uint, projectID uint) ([]models.SaTag, error) {
|
|
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.ListTags(projectID)
|
|
}
|
|
|
|
func (s *Service) ListTags(projectID uint) ([]models.SaTag, error) {
|
|
var tags []models.SaTag
|
|
err := models.DBService.Where("project_id = ?", projectID).Order("name asc").Find(&tags).Error
|
|
return tags, err
|
|
}
|
|
|
|
func ensureProjectOwner(ownerID uint, projectID uint) error {
|
|
var count int64
|
|
if err := models.DBService.Model(&models.SaProject{}).Where("id = ? AND owner_id = ?", projectID, ownerID).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
return nil
|
|
}
|