feat: add project workspace service

This commit is contained in:
2026-07-18 15:57:44 +08:00
parent 437c070e45
commit 18b2f30024
3 changed files with 191 additions and 0 deletions

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

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

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