From 238ce153dcb64ff9acb55e0951c981087ee1e44e Mon Sep 17 00:00:00 2001 From: st <2663600842@qq.com> Date: Tue, 28 Jul 2026 14:54:57 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=B1=82(Entity/Repo/DTO?= =?UTF-8?q?)=EF=BC=9A=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E7=94=BB?= =?UTF-8?q?=E5=83=8F=E6=89=A9=E5=B1=95=E5=AD=97=E6=AE=B5=20+=20=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=81=8F=E5=A5=BD=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - users 表扩展:department/position/expertise/preferred_language/timezone 画像字段 - 新增 UserPreference Entity + Repository 接口与实现(用户偏好 CRUD + 按 user_id 唯一查询) - 调整 UserRepository.UpdateProfile 支持画像字段局部更新,移除 role_template_id 关联 - 请求/响应 DTO:用户画像 5 字段 + 偏好 8 字段,移除角色模板请求响应结构 --- internal/model/dto/request/user.go | 23 ++++++++- internal/model/dto/response/user.go | 45 +++++++++++++---- internal/model/entity/user.go | 25 ++++++---- internal/model/entity/user_preference.go | 26 ++++++++++ internal/repository/user_interface.go | 1 + internal/repository/user_profile_interface.go | 22 +++++++++ .../repository/user_profile_repository.go | 48 +++++++++++++++++++ internal/repository/user_repository.go | 27 +++++++++++ 8 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 internal/model/entity/user_preference.go create mode 100644 internal/repository/user_profile_interface.go create mode 100644 internal/repository/user_profile_repository.go diff --git a/internal/model/dto/request/user.go b/internal/model/dto/request/user.go index f2d43ce..b28c259 100644 --- a/internal/model/dto/request/user.go +++ b/internal/model/dto/request/user.go @@ -17,12 +17,32 @@ type LoginRequest struct { Captcha string `json:"captcha" binding:"required,len=4"` } -// UpdateUserRequest 更新用户信息请求 +// UpdateUserRequest 更新用户基本信息请求(头像/邮箱) type UpdateUserRequest struct { Avatar string `json:"avatar" binding:"omitempty,url,max=255"` Email string `json:"email" binding:"omitempty,email,max=100"` } +// UpdateProfileRequest 更新用户画像请求(部门/职位/擅长/语言/时区) +type UpdateProfileRequest struct { + Department string `json:"department" binding:"omitempty,max=100"` + Position string `json:"position" binding:"omitempty,max=100"` + Expertise string `json:"expertise" binding:"omitempty,max=255"` + PreferredLanguage string `json:"preferred_language" binding:"omitempty,oneof=zh-CN en-US ja-JP ko-KR fr-FR de-DE es-ES"` + Timezone string `json:"timezone" binding:"omitempty,max=50"` +} + +// UpdateUserPreferenceRequest 更新用户偏好请求 +type UpdateUserPreferenceRequest struct { + DefaultModelID *string `json:"default_model_id" binding:"omitempty,max=36"` + PreferredKBIDs []string `json:"preferred_kb_ids" binding:"omitempty,max=50"` + AnswerStyle string `json:"answer_style" binding:"omitempty,oneof=concise balanced detailed step_by_step"` + AutoDeepMode *bool `json:"auto_deep_mode"` + AutoDeepThreshold *int `json:"auto_deep_threshold" binding:"omitempty,min=1,max=5"` + UseMarkdownTable *bool `json:"use_markdown_table"` + CitationStyle string `json:"citation_style" binding:"omitempty,oneof=none section_title doc_title_only"` +} + // ChangePasswordRequest 修改密码请求 type ChangePasswordRequest struct { OldPassword string `json:"old_password" binding:"required"` @@ -60,3 +80,4 @@ type AdminUpdateUserRequest struct { type AdminResetPasswordRequest struct { Password string `json:"password" binding:"required,min=6,max=50"` } + diff --git a/internal/model/dto/response/user.go b/internal/model/dto/response/user.go index 1b3b0b7..ad4d296 100644 --- a/internal/model/dto/response/user.go +++ b/internal/model/dto/response/user.go @@ -2,17 +2,41 @@ package response import "time" -// UserResponse 用户响应 +// UserResponse 用户基本信息响应(含画像字段) type UserResponse struct { - ID string `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Avatar string `json:"avatar"` - Status int `json:"status"` - Role int `json:"role"` - LastModel string `json:"lastModel,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Avatar string `json:"avatar"` + Status int `json:"status"` + Role int `json:"role"` + LastModel string `json:"lastModel,omitempty"` + Department string `json:"department,omitempty"` + Position string `json:"position,omitempty"` + Expertise string `json:"expertise,omitempty"` + PreferredLanguage string `json:"preferred_language,omitempty"` + Timezone string `json:"timezone,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// UserPreferenceResponse 用户偏好响应 +type UserPreferenceResponse struct { + UserID string `json:"user_id"` + DefaultModelID string `json:"default_model_id,omitempty"` + PreferredKBIDs []string `json:"preferred_kb_ids,omitempty"` + AnswerStyle string `json:"answer_style"` + AutoDeepMode bool `json:"auto_deep_mode"` + AutoDeepThreshold int `json:"auto_deep_threshold"` + UseMarkdownTable bool `json:"use_markdown_table"` + CitationStyle string `json:"citation_style"` + UpdatedAt string `json:"updated_at"` +} + +// ProfileResponse 合并返回 基本信息 + 偏好 +type ProfileResponse struct { + User UserResponse `json:"user"` + Preference UserPreferenceResponse `json:"preference"` } // AdminUserListItem 管理员用户列表项 @@ -32,3 +56,4 @@ type LoginResponse struct { Token string `json:"token"` User UserResponse `json:"user"` } + diff --git a/internal/model/entity/user.go b/internal/model/entity/user.go index fa51c64..f728f4f 100644 --- a/internal/model/entity/user.go +++ b/internal/model/entity/user.go @@ -4,16 +4,21 @@ import "time" // User 用户实体 type User struct { - ID string `gorm:"column:id;type:uuid;default:gen_random_uuid();primaryKey" json:"id"` - Username string `gorm:"type:varchar(50);not null;comment:用户名" json:"username"` - Password string `gorm:"type:varchar(255);not null;comment:密码哈希" json:"-"` - Email string `gorm:"type:varchar(100);comment:邮箱" json:"email"` - Avatar string `gorm:"type:varchar(255);comment:头像" json:"avatar"` - Status int `gorm:"type:smallint;default:1;comment:状态:1正常, 2禁用, 3注销, 4待验证" json:"status"` - Role int `gorm:"type:smallint;default:1;comment:角色:1普通用户, 2管理员" json:"role"` - LastModel string `gorm:"type:varchar(255);comment:上次使用的模型" json:"lastModel"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` + ID string `gorm:"column:id;type:uuid;default:gen_random_uuid();primaryKey" json:"id"` + Username string `gorm:"type:varchar(50);not null;comment:用户名" json:"username"` + Password string `gorm:"type:varchar(255);not null;comment:密码哈希" json:"-"` + Email string `gorm:"type:varchar(100);comment:邮箱" json:"email"` + Avatar string `gorm:"type:varchar(255);comment:头像" json:"avatar"` + Status int `gorm:"type:smallint;default:1;comment:状态:1正常, 2禁用, 3注销, 4待验证" json:"status"` + Role int `gorm:"type:smallint;default:1;comment:角色:1普通用户, 2管理员" json:"role"` + LastModel string `gorm:"type:varchar(255);comment:上次使用的模型" json:"lastModel"` + Department string `gorm:"type:varchar(100);comment:部门" json:"department"` + Position string `gorm:"type:varchar(100);comment:职位" json:"position"` + Expertise string `gorm:"type:varchar(255);comment:擅长领域/业务方向,逗号分隔" json:"expertise"` + PreferredLanguage string `gorm:"type:varchar(20);default:zh-CN;comment:偏好回答语言:zh-CN/en-US/ja-JP 等" json:"preferred_language"` + Timezone string `gorm:"type:varchar(50);default:Asia/Shanghai;comment:时区 IANA,如 Asia/Shanghai" json:"timezone"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` } // TableName 指定表名 diff --git a/internal/model/entity/user_preference.go b/internal/model/entity/user_preference.go new file mode 100644 index 0000000..5d38e63 --- /dev/null +++ b/internal/model/entity/user_preference.go @@ -0,0 +1,26 @@ +package entity + +import ( + "time" + + "gorm.io/datatypes" +) + +// UserPreference 用户偏好:常用模型、常用知识库、回答风格、是否自动深度模式 +type UserPreference struct { + ID string `gorm:"column:id;type:uuid;default:gen_random_uuid();primaryKey" json:"id"` + UserID string `gorm:"column:user_id;type:uuid;not null;uniqueIndex;comment:用户ID,一人一条" json:"user_id"` + DefaultModelID string `gorm:"column:default_model_id;type:varchar(36);comment:默认使用的模型ID(系统模型表ID,可空)" json:"default_model_id"` + PreferredKBIDs datatypes.JSON `gorm:"column:preferred_kb_ids;type:jsonb;comment:常用知识库ID列表,JSON array[string]" json:"preferred_kb_ids"` + AnswerStyle string `gorm:"column:answer_style;type:varchar(20);default:balanced;comment:回答风格:concise(简洁)/balanced(平衡)/detailed(详细)/step_by_step(分步)" json:"answer_style"` + AutoDeepMode bool `gorm:"column:auto_deep_mode;default:false;comment:是否自动切深度模式(true=复杂问题自动切;false=始终用户手动)" json:"auto_deep_mode"` + AutoDeepThreshold int `gorm:"column:auto_deep_threshold;default:2;comment:自动切深度模式的信号阈值,越大越不容易切(1~5)" json:"auto_deep_threshold"` + UseMarkdownTable bool `gorm:"column:use_markdown_table;default:true;comment:回答尽量用表格呈现结构化数据" json:"use_markdown_table"` + CitationStyle string `gorm:"column:citation_style;type:varchar(20);default:section_title;comment:引用格式:none(不标)/section_title(章节标题)/doc_title_only(仅文档名)" json:"citation_style"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` +} + +func (*UserPreference) TableName() string { + return "user_preferences" +} diff --git a/internal/repository/user_interface.go b/internal/repository/user_interface.go index 6652a1f..ee7c6f6 100644 --- a/internal/repository/user_interface.go +++ b/internal/repository/user_interface.go @@ -19,6 +19,7 @@ type UserRepository interface { FindByEmail(email string) (*entity.User, error) Create(user *entity.User) error Update(id string, updates map[string]interface{}) error + UpdateProfile(id string, upd *UserProfileUpdate) error Delete(id string) error AdminList(offset, limit int, filter *UserListFilter) ([]*entity.User, int64, error) ExistsByUsername(username string) (bool, error) diff --git a/internal/repository/user_profile_interface.go b/internal/repository/user_profile_interface.go new file mode 100644 index 0000000..a85d685 --- /dev/null +++ b/internal/repository/user_profile_interface.go @@ -0,0 +1,22 @@ +package repository + +import ( + "solvify-agent/internal/model/entity" +) + +// UserProfileUpdate 用户画像字段更新(由 Service 组装) +type UserProfileUpdate struct { + Department *string + Position *string + Expertise *string + PreferredLanguage *string + Timezone *string +} + +// UserPreferenceRepository 用户偏好仓储接口 +type UserPreferenceRepository interface { + FindByUserID(userID string) (*entity.UserPreference, error) + Upsert(pref *entity.UserPreference) error + Update(userID string, updates map[string]interface{}) error + DeleteByUserID(userID string) error +} diff --git a/internal/repository/user_profile_repository.go b/internal/repository/user_profile_repository.go new file mode 100644 index 0000000..f9534c4 --- /dev/null +++ b/internal/repository/user_profile_repository.go @@ -0,0 +1,48 @@ +package repository + +import ( + "errors" + + "solvify-agent/internal/model/entity" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type userPreferenceRepo struct { + db *gorm.DB +} + +func NewUserPreferenceRepository(db *gorm.DB) UserPreferenceRepository { + return &userPreferenceRepo{db: db} +} + +func (r *userPreferenceRepo) FindByUserID(userID string) (*entity.UserPreference, error) { + var p entity.UserPreference + err := r.db.Where("user_id = ?", userID).First(&p).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &p, nil +} + +func (r *userPreferenceRepo) Upsert(pref *entity.UserPreference) error { + return r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "user_id"}}, + UpdateAll: true, + }).Create(pref).Error +} + +func (r *userPreferenceRepo) Update(userID string, updates map[string]interface{}) error { + if len(updates) == 0 { + return nil + } + return r.db.Model(&entity.UserPreference{}).Where("user_id = ?", userID).Updates(updates).Error +} + +func (r *userPreferenceRepo) DeleteByUserID(userID string) error { + return r.db.Where("user_id = ?", userID).Delete(&entity.UserPreference{}).Error +} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index ba31727..1188cf3 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -79,6 +79,33 @@ func (r *userRepository) Delete(id string) error { return r.db.Where("id = ?", id).Delete(&entity.User{}).Error } +// UpdateProfile 局部更新用户画像字段(未设置的指针字段不参与更新) +func (r *userRepository) UpdateProfile(id string, upd *UserProfileUpdate) error { + if upd == nil { + return nil + } + updates := map[string]interface{}{} + if upd.Department != nil { + updates["department"] = *upd.Department + } + if upd.Position != nil { + updates["position"] = *upd.Position + } + if upd.Expertise != nil { + updates["expertise"] = *upd.Expertise + } + if upd.PreferredLanguage != nil { + updates["preferred_language"] = *upd.PreferredLanguage + } + if upd.Timezone != nil { + updates["timezone"] = *upd.Timezone + } + if len(updates) == 0 { + return nil + } + return r.db.Model(&entity.User{}).Where("id = ?", id).Updates(updates).Error +} + // AdminList 管理员分页获取用户列表 func (r *userRepository) AdminList(offset, limit int, filter *UserListFilter) ([]*entity.User, int64, error) { var ( From a1f4aab89cce6aecd6355a36dab23256a0b1aa62 Mon Sep 17 00:00:00 2001 From: st <2663600842@qq.com> Date: Tue, 28 Jul 2026 14:55:25 +0800 Subject: [PATCH 2/5] =?UTF-8?q?Service=E5=B1=82=EF=BC=9A=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=94=BB=E5=83=8F+=E5=81=8F=E5=A5=BD=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8E=E5=AE=9E=E7=8E=B0=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=A7=92=E8=89=B2=E6=A8=A1=E6=9D=BF=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserService 依赖拆分:依赖 UserPreferenceService 接口,Remove RoleTemplateService 全链路引用 - UserPreferenceService 接口:GetByUserID/Upsert/ToDTO,默认偏好兜底+按 user_id 唯一约束 - profile 接口/实现分离:profile_interface.go 定义接口,profile_service.go 保留实现,符合分层规范 - 画像更新校验:部门/职位/擅长字段长度校验,偏好枚举值校验(answer_style/citation_style 取值范围) --- internal/service/profile_interface.go | 16 ++++ internal/service/profile_service.go | 105 ++++++++++++++++++++++++++ internal/service/user_interface.go | 2 + internal/service/user_service.go | 74 +++++++++++++++--- 4 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 internal/service/profile_interface.go create mode 100644 internal/service/profile_service.go diff --git a/internal/service/profile_interface.go b/internal/service/profile_interface.go new file mode 100644 index 0000000..e802ae4 --- /dev/null +++ b/internal/service/profile_interface.go @@ -0,0 +1,16 @@ +package service + +import ( + "context" + + "solvify-agent/internal/model/dto/request" + dto "solvify-agent/internal/model/dto/response" + "solvify-agent/internal/model/entity" +) + +// UserPreferenceService 用户偏好服务接口 +type UserPreferenceService interface { + GetByUserID(ctx context.Context, userID string) (*entity.UserPreference, error) + Upsert(ctx context.Context, userID string, req *request.UpdateUserPreferenceRequest) (*dto.UserPreferenceResponse, error) + ToDTO(p *entity.UserPreference) *dto.UserPreferenceResponse +} diff --git a/internal/service/profile_service.go b/internal/service/profile_service.go new file mode 100644 index 0000000..b26ba53 --- /dev/null +++ b/internal/service/profile_service.go @@ -0,0 +1,105 @@ +package service + +import ( + "context" + "encoding/json" + + "solvify-agent/internal/model/dto/request" + dto "solvify-agent/internal/model/dto/response" + "solvify-agent/internal/model/entity" + "solvify-agent/internal/repository" + apperrors "solvify-agent/pkg/errors" +) + +type userPreferenceService struct { + repo repository.UserPreferenceRepository +} + +func NewUserPreferenceService(repo repository.UserPreferenceRepository) UserPreferenceService { + return &userPreferenceService{repo: repo} +} + +func (s *userPreferenceService) GetByUserID(_ context.Context, userID string) (*entity.UserPreference, error) { + p, err := s.repo.FindByUserID(userID) + if err != nil { + return nil, apperrors.WrapDefault(apperrors.CodeInternalError, err) + } + if p == nil { + p = s.defaultPreference(userID) + } + return p, nil +} + +func (s *userPreferenceService) Upsert(_ context.Context, userID string, req *request.UpdateUserPreferenceRequest) (*dto.UserPreferenceResponse, error) { + cur, err := s.repo.FindByUserID(userID) + if err != nil { + return nil, apperrors.WrapDefault(apperrors.CodeInternalError, err) + } + if cur == nil { + cur = s.defaultPreference(userID) + } + if req.DefaultModelID != nil { + cur.DefaultModelID = *req.DefaultModelID + } + if req.PreferredKBIDs != nil { + kbJSON, _ := json.Marshal(req.PreferredKBIDs) + cur.PreferredKBIDs = kbJSON + } + if req.AnswerStyle != "" { + cur.AnswerStyle = req.AnswerStyle + } + if req.AutoDeepMode != nil { + cur.AutoDeepMode = *req.AutoDeepMode + } + if req.AutoDeepThreshold != nil { + cur.AutoDeepThreshold = *req.AutoDeepThreshold + } + if req.UseMarkdownTable != nil { + cur.UseMarkdownTable = *req.UseMarkdownTable + } + if req.CitationStyle != "" { + cur.CitationStyle = req.CitationStyle + } + + if err := s.repo.Upsert(cur); err != nil { + return nil, apperrors.WrapDefault(apperrors.CodeInternalError, err) + } + updated, _ := s.repo.FindByUserID(userID) + return s.ToDTO(updated), nil +} + +func (s *userPreferenceService) ToDTO(p *entity.UserPreference) *dto.UserPreferenceResponse { + if p == nil { + return s.ToDTO(s.defaultPreference("")) + } + kbs := []string{} + if len(p.PreferredKBIDs) > 0 { + _ = json.Unmarshal(p.PreferredKBIDs, &kbs) + } + updated := "" + if !p.UpdatedAt.IsZero() { + updated = p.UpdatedAt.Format("2006-01-02 15:04:05") + } + return &dto.UserPreferenceResponse{ + UserID: p.UserID, + DefaultModelID: p.DefaultModelID, + PreferredKBIDs: kbs, + AnswerStyle: p.AnswerStyle, + AutoDeepMode: p.AutoDeepMode, + AutoDeepThreshold: p.AutoDeepThreshold, + UseMarkdownTable: p.UseMarkdownTable, + CitationStyle: p.CitationStyle, + UpdatedAt: updated, + } +} + +func (*userPreferenceService) defaultPreference(userID string) *entity.UserPreference { + return &entity.UserPreference{ + UserID: userID, + AnswerStyle: "balanced", + AutoDeepMode: false, + AutoDeepThreshold: 2, + UseMarkdownTable: true, + CitationStyle: "section_title", + } +} diff --git a/internal/service/user_interface.go b/internal/service/user_interface.go index d687206..50fbc47 100644 --- a/internal/service/user_interface.go +++ b/internal/service/user_interface.go @@ -11,7 +11,9 @@ import ( type UserServiceInterface interface { GetUserByID(id string) (*entity.User, error) UpdateUser(id string, req *request.UpdateUserRequest) error + UpdateProfile(id string, req *request.UpdateProfileRequest) error ChangePassword(id string, req *request.ChangePasswordRequest) error GetUserResponse(user *entity.User) *dto.UserResponse + GetProfile(id string) (*dto.ProfileResponse, error) AdminListUsers(adminID string, req *request.AdminUserListRequest) (*response.PageResponse, error) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 6ea16c8..b0fc4da 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -1,6 +1,7 @@ package service import ( + "context" "strings" "solvify-agent/internal/model/dto/request" @@ -15,12 +16,14 @@ import ( type userService struct { userRepo repository.UserRepository + prefSvc UserPreferenceService } // NewUserService 创建用户服务 -func NewUserService(userRepo repository.UserRepository) UserServiceInterface { +func NewUserService(userRepo repository.UserRepository, prefSvc UserPreferenceService) UserServiceInterface { return &userService{ userRepo: userRepo, + prefSvc: prefSvc, } } @@ -105,6 +108,37 @@ func (s *userService) UpdateUser(id string, req *request.UpdateUserRequest) erro return s.userRepo.Update(id, updates) } +// UpdateProfile 更新用户画像字段(部门/职位/擅长/语言/时区) +func (s *userService) UpdateProfile(id string, req *request.UpdateProfileRequest) error { + user, err := s.GetUserByID(id) + if err != nil { + return err + } + dept := req.Department + pos := req.Position + exp := req.Expertise + lang := req.PreferredLanguage + tz := req.Timezone + upd := &repository.UserProfileUpdate{ + Department: nonEmptyPtrOrNil(dept, &user.Department), + Position: nonEmptyPtrOrNil(pos, &user.Position), + Expertise: nonEmptyPtrOrNil(exp, &user.Expertise), + PreferredLanguage: nonEmptyPtrOrNil(lang, &user.PreferredLanguage), + Timezone: nonEmptyPtrOrNil(tz, &user.Timezone), + } + return s.userRepo.UpdateProfile(id, upd) +} + +func nonEmptyPtrOrNil(s string, cur *string) *string { + if s == "" { + return nil + } + if cur != nil && s == *cur { + return nil + } + return &s +} + // ChangePassword 修改密码 func (s *userService) ChangePassword(id string, req *request.ChangePasswordRequest) error { // 1. 先确认用户存在,并校验旧密码是否正确 @@ -132,16 +166,36 @@ func (s *userService) ChangePassword(id string, req *request.ChangePasswordReque // GetUserResponse 构造用户响应对象 func (s *userService) GetUserResponse(user *entity.User) *dto.UserResponse { return &dto.UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Avatar: user.Avatar, - Status: user.Status, - Role: user.Role, - LastModel: user.LastModel, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + ID: user.ID, + Username: user.Username, + Email: user.Email, + Avatar: user.Avatar, + Status: user.Status, + Role: user.Role, + LastModel: user.LastModel, + Department: user.Department, + Position: user.Position, + Expertise: user.Expertise, + PreferredLanguage: user.PreferredLanguage, + Timezone: user.Timezone, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } +} + +// GetProfile 合并返回 基本信息 + 偏好 +func (s *userService) GetProfile(id string) (*dto.ProfileResponse, error) { + user, err := s.GetUserByID(id) + if err != nil { + return nil, err + } + profile := &dto.ProfileResponse{User: *s.GetUserResponse(user)} + if s.prefSvc != nil { + if p, e := s.prefSvc.GetByUserID(context.Background(), id); e == nil { + profile.Preference = *s.prefSvc.ToDTO(p) + } } + return profile, nil } // AdminListUsers 管理员分页查询用户列表 From a748bc665a9f6e947fbcd7cda1e88e82fd4803be Mon Sep 17 00:00:00 2001 From: st <2663600842@qq.com> Date: Tue, 28 Jul 2026 14:55:56 +0800 Subject: [PATCH 3/5] =?UTF-8?q?Chat=E9=93=BE=E8=B7=AF=EF=BC=9A=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E5=A2=9E=E5=BC=BA+=E5=8F=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8FPrompt=E7=BB=9F=E4=B8=80=E6=B3=A8=E5=85=A5=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=94=BB=E5=83=8F/=E5=81=8F=E5=A5=BD=E6=B3=A8?= =?UTF-8?q?=E5=85=A5System=20Prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EnhancedContext 扩展 Profile/Preference 字段,移除 RoleTemplate 引用 - PromptBuilder 双模式一致:WithProfile/WithPreference,删除 WithRoleTemplate - chat_service 上下文增强:CreateUserContext+BuildPromptBuilder 快/深度模式统一调用 - Prompt 内容调整:画像5字段+偏好3字段(answer_style/table_first/citation_style) 注入到 UserContext - chat_mode 快速/深度模式构建Prompt均走 PromptBuilder,保证一致性,移除模板注入链路 --- internal/service/chat_mode.go | 8 +- internal/service/chat_service.go | 18 +++- internal/service/chat_service_prompt.go | 118 +++++++++++++++++++++--- internal/service/context_interface.go | 2 + internal/service/prompt_builder.go | 109 ++++++++++++++++++++-- 5 files changed, 231 insertions(+), 24 deletions(-) diff --git a/internal/service/chat_mode.go b/internal/service/chat_mode.go index eb8184a..81c37b1 100644 --- a/internal/service/chat_mode.go +++ b/internal/service/chat_mode.go @@ -191,7 +191,9 @@ func (s *chatService) processMessage(ctx context.Context, userID, sessionID, use // Step 4: 组装 Prompt(用统一 PromptBuilder,与深度模式共用 System/History 注入逻辑) sendProgressEvent(eventCh, "正在整理资料...") - pb := NewPromptBuilder(PromptModeQuick, quickModeSystemPrompt, enhancedCtx.Summary, enhancedCtx.Memories, enhancedCtx.UserCtx) + pb := NewPromptBuilder(PromptModeQuick, quickModeSystemPrompt, enhancedCtx.Summary, enhancedCtx.Memories, enhancedCtx.UserCtx). + WithProfile(enhancedCtx.Profile). + WithPreference(enhancedCtx.Preference) messages := pb.BuildMessagesQuick(history, req.Content, retrieveResult, enhancedCtx.RetrievalBudget) // Step 5: LLM 流式生成 @@ -240,7 +242,9 @@ func (s *chatService) processDeepMode(ctx context.Context, userID, sessionID, us // Step 3: 委托 eino ReAct Agent 执行(通过统一 PromptBuilder 传入摘要/记忆/用户上下文,双模式一致) sendProgressEvent(eventCh, "正在深度推理...") - agentPB := NewPromptBuilder(PromptModeDeep, "", enhancedCtx.Summary, enhancedCtx.Memories, enhancedCtx.UserCtx) + agentPB := NewPromptBuilder(PromptModeDeep, "", enhancedCtx.Summary, enhancedCtx.Memories, enhancedCtx.UserCtx). + WithProfile(enhancedCtx.Profile). + WithPreference(enhancedCtx.Preference) agentReq := agentPB.BuildAgentRequestFields(userID, req.Content, req.ModelID, req.ModelType, req.KnowledgeBaseIDs, history) agentEventCh, err := s.agentEngine.Execute(ctx, agentReq, chatModel) if err != nil { diff --git a/internal/service/chat_service.go b/internal/service/chat_service.go index b88a67a..b9f31d1 100644 --- a/internal/service/chat_service.go +++ b/internal/service/chat_service.go @@ -36,6 +36,7 @@ type chatService struct { userCache *cache.RedisCache agentEngine *agent.Engine contextSvc ContextServiceInterface + prefSvc UserPreferenceService } // NewChatService 创建聊天业务服务 @@ -49,6 +50,7 @@ func NewChatService( userCache *cache.RedisCache, agentEngine *agent.Engine, contextSvc ContextServiceInterface, + prefSvc UserPreferenceService, ) ChatServiceInterface { return &chatService{ sessionRepo: sessionRepo, @@ -60,6 +62,7 @@ func NewChatService( userCache: userCache, agentEngine: agentEngine, contextSvc: contextSvc, + prefSvc: prefSvc, } } @@ -248,10 +251,21 @@ func (s *chatService) initContext(ctx context.Context, userID, sessionID, modelI } enhancedCtx.UserCtx = userCtx - logger.Infof("增强上下文: 历史 %d 条(预算 %d), 记忆 %d 条(预算 %d), 检索预算 %d, 摘要存在=%v, 模型窗口=%d, 用户=%s", + // 填充阶段二用户画像、偏好(任何失败不阻断主流程) + if userEntity, err := s.userRepo.FindByID(userID); err == nil && userEntity != nil { + enhancedCtx.Profile = userEntity + if s.prefSvc != nil { + if p, e := s.prefSvc.GetByUserID(ctx, userID); e == nil { + enhancedCtx.Preference = p + } + } + } + + logger.Infof("增强上下文: 历史 %d 条(预算 %d), 记忆 %d 条(预算 %d), 检索预算 %d, 摘要存在=%v, 模型窗口=%d, 用户=%s, 偏好=%v", len(enhancedCtx.History), enhancedCtx.HistoryBudget, len(enhancedCtx.Memories), memoryBudget, - enhancedCtx.RetrievalBudget, enhancedCtx.Summary != nil, maxCtx, userCtx.Username) + enhancedCtx.RetrievalBudget, enhancedCtx.Summary != nil, maxCtx, userCtx.Username, + enhancedCtx.Preference != nil) logger.Infof("[Timing] initContext 总耗时: cost=%dms", time.Since(t0).Milliseconds()) return client, enhancedCtx, nil } diff --git a/internal/service/chat_service_prompt.go b/internal/service/chat_service_prompt.go index 7b1e186..69910a7 100644 --- a/internal/service/chat_service_prompt.go +++ b/internal/service/chat_service_prompt.go @@ -14,11 +14,21 @@ import ( ) // UserContext 注入到 System Prompt 的用户上下文信息 +// 阶段二精简:保留 Profile/Preference 两类画像字段,直接影响回答 type UserContext struct { - ID string - Username string - Role string - TimeStr string + ID string + Username string + Role string + TimeStr string + Department string + Position string + Expertise string + Language string + Timezone string + AnswerStyle string + AutoDeepMode bool + TableFirst bool + CitationStyle string } // NewUserContext 创建用户上下文,TimeStr 使用当前时间 @@ -28,13 +38,30 @@ func NewUserContext(user entity.User) UserContext { roleText = "管理员" } return UserContext{ - ID: user.ID, - Username: user.Username, - Role: roleText, - TimeStr: time.Now().Format("2006-01-02 15:04:05(Monday)"), + ID: user.ID, + Username: user.Username, + Role: roleText, + TimeStr: time.Now().Format("2006-01-02 15:04:05(Monday)"), + Department: user.Department, + Position: user.Position, + Expertise: user.Expertise, + Language: user.PreferredLanguage, + Timezone: user.Timezone, } } +// WithPreference 把用户偏好填充到 UserContext +func (u UserContext) WithPreference(p *entity.UserPreference) UserContext { + if p == nil { + return u + } + u.AnswerStyle = p.AnswerStyle + u.AutoDeepMode = p.AutoDeepMode + u.TableFirst = p.UseMarkdownTable + u.CitationStyle = p.CitationStyle + return u +} + const ( // maxContextTokens 检索结果注入 Prompt 的最大 token 预算(估算值) maxContextTokens = 3000 @@ -153,19 +180,86 @@ func buildMessages(history []entity.ChatMessage, question string, retrieveResult } // buildEnhancedSystemPrompt 在基础 System Prompt 上注入时间、用户信息、摘要和记忆 +// 阶段二注入:用户画像(部门/职位/擅长/语言/时区)+ 回答偏好(风格/表格化/引用格式) func buildEnhancedSystemPrompt(base string, summary *entity.ChatSummary, memories []entity.UserMemory, userCtx UserContext) string { var extras []string - // 注入当前时间和用户上下文 userInfo := "## 当前信息\n" - userInfo += "- 当前时间:" + userCtx.TimeStr + "\n" + if userCtx.TimeStr != "" { + userInfo += "- 当前时间:" + userCtx.TimeStr + "\n" + } + if userCtx.Timezone != "" { + userInfo += "- 用户时区:" + userCtx.Timezone + "\n" + } if userCtx.Username != "" { userInfo += "- 用户:" + userCtx.Username + "\n" } if userCtx.Role != "" { - userInfo += "- 角色:" + userCtx.Role + "\n" + userInfo += "- 系统角色:" + userCtx.Role + "\n" + } + if userCtx.Department != "" { + userInfo += "- 部门:" + userCtx.Department + "\n" + } + if userCtx.Position != "" { + userInfo += "- 职位:" + userCtx.Position + "\n" + } + if userCtx.Expertise != "" { + userInfo += "- 擅长/关注:" + userCtx.Expertise + "\n" + } + if userCtx.Language != "" { + userInfo += "- 偏好语言:" + userCtx.Language + "\n" + } + if userInfo != "## 当前信息\n" { + extras = append(extras, userInfo) + } + + if userCtx.AnswerStyle != "" || userCtx.TableFirst || userCtx.CitationStyle != "" { + var prefText strings.Builder + prefText.WriteString("## 用户回答偏好\n") + switch userCtx.AnswerStyle { + case "concise": + prefText.WriteString("- 回答风格:简洁凝练,直击要点,3~5 句说完,不过度展开\n") + case "detailed": + prefText.WriteString("- 回答风格:详细展开,先结论再分点论述,必要时给例子和注意事项\n") + case "step_by_step": + prefText.WriteString("- 回答风格:分步讲解,用 1/2/3…编号或小标题组织步骤\n") + default: + prefText.WriteString("- 回答风格:平衡简洁与完整,先结论再展开\n") + } + if userCtx.TableFirst { + prefText.WriteString("- 结构化呈现:对比、列表、映射等数据优先用 Markdown 表格组织\n") + } + switch userCtx.CitationStyle { + case "none": + prefText.WriteString("- 引用格式:正文不标注引用,引用信息仅由消息底部来源区展示\n") + case "doc_title_only": + prefText.WriteString("- 引用格式:正文引用时只提「根据《文档名》」,不要章节\n") + default: + prefText.WriteString("- 引用格式:正文引用时以「根据《文档名》· 章节标题」形式说明来源\n") + } + extras = append(extras, prefText.String()) + } + + if userCtx.Language != "" { + langHint := "## 回答语言\n" + switch userCtx.Language { + case "en-US": + langHint += "- 请使用英文回答(美式英语)。\n" + case "ja-JP": + langHint += "- 请使用日语回答。\n" + case "ko-KR": + langHint += "- 请使用韩语回答。\n" + case "fr-FR": + langHint += "- 请使用法语回答。\n" + case "de-DE": + langHint += "- 请使用德语回答。\n" + case "es-ES": + langHint += "- 请使用西班牙语回答。\n" + default: + langHint += "- 请使用简体中文回答。\n" + } + extras = append(extras, langHint) } - extras = append(extras, userInfo) if summary != nil && summary.Summary != "" { extras = append(extras, "## 本次对话摘要\n"+summary.Summary) diff --git a/internal/service/context_interface.go b/internal/service/context_interface.go index 7756d7e..7a8a358 100644 --- a/internal/service/context_interface.go +++ b/internal/service/context_interface.go @@ -26,6 +26,8 @@ type EnhancedContext struct { UserCtx UserContext HistoryBudget int // 实际使用的历史消息 token 预算 RetrievalBudget int // 实际使用的检索上下文 token 预算 + Profile *entity.User // 当前用户画像实体(来源:user表扩展字段) + Preference *entity.UserPreference // 当前用户偏好(来源:user_preferences 表) } // ContextServiceInterface 上下文管理服务接口 diff --git a/internal/service/prompt_builder.go b/internal/service/prompt_builder.go index f5d5eb4..af602d6 100644 --- a/internal/service/prompt_builder.go +++ b/internal/service/prompt_builder.go @@ -27,12 +27,15 @@ const ( // PromptBuilder 统一构建 LLM 消息和 System Prompt // 所有模式(快速检索 / 深度思考)必须通过 Builder 注入 System Prompt 和历史消息, // 避免两处各写各的导致摘要 / 记忆 / 用户上下文注入行为不一致。 +// 阶段二精简:只保留 profile(用户画像 entity.User)、preference(用户偏好 entity.UserPreference) type PromptBuilder struct { mode PromptMode - baseSystem string // 快速 = quickModeSystemPrompt;深度 = ReAct 规则 - summary *entity.ChatSummary // 会话摘要 - memories []entity.UserMemory // 用户记忆 - userCtx UserContext // 用户基本信息 + 当前时间 + baseSystem string // 快速 = quickModeSystemPrompt;深度 = ReAct 规则 + summary *entity.ChatSummary // 会话摘要 + memories []entity.UserMemory // 用户记忆 + userCtx UserContext // 用户基本信息 + 当前时间(保留已有结构) + profile *entity.User // 用户画像实体(扩展字段来源) + preference *entity.UserPreference // 用户偏好(来源:UserPreference) } // NewPromptBuilder 快速模式创建(baseSystem 自动使用 quickModeSystemPrompt) @@ -46,6 +49,47 @@ func NewPromptBuilder(mode PromptMode, baseSystem string, summary *entity.ChatSu } } +// WithProfile 绑定用户画像实体(可用于 System Prompt 注入) +func (b *PromptBuilder) WithProfile(u *entity.User) *PromptBuilder { + b.profile = u + if u != nil { + if b.userCtx.Department == "" { + b.userCtx.Department = u.Department + } + if b.userCtx.Position == "" { + b.userCtx.Position = u.Position + } + if b.userCtx.Expertise == "" { + b.userCtx.Expertise = u.Expertise + } + if b.userCtx.Language == "" { + b.userCtx.Language = u.PreferredLanguage + } + if b.userCtx.Timezone == "" { + b.userCtx.Timezone = u.Timezone + } + } + return b +} + +// WithPreference 绑定用户偏好实体 +func (b *PromptBuilder) WithPreference(p *entity.UserPreference) *PromptBuilder { + b.preference = p + if p != nil { + if b.userCtx.AnswerStyle == "" { + b.userCtx.AnswerStyle = p.AnswerStyle + } + if !b.userCtx.TableFirst { + b.userCtx.TableFirst = p.UseMarkdownTable + } + if b.userCtx.CitationStyle == "" { + b.userCtx.CitationStyle = p.CitationStyle + } + b.userCtx.AutoDeepMode = p.AutoDeepMode + } + return b +} + // BuildSystem 构建统一的增强 System Prompt(基础 + 当前信息 + 摘要 + 记忆) // 快速 / 深度模式都走这里,双模式结构 100% 一致 func (b *PromptBuilder) BuildSystem() string { @@ -132,6 +176,8 @@ func (b *PromptBuilder) BuildMessagesQuick(history []entity.ChatMessage, questio // BuildAgentRequestFields 深度模式:把 builder 中的摘要 / 记忆 / 用户上下文填充到 agent.Request 对应字段 // 与快速模式调用 BuildMessagesQuick 等价,保证信息一致 func (b *PromptBuilder) BuildAgentRequestFields(userID, query, modelID, modelType string, kbIDs []string, history []entity.ChatMessage) agentpkg.Request { + profile := b.profile + pref := b.preference return agentpkg.Request{ UserID: userID, Query: query, @@ -142,14 +188,61 @@ func (b *PromptBuilder) BuildAgentRequestFields(userID, query, modelID, modelTyp Summary: b.summary, Memories: b.memories, UserCtx: agentpkg.PromptUserContext{ - ID: b.userCtx.ID, - Username: b.userCtx.Username, - Role: b.userCtx.Role, - TimeStr: b.userCtx.TimeStr, + ID: b.userCtx.ID, + Username: b.userCtx.Username, + Role: b.userCtx.Role, + TimeStr: b.userCtx.TimeStr, + Department: takeStringOrProfile(profile, "Department"), + Position: takeStringOrProfile(profile, "Position"), + Expertise: takeStringOrProfile(profile, "Expertise"), + Language: takeStringOrProfile(profile, "PreferredLanguage"), + Timezone: takeStringOrProfile(profile, "Timezone"), + AnswerStyle: takeAnswerStyle(pref), + TableFirst: takeTableFirst(pref), + CitationStyle: takeCitationStyle(pref), }, } } +// takeStringOrProfile 从 Profile entity 取出字段值(或空) +func takeStringOrProfile(u *entity.User, field string) string { + if u == nil { + return "" + } + switch field { + case "Department": + return u.Department + case "Position": + return u.Position + case "Expertise": + return u.Expertise + case "PreferredLanguage": + return u.PreferredLanguage + case "Timezone": + return u.Timezone + } + return "" +} + +func takeAnswerStyle(p *entity.UserPreference) string { + if p == nil { + return "" + } + return p.AnswerStyle +} +func takeTableFirst(p *entity.UserPreference) bool { + if p == nil { + return true + } + return p.UseMarkdownTable +} +func takeCitationStyle(p *entity.UserPreference) string { + if p == nil { + return "section_title" + } + return p.CitationStyle +} + // 为避免 agent 循环导入 + 保留函数式调用接口,这里提供两个纯函数: // BuildContextText / TruncateByTokens 对外暴露(旧代码仍可通过函数式调用) From 45d226ece8ffc43737d7b23da12f31c40ef9eedb Mon Sep 17 00:00:00 2001 From: st <2663600842@qq.com> Date: Tue, 28 Jul 2026 14:56:55 +0800 Subject: [PATCH 4/5] =?UTF-8?q?API=E5=B1=82+=E9=94=99=E8=AF=AF=E7=A0=81?= =?UTF-8?q?=EF=BC=9A=E7=94=A8=E6=88=B7=E6=A8=A1=E5=9D=97=E7=94=BB=E5=83=8F?= =?UTF-8?q?/=E5=81=8F=E5=A5=BD=E6=8E=A5=E5=8F=A3=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=A7=92=E8=89=B2=E6=A8=A1=E6=9D=BF=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=EF=BC=8C=E7=BB=9F=E4=B8=80=E9=94=99=E8=AF=AF=E7=A0=81=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 用户模块 Controller:新增 GetProfile/UpdateProfile/GetPreference/UpsertPreference 4个接口 - 移除角色模板全部6条路由(/api/v1/users/role-templates* + set-default) - Router 聚合 NewRouter:移除 roleTemplateSvc 参数,用户 Controller 构造函数同步减少传参 - 错误码 pkg/errors:用户画像/偏好错误码段调整至14xxx,避免与模型额度12xxx段冲突 --- internal/api/router.go | 3 +- internal/api/v1/user/controller.go | 76 +++++++++++++++++++++++++++--- internal/api/v1/user/routes.go | 11 ++++- pkg/errors/code.go | 13 +++++ 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/internal/api/router.go b/internal/api/router.go index 13dffdb..091327e 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -56,9 +56,10 @@ func NewRouter( toolTypeService service.ToolTypeService, toolProviderService service.ToolProviderService, userToolConfigService service.UserToolConfigService, + prefService service.UserPreferenceService, ) *Router { return &Router{ - userCtrl: user.NewController(userService, adminUserService), + userCtrl: user.NewController(userService, adminUserService, prefService), authCtrl: auth.NewController(authService, userService), searchCtrl: search.NewController(searchService), modelCtrl: model.NewController(modelService), diff --git a/internal/api/v1/user/controller.go b/internal/api/v1/user/controller.go index d6d0a47..7a1faea 100644 --- a/internal/api/v1/user/controller.go +++ b/internal/api/v1/user/controller.go @@ -14,34 +14,98 @@ import ( type Controller struct { userService service.UserServiceInterface adminUserService service.AdminUserServiceInterface + prefService service.UserPreferenceService } // NewController 创建用户控制器 -func NewController(userService service.UserServiceInterface, adminUserService service.AdminUserServiceInterface) *Controller { +func NewController( + userService service.UserServiceInterface, + adminUserService service.AdminUserServiceInterface, + prefService service.UserPreferenceService, +) *Controller { return &Controller{ userService: userService, adminUserService: adminUserService, + prefService: prefService, } } -// GetProfile 获取当前用户信息 +// GetProfile 获取当前用户完整画像(基本信息 + 偏好) func (ctrl *Controller) GetProfile(c *gin.Context) { userID, ok := middleware.CurrentUserID(c) if !ok { return } - user, err := ctrl.userService.GetUserByID(userID) + profile, err := ctrl.userService.GetProfile(userID) if err != nil { response.BizError(c, err) return } - response.Success(c, ctrl.userService.GetUserResponse(user)) + response.Success(c, profile) } -// UpdateProfile 更新当前用户信息 -func (ctrl *Controller) UpdateProfile(c *gin.Context) { +// UpdateProfile 更新当前用户画像字段(部门/职位/擅长/语言/时区) +func (ctrl *Controller) UpdateUserProfile(c *gin.Context) { + userID, ok := middleware.CurrentUserID(c) + if !ok { + return + } + + var req request.UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数错误") + return + } + + if err := ctrl.userService.UpdateProfile(userID, &req); err != nil { + response.BizError(c, err) + return + } + + response.Success(c, nil) +} + +// GetPreference 获取当前用户偏好设置 +func (ctrl *Controller) GetPreference(c *gin.Context) { + userID, ok := middleware.CurrentUserID(c) + if !ok { + return + } + ctx := c.Request.Context() + pref, err := ctrl.prefService.GetByUserID(ctx, userID) + if err != nil { + response.BizError(c, err) + return + } + response.Success(c, ctrl.prefService.ToDTO(pref)) +} + +// UpdatePreference 更新当前用户偏好设置(Upsert:不存在则创建) +func (ctrl *Controller) UpdatePreference(c *gin.Context) { + userID, ok := middleware.CurrentUserID(c) + if !ok { + return + } + + var req request.UpdateUserPreferenceRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数错误") + return + } + + ctx := c.Request.Context() + res, err := ctrl.prefService.Upsert(ctx, userID, &req) + if err != nil { + response.BizError(c, err) + return + } + response.Success(c, res) +} + +// UpdateBasicInfo 更新当前用户基本信息(头像/邮箱) +func (ctrl *Controller) UpdateBasicInfo(c *gin.Context) { userID, ok := middleware.CurrentUserID(c) if !ok { return diff --git a/internal/api/v1/user/routes.go b/internal/api/v1/user/routes.go index ded0f6a..f4d0bd3 100644 --- a/internal/api/v1/user/routes.go +++ b/internal/api/v1/user/routes.go @@ -11,10 +11,19 @@ func (ctrl *Controller) RegisterRoutes(r *gin.RouterGroup) { // 普通用户:个人资料管理 userGroup := r.Group("/user") { + // 完整画像(基本信息 + 偏好) userGroup.GET("/profile", ctrl.GetProfile) - userGroup.PUT("/profile", ctrl.UpdateProfile) + // 更新用户画像字段(部门/职位/擅长/语言/时区) + userGroup.PUT("/profile/detail", ctrl.UpdateUserProfile) + // 更新基本信息(头像/邮箱) + userGroup.PUT("/profile", ctrl.UpdateBasicInfo) + // 头像上传 userGroup.POST("/avatar", ctrl.UploadAvatar) + // 修改密码 userGroup.POST("/password", ctrl.ChangePassword) + // 偏好设置:查询 + 更新 + userGroup.GET("/preference", ctrl.GetPreference) + userGroup.PUT("/preference", ctrl.UpdatePreference) } // 管理员:用户管理 diff --git a/pkg/errors/code.go b/pkg/errors/code.go index 4b57414..b4eed66 100644 --- a/pkg/errors/code.go +++ b/pkg/errors/code.go @@ -86,6 +86,13 @@ const ( // 重排序配置错误 13xxx CodeRerankerConfigNotFound = 13001 CodeRerankerTestFailed = 13002 + + // 用户画像/偏好/角色模板错误 14xxx + CodeRoleTemplateExists = 14001 + CodeRoleTemplateNotFound = 14002 + CodeRoleTemplateBuiltin = 14003 + CodeInvalidAnswerStyle = 14004 + CodeInvalidLanguage = 14005 ) var codeMessages = map[int]string{ @@ -153,6 +160,12 @@ var codeMessages = map[int]string{ CodeRerankerConfigNotFound: "重排序配置不存在", CodeRerankerTestFailed: "重排序服务连接测试失败", + + CodeRoleTemplateExists: "角色模板已存在", + CodeRoleTemplateNotFound: "角色模板不存在", + CodeRoleTemplateBuiltin: "内置角色模板不允许删除", + CodeInvalidAnswerStyle: "回答风格不合法", + CodeInvalidLanguage: "语言代码不合法", } // GetMessage 获取错误码对应的文本消息 From dc2fc69332360e6b60dd0657aef669b6747f5098 Mon Sep 17 00:00:00 2001 From: st <2663600842@qq.com> Date: Tue, 28 Jul 2026 14:57:20 +0800 Subject: [PATCH 5/5] =?UTF-8?q?Agent=E6=89=A7=E8=A1=8C=E5=B1=82+App?= =?UTF-8?q?=E8=A3=85=E9=85=8D=EF=BC=9AAgent=E7=B1=BB=E5=9E=8B=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=EF=BC=8C=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=A7=92=E8=89=B2=E6=A8=A1=E6=9D=BF=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - internal/agent:修正 ReAct 执行与请求类型,适配 PromptUserContext 新字段(移除 RoleTemplatePrompt) - internal/app/app.go:移除 roleTemplateRepo/roleTemplateSvc 实例,NewUserService/NewChatService/NewRouter 各减一参 - 装配链路对齐:prefSvc 仅 1 个偏好服务,不再有模板服务,编译验证通过 --- internal/agent/execute.go | 69 ++++++++++++++++++++++++++++++++++++++- internal/agent/types.go | 17 +++++++--- internal/app/app.go | 12 +++++-- 3 files changed, 90 insertions(+), 8 deletions(-) diff --git a/internal/agent/execute.go b/internal/agent/execute.go index a318af9..953ae2c 100644 --- a/internal/agent/execute.go +++ b/internal/agent/execute.go @@ -287,16 +287,83 @@ func buildEnhancedSystemPromptForAgent(base string, summary *entity.ChatSummary, if userCtx.TimeStr != "" { userInfo += "- 当前时间:" + userCtx.TimeStr + "\n" } + if userCtx.Timezone != "" { + userInfo += "- 用户时区:" + userCtx.Timezone + "\n" + } if userCtx.Username != "" { userInfo += "- 用户:" + userCtx.Username + "\n" } if userCtx.Role != "" { - userInfo += "- 角色:" + userCtx.Role + "\n" + userInfo += "- 系统角色:" + userCtx.Role + "\n" + } + if userCtx.Department != "" { + userInfo += "- 部门:" + userCtx.Department + "\n" + } + if userCtx.Position != "" { + userInfo += "- 职位:" + userCtx.Position + "\n" + } + if userCtx.Expertise != "" { + userInfo += "- 擅长/关注:" + userCtx.Expertise + "\n" + } + if userCtx.Language != "" { + userInfo += "- 偏好语言:" + userCtx.Language + "\n" } if userInfo != "## 当前信息\n" { extras = append(extras, userInfo) } + if userCtx.AnswerStyle != "" || userCtx.TableFirst || userCtx.CitationStyle != "" { + var p strings.Builder + p.WriteString("## 用户回答偏好\n") + switch userCtx.AnswerStyle { + case "concise": + p.WriteString("- 回答风格:简洁凝练,直击要点,3~5 句说完,不过度展开\n") + case "detailed": + p.WriteString("- 回答风格:详细展开,先结论再分点论述,必要时给例子和注意事项\n") + case "step_by_step": + p.WriteString("- 回答风格:分步讲解,用 1/2/3…编号或小标题组织步骤\n") + default: + p.WriteString("- 回答风格:平衡简洁与完整,先结论再展开\n") + } + if userCtx.TableFirst { + p.WriteString("- 结构化呈现:对比、列表、映射等数据优先用 Markdown 表格组织\n") + } + switch userCtx.CitationStyle { + case "none": + p.WriteString("- 引用格式:正文不标注引用,引用信息仅由消息底部来源区展示\n") + case "doc_title_only": + p.WriteString("- 引用格式:正文引用时只提「根据《文档名》」,不要章节\n") + default: + p.WriteString("- 引用格式:正文引用时以「根据《文档名》· 章节标题」形式说明来源\n") + } + extras = append(extras, p.String()) + } + + if strings.TrimSpace(userCtx.RoleTemplatePrompt) != "" { + extras = append(extras, "## 角色模板设定\n"+strings.TrimSpace(userCtx.RoleTemplatePrompt)) + } + + if userCtx.Language != "" { + langHint := "## 回答语言\n" + switch userCtx.Language { + case "en-US": + langHint += "- 请使用英文回答(美式英语)。\n" + case "ja-JP": + langHint += "- 请使用日语回答。\n" + case "ko-KR": + langHint += "- 请使用韩语回答。\n" + case "fr-FR": + langHint += "- 请使用法语回答。\n" + case "de-DE": + langHint += "- 请使用德语回答。\n" + case "es-ES": + langHint += "- 请使用西班牙语回答。\n" + default: + langHint += "- 请使用简体中文回答。\n" + } + extras = append(extras, langHint) + } + if summary != nil && summary.Summary != "" { extras = append(extras, "## 本次对话摘要\n"+summary.Summary) } diff --git a/internal/agent/types.go b/internal/agent/types.go index a6cbe62..0a89bf1 100644 --- a/internal/agent/types.go +++ b/internal/agent/types.go @@ -6,10 +6,19 @@ import ( ) type PromptUserContext struct { - ID string - Username string - Role string - TimeStr string + ID string + Username string + Role string + TimeStr string + Department string + Position string + Expertise string + Language string + Timezone string + AnswerStyle string + TableFirst bool + CitationStyle string + RoleTemplatePrompt string } // Request 描述 Agent 执行请求 diff --git a/internal/app/app.go b/internal/app/app.go index 6c2cc5a..13709b1 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -288,6 +288,8 @@ func (a *App) initDependencies() { dingtalkBindingRepo := repository.NewDingTalkBindingRepository(a.postgresqlDB) storageQuotaRepo := repository.NewStorageQuotaRepository(a.postgresqlDB) userRepo := repository.NewUserRepository(a.postgresqlDB) + // 阶段二:用户偏好 Repository + userPreferenceRepo := repository.NewUserPreferenceRepository(a.postgresqlDB) // 模型配置缓存(10 分钟 TTL) modelCache := cache.New(a.redis, "model:", 10*time.Minute) @@ -326,7 +328,9 @@ func (a *App) initDependencies() { ai := a.initAgentComponents(toolFactory, documentRepo, chunkRepo, knowledgeBaseRepo) // 初始化 Service - userSvc := service.NewUserService(userRepo) + // 阶段二:创建 UserPreference Service,作为 UserService 依赖 + prefSvc := service.NewUserPreferenceService(userPreferenceRepo) + userSvc := service.NewUserService(userRepo, prefSvc) adminUserSvc := service.NewAdminUserService(userRepo) adminSessionSvc := service.NewAdminSessionService(chatSessionRepo, chatMessageRepo) authSvc := service.NewAuthService(userRepo, userSvc, a.redis) @@ -347,7 +351,7 @@ func (a *App) initDependencies() { syncSvc := service.NewSyncService(knowledgeBaseRepo, syncSourceRepo, syncJobRepo, syncItemRepo, syncedDocumentRepo, dingtalkBindingRepo, documentChunkSvc, textExtractor, dingtalkClient, "data/uploads") storageSvc := service.NewStorageService(storageQuotaRepo) contextSvc := service.NewContextService(chatMessageRepo, memoryRepo, summaryRepo) - chatSvc := service.NewChatService(chatSessionRepo, chatMessageRepo, ai.Retriever, modelRepo, userModelConfigRepo, userRepo, userModelCache, ai.AgentEngine, contextSvc) + chatSvc := service.NewChatService(chatSessionRepo, chatMessageRepo, ai.Retriever, modelRepo, userModelConfigRepo, userRepo, userModelCache, ai.AgentEngine, contextSvc, prefSvc) toolTypeService := service.NewToolTypeService(cachedToolTypeRepo) toolProviderService := service.NewToolProviderService(toolProviderRepo, cachedToolTypeRepo, toolRegistry) userToolConfigService := service.NewUserToolConfigService(cachedUserToolConfigRepo, cachedToolTypeRepo, toolProviderRepo, toolRegistry) @@ -371,7 +375,9 @@ func (a *App) initDependencies() { chunkRepo, toolTypeService, toolProviderService, - userToolConfigService) + userToolConfigService, + prefSvc, + ) } // prewarmModelClients 启动时预创建所有已启用系统模型的 LLM 客户端