package ai import ( "context" "encoding/json" "fmt" "resume-platform/internal/ai" "resume-platform/internal/prompt" "resume-platform/pkg/logger" "strings" "sync" ) type QuestionGeneratorService struct { provider ai.Provider } func NewQuestionGeneratorService(provider ai.Provider) *QuestionGeneratorService { return &QuestionGeneratorService{provider: provider} } func (s *QuestionGeneratorService) GenerateQuestions(ctx context.Context, resumeText, keywordsStr, typesStr string) ([]map[string]interface{}, error) { if resumeText == "" && keywordsStr == "" { return nil, fmt.Errorf("resume_text or keywords is required") } if resumeText == "" { resumeText = "基于关键词:" + keywordsStr } techKeywords := extractTechKeywords(resumeText) if len(techKeywords) == 0 { techKeywords = []string{"HTML", "CSS", "JavaScript", "Go", "MySQL", "Redis"} } if keywordsStr != "" { techKeywords = strings.Split(keywordsStr, ",") } selectedTypes := parseTypes(typesStr) logger.CtxInfof(ctx, "[QuestionGenerator] Generating questions for keywords: %v, types: %v", techKeywords, selectedTypes) var wg sync.WaitGroup resultChan := make(chan []map[string]interface{}, len(selectedTypes)) errChan := make(chan error, len(selectedTypes)) for _, qType := range selectedTypes { wg.Add(1) go func(t string) { defer wg.Done() prompt := buildQuestionGenerationPromptForType(resumeText, techKeywords, t) logger.CtxInfof(ctx, "[QuestionGenerator] Generating %s questions...", t) result, err := s.provider.Generate(ctx, prompt) if err != nil { logger.CtxErrorf(ctx, "[QuestionGenerator] AI generate failed for %s: %v", t, err) errChan <- err return } questions, err := parseAIQuestionsWithCtx(ctx, result) if err != nil { logger.CtxErrorf(ctx, "[QuestionGenerator] Parse failed for %s: %v", t, err) errChan <- err return } if len(questions) > 0 { resultChan <- questions logger.CtxInfof(ctx, "[QuestionGenerator] Generated %d %s questions", len(questions), t) } }(qType) } go func() { wg.Wait() close(resultChan) close(errChan) }() var allQuestions []map[string]interface{} for questions := range resultChan { allQuestions = append(allQuestions, questions...) } logger.CtxInfof(ctx, "[QuestionGenerator] Total generated %d questions", len(allQuestions)) if len(allQuestions) == 0 { return nil, fmt.Errorf("未能生成任何题目") } return allQuestions, nil } func buildQuestionGenerationPrompt(resumeText string, keywords []string, types []string) string { return prompt.BuildQuestionGenerationPrompt(resumeText, keywords, types) } func buildQuestionGenerationPromptForType(resumeText string, keywords []string, qType string) string { return prompt.BuildQuestionGenerationPromptForType(resumeText, keywords, qType) } func parseAIQuestionsWithCtx(ctx context.Context, result string) ([]map[string]interface{}, error) { result = strings.TrimSpace(result) logger.CtxInfof(ctx, "[QuestionGenerator] AI response length: %d", len(result)) logger.CtxInfof(ctx, "[QuestionGenerator] AI response raw result (first 2000 chars): %s", substr(result, 0, 2000)) result = strings.ReplaceAll(result, "```json", "") result = strings.ReplaceAll(result, "```", "") result = strings.TrimSpace(result) startIdx := strings.Index(result, "[") endIdx := strings.LastIndex(result, "]") if startIdx == -1 || endIdx == -1 || endIdx <= startIdx { logger.CtxErrorf(ctx, "[QuestionGenerator] Cannot find JSON array boundaries") return nil, fmt.Errorf("无法找到JSON数组边界") } jsonStr := result[startIdx : endIdx+1] logger.CtxInfof(ctx, "[QuestionGenerator] Extracted JSON array (length=%d)", len(jsonStr)) var questions []map[string]interface{} if err := json.Unmarshal([]byte(jsonStr), &questions); err != nil { logger.CtxErrorf(ctx, "[QuestionGenerator] Failed to unmarshal as array: %v", err) logger.CtxErrorf(ctx, "[QuestionGenerator] Raw result that failed (first 500 chars): %s", substr(jsonStr, 0, 500)) return nil, fmt.Errorf("AI返回的JSON格式不正确: %w", err) } var validQuestions []map[string]interface{} for _, q := range questions { if !isValidQuestion(q) { logger.CtxWarnf(ctx, "[QuestionGenerator] Skipping invalid question: %v", q) continue } q = normalizeQuestion(q) validQuestions = append(validQuestions, q) } logger.CtxInfof(ctx, "[QuestionGenerator] Successfully parsed %d valid questions (filtered %d invalid)", len(validQuestions), len(questions)-len(validQuestions)) if len(validQuestions) == 0 { return nil, fmt.Errorf("未解析到有效题目") } return validQuestions, nil } func isValidQuestion(q map[string]interface{}) bool { if q == nil { return false } qType := fmt.Sprintf("%v", q["type"]) text := fmt.Sprintf("%v", q["text"]) answer := fmt.Sprintf("%v", q["answer"]) if qType == "" || text == "" || answer == "" { return false } if qType == "mcq" { if options, ok := q["options"].([]interface{}); ok { if len(options) != 4 { return false } for _, opt := range options { if fmt.Sprintf("%v", opt) == "" { return false } } } else { return false } answer = strings.ToUpper(strings.TrimSpace(answer)) if len(answer) != 1 || (answer < "A" || answer > "D") { return false } } if qType == "fill" { if !strings.Contains(text, "___") { return false } } if qType == "sa" || qType == "algo" { if len(answer) < 10 { return false } } analysis := fmt.Sprintf("%v", q["analysis"]) if len(analysis) < 30 { return false } return true } func normalizeQuestion(q map[string]interface{}) map[string]interface{} { qType := fmt.Sprintf("%v", q["type"]) if qType == "mcq" { answer := strings.TrimSpace(fmt.Sprintf("%v", q["answer"])) if len(answer) > 0 { q["answer"] = strings.ToUpper(answer[:1]) } if options, ok := q["options"].([]interface{}); ok { var optionStrings []string for _, opt := range options { optStr := strings.TrimSpace(fmt.Sprintf("%v", opt)) optionStrings = append(optionStrings, optStr) } q["options"] = optionStrings } } if _, ok := q["score"]; !ok { diff := fmt.Sprintf("%v", q["difficulty"]) q["score"] = getScoreByDifficulty(diff) } if _, ok := q["category"]; !ok { q["category"] = "综合" } if _, ok := q["keywords"]; !ok { q["keywords"] = []string{} } if _, ok := q["options"]; !ok { q["options"] = []string{} } if _, ok := q["analysis"]; !ok { q["analysis"] = "" } if _, ok := q["difficulty"]; !ok { q["difficulty"] = "初级" } return q } func isSingleLetter(s string) bool { return len(s) == 1 && (s >= "A" && s <= "Z" || s >= "a" && s <= "z") } func substr(s string, start, length int) string { if len(s) <= start { return "" } end := start + length if end > len(s) { end = len(s) } return s[start:end] } func extractTechKeywords(text string) []string { techPatterns := []string{"Go", "Golang", "Java", "Python", "JavaScript", "TypeScript", "React", "Vue", "MySQL", "PostgreSQL", "Redis", "MongoDB", "Kafka", "Docker", "Kubernetes", "微服务", "分布式"} var result []string for _, pattern := range techPatterns { if strings.Contains(text, pattern) { result = append(result, pattern) } } return result } func parseTypes(typesStr string) []string { if typesStr == "" { return []string{"mcq", "fill", "sa", "algo"} } return strings.Split(typesStr, ",") } func getScoreByDifficulty(difficulty string) int { switch difficulty { case "入门": return 5 case "初级": return 10 case "中级": return 15 case "进阶": return 20 case "高级": return 25 default: return 10 } }