refactor: shorten model and table prefixes

This commit is contained in:
2026-07-23 14:30:17 +08:00
parent 6e7be2f3a4
commit 0ca2898ac0
71 changed files with 707 additions and 638 deletions

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentAICallLog struct {
type SaAICallLog struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
UserID uint `gorm:"index;not null"`
@@ -15,6 +15,6 @@ type SenlinAgentAICallLog struct {
CreatedAt time.Time
}
func (SenlinAgentAICallLog) TableName() string {
return "senlin_agent_ai_call_logs"
func (SaAICallLog) TableName() string {
return "sa_ai_call_logs"
}

View File

@@ -2,8 +2,8 @@ package models
import "time"
// SenlinAgentAIExpertCategory groups experts from the local expert catalog.
type SenlinAgentAIExpertCategory struct {
// SaAIExpertCategory groups experts from the local expert catalog.
type SaAIExpertCategory struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
Slug string `gorm:"size:80;uniqueIndex;not null"`
@@ -12,6 +12,6 @@ type SenlinAgentAIExpertCategory struct {
UpdatedAt time.Time
}
func (SenlinAgentAIExpertCategory) TableName() string {
return "senlin_agent_ai_expert_categories"
func (SaAIExpertCategory) TableName() string {
return "sa_ai_expert_categories"
}

View File

@@ -2,29 +2,29 @@ package models
import "time"
// SenlinAgentAIExpertItem 是本地专家库中的可选 AI 角色。
type SenlinAgentAIExpertItem struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
Slug string `gorm:"size:160;uniqueIndex;not null"`
CategoryID uint `gorm:"index"`
CategoryIdentity string `gorm:"type:char(36);index"`
ExpertCategory SenlinAgentAIExpertCategory `gorm:"foreignKey:CategoryID"`
Category string `gorm:"size:80;index;not null"`
CategoryName string `gorm:"size:120;not null"`
Name string `gorm:"size:160;index;not null"`
Description string `gorm:"type:text;not null"`
Emoji string `gorm:"size:32"`
Color string `gorm:"size:32"`
SystemPrompt string `gorm:"type:text;not null"`
Source string `gorm:"size:255;not null"`
Reference string `gorm:"size:255"`
SourceLicense string `gorm:"size:32;not null"`
Enabled bool `gorm:"not null;default:true;index"`
// SaAIExpertItem 是本地专家库中的可选 AI 角色。
type SaAIExpertItem struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
Slug string `gorm:"size:160;uniqueIndex;not null"`
CategoryID uint `gorm:"index"`
CategoryIdentity string `gorm:"type:char(36);index"`
ExpertCategory SaAIExpertCategory `gorm:"foreignKey:CategoryID"`
Category string `gorm:"size:80;index;not null"`
CategoryName string `gorm:"size:120;not null"`
Name string `gorm:"size:160;index;not null"`
Description string `gorm:"type:text;not null"`
Emoji string `gorm:"size:32"`
Color string `gorm:"size:32"`
SystemPrompt string `gorm:"type:text;not null"`
Source string `gorm:"size:255;not null"`
Reference string `gorm:"size:255"`
SourceLicense string `gorm:"size:32;not null"`
Enabled bool `gorm:"not null;default:true;index"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentAIExpertItem) TableName() string {
return "senlin_agent_ai_experts"
func (SaAIExpertItem) TableName() string {
return "sa_ai_experts"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentAIKey struct {
type SaAIKey struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
UserID uint `gorm:"uniqueIndex;not null"`
@@ -13,6 +13,6 @@ type SenlinAgentAIKey struct {
UpdatedAt time.Time
}
func (SenlinAgentAIKey) TableName() string {
return "senlin_agent_ai_keys"
func (SaAIKey) TableName() string {
return "sa_ai_keys"
}

View File

@@ -2,18 +2,18 @@ package models
import "time"
// SenlinAgentAIRateBucket 保存用户在固定窗口内已占用的 AI 请求配额。
// SaAIRateBucket 保存用户在固定窗口内已占用的 AI 请求配额。
// 复合唯一键让单条 UPSERT 在数据库层完成跨进程并发仲裁。
type SenlinAgentAIRateBucket struct {
type SaAIRateBucket struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"not null;uniqueIndex:uidx_senlin_agent_ai_rate_bucket,priority:1"`
Action string `gorm:"size:100;not null;uniqueIndex:uidx_senlin_agent_ai_rate_bucket,priority:2"`
WindowStart time.Time `gorm:"not null;uniqueIndex:uidx_senlin_agent_ai_rate_bucket,priority:3"`
UserID uint `gorm:"not null;uniqueIndex:uidx_sa_ai_rate_bucket,priority:1"`
Action string `gorm:"size:100;not null;uniqueIndex:uidx_sa_ai_rate_bucket,priority:2"`
WindowStart time.Time `gorm:"not null;uniqueIndex:uidx_sa_ai_rate_bucket,priority:3"`
Count int `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentAIRateBucket) TableName() string {
return "senlin_agent_ai_rate_buckets"
func (SaAIRateBucket) TableName() string {
return "sa_ai_rate_buckets"
}

View File

@@ -2,23 +2,23 @@ package models
import "time"
type SenlinAgentAISession struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
ExpertID *uint `gorm:"index"`
ExpertIdentity *string `gorm:"type:char(36);index"`
Expert *SenlinAgentAIExpertItem `gorm:"foreignKey:ExpertID"`
Title string `gorm:"not null"`
Context string `gorm:"type:text"`
Status string `gorm:"not null;default:ready"`
type SaAISession struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
ExpertID *uint `gorm:"index"`
ExpertIdentity *string `gorm:"type:char(36);index"`
Expert *SaAIExpertItem `gorm:"foreignKey:ExpertID"`
Title string `gorm:"not null"`
Context string `gorm:"type:text"`
Status string `gorm:"not null;default:ready"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentAISession) TableName() string {
return "senlin_agent_ai_sessions"
func (SaAISession) TableName() string {
return "sa_ai_sessions"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentCronPlan struct {
type SaCronPlan struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -18,6 +18,6 @@ type SenlinAgentCronPlan struct {
UpdatedAt time.Time
}
func (SenlinAgentCronPlan) TableName() string {
return "senlin_agent_cron_plans"
func (SaCronPlan) TableName() string {
return "sa_cron_plans"
}

View File

@@ -7,162 +7,162 @@ import (
"gorm.io/gorm"
)
func (m *SenlinAgentUser) BeforeCreate(tx *gorm.DB) error {
func (m *SaUser) BeforeCreate(tx *gorm.DB) error {
return ensureIdentity(&m.Identity)
}
func (m *SenlinAgentProject) BeforeCreate(tx *gorm.DB) error {
func (m *SaProject) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.OwnerID, &m.OwnerIdentity)
return resolveIdentity(tx, &SaUser{}, m.OwnerID, &m.OwnerIdentity)
}
func (m *SenlinAgentInboxItem) BeforeCreate(tx *gorm.DB) error {
func (m *SaInboxItem) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
return resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity)
}
func (m *SenlinAgentInboxSuggestion) BeforeCreate(tx *gorm.DB) error {
func (m *SaInboxSuggestion) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentInboxItem{}, m.InboxItemID, &m.InboxItemIdentity); err != nil {
if err := resolveIdentity(tx, &SaInboxItem{}, m.InboxItemID, &m.InboxItemIdentity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
return resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity)
}
func (m *SenlinAgentTask) BeforeCreate(tx *gorm.DB) error {
func (m *SaTask) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
return err
}
if err := resolveOptionalIdentity(tx, &SenlinAgentUser{}, m.AssigneeID, &m.AssigneeIdentity); err != nil {
if err := resolveOptionalIdentity(tx, &SaUser{}, m.AssigneeID, &m.AssigneeIdentity); err != nil {
return err
}
if err := resolveOptionalIdentity(tx, &SenlinAgentTag{}, m.TagID, &m.TagIdentity); err != nil {
if err := resolveOptionalIdentity(tx, &SaTag{}, m.TagID, &m.TagIdentity); err != nil {
return err
}
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
}
func (m *SenlinAgentNote) BeforeCreate(tx *gorm.DB) error {
func (m *SaNote) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
return err
}
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
}
func (m *SenlinAgentSource) BeforeCreate(tx *gorm.DB) error {
func (m *SaSource) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
return err
}
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
}
func (m *SenlinAgentAISession) BeforeCreate(tx *gorm.DB) error {
func (m *SaAISession) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
return err
}
return resolveOptionalIdentity(tx, &SenlinAgentAIExpertItem{}, m.ExpertID, &m.ExpertIdentity)
return resolveOptionalIdentity(tx, &SaAIExpertItem{}, m.ExpertID, &m.ExpertIdentity)
}
func (m *SenlinAgentAIExpertCategory) BeforeCreate(_ *gorm.DB) error {
func (m *SaAIExpertCategory) BeforeCreate(_ *gorm.DB) error {
return ensureIdentity(&m.Identity)
}
func (m *SenlinAgentAIExpertItem) BeforeCreate(tx *gorm.DB) error {
func (m *SaAIExpertItem) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentAIExpertCategory{}, m.CategoryID, &m.CategoryIdentity)
return resolveIdentity(tx, &SaAIExpertCategory{}, m.CategoryID, &m.CategoryIdentity)
}
func (m *SenlinAgentTag) BeforeCreate(tx *gorm.DB) error {
func (m *SaTag) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity)
return resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity)
}
func (m *SenlinAgentProjectChannel) BeforeCreate(tx *gorm.DB) error {
func (m *SaProjectChannel) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity)
return resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity)
}
func (m *SenlinAgentCronPlan) BeforeCreate(tx *gorm.DB) error {
func (m *SaCronPlan) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
return resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity)
}
func (m *SenlinAgentProjectEvent) BeforeCreate(tx *gorm.DB) error {
func (m *SaProjectEvent) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.ActorID, &m.ActorIdentity); err != nil {
if err := resolveIdentity(tx, &SaUser{}, m.ActorID, &m.ActorIdentity); err != nil {
return err
}
return resolveEntityIdentity(tx, m.EntityType, m.EntityID, &m.EntityIdentity)
}
func (m *SenlinAgentAIKey) BeforeCreate(tx *gorm.DB) error {
func (m *SaAIKey) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity)
return resolveIdentity(tx, &SaUser{}, m.UserID, &m.UserIdentity)
}
func (m *SenlinAgentAICallLog) BeforeCreate(tx *gorm.DB) error {
func (m *SaAICallLog) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity)
return resolveIdentity(tx, &SaUser{}, m.UserID, &m.UserIdentity)
}
func (m *SenlinAgentTaskShare) BeforeCreate(tx *gorm.DB) error {
func (m *SaTaskShare) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentTask{}, m.TaskID, &m.TaskIdentity); err != nil {
if err := resolveIdentity(tx, &SaTask{}, m.TaskID, &m.TaskIdentity); err != nil {
return err
}
return resolveEntityIdentity(tx, m.ObjectType, m.ObjectID, &m.ObjectIdentity)
@@ -210,19 +210,19 @@ func resolveEntityIdentity(tx *gorm.DB, entityType string, entityID uint, identi
}
switch entityType {
case "project":
return resolveIdentity(tx, &SenlinAgentProject{}, entityID, identity)
return resolveIdentity(tx, &SaProject{}, entityID, identity)
case "inbox", "inbox_item":
return resolveIdentity(tx, &SenlinAgentInboxItem{}, entityID, identity)
return resolveIdentity(tx, &SaInboxItem{}, entityID, identity)
case "task":
return resolveIdentity(tx, &SenlinAgentTask{}, entityID, identity)
return resolveIdentity(tx, &SaTask{}, entityID, identity)
case "note":
return resolveIdentity(tx, &SenlinAgentNote{}, entityID, identity)
return resolveIdentity(tx, &SaNote{}, entityID, identity)
case "source":
return resolveIdentity(tx, &SenlinAgentSource{}, entityID, identity)
return resolveIdentity(tx, &SaSource{}, entityID, identity)
case "ai_session":
return resolveIdentity(tx, &SenlinAgentAISession{}, entityID, identity)
return resolveIdentity(tx, &SaAISession{}, entityID, identity)
case "tag":
return resolveIdentity(tx, &SenlinAgentTag{}, entityID, identity)
return resolveIdentity(tx, &SaTag{}, entityID, identity)
default:
return nil
}

View File

@@ -11,7 +11,7 @@ import (
func TestBeforeCreateAssignsUUIDV7Identity(t *testing.T) {
database := newIdentityTestDB(t)
user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
user := SaUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
require.NoError(t, database.Create(&user).Error)
@@ -21,14 +21,14 @@ func TestBeforeCreateAssignsUUIDV7Identity(t *testing.T) {
func TestBeforeCreateCopiesParentIdentitiesFromNumericIDs(t *testing.T) {
database := newIdentityTestDB(t)
user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
user := SaUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
require.NoError(t, database.Create(&user).Error)
project := SenlinAgentProject{OwnerID: user.ID, Name: "Alpha"}
project := SaProject{OwnerID: user.ID, Name: "Alpha"}
require.NoError(t, database.Create(&project).Error)
item := SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: user.ID, SourceType: "text"}
item := SaInboxItem{ProjectID: project.ID, CreatedBy: user.ID, SourceType: "text"}
require.NoError(t, database.Create(&item).Error)
task := SenlinAgentTask{ProjectID: project.ID, CreatedBy: user.ID, SourceInboxItemID: &item.ID, Title: "Follow up"}
task := SaTask{ProjectID: project.ID, CreatedBy: user.ID, SourceInboxItemID: &item.ID, Title: "Follow up"}
require.NoError(t, database.Create(&task).Error)

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentInboxItem struct {
type SaInboxItem struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -18,6 +18,6 @@ type SenlinAgentInboxItem struct {
UpdatedAt time.Time
}
func (SenlinAgentInboxItem) TableName() string {
return "senlin_agent_inbox_items"
func (SaInboxItem) TableName() string {
return "sa_inbox_items"
}

View File

@@ -2,8 +2,8 @@ package models
import "time"
// SenlinAgentInboxSuggestion 保存分析阶段的候选草稿;它不是任务、笔记或资料,只有用户确认后才会创建正式对象。
type SenlinAgentInboxSuggestion struct {
// SaInboxSuggestion 保存分析阶段的候选草稿;它不是任务、笔记或资料,只有用户确认后才会创建正式对象。
type SaInboxSuggestion struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
InboxItemID uint `gorm:"index;not null"`
@@ -16,6 +16,6 @@ type SenlinAgentInboxSuggestion struct {
CreatedAt time.Time
}
func (SenlinAgentInboxSuggestion) TableName() string {
return "senlin_agent_inbox_suggestions"
func (SaInboxSuggestion) TableName() string {
return "sa_inbox_suggestions"
}

View File

@@ -0,0 +1,48 @@
package models
import (
"fmt"
"gorm.io/gorm"
)
var legacyTableRenames = []struct {
legacy string
current string
}{
{legacy: "senlin_agent_schema_migrations", current: "sa_schema_migrations"},
{legacy: "senlin_agent_users", current: "sa_users"},
{legacy: "senlin_agent_projects", current: "sa_projects"},
{legacy: "senlin_agent_inbox_items", current: "sa_inbox_items"},
{legacy: "senlin_agent_inbox_suggestions", current: "sa_inbox_suggestions"},
{legacy: "senlin_agent_tasks", current: "sa_tasks"},
{legacy: "senlin_agent_notes", current: "sa_notes"},
{legacy: "senlin_agent_sources", current: "sa_sources"},
{legacy: "senlin_agent_ai_expert_categories", current: "sa_ai_expert_categories"},
{legacy: "senlin_agent_ai_experts", current: "sa_ai_experts"},
{legacy: "senlin_agent_ai_sessions", current: "sa_ai_sessions"},
{legacy: "senlin_agent_tags", current: "sa_tags"},
{legacy: "senlin_agent_project_channels", current: "sa_project_channels"},
{legacy: "senlin_agent_cron_plans", current: "sa_cron_plans"},
{legacy: "senlin_agent_project_events", current: "sa_project_events"},
{legacy: "senlin_agent_ai_keys", current: "sa_ai_keys"},
{legacy: "senlin_agent_ai_call_logs", current: "sa_ai_call_logs"},
{legacy: "senlin_agent_ai_rate_buckets", current: "sa_ai_rate_buckets"},
{legacy: "senlin_agent_task_shares", current: "sa_task_shares"},
}
func renameLegacyTables(database *gorm.DB) error {
migrator := database.Migrator()
for _, table := range legacyTableRenames {
if !migrator.HasTable(table.legacy) {
continue
}
if migrator.HasTable(table.current) {
return fmt.Errorf("cannot rename legacy table %s: target table %s already exists", table.legacy, table.current)
}
if err := migrator.RenameTable(table.legacy, table.current); err != nil {
return fmt.Errorf("rename legacy table %s to %s: %w", table.legacy, table.current, err)
}
}
return nil
}

View File

@@ -9,16 +9,16 @@ import (
"gorm.io/gorm"
)
// SenlinAgentSchemaMigration 记录已完成的数据修复及审计摘要,确保升级可重入且可追溯。
type SenlinAgentSchemaMigration struct {
// SaSchemaMigration 记录已完成的数据修复及审计摘要,确保升级可重入且可追溯。
type SaSchemaMigration struct {
Version int `gorm:"primaryKey;autoIncrement:false"`
Name string `gorm:"not null"`
Details string `gorm:"type:text;not null"`
AppliedAt time.Time `gorm:"not null"`
}
func (SenlinAgentSchemaMigration) TableName() string {
return "senlin_agent_schema_migrations"
func (SaSchemaMigration) TableName() string {
return "sa_schema_migrations"
}
type versionedMigration struct {
@@ -38,7 +38,7 @@ func runVersionedMigrations(database *gorm.DB) error {
// version table are not inherited by the data-repair transaction.
migrationDatabase := database.Session(&gorm.Session{NewDB: true})
var applied int64
if err := migrationDatabase.Model(&SenlinAgentSchemaMigration{}).Where("version = ?", migration.version).Count(&applied).Error; err != nil {
if err := migrationDatabase.Model(&SaSchemaMigration{}).Where("version = ?", migration.version).Count(&applied).Error; err != nil {
return err
}
if applied > 0 {
@@ -53,7 +53,7 @@ func runVersionedMigrations(database *gorm.DB) error {
if err != nil {
return err
}
return tx.Create(&SenlinAgentSchemaMigration{
return tx.Create(&SaSchemaMigration{
Version: migration.version, Name: migration.name, Details: string(encoded), AppliedAt: time.Now().UTC(),
}).Error
}); err != nil {
@@ -64,8 +64,8 @@ func runVersionedMigrations(database *gorm.DB) error {
}
func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) {
var projects []SenlinAgentProject
if err := tx.Model(&SenlinAgentProject{}).Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil {
var projects []SaProject
if err := tx.Model(&SaProject{}).Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil {
return nil, err
}
@@ -102,12 +102,12 @@ func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) {
if candidate == project.Identifier {
continue
}
if err := tx.Model(&SenlinAgentProject{}).Where("id = ?", project.ID).Update("identifier", candidate).Error; err != nil {
if err := tx.Model(&SaProject{}).Where("id = ?", project.ID).Update("identifier", candidate).Error; err != nil {
return nil, err
}
updated++
}
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_projects_owner_identifier ON senlin_agent_projects (owner_id, identifier)`).Error; err != nil {
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_sa_projects_owner_identifier ON sa_projects (owner_id, identifier)`).Error; err != nil {
return nil, err
}
return map[string]int{"audited": len(projects), "updated": updated}, nil
@@ -124,11 +124,11 @@ func uniqueIdentifier(base string, reserved map[string]struct{}) string {
}
func deduplicateLegacyProjectTags(tx *gorm.DB) (map[string]int, error) {
var tags []SenlinAgentTag
if err := tx.Model(&SenlinAgentTag{}).Select("id", "identity", "project_id", "name").Order("project_id asc, name asc, id asc").Find(&tags).Error; err != nil {
var tags []SaTag
if err := tx.Model(&SaTag{}).Select("id", "identity", "project_id", "name").Order("project_id asc, name asc, id asc").Find(&tags).Error; err != nil {
return nil, err
}
keepers := make(map[string]SenlinAgentTag)
keepers := make(map[string]SaTag)
deduplicated := 0
for _, tag := range tags {
key := fmt.Sprintf("%d\x00%s", tag.ProjectID, tag.Name)
@@ -137,17 +137,17 @@ func deduplicateLegacyProjectTags(tx *gorm.DB) (map[string]int, error) {
keepers[key] = tag
continue
}
if err := tx.Model(&SenlinAgentTask{}).Where("tag_id = ?", tag.ID).Updates(map[string]any{
if err := tx.Model(&SaTask{}).Where("tag_id = ?", tag.ID).Updates(map[string]any{
"tag_id": keeper.ID, "tag_identity": keeper.Identity,
}).Error; err != nil {
return nil, err
}
if err := tx.Where("id = ?", tag.ID).Delete(&SenlinAgentTag{}).Error; err != nil {
if err := tx.Where("id = ?", tag.ID).Delete(&SaTag{}).Error; err != nil {
return nil, err
}
deduplicated++
}
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_tags_project_name ON senlin_agent_tags (project_id, name)`).Error; err != nil {
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_sa_tags_project_name ON sa_tags (project_id, name)`).Error; err != nil {
return nil, err
}
return map[string]int{"audited": len(tags), "deduplicated": deduplicated}, nil

View File

@@ -39,16 +39,22 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
// carry an initialized Gorm statement. Exercise that mode so model state from
// the migration-version lookup cannot leak into the data-repair transaction.
require.NoError(t, AutoMigrate(database.Session(&gorm.Session{Initialized: true})))
require.False(t, database.Migrator().HasTable("senlin_agent_projects"))
require.False(t, database.Migrator().HasTable("senlin_agent_tags"))
require.False(t, database.Migrator().HasTable("senlin_agent_tasks"))
require.True(t, database.Migrator().HasTable("sa_projects"))
require.True(t, database.Migrator().HasTable("sa_tags"))
require.True(t, database.Migrator().HasTable("sa_tasks"))
var projects []SenlinAgentProject
var projects []SaProject
require.NoError(t, database.Order("id asc").Find(&projects).Error)
require.Len(t, projects, 6, "项目迁移不得删除 legacy 行")
require.Equal(t, []string{"project-1", "project-2", "DUP", "DUP-4-2", "DUP-4", "DUP"}, projectIdentifiers(projects))
var tags []SenlinAgentTag
var tags []SaTag
require.NoError(t, database.Order("id asc").Find(&tags).Error)
require.Equal(t, []uint{10, 12}, tagIDs(tags), "同项目同名标签应保留最早记录,不影响其他项目")
var task SenlinAgentTask
var task SaTask
require.NoError(t, database.First(&task, 20).Error)
require.NotNil(t, task.TagID)
require.Equal(t, uint(10), *task.TagID)
@@ -56,21 +62,32 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
require.Equal(t, "tag-keeper", *task.TagIdentity)
var migrationCount int64
require.NoError(t, database.Table("senlin_agent_schema_migrations").Count(&migrationCount).Error)
require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error)
require.Equal(t, int64(2), migrationCount)
var auditDetails []string
require.NoError(t, database.Table("senlin_agent_schema_migrations").Order("version asc").Pluck("details", &auditDetails).Error)
require.NoError(t, database.Table("sa_schema_migrations").Order("version asc").Pluck("details", &auditDetails).Error)
require.Contains(t, auditDetails[0], `"updated":3`)
require.Contains(t, auditDetails[1], `"deduplicated":1`)
require.NoError(t, AutoMigrate(database), "versioned migrations must be safe to run again")
require.NoError(t, database.Table("senlin_agent_schema_migrations").Count(&migrationCount).Error)
require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error)
require.Equal(t, int64(2), migrationCount)
require.Error(t, database.Exec(`INSERT INTO senlin_agent_projects (owner_id, name, identifier) VALUES (7, 'still duplicate', 'DUP')`).Error)
require.Error(t, database.Exec(`INSERT INTO senlin_agent_tags (project_id, name) VALUES (1, 'UI')`).Error)
require.Error(t, database.Exec(`INSERT INTO sa_projects (owner_id, name, identifier) VALUES (7, 'still duplicate', 'DUP')`).Error)
require.Error(t, database.Exec(`INSERT INTO sa_tags (project_id, name) VALUES (1, 'UI')`).Error)
}
func projectIdentifiers(projects []SenlinAgentProject) []string {
func TestAutoMigrateRejectsAmbiguousLegacyAndCurrentTables(t *testing.T) {
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, database.Exec(`CREATE TABLE senlin_agent_users (id integer primary key)`).Error)
require.NoError(t, database.Exec(`CREATE TABLE sa_users (id integer primary key)`).Error)
err = AutoMigrate(database)
require.EqualError(t, err, "cannot rename legacy table senlin_agent_users: target table sa_users already exists")
}
func projectIdentifiers(projects []SaProject) []string {
result := make([]string, 0, len(projects))
for _, project := range projects {
result = append(result, project.Identifier)
@@ -78,7 +95,7 @@ func projectIdentifiers(projects []SenlinAgentProject) []string {
return result
}
func tagIDs(tags []SenlinAgentTag) []uint {
func tagIDs(tags []SaTag) []uint {
result := make([]uint, 0, len(tags))
for _, tag := range tags {
result = append(result, tag.ID)

View File

@@ -23,28 +23,32 @@ func New(dsn string) error {
}
func AutoMigrate(database *gorm.DB) error {
if err := database.AutoMigrate(
&SenlinAgentSchemaMigration{},
&SenlinAgentUser{},
&SenlinAgentProject{},
&SenlinAgentInboxItem{},
&SenlinAgentInboxSuggestion{},
&SenlinAgentTask{},
&SenlinAgentNote{},
&SenlinAgentSource{},
&SenlinAgentAIExpertCategory{},
&SenlinAgentAIExpertItem{},
&SenlinAgentAISession{},
&SenlinAgentTag{},
&SenlinAgentProjectChannel{},
&SenlinAgentCronPlan{},
&SenlinAgentProjectEvent{},
&SenlinAgentAIKey{},
&SenlinAgentAICallLog{},
&SenlinAgentAIRateBucket{},
&SenlinAgentTaskShare{},
cleanDatabase := database.Session(&gorm.Session{NewDB: true})
if err := cleanDatabase.Transaction(renameLegacyTables); err != nil {
return err
}
if err := cleanDatabase.AutoMigrate(
&SaSchemaMigration{},
&SaUser{},
&SaProject{},
&SaInboxItem{},
&SaInboxSuggestion{},
&SaTask{},
&SaNote{},
&SaSource{},
&SaAIExpertCategory{},
&SaAIExpertItem{},
&SaAISession{},
&SaTag{},
&SaProjectChannel{},
&SaCronPlan{},
&SaProjectEvent{},
&SaAIKey{},
&SaAICallLog{},
&SaAIRateBucket{},
&SaTaskShare{},
); err != nil {
return err
}
return runVersionedMigrations(database)
return runVersionedMigrations(cleanDatabase)
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentNote struct {
type SaNote struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -17,6 +17,6 @@ type SenlinAgentNote struct {
UpdatedAt time.Time
}
func (SenlinAgentNote) TableName() string {
return "senlin_agent_notes"
func (SaNote) TableName() string {
return "sa_notes"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentProject struct {
type SaProject struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
OwnerID uint `gorm:"index;not null"`
@@ -16,6 +16,6 @@ type SenlinAgentProject struct {
UpdatedAt time.Time
}
func (SenlinAgentProject) TableName() string {
return "senlin_agent_projects"
func (SaProject) TableName() string {
return "sa_projects"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentProjectChannel struct {
type SaProjectChannel struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -15,6 +15,6 @@ type SenlinAgentProjectChannel struct {
UpdatedAt time.Time
}
func (SenlinAgentProjectChannel) TableName() string {
return "senlin_agent_project_channels"
func (SaProjectChannel) TableName() string {
return "sa_project_channels"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentProjectEvent struct {
type SaProjectEvent struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -17,6 +17,6 @@ type SenlinAgentProjectEvent struct {
CreatedAt time.Time
}
func (SenlinAgentProjectEvent) TableName() string {
return "senlin_agent_project_events"
func (SaProjectEvent) TableName() string {
return "sa_project_events"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentSource struct {
type SaSource struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -20,6 +20,6 @@ type SenlinAgentSource struct {
UpdatedAt time.Time
}
func (SenlinAgentSource) TableName() string {
return "senlin_agent_sources"
func (SaSource) TableName() string {
return "sa_sources"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentTag struct {
type SaTag struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -11,6 +11,6 @@ type SenlinAgentTag struct {
CreatedAt time.Time
}
func (SenlinAgentTag) TableName() string {
return "senlin_agent_tags"
func (SaTag) TableName() string {
return "sa_tags"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentTask struct {
type SaTask struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
@@ -24,6 +24,6 @@ type SenlinAgentTask struct {
UpdatedAt time.Time
}
func (SenlinAgentTask) TableName() string {
return "senlin_agent_tasks"
func (SaTask) TableName() string {
return "sa_tasks"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentTaskShare struct {
type SaTaskShare struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
TaskID uint `gorm:"index;not null"`
@@ -13,6 +13,6 @@ type SenlinAgentTaskShare struct {
CreatedAt time.Time
}
func (SenlinAgentTaskShare) TableName() string {
return "senlin_agent_task_shares"
func (SaTaskShare) TableName() string {
return "sa_task_shares"
}

View File

@@ -2,7 +2,7 @@ package models
import "time"
type SenlinAgentUser struct {
type SaUser struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
Email string `gorm:"uniqueIndex;not null"`
@@ -13,6 +13,6 @@ type SenlinAgentUser struct {
UpdatedAt time.Time
}
func (SenlinAgentUser) TableName() string {
return "senlin_agent_users"
func (SaUser) TableName() string {
return "sa_users"
}