Files
agent/backend/internal/seed/demo.go

298 lines
11 KiB
Go

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.SaUser
Projects []models.SaProject
}
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.SaUser, error) {
var user models.SaUser
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.SaUser{
Email: email,
DisplayName: displayName,
PasswordHash: string(hash),
Role: "user",
}
return user, tx.Create(&user).Error
}
func seedProjects(tx *gorm.DB, ownerID uint) ([]models.SaProject, 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.SaProject, 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.SaProject, error) {
var project models.SaProject
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.SaProject{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.SaInboxItem{
{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.SaTask{
{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.SaAISession{
{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 _, document := range []struct {
name string
markdown string
}{
{name: "用户权限体系设计文档 v2.1.md", markdown: "# 权限体系\n\n围绕项目、任务和资料分享做保守授权。"},
{name: "导出功能技术方案评审.md", markdown: "# 导出功能\n\n记录权限校验和任务队列方案。"},
} {
if err := firstOrCreateDocument(tx, projectID, userID, document.name, document.markdown); err != nil {
return err
}
}
for _, plan := range []models.SaCronPlan{
{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.SaProjectChannel{
{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.SaInboxItem{ProjectID: projectID, CreatedBy: userID, SourceType: "手动收集", Title: "待处理事项", Body: "项目启动资料待整理。", Status: "open"}
if err := firstOrCreateInbox(tx, item); err != nil {
return err
}
task := models.SaTask{ProjectID: projectID, CreatedBy: userID, Title: "项目例会纪要整理", Description: "归档本周同步内容。", Status: "open"}
return firstOrCreateTask(tx, task)
}
func firstOrCreateTag(tx *gorm.DB, projectID uint, name string) error {
candidate := models.SaTag{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.SaTag{}).Error
}
func tagID(tx *gorm.DB, projectID uint, name string) (uint, error) {
var tag models.SaTag
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.SaInboxItem) error {
return firstOrCreate(tx, &models.SaInboxItem{}, "project_id = ? AND title = ?", []any{item.ProjectID, item.Title}, item)
}
func firstOrCreateTask(tx *gorm.DB, task models.SaTask) error {
var existing models.SaTask
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.SaAISession) error {
return firstOrCreate(tx, &models.SaAISession{}, "project_id = ? AND title = ?", []any{session.ProjectID, session.Title}, session)
}
func firstOrCreateDocument(tx *gorm.DB, projectID, userID uint, name, markdown string) error {
var document models.SaDocumentTree
err := tx.Where("project_id = ? AND normalized_name = ?", projectID, strings.ToLower(name)).First(&document).Error
if err == nil {
return nil
}
if err != gorm.ErrRecordNotFound {
return err
}
document = models.SaDocumentTree{
ProjectID: projectID, OwnerID: userID, Kind: models.DocumentKindFile,
Name: name, NormalizedName: strings.ToLower(name), Extension: ".md",
MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(markdown))),
Revision: 1, ScanStatus: "not_scanned",
}
if err := tx.Create(&document).Error; err != nil {
return err
}
return tx.Create(&models.SaDocumentContent{DocumentID: document.ID, Markdown: markdown}).Error
}
func firstOrCreateCronPlan(tx *gorm.DB, plan models.SaCronPlan) error {
return firstOrCreate(tx, &models.SaCronPlan{}, "project_id = ? AND title = ?", []any{plan.ProjectID, plan.Title}, plan)
}
func firstOrCreateChannel(tx *gorm.DB, channel models.SaProjectChannel) error {
return firstOrCreate(tx, &models.SaProjectChannel{}, "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
}