mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-12 18:50:58 +00:00
9ccff4efbc
- Sync backend and frontend from GeekAI Plus v4.3.0 - Remove commercial License flows and update open-source deployment defaults - Preserve Docker Compose deployment and bump image tags to v4.3.0 BREAKING CHANGE: commercial License configuration and related endpoints are removed
706 lines
23 KiB
Go
706 lines
23 KiB
Go
package service
|
||
|
||
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||
// Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||
// Use of this source code is governed by a Apache-2.0 license
|
||
// that can be found in the LICENSE file.
|
||
// @Author yangjian102621@163.com
|
||
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"geekai/core/types"
|
||
"geekai/store/model"
|
||
"strings"
|
||
|
||
"github.com/go-redis/redis/v8"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
const (
|
||
// 迁移状态Redis key
|
||
MigrationStatusKey = "config_migration:status"
|
||
// 迁移完成标志
|
||
MigrationCompleted = "completed"
|
||
)
|
||
|
||
// MigrationService 配置迁移服务
|
||
type MigrationService struct {
|
||
db *gorm.DB
|
||
redisClient *redis.Client
|
||
appConfig *types.AppConfig
|
||
}
|
||
|
||
func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig) *MigrationService {
|
||
return &MigrationService{
|
||
db: db,
|
||
redisClient: redisClient,
|
||
appConfig: appConfig,
|
||
}
|
||
}
|
||
|
||
func (s *MigrationService) StartMigrate() {
|
||
// 表结构迁移必须在业务服务启动前完成,避免新表和新列尚未创建就被后台任务查询。
|
||
s.TableMigration()
|
||
go func() {
|
||
s.MigrateConfig(s.appConfig)
|
||
}()
|
||
}
|
||
|
||
// 迁移配置内容
|
||
func (s *MigrationService) MigrateConfigContent() error {
|
||
// 用户协议
|
||
if err := s.saveConfig(types.ConfigKeyPrivacy, map[string]string{
|
||
"content": "用户协议内容",
|
||
}); err != nil {
|
||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||
}
|
||
// 隐私政策
|
||
if err := s.saveConfig(types.ConfigKeyAgreement, map[string]string{
|
||
"content": "隐私政策内容",
|
||
}); err != nil {
|
||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||
}
|
||
// 思维导图
|
||
if err := s.saveConfig(types.ConfigKeyMarkMap, map[string]string{
|
||
"content": `# GeekAI 演示站
|
||
|
||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||
- 基于 Websocket 实现,完美的打字机体验。
|
||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
|
||
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。`,
|
||
}); err != nil {
|
||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||
}
|
||
|
||
// 微信登录配置
|
||
if err := s.saveConfig(types.ConfigKeyWxLogin, map[string]string{
|
||
"api_key": "",
|
||
"notify_url": "",
|
||
"enabled": "false",
|
||
}); err != nil {
|
||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||
}
|
||
|
||
// 验证码配置
|
||
if err := s.saveConfig(types.ConfigKeyCaptcha, map[string]string{
|
||
"api_key": "",
|
||
"type": "dot",
|
||
"enabled": "false",
|
||
}); err != nil {
|
||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||
}
|
||
|
||
// 文本审核
|
||
if err := s.saveConfig(types.ConfigKeyModeration, map[string]any{
|
||
"enable": "false",
|
||
"active": "gitee",
|
||
"enable_guide": "false",
|
||
"guide_prompt": "",
|
||
"gitee": map[string]string{
|
||
"api_key": "",
|
||
"model": "Security-semantic-filtering",
|
||
},
|
||
"baidu": map[string]string{
|
||
"access_key": "",
|
||
"secret_key": "",
|
||
},
|
||
"tencent": map[string]string{
|
||
"access_key": "",
|
||
"secret_key": "",
|
||
},
|
||
}); err != nil {
|
||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// fullTableMigration 第一步:全量表迁移,同步所有表结构(新增表、新增字段、字段类型)
|
||
// 适用于首次安装或导入旧版数据库后同步 schema,AutoMigrate 会补齐缺失的表和列
|
||
func (s *MigrationService) fullTableMigration() {
|
||
logger.Info("执行全量表迁移(同步 schema)...")
|
||
models := []any{
|
||
&model.Config{},
|
||
&model.AdminUser{},
|
||
&model.ChatApp{},
|
||
&model.ApiKey{},
|
||
&model.AppType{},
|
||
&model.ChatModel{},
|
||
&model.User{},
|
||
&model.ChatItem{},
|
||
&model.ChatMessage{},
|
||
&model.Order{},
|
||
&model.Product{},
|
||
&model.Function{},
|
||
&model.Menu{},
|
||
&model.InviteCode{},
|
||
&model.InviteLog{},
|
||
&model.Redeem{},
|
||
&model.PowerLog{},
|
||
&model.File{},
|
||
&model.UserLoginLog{},
|
||
&model.MidJourneyJob{},
|
||
&model.SunoJob{},
|
||
&model.VideoJob{},
|
||
&model.JimengJob{},
|
||
&model.PPTJob{},
|
||
&model.Moderation{},
|
||
&model.ImageJob{},
|
||
}
|
||
if err := s.db.AutoMigrate(models...); err != nil {
|
||
logger.Errorf("全量表迁移失败: %v", err)
|
||
return
|
||
}
|
||
logger.Info("全量表迁移完成")
|
||
}
|
||
|
||
// fixTableConstraints 第一步之后:根据模型定义修复各表的主键、自增和关键索引
|
||
// 主要解决初始化 SQL 中缺少 AUTO_INCREMENT 或 PRIMARY KEY 导致插入失败的问题
|
||
func (s *MigrationService) fixTableConstraints() {
|
||
logger.Info("开始修复各表的主键、自增属性和索引...")
|
||
|
||
// 当前数据库名
|
||
var dbName string
|
||
if err := s.db.Raw("SELECT DATABASE()").Scan(&dbName).Error; err != nil {
|
||
logger.Errorf("获取当前数据库名失败: %v", err)
|
||
return
|
||
}
|
||
if dbName == "" {
|
||
logger.Warn("当前连接未选择数据库,跳过约束修复")
|
||
return
|
||
}
|
||
|
||
// === 修复所有包含 id 字段的表的主键 + 自增 ===
|
||
type columnInfo struct {
|
||
TableName string `gorm:"column:TABLE_NAME"`
|
||
ColumnName string `gorm:"column:COLUMN_NAME"`
|
||
ColumnKey string `gorm:"column:COLUMN_KEY"`
|
||
Extra string `gorm:"column:EXTRA"`
|
||
DataType string `gorm:"column:DATA_TYPE"`
|
||
}
|
||
|
||
var idColumns []columnInfo
|
||
if err := s.db.Raw(`
|
||
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_KEY, EXTRA, DATA_TYPE
|
||
FROM INFORMATION_SCHEMA.COLUMNS
|
||
WHERE TABLE_SCHEMA = ? AND COLUMN_NAME = 'id'
|
||
`, dbName).Scan(&idColumns).Error; err != nil {
|
||
logger.Errorf("查询各表 id 字段信息失败: %v", err)
|
||
} else {
|
||
for _, col := range idColumns {
|
||
if col.ColumnName == "" {
|
||
continue
|
||
}
|
||
|
||
// 检查当前表是否已经存在主键
|
||
type pkInfo struct {
|
||
ColumnName string `gorm:"column:COLUMN_NAME"`
|
||
}
|
||
var pkColumns []pkInfo
|
||
if err := s.db.Raw(`
|
||
SELECT COLUMN_NAME
|
||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'
|
||
`, dbName, col.TableName).Scan(&pkColumns).Error; err != nil {
|
||
logger.Errorf("查询表 %s 的主键信息失败: %v", col.TableName, err)
|
||
continue
|
||
}
|
||
|
||
hasPK := len(pkColumns) > 0
|
||
pkOnIdOnly := hasPK && len(pkColumns) == 1 && pkColumns[0].ColumnName == "id"
|
||
|
||
needAlter := false
|
||
switch {
|
||
case !hasPK:
|
||
// 没有任何主键:允许将 id 设置为自增主键
|
||
if col.ColumnKey != "PRI" || col.Extra == "" || !strings.Contains(col.Extra, "auto_increment") {
|
||
needAlter = true
|
||
}
|
||
case pkOnIdOnly:
|
||
// 只有 id 作为主键:只补充自增属性
|
||
if col.Extra == "" || !strings.Contains(col.Extra, "auto_increment") {
|
||
needAlter = true
|
||
}
|
||
default:
|
||
// 已存在非 id 或组合主键:避免破坏原有主键,直接跳过
|
||
logger.Infof("表 %s 已存在非 id 主键,跳过 id 自增主键修复", col.TableName)
|
||
}
|
||
if !needAlter {
|
||
continue
|
||
}
|
||
|
||
// 维持原来的数据类型,避免与历史 SQL 冲突
|
||
dataType := col.DataType
|
||
if dataType == "" {
|
||
dataType = "int"
|
||
}
|
||
|
||
alterSQL := fmt.Sprintf(
|
||
"ALTER TABLE `%s` MODIFY COLUMN id %s NOT NULL AUTO_INCREMENT PRIMARY KEY",
|
||
col.TableName,
|
||
dataType,
|
||
)
|
||
if err := s.db.Exec(alterSQL).Error; err != nil {
|
||
logger.Errorf("修复表 %s 的 id 自增主键失败: %v", col.TableName, err)
|
||
} else {
|
||
logger.Infof("已修复表 %s 的 id 为 AUTO_INCREMENT PRIMARY KEY", col.TableName)
|
||
}
|
||
}
|
||
}
|
||
|
||
// === 为 geekai_users.username 补充唯一索引(根据模型 uniqueIndex 定义) ===
|
||
var usernameUniqueCount int64
|
||
err := s.db.Raw(`
|
||
SELECT COUNT(1)
|
||
FROM INFORMATION_SCHEMA.STATISTICS
|
||
WHERE TABLE_SCHEMA = ?
|
||
AND TABLE_NAME = 'geekai_users'
|
||
AND COLUMN_NAME = 'username'
|
||
AND NON_UNIQUE = 0
|
||
`, dbName).Scan(&usernameUniqueCount).Error
|
||
if err != nil {
|
||
logger.Errorf("检查 geekai_users.username 唯一索引失败: %v", err)
|
||
} else if usernameUniqueCount == 0 {
|
||
// 索引名尽量固定,避免重复创建
|
||
if err := s.db.Exec("ALTER TABLE geekai_users ADD UNIQUE KEY idx_geekai_users_username (username)").Error; err != nil {
|
||
logger.Errorf("创建 geekai_users.username 唯一索引失败: %v", err)
|
||
} else {
|
||
logger.Info("已为 geekai_users.username 创建唯一索引 idx_geekai_users_username")
|
||
}
|
||
} else {
|
||
logger.Info("geekai_users.username 唯一索引已存在,跳过创建")
|
||
}
|
||
|
||
logger.Info("关键表主键、自增和索引修复完成")
|
||
}
|
||
|
||
// incrementalTableMigration 第二步:增量迁移,仅处理删除字段与数据迁移
|
||
// AutoMigrate 不会删除列,故需在此显式 DropColumn;字段重命名需先拷贝数据再删除旧列
|
||
func (s *MigrationService) incrementalTableMigration() {
|
||
logger.Info("执行增量迁移(删除字段 + 数据迁移)...")
|
||
|
||
// ========== 字段重命名:全量迁移已添加新列,需将旧列数据拷贝到新列后删除旧列 ==========
|
||
|
||
// ChatApp(geekai_chat_roles): context_json -> system_prompt 历史数据迁移
|
||
if s.db.Migrator().HasColumn(&model.ChatApp{}, "context_json") {
|
||
// 将旧列 context_json 的值拷贝到 system_prompt(NULL 转为空字符串,保证 NOT NULL 约束)
|
||
if err := s.db.Exec(`
|
||
UPDATE geekai_chat_roles
|
||
SET system_prompt = IFNULL(NULLIF(TRIM(COALESCE(context_json, '')), ''), '')
|
||
`).Error; err != nil {
|
||
logger.Errorf("迁移 geekai_chat_roles.context_json -> system_prompt 失败: %v", err)
|
||
} else {
|
||
if err := s.db.Migrator().DropColumn(&model.ChatApp{}, "context_json"); err != nil {
|
||
logger.Errorf("删除 geekai_chat_roles.context_json 失败: %v", err)
|
||
} else {
|
||
logger.Info("geekai_chat_roles: context_json 已迁移至 system_prompt 并删除旧列")
|
||
}
|
||
}
|
||
}
|
||
|
||
// ChatApp: 将 user_id 为 NULL 的历史记录置为 0(系统内置)
|
||
if s.db.Migrator().HasColumn(&model.ChatApp{}, "user_id") {
|
||
if err := s.db.Exec(`UPDATE geekai_chat_roles SET user_id = 0 WHERE user_id IS NULL`).Error; err != nil {
|
||
logger.Errorf("初始化 geekai_chat_roles.user_id 失败: %v", err)
|
||
}
|
||
}
|
||
|
||
// ChatApp(geekai_chat_roles): 删除 marker 列(应用仅通过 id 区分)
|
||
var hasMarker int
|
||
if s.db.Raw("SELECT COUNT(1) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'geekai_chat_roles' AND COLUMN_NAME = 'marker'").Scan(&hasMarker).Error == nil && hasMarker > 0 {
|
||
// 先删除可能存在的唯一索引(不同版本 SQL 索引名不同)
|
||
for _, idxName := range []string{"marker", "idx_chatgpt_chat_roles_marker", "idx_chatgpt_chat_roles_key", "idx_geekai_chat_roles_marker"} {
|
||
_ = s.db.Exec(fmt.Sprintf("ALTER TABLE geekai_chat_roles DROP INDEX `%s`", idxName)).Error
|
||
}
|
||
if err := s.db.Exec("ALTER TABLE geekai_chat_roles DROP COLUMN marker").Error; err != nil {
|
||
logger.Errorf("删除 geekai_chat_roles.marker 失败: %v", err)
|
||
} else {
|
||
logger.Info("geekai_chat_roles: 已删除 marker 列")
|
||
}
|
||
}
|
||
|
||
// Config: config_json -> value, marker -> name
|
||
if s.db.Migrator().HasColumn(&model.Config{}, "config_json") {
|
||
s.db.Exec("UPDATE geekai_configs SET `value` = config_json WHERE config_json IS NOT NULL AND config_json != ''")
|
||
s.db.Migrator().DropColumn(&model.Config{}, "config_json")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.Config{}, "marker") {
|
||
s.db.Exec("UPDATE geekai_configs SET `name` = marker WHERE marker IS NOT NULL AND marker != ''")
|
||
s.db.Migrator().DropColumn(&model.Config{}, "marker")
|
||
}
|
||
if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
|
||
s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
|
||
}
|
||
if s.db.Migrator().HasIndex(&model.Config{}, "marker") {
|
||
s.db.Migrator().DropIndex(&model.Config{}, "marker")
|
||
}
|
||
|
||
// Order: pay_type -> channel
|
||
if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") {
|
||
s.db.Exec("UPDATE geekai_orders SET channel = pay_type WHERE pay_type IS NOT NULL AND pay_type != ''")
|
||
s.db.Migrator().DropColumn(&model.Order{}, "pay_type")
|
||
}
|
||
|
||
// JimengJob: task_params -> params
|
||
if s.db.Migrator().HasColumn(&model.JimengJob{}, "task_params") {
|
||
s.db.Exec("UPDATE geekai_jimeng_jobs SET params = task_params WHERE task_params IS NOT NULL AND task_params != ''")
|
||
s.db.Migrator().DropColumn(&model.JimengJob{}, "task_params")
|
||
}
|
||
|
||
// VideoJob: task_info -> params, raw_data -> output
|
||
if s.db.Migrator().HasColumn(&model.VideoJob{}, "task_info") {
|
||
s.db.Exec("UPDATE geekai_video_jobs SET params = task_info WHERE task_info IS NOT NULL AND task_info != ''")
|
||
s.db.Migrator().DropColumn(&model.VideoJob{}, "task_info")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.VideoJob{}, "raw_data") {
|
||
s.db.Exec("UPDATE geekai_video_jobs SET `output` = raw_data WHERE raw_data IS NOT NULL AND raw_data != ''")
|
||
s.db.Migrator().DropColumn(&model.VideoJob{}, "raw_data")
|
||
}
|
||
|
||
// SunoJob: task_info -> params, raw_data -> output
|
||
if s.db.Migrator().HasColumn(&model.SunoJob{}, "task_info") {
|
||
s.db.Exec("UPDATE geekai_suno_jobs SET params = task_info WHERE task_info IS NOT NULL AND task_info != ''")
|
||
s.db.Migrator().DropColumn(&model.SunoJob{}, "task_info")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.SunoJob{}, "raw_data") {
|
||
s.db.Exec("UPDATE geekai_suno_jobs SET `output` = raw_data WHERE raw_data IS NOT NULL AND raw_data != ''")
|
||
s.db.Migrator().DropColumn(&model.SunoJob{}, "raw_data")
|
||
}
|
||
|
||
// ========== 删除不再使用的字段 ==========
|
||
|
||
if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
|
||
s.db.Migrator().DropColumn(&model.Order{}, "deleted_at")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
|
||
s.db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
|
||
s.db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.User{}, "chat_config") {
|
||
s.db.Migrator().DropColumn(&model.User{}, "chat_config")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "category") {
|
||
s.db.Migrator().DropColumn(&model.ChatModel{}, "category")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "description") {
|
||
s.db.Migrator().DropColumn(&model.ChatModel{}, "description")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.Product{}, "discount") {
|
||
s.db.Migrator().DropColumn(&model.Product{}, "discount")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.Product{}, "days") {
|
||
s.db.Migrator().DropColumn(&model.Product{}, "days")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.Product{}, "app_url") {
|
||
s.db.Migrator().DropColumn(&model.Product{}, "app_url")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.Product{}, "url") {
|
||
s.db.Migrator().DropColumn(&model.Product{}, "url")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.VideoJob{}, "water_url") {
|
||
s.db.Migrator().DropColumn(&model.VideoJob{}, "water_url")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.VideoJob{}, "cover_url") {
|
||
s.db.Migrator().DropColumn(&model.VideoJob{}, "cover_url")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.VideoJob{}, "prompt_ext") {
|
||
s.db.Migrator().DropColumn(&model.VideoJob{}, "prompt_ext")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.SunoJob{}, "instrumental") {
|
||
s.db.Migrator().DropColumn(&model.SunoJob{}, "instrumental")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.SunoJob{}, "tags") {
|
||
s.db.Migrator().DropColumn(&model.SunoJob{}, "tags")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.SunoJob{}, "extend_secs") {
|
||
s.db.Migrator().DropColumn(&model.SunoJob{}, "extend_secs")
|
||
}
|
||
if s.db.Migrator().HasColumn(&model.SunoJob{}, "model_name") {
|
||
s.db.Migrator().DropColumn(&model.SunoJob{}, "model_name")
|
||
}
|
||
|
||
// ========== 数据迁移:根据业务逻辑更新现有数据 ==========
|
||
|
||
// video_job: 根据 progress 填充 status
|
||
if s.db.Migrator().HasColumn(&model.VideoJob{}, "status") {
|
||
s.db.Exec(`UPDATE geekai_video_jobs SET status = CASE
|
||
WHEN progress < 100 THEN 'in_progress'
|
||
WHEN progress = 100 THEN 'success'
|
||
WHEN progress = 101 THEN 'failed'
|
||
WHEN progress = 102 THEN 'downloading'
|
||
ELSE 'pending'
|
||
END WHERE status = '' OR status IS NULL`)
|
||
}
|
||
|
||
// suno_job: 从 output 提取 tags/model_name 填入 params
|
||
s.migrateSunoJobData()
|
||
|
||
logger.Info("增量迁移完成")
|
||
}
|
||
|
||
// TableMigration 数据表迁移入口:先全量同步 schema,再增量删除字段并迁移数据
|
||
func (s *MigrationService) TableMigration() {
|
||
s.fullTableMigration()
|
||
s.fixTableConstraints()
|
||
s.incrementalTableMigration()
|
||
s.migrateChatAppSystemPromptFromJSON()
|
||
}
|
||
|
||
// migrateChatAppSystemPromptFromJSON 将智能体 system_prompt 字段中历史 JSON 数组
|
||
// 解析后取出 role 为 system 的 content,覆盖回 system_prompt(纯文本)
|
||
func (s *MigrationService) migrateChatAppSystemPromptFromJSON() {
|
||
key := "migrate:chat_app_system_prompt_json"
|
||
if s.redisClient.Get(context.Background(), key).Val() == "1" {
|
||
logger.Info("ChatApp system_prompt JSON 已迁移,跳过")
|
||
return
|
||
}
|
||
logger.Info("开始迁移智能体 system_prompt 历史 JSON 数据...")
|
||
|
||
var apps []model.ChatApp
|
||
if err := s.db.Find(&apps).Error; err != nil {
|
||
logger.Errorf("查询 ChatApp 失败: %v", err)
|
||
return
|
||
}
|
||
|
||
updated := 0
|
||
for i := range apps {
|
||
raw := strings.TrimSpace(apps[i].SystemPrompt)
|
||
if raw == "" {
|
||
continue
|
||
}
|
||
if len(raw) < 2 || raw[0] != '[' {
|
||
continue
|
||
}
|
||
var messages []types.Message
|
||
if err := json.Unmarshal([]byte(raw), &messages); err != nil {
|
||
continue
|
||
}
|
||
var systemContent string
|
||
for _, m := range messages {
|
||
if strings.ToLower(strings.TrimSpace(m.Role)) == "system" && m.Content != "" {
|
||
systemContent = m.Content
|
||
break
|
||
}
|
||
}
|
||
if err := s.db.Model(&model.ChatApp{}).Where("id = ?", apps[i].Id).Update("system_prompt", systemContent).Error; err != nil {
|
||
logger.Warnf("更新 ChatApp id=%d system_prompt 失败: %v", apps[i].Id, err)
|
||
continue
|
||
}
|
||
updated++
|
||
}
|
||
|
||
logger.Infof("智能体 system_prompt JSON 迁移完成,共更新 %d 条", updated)
|
||
s.redisClient.Set(context.Background(), key, "1", 0)
|
||
}
|
||
|
||
// 迁移配置数据
|
||
func (s *MigrationService) MigrateConfig(config *types.AppConfig) error {
|
||
|
||
logger.Info("开始迁移配置到数据库...")
|
||
|
||
// 迁移支付配置
|
||
if err := s.migratePaymentConfig(config); err != nil {
|
||
logger.Errorf("迁移支付配置失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
// 迁移存储配置
|
||
if err := s.migrateStorageConfig(config); err != nil {
|
||
logger.Errorf("迁移存储配置失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
// 迁移通信配置
|
||
if err := s.migrateCommunicationConfig(config); err != nil {
|
||
logger.Errorf("迁移通信配置失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
// 迁移配置内容
|
||
if err := s.MigrateConfigContent(); err != nil {
|
||
logger.Errorf("迁移配置内容失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
logger.Info("配置迁移完成")
|
||
return nil
|
||
}
|
||
|
||
// 迁移支付配置
|
||
func (s *MigrationService) migratePaymentConfig(config *types.AppConfig) error {
|
||
|
||
paymentConfig := types.PaymentConfig{
|
||
Alipay: config.AlipayConfig,
|
||
Epay: config.GeekPayConfig,
|
||
WxPay: config.WechatPayConfig,
|
||
}
|
||
if err := s.saveConfig(types.ConfigKeyPayment, paymentConfig); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 迁移存储配置
|
||
func (s *MigrationService) migrateStorageConfig(config *types.AppConfig) error {
|
||
|
||
ossConfig := types.OSSConfig{
|
||
Active: config.OSS.Active,
|
||
Local: config.OSS.Local,
|
||
Minio: config.OSS.Minio,
|
||
QiNiu: config.OSS.QiNiu,
|
||
AliYun: config.OSS.AliYun,
|
||
}
|
||
return s.saveConfig(types.ConfigKeyOss, ossConfig)
|
||
}
|
||
|
||
// 迁移通信配置
|
||
func (s *MigrationService) migrateCommunicationConfig(config *types.AppConfig) error {
|
||
// SMTP配置
|
||
smtpConfig := map[string]any{
|
||
"use_tls": config.SmtpConfig.UseTls,
|
||
"host": config.SmtpConfig.Host,
|
||
"port": config.SmtpConfig.Port,
|
||
"app_name": config.SmtpConfig.AppName,
|
||
"from": config.SmtpConfig.From,
|
||
"password": config.SmtpConfig.Password,
|
||
}
|
||
if err := s.saveConfig(types.ConfigKeySmtp, smtpConfig); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 短信配置
|
||
smsConfig := map[string]any{
|
||
"active": strings.ToLower(config.SMS.Active),
|
||
"aliyun": map[string]any{
|
||
"access_key": config.SMS.Ali.AccessKey,
|
||
"access_secret": config.SMS.Ali.AccessSecret,
|
||
"sign": config.SMS.Ali.Sign,
|
||
"code_temp_id": config.SMS.Ali.CodeTempId,
|
||
},
|
||
"bao": map[string]any{
|
||
"username": config.SMS.Bao.Username,
|
||
"password": config.SMS.Bao.Password,
|
||
"sign": config.SMS.Bao.Sign,
|
||
"code_template": config.SMS.Bao.CodeTemplate,
|
||
},
|
||
"tencent": map[string]any{
|
||
"secret_id": config.SMS.Tencent.SecretId,
|
||
"secret_key": config.SMS.Tencent.SecretKey,
|
||
"sms_sdk_app_id": config.SMS.Tencent.SmsSdkAppId,
|
||
"sign": config.SMS.Tencent.Sign,
|
||
"code_temp_id": config.SMS.Tencent.CodeTempId,
|
||
"region": config.SMS.Tencent.Region,
|
||
},
|
||
}
|
||
return s.saveConfig(types.ConfigKeySms, smsConfig)
|
||
}
|
||
|
||
// 保存配置到数据库
|
||
func (s *MigrationService) saveConfig(key string, config any) error {
|
||
// 检查是否已存在
|
||
var existingConfig model.Config
|
||
if err := s.db.Where("name", key).First(&existingConfig).Error; err == nil {
|
||
// 配置已存在,跳过
|
||
logger.Infof("配置 %s 已存在,跳过迁移", key)
|
||
return nil
|
||
}
|
||
|
||
// 序列化配置
|
||
configJSON, err := json.Marshal(config)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 保存到数据库
|
||
newConfig := model.Config{
|
||
Name: key,
|
||
Value: string(configJSON),
|
||
}
|
||
if err := s.db.Create(&newConfig).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
logger.Infof("成功迁移配置 %s", key)
|
||
return nil
|
||
}
|
||
|
||
// migrateSunoJobData 合并 suno_job 数据:从 Output 原始数据中解析出 tags 和 model_name 填入 params 字段
|
||
func (s *MigrationService) migrateSunoJobData() {
|
||
key := "migrate:suno_job_data"
|
||
if s.redisClient.Get(context.Background(), key).Val() == "1" {
|
||
logger.Info("SunoJob 数据已合并,跳过迁移")
|
||
return
|
||
}
|
||
|
||
logger.Info("开始合并 SunoJob 数据...")
|
||
|
||
// 查询所有有 output 数据的记录
|
||
var jobs []model.SunoJob
|
||
if err := s.db.Where("output != ? AND output != ''", "").Find(&jobs).Error; err != nil {
|
||
logger.Errorf("查询 SunoJob 数据失败: %v", err)
|
||
return
|
||
}
|
||
|
||
updatedCount := 0
|
||
for _, job := range jobs {
|
||
if job.Output == "" {
|
||
continue
|
||
}
|
||
|
||
// 解析 Output JSON 数据
|
||
var outputData struct {
|
||
Metadata struct {
|
||
Tags string `json:"tags"`
|
||
} `json:"metadata"`
|
||
ModelName string `json:"model_name"`
|
||
}
|
||
|
||
if err := json.Unmarshal([]byte(job.Output), &outputData); err != nil {
|
||
logger.Warnf("解析 Output 数据失败 (ID: %d): %v", job.Id, err)
|
||
continue
|
||
}
|
||
|
||
// 检查是否需要更新 params
|
||
needUpdate := false
|
||
params := job.Params
|
||
|
||
// 如果 params 中的 tags 为空,但 output 中有 tags,则更新
|
||
if params.Tags == "" && outputData.Metadata.Tags != "" {
|
||
params.Tags = outputData.Metadata.Tags
|
||
// 修复 tags 字段过长导致更新失败
|
||
if len(params.Tags) > 255 {
|
||
params.Tags = params.Tags[:255]
|
||
}
|
||
needUpdate = true
|
||
}
|
||
|
||
// 如果 params 中的 model 为空,但 output 中有 model_name,则更新
|
||
if params.Model == "" && outputData.ModelName != "" {
|
||
params.Model = outputData.ModelName
|
||
needUpdate = true
|
||
}
|
||
|
||
// 如果需要更新,则保存
|
||
if needUpdate {
|
||
if err := s.db.Model(&model.SunoJob{}).Where("id = ?", job.Id).Update("params", params).Error; err != nil {
|
||
logger.Errorf("更新 SunoJob 数据失败 (ID: %d): %v", job.Id, err)
|
||
continue
|
||
}
|
||
updatedCount++
|
||
}
|
||
}
|
||
|
||
logger.Infof("SunoJob 数据合并完成,共更新 %d 条记录", updatedCount)
|
||
s.redisClient.Set(context.Background(), key, "1", 0)
|
||
}
|