feat(layout): 更新应用布局和UI组件样式
- 重构App.vue中的侧边栏布局,更新Logo设计为带有标识和副标题的新样式 - 调整顶部导航栏,增加标题区域显示当前路由标题和日期 - 修改菜单项配置,更新导航标签为更直观的中文描述 - 在Home.vue中替换原有的仪表板为新的Hero卡片和项目进展展示 - 更新Memory.vue中的学习界面,添加学习计划设置和多阶段学习模式 - 集成新的API端点路径,将baseURL从/api调整为/api/v1 - 调整整体视觉风格,包括颜色主题、字体家族和响应式布局 - 更新数据库模型以支持词库功能,添加相关的数据迁移和种子数据 - 调整认证系统的用户ID类型从整型到字符串的变更 - 更改前端构建工具从npm到pnpm,并更新相应的Dockerfile配置
This commit is contained in:
@@ -10,6 +10,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/snowflake v0.3.0 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
@@ -19,6 +20,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
@@ -29,6 +31,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
|
||||
@@ -13,5 +13,37 @@ func InitDB(cfg *config.Config) (*gorm.DB, error) {
|
||||
}
|
||||
|
||||
func AutoMigrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(&model.User{}, &model.Word{}, &model.MemoryRecord{})
|
||||
if err := db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.WordBook{},
|
||||
&model.Word{},
|
||||
&model.WordSentence{},
|
||||
&model.WordTranslation{},
|
||||
&model.WordSynonym{},
|
||||
&model.WordSynonymItem{},
|
||||
&model.WordPhrase{},
|
||||
&model.WordRel{},
|
||||
&model.WordRelItem{},
|
||||
&model.MemoryRecord{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return seedWordBooks(db)
|
||||
}
|
||||
|
||||
func seedWordBooks(db *gorm.DB) error {
|
||||
books := []model.WordBook{
|
||||
{ID: model.DefaultWordBookID, Code: "default", Name: "默认词库", SourceBookID: "DEFAULT"},
|
||||
{ID: "00000000000000000000000000000002", Code: "cet4", Name: "四级词库", SourceBookID: "CET4"},
|
||||
{ID: "00000000000000000000000000000003", Code: "ielts", Name: "雅思词库", SourceBookID: "IELTS"},
|
||||
{ID: "00000000000000000000000000000004", Code: "toefl", Name: "托福词库", SourceBookID: "TOEFL"},
|
||||
}
|
||||
|
||||
for i := range books {
|
||||
book := books[i]
|
||||
if err := db.Where("code = ?", book.Code).FirstOrCreate(&book).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,14 +16,14 @@ type WordHandler struct {
|
||||
wordService *service.WordService
|
||||
}
|
||||
|
||||
func userIDFromContext(c *gin.Context) int64 {
|
||||
func userIDFromContext(c *gin.Context) string {
|
||||
v, ok := c.Get("user_id")
|
||||
if !ok {
|
||||
return 1
|
||||
return ""
|
||||
}
|
||||
uid, ok := v.(int64)
|
||||
if !ok || uid <= 0 {
|
||||
return 1
|
||||
uid, ok := v.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return uid
|
||||
}
|
||||
@@ -40,7 +40,6 @@ func (h *WordHandler) AddWord(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 调用有道API查询
|
||||
youdaoResp, err := h.wordService.QueryWord(req.Word)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败: " + err.Error()})
|
||||
@@ -52,7 +51,6 @@ func (h *WordHandler) AddWord(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
word, err := h.wordService.SaveWord(userIDFromContext(c), req.Word, youdaoResp)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败: " + err.Error()})
|
||||
@@ -114,6 +112,22 @@ func (h *WordHandler) GetWords(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -135,7 +149,6 @@ func (h *WordHandler) GetAudio(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// very small sanitization
|
||||
word = strings.TrimSpace(word)
|
||||
if word == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "word为空"})
|
||||
|
||||
36
memora-api/internal/model/id.go
Normal file
36
memora-api/internal/model/id.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/bwmarrin/snowflake"
|
||||
)
|
||||
|
||||
var (
|
||||
snowflakeNode *snowflake.Node
|
||||
snowflakeOnce sync.Once
|
||||
)
|
||||
|
||||
func getSnowflakeNode() *snowflake.Node {
|
||||
snowflakeOnce.Do(func() {
|
||||
nodeID := int64(1)
|
||||
if raw := os.Getenv("MEMORA_SNOWFLAKE_NODE"); raw != "" {
|
||||
if parsed, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||
nodeID = parsed
|
||||
}
|
||||
}
|
||||
node, err := snowflake.NewNode(nodeID)
|
||||
if err != nil {
|
||||
log.Panicf("init snowflake node failed: %v", err)
|
||||
}
|
||||
snowflakeNode = node
|
||||
})
|
||||
return snowflakeNode
|
||||
}
|
||||
|
||||
func NewID() string {
|
||||
return getSnowflakeNode().Generate().String()
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MemoryRecord struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey"`
|
||||
WordID int64 `json:"word_id" gorm:"index;not null"`
|
||||
UserID int64 `json:"user_id" gorm:"index;default:1"`
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
WordID string `json:"word_id" gorm:"size:32;index;not null"`
|
||||
UserID string `json:"user_id" gorm:"size:32;index;not null"`
|
||||
CorrectCount int `json:"correct_count" gorm:"default:0"`
|
||||
TotalCount int `json:"total_count" gorm:"default:0"`
|
||||
MasteryLevel int `json:"mastery_level" gorm:"default:0"`
|
||||
@@ -13,9 +17,16 @@ type MemoryRecord struct {
|
||||
NextReviewAt *time.Time `json:"next_review_at" gorm:"index"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Word *Word `json:"word,omitempty" gorm:"foreignKey:WordID"`
|
||||
Word *Word `json:"word,omitempty" gorm:"foreignKey:WordID;references:ID"`
|
||||
}
|
||||
|
||||
func (MemoryRecord) TableName() string {
|
||||
return "memory_records"
|
||||
}
|
||||
|
||||
func (m *MemoryRecord) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey"`
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
Email string `json:"email" gorm:"size:120;uniqueIndex;not null"`
|
||||
Name string `json:"name" gorm:"size:80;not null"`
|
||||
PasswordHash string `json:"-" gorm:"size:255;not null"`
|
||||
@@ -14,3 +18,10 @@ type User struct {
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.ID == "" {
|
||||
u.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,21 +1,195 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Word struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey"`
|
||||
Word string `json:"word" gorm:"size:100;uniqueIndex;not null"`
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
BookID string `json:"book_id" gorm:"size:32;index;not null"`
|
||||
WordRank int `json:"word_rank" gorm:"index;default:0"`
|
||||
HeadWord string `json:"head_word" gorm:"size:120;index"`
|
||||
Word string `json:"word" gorm:"size:120;uniqueIndex;not null"`
|
||||
SourceWordID string `json:"source_word_id" gorm:"size:64;index"`
|
||||
PhoneticUK string `json:"phonetic_uk" gorm:"size:255"`
|
||||
PhoneticUS string `json:"phonetic_us" gorm:"size:255"`
|
||||
AudioUK string `json:"audio_uk" gorm:"size:500"`
|
||||
AudioUS string `json:"audio_us" gorm:"size:500"`
|
||||
UKSpeech string `json:"uk_speech" gorm:"size:255"`
|
||||
USSpeech string `json:"us_speech" gorm:"size:255"`
|
||||
PartOfSpeech string `json:"part_of_speech" gorm:"size:50"`
|
||||
Definition string `json:"definition" gorm:"type:text"`
|
||||
ExampleSentence string `json:"example_sentence" gorm:"type:text"`
|
||||
RawPayload string `json:"raw_payload,omitempty" gorm:"type:longtext"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Book *WordBook `json:"book.sql,omitempty" gorm:"foreignKey:BookID;references:ID"`
|
||||
}
|
||||
|
||||
func (Word) TableName() string {
|
||||
return "words"
|
||||
}
|
||||
|
||||
func (w *Word) BeforeCreate(tx *gorm.DB) error {
|
||||
if w.ID == "" {
|
||||
w.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WordSentence 对应 content.word.content.sentence.sentences
|
||||
// sContent/sCn 以原字段命名映射。
|
||||
type WordSentence struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
WordID string `json:"word_id" gorm:"size:32;index;not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"index;default:0"`
|
||||
SContent string `json:"s_content" gorm:"type:text"`
|
||||
SCn string `json:"s_cn" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordSentence) TableName() string {
|
||||
return "word_sentences"
|
||||
}
|
||||
|
||||
func (m *WordSentence) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WordTranslation 对应 content.word.content.trans
|
||||
// 保存中英释义及词性信息。
|
||||
type WordTranslation struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
WordID string `json:"word_id" gorm:"size:32;index;not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"index;default:0"`
|
||||
Pos string `json:"pos" gorm:"size:32"`
|
||||
TranCn string `json:"tran_cn" gorm:"type:text"`
|
||||
TranOther string `json:"tran_other" gorm:"type:text"`
|
||||
DescCn string `json:"desc_cn" gorm:"size:64"`
|
||||
DescOther string `json:"desc_other" gorm:"size:64"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordTranslation) TableName() string {
|
||||
return "word_translations"
|
||||
}
|
||||
|
||||
func (m *WordTranslation) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WordSynonym + WordSynonymItem 对应 content.word.content.syno.synos[*].hwds[*]
|
||||
type WordSynonym struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
WordID string `json:"word_id" gorm:"size:32;index;not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"index;default:0"`
|
||||
Pos string `json:"pos" gorm:"size:32"`
|
||||
Tran string `json:"tran" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordSynonym) TableName() string {
|
||||
return "word_synonyms"
|
||||
}
|
||||
|
||||
func (m *WordSynonym) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WordSynonymItem struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
SynonymID string `json:"synonym_id" gorm:"size:32;index;not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"index;default:0"`
|
||||
Word string `json:"word" gorm:"size:120;not null"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordSynonymItem) TableName() string {
|
||||
return "word_synonym_items"
|
||||
}
|
||||
|
||||
func (m *WordSynonymItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WordPhrase 对应 content.word.content.phrase.phrases
|
||||
type WordPhrase struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
WordID string `json:"word_id" gorm:"size:32;index;not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"index;default:0"`
|
||||
PContent string `json:"p_content" gorm:"type:text"`
|
||||
PCn string `json:"p_cn" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordPhrase) TableName() string {
|
||||
return "word_phrases"
|
||||
}
|
||||
|
||||
func (m *WordPhrase) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WordRel + WordRelItem 对应 content.word.content.relWord.rels[*].words[*]
|
||||
type WordRel struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
WordID string `json:"word_id" gorm:"size:32;index;not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"index;default:0"`
|
||||
Pos string `json:"pos" gorm:"size:32"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordRel) TableName() string {
|
||||
return "word_rels"
|
||||
}
|
||||
|
||||
func (m *WordRel) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WordRelItem struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
RelID string `json:"rel_id" gorm:"size:32;index;not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"index;default:0"`
|
||||
Hwd string `json:"hwd" gorm:"size:120"`
|
||||
Tran string `json:"tran" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordRelItem) TableName() string {
|
||||
return "word_rel_items"
|
||||
}
|
||||
|
||||
func (m *WordRelItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
m.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
32
memora-api/internal/model/word_book.go
Normal file
32
memora-api/internal/model/word_book.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultWordBookID = "00000000000000000000000000000001"
|
||||
DefaultWordBookCode = "default"
|
||||
)
|
||||
|
||||
type WordBook struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:32"`
|
||||
Code string `json:"code" gorm:"size:64;uniqueIndex;not null"`
|
||||
Name string `json:"name" gorm:"size:120;not null"`
|
||||
SourceBookID string `json:"source_book_id" gorm:"size:64;uniqueIndex"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WordBook) TableName() string {
|
||||
return "word_books"
|
||||
}
|
||||
|
||||
func (b *WordBook) BeforeCreate(tx *gorm.DB) error {
|
||||
if b.ID == "" {
|
||||
b.ID = NewID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func (r *MemoryRepository) Create(record *model.MemoryRecord) error {
|
||||
return r.db.Create(record).Error
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) FindByWord(userID, wordID int64) (*model.MemoryRecord, error) {
|
||||
func (r *MemoryRepository) FindByWord(userID, wordID string) (*model.MemoryRecord, error) {
|
||||
var record model.MemoryRecord
|
||||
if err := r.db.Where("word_id = ? AND user_id = ?", wordID, userID).First(&record).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -28,7 +28,7 @@ func (r *MemoryRepository) FindByWord(userID, wordID int64) (*model.MemoryRecord
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) FindByID(userID, recordID int64) (*model.MemoryRecord, error) {
|
||||
func (r *MemoryRepository) FindByID(userID, recordID string) (*model.MemoryRecord, error) {
|
||||
var record model.MemoryRecord
|
||||
if err := r.db.Preload("Word").Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -40,7 +40,7 @@ func (r *MemoryRepository) Save(record *model.MemoryRecord) error {
|
||||
return r.db.Save(record).Error
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) Due(userID int64, limit int) ([]model.MemoryRecord, error) {
|
||||
func (r *MemoryRepository) Due(userID string, limit int) ([]model.MemoryRecord, error) {
|
||||
var records []model.MemoryRecord
|
||||
if err := r.db.Preload("Word").Where("user_id = ? AND (next_review_at <= ? OR next_review_at IS NULL)", userID, time.Now()).Limit(limit).Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -48,7 +48,7 @@ func (r *MemoryRepository) Due(userID int64, limit int) ([]model.MemoryRecord, e
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) ListByUser(userID int64, limit int) ([]model.MemoryRecord, error) {
|
||||
func (r *MemoryRepository) ListByUser(userID string, limit int) ([]model.MemoryRecord, error) {
|
||||
var records []model.MemoryRecord
|
||||
if err := r.db.Preload("Word").Where("user_id = ?", userID).Limit(limit).Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -56,7 +56,7 @@ func (r *MemoryRepository) ListByUser(userID int64, limit int) ([]model.MemoryRe
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) CountOverview(userID int64) (total, mastered, needReview, todayReviewed int64) {
|
||||
func (r *MemoryRepository) CountOverview(userID string) (total, mastered, needReview, todayReviewed int64) {
|
||||
r.db.Model(&model.MemoryRecord{}).Where("user_id = ?", userID).Count(&total)
|
||||
r.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND mastery_level >= 4", userID).Count(&mastered)
|
||||
r.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND next_review_at <= ?", userID, time.Now()).Count(&needReview)
|
||||
|
||||
@@ -22,6 +22,14 @@ func (r *WordRepository) FindByWord(word string) (*model.Word, error) {
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
func (r *WordRepository) FindByID(id string) (*model.Word, error) {
|
||||
var w model.Word
|
||||
if err := r.db.Where("id = ?", id).First(&w).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
func (r *WordRepository) Create(word *model.Word) error {
|
||||
return r.db.Create(word).Error
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ type AddWordRequest struct {
|
||||
}
|
||||
|
||||
type ReviewAnswerRequest struct {
|
||||
RecordID int64 `json:"record_id" binding:"required"`
|
||||
RecordID string `json:"record_id" binding:"required"`
|
||||
Answer string `json:"answer" binding:"required"`
|
||||
Mode string `json:"mode" binding:"required"` // spelling, en2cn, cn2en
|
||||
}
|
||||
@@ -15,7 +15,7 @@ type CreateStudySessionRequest struct {
|
||||
}
|
||||
|
||||
type SubmitStudyAnswerRequest struct {
|
||||
WordID int64 `json:"word_id" binding:"required"`
|
||||
WordID string `json:"word_id" binding:"required"`
|
||||
Answer string `json:"answer" binding:"required"`
|
||||
Mode string `json:"mode" binding:"required"` // spelling, en2cn, cn2en
|
||||
}
|
||||
|
||||
@@ -12,24 +12,31 @@ func New(wordHandler *handler.WordHandler, authHandler *handler.AuthHandler) *gi
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
api.POST("/auth/register", authHandler.Register)
|
||||
api.POST("/auth/login", authHandler.Login)
|
||||
api.GET("/auth/me", authHandler.Me)
|
||||
|
||||
protected := api.Group("")
|
||||
protected.Use(middleware.AuthRequired())
|
||||
{
|
||||
protected.POST("/words", wordHandler.AddWord)
|
||||
protected.GET("/words", wordHandler.GetWords)
|
||||
protected.POST("/study/sessions", wordHandler.CreateStudySession)
|
||||
protected.POST("/study/answers", wordHandler.SubmitStudyAnswer)
|
||||
protected.GET("/review", wordHandler.GetReviewWords)
|
||||
protected.POST("/review", wordHandler.SubmitReview)
|
||||
protected.GET("/stats", wordHandler.GetStatistics)
|
||||
protected.GET("/audio", wordHandler.GetAudio)
|
||||
}
|
||||
}
|
||||
registerRoutes(api, wordHandler, authHandler)
|
||||
registerRoutes(api.Group("/v1"), wordHandler, authHandler)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func registerRoutes(group *gin.RouterGroup, wordHandler *handler.WordHandler, authHandler *handler.AuthHandler) {
|
||||
group.POST("/auth/register", authHandler.Register)
|
||||
group.POST("/auth/login", authHandler.Login)
|
||||
group.GET("/auth/me", authHandler.Me)
|
||||
|
||||
protected := group.Group("")
|
||||
protected.Use(middleware.AuthRequired())
|
||||
{
|
||||
protected.POST("/words", wordHandler.AddWord)
|
||||
protected.GET("/words", wordHandler.GetWords)
|
||||
protected.GET("/words/:id", wordHandler.GetWordByID)
|
||||
protected.POST("/study/sessions", wordHandler.CreateStudySession)
|
||||
protected.POST("/study/answers", wordHandler.SubmitStudyAnswer)
|
||||
protected.GET("/review", wordHandler.GetReviewWords)
|
||||
protected.POST("/review", wordHandler.SubmitReview)
|
||||
protected.GET("/review/today", wordHandler.GetReviewWords)
|
||||
protected.POST("/review/submit", wordHandler.SubmitReview)
|
||||
protected.GET("/stats", wordHandler.GetStatistics)
|
||||
protected.GET("/stats/overview", wordHandler.GetStatistics)
|
||||
protected.GET("/audio", wordHandler.GetAudio)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,9 @@ func sign(data string) string {
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func makeToken(uid int64) string {
|
||||
func makeToken(uid string) string {
|
||||
exp := time.Now().Add(7 * 24 * time.Hour).Unix()
|
||||
payload := fmt.Sprintf("%d:%d", uid, exp)
|
||||
payload := fmt.Sprintf("%s:%d", uid, exp)
|
||||
encPayload := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||
sig := sign(encPayload)
|
||||
return encPayload + "." + sig
|
||||
@@ -84,35 +84,35 @@ func (s *AuthService) Login(req request.LoginRequest) (string, *model.User, erro
|
||||
return makeToken(user.ID), &user, nil
|
||||
}
|
||||
|
||||
func ParseToken(tokenString string) (int64, error) {
|
||||
func ParseToken(tokenString string) (string, error) {
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 2 {
|
||||
return 0, errors.New("token无效")
|
||||
return "", errors.New("token无效")
|
||||
}
|
||||
encPayload, gotSig := parts[0], parts[1]
|
||||
if sign(encPayload) != gotSig {
|
||||
return 0, errors.New("token无效")
|
||||
return "", errors.New("token无效")
|
||||
}
|
||||
|
||||
payloadBytes, err := base64.RawURLEncoding.DecodeString(encPayload)
|
||||
if err != nil {
|
||||
return 0, errors.New("token无效")
|
||||
return "", errors.New("token无效")
|
||||
}
|
||||
|
||||
payloadParts := strings.Split(string(payloadBytes), ":")
|
||||
if len(payloadParts) != 2 {
|
||||
return 0, errors.New("token无效")
|
||||
return "", errors.New("token无效")
|
||||
}
|
||||
uid, err := strconv.ParseInt(payloadParts[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, errors.New("token无效")
|
||||
uid := payloadParts[0]
|
||||
if uid == "" {
|
||||
return "", errors.New("token无效")
|
||||
}
|
||||
exp, err := strconv.ParseInt(payloadParts[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, errors.New("token无效")
|
||||
return "", errors.New("token无效")
|
||||
}
|
||||
if time.Now().Unix() > exp {
|
||||
return 0, errors.New("token已过期")
|
||||
return "", errors.New("token已过期")
|
||||
}
|
||||
|
||||
return uid, nil
|
||||
|
||||
@@ -176,7 +176,7 @@ func (s *WordService) DownloadAudio(url, filePath string) error {
|
||||
}
|
||||
|
||||
// 保存单词到数据库
|
||||
func (s *WordService) SaveWord(userID int64, word string, youdaoResp *model.YoudaoResponse) (*model.Word, error) {
|
||||
func (s *WordService) SaveWord(userID string, word string, youdaoResp *model.YoudaoResponse) (*model.Word, error) {
|
||||
// 检查单词是否已存在
|
||||
if w, err := s.wordRepo.FindByWord(word); err == nil {
|
||||
// 单词已存在,更新记忆记录
|
||||
@@ -262,6 +262,8 @@ func (s *WordService) SaveWord(userID int64, word string, youdaoResp *model.Youd
|
||||
|
||||
// 创建新单词
|
||||
newWord := model.Word{
|
||||
BookID: model.DefaultWordBookID,
|
||||
HeadWord: word,
|
||||
Word: word,
|
||||
PhoneticUK: phoneticUK,
|
||||
PhoneticUS: phoneticUS,
|
||||
@@ -284,7 +286,7 @@ func (s *WordService) SaveWord(userID int64, word string, youdaoResp *model.Youd
|
||||
}
|
||||
|
||||
// 创建记忆记录
|
||||
func (s *WordService) createMemoryRecord(userID, wordID int64) error {
|
||||
func (s *WordService) createMemoryRecord(userID, wordID string) error {
|
||||
record := model.MemoryRecord{
|
||||
WordID: wordID,
|
||||
UserID: userID,
|
||||
@@ -296,7 +298,7 @@ func (s *WordService) createMemoryRecord(userID, wordID int64) error {
|
||||
}
|
||||
|
||||
// 更新记忆记录
|
||||
func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) error {
|
||||
func (s *WordService) updateMemoryRecord(userID, wordID string, correct bool) error {
|
||||
record, err := s.memoryRepo.FindByWord(userID, wordID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -333,7 +335,7 @@ func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) err
|
||||
}
|
||||
|
||||
// 获取待复习单词
|
||||
func (s *WordService) GetReviewWords(userID int64, mode string, limit int) ([]model.MemoryRecord, error) {
|
||||
func (s *WordService) GetReviewWords(userID string, mode string, limit int) ([]model.MemoryRecord, error) {
|
||||
records, err := s.memoryRepo.Due(userID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -375,7 +377,7 @@ func containsAny(def string, ans string) bool {
|
||||
}
|
||||
|
||||
// 提交复习答案
|
||||
func (s *WordService) SubmitReviewAnswer(userID int64, req request.ReviewAnswerRequest) (*response.ReviewResult, error) {
|
||||
func (s *WordService) SubmitReviewAnswer(userID string, req request.ReviewAnswerRequest) (*response.ReviewResult, error) {
|
||||
record, err := s.memoryRepo.FindByID(userID, req.RecordID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -416,8 +418,12 @@ func (s *WordService) GetAllWords(limit, offset int, query string) ([]model.Word
|
||||
return s.wordRepo.List(limit, offset, strings.TrimSpace(query))
|
||||
}
|
||||
|
||||
func (s *WordService) GetWordByID(id string) (*model.Word, error) {
|
||||
return s.wordRepo.FindByID(id)
|
||||
}
|
||||
|
||||
// 获取记忆统计
|
||||
func (s *WordService) GetStatistics(userID int64) (map[string]interface{}, error) {
|
||||
func (s *WordService) GetStatistics(userID string) (map[string]interface{}, error) {
|
||||
totalWords, masteredWords, needReview, todayReviewed := s.memoryRepo.CountOverview(userID)
|
||||
|
||||
return map[string]interface{}{
|
||||
@@ -428,7 +434,7 @@ func (s *WordService) GetStatistics(userID int64) (map[string]interface{}, error
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WordService) CreateStudySession(userID int64, limit int) ([]model.Word, error) {
|
||||
func (s *WordService) CreateStudySession(userID string, limit int) ([]model.Word, error) {
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 10
|
||||
}
|
||||
@@ -442,9 +448,9 @@ func (s *WordService) CreateStudySession(userID int64, limit int) ([]model.Word,
|
||||
return words, nil
|
||||
}
|
||||
|
||||
func (s *WordService) SubmitStudyAnswer(userID int64, req request.SubmitStudyAnswerRequest) (*response.ReviewResult, error) {
|
||||
func (s *WordService) SubmitStudyAnswer(userID string, req request.SubmitStudyAnswerRequest) (*response.ReviewResult, error) {
|
||||
reviewReq := request.ReviewAnswerRequest{
|
||||
RecordID: 0,
|
||||
RecordID: "",
|
||||
Answer: req.Answer,
|
||||
Mode: req.Mode,
|
||||
}
|
||||
|
||||
8
memora-api/sql/en_book.sql
Normal file
8
memora-api/sql/en_book.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_book` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '词书主键ID',
|
||||
`book_name` VARCHAR(100) NOT NULL COMMENT '词书名称,如雅思核心词',
|
||||
`exam_type` VARCHAR(50) NOT NULL COMMENT '考试类型,如 IELTS/TOEFL/CET4',
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='英语词书表';
|
||||
11
memora-api/sql/en_book_word_rel.sql
Normal file
11
memora-api/sql/en_book_word_rel.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_book_word_rl`
|
||||
(
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '词书单词关联主键ID',
|
||||
`book_id` BIGINT NOT NULL COMMENT '词书ID,关联 en_book.id',
|
||||
`word_id` BIGINT NOT NULL COMMENT '单词ID,关联 en_word.id',
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='词书单词关联表';
|
||||
7
memora-api/sql/en_word.sql
Normal file
7
memora-api/sql/en_word.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_word` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '单词主键ID',
|
||||
`word` VARCHAR(100) NOT NULL COMMENT '单词词头,如 cancel',
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='英语单词主表';
|
||||
9
memora-api/sql/en_word_phrase.sql
Normal file
9
memora-api/sql/en_word_phrase.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_word_phrase` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '短语记录主键ID',
|
||||
`word_id` BIGINT NOT NULL COMMENT '单词ID,关联 en_word.id',
|
||||
`phrase_en` VARCHAR(200) NOT NULL COMMENT '英文短语',
|
||||
`phrase_cn` VARCHAR(200) COMMENT '中文释义'
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='英语单词短语表';
|
||||
11
memora-api/sql/en_word_pron.sql
Normal file
11
memora-api/sql/en_word_pron.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_word_pron` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '发音记录主键ID',
|
||||
`word_id` BIGINT NOT NULL COMMENT '单词ID,关联 en_word.id',
|
||||
`uk_phone` VARCHAR(100) COMMENT '英式音标',
|
||||
`us_phone` VARCHAR(100) COMMENT '美式音标',
|
||||
`uk_speech` VARCHAR(200) COMMENT '英式发音资源地址或参数',
|
||||
`us_speech` VARCHAR(200) COMMENT '美式发音资源地址或参数',
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='英语单词发音表';
|
||||
12
memora-api/sql/en_word_sentence.sql
Normal file
12
memora-api/sql/en_word_sentence.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_word_sentence`
|
||||
(
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '例句记录主键ID',
|
||||
`word_id` BIGINT NOT NULL COMMENT '单词ID,关联 en_word.id',
|
||||
`sentence_en` TEXT COMMENT '英文例句',
|
||||
`sentence_cn` TEXT COMMENT '中文例句',
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='英语单词例句表';
|
||||
13
memora-api/sql/en_word_synonym.sql
Normal file
13
memora-api/sql/en_word_synonym.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_word_synonym`
|
||||
(
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '同义词记录主键ID',
|
||||
`word_id` BIGINT NOT NULL COMMENT '单词ID,关联 en_word.id',
|
||||
`pos` VARCHAR(20) COMMENT '词性,如 vt/vi/n',
|
||||
`tran` TEXT COMMENT '同义词组释义',
|
||||
`synonym_word` VARCHAR(100) NOT NULL COMMENT '同义词词头',
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='英语单词同义词表';
|
||||
13
memora-api/sql/en_word_trans.sql
Normal file
13
memora-api/sql/en_word_trans.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS `en_word_trans`
|
||||
(
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '释义记录主键ID',
|
||||
`word_id` BIGINT NOT NULL COMMENT '单词ID,关联 en_word.id',
|
||||
`pos` VARCHAR(20) COMMENT '词性,如 v/n/adj',
|
||||
`tran_cn` TEXT COMMENT '中文释义',
|
||||
`tran_en` TEXT COMMENT '英文释义',
|
||||
`delete_flag` TINYINT(1) DEFAULT 0 COMMENT '删除标志,0-正常,1-删除',
|
||||
`create_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='英语单词释义表';
|
||||
@@ -2,20 +2,150 @@
|
||||
CREATE DATABASE IF NOT EXISTS memora DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE memora;
|
||||
|
||||
-- 标准单词表
|
||||
-- 词库表(对应 JSON 顶层 bookId)
|
||||
CREATE TABLE IF NOT EXISTS word_books (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL UNIQUE COMMENT '系统词库编码',
|
||||
name VARCHAR(120) NOT NULL COMMENT '词库名称',
|
||||
source_json_filename VARCHAR(255) NULL UNIQUE COMMENT '来源词库json文件名,如 IELTS_3',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO word_books (id, code, name, source_book_id)
|
||||
VALUES
|
||||
(1, 'default', '默认词库', 'DEFAULT'),
|
||||
(2, 'cet4', '四级词库', 'CET4'),
|
||||
(3, 'ielts', '雅思词库', 'IELTS'),
|
||||
(4, 'toefl', '托福词库', 'TOEFL')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
source_book_id = VALUES(source_book_id);
|
||||
|
||||
-- 主单词表(覆盖 JSON 顶层与 content.word 核心字段)
|
||||
CREATE TABLE IF NOT EXISTS words (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
word VARCHAR(100) NOT NULL UNIQUE COMMENT '单词',
|
||||
phonetic_uk VARCHAR(255) COMMENT '英式音标',
|
||||
phonetic_us VARCHAR(255) COMMENT '美式音标',
|
||||
audio_uk VARCHAR(500) COMMENT '英式音频文件路径',
|
||||
audio_us VARCHAR(500) COMMENT '美式音频文件路径',
|
||||
part_of_speech VARCHAR(50) COMMENT '词性',
|
||||
definition TEXT COMMENT '标准释义',
|
||||
example_sentence TEXT COMMENT '例句',
|
||||
book_id BIGINT NOT NULL DEFAULT 1 COMMENT '关联词库ID',
|
||||
word_rank INT DEFAULT 0 COMMENT 'JSON: wordRank',
|
||||
head_word VARCHAR(120) COMMENT 'JSON: headWord',
|
||||
word VARCHAR(120) NOT NULL UNIQUE COMMENT 'JSON: content.word.wordHead',
|
||||
source_word_id VARCHAR(64) COMMENT 'JSON: content.word.wordId',
|
||||
phonetic_uk VARCHAR(255) COMMENT 'JSON: ukphone',
|
||||
phonetic_us VARCHAR(255) COMMENT 'JSON: usphone',
|
||||
audio_uk VARCHAR(500) COMMENT '英式音频文件路径(本地缓存)',
|
||||
audio_us VARCHAR(500) COMMENT '美式音频文件路径(本地缓存)',
|
||||
uk_speech VARCHAR(255) COMMENT 'JSON: ukspeech',
|
||||
us_speech VARCHAR(255) COMMENT 'JSON: usspeech',
|
||||
part_of_speech VARCHAR(50) COMMENT '兼容旧结构',
|
||||
definition TEXT COMMENT '兼容旧结构(可由 trans 聚合)',
|
||||
example_sentence TEXT COMMENT '兼容旧结构(可由 sentence 聚合)',
|
||||
raw_payload LONGTEXT COMMENT '完整原始JSON',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word (word)
|
||||
INDEX idx_word (word),
|
||||
INDEX idx_book_id (book_id),
|
||||
INDEX idx_word_rank (word_rank),
|
||||
INDEX idx_source_word_id (source_word_id),
|
||||
FOREIGN KEY (book_id) REFERENCES word_books(id) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 例句表:content.word.content.sentence.sentences
|
||||
CREATE TABLE IF NOT EXISTS word_sentences (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
word_id BIGINT NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
s_content TEXT COMMENT 'JSON: sContent',
|
||||
s_cn TEXT COMMENT 'JSON: sCn',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word_sentence_word_id (word_id),
|
||||
INDEX idx_word_sentence_sort (sort_order),
|
||||
FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 释义表:content.word.content.trans
|
||||
CREATE TABLE IF NOT EXISTS word_translations (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
word_id BIGINT NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
pos VARCHAR(32),
|
||||
tran_cn TEXT,
|
||||
tran_other TEXT,
|
||||
desc_cn VARCHAR(64),
|
||||
desc_other VARCHAR(64),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word_trans_word_id (word_id),
|
||||
INDEX idx_word_trans_sort (sort_order),
|
||||
FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 同近组表:content.word.content.syno.synos
|
||||
CREATE TABLE IF NOT EXISTS word_synonyms (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
word_id BIGINT NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
pos VARCHAR(32),
|
||||
tran TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word_syn_word_id (word_id),
|
||||
INDEX idx_word_syn_sort (sort_order),
|
||||
FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 同近词项表:content.word.content.syno.synos[*].hwds[*]
|
||||
CREATE TABLE IF NOT EXISTS word_synonym_items (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
synonym_id BIGINT NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
word VARCHAR(120) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word_syn_item_syn_id (synonym_id),
|
||||
INDEX idx_word_syn_item_sort (sort_order),
|
||||
FOREIGN KEY (synonym_id) REFERENCES word_synonyms(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 短语表:content.word.content.phrase.phrases
|
||||
CREATE TABLE IF NOT EXISTS word_phrases (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
word_id BIGINT NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
p_content TEXT,
|
||||
p_cn TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word_phrase_word_id (word_id),
|
||||
INDEX idx_word_phrase_sort (sort_order),
|
||||
FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 同根组表:content.word.content.relWord.rels
|
||||
CREATE TABLE IF NOT EXISTS word_rels (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
word_id BIGINT NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
pos VARCHAR(32),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word_rel_word_id (word_id),
|
||||
INDEX idx_word_rel_sort (sort_order),
|
||||
FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 同根词项表:content.word.content.relWord.rels[*].words[*]
|
||||
CREATE TABLE IF NOT EXISTS word_rel_items (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
rel_id BIGINT NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
hwd VARCHAR(120),
|
||||
tran TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_word_rel_item_rel_id (rel_id),
|
||||
INDEX idx_word_rel_item_sort (sort_order),
|
||||
FOREIGN KEY (rel_id) REFERENCES word_rels(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 记忆记录表
|
||||
|
||||
Reference in New Issue
Block a user