feat: add project workspace service
This commit is contained in:
62
backend/internal/projects/handlers.go
Normal file
62
backend/internal/projects/handlers.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
func (h *Handler) Register(router gin.IRouter) {
|
||||
router.POST("/projects", h.createProject)
|
||||
router.GET("/projects", h.listProjects)
|
||||
router.GET("/projects/:id/dashboard", h.dashboard)
|
||||
}
|
||||
|
||||
func (h *Handler) createProject(c *gin.Context) {
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
project, err := h.service.CreateProject(1, input.Name, input.Description)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, project)
|
||||
}
|
||||
|
||||
func (h *Handler) listProjects(c *gin.Context) {
|
||||
projects, err := h.service.ListProjects(1)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list projects"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, projects)
|
||||
}
|
||||
|
||||
func (h *Handler) dashboard(c *gin.Context) {
|
||||
projectID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
dashboard, err := h.service.Dashboard(uint(projectID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load dashboard"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, dashboard)
|
||||
}
|
||||
72
backend/internal/projects/service.go
Normal file
72
backend/internal/projects/service.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/domain"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
ProjectID uint `json:"project_id"`
|
||||
PendingInboxCount int64 `json:"pending_inbox_count"`
|
||||
OpenTaskCount int64 `json:"open_task_count"`
|
||||
RecentNoteCount int64 `json:"recent_note_count"`
|
||||
RecentSessionCount int64 `json:"recent_session_count"`
|
||||
}
|
||||
|
||||
func NewService(database *gorm.DB) *Service {
|
||||
return &Service{db: database}
|
||||
}
|
||||
|
||||
func (s *Service) CreateProject(ownerID uint, name string, description string) (*domain.Project, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, errors.New("project name is required")
|
||||
}
|
||||
project := &domain.Project{OwnerID: ownerID, Name: name, Description: description}
|
||||
return project, s.db.Create(project).Error
|
||||
}
|
||||
|
||||
func (s *Service) ListProjects(ownerID uint) ([]domain.Project, error) {
|
||||
var projects []domain.Project
|
||||
err := s.db.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error
|
||||
return projects, err
|
||||
}
|
||||
|
||||
func (s *Service) CreateTag(projectID uint, name string) (*domain.Tag, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, errors.New("tag name is required")
|
||||
}
|
||||
tag := &domain.Tag{ProjectID: projectID, Name: name}
|
||||
return tag, s.db.Create(tag).Error
|
||||
}
|
||||
|
||||
func (s *Service) ListTags(projectID uint) ([]domain.Tag, error) {
|
||||
var tags []domain.Tag
|
||||
err := s.db.Where("project_id = ?", projectID).Order("name asc").Find(&tags).Error
|
||||
return tags, err
|
||||
}
|
||||
|
||||
func (s *Service) Dashboard(projectID uint) (Dashboard, error) {
|
||||
dashboard := Dashboard{ProjectID: projectID}
|
||||
if err := s.db.Model(&domain.InboxItem{}).Where("project_id = ? AND status = ?", projectID, "open").Count(&dashboard.PendingInboxCount).Error; err != nil {
|
||||
return dashboard, err
|
||||
}
|
||||
if err := s.db.Model(&domain.Task{}).Where("project_id = ? AND status <> ?", projectID, "done").Count(&dashboard.OpenTaskCount).Error; err != nil {
|
||||
return dashboard, err
|
||||
}
|
||||
if err := s.db.Model(&domain.Note{}).Where("project_id = ?", projectID).Count(&dashboard.RecentNoteCount).Error; err != nil {
|
||||
return dashboard, err
|
||||
}
|
||||
if err := s.db.Model(&domain.AISession{}).Where("project_id = ?", projectID).Count(&dashboard.RecentSessionCount).Error; err != nil {
|
||||
return dashboard, err
|
||||
}
|
||||
return dashboard, nil
|
||||
}
|
||||
57
backend/internal/projects/service_test.go
Normal file
57
backend/internal/projects/service_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/domain"
|
||||
)
|
||||
|
||||
func TestProjectTagsAreScopedToProject(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService(database)
|
||||
first, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
second, err := service.CreateProject(1, "Beta", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.CreateTag(first.ID, "重要")
|
||||
require.NoError(t, err)
|
||||
_, err = service.CreateTag(second.ID, "重要")
|
||||
require.NoError(t, err)
|
||||
|
||||
firstTags, err := service.ListTags(first.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, firstTags, 1)
|
||||
require.Equal(t, first.ID, firstTags[0].ProjectID)
|
||||
}
|
||||
|
||||
func TestDashboardCountsOnlyRequestedProject(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService(database)
|
||||
first, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
second, err := service.CreateProject(1, "Beta", "")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, database.Create(&domain.InboxItem{ProjectID: first.ID, CreatedBy: 1, SourceType: "text", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&domain.InboxItem{ProjectID: second.ID, CreatedBy: 1, SourceType: "text", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&domain.Task{ProjectID: first.ID, CreatedBy: 1, Title: "A", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&domain.Task{ProjectID: second.ID, CreatedBy: 1, Title: "B", Status: "open"}).Error)
|
||||
|
||||
dashboard, err := service.Dashboard(first.ID)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), dashboard.PendingInboxCount)
|
||||
require.Equal(t, int64(1), dashboard.OpenTaskCount)
|
||||
}
|
||||
|
||||
func newTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, domain.AutoMigrate(database))
|
||||
return database
|
||||
}
|
||||
Reference in New Issue
Block a user