初始化
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
// 提示词业务模块,统一封装用户提示词规则、选择关系和持久化操作。
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Service 提供提示词模块的公开业务接口,并隐藏底层数据库实现。
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// CustomPromptInput 表示用户自定义提示词的可编辑内容。
|
||||
type CustomPromptInput struct {
|
||||
Name string
|
||||
Type string
|
||||
Content string
|
||||
}
|
||||
|
||||
// CustomPrompt 表示返回给接口层的用户自定义提示词。
|
||||
type CustomPrompt struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Scope string `json:"scope"`
|
||||
Editable bool `json:"editable"`
|
||||
}
|
||||
|
||||
// SystemPromptInput 表示管理端可维护的系统提示词内容。
|
||||
type SystemPromptInput struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// SystemPrompt 表示管理端保存后返回的系统提示词。
|
||||
type SystemPrompt struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// VersionedPromptInput 表示旧版提示词工作流中的草稿和版本字段。
|
||||
type VersionedPromptInput struct {
|
||||
Code string
|
||||
Name string
|
||||
Category string
|
||||
Type string
|
||||
Content string
|
||||
Variables []string
|
||||
VersionNote string
|
||||
}
|
||||
|
||||
// UserPromptList 表示指定类型的可用提示词及用户当前选择。
|
||||
type UserPromptList struct {
|
||||
Prompts []map[string]any
|
||||
SelectedID *uuid.UUID
|
||||
}
|
||||
|
||||
// PersistenceError 标识应由接口层转换为内部错误的数据库故障。
|
||||
type PersistenceError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error 返回底层数据库错误文本,供日志与测试定位。
|
||||
func (e PersistenceError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// Unwrap 暴露底层错误,保留 errors.Is 和 errors.As 语义。
|
||||
func (e PersistenceError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
var fixedDramaParsePromptTypes = []string{"剧本解析", "角色、场景、道具解析"}
|
||||
|
||||
// NewService 创建提示词服务,数据库连接仅在模块内部使用。
|
||||
func NewService(db *gorm.DB) *Service {
|
||||
return &Service{db: db}
|
||||
}
|
||||
|
||||
// IsFixedDramaParseType 判断提示词类型是否由后端固定配置且禁止用户编辑。
|
||||
func IsFixedDramaParseType(promptType string) bool {
|
||||
for _, fixedType := range fixedDramaParsePromptTypes {
|
||||
if strings.TrimSpace(promptType) == fixedType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsPersistenceError 判断错误是否属于需要隐藏细节的数据库故障。
|
||||
func IsPersistenceError(err error) bool {
|
||||
var target PersistenceError
|
||||
return errors.As(err, &target)
|
||||
}
|
||||
|
||||
// ListSystemPrompts 查询管理端可维护的系统提示词,不返回后端固定配置。
|
||||
func (s *Service) ListSystemPrompts(keyword, promptType string, page, size int) ([]map[string]any, int64, error) {
|
||||
query := s.db.Table("prompts p").Where("p.deleted_at IS NULL AND p.scope='system' AND p.type NOT IN ?", fixedDramaParsePromptTypes)
|
||||
if keyword = strings.TrimSpace(keyword); keyword != "" {
|
||||
query = query.Where("p.name::text ILIKE ? OR p.content ILIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
if promptType = strings.TrimSpace(promptType); promptType != "" {
|
||||
query = query.Where("p.type=?", promptType)
|
||||
}
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items := make([]map[string]any, 0)
|
||||
err := query.Select("p.id,p.name,p.type,p.content,p.scope,p.owner_user_id,p.created_at,p.updated_at").
|
||||
Order("p.updated_at DESC").Offset((page - 1) * size).Limit(size).Find(&items).Error
|
||||
return items, total, err
|
||||
}
|
||||
|
||||
// SaveSystemPrompt 校验并新增或更新管理端系统提示词。
|
||||
func (s *Service) SaveSystemPrompt(promptID string, input SystemPromptInput) (SystemPrompt, error) {
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.Type = strings.TrimSpace(input.Type)
|
||||
input.Content = strings.TrimSpace(input.Content)
|
||||
if input.Name == "" || input.Type == "" || input.Content == "" {
|
||||
return SystemPrompt{}, errors.New("name、type、content不能为空")
|
||||
}
|
||||
if IsFixedDramaParseType(input.Type) {
|
||||
return SystemPrompt{}, errors.New("该提示词已固定在后端代码中,不支持管理")
|
||||
}
|
||||
if promptID == "" {
|
||||
promptID = uuid.NewString()
|
||||
if err := s.db.Exec("INSERT INTO prompts(id,name,type,content,scope,owner_user_id) VALUES(?,?,?,?,'system',NULL)", promptID, input.Name, input.Type, input.Content).Error; err != nil {
|
||||
return SystemPrompt{}, err
|
||||
}
|
||||
} else if err := s.db.Exec("UPDATE prompts SET name=?,type=?,content=?,updated_at=CURRENT_TIMESTAMP WHERE id=? AND scope='system' AND deleted_at IS NULL", input.Name, input.Type, input.Content, promptID).Error; err != nil {
|
||||
return SystemPrompt{}, err
|
||||
}
|
||||
return SystemPrompt{ID: promptID, Name: input.Name, Type: input.Type, Content: input.Content}, nil
|
||||
}
|
||||
|
||||
// DeleteSystemPrompt 删除管理端指定的系统提示词。
|
||||
func (s *Service) DeleteSystemPrompt(promptID string) error {
|
||||
return s.db.Exec("DELETE FROM prompts WHERE id=? AND scope='system'", promptID).Error
|
||||
}
|
||||
|
||||
// SaveVersionedPrompt 保存旧版管理接口使用的提示词草稿及版本快照。
|
||||
func (s *Service) SaveVersionedPrompt(adminID uuid.UUID, promptID string, input VersionedPromptInput) (string, error) {
|
||||
if strings.TrimSpace(input.Category) == "" {
|
||||
input.Category = input.Type
|
||||
}
|
||||
if strings.TrimSpace(input.Type) == "" {
|
||||
input.Type = input.Category
|
||||
}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
version := 1
|
||||
if promptID == "" {
|
||||
promptID = uuid.NewString()
|
||||
if err := tx.Exec("INSERT INTO prompts(id,code,name,category,type,content,status) VALUES(?,?,?,?,?,?,'draft')", promptID, input.Code, input.Name, input.Category, input.Type, input.Content).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tx.Raw("SELECT coalesce(max(version),0)+1 FROM prompt_versions WHERE prompt_id=?", promptID).Scan(&version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec("UPDATE prompts SET code=?,name=?,category=?,type=?,content=? WHERE id=? AND deleted_at IS NULL", input.Code, input.Name, input.Category, input.Type, input.Content, promptID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
variables, _ := json.Marshal(input.Variables)
|
||||
versionID := uuid.NewString()
|
||||
if err := tx.Exec("INSERT INTO prompt_versions(id,prompt_id,version,content,variables,version_note,status,operator_id) VALUES(?,?,?,?,?::jsonb,?,'draft',?)", versionID, promptID, version, input.Content, string(variables), input.VersionNote, adminID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Exec("UPDATE prompts SET current_version_id=?,status='draft' WHERE id=?", versionID, promptID).Error
|
||||
})
|
||||
return promptID, err
|
||||
}
|
||||
|
||||
// ApplyPromptAction 执行旧版提示词接口的发布、停用或版本回滚事务。
|
||||
func (s *Service) ApplyPromptAction(adminID uuid.UUID, promptID, action, versionID, versionNote string) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
switch action {
|
||||
case "publish":
|
||||
if err := tx.Exec("UPDATE prompt_versions SET status=CASE WHEN id=(SELECT current_version_id FROM prompts WHERE id=?) THEN 'published' ELSE 'retired' END WHERE prompt_id=?", promptID, promptID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Exec("UPDATE prompts SET status='published' WHERE id=?", promptID).Error
|
||||
case "disable":
|
||||
return tx.Exec("UPDATE prompts SET status='disabled' WHERE id=?", promptID).Error
|
||||
case "rollback":
|
||||
var source struct {
|
||||
Content string
|
||||
Variables string
|
||||
}
|
||||
if err := tx.Raw("SELECT content,variables::text FROM prompt_versions WHERE id=? AND prompt_id=?", versionID, promptID).Scan(&source).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var version int
|
||||
if err := tx.Raw("SELECT coalesce(max(version),0)+1 FROM prompt_versions WHERE prompt_id=?", promptID).Scan(&version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
newVersionID := uuid.NewString()
|
||||
if err := tx.Exec("INSERT INTO prompt_versions(id,prompt_id,version,content,variables,version_note,status,operator_id) VALUES(?,?,?,?,?::jsonb,?,'draft',?)", newVersionID, promptID, version, source.Content, source.Variables, versionNote, adminID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Exec("UPDATE prompts SET current_version_id=?,status='draft' WHERE id=?", newVersionID, promptID).Error
|
||||
default:
|
||||
return errors.New("不支持的提示词操作")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ListPromptHistory 查询旧版提示词接口保留的版本历史。
|
||||
func (s *Service) ListPromptHistory(promptID string) ([]map[string]any, error) {
|
||||
items := make([]map[string]any, 0)
|
||||
err := s.db.Table("prompt_versions v").
|
||||
Select("v.id,v.version,v.content,v.variables,v.version_note,v.status,v.created_at,a.username AS operator").
|
||||
Joins("LEFT JOIN admin_users a ON a.id=v.operator_id").Where("v.prompt_id=?", promptID).
|
||||
Order("v.version DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// ListUserPrompts 查询系统提示词、当前用户自定义提示词及用户选择。
|
||||
func (s *Service) ListUserPrompts(userID uuid.UUID, promptType string) (UserPromptList, error) {
|
||||
if IsFixedDramaParseType(promptType) {
|
||||
return UserPromptList{Prompts: []map[string]any{}}, nil
|
||||
}
|
||||
var prompts []map[string]any
|
||||
err := s.db.Table("prompts").Select("id,name,type,content,scope,owner_user_id,created_at,updated_at,(scope='user') AS editable").
|
||||
Where("deleted_at IS NULL AND type=? AND (scope='system' OR (scope='user' AND owner_user_id=?))", promptType, userID).
|
||||
Order("CASE WHEN scope='system' THEN 0 ELSE 1 END, updated_at DESC").Find(&prompts).Error
|
||||
if err != nil {
|
||||
return UserPromptList{}, err
|
||||
}
|
||||
var selected struct{ PromptID *uuid.UUID }
|
||||
// 用户未设置偏好时返回空属于正常情况,用 Limit(1).Find 避免触发 GORM 的 record not found 日志
|
||||
s.db.Table("user_prompt_preferences").Select("prompt_id").Where("user_id=? AND prompt_type=?", userID, promptType).Limit(1).Find(&selected)
|
||||
return UserPromptList{Prompts: prompts, SelectedID: selected.PromptID}, nil
|
||||
}
|
||||
|
||||
// SelectUserPrompt 更新用户对指定提示词类型的选择,空值表示关闭选择。
|
||||
func (s *Service) SelectUserPrompt(userID uuid.UUID, promptType string, promptID *uuid.UUID) error {
|
||||
if IsFixedDramaParseType(promptType) {
|
||||
return errors.New("该提示词由平台固定配置,不支持选择")
|
||||
}
|
||||
if promptID == nil {
|
||||
if err := s.db.Exec("DELETE FROM user_prompt_preferences WHERE user_id=? AND prompt_type=?", userID, promptType).Error; err != nil {
|
||||
return PersistenceError{Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var count int64
|
||||
if err := s.db.Table("prompts").Where("id=? AND type=? AND deleted_at IS NULL AND (scope='system' OR (scope='user' AND owner_user_id=?))", *promptID, promptType, userID).Count(&count).Error; err != nil {
|
||||
return PersistenceError{Err: err}
|
||||
}
|
||||
if count == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := s.db.Exec(`INSERT INTO user_prompt_preferences(id,user_id,prompt_id,prompt_type) VALUES(gen_random_uuid(),?,?,?) ON CONFLICT(user_id,prompt_type) DO UPDATE SET prompt_id=excluded.prompt_id,updated_at=CURRENT_TIMESTAMP`, userID, *promptID, promptType).Error; err != nil {
|
||||
return PersistenceError{Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCustomPrompt 校验并创建当前用户拥有的自定义提示词。
|
||||
func (s *Service) CreateCustomPrompt(userID uuid.UUID, input CustomPromptInput) (CustomPrompt, error) {
|
||||
input = normalizeCustomPromptInput(input)
|
||||
if input.Name == "" || input.Type == "" || input.Content == "" {
|
||||
return CustomPrompt{}, errors.New("name、type、content不能为空")
|
||||
}
|
||||
if IsFixedDramaParseType(input.Type) {
|
||||
return CustomPrompt{}, errors.New("该提示词由平台固定配置,不支持自定义")
|
||||
}
|
||||
item := CustomPrompt{ID: uuid.New(), Name: input.Name, Type: input.Type, Content: input.Content, Scope: "user", Editable: true}
|
||||
if err := s.db.Exec("INSERT INTO prompts(id,name,type,content,scope,owner_user_id) VALUES(?,?,?,?,'user',?)", item.ID, item.Name, item.Type, item.Content, userID).Error; err != nil {
|
||||
return CustomPrompt{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// UpdateCustomPrompt 校验并更新当前用户拥有的自定义提示词。
|
||||
func (s *Service) UpdateCustomPrompt(userID, promptID uuid.UUID, input CustomPromptInput) (CustomPrompt, error) {
|
||||
input = normalizeCustomPromptInput(input)
|
||||
if input.Name == "" || input.Content == "" {
|
||||
return CustomPrompt{}, errors.New("name、content不能为空")
|
||||
}
|
||||
if IsFixedDramaParseType(input.Type) {
|
||||
return CustomPrompt{}, errors.New("该提示词由平台固定配置,不支持自定义")
|
||||
}
|
||||
result := s.db.Exec("UPDATE prompts SET name=?,type=COALESCE(NULLIF(?,''),type),content=?,updated_at=CURRENT_TIMESTAMP WHERE id=? AND scope='user' AND owner_user_id=? AND deleted_at IS NULL", input.Name, input.Type, input.Content, promptID, userID)
|
||||
if result.Error != nil {
|
||||
return CustomPrompt{}, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return CustomPrompt{}, gorm.ErrRecordNotFound
|
||||
}
|
||||
return CustomPrompt{ID: promptID, Name: input.Name, Type: input.Type, Content: input.Content, Scope: "user", Editable: true}, nil
|
||||
}
|
||||
|
||||
// DeleteCustomPrompt 在同一事务中删除用户选择关系及其自定义提示词。
|
||||
func (s *Service) DeleteCustomPrompt(userID, promptID uuid.UUID) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec("DELETE FROM user_prompt_preferences WHERE prompt_id=? AND user_id=?", promptID, userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Exec("DELETE FROM prompts WHERE id=? AND scope='user' AND owner_user_id=?", promptID, userID)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// normalizeCustomPromptInput 清理用户输入两端空白,保证校验和写入使用一致值。
|
||||
func normalizeCustomPromptInput(input CustomPromptInput) CustomPromptInput {
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.Type = strings.TrimSpace(input.Type)
|
||||
input.Content = strings.TrimSpace(input.Content)
|
||||
return input
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 提示词模块单元测试,验证固定类型规则和用户输入标准化行为。
|
||||
package prompt
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestIsFixedDramaParseType 验证固定提示词类型及空白清理规则。
|
||||
func TestIsFixedDramaParseType(t *testing.T) {
|
||||
for _, value := range []string{"剧本解析", " 角色、场景、道具解析 "} {
|
||||
if !IsFixedDramaParseType(value) {
|
||||
t.Fatalf("expected %q to be fixed", value)
|
||||
}
|
||||
}
|
||||
if IsFixedDramaParseType("剧本分析") {
|
||||
t.Fatal("剧本分析不应被识别为固定解析提示词")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeCustomPromptInput 验证提示词字段在校验和写入前统一清理两端空白。
|
||||
func TestNormalizeCustomPromptInput(t *testing.T) {
|
||||
input := normalizeCustomPromptInput(CustomPromptInput{Name: " 名称 ", Type: " 类型\n", Content: " 内容 "})
|
||||
if input.Name != "名称" || input.Type != "类型" || input.Content != "内容" {
|
||||
t.Fatalf("unexpected normalized input: %#v", input)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user