refactor backend packages and global db service

This commit is contained in:
2026-07-18 21:58:15 +08:00
parent 62bd3d0455
commit 7800b07d42
51 changed files with 719 additions and 647 deletions

View File

@@ -0,0 +1,90 @@
package inbox
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"senlinai-agent/backend/internal/logic/auth"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) Register(router gin.IRouter) {
router.POST("/projects/:projectID/inbox", h.capture)
router.POST("/inbox/:id/analyze", h.analyze)
router.POST("/inbox/:id/confirm", h.confirm)
}
func (h *Handler) capture(c *gin.Context) {
userID, ok := auth.CurrentUserID(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
projectID, err := strconv.ParseUint(c.Param("projectID"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
return
}
var input struct {
SourceType string `json:"source_type"`
Title string `json:"title"`
Body string `json:"body"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
item, err := h.service.Capture(CaptureInput{ProjectID: uint(projectID), UserID: userID, SourceType: input.SourceType, Title: input.Title, Body: input.Body})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, item)
}
func (h *Handler) analyze(c *gin.Context) {
userID, ok := auth.CurrentUserID(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
itemID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
suggestions, err := h.service.Analyze(uint(itemID), userID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"suggestions": suggestions})
}
func (h *Handler) confirm(c *gin.Context) {
itemID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
var input struct {
Suggestions []Suggestion `json:"suggestions"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if err := h.service.Confirm(uint(itemID), input.Suggestions); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}

View File

@@ -0,0 +1,100 @@
package inbox
import (
"errors"
"gorm.io/gorm"
"senlinai-agent/backend/internal/models"
)
type CaptureInput struct {
ProjectID uint
UserID uint
SourceType string
Title string
Body string
}
type Suggestion struct {
Kind string `json:"kind"`
Title string `json:"title"`
Body string `json:"body"`
}
type Analyzer interface {
Analyze(item models.SenlinAgentInboxItem, userID uint) ([]Suggestion, error)
}
type StaticAnalyzer struct {
Suggestions []Suggestion
}
func (a StaticAnalyzer) Analyze(item models.SenlinAgentInboxItem, userID uint) ([]Suggestion, error) {
return a.Suggestions, nil
}
type Service struct {
analyzer Analyzer
}
func NewService(analyzer Analyzer) *Service {
return &Service{analyzer: analyzer}
}
func (s *Service) Capture(input CaptureInput) (*models.SenlinAgentInboxItem, error) {
if input.ProjectID == 0 || input.UserID == 0 {
return nil, errors.New("project and user are required")
}
if input.SourceType == "" {
return nil, errors.New("source type is required")
}
item := &models.SenlinAgentInboxItem{
ProjectID: input.ProjectID,
CreatedBy: input.UserID,
SourceType: input.SourceType,
Title: input.Title,
Body: input.Body,
Status: "open",
}
return item, models.DBService.Create(item).Error
}
func (s *Service) Analyze(itemID uint, userID uint) ([]Suggestion, error) {
var item models.SenlinAgentInboxItem
if err := models.DBService.First(&item, itemID).Error; err != nil {
return nil, err
}
if s.analyzer == nil {
return []Suggestion{}, nil
}
return s.analyzer.Analyze(item, userID)
}
func (s *Service) Confirm(itemID uint, selected []Suggestion) error {
return models.DBService.Transaction(func(tx *gorm.DB) error {
var item models.SenlinAgentInboxItem
if err := tx.First(&item, itemID).Error; err != nil {
return err
}
sourceInboxItemID := item.ID
for _, suggestion := range selected {
switch suggestion.Kind {
case "task":
if err := tx.Create(&models.SenlinAgentTask{ProjectID: item.ProjectID, CreatedBy: item.CreatedBy, SourceInboxItemID: &sourceInboxItemID, Title: suggestion.Title, Description: suggestion.Body}).Error; err != nil {
return err
}
case "note":
if err := tx.Create(&models.SenlinAgentNote{ProjectID: item.ProjectID, CreatedBy: item.CreatedBy, SourceInboxItemID: &sourceInboxItemID, Title: suggestion.Title, Markdown: suggestion.Body}).Error; err != nil {
return err
}
case "source":
if err := tx.Create(&models.SenlinAgentSource{ProjectID: item.ProjectID, CreatedBy: item.CreatedBy, SourceInboxItemID: &sourceInboxItemID, Kind: "link", Title: suggestion.Title, ContentText: suggestion.Body}).Error; err != nil {
return err
}
default:
return errors.New("unsupported suggestion kind")
}
}
return tx.Model(&item).Update("status", "processed").Error
})
}

View File

@@ -0,0 +1,61 @@
package inbox
import (
"fmt"
"testing"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"senlinai-agent/backend/internal/models"
)
func TestAnalyzeReturnsSuggestionsWithoutCreatingObjects(t *testing.T) {
database := newTestDB(t)
service := NewService(StaticAnalyzer{
Suggestions: []Suggestion{{Kind: "task", Title: "跟进报价", Body: "联系客户确认报价"}},
})
item, err := service.Capture(CaptureInput{ProjectID: 1, UserID: 1, SourceType: "text", Body: "需要跟进报价"})
require.NoError(t, err)
suggestions, err := service.Analyze(item.ID, 1)
require.NoError(t, err)
require.Len(t, suggestions, 1)
var count int64
require.NoError(t, database.Model(&models.SenlinAgentTask{}).Count(&count).Error)
require.Equal(t, int64(0), count)
}
func TestConfirmCreatesSelectedObjectsAndKeepsInboxItem(t *testing.T) {
database := newTestDB(t)
service := NewService(StaticAnalyzer{
Suggestions: []Suggestion{{Kind: "task", Title: "跟进报价", Body: "联系客户确认报价"}},
})
item, err := service.Capture(CaptureInput{ProjectID: 1, UserID: 1, SourceType: "text", Body: "需要跟进报价"})
require.NoError(t, err)
suggestions, err := service.Analyze(item.ID, 1)
require.NoError(t, err)
err = service.Confirm(item.ID, suggestions)
require.NoError(t, err)
var tasks []models.SenlinAgentTask
require.NoError(t, database.Find(&tasks).Error)
require.Len(t, tasks, 1)
require.Equal(t, "跟进报价", tasks[0].Title)
require.NotNil(t, tasks[0].SourceInboxItemID)
require.Equal(t, item.ID, *tasks[0].SourceInboxItemID)
var reloaded models.SenlinAgentInboxItem
require.NoError(t, database.First(&reloaded, item.ID).Error)
require.Equal(t, "processed", reloaded.Status)
}
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, models.AutoMigrate(database))
models.DBService = database
return database
}