package service import ( "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "regexp" "strings" "time" "memora-api/internal/config" "memora-api/internal/model" "gorm.io/gorm" ) type WordService struct { db *gorm.DB } func NewWordService(db *gorm.DB) *WordService { return &WordService{db: db} } // 查询单词(调用有道API) func (s *WordService) QueryWord(word string) (*model.YoudaoResponse, error) { // 优先用 app_id,兼容 app_key appID := config.AppConfig.Youdao.AppID if appID == "" { appID = config.AppConfig.Youdao.AppKey } appSecret := config.AppConfig.Youdao.AppSecret // 未配置有道密钥时,先走 mock(保证流程可跑通) if strings.TrimSpace(appID) == "" || strings.TrimSpace(appSecret) == "" { return &model.YoudaoResponse{ErrorCode: "0"}, nil } // 有道 API 签名算法: sign = MD5(appKey + q + salt + appSecret) salt := fmt.Sprintf("%d", time.Now().UnixMilli()) q := strings.ToLower(word) rawStr := appID + q + salt + appSecret hash := md5.Sum([]byte(rawStr)) sign := hex.EncodeToString(hash[:]) // 调用有道 API - 智能词典查询 url := fmt.Sprintf("https://openapi.youdao.com/api?q=%s&from=en&to=zh_CHS&appKey=%s&salt=%s&sign=%s", word, appID, salt, sign) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var result model.YoudaoResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } // Debug: 打印响应 fmt.Printf("[Youdao API] word=%s, errorCode=%s, translation=%v\n", word, result.ErrorCode, result.Translation) return &result, nil } // 下载音频文件 func (s *WordService) DownloadAudio(url, filePath string) error { if url == "" { return nil } resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("download failed: %d", resp.StatusCode) } // 创建目录 dir := filePath[:strings.LastIndex(filePath, "/")] if err := os.MkdirAll(dir, 0755); err != nil { return err } file, err := os.Create(filePath) if err != nil { return err } defer file.Close() _, err = io.Copy(file, resp.Body) return err } // 保存单词到数据库 func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (*model.Word, error) { var existingWord model.Word // 检查单词是否已存在 if err := s.db.Where("word = ?", word).First(&existingWord).Error; err == nil { // 单词已存在,更新记忆记录 s.updateMemoryRecord(existingWord.ID, true) return &existingWord, nil } // 解析有道API响应 var phoneticUK, phoneticUS, partOfSpeech, definition string // 解析音标 if youdaoResp.Basic.UkPhonetic != "" { phoneticUK = "[" + youdaoResp.Basic.UkPhonetic + "]" } else if youdaoResp.Basic.Phonetic != "" { phoneticUK = "[" + youdaoResp.Basic.Phonetic + "]" } if youdaoResp.Basic.UsPhonetic != "" { phoneticUS = "[" + youdaoResp.Basic.UsPhonetic + "]" } else if youdaoResp.Basic.Phonetic != "" { phoneticUS = "[" + youdaoResp.Basic.Phonetic + "]" } // 解析释义 - 优先用 translation(更准确) if len(youdaoResp.Translation) > 0 { definition = strings.Join(youdaoResp.Translation, "; ") } // 如果 basic 有 wfs,也加到释义里 if len(youdaoResp.Basic.Wfs) > 0 { var meanings []string for _, wf := range youdaoResp.Basic.Wfs { if len(wf.Means) > 0 { meanings = append(meanings, wf.Wf.Name+": "+wf.Means[0].Mean.Text) } } if len(meanings) > 0 { if definition != "" { definition += " | " } definition += strings.Join(meanings, "; ") } partOfSpeech = "basic" } if partOfSpeech == "" { partOfSpeech = "n./v./adj." } // 音频 URL - 有道返回的 speakUrl 示例: https://dict.youdao.com/dictvoice?audio=hello audioPath := config.AppConfig.Audio.Path var audioUK, audioUS string if youdaoResp.SpeakUrl != "" { // 默认是美音,加 _uk 是英音 audioUK = strings.Replace(youdaoResp.SpeakUrl, "audio=", "audio=word_uk_", 1) audioUS = youdaoResp.SpeakUrl // 异步下载音频文件 go func() { if audioPath != "" { ukPath := fmt.Sprintf("%s/%s_uk.mp3", audioPath, word) usPath := fmt.Sprintf("%s/%s_us.mp3", audioPath, word) s.DownloadAudio(audioUK, ukPath) s.DownloadAudio(audioUS, usPath) } }() audioUK = fmt.Sprintf("%s/%s_uk.mp3", audioPath, word) audioUS = fmt.Sprintf("%s/%s_us.mp3", audioPath, word) } // 创建新单词 newWord := model.Word{ Word: word, PhoneticUK: phoneticUK, PhoneticUS: phoneticUS, AudioUK: audioUK, AudioUS: audioUS, PartOfSpeech: partOfSpeech, Definition: definition, } if err := s.db.Create(&newWord).Error; err != nil { return nil, err } // 创建记忆记录 + 记一次"背过" _ = s.createMemoryRecord(newWord.ID) _ = s.updateMemoryRecord(newWord.ID, true) return &newWord, nil } // 创建记忆记录 func (s *WordService) createMemoryRecord(wordID int64) error { record := model.MemoryRecord{ WordID: wordID, UserID: 1, CorrectCount: 0, TotalCount: 0, MasteryLevel: 0, } return s.db.Create(&record).Error } // 更新记忆记录 func (s *WordService) updateMemoryRecord(wordID int64, correct bool) error { var record model.MemoryRecord if err := s.db.Where("word_id = ?", wordID).First(&record).Error; err != nil { return err } record.TotalCount++ if correct { record.CorrectCount++ // 计算掌握程度 record.MasteryLevel = (record.CorrectCount * 5) / (record.TotalCount + 1) if record.MasteryLevel > 5 { record.MasteryLevel = 5 } } now := time.Now() record.LastReviewedAt = &now // 根据掌握程度计算下次复习时间 reviewInterval := time.Hour * 24 * time.Duration(record.MasteryLevel+1) nextReview := now.Add(reviewInterval) record.NextReviewAt = &nextReview return s.db.Save(&record).Error } // 获取待复习单词 func (s *WordService) GetReviewWords(mode string, limit int) ([]model.MemoryRecord, error) { var records []model.MemoryRecord query := s.db.Preload("Word").Where("next_review_at <= ? OR next_review_at IS NULL", time.Now()) if err := query.Limit(limit).Find(&records).Error; err != nil { return nil, err } // 如果没有需要复习的,随机获取一些 if len(records) == 0 { if err := s.db.Preload("Word").Limit(limit).Find(&records).Error; err != nil { return nil, err } } return records, nil } func normalizeText(s string) string { s = strings.ToLower(strings.TrimSpace(s)) re := regexp.MustCompile(`[^\p{Han}a-z0-9]+`) s = re.ReplaceAllString(s, "") return s } func containsAny(def string, ans string) bool { nDef := normalizeText(def) nAns := normalizeText(ans) if nDef == "" || nAns == "" { return false } if strings.Contains(nDef, nAns) { return true } for _, token := range strings.Split(def, "|") { if strings.Contains(normalizeText(token), nAns) { return true } } return false } // 提交复习答案 func (s *WordService) SubmitReviewAnswer(req model.ReviewAnswerRequest) (*model.ReviewResult, error) { var record model.MemoryRecord if err := s.db.Preload("Word").Where("id = ?", req.RecordID).First(&record).Error; err != nil { return nil, err } word := record.Word correct := false correctAns := "" switch req.Mode { case "spelling": // 读音拼单词 correct = normalizeText(req.Answer) == normalizeText(word.Word) correctAns = word.Word case "en2cn": // 英文写中文 correct = containsAny(word.Definition, req.Answer) correctAns = word.Definition case "cn2en": // 中文写英文 correct = normalizeText(req.Answer) == normalizeText(word.Word) correctAns = word.Word default: correct = false correctAns = "模式错误" } // 更新记忆记录 s.updateMemoryRecord(record.WordID, correct) return &model.ReviewResult{ Word: word, Correct: correct, Answer: req.Answer, CorrectAns: correctAns, }, nil } // 获取所有单词 func (s *WordService) GetAllWords(limit, offset int) ([]model.Word, int64, error) { var words []model.Word var total int64 s.db.Model(&model.Word{}).Count(&total) if err := s.db.Limit(limit).Offset(offset).Order("created_at DESC").Find(&words).Error; err != nil { return nil, 0, err } return words, total, nil } // 获取记忆统计 func (s *WordService) GetStatistics() (map[string]interface{}, error) { var totalWords int64 var masteredWords int64 var needReview int64 var todayReviewed int64 s.db.Model(&model.Word{}).Count(&totalWords) s.db.Model(&model.MemoryRecord{}).Where("mastery_level >= 4").Count(&masteredWords) s.db.Model(&model.MemoryRecord{}).Where("next_review_at <= ?", time.Now()).Count(&needReview) s.db.Model(&model.MemoryRecord{}).Where("last_reviewed_at >= ?", time.Now().Format("2006-01-02")).Count(&todayReviewed) return map[string]interface{}{ "total_words": totalWords, "mastered_words": masteredWords, "need_review": needReview, "today_reviewed": todayReviewed, }, nil }