Files
agent/backend/internal/logic/projects/service.go

186 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package projects
import (
"errors"
"net/url"
"strings"
"gorm.io/gorm"
"senlinai-agent/backend/internal/models"
)
type Service struct{}
func NewService() *Service {
return &Service{}
}
func (s *Service) CreateProject(ownerID uint, name string, description string) (*models.SaProject, error) {
return s.CreateProjectWithInput(ownerID, CreateProjectRequest{Name: name, Description: description})
}
func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectRequest) (*models.SaProject, error) {
name := strings.TrimSpace(input.Name)
if name == "" {
return nil, ErrProjectNameRequired
}
identifier := strings.TrimSpace(input.Identifier)
if identifier == "" {
identifier = projectInitials(name)
}
project := &models.SaProject{
OwnerID: ownerID, Name: name, Identifier: identifier,
Icon: strings.TrimSpace(input.Icon), Background: strings.TrimSpace(input.Background),
Description: strings.TrimSpace(input.Description),
}
if err := projectWriteError(models.DBService.Create(project).Error); err != nil {
return nil, err
}
return project, nil
}
// ListProjects 只返回当前所有者的公开 DTO不让 handler 接触或序列化数据库模型。
func (s *Service) ListProjects(ownerID uint) ([]ProjectDTO, error) {
var projects []models.SaProject
if err := models.DBService.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error; err != nil {
return nil, err
}
result := make([]ProjectDTO, 0, len(projects))
for _, project := range projects {
result = append(result, projectDTO(project))
}
return result, nil
}
// GetProject 使用公开 identity 和所有权边界读取单个项目。
func (s *Service) GetProject(ownerID uint, identity string) (ProjectDTO, error) {
project, err := FindOwnedProject(ownerID, identity)
if err != nil {
return ProjectDTO{}, err
}
return projectDTO(*project), nil
}
var (
ErrProjectNameRequired = errors.New("project name is required")
ErrProjectIdentifierRequired = errors.New("project identifier is required")
ErrProjectIdentifierConflict = errors.New("project identifier already exists")
ErrChannelTitleRequired = errors.New("channel title is required")
ErrChannelURLRequired = errors.New("channel url is required")
ErrChannelURLInvalid = errors.New("channel url must use http or https")
)
func (s *Service) CreateProjectChannel(ownerID, projectID uint, input CreateProjectChannelInput) (*models.SaProjectChannel, error) {
if err := ensureProjectOwner(ownerID, projectID); err != nil {
return nil, err
}
title := strings.TrimSpace(input.Title)
if title == "" {
return nil, ErrChannelTitleRequired
}
address := strings.TrimSpace(input.URL)
if address == "" {
return nil, ErrChannelURLRequired
}
parsedURL, err := url.ParseRequestURI(address)
if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") || parsedURL.Host == "" {
return nil, ErrChannelURLInvalid
}
icon := strings.TrimSpace(input.Icon)
if icon == "" {
icon = "link"
}
var previous models.SaProjectChannel
_ = models.DBService.Where("project_id = ?", projectID).Order("sort_order DESC").First(&previous).Error
channel := &models.SaProjectChannel{ProjectID: projectID, Title: title, Icon: icon, URL: address, SortOrder: previous.SortOrder + 1}
if err := models.DBService.Create(channel).Error; err != nil {
return nil, err
}
return channel, nil
}
// projectWriteError 将 Postgres/SQLite 经 Gorm 翻译后的唯一约束错误收敛为稳定业务冲突。
func projectWriteError(err error) error {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return ErrProjectIdentifierConflict
}
return err
}
// UpdateProject 仅更新项目设置白名单字段,查询和冲突检查均限定在当前所有者内。
func (s *Service) UpdateProject(ownerID uint, identity string, input UpdateProjectRequest) error {
project, err := FindOwnedProject(ownerID, identity)
if err != nil {
return err
}
updates := make(map[string]any, 5)
if input.Name != nil {
name := strings.TrimSpace(*input.Name)
if name == "" {
return ErrProjectNameRequired
}
updates["name"] = name
}
if input.Identifier != nil {
identifier := strings.TrimSpace(*input.Identifier)
if identifier == "" {
return ErrProjectIdentifierRequired
}
updates["identifier"] = identifier
}
if input.Icon != nil {
updates["icon"] = strings.TrimSpace(*input.Icon)
}
if input.Background != nil {
updates["background"] = strings.TrimSpace(*input.Background)
}
if input.Description != nil {
updates["description"] = strings.TrimSpace(*input.Description)
}
if len(updates) == 0 {
return nil
}
return projectWriteError(models.DBService.Model(project).Updates(updates).Error)
}
func projectInitials(name string) string {
words := strings.Fields(name)
if len(words) == 0 {
return "P"
}
for i := len(words) - 1; i >= 0; i-- {
word := strings.Trim(words[i], "#_-")
if isShortCode(word) {
return strings.ToUpper(word)
}
}
if len(words) == 1 {
runes := []rune(words[0])
if len(runes) == 1 {
return strings.ToUpper(string(runes[0]))
}
return strings.ToUpper(string(runes[:2]))
}
return strings.ToUpper(string([]rune(words[0])[0]) + string([]rune(words[len(words)-1])[0]))
}
func isShortCode(value string) bool {
runes := []rune(value)
if len(runes) == 0 || len(runes) > 3 {
return false
}
hasDigit := false
for _, r := range runes {
if r >= '0' && r <= '9' {
hasDigit = true
continue
}
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
continue
}
return false
}
return hasDigit
}