首次提交:初始化项目代码
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,594 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"resume-platform/internal/agent"
|
||||
"resume-platform/internal/model"
|
||||
aiservice "resume-platform/internal/service/ai"
|
||||
"resume-platform/internal/service/quiz"
|
||||
"resume-platform/internal/service/resume"
|
||||
"resume-platform/pkg/logger"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AIHandler AI 能力处理器,封装简历润色、分析、题目生成、评分等 AI 相关接口
|
||||
type AIHandler struct {
|
||||
aiService *aiservice.AIService
|
||||
questionGenerator *aiservice.QuestionGeneratorService
|
||||
resumeAgent *agent.ResumeAgent
|
||||
resumeService resume.ResumeService
|
||||
quizService quiz.QuizService
|
||||
}
|
||||
|
||||
// NewAIHandler 创建 AI 处理器实例
|
||||
// @param aiService AI 服务层
|
||||
// @param questionGenerator 题目生成服务层
|
||||
// @param resumeAgent AI Agent 实例
|
||||
// @param resumeService 简历服务层
|
||||
// @param quizService 答题服务层
|
||||
// @return *AIHandler 处理器实例
|
||||
// @author sunct
|
||||
func NewAIHandler(aiService *aiservice.AIService, questionGenerator *aiservice.QuestionGeneratorService, resumeAgent *agent.ResumeAgent, resumeService resume.ResumeService, quizService quiz.QuizService) *AIHandler {
|
||||
return &AIHandler{
|
||||
aiService: aiService,
|
||||
questionGenerator: questionGenerator,
|
||||
resumeAgent: resumeAgent,
|
||||
resumeService: resumeService,
|
||||
quizService: quizService,
|
||||
}
|
||||
}
|
||||
|
||||
// SetResumeAgent 设置 ResumeAgent(延迟初始化使用)
|
||||
// @param resumeAgent AI Agent 实例
|
||||
func (h *AIHandler) SetResumeAgent(resumeAgent *agent.ResumeAgent) {
|
||||
h.resumeAgent = resumeAgent
|
||||
}
|
||||
|
||||
// buildResumeText 将简历对象拼接为纯文本,用于 AI 关键词提取
|
||||
// @param resume 简历数据
|
||||
// @return string 拼接后的纯文本
|
||||
// @author sunct
|
||||
func buildResumeText(resume *model.Resume) string {
|
||||
if resume == nil {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString(resume.BasicInfo.Name + " ")
|
||||
sb.WriteString(resume.BasicInfo.Title + " ")
|
||||
sb.WriteString(resume.BasicInfo.Summary + " ")
|
||||
sb.WriteString(resume.BasicInfo.Industry + " ")
|
||||
sb.WriteString(resume.BasicInfo.JobTarget + " ")
|
||||
for _, e := range resume.Experience {
|
||||
sb.WriteString(e.Company + " ")
|
||||
sb.WriteString(e.Position + " ")
|
||||
sb.WriteString(e.Description + " ")
|
||||
for _, h := range e.Highlights {
|
||||
sb.WriteString(h + " ")
|
||||
}
|
||||
}
|
||||
for _, s := range resume.Skills {
|
||||
sb.WriteString(s.Name + " ")
|
||||
sb.WriteString(s.Category + " ")
|
||||
}
|
||||
for _, p := range resume.Projects {
|
||||
sb.WriteString(p.Name + " ")
|
||||
sb.WriteString(p.Description + " ")
|
||||
for _, t := range p.TechStack {
|
||||
sb.WriteString(t + " ")
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// PolishText 单字段润色接口,根据字段类型调用不同的 AI 润色 Prompt
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *AIHandler) PolishText(c *gin.Context) {
|
||||
var req struct {
|
||||
Original string `json:"original"`
|
||||
FieldType string `json:"field_type"`
|
||||
Company string `json:"company,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
ProjectName string `json:"project_name,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.Original == "" {
|
||||
BadRequest(c, "original text is required")
|
||||
return
|
||||
}
|
||||
|
||||
var result string
|
||||
var err error
|
||||
|
||||
switch req.FieldType {
|
||||
case "summary":
|
||||
result, err = h.aiService.PolishSummary(c.Request.Context(), req.Original)
|
||||
case "experience":
|
||||
result, err = h.aiService.PolishExperience(c.Request.Context(), req.Original, req.Company, req.Position)
|
||||
case "project":
|
||||
result, err = h.aiService.PolishProject(c.Request.Context(), req.Original, req.ProjectName)
|
||||
default:
|
||||
result, err = h.aiService.PolishDescription(c.Request.Context(), req.Original, req.FieldType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"result": result,
|
||||
"provider": h.aiService.GetProviderName(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetProviderInfo 获取当前使用的 AI 供应商名称
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *AIHandler) GetProviderInfo(c *gin.Context) {
|
||||
provider := h.aiService.GetProviderName()
|
||||
if h.resumeAgent != nil {
|
||||
provider = h.resumeAgent.GetProvider()
|
||||
}
|
||||
OK(c, gin.H{"provider": provider})
|
||||
}
|
||||
|
||||
// AgentPolish Agent 模式润色接口,优先使用 Agent 工具链,降级为普通 AI 润色
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *AIHandler) AgentPolish(c *gin.Context) {
|
||||
var req struct {
|
||||
Original string `json:"original"`
|
||||
FieldType string `json:"field_type"`
|
||||
Company string `json:"company,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
ProjectName string `json:"project_name,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.Original == "" {
|
||||
BadRequest(c, "original text is required")
|
||||
return
|
||||
}
|
||||
|
||||
context := ""
|
||||
if req.Company != "" {
|
||||
context += "公司:" + req.Company + " "
|
||||
}
|
||||
if req.Position != "" {
|
||||
context += "职位:" + req.Position + " "
|
||||
}
|
||||
if req.ProjectName != "" {
|
||||
context += "项目名称:" + req.ProjectName
|
||||
}
|
||||
|
||||
if h.resumeAgent != nil {
|
||||
query := fmt.Sprintf("请帮我优化简历的%s字段。原文:%s", getFieldTypeName(req.FieldType), req.Original)
|
||||
if context != "" {
|
||||
query += "。上下文:" + context
|
||||
}
|
||||
|
||||
result, err := h.resumeAgent.Run(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"result": result,
|
||||
"provider": h.resumeAgent.GetProvider(),
|
||||
"agent_mode": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var result string
|
||||
var err error
|
||||
|
||||
switch req.FieldType {
|
||||
case "summary":
|
||||
result, err = h.aiService.PolishSummary(c.Request.Context(), req.Original)
|
||||
case "experience":
|
||||
result, err = h.aiService.PolishExperience(c.Request.Context(), req.Original, req.Company, req.Position)
|
||||
case "project":
|
||||
result, err = h.aiService.PolishProject(c.Request.Context(), req.Original, req.ProjectName)
|
||||
default:
|
||||
result, err = h.aiService.PolishDescription(c.Request.Context(), req.Original, req.FieldType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"result": result,
|
||||
"provider": h.aiService.GetProviderName(),
|
||||
"agent_mode": false,
|
||||
})
|
||||
}
|
||||
|
||||
// AgentAnalyze 简历整体分析接口,调用 Agent 对简历进行专业评估
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *AIHandler) AgentAnalyze(c *gin.Context) {
|
||||
var req struct {
|
||||
ResumeText string `json:"resume_text"`
|
||||
TargetPosition string `json:"target_position,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.ResumeText == "" {
|
||||
BadRequest(c, "resume_text is required")
|
||||
return
|
||||
}
|
||||
|
||||
if h.resumeAgent != nil {
|
||||
query := "请分析以下简历内容并提供专业评估:" + req.ResumeText
|
||||
if req.TargetPosition != "" {
|
||||
query += "。目标职位:" + req.TargetPosition
|
||||
}
|
||||
|
||||
result, err := h.resumeAgent.Run(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"result": result,
|
||||
"provider": h.resumeAgent.GetProvider(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"result": "Eino Agent not available",
|
||||
"provider": h.aiService.GetProviderName(),
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateQuestions AI 生成面试题接口,支持按简历内容或关键词出题
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *AIHandler) GenerateQuestions(c *gin.Context) {
|
||||
var req struct {
|
||||
ResumeID string `json:"resume_id,omitempty"`
|
||||
ResumeText string `json:"resume_text,omitempty"`
|
||||
Keywords string `json:"keywords,omitempty"`
|
||||
Types string `json:"types,omitempty"`
|
||||
Seed string `json:"seed,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.ResumeText == "" && req.ResumeID == "" && req.Keywords == "" {
|
||||
BadRequest(c, "resume_text, resume_id or keywords is required")
|
||||
return
|
||||
}
|
||||
|
||||
resumeText := req.ResumeText
|
||||
keywords := req.Keywords
|
||||
|
||||
// 当提供 resume_id 时,真正获取简历内容并提取文本
|
||||
if req.ResumeID != "" {
|
||||
resume, err := h.resumeService.GetResume(req.ResumeID)
|
||||
if err != nil || resume == nil {
|
||||
Error(c, "简历不存在")
|
||||
return
|
||||
}
|
||||
text := buildResumeText(resume)
|
||||
if text != "" {
|
||||
// 简历文本作为基础,与已有 resumeText 合并
|
||||
if resumeText != "" {
|
||||
resumeText = resumeText + " " + text
|
||||
} else {
|
||||
resumeText = text
|
||||
}
|
||||
}
|
||||
// 从简历技能中补充关键词
|
||||
if resume != nil && len(resume.Skills) > 0 && keywords == "" {
|
||||
var skillNames []string
|
||||
for _, s := range resume.Skills {
|
||||
if s.Name != "" {
|
||||
skillNames = append(skillNames, s.Name)
|
||||
}
|
||||
}
|
||||
if len(skillNames) > 0 {
|
||||
keywords = strings.Join(skillNames, ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当只有 keywords 时,构造基础文本
|
||||
if resumeText == "" && keywords != "" {
|
||||
resumeText = "基于关键词:" + keywords
|
||||
}
|
||||
|
||||
if resumeText == "" {
|
||||
BadRequest(c, "无法获取简历内容或关键词")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
logger.CtxInfof(ctx, "[AIHandler] GenerateQuestions resume_id=%s, keywords=%s, resume_text_len=%d", req.ResumeID, keywords, len(resumeText))
|
||||
|
||||
var questions []map[string]interface{}
|
||||
var err error
|
||||
|
||||
if h.questionGenerator != nil {
|
||||
logger.CtxInfof(ctx, "[AIHandler] Using AI question generator")
|
||||
questions, err = h.questionGenerator.GenerateQuestions(ctx, resumeText, keywords, req.Types)
|
||||
if err != nil {
|
||||
logger.CtxErrorf(ctx, "[AIHandler] GenerateQuestions failed: %v", err)
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.CtxInfof(ctx, "[AIHandler] Using fallback question generator")
|
||||
questions, err = h.generateFallbackQuestions(ctx, resumeText, keywords, req.Types, req.Seed)
|
||||
if err != nil {
|
||||
logger.CtxErrorf(ctx, "[AIHandler] generateFallbackQuestions failed: %v", err)
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(questions) == 0 {
|
||||
Error(c, "未找到与关键词匹配的题目,请尝试其他关键词")
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{"questions": questions})
|
||||
}
|
||||
|
||||
// generateFallbackQuestions 备用题目生成方法,当 AI 服务不可用时使用本地题库
|
||||
// @param resumeText 简历文本
|
||||
// @param keywords 关键词
|
||||
// @param types 题目类型
|
||||
// @param seed 随机种子
|
||||
// @return []map[string]interface{} 题目列表
|
||||
// @return error 错误信息
|
||||
// @author sunct
|
||||
func (h *AIHandler) generateFallbackQuestions(ctx context.Context, resumeText, keywords, types, seed string) ([]map[string]interface{}, error) {
|
||||
logger.CtxInfof(ctx, "[AIHandler] generateFallbackQuestions keywords=%s, types=%s", keywords, types)
|
||||
|
||||
tool := agent.NewGenerateQuestionsTool()
|
||||
|
||||
args := map[string]interface{}{
|
||||
"resume_text": resumeText,
|
||||
"keywords": keywords,
|
||||
"types": types,
|
||||
"seed": seed,
|
||||
"random": true,
|
||||
}
|
||||
|
||||
argsJSON, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
logger.CtxErrorf(ctx, "[AIHandler] Failed to marshal arguments: %v", err)
|
||||
return nil, fmt.Errorf("failed to marshal arguments: %w", err)
|
||||
}
|
||||
|
||||
resultJSON, err := tool.InvokableRun(ctx, string(argsJSON))
|
||||
if err != nil {
|
||||
logger.CtxErrorf(ctx, "[AIHandler] tool.InvokableRun failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var questions []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(resultJSON), &questions); err != nil {
|
||||
logger.CtxErrorf(ctx, "[AIHandler] Failed to parse questions: %v", err)
|
||||
return nil, fmt.Errorf("failed to parse questions: %w", err)
|
||||
}
|
||||
|
||||
logger.CtxInfof(ctx, "[AIHandler] generateFallbackQuestions success, count=%d", len(questions))
|
||||
|
||||
return questions, nil
|
||||
}
|
||||
|
||||
// EvaluateAnswers 本地答题评分接口,选择/填空按答案比对,简答按非空判定
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *AIHandler) EvaluateAnswers(c *gin.Context) {
|
||||
var req struct {
|
||||
Questions []map[string]interface{} `json:"questions"`
|
||||
Answers map[string]string `json:"answers"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Questions) == 0 {
|
||||
BadRequest(c, "questions is required")
|
||||
return
|
||||
}
|
||||
|
||||
correctCount := 0
|
||||
|
||||
for i, question := range req.Questions {
|
||||
answerKey := fmt.Sprintf("%d", i)
|
||||
userAnswer := req.Answers[answerKey]
|
||||
correctAnswer := question["answer"]
|
||||
|
||||
if correctAnswer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
correctStr := fmt.Sprintf("%v", correctAnswer)
|
||||
|
||||
if question["type"] == "mcq" || question["type"] == "fill" {
|
||||
if strings.EqualFold(strings.TrimSpace(userAnswer), strings.TrimSpace(correctStr)) {
|
||||
correctCount++
|
||||
}
|
||||
} else if question["type"] == "sa" {
|
||||
if len(strings.TrimSpace(userAnswer)) > 0 {
|
||||
correctCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalQuestions := len(req.Questions)
|
||||
score := float64(correctCount) / float64(totalQuestions) * 100
|
||||
|
||||
OK(c, gin.H{
|
||||
"score": score,
|
||||
"correct_count": correctCount,
|
||||
"total_count": totalQuestions,
|
||||
})
|
||||
}
|
||||
|
||||
// SubmitAnswers 提交答题记录接口,计算得分并保存答题记录到数据库
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *AIHandler) SubmitAnswers(c *gin.Context) {
|
||||
var req struct {
|
||||
Questions []map[string]interface{} `json:"questions"`
|
||||
UserAnswers map[string]string `json:"user_answers"`
|
||||
Score float64 `json:"score"`
|
||||
CorrectCount int `json:"correct_count"`
|
||||
TotalCount int `json:"total_count"`
|
||||
UnansweredCount int `json:"unanswered_count"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
ResumeID string `json:"resume_id,omitempty"`
|
||||
ResumeRoute string `json:"resume_route,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Questions) == 0 {
|
||||
BadRequest(c, "questions is required")
|
||||
return
|
||||
}
|
||||
|
||||
questions := make([]model.QuizQuestion, 0, len(req.Questions))
|
||||
for _, q := range req.Questions {
|
||||
question := model.QuizQuestion{
|
||||
Type: getStringValue(q, "type"),
|
||||
Text: getStringValue(q, "text"),
|
||||
Answer: getStringValue(q, "answer"),
|
||||
Analysis: getStringValue(q, "analysis"),
|
||||
Score: getIntValue(q, "score"),
|
||||
Difficulty: getStringValue(q, "difficulty"),
|
||||
Category: getStringValue(q, "category"),
|
||||
}
|
||||
|
||||
if options, ok := q["options"].([]interface{}); ok {
|
||||
question.Options = make([]string, 0, len(options))
|
||||
for _, opt := range options {
|
||||
question.Options = append(question.Options, fmt.Sprintf("%v", opt))
|
||||
}
|
||||
}
|
||||
|
||||
if keywords, ok := q["keywords"].([]interface{}); ok {
|
||||
question.Keywords = make([]string, 0, len(keywords))
|
||||
for _, kw := range keywords {
|
||||
question.Keywords = append(question.Keywords, fmt.Sprintf("%v", kw))
|
||||
}
|
||||
}
|
||||
|
||||
questions = append(questions, question)
|
||||
}
|
||||
|
||||
record := &model.QuizRecord{
|
||||
UserID: req.UserID,
|
||||
ResumeID: req.ResumeID,
|
||||
ResumeRoute: req.ResumeRoute,
|
||||
Title: req.Title,
|
||||
Questions: questions,
|
||||
UserAnswers: req.UserAnswers,
|
||||
Score: req.Score,
|
||||
CorrectCount: req.CorrectCount,
|
||||
TotalCount: req.TotalCount,
|
||||
}
|
||||
|
||||
if h.quizService != nil {
|
||||
err := h.quizService.CreateQuizRecord(record)
|
||||
if err != nil {
|
||||
logger.CtxErrorf(c.Request.Context(), "[AIHandler] SubmitAnswers failed to save record: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("答题完成!得分:%.1f分", req.Score)
|
||||
if req.UnansweredCount > 0 {
|
||||
message += fmt.Sprintf("(%d/%d正确,%d题未作答)", req.CorrectCount, req.TotalCount-req.UnansweredCount, req.UnansweredCount)
|
||||
} else {
|
||||
message += fmt.Sprintf("(%d/%d正确)", req.CorrectCount, req.TotalCount)
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"message": message,
|
||||
"score": req.Score,
|
||||
"correct_count": req.CorrectCount,
|
||||
"total_count": req.TotalCount,
|
||||
"unanswered_count": req.UnansweredCount,
|
||||
"record_id": record.ID,
|
||||
}, message)
|
||||
}
|
||||
|
||||
func getStringValue(m map[string]interface{}, key string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getIntValue(m map[string]interface{}, key string) int {
|
||||
if v, ok := m[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
return val
|
||||
case float64:
|
||||
return int(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getFieldTypeName 将字段类型英文标识转换为中文名称
|
||||
// @param fieldType 字段类型
|
||||
// @return string 中文名称
|
||||
// @author sunct
|
||||
func getFieldTypeName(fieldType string) string {
|
||||
switch fieldType {
|
||||
case "summary":
|
||||
return "个人简介"
|
||||
case "experience":
|
||||
return "工作经历"
|
||||
case "project":
|
||||
return "项目描述"
|
||||
case "education":
|
||||
return "教育背景"
|
||||
case "skill":
|
||||
return "技能描述"
|
||||
default:
|
||||
return "内容"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"resume-platform/internal/service/chat"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChatHandler 聊天对话处理器,基于文档向量库进行问答
|
||||
type ChatHandler struct {
|
||||
chatService chat.ChatService
|
||||
}
|
||||
|
||||
// NewChatHandler 创建聊天处理器实例
|
||||
// @param chatService 聊天服务层
|
||||
// @return *ChatHandler 处理器实例
|
||||
// @author sunct
|
||||
func NewChatHandler(chatService chat.ChatService) *ChatHandler {
|
||||
return &ChatHandler{chatService: chatService}
|
||||
}
|
||||
|
||||
// Chat 流式对话接口,以 SSE 方式返回 AI 回答
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *ChatHandler) Chat(c *gin.Context) {
|
||||
userID := c.Query("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Question string `json:"question"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
answer, err := h.chatService.Chat(c.Request.Context(), userID, request.Question)
|
||||
if err != nil {
|
||||
c.SSEvent("error", gin.H{"message": err.Error()})
|
||||
return false
|
||||
}
|
||||
|
||||
c.SSEvent("message", answer)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// GetChatHistory 获取用户聊天历史记录接口
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *ChatHandler) GetChatHistory(c *gin.Context) {
|
||||
userID := c.Query("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
history, err := h.chatService.GetChatHistory(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, history)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"resume-platform/internal/service/document"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DocumentHandler 文档处理器,负责作品集文档的上传、查询和删除
|
||||
type DocumentHandler struct {
|
||||
documentService document.DocumentService
|
||||
}
|
||||
|
||||
// NewDocumentHandler 创建文档处理器实例
|
||||
// @param documentService 文档服务层
|
||||
// @return *DocumentHandler 处理器实例
|
||||
// @author sunct
|
||||
func NewDocumentHandler(documentService document.DocumentService) *DocumentHandler {
|
||||
return &DocumentHandler{documentService: documentService}
|
||||
}
|
||||
|
||||
// UploadDocument 上传文档接口,读取文件内容后交给 Service 层做向量化处理
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *DocumentHandler) UploadDocument(c *gin.Context) {
|
||||
userID := c.Query("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
|
||||
return
|
||||
}
|
||||
|
||||
fileType := filepath.Ext(file.Filename)
|
||||
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open file"})
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"})
|
||||
return
|
||||
}
|
||||
|
||||
document, err := h.documentService.UploadDocument(c.Request.Context(), userID, file.Filename, fileType, file.Size, string(content))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"id": document.ID, "name": document.Name, "status": document.Status})
|
||||
}
|
||||
|
||||
// GetDocuments 获取用户文档列表接口
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *DocumentHandler) GetDocuments(c *gin.Context) {
|
||||
userID := c.Query("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
documents, err := h.documentService.GetDocuments(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, documents)
|
||||
}
|
||||
|
||||
// GetDocument 获取单篇文档详情接口
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *DocumentHandler) GetDocument(c *gin.Context) {
|
||||
userID := c.Query("user_id")
|
||||
documentID := c.Param("id")
|
||||
|
||||
if userID == "" || documentID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id and document_id are required"})
|
||||
return
|
||||
}
|
||||
|
||||
document, err := h.documentService.GetDocument(c.Request.Context(), userID, documentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "document not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, document)
|
||||
}
|
||||
|
||||
// DeleteDocument 删除文档接口
|
||||
// @param c Gin 上下文
|
||||
// @author sunct
|
||||
func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
|
||||
userID := c.Query("user_id")
|
||||
documentID := c.Param("id")
|
||||
|
||||
if userID == "" || documentID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id and document_id are required"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.documentService.DeleteDocument(c.Request.Context(), userID, documentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "document deleted successfully"})
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"resume-platform/internal/service/chat"
|
||||
"resume-platform/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
type IMHandler struct {
|
||||
imService *chat.IMService
|
||||
}
|
||||
|
||||
func NewIMHandler(imService *chat.IMService) *IMHandler {
|
||||
return &IMHandler{imService: imService}
|
||||
}
|
||||
|
||||
func (h *IMHandler) WebSocket(c *gin.Context) {
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ctx := c.Request.Context()
|
||||
logger.CtxInfof(ctx, "WebSocket connection opened")
|
||||
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
logger.CtxInfof(ctx, "WebSocket connection closed normally")
|
||||
} else {
|
||||
logger.CtxErrorf(ctx, "WebSocket read error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(msg, &request); err != nil {
|
||||
logger.CtxErrorf(ctx, "Failed to parse message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if request.Message == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
go h.handleMessage(ctx, conn, request.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *IMHandler) handleMessage(ctx context.Context, conn *websocket.Conn, message string) {
|
||||
logger.CtxInfof(ctx, "Received message: %s", message)
|
||||
|
||||
agent := h.imService.GetResumeAgent()
|
||||
var result string
|
||||
var err error
|
||||
if agent != nil {
|
||||
logger.CtxInfof(ctx, "Using Eino Resume Agent for request")
|
||||
result, err = h.imService.GenerateWithAgent(ctx, message)
|
||||
} else {
|
||||
logger.CtxInfof(ctx, "Using AI Provider for request")
|
||||
result, err = h.imService.GetAIProvider().Generate(ctx, message)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.CtxErrorf(ctx, "AI generate failed: %v", err)
|
||||
response := map[string]interface{}{
|
||||
"type": "error",
|
||||
"message": err.Error(),
|
||||
}
|
||||
if err := conn.WriteJSON(response); err != nil {
|
||||
logger.CtxErrorf(ctx, "Failed to send error response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"type": "message",
|
||||
"content": result,
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(response); err != nil {
|
||||
logger.CtxErrorf(ctx, "Failed to send response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *IMHandler) Chat(c *gin.Context) {
|
||||
var request struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
if request.Message == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "message is required"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
logger.CtxInfof(ctx, "Chat request: %s", request.Message)
|
||||
|
||||
result, err := h.imService.GetAIProvider().Generate(ctx, request.Message)
|
||||
if err != nil {
|
||||
logger.CtxErrorf(ctx, "AI generate failed: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"response": result})
|
||||
}
|
||||
|
||||
func (h *IMHandler) ChatStream(c *gin.Context) {
|
||||
var request struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
if request.Message == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "message is required"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
logger.CtxInfof(ctx, "Chat stream request: %s", request.Message)
|
||||
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
result, err := h.imService.GetAIProvider().Generate(ctx, request.Message)
|
||||
if err != nil {
|
||||
logger.CtxErrorf(ctx, "AI generate failed: %v", err)
|
||||
c.SSEvent("error", gin.H{"message": err.Error()})
|
||||
return false
|
||||
}
|
||||
|
||||
for _, char := range result {
|
||||
c.SSEvent("message", string(char))
|
||||
}
|
||||
c.SSEvent("done", gin.H{})
|
||||
return false
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"resume-platform/internal/model"
|
||||
"resume-platform/internal/service/knowledge"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type KnowledgeHandler struct {
|
||||
knowledgeBaseService *knowledge.KnowledgeBaseService
|
||||
ragService *knowledge.RAGService
|
||||
}
|
||||
|
||||
func NewKnowledgeHandler(knowledgeBaseService *knowledge.KnowledgeBaseService, ragService *knowledge.RAGService) *KnowledgeHandler {
|
||||
return &KnowledgeHandler{knowledgeBaseService: knowledgeBaseService, ragService: ragService}
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) Search(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
topK := 5
|
||||
if k := c.Query("k"); k != "" {
|
||||
fmt.Sscanf(k, "%d", &topK)
|
||||
}
|
||||
userID := c.GetHeader("X-User-ID")
|
||||
|
||||
if query == "" {
|
||||
BadRequest(c, "Query parameter is required")
|
||||
return
|
||||
}
|
||||
|
||||
results, err := h.knowledgeBaseService.Retrieve(c.Request.Context(), query, topK, userID)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, results)
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) GetStats(c *gin.Context) {
|
||||
userID := c.GetHeader("X-User-ID")
|
||||
|
||||
stats, err := h.knowledgeBaseService.GetStats(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, stats)
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) AddDocument(c *gin.Context) {
|
||||
var req struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
Content string `json:"content"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.DocumentID == "" || req.Content == "" {
|
||||
BadRequest(c, "document_id and content are required")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.knowledgeBaseService.AddDocument(c.Request.Context(), req.DocumentID, req.Content, req.UserID)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{"message": "Document added to knowledge base successfully"})
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) AddResume(c *gin.Context) {
|
||||
var req struct {
|
||||
ResumeID string `json:"resume_id"`
|
||||
Resume map[string]interface{} `json:"resume"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.ResumeID == "" {
|
||||
BadRequest(c, "resume_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
resumeJSON, _ := json.Marshal(req.Resume)
|
||||
var resume model.Resume
|
||||
if err := json.Unmarshal(resumeJSON, &resume); err != nil {
|
||||
BadRequest(c, "Invalid resume format")
|
||||
return
|
||||
}
|
||||
|
||||
resume.ID = req.ResumeID
|
||||
resume.UserID = req.UserID
|
||||
|
||||
err := h.knowledgeBaseService.AddResume(c.Request.Context(), req.ResumeID, &resume)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{"message": "Resume added to knowledge base successfully"})
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) AddText(c *gin.Context) {
|
||||
var req struct {
|
||||
Text string `json:"text"`
|
||||
SourceType string `json:"source_type"`
|
||||
SourceID string `json:"source_id"`
|
||||
SourceName string `json:"source_name"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Text == "" || req.SourceType == "" {
|
||||
BadRequest(c, "text and source_type are required")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.knowledgeBaseService.AddText(c.Request.Context(), req.Text, req.SourceType, req.SourceID, req.SourceName, req.UserID)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{"message": "Text added to knowledge base successfully"})
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) Chat(c *gin.Context) {
|
||||
var req struct {
|
||||
Query string `json:"query"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Query == "" {
|
||||
BadRequest(c, "query is required")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.ragService.Generate(c.Request.Context(), req.Query, req.UserID)
|
||||
if err != nil {
|
||||
Error(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{"answer": result})
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Package handler 处理所有 HTTP 请求,负责参数校验、调用 Service 层和返回响应
|
||||
// 按业务模块分为简历管理、后台管理、AI 能力、文档处理、聊天对话等处理器
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// APIResponse 统一 API 响应结构体,所有接口返回格式遵循此结构
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"` // 请求是否成功
|
||||
Message string `json:"message,omitempty"`// 提示消息
|
||||
Data interface{} `json:"data,omitempty"` // 响应数据
|
||||
Error string `json:"error,omitempty"` // 错误信息
|
||||
}
|
||||
|
||||
// OK 返回 200 成功响应,附带数据和可选消息
|
||||
// @param c Gin 上下文
|
||||
// @param data 响应数据
|
||||
// @param message 可选提示消息
|
||||
// @author sunct
|
||||
func OK(c *gin.Context, data interface{}, message ...string) {
|
||||
msg := ""
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
c.JSON(http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// Created 返回 201 创建成功响应
|
||||
// @param c Gin 上下文
|
||||
// @param data 创建的资源数据
|
||||
// @param message 可选提示消息
|
||||
// @author sunct
|
||||
func Created(c *gin.Context, data interface{}, message ...string) {
|
||||
msg := ""
|
||||
if len(message) > 0 {
|
||||
msg = message[0]
|
||||
}
|
||||
c.JSON(http.StatusCreated, APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// Success 返回 200 成功响应,仅包含提示消息
|
||||
// @param c Gin 上下文
|
||||
// @param message 成功提示消息
|
||||
// @author sunct
|
||||
func Success(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// BadRequest 返回 400 参数错误响应
|
||||
// @param c Gin 上下文
|
||||
// @param message 错误提示消息
|
||||
// @author sunct
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusBadRequest, APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// NotFound 返回 404 资源不存在响应
|
||||
// @param c Gin 上下文
|
||||
// @param message 错误提示消息
|
||||
// @author sunct
|
||||
func NotFound(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusNotFound, APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Conflict 返回 409 资源冲突响应(如重复创建)
|
||||
// @param c Gin 上下文
|
||||
// @param message 错误提示消息
|
||||
// @author sunct
|
||||
func Conflict(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusConflict, APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Unauthorized 返回 401 未授权响应
|
||||
// @param c Gin 上下文
|
||||
// @param message 错误提示消息
|
||||
// @author sunct
|
||||
func Unauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// TooManyRequests 返回 429 请求过于频繁响应(限流)
|
||||
// @param c Gin 上下文
|
||||
// @param message 错误提示消息
|
||||
// @author sunct
|
||||
func TooManyRequests(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusTooManyRequests, APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Error 返回 500 服务器内部错误响应
|
||||
// @param c Gin 上下文
|
||||
// @param message 错误提示消息
|
||||
// @author sunct
|
||||
func Error(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusInternalServerError, APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// SuccessResponse 返回成功响应(支持标准库 http.ResponseWriter)
|
||||
// @param w http.ResponseWriter
|
||||
// @param data 响应数据
|
||||
func SuccessResponse(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorResponse 返回错误响应(支持标准库 http.ResponseWriter)
|
||||
// @param w http.ResponseWriter
|
||||
// @param statusCode HTTP 状态码
|
||||
// @param message 错误消息
|
||||
func ErrorResponse(w http.ResponseWriter, statusCode int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user