package seed import ( "strings" "time" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "gorm.io/gorm/clause" "senlinai-agent/backend/internal/models" ) type DemoOptions struct { Email string DisplayName string Password string } type DemoResult struct { User models.SenlinAgentUser Projects []models.SenlinAgentProject } func Demo(database *gorm.DB, options DemoOptions) (DemoResult, error) { email := strings.ToLower(strings.TrimSpace(options.Email)) if email == "" { email = "demo@senlin.ai" } displayName := strings.TrimSpace(options.DisplayName) if displayName == "" { displayName = "演示用户" } password := options.Password if password == "" { password = "password123" } var result DemoResult err := database.Transaction(func(tx *gorm.DB) error { user, err := upsertUser(tx, email, displayName, password) if err != nil { return err } result.User = user projects, err := seedProjects(tx, user.ID) if err != nil { return err } result.Projects = projects return nil }) return result, err } func upsertUser(tx *gorm.DB, email string, displayName string, password string) (models.SenlinAgentUser, error) { var user models.SenlinAgentUser err := tx.Where("email = ?", email).First(&user).Error if err == nil { return user, nil } if err != gorm.ErrRecordNotFound { return user, err } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return user, err } user = models.SenlinAgentUser{ Email: email, DisplayName: displayName, PasswordHash: string(hash), Role: "user", } return user, tx.Create(&user).Error } func seedProjects(tx *gorm.DB, ownerID uint) ([]models.SenlinAgentProject, error) { definitions := []struct { Name string Identifier string Description string }{ {Name: "项目 A1", Identifier: "A1", Description: "企业智能协作平台的核心工作空间"}, {Name: "项目 A2", Identifier: "A2", Description: "客户成功与交付跟踪"}, {Name: "数据中台", Identifier: "DATA", Description: "内部数据治理与指标分析"}, {Name: "运营自动化", Identifier: "AUTO", Description: "定时任务和业务流程自动化"}, } projects := make([]models.SenlinAgentProject, 0, len(definitions)) for _, definition := range definitions { project, err := firstOrCreateProject(tx, ownerID, definition.Name, definition.Identifier, definition.Description) if err != nil { return nil, err } projects = append(projects, project) } if len(projects) == 0 { return projects, nil } if err := seedProjectA1(tx, ownerID, projects[0].ID); err != nil { return nil, err } for _, project := range projects[1:] { if err := seedCompactProject(tx, ownerID, project.ID); err != nil { return nil, err } } return projects, nil } func firstOrCreateProject(tx *gorm.DB, ownerID uint, name string, identifier string, description string) (models.SenlinAgentProject, error) { var project models.SenlinAgentProject err := tx.Where("owner_id = ? AND name = ?", ownerID, name).First(&project).Error if err == nil { return project, nil } if err != gorm.ErrRecordNotFound { return project, err } project = models.SenlinAgentProject{OwnerID: ownerID, Name: name, Identifier: identifier, Description: description} return project, tx.Create(&project).Error } func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error { now := time.Now().UTC() nextRun := now.Add(6 * time.Hour) dueSoon := now.Add(72 * time.Hour) dueLater := now.Add(7 * 24 * time.Hour) for _, tag := range []string{"UI", "知识碎片", "客户反馈", "自动化"} { if err := firstOrCreateTag(tx, projectID, tag); err != nil { return err } } uiTagID, err := tagID(tx, projectID, "UI") if err != nil { return err } feedbackTagID, err := tagID(tx, projectID, "客户反馈") if err != nil { return err } automationTagID, err := tagID(tx, projectID, "自动化") if err != nil { return err } for _, item := range []models.SenlinAgentInboxItem{ {ProjectID: projectID, CreatedBy: userID, SourceType: "产品研究", Title: "竞争对手定价更新监控", Body: "跟进企业版价格调整和功能组合变化。", Status: "open"}, {ProjectID: projectID, CreatedBy: userID, SourceType: "客户支持", Title: "客户反馈:导出功能报错", Body: "客户在导出智能报表时遇到权限校验异常。", Status: "open"}, {ProjectID: projectID, CreatedBy: userID, SourceType: "架构讨论", Title: "API 调用策略评估建议", Body: "评估批量任务调度和 AI 限流策略。", Status: "open"}, {ProjectID: projectID, CreatedBy: userID, SourceType: "需求文档", Title: "更新需求文档 v1.3.2", Body: "补充项目频道和属性弹层的交互说明。", Status: "open"}, } { if err := firstOrCreateInbox(tx, item); err != nil { return err } } for _, task := range []models.SenlinAgentTask{ {ProjectID: projectID, CreatedBy: userID, TagID: &uiTagID, Title: "智能报表导出功能", Description: "修复权限校验并补充导出失败提示。", Status: "open", DueAt: &dueSoon, SortOrder: 1}, {ProjectID: projectID, CreatedBy: userID, TagID: &automationTagID, Title: "企业版权限体系梳理", Description: "整理角色、项目和任务分享边界。", Status: "open", DueAt: &dueLater, SortOrder: 2}, {ProjectID: projectID, CreatedBy: userID, TagID: &feedbackTagID, Title: "Q3 营销策略制定", Description: "汇总竞品情报和客户反馈,生成策略草案。", Status: "open", DueAt: &dueLater, SortOrder: 3}, } { if err := firstOrCreateTask(tx, task); err != nil { return err } } for _, session := range []models.SenlinAgentAISession{ {ProjectID: projectID, CreatedBy: userID, Title: "生成 Q3 营销策略初稿", Context: "基于市场分析和竞品数据,输出 Q3 营销策略重点。"}, {ProjectID: projectID, CreatedBy: userID, Title: "产品定价模型讨论", Context: "对企业版与标准版定价模型进行对比分析。"}, {ProjectID: projectID, CreatedBy: userID, Title: "客户流失原因分析", Context: "分析近三个月客户流失数据与反馈原因。"}, {ProjectID: projectID, CreatedBy: userID, Title: "行业趋势与机会洞察", Context: "围绕 AI 在企业协同领域的趋势与机会。"}, } { if err := firstOrCreateAISession(tx, session); err != nil { return err } } for _, note := range []models.SenlinAgentNote{ {ProjectID: projectID, CreatedBy: userID, Title: "用户权限体系设计文档 v2.1", Markdown: "# 权限体系\n\n围绕项目、任务和资料分享做保守授权。"}, {ProjectID: projectID, CreatedBy: userID, Title: "导出功能技术方案评审", Markdown: "# 导出功能\n\n记录权限校验和任务队列方案。"}, } { if err := firstOrCreateNote(tx, note); err != nil { return err } } for _, source := range []models.SenlinAgentSource{ {ProjectID: projectID, CreatedBy: userID, Kind: "file", Title: "2026 Q3 竞品功能对比表", FilePath: "projects/a1/competitors.xlsx", ContentText: "竞品功能矩阵和价格摘要"}, {ProjectID: projectID, CreatedBy: userID, Kind: "link", Title: "客户访谈记录索引", URL: "https://example.com/interviews", ContentText: "客户访谈和问题归类"}, } { if err := firstOrCreateSource(tx, source); err != nil { return err } } for _, plan := range []models.SenlinAgentCronPlan{ {ProjectID: projectID, CreatedBy: userID, Title: "每日 Inbox 摘要", Schedule: "0 9 * * *", NextRunAt: &nextRun, Enabled: true}, {ProjectID: projectID, CreatedBy: userID, Title: "每周任务风险扫描", Schedule: "0 10 * * 1", NextRunAt: &dueSoon, Enabled: true}, {ProjectID: projectID, CreatedBy: userID, Title: "月底知识归档提醒", Schedule: "0 18 28 * *", NextRunAt: &dueLater, Enabled: false}, } { if err := firstOrCreateCronPlan(tx, plan); err != nil { return err } } for _, channel := range []models.SenlinAgentProjectChannel{ {ProjectID: projectID, Title: "客户研究频道", Icon: "user", URL: "https://example.com/research", SortOrder: 7}, {ProjectID: projectID, Title: "产品设计频道", Icon: "link", URL: "https://example.com/design", SortOrder: 8}, } { if err := firstOrCreateChannel(tx, channel); err != nil { return err } } return nil } func seedCompactProject(tx *gorm.DB, userID uint, projectID uint) error { item := models.SenlinAgentInboxItem{ProjectID: projectID, CreatedBy: userID, SourceType: "手动收集", Title: "待处理事项", Body: "项目启动资料待整理。", Status: "open"} if err := firstOrCreateInbox(tx, item); err != nil { return err } task := models.SenlinAgentTask{ProjectID: projectID, CreatedBy: userID, Title: "项目例会纪要整理", Description: "归档本周同步内容。", Status: "open"} return firstOrCreateTask(tx, task) } func firstOrCreateTag(tx *gorm.DB, projectID uint, name string) error { candidate := models.SenlinAgentTag{ProjectID: projectID, Name: name} if err := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "project_id"}, {Name: "name"}}, DoNothing: true, }).Create(&candidate).Error; err != nil { return err } return tx.Where("project_id = ? AND name = ?", projectID, name).First(&models.SenlinAgentTag{}).Error } func tagID(tx *gorm.DB, projectID uint, name string) (uint, error) { var tag models.SenlinAgentTag if err := tx.Where("project_id = ? AND name = ?", projectID, name).First(&tag).Error; err != nil { return 0, err } return tag.ID, nil } func firstOrCreateInbox(tx *gorm.DB, item models.SenlinAgentInboxItem) error { return firstOrCreate(tx, &models.SenlinAgentInboxItem{}, "project_id = ? AND title = ?", []any{item.ProjectID, item.Title}, item) } func firstOrCreateTask(tx *gorm.DB, task models.SenlinAgentTask) error { var existing models.SenlinAgentTask err := tx.Where("project_id = ? AND title = ?", task.ProjectID, task.Title).First(&existing).Error if err == nil { if task.TagID != nil && existing.TagID == nil { return tx.Model(&existing).Update("tag_id", *task.TagID).Error } return nil } if err != gorm.ErrRecordNotFound { return err } return tx.Create(&task).Error } func firstOrCreateAISession(tx *gorm.DB, session models.SenlinAgentAISession) error { return firstOrCreate(tx, &models.SenlinAgentAISession{}, "project_id = ? AND title = ?", []any{session.ProjectID, session.Title}, session) } func firstOrCreateNote(tx *gorm.DB, note models.SenlinAgentNote) error { return firstOrCreate(tx, &models.SenlinAgentNote{}, "project_id = ? AND title = ?", []any{note.ProjectID, note.Title}, note) } func firstOrCreateSource(tx *gorm.DB, source models.SenlinAgentSource) error { return firstOrCreate(tx, &models.SenlinAgentSource{}, "project_id = ? AND title = ?", []any{source.ProjectID, source.Title}, source) } func firstOrCreateCronPlan(tx *gorm.DB, plan models.SenlinAgentCronPlan) error { return firstOrCreate(tx, &models.SenlinAgentCronPlan{}, "project_id = ? AND title = ?", []any{plan.ProjectID, plan.Title}, plan) } func firstOrCreateChannel(tx *gorm.DB, channel models.SenlinAgentProjectChannel) error { return firstOrCreate(tx, &models.SenlinAgentProjectChannel{}, "project_id = ? AND title = ?", []any{channel.ProjectID, channel.Title}, channel) } func firstOrCreate[T any](tx *gorm.DB, model *T, where string, args []any, create T) error { err := tx.Where(where, args...).First(model).Error if err == nil { return nil } if err != gorm.ErrRecordNotFound { return err } return tx.Create(&create).Error }