- 重构App.vue中的侧边栏布局,更新Logo设计为带有标识和副标题的新样式 - 调整顶部导航栏,增加标题区域显示当前路由标题和日期 - 修改菜单项配置,更新导航标签为更直观的中文描述 - 在Home.vue中替换原有的仪表板为新的Hero卡片和项目进展展示 - 更新Memory.vue中的学习界面,添加学习计划设置和多阶段学习模式 - 集成新的API端点路径,将baseURL从/api调整为/api/v1 - 调整整体视觉风格,包括颜色主题、字体家族和响应式布局 - 更新数据库模型以支持词库功能,添加相关的数据迁移和种子数据 - 调整认证系统的用户ID类型从整型到字符串的变更 - 更改前端构建工具从npm到pnpm,并更新相应的Dockerfile配置
196 lines
4.7 KiB
Go
196 lines
4.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"memora-api/internal/request"
|
|
"memora-api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type WordHandler struct {
|
|
wordService *service.WordService
|
|
}
|
|
|
|
func userIDFromContext(c *gin.Context) string {
|
|
v, ok := c.Get("user_id")
|
|
if !ok {
|
|
return ""
|
|
}
|
|
uid, ok := v.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return uid
|
|
}
|
|
|
|
func NewWordHandler(wordService *service.WordService) *WordHandler {
|
|
return &WordHandler{wordService: wordService}
|
|
}
|
|
|
|
// 添加单词(记忆模式)
|
|
func (h *WordHandler) AddWord(c *gin.Context) {
|
|
var req request.AddWordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
youdaoResp, err := h.wordService.QueryWord(req.Word)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if youdaoResp.ErrorCode != "0" && youdaoResp.ErrorCode != "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("有道API错误: %s", youdaoResp.ErrorCode)})
|
|
return
|
|
}
|
|
|
|
word, err := h.wordService.SaveWord(userIDFromContext(c), req.Word, youdaoResp)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "单词添加成功",
|
|
"data": word,
|
|
})
|
|
}
|
|
|
|
// 获取复习单词列表
|
|
func (h *WordHandler) GetReviewWords(c *gin.Context) {
|
|
mode := c.DefaultQuery("mode", "spelling")
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
|
|
words, err := h.wordService.GetReviewWords(userIDFromContext(c), mode, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": words})
|
|
}
|
|
|
|
// 提交复习答案
|
|
func (h *WordHandler) SubmitReview(c *gin.Context) {
|
|
var req request.ReviewAnswerRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
result, err := h.wordService.SubmitReviewAnswer(userIDFromContext(c), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": result})
|
|
}
|
|
|
|
// 获取所有单词
|
|
func (h *WordHandler) GetWords(c *gin.Context) {
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
|
query := strings.TrimSpace(c.DefaultQuery("q", ""))
|
|
|
|
words, total, err := h.wordService.GetAllWords(limit, offset, query)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"data": words,
|
|
"total": total,
|
|
})
|
|
}
|
|
|
|
func (h *WordHandler) GetWordByID(c *gin.Context) {
|
|
id := strings.TrimSpace(c.Param("id"))
|
|
if id == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "非法单词ID"})
|
|
return
|
|
}
|
|
|
|
word, err := h.wordService.GetWordByID(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "单词不存在"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": word})
|
|
}
|
|
|
|
// 获取统计信息
|
|
func (h *WordHandler) GetStatistics(c *gin.Context) {
|
|
stats, err := h.wordService.GetStatistics(userIDFromContext(c))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": stats})
|
|
}
|
|
|
|
// 获取音频文件
|
|
func (h *WordHandler) GetAudio(c *gin.Context) {
|
|
word := c.Query("word")
|
|
audioType := c.DefaultQuery("type", "uk")
|
|
|
|
if word == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少word参数"})
|
|
return
|
|
}
|
|
|
|
word = strings.TrimSpace(word)
|
|
if word == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "word为空"})
|
|
return
|
|
}
|
|
for _, r := range word {
|
|
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z') {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "word包含非法字符"})
|
|
return
|
|
}
|
|
}
|
|
filename := word + "_" + audioType + ".mp3"
|
|
path := "./audio/" + filename
|
|
c.File(path)
|
|
}
|
|
|
|
func (h *WordHandler) CreateStudySession(c *gin.Context) {
|
|
var req request.CreateStudySessionRequest
|
|
_ = c.ShouldBindJSON(&req)
|
|
|
|
words, err := h.wordService.CreateStudySession(userIDFromContext(c), req.Limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": words})
|
|
}
|
|
|
|
func (h *WordHandler) SubmitStudyAnswer(c *gin.Context) {
|
|
var req request.SubmitStudyAnswerRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
result, err := h.wordService.SubmitStudyAnswer(userIDFromContext(c), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": result})
|
|
}
|