refactor: shorten model and table prefixes
This commit is contained in:
@@ -81,7 +81,7 @@ func writeCronError(c *gin.Context, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func cronPlanDTO(plan models.SenlinAgentCronPlan) CronPlanDTO {
|
||||
func cronPlanDTO(plan models.SaCronPlan) CronPlanDTO {
|
||||
return CronPlanDTO{
|
||||
ID: plan.Identity, ProjectID: plan.ProjectIdentity, Title: plan.Title, Schedule: plan.Schedule,
|
||||
NextRunAt: projectUTCOptionalTime(plan.NextRunAt), Enabled: plan.Enabled, LastResult: plan.LastResult,
|
||||
|
||||
@@ -144,7 +144,7 @@ type WorkspaceCronPlanDTO struct {
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
func projectDTO(project models.SenlinAgentProject) ProjectDTO {
|
||||
func projectDTO(project models.SaProject) ProjectDTO {
|
||||
return ProjectDTO{
|
||||
ID: project.Identity,
|
||||
Name: project.Name,
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestCreateProjectHandlerPersistsMetadata(t *testing.T) {
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, rec.Code)
|
||||
var project models.SenlinAgentProject
|
||||
var project models.SaProject
|
||||
require.NoError(t, models.DBService.Where("owner_id = ? AND identifier = ?", 1, "EXP").First(&project).Error)
|
||||
require.Equal(t, "Explore", project.Name)
|
||||
require.Equal(t, "compass", project.Icon)
|
||||
@@ -138,7 +138,7 @@ func TestUpdateProject(t *testing.T) {
|
||||
require.Equal(t, "tree", payload["icon"])
|
||||
require.Equal(t, "#0FC6C2", payload["background"])
|
||||
require.Equal(t, "更新后的项目说明", payload["description"])
|
||||
var updated models.SenlinAgentProject
|
||||
var updated models.SaProject
|
||||
require.NoError(t, models.DBService.First(&updated, project.ID).Error)
|
||||
require.Equal(t, "Alpha Next", updated.Name)
|
||||
require.Equal(t, "ALPHA-NEXT", updated.Identifier)
|
||||
@@ -178,7 +178,7 @@ func TestUpdateProject(t *testing.T) {
|
||||
require.Empty(t, payload.Icon)
|
||||
require.Empty(t, payload.Background)
|
||||
require.Empty(t, payload.Description)
|
||||
var updated models.SenlinAgentProject
|
||||
var updated models.SaProject
|
||||
require.NoError(t, models.DBService.First(&updated, project.ID).Error)
|
||||
require.Equal(t, originalName, updated.Name)
|
||||
require.Equal(t, originalIdentifier, updated.Identifier)
|
||||
@@ -228,10 +228,10 @@ func TestUpdateProject(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func newProjectsHandlerTestRouter(t *testing.T) (*gin.Engine, *models.SenlinAgentProject, string) {
|
||||
func newProjectsHandlerTestRouter(t *testing.T) (*gin.Engine, *models.SaProject, string) {
|
||||
t.Helper()
|
||||
newTestDB(t)
|
||||
require.NoError(t, models.DBService.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, models.DBService.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
service := NewService()
|
||||
project, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -16,7 +16,7 @@ var (
|
||||
ErrCronScheduleRequired = errors.New("cron schedule is required")
|
||||
)
|
||||
|
||||
func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronPlanInput) (*models.SenlinAgentCronPlan, error) {
|
||||
func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronPlanInput) (*models.SaCronPlan, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -28,34 +28,34 @@ func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronP
|
||||
if schedule == "" {
|
||||
return nil, ErrCronScheduleRequired
|
||||
}
|
||||
plan := &models.SenlinAgentCronPlan{
|
||||
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.SenlinAgentTag, 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.SenlinAgentTag, error) {
|
||||
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.SenlinAgentTag{ProjectID: projectID, Name: name}
|
||||
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.SenlinAgentTag
|
||||
var stored models.SaTag
|
||||
lastErr = models.DBService.Where("project_id = ? AND name = ?", projectID, name).First(&stored).Error
|
||||
if lastErr == nil {
|
||||
return &stored, nil
|
||||
@@ -83,22 +83,22 @@ func retryableTagWrite(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) ListProjectTags(ownerID uint, projectID uint) ([]models.SenlinAgentTag, error) {
|
||||
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.SenlinAgentTag, error) {
|
||||
var tags []models.SenlinAgentTag
|
||||
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.SenlinAgentProject{}).Where("id = ? AND owner_id = ?", projectID, ownerID).Count(&count).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaProject{}).Where("id = ? AND owner_id = ?", projectID, ownerID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
// FindOwnedProject 同时按 owner_id 与公开 identity 查询,防止仅凭可猜测标识越权访问项目。
|
||||
// 内部自增 ID 只在通过所有权校验后供关联查询使用,不进入 API 契约。
|
||||
func FindOwnedProject(userID uint, identity string) (*models.SenlinAgentProject, error) {
|
||||
var project models.SenlinAgentProject
|
||||
func FindOwnedProject(userID uint, identity string) (*models.SaProject, error) {
|
||||
var project models.SaProject
|
||||
result := models.DBService.Where("owner_id = ? AND identity = ?", userID, identity).Limit(1).Find(&project)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
|
||||
@@ -14,11 +14,11 @@ func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) CreateProject(ownerID uint, name string, description string) (*models.SenlinAgentProject, error) {
|
||||
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.SenlinAgentProject, error) {
|
||||
func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectRequest) (*models.SaProject, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
if name == "" {
|
||||
return nil, ErrProjectNameRequired
|
||||
@@ -27,7 +27,7 @@ func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectReques
|
||||
if identifier == "" {
|
||||
identifier = projectInitials(name)
|
||||
}
|
||||
project := &models.SenlinAgentProject{
|
||||
project := &models.SaProject{
|
||||
OwnerID: ownerID, Name: name, Identifier: identifier,
|
||||
Icon: strings.TrimSpace(input.Icon), Background: strings.TrimSpace(input.Background),
|
||||
Description: strings.TrimSpace(input.Description),
|
||||
@@ -40,7 +40,7 @@ func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectReques
|
||||
|
||||
// ListProjects 只返回当前所有者的公开 DTO,不让 handler 接触或序列化数据库模型。
|
||||
func (s *Service) ListProjects(ownerID uint) ([]ProjectDTO, error) {
|
||||
var projects []models.SenlinAgentProject
|
||||
var projects []models.SaProject
|
||||
if err := models.DBService.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -37,21 +37,21 @@ func TestFindOwnedProjectScopesIdentityByOwner(t *testing.T) {
|
||||
|
||||
func TestProjectIdentifierDatabaseConstraintIsScopedByOwner(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
first := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
first := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
|
||||
duplicate := models.SenlinAgentProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
duplicate := models.SaProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
require.ErrorIs(t, database.Create(&duplicate).Error, gorm.ErrDuplicatedKey)
|
||||
|
||||
otherOwner := models.SenlinAgentProject{OwnerID: 2, Name: "Allowed", Identifier: "SHARED"}
|
||||
otherOwner := models.SaProject{OwnerID: 2, Name: "Allowed", Identifier: "SHARED"}
|
||||
require.NoError(t, database.Create(&otherOwner).Error)
|
||||
}
|
||||
|
||||
func TestProjectIdentifierDatabaseErrorMapsToConflict(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
first := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
first := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
duplicate := models.SenlinAgentProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
duplicate := models.SaProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
databaseErr := database.Create(&duplicate).Error
|
||||
require.ErrorIs(t, databaseErr, gorm.ErrDuplicatedKey)
|
||||
|
||||
@@ -113,26 +113,26 @@ func TestCreateProjectPersistsWorkspaceMetadata(t *testing.T) {
|
||||
func TestWorkspaceMatchesFrontendContract(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProject(1, "Project A1", "Client strategy workspace")
|
||||
require.NoError(t, err)
|
||||
other, err := service.CreateProject(1, "Project A2", "")
|
||||
require.NoError(t, err)
|
||||
nextRun := time.Date(2026, 7, 20, 9, 0, 0, 0, time.UTC)
|
||||
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTag{ProjectID: project.ID, Name: "UI"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTag{ProjectID: project.ID, Name: "knowledge"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn pasted research into tasks.", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentInboxItem{ProjectID: other.ID, CreatedBy: 1, SourceType: "Manual", Title: "Other notes", Body: "Hidden", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: 1, Title: "Confirm IA", Description: "Review channel layout.", Status: "open", DueAt: &nextRun}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: 1, Title: "Archive notes", Description: "Move outdated notes.", Status: "done"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI prototype critique", Context: "Discuss channel behavior."}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap-a1", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProjectChannel{ProjectID: other.ID, Title: "Other Board", Icon: "link", URL: "https://example.com/other", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly inbox review reminder", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, LastResult: "Not run yet"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentCronPlan{ProjectID: other.ID, CreatedBy: 1, Title: "Other cron", Schedule: "0 9 * * 2", Enabled: true}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTag{ProjectID: project.ID, Name: "UI"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTag{ProjectID: project.ID, Name: "knowledge"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn pasted research into tasks.", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaInboxItem{ProjectID: other.ID, CreatedBy: 1, SourceType: "Manual", Title: "Other notes", Body: "Hidden", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Confirm IA", Description: "Review channel layout.", Status: "open", DueAt: &nextRun}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Archive notes", Description: "Move outdated notes.", Status: "done"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI prototype critique", Context: "Discuss channel behavior."}).Error)
|
||||
require.NoError(t, database.Create(&models.SaNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap-a1", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: other.ID, Title: "Other Board", Icon: "link", URL: "https://example.com/other", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly inbox review reminder", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, LastResult: "Not run yet"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaCronPlan{ProjectID: other.ID, CreatedBy: 1, Title: "Other cron", Schedule: "0 9 * * 2", Enabled: true}).Error)
|
||||
|
||||
workspace, err := service.Workspace(1, project.Identity)
|
||||
|
||||
@@ -221,7 +221,7 @@ func TestCreateProjectTagConcurrentlyReturnsOneProjectScopedTag(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
var synchronizedQueries atomic.Int32
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:synchronize_tag_inserts", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SenlinAgentTag{}).TableName() {
|
||||
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SaTag{}).TableName() {
|
||||
return
|
||||
}
|
||||
if synchronizedQueries.Add(1) <= 2 {
|
||||
@@ -231,7 +231,7 @@ func TestCreateProjectTagConcurrentlyReturnsOneProjectScopedTag(t *testing.T) {
|
||||
}))
|
||||
|
||||
type result struct {
|
||||
tag *models.SenlinAgentTag
|
||||
tag *models.SaTag
|
||||
err error
|
||||
}
|
||||
results := make(chan result, 2)
|
||||
@@ -258,14 +258,14 @@ func TestCreateProjectTagConcurrentlyReturnsOneProjectScopedTag(t *testing.T) {
|
||||
}
|
||||
require.Len(t, identities, 1)
|
||||
var count int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentTag{}).Where("project_id = ? AND name = ?", project.ID, "UI").Count(&count).Error)
|
||||
require.NoError(t, database.Model(&models.SaTag{}).Where("project_id = ? AND name = ?", project.ID, "UI").Count(&count).Error)
|
||||
require.Equal(t, int64(1), count)
|
||||
}
|
||||
|
||||
func TestCreateCronPlanAddsPlanToOwnedProject(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
nextRun := time.Date(2026, 7, 22, 8, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -75,7 +75,7 @@ func writeTagError(c *gin.Context, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func ownedProjectFromRequest(c *gin.Context) (uint, *models.SenlinAgentProject, bool) {
|
||||
func ownedProjectFromRequest(c *gin.Context) (uint, *models.SaProject, bool) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
|
||||
|
||||
@@ -81,32 +81,32 @@ func (s *Service) Workspace(ownerID uint, projectIdentity string) (WorkspaceDTO,
|
||||
|
||||
func (s *Service) workspaceCounts(projectID uint) (workspaceCounts, error) {
|
||||
var counts workspaceCounts
|
||||
if err := models.DBService.Model(&models.SenlinAgentInboxItem{}).Where("project_id = ? AND status = ?", projectID, "open").Count(&counts.inbox).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaInboxItem{}).Where("project_id = ? AND status = ?", projectID, "open").Count(&counts.inbox).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
if err := models.DBService.Model(&models.SenlinAgentTask{}).Where("project_id = ? AND status <> ?", projectID, "done").Count(&counts.tasks).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaTask{}).Where("project_id = ? AND status <> ?", projectID, "done").Count(&counts.tasks).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
if err := models.DBService.Model(&models.SenlinAgentAISession{}).Where("project_id = ?", projectID).Count(&counts.aiSessions).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaAISession{}).Where("project_id = ?", projectID).Count(&counts.aiSessions).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
var noteCount int64
|
||||
if err := models.DBService.Model(&models.SenlinAgentNote{}).Where("project_id = ?", projectID).Count(¬eCount).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaNote{}).Where("project_id = ?", projectID).Count(¬eCount).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
var sourceCount int64
|
||||
if err := models.DBService.Model(&models.SenlinAgentSource{}).Where("project_id = ?", projectID).Count(&sourceCount).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaSource{}).Where("project_id = ?", projectID).Count(&sourceCount).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
counts.notesSources = noteCount + sourceCount
|
||||
if err := models.DBService.Model(&models.SenlinAgentCronPlan{}).Where("project_id = ?", projectID).Count(&counts.cronPlans).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaCronPlan{}).Where("project_id = ?", projectID).Count(&counts.cronPlans).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (s *Service) workspaceTags(projectID uint) ([]WorkspaceTagDTO, error) {
|
||||
var tags []models.SenlinAgentTag
|
||||
var tags []models.SaTag
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("name asc").Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (s *Service) workspaceChannels(projectID uint, projectIdentity string, coun
|
||||
{ID: projectIdentity + ":notes", ProjectID: projectIdentity, Type: "notes_sources", Title: "Notes & Sources", Icon: "file", Count: counts.notesSources, URL: "", SortOrder: 5},
|
||||
{ID: projectIdentity + ":cron", ProjectID: projectIdentity, Type: "cron", Title: "Cron Plans", Icon: "clock", Count: counts.cronPlans, URL: "", SortOrder: 6},
|
||||
}
|
||||
var customChannels []models.SenlinAgentProjectChannel
|
||||
var customChannels []models.SaProjectChannel
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("sort_order asc, id asc").Find(&customChannels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (s *Service) workspaceChannels(projectID uint, projectIdentity string, coun
|
||||
}
|
||||
|
||||
func (s *Service) workspaceInbox(projectID uint, projectIdentity string) ([]WorkspaceInboxDTO, error) {
|
||||
var records []models.SenlinAgentInboxItem
|
||||
var records []models.SaInboxItem
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (s *Service) workspaceInbox(projectID uint, projectIdentity string) ([]Work
|
||||
}
|
||||
|
||||
func (s *Service) workspaceTasks(projectID uint, projectIdentity string) ([]WorkspaceTaskDTO, error) {
|
||||
var tasks []models.SenlinAgentTask
|
||||
var tasks []models.SaTask
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("CASE WHEN status = 'done' THEN 1 ELSE 0 END asc").Order("sort_order asc, updated_at desc, id desc").Limit(50).Find(&tasks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func (s *Service) workspaceTasks(projectID uint, projectIdentity string) ([]Work
|
||||
}
|
||||
|
||||
// workspaceTaskTags 同时限定 tag 主键和项目主键,避免损坏数据把其他项目标签带入任务响应。
|
||||
func workspaceTaskTags(projectID uint, tasks []models.SenlinAgentTask) (map[uint]WorkspaceTagDTO, error) {
|
||||
func workspaceTaskTags(projectID uint, tasks []models.SaTask) (map[uint]WorkspaceTagDTO, error) {
|
||||
tagIDs := make([]uint, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
if task.TagID != nil {
|
||||
@@ -196,7 +196,7 @@ func workspaceTaskTags(projectID uint, tasks []models.SenlinAgentTask) (map[uint
|
||||
if len(tagIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var tags []models.SenlinAgentTag
|
||||
var tags []models.SaTag
|
||||
if err := models.DBService.Where("project_id = ? AND id IN ?", projectID, tagIDs).Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func workspaceTaskTags(projectID uint, tasks []models.SenlinAgentTask) (map[uint
|
||||
}
|
||||
|
||||
func (s *Service) workspaceAISessions(projectID uint, projectIdentity string) ([]WorkspaceAISessionDTO, error) {
|
||||
var sessions []models.SenlinAgentAISession
|
||||
var sessions []models.SaAISession
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&sessions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func (s *Service) workspaceAISessions(projectID uint, projectIdentity string) ([
|
||||
}
|
||||
|
||||
func (s *Service) workspaceNotesSources(projectID uint, projectIdentity string) ([]WorkspaceNoteSourceDTO, error) {
|
||||
var notes []models.SenlinAgentNote
|
||||
var notes []models.SaNote
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(¬es).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -242,7 +242,7 @@ func (s *Service) workspaceNotesSources(projectID uint, projectIdentity string)
|
||||
})
|
||||
}
|
||||
|
||||
var sources []models.SenlinAgentSource
|
||||
var sources []models.SaSource
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&sources).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func (s *Service) workspaceNotesSources(projectID uint, projectIdentity string)
|
||||
}
|
||||
|
||||
func (s *Service) workspaceCronPlans(projectID uint, projectIdentity string) ([]WorkspaceCronPlanDTO, error) {
|
||||
var plans []models.SenlinAgentCronPlan
|
||||
var plans []models.SaCronPlan
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("enabled desc, updated_at desc, id desc").Limit(50).Find(&plans).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func utcOptionalTime(value *time.Time) *time.Time {
|
||||
return &utc
|
||||
}
|
||||
|
||||
func sourceDisplayName(source models.SenlinAgentSource) string {
|
||||
func sourceDisplayName(source models.SaSource) string {
|
||||
switch source.Kind {
|
||||
case "file":
|
||||
return "Attachment"
|
||||
@@ -299,7 +299,7 @@ func userDisplayName(assigneeID *uint, createdBy uint) (string, error) {
|
||||
if assigneeID != nil && *assigneeID != 0 {
|
||||
userID = *assigneeID
|
||||
}
|
||||
var user models.SenlinAgentUser
|
||||
var user models.SaUser
|
||||
if err := models.DBService.Select("display_name").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "未知用户", nil
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProjectWithInput(1, CreateProjectRequest{
|
||||
Name: "Project A1", Identifier: "A1", Icon: "folder", Background: "#165DFF", Description: "Client strategy workspace",
|
||||
})
|
||||
@@ -21,8 +21,8 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
other, err := service.CreateProject(1, "Project A2", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
projectTag := models.SenlinAgentTag{ProjectID: project.ID, Name: "UI"}
|
||||
otherTag := models.SenlinAgentTag{ProjectID: other.ID, Name: "UI"}
|
||||
projectTag := models.SaTag{ProjectID: project.ID, Name: "UI"}
|
||||
otherTag := models.SaTag{ProjectID: other.ID, Name: "UI"}
|
||||
require.NoError(t, database.Create(&projectTag).Error)
|
||||
require.NoError(t, database.Create(&otherTag).Error)
|
||||
|
||||
@@ -30,13 +30,13 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
createdAt := time.Date(2026, 7, 20, 10, 30, 0, 0, localZone)
|
||||
updatedAt := createdAt.Add(90 * time.Minute)
|
||||
nextRun := time.Date(2026, 7, 21, 9, 0, 0, 0, localZone)
|
||||
inbox := models.SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn research into tasks.", Status: "open", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: 1, TagID: &projectTag.ID, Title: "Confirm IA", Description: "Review channel layout.", Status: "done", DueAt: &nextRun, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
session := models.SenlinAgentAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI critique", Context: "Discuss channel behavior.", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
note := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
source := models.SenlinAgentSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
channel := models.SenlinAgentProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap", SortOrder: 7, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
plan := models.SenlinAgentCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly review", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
inbox := models.SaInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn research into tasks.", Status: "open", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
task := models.SaTask{ProjectID: project.ID, CreatedBy: 1, TagID: &projectTag.ID, Title: "Confirm IA", Description: "Review channel layout.", Status: "done", DueAt: &nextRun, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
session := models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI critique", Context: "Discuss channel behavior.", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
note := models.SaNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
source := models.SaSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
channel := models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap", SortOrder: 7, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
plan := models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly review", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
for _, record := range []any{&inbox, &task, &session, ¬e, &source, &channel, &plan} {
|
||||
require.NoError(t, database.Create(record).Error)
|
||||
}
|
||||
@@ -103,12 +103,12 @@ func TestWorkspaceChannelDTOSerializesRequiredCountAndURL(t *testing.T) {
|
||||
func TestWorkspaceDoneTaskDoesNotInventCompletedAt(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
createdAt := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
|
||||
updatedAt := createdAt.Add(2 * time.Hour)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{
|
||||
require.NoError(t, database.Create(&models.SaTask{
|
||||
ProjectID: project.ID, CreatedBy: 1, Title: "Done", Status: "done", CreatedAt: createdAt, UpdatedAt: updatedAt,
|
||||
}).Error)
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
|
||||
func TestTagRegistrarUsesIdentityAndListsOnlyProjectTags(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
first := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
second := models.SenlinAgentProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
first := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
second := models.SaProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
require.NoError(t, database.Create(&second).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTag{ProjectID: second.ID, Name: "其他项目"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTag{ProjectID: second.ID, Name: "其他项目"}).Error)
|
||||
router := newProjectWriteHandlerRouter(t, 1, NewTagHandler(NewService()))
|
||||
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+first.Identity+"/tags", bytes.NewBufferString(`{"name":"设计"}`))
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
@@ -52,8 +52,8 @@ func TestTagRegistrarUsesIdentityAndListsOnlyProjectTags(t *testing.T) {
|
||||
|
||||
func TestTagRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
other := models.SenlinAgentProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
other := models.SaProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
|
||||
require.NoError(t, database.Create(&other).Error)
|
||||
router := newProjectWriteHandlerRouter(t, 1, NewTagHandler(NewService()))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+other.Identity+"/tags", bytes.NewBufferString(`{"name":"越权"}`))
|
||||
@@ -72,8 +72,8 @@ func TestTagRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
|
||||
|
||||
func TestTagAndCronServicesReturnTypedValidationErrors(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
service := NewService()
|
||||
|
||||
@@ -87,11 +87,11 @@ func TestTagAndCronServicesReturnTypedValidationErrors(t *testing.T) {
|
||||
|
||||
func TestTagRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_tag_create", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentTag{}).TableName() {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SaTag{}).TableName() {
|
||||
tx.AddError(errors.New("forced tag create failure"))
|
||||
}
|
||||
}))
|
||||
@@ -112,11 +112,11 @@ func TestTagRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
|
||||
func TestCronRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_cron_create", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentCronPlan{}).TableName() {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SaCronPlan{}).TableName() {
|
||||
tx.AddError(errors.New("forced cron create failure"))
|
||||
}
|
||||
}))
|
||||
@@ -137,8 +137,8 @@ func TestCronRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
|
||||
func TestCronRegistrarCreatesCamelCaseIdentityDTO(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
router := newProjectWriteHandlerRouter(t, 1, NewCronHandler(NewService()))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+project.Identity+"/cron-plans", bytes.NewBufferString(`{"title":"每日整理","schedule":"0 9 * * *","enabled":true,"nextRunAt":"2026-07-22T08:00:00Z"}`))
|
||||
@@ -149,7 +149,7 @@ func TestCronRegistrarCreatesCamelCaseIdentityDTO(t *testing.T) {
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String())
|
||||
var plan models.SenlinAgentCronPlan
|
||||
var plan models.SaCronPlan
|
||||
require.NoError(t, database.Where("project_id = ?", project.ID).First(&plan).Error)
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
|
||||
Reference in New Issue
Block a user