# 知识图谱系统 - 后端项目设计概要
## 项目概述

基于三层架构（Knowledge-Method-Problem）的高中数学知识图谱系统，结合学生个性化学习评分功能，为开发团队提供完整的后端技术方案。

---

## 一、系统架构设计

### 1.1 整体架构
```
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   前端应用层     │    │   后端API层     │    │   数据存储层    │
│   (Web/Mobile)  │◄──►│  (RESTful API)  │◄──►│   (MySQL/Redis) │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │
                      ┌─────────────────┐
                      │   业务逻辑层    │
                      │  (Service层)   │
                      └─────────────────┘
                              │
                      ┌─────────────────┐
                      │   数据访问层    │
                      │   (DAO/ORM)    │
                      └─────────────────┘
```

### 1.2 技术栈推荐
- **后端框架**: Go + Gin
- **数据库**: PostgreSQL 14+ + Redis
- **数据库驱动**: pgx/v5 (官方推荐)
- **ORM**: sqlx (轻量级 SQL 工具包)
- **API文档**: swaggo (自动生成 Swagger)
- **任务调度**: cron (Go 标准库) 或 robfig/cron
- **缓存**: Redis
- **部署**: Docker + Kubernetes

---

## 二、数据库设计（7张核心表）

### 2.1 知识图谱相关表

#### **knowledge（知识点表）**
```sql
CREATE TABLE knowledge (
    id VARCHAR(20) PRIMARY KEY,        -- K3-1-1-01
    name VARCHAR(100) NOT NULL,        -- 函数的三要素
    type VARCHAR(20),                  -- 概念/定理/公式
    definition TEXT,                   -- 定义内容
    core_features TEXT[],              -- PostgreSQL 数组类型 ["任意性", "唯一性"]
    prerequisites TEXT[],              -- PostgreSQL 数组类型 ["K1-1-01", "K1-2-01"]
    importance VARCHAR(10),            -- 核心/重要/基础
    textbook_location VARCHAR(100),    -- 必修1 P62
    created_at TIMESTAMP DEFAULT NOW()
);

-- 数组字段的 GIN 索引，支持高效的包含查询
CREATE INDEX idx_knowledge_core_features ON knowledge USING GIN(core_features);
CREATE INDEX idx_knowledge_prerequisites ON knowledge USING GIN(prerequisites);
```

#### **method（方法表）**
```sql
CREATE TABLE method (
    id VARCHAR(20) PRIMARY KEY,        -- M3-1-1-01
    name VARCHAR(100) NOT NULL,        -- 分式型定义域求解法
    type VARCHAR(20),                  -- 解题方法/计算技巧
    scenario VARCHAR(200),             -- 适用场景
    steps TEXT,                        -- 方法步骤描述，纯文本
    supported_knowledge TEXT[],        -- PostgreSQL 数组类型 ["K3-1-1-02"]
    common_errors TEXT[],              -- PostgreSQL 数组类型 ["遗漏分母"]
    difficulty_level INT,              -- 1-5
    created_at TIMESTAMP DEFAULT NOW()
);

-- 数组字段的 GIN 索引
CREATE INDEX idx_method_supported_knowledge ON method USING GIN(supported_knowledge);
CREATE INDEX idx_method_common_errors ON method USING GIN(common_errors);
```

#### **problem（题目表）**
```sql
CREATE TABLE problem (
    id VARCHAR(20) PRIMARY KEY,        -- T3-1-1-E02
    problem_type VARCHAR(20),          -- 例题/练习题/习题
    content TEXT NOT NULL,             -- 完整题目内容
    problem_category VARCHAR(50),      -- 函数基础
    difficulty INT,                    -- 1-5
    source VARCHAR(100),               -- 必修1 P65 例2
    created_at TIMESTAMP DEFAULT NOW()
);

-- 题目-知识点映射表
CREATE TABLE problem_knowledge_mapping (
    problem_id VARCHAR(20),
    question_part VARCHAR(10),        -- 小题1/小题2/整体
    knowledge_id VARCHAR(20),
    is_primary BOOLEAN DEFAULT TRUE,   -- 是否为主要考查点
    weight DECIMAL(3,2) DEFAULT 1.0, -- 权重(0.1-1.0)
    PRIMARY KEY (problem_id, question_part, knowledge_id),
    FOREIGN KEY (problem_id) REFERENCES problem(id),
    FOREIGN KEY (knowledge_id) REFERENCES knowledge(id)
);

-- 题目-方法映射表
CREATE TABLE problem_method_mapping (
    problem_id VARCHAR(20),
    question_part VARCHAR(10),
    method_id VARCHAR(20),
    is_primary BOOLEAN DEFAULT TRUE,
    weight DECIMAL(3,2) DEFAULT 1.0,
    PRIMARY KEY (problem_id, question_part, method_id),
    FOREIGN KEY (problem_id) REFERENCES problem(id),
    FOREIGN KEY (method_id) REFERENCES method(id)
);

-- 性能索引
CREATE INDEX idx_problem_knowledge_problem ON problem_knowledge_mapping(problem_id);
CREATE INDEX idx_problem_knowledge_knowledge ON problem_knowledge_mapping(knowledge_id);
CREATE INDEX idx_problem_method_problem ON problem_method_mapping(problem_id);
CREATE INDEX idx_problem_method_method ON problem_method_mapping(method_id);
```

### 2.2 学生评分相关表

#### **student（学生表）**
```sql
CREATE TABLE student (
    id VARCHAR(20) PRIMARY KEY,        -- S001
    name VARCHAR(50) NOT NULL,
    grade VARCHAR(10),                 -- 高一/高二/高三
    class_name VARCHAR(20),            -- 班级
    created_at TIMESTAMP DEFAULT NOW()
);
```

#### **learning_record（学习记录表 - 核心流水表）**
```sql
CREATE TABLE learning_record (
    id BIGSERIAL PRIMARY KEY,          -- PostgreSQL 自增类型
    student_id VARCHAR(20) NOT NULL,
    problem_id VARCHAR(20) NOT NULL,   -- T3-1-1-E02
    knowledge_id VARCHAR(20) NOT NULL, -- K3-1-1-02
    method_id VARCHAR(20),             -- M3-1-1-01 (可选)
    question_part VARCHAR(10),         -- 小题1/小题2/整体
    is_correct BOOLEAN NOT NULL,       -- 正确/错误
    score DECIMAL(5,2),                -- 得分(0-100)
    max_score DECIMAL(5,2) NOT NULL,   -- 该题/该小题的满分
    exam_id VARCHAR(20) NOT NULL,      -- 该场考试的id或名称
    exam_date DATE,                    -- 考试/练习日期
    response_time_ms INT,              -- 响应时间（毫秒）
    created_at TIMESTAMP DEFAULT NOW(),

    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (problem_id) REFERENCES problem(id),
    FOREIGN KEY (knowledge_id) REFERENCES knowledge(id),
    FOREIGN KEY (method_id) REFERENCES method(id)
);

-- 分区表（按月分区，提高查询性能）
CREATE TABLE learning_record_y2024m01 PARTITION OF learning_record
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- 性能索引
CREATE INDEX idx_learning_record_student_knowledge ON learning_record(student_id, knowledge_id);
CREATE INDEX idx_learning_record_exam_date ON learning_record(exam_date);
CREATE INDEX idx_learning_record_student_exam ON learning_record(student_id, exam_date DESC);
```

#### **knowledge_mastery（知识点掌握度表）**
```sql
CREATE TABLE knowledge_mastery (
    student_id VARCHAR(20),
    knowledge_id VARCHAR(20),
    mastery_score DECIMAL(5,2) DEFAULT 100.0, -- 掌握度分数(0-100)
    total_attempts INT DEFAULT 0,      -- 总尝试次数
    correct_attempts INT DEFAULT 0,    -- 正确次数
    avg_response_time DECIMAL(8,2),    -- 平均答题时间
    last_practiced DATE,               -- 最后练习日期
    confidence_level DECIMAL(3,2),     -- 置信度(0-1)
    updated_at TIMESTAMP DEFAULT NOW(),

    PRIMARY KEY (student_id, knowledge_id),
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (knowledge_id) REFERENCES knowledge(id)
);

-- 性能索引
CREATE INDEX idx_knowledge_mastery_score ON knowledge_mastery(mastery_score);
CREATE INDEX idx_knowledge_mastery_updated ON knowledge_mastery(updated_at);
```

#### **method_mastery（方法掌握度表）**
```sql
CREATE TABLE method_mastery (
    student_id VARCHAR(20),
    method_id VARCHAR(20),
    mastery_score DECIMAL(5,2) DEFAULT 100.0,
    total_usage INT DEFAULT 0,         -- 总使用次数
    successful_usage INT DEFAULT 0,    -- 成功使用次数
    avg_success_rate DECIMAL(5,2),     -- 平均成功率
    updated_at TIMESTAMP DEFAULT NOW(),

    PRIMARY KEY (student_id, method_id),
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (method_id) REFERENCES method(id)
);
```

---

## 三、API接口设计

### 3.1 知识图谱管理API

#### **知识点管理**
```http
GET    /api/knowledge                  # 获取知识点列表
POST   /api/knowledge                  # 创建知识点
GET    /api/knowledge/{id}             # 获取知识点详情
PUT    /api/knowledge/{id}             # 更新知识点
DELETE /api/knowledge/{id}             # 删除知识点
GET    /api/knowledge/{id}/prerequisites # 获取前置知识点
GET    /api/knowledge/{id}/related-problems # 获取相关题目
```

#### **方法管理**
```http
GET    /api/methods                    # 获取方法列表
POST   /api/methods                    # 创建方法
GET    /api/methods/{id}               # 获取方法详情
PUT    /api/methods/{id}               # 更新方法
GET    /api/methods/{id}/applicable-problems # 获取适用题目
```

#### **题目管理**
```http
GET    /api/problems                   # 获取题目列表（支持筛选）
POST   /api/problems                   # 创建题目
GET    /api/problems/{id}              # 获取题目详情
PUT    /api/problems/{id}              # 更新题目
GET    /api/problems/recommendations   # 个性化题目推荐
```

### 3.2 学习记录API

#### **答题记录**
```http
POST   /api/learning-records           # 提交答题记录
GET    /api/students/{id}/learning-records # 获取学生学习记录
GET    /api/learning-records/statistics # 学习统计
```

#### **批量记录处理**
```http
POST   /api/learning-records/batch     # 批量提交答题记录
```

### 3.3 掌握度分析API

#### **知识点掌握度**
```http
GET    /api/students/{id}/knowledge-mastery          # 获取知识点掌握情况
GET    /api/students/{id}/weak-knowledge             # 获取薄弱知识点
GET    /api/knowledge/{id}/student-mastery           # 获取知识点学生掌握情况
```

#### **方法掌握度**
```http
GET    /api/students/{id}/method-mastery             # 获取方法掌握情况
```

### 3.4 个性化推荐API

#### **题目推荐**
```http
GET    /api/students/{id}/recommendations/problems   # 推荐题目
GET    /api/students/{id}/recommendations/knowledge  # 推荐知识点
GET    /api/students/{id}/learning-path              # 学习路径规划
```

---

## 四、核心业务逻辑

### 4.1 掌握度计算服务

```go
package service

import (
    "context"
    "math"
    "time"
)

type MasteryCalculationService struct {
    db     *sqlx.DB
    redis  *redis.Client
}

// 计算知识点掌握度
func (s *MasteryCalculationService) CalculateKnowledgeMastery(ctx context.Context, studentID, knowledgeID string) (float64, error) {
    // 基于学习记录计算掌握度分数
    // 考虑因素：正确率、答题次数、最近表现等

    var stats struct {
        TotalAttempts   int     `db:"total_attempts"`
        CorrectAttempts int     `db:"correct_attempts"`
        AvgScore        float64 `db:"avg_score"`
        LastScore       float64 `db:"last_score"`
        RecentTrend     float64 `db:"recent_trend"`
    }

    query := `
        SELECT
            COUNT(*) as total_attempts,
            SUM(CASE WHEN is_correct THEN 1 ELSE 0 END) as correct_attempts,
            AVG(score / max_score * 100) as avg_score,
            (score / max_score * 100) as last_score,
            -- 计算最近趋势（最近5次 vs 之前5次的对比）
            COALESCE(
                (SELECT AVG(score / max_score * 100)
                 FROM learning_record
                 WHERE student_id = $1 AND knowledge_id = $2
                 ORDER BY created_at DESC LIMIT 5) -
                (SELECT AVG(score / max_score * 100)
                 FROM learning_record
                 WHERE student_id = $1 AND knowledge_id = $2
                 ORDER BY created_at DESC LIMIT 10 OFFSET 5), 0
            ) as recent_trend
        FROM learning_record
        WHERE student_id = $1 AND knowledge_id = $2
        ORDER BY created_at DESC
        LIMIT 1
    `

    err := s.db.GetContext(ctx, &stats, query, studentID, knowledgeID)
    if err != nil {
        return 0, err
    }

    if stats.TotalAttempts == 0 {
        return 100.0, nil // 默认满分
    }

    // 掌握度计算公式
    accuracy := float64(stats.CorrectAttempts) / float64(stats.TotalAttempts) * 100
    weightAccuracy := 0.4
    weightAvgScore := 0.3
    weightLastScore := 0.2
    weightTrend := 0.1

    mastery := accuracy*weightAccuracy +
               stats.AvgScore*weightAvgScore +
               stats.LastScore*weightLastScore +
               math.Max(0, math.Min(100, 50+stats.RecentTrend))*weightTrend

    return math.Max(0, math.Min(100, mastery)), nil
}

// 批量更新掌握度
func (s *MasteryCalculationService) BatchUpdateMastery(ctx context.Context) error {
    // 每天凌晨2点执行
    // 批量更新所有学生的知识点和方法掌握度

    // 获取所有需要更新的学生-知识点对
    query := `
        SELECT DISTINCT lr.student_id, lr.knowledge_id
        FROM learning_record lr
        LEFT JOIN knowledge_mastery km ON lr.student_id = km.student_id AND lr.knowledge_id = km.knowledge_id
        WHERE km.updated_at < NOW() - INTERVAL '1 hour'
           OR km.updated_at IS NULL
    `

    var pairs []struct {
        StudentID    string `db:"student_id"`
        KnowledgeID  string `db:"knowledge_id"`
    }

    err := s.db.SelectContext(ctx, &pairs, query)
    if err != nil {
        return err
    }

    // 批量计算和更新
    for _, pair := range pairs {
        mastery, err := s.CalculateKnowledgeMastery(ctx, pair.StudentID, pair.KnowledgeID)
        if err != nil {
            continue // 记录日志，继续处理其他记录
        }

        upsertQuery := `
            INSERT INTO knowledge_mastery
            (student_id, knowledge_id, mastery_score, updated_at)
            VALUES ($1, $2, $3, NOW())
            ON CONFLICT (student_id, knowledge_id)
            DO UPDATE SET
                mastery_score = EXCLUDED.mastery_score,
                updated_at = NOW()
        `

        s.db.ExecContext(ctx, upsertQuery, pair.StudentID, pair.KnowledgeID, mastery)
    }

    return nil
}
```

### 4.2 个性化推荐引擎

```go
package service

import (
    "context"
    "fmt"
    "math/rand"
    "sort"
)

type RecommendationService struct {
    db    *sqlx.DB
    redis *redis.Client
}

// 基于掌握度推荐题目
func (s *RecommendationService) RecommendProblems(ctx context.Context, studentID string, count int) ([]Problem, error) {
    // 1. 获取学生薄弱知识点
    weakKnowledge, err := s.getWeakKnowledge(ctx, studentID)
    if err != nil {
        return nil, err
    }

    if len(weakKnowledge) == 0 {
        // 没有薄弱知识点，推荐综合练习
        return s.getComprehensiveProblems(ctx, studentID, count)
    }

    // 2. 基于知识点关联度推荐题目
    var problems []Problem
    for _, wk := range weakKnowledge[:3] { // 取前3个最薄弱的知识点
        probs, err := s.getProblemsByKnowledge(ctx, wk.KnowledgeID, count/3)
        if err != nil {
            continue
        }
        problems = append(problems, probs...)
    }

    // 3. 考虑题目难度梯度
    problems = s.balanceDifficulty(problems, studentID)

    // 4. 避免重复推荐
    problems = s.filterRecentProblems(ctx, studentID, problems)

    if len(problems) > count {
        problems = problems[:count]
    }

    return problems, nil
}

// 获取推荐题目（基于薄弱知识点）- 使用规范化表
func (s *RecommendationService) getProblemsByKnowledge(ctx context.Context, knowledgeID string, count int) ([]Problem, error) {
    query := `
        SELECT DISTINCT p.* FROM problem p
        JOIN problem_knowledge_mapping pkm ON p.id = pkm.problem_id
        WHERE pkm.knowledge_id = $1 AND pkm.is_primary = true
        ORDER BY p.difficulty ASC, pkm.weight DESC
        LIMIT $2
    `

    var problems []Problem
    err := s.db.SelectContext(ctx, &problems, query, knowledgeID, count)
    return problems, err
}

// 获取题目的完整知识点映射
func (s *RecommendationService) getProblemKnowledgeMapping(ctx context.Context, problemID string) ([]ProblemKnowledgeMapping, error) {
    query := `
        SELECT pkm.question_part, pkm.knowledge_id, k.name, pkm.is_primary, pkm.weight
        FROM problem_knowledge_mapping pkm
        JOIN knowledge k ON pkm.knowledge_id = k.id
        WHERE pkm.problem_id = $1
        ORDER BY pkm.question_part, pkm.weight DESC
    `

    var mappings []ProblemKnowledgeMapping
    err := s.db.SelectContext(ctx, &mappings, query, problemID)
    return mappings, err
}

// 获取题目使用的方法
func (s *RecommendationService) getProblemMethods(ctx context.Context, problemID string) ([]ProblemMethodMapping, error) {
    query := `
        SELECT pmm.question_part, pmm.method_id, m.name, pmm.is_primary, pmm.weight
        FROM problem_method_mapping pmm
        JOIN method m ON pmm.method_id = m.id
        WHERE pmm.problem_id = $1
        ORDER BY pmm.question_part, pmm.weight DESC
    `

    var methods []ProblemMethodMapping
    err := s.db.SelectContext(ctx, &methods, query, problemID)
    return methods, err
}

// 创建题目和知识点/方法映射
func (s *RecommendationService) CreateProblemWithMappings(ctx context.Context, problem *Problem, knowledgeMappings []ProblemKnowledgeMapping, methodMappings []ProblemMethodMapping) error {
    tx, err := s.db.BeginTxx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // 1. 插入题目基本信息
    insertProblemQuery := `
        INSERT INTO problem (id, problem_type, content, problem_category, difficulty, source)
        VALUES ($1, $2, $3, $4, $5, $6)
    `
    _, err = tx.ExecContext(ctx, insertProblemQuery,
        problem.ID, problem.ProblemType, problem.Content,
        problem.ProblemCategory, problem.Difficulty, problem.Source)
    if err != nil {
        return err
    }

    // 2. 插入知识点映射
    for _, km := range knowledgeMappings {
        insertKmQuery := `
            INSERT INTO problem_knowledge_mapping (problem_id, question_part, knowledge_id, is_primary, weight)
            VALUES ($1, $2, $3, $4, $5)
            ON CONFLICT (problem_id, question_part, knowledge_id) DO UPDATE SET
                is_primary = EXCLUDED.is_primary,
                weight = EXCLUDED.weight
        `
        _, err = tx.ExecContext(ctx, insertKmQuery,
            problem.ID, km.QuestionPart, km.KnowledgeID, km.IsPrimary, km.Weight)
        if err != nil {
            return err
        }
    }

    // 3. 插入方法映射
    for _, mm := range methodMappings {
        insertMmQuery := `
            INSERT INTO problem_method_mapping (problem_id, question_part, method_id, is_primary, weight)
            VALUES ($1, $2, $3, $4, $5)
            ON CONFLICT (problem_id, question_part, method_id) DO UPDATE SET
                is_primary = EXCLUDED.is_primary,
                weight = EXCLUDED.weight
        `
        _, err = tx.ExecContext(ctx, insertMmQuery,
            problem.ID, mm.QuestionPart, mm.MethodID, mm.IsPrimary, mm.Weight)
        if err != nil {
            return err
        }
    }

    return tx.Commit()
}

type ProblemKnowledgeMapping struct {
    QuestionPart string  `json:"question_part" db:"question_part"`
    KnowledgeID  string  `json:"knowledge_id" db:"knowledge_id"`
    KnowledgeName string `json:"knowledge_name" db:"name"`
    IsPrimary    bool    `json:"is_primary" db:"is_primary"`
    Weight       float64 `json:"weight" db:"weight"`
}

type ProblemMethodMapping struct {
    QuestionPart string  `json:"question_part" db:"question_part"`
    MethodID     string  `json:"method_id" db:"method_id"`
    MethodName   string  `json:"method_name" db:"name"`
    IsPrimary    bool    `json:"is_primary" db:"is_primary"`
    Weight       float64 `json:"weight" db:"weight"`
}

type Problem struct {
    ID               string                 `json:"id" db:"id"`
    ProblemType      string                 `json:"problem_type" db:"problem_type"`
    Content          string                 `json:"content" db:"content"`
    ProblemCategory  string                 `json:"problem_category" db:"problem_category"`
    Difficulty       int                    `json:"difficulty" db:"difficulty"`
    Source           string                 `json:"source" db:"source"`
    KnowledgeMappings []ProblemKnowledgeMapping `json:"knowledge_mappings,omitempty"`
    MethodMappings    []ProblemMethodMapping    `json:"method_mappings,omitempty"`
}

type Method struct {
    ID                string   `json:"id" db:"id"`
    Name              string   `json:"name" db:"name"`
    Type              string   `json:"type" db:"type"`
    Scenario          string   `json:"scenario" db:"scenario"`
    Steps             string   `json:"steps" db:"steps"`
    SupportedKnowledge []string `json:"supported_knowledge" db:"supported_knowledge"`
    CommonErrors      []string `json:"common_errors" db:"common_errors"`
    DifficultyLevel   int      `json:"difficulty_level" db:"difficulty_level"`
    CreatedAt         time.Time `json:"created_at" db:"created_at"`
}

// 方法步骤查询服务
type MethodService struct {
    db *sqlx.DB
}

// 获取支持特定知识点的方法
func (s *MethodService) GetMethodsByKnowledge(ctx context.Context, knowledgeID string) ([]Method, error) {
    query := `
        SELECT m.* FROM method m
        WHERE $1 = ANY(m.supported_knowledge)
        ORDER BY m.difficulty_level ASC
    `

    var methods []Method
    err := s.db.SelectContext(ctx, &methods, query, knowledgeID)
    return methods, err
}

// 全文搜索方法步骤
func (s *MethodService) SearchMethodSteps(ctx context.Context, keyword string) ([]Method, error) {
    query := `
        SELECT m.* FROM method m
        WHERE m.steps ILIKE '%' || $1 || '%'
        ORDER BY m.difficulty_level ASC
    `

    var methods []Method
    err := s.db.SelectContext(ctx, &methods, query, keyword)
    return methods, err
}

// 获取方法的详细步骤（用于展示）
func (s *MethodService) GetMethodWithFormattedSteps(ctx context.Context, methodID string) (*MethodWithSteps, error) {
    query := `
        SELECT m.*, array_to_string(m.common_errors, '; ') as common_errors_text
        FROM method m
        WHERE m.id = $1
    `

    var method MethodWithSteps
    err := s.db.GetContext(ctx, &method, query, methodID)
    if err != nil {
        return nil, err
    }

    // 格式化步骤文本（将长文本按行分割）
    method.FormattedSteps = strings.Split(method.Steps, "\n")

    return &method, nil
}

type MethodWithSteps struct {
    Method
    FormattedSteps    []string `json:"formatted_steps"`
    CommonErrorsText string   `json:"common_errors_text"`
}
```

type WeakKnowledge struct {
    KnowledgeID  string  `db:"knowledge_id"`
    Name         string  `db:"name"`
    MasteryScore float64 `db:"mastery_score"`
    Priority     int     `db:"priority"`
}

func (s *RecommendationService) getWeakKnowledge(ctx context.Context, studentID string) ([]WeakKnowledge, error) {
    query := `
        SELECT k.id as knowledge_id, k.name, km.mastery_score,
               CASE
                   WHEN km.mastery_score < 40 THEN 1
                   WHEN km.mastery_score < 60 THEN 2
                   WHEN km.mastery_score < 80 THEN 3
                   ELSE 4
               END as priority
        FROM knowledge k
        JOIN knowledge_mastery km ON k.id = km.knowledge_id
        WHERE km.student_id = $1 AND km.mastery_score < 80
        ORDER BY km.mastery_score ASC, km.total_attempts DESC
        LIMIT 10
    `

    var weak []WeakKnowledge
    err := s.db.SelectContext(ctx, &weak, query, studentID)
    return weak, err
}

// 学习路径规划
func (s *RecommendationService) GenerateLearningPath(ctx context.Context, studentID, targetKnowledgeID string) (*LearningPath, error) {
    // 基于知识图谱的前置关系规划学习路径

    // 1. 递归获取所有前置知识点
    prerequisites, err := s.getAllPrerequisites(ctx, targetKnowledgeID)
    if err != nil {
        return nil, err
    }

    // 2. 检查学生已掌握的知识点
    mastered, err := s.getMasteredKnowledge(ctx, studentID)
    if err != nil {
        return nil, err
    }

    // 3. 生成学习路径
    var path []*LearningStep
    for _, prereq := range prerequisites {
        if !contains(mastered, prereq.ID) {
            step := &LearningStep{
                KnowledgeID: prereq.ID,
                Name:        prereq.Name,
                Type:        "prerequisite",
                Status:      "pending",
            }
            path = append(path, step)
        }
    }

    // 4. 添加目标知识点
    targetStep := &LearningStep{
        KnowledgeID: targetKnowledgeID,
        Name:        s.getKnowledgeName(ctx, targetKnowledgeID),
        Type:        "target",
        Status:      "pending",
    }
    path = append(path, targetStep)

    return &LearningPath{
        StudentID: studentID,
        TargetID:  targetKnowledgeID,
        Steps:     path,
        EstimatedDuration: len(path) * 2, // 每个知识点估计2小时
    }, nil
}

type LearningPath struct {
    StudentID         string         `json:"student_id"`
    TargetID          string         `json:"target_id"`
    Steps             []*LearningStep `json:"steps"`
    EstimatedDuration int            `json:"estimated_duration"`
}

type LearningStep struct {
    KnowledgeID string `json:"knowledge_id"`
    Name        string `json:"name"`
    Type        string `json:"type"` // prerequisite, target, optional
    Status      string `json:"status"` // pending, in_progress, completed
}
```

### 4.3 数据统计服务

```go
package service

import (
    "context"
    "time"
)

type StatisticsService struct {
    db    *sqlx.DB
    redis *redis.Client
}

// 学习进度统计
func (s *StatisticsService) GetLearningProgress(ctx context.Context, studentID string) (*LearningProgress, error) {
    // 统计已学习知识点、掌握程度、学习时长等

    var progress LearningProgress

    // 1. 基础统计
    statsQuery := `
        SELECT
            COUNT(DISTINCT km.knowledge_id) as total_studied,
            COUNT(DISTINCT k.id) as total_knowledge,
            AVG(km.mastery_score) as avg_mastery,
            SUM(lr.total_time) as total_time
        FROM knowledge_mastery km
        JOIN knowledge k ON km.knowledge_id = k.id
        LEFT JOIN (
            SELECT student_id, SUM(response_time_ms) as total_time
            FROM learning_record
            WHERE student_id = $1
        ) lr ON km.student_id = lr.student_id
        WHERE km.student_id = $1
    `

    err := s.db.GetContext(ctx, &progress, statsQuery, studentID)
    if err != nil {
        return nil, err
    }

    // 2. 掌握度分布
    distributionQuery := `
        SELECT
            CASE
                WHEN mastery_score >= 90 THEN 'excellent'
                WHEN mastery_score >= 80 THEN 'good'
                WHEN mastery_score >= 60 THEN 'average'
                WHEN mastery_score >= 40 THEN 'poor'
                ELSE 'very_poor'
            END as level,
            COUNT(*) as count
        FROM knowledge_mastery
        WHERE student_id = $1
        GROUP BY level
    `

    err = s.db.SelectContext(ctx, &progress.Distribution, distributionQuery, studentID)
    if err != nil {
        return nil, err
    }

    return &progress, nil
}

type LearningProgress struct {
    TotalStudied  int                     `json:"total_studied"`
    TotalKnowledge int                     `json:"total_knowledge"`
    AvgMastery    float64                 `json:"avg_mastery"`
    TotalTime     int                     `json:"total_time"`
    Distribution  []MasteryDistribution   `json:"distribution"`
}

type MasteryDistribution struct {
    Level string `json:"level"`
    Count int    `json:"count"`
}

// 班级统计分析
func (s *StatisticsService) GetClassStatistics(ctx context.Context, classID string) (*ClassStatistics, error) {
    // 班级整体掌握情况、薄弱环节分析

    var stats ClassStatistics

    // 1. 班级基本信息
    basicQuery := `
        SELECT
            COUNT(*) as total_students,
            AVG(km.mastery_score) as class_avg_mastery,
            COUNT(DISTINCT km.knowledge_id) as total_knowledge_studied
        FROM student s
        JOIN knowledge_mastery km ON s.id = km.student_id
        WHERE s.class_name = $1
    `

    err := s.db.GetContext(ctx, &stats, basicQuery, classID)
    if err != nil {
        return nil, err
    }

    // 2. 薄弱知识点分析
    weakQuery := `
        SELECT
            k.id as knowledge_id,
            k.name,
            AVG(km.mastery_score) as avg_mastery,
            COUNT(km.student_id) as student_count
        FROM knowledge k
        JOIN knowledge_mastery km ON k.id = km.knowledge_id
        JOIN student s ON km.student_id = s.id
        WHERE s.class_name = $1
        GROUP BY k.id, k.name
        HAVING AVG(km.mastery_score) < 70
        ORDER BY AVG(km.mastery_score) ASC
        LIMIT 10
    `

    err = s.db.SelectContext(ctx, &stats.WeakKnowledge, weakQuery, classID)
    if err != nil {
        return nil, err
    }

    return &stats, nil
}

type ClassStatistics struct {
    TotalStudents      int               `json:"total_students"`
    ClassAvgMastery    float64           `json:"class_avg_mastery"`
    TotalKnowledgeStudied int            `json:"total_knowledge_studied"`
    WeakKnowledge      []WeakKnowledgeStat `json:"weak_knowledge"`
}

type WeakKnowledgeStat struct {
    KnowledgeID string  `db:"knowledge_id"`
    Name        string  `db:"name"`
    AvgMastery  float64 `db:"avg_mastery"`
    StudentCount int    `db:"student_count"`
}
```

---

## 五、数据流与定时任务

### 5.1 数据流向
```
学生答题 → learning_record (实时记录)
           ↓
定时任务 → knowledge_mastery (每日汇总)
           ↓
推荐引擎 → 个性化推荐 (实时计算)
```

### 5.2 定时任务配置

```go
package main

import (
    "context"
    "log"
    "time"

    "github.com/robfig/cron/v3"
)

type CronScheduler struct {
    masteryService    *service.MasteryCalculationService
    backupService     *service.BackupService
    cacheService      *service.CacheService
}

func NewCronScheduler(
    masteryService *service.MasteryCalculationService,
    backupService *service.BackupService,
    cacheService *service.CacheService,
) *CronScheduler {
    return &CronScheduler{
        masteryService: masteryService,
        backupService:  backupService,
        cacheService:   cacheService,
    }
}

func (c *CronScheduler) Start() {
    cr := cron.New(cron.WithSeconds())

    // 每天凌晨2点执行掌握度计算
    _, err := cr.AddFunc("0 0 2 * * *", func() {
        log.Println("开始执行掌握度计算任务")
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
        defer cancel()

        if err := c.masteryService.BatchUpdateMastery(ctx); err != nil {
            log.Printf("掌握度计算任务失败: %v", err)
        } else {
            log.Println("掌握度计算任务完成")
        }
    })
    if err != nil {
        log.Fatalf("添加掌握度计算任务失败: %v", err)
    }

    // 每天凌晨4点执行数据备份
    _, err = cr.AddFunc("0 0 4 * * *", func() {
        log.Println("开始执行数据备份任务")
        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Hour)
        defer cancel()

        if err := c.backupService.PerformBackup(ctx); err != nil {
            log.Printf("数据备份任务失败: %v", err)
        } else {
            log.Println("数据备份任务完成")
        }
    })
    if err != nil {
        log.Fatalf("添加数据备份任务失败: %v", err)
    }

    // 每30分钟刷新缓存
    _, err = cr.AddFunc("0 */30 * * * *", func() {
        log.Println("开始刷新缓存")
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
        defer cancel()

        if err := c.cacheService.RefreshHotCache(ctx); err != nil {
            log.Printf("缓存刷新任务失败: %v", err)
        } else {
            log.Println("缓存刷新任务完成")
        }
    })
    if err != nil {
        log.Fatalf("添加缓存刷新任务失败: %v", err)
    }

    cr.Start()
    log.Println("定时任务调度器已启动")
}

func (c *CronScheduler) Stop() {
    log.Println("定时任务调度器已停止")
}
```

---

## 六、性能优化策略

### 6.1 数据库优化
- **索引设计**: 为所有外键和查询字段建立索引，PostgreSQL GIN 索引支持数组和 JSONB 查询
- **分区表**: 对 learning_record 按时间分区，提高查询性能
- **读写分离**: 主从数据库配置，读操作使用从库
- **连接池**: 使用 pgxpool 管理数据库连接

### 6.2 缓存策略
```go
package service

import (
    "context"
    "encoding/json"
    "time"

    "github.com/go-redis/redis/v8"
)

type KnowledgeService struct {
    db    *sqlx.DB
    redis *redis.Client
}

func (s *KnowledgeService) GetKnowledge(ctx context.Context, id string) (*Knowledge, error) {
    // 1. 尝试从缓存获取
    cacheKey := fmt.Sprintf("knowledge:%s", id)
    cached, err := s.redis.Get(ctx, cacheKey).Result()
    if err == nil {
        var knowledge Knowledge
        if err := json.Unmarshal([]byte(cached), &knowledge); err == nil {
            return &knowledge, nil
        }
    }

    // 2. 从数据库查询
    var knowledge Knowledge
    query := `SELECT id, name, type, definition, core_features, prerequisites, importance, textbook_location, created_at
              FROM knowledge WHERE id = $1`

    err = s.db.GetContext(ctx, &knowledge, query, id)
    if err != nil {
        return nil, err
    }

    // 3. 写入缓存
    data, _ := json.Marshal(knowledge)
    s.redis.Set(ctx, cacheKey, data, 30*time.Minute)

    return &knowledge, nil
}

func (s *KnowledgeService) UpdateKnowledge(ctx context.Context, id string, knowledge *Knowledge) error {
    // 更新数据库
    query := `UPDATE knowledge SET name = $1, type = $2, definition = $3,
              core_features = $4, prerequisites = $5, importance = $6,
              textbook_location = $7 WHERE id = $8`

    _, err := s.db.ExecContext(ctx, query,
        knowledge.Name, knowledge.Type, knowledge.Definition,
        pq.Array(knowledge.CoreFeatures), pq.Array(knowledge.Prerequisites),
        knowledge.Importance, knowledge.TextbookLocation, id)

    if err != nil {
        return err
    }

    // 清除缓存
    cacheKey := fmt.Sprintf("knowledge:%s", id)
    s.redis.Del(ctx, cacheKey)

    return nil
}
```

### 6.3 异步处理
```go
package service

import (
    "context"
    "log"
    "sync"

    "github.com/go-redis/redis/v8"
)

type AsyncProcessor struct {
    masteryService     *MasteryCalculationService
    recommendationSvc  *RecommendationService
    workerPool        chan struct{}
    wg                sync.WaitGroup
}

func NewAsyncProcessor(
    masteryService *MasteryCalculationService,
    recommendationSvc *RecommendationService,
    workerCount int,
) *AsyncProcessor {
    return &AsyncProcessor{
        masteryService:    masteryService,
        recommendationSvc: recommendationSvc,
        workerPool:        make(chan struct{}, workerCount),
    }
}

// 异步处理学习记录，不影响用户响应
func (p *AsyncProcessor) ProcessLearningRecord(ctx context.Context, record *LearningRecord) {
    p.wg.Add(1)
    go func() {
        defer p.wg.Done()

        // 获取工作槽位
        p.workerPool <- struct{}{}
        defer func() { <-p.workerPool }()

        // 异步处理学习记录
        if err := p.masteryService.UpdateMasteryForRecord(ctx, record); err != nil {
            log.Printf("更新掌握度失败: %v", err)
        }

        if err := p.recommendationSvc.UpdateRecommendations(ctx, record.StudentID); err != nil {
            log.Printf("更新推荐失败: %v", err)
        }
    }()
}

// 批量异步处理
func (p *AsyncProcessor) ProcessBatch(ctx context.Context, records []*LearningRecord) {
    for _, record := range records {
        p.ProcessLearningRecord(ctx, record)
    }
}

// 等待所有异步任务完成
func (p *AsyncProcessor) Wait() {
    p.wg.Wait()
}

// 优雅关闭
func (p *AsyncProcessor) Shutdown(ctx context.Context) {
    done := make(chan struct{})
    go func() {
        p.Wait()
        close(done)
    }()

    select {
    case <-done:
        log.Println("所有异步任务已完成")
    case <-ctx.Done():
        log.Println("异步任务处理超时")
    }
}
```

---

## 七、安全与权限设计

### 7.1 权限控制
```go
package middleware

import (
    "context"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v4"
)

type Claims struct {
    UserID   string   `json:"user_id"`
    Username string   `json:"username"`
    Role     string   `json:"role"`
    jwt.RegisteredClaims
}

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少认证信息"})
            c.Abort()
            return
        }

        // 验证 Bearer Token
        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        claims := &Claims{}

        token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
            return []byte("your-secret-key"), nil
        })

        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证信息"})
            c.Abort()
            return
        }

        // 将用户信息存入上下文
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Set("role", claims.Role)
        c.Next()
    }
}

// 权限检查中间件
func RequireRole(role string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userRole, exists := c.Get("role")
        if !exists || userRole != role {
            c.JSON(http.StatusForbidden, gin.H{"error": "权限不足"})
            c.Abort()
            return
        }
        c.Next()
    }
}

// 学生只能访问自己的数据，教师可以访问所有学生数据
func CheckStudentAccess() gin.HandlerFunc {
    return func(c *gin.Context) {
        userRole, _ := c.Get("role")
        userID, _ := c.Get("user_id")
        targetStudentID := c.Param("studentId")

        // 如果是教师，允许访问
        if userRole == "teacher" {
            c.Next()
            return
        }

        // 如果是学生，只能访问自己的数据
        if userRole == "student" && userID != targetStudentID {
            c.JSON(http.StatusForbidden, gin.H{"error": "只能访问自己的数据"})
            c.Abort()
            return
        }

        c.Next()
    }
}
```

### 7.2 数据验证
```go
package dto

import (
    "errors"
    "strconv"
)

type LearningRecordDTO struct {
    StudentID    string  `json:"student_id" binding:"required"`
    ProblemID    string  `json:"problem_id" binding:"required"`
    KnowledgeID  string  `json:"knowledge_id" binding:"required"`
    MethodID     *string `json:"method_id,omitempty"`
    QuestionPart string  `json:"question_part" binding:"required"`
    IsCorrect    bool    `json:"is_correct" binding:"required"`
    Score        float64 `json:"score" binding:"min=0,max=100"`
    MaxScore     float64 `json:"max_score" binding:"required,min=0.1"`
    ExamID       string  `json:"exam_id" binding:"required"`
    ExamDate     string  `json:"exam_date" binding:"required"`
    ResponseTime *int    `json:"response_time_ms,omitempty"`
}

// 自定义验证器
func (dto *LearningRecordDTO) Validate() error {
    // 检查分数不能超过满分
    if dto.Score > dto.MaxScore {
        return errors.New("分数不能超过满分")
    }

    // 验证考试日期格式
    if _, err := time.Parse("2006-01-02", dto.ExamDate); err != nil {
        return errors.New("考试日期格式错误，应为 YYYY-MM-DD")
    }

    // 验证小题标识
    validParts := []string{"小题1", "小题2", "小题3", "整体"}
    isValid := false
    for _, part := range validParts {
        if dto.QuestionPart == part {
            isValid = true
            break
        }
    }
    if !isValid {
        return errors.New("无效的小题标识")
    }

    // 验证响应时间（如果提供）
    if dto.ResponseTime != nil && *dto.ResponseTime < 0 {
        return errors.New("响应时间不能为负数")
    }

    return nil
}

// API 中的使用示例
func (h *Handler) CreateLearningRecord(c *gin.Context) {
    var dto LearningRecordDTO
    if err := c.ShouldBindJSON(&dto); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    // 自定义验证
    if err := dto.Validate(); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    // 检查学生ID权限
    userRole, _ := c.Get("role")
    userID, _ := c.Get("user_id")
    if userRole == "student" && userID != dto.StudentID {
        c.JSON(403, gin.H{"error": "只能为自己提交学习记录"})
        return
    }

    // 处理业务逻辑
    record := &model.LearningRecord{
        StudentID:    dto.StudentID,
        ProblemID:    dto.ProblemID,
        KnowledgeID:  dto.KnowledgeID,
        MethodID:     dto.MethodID,
        QuestionPart: dto.QuestionPart,
        IsCorrect:    dto.IsCorrect,
        Score:        dto.Score,
        MaxScore:     dto.MaxScore,
        ExamID:       dto.ExamID,
        ExamDate:     dto.ExamDate,
        ResponseTime: dto.ResponseTime,
    }

    if err := h.learningService.CreateRecord(c.Request.Context(), record); err != nil {
        c.JSON(500, gin.H{"error": "创建学习记录失败"})
        return
    }

    c.JSON(201, gin.H{"message": "学习记录创建成功"})
}
```

---

## 八、部署与监控

### 8.1 Docker 配置

```dockerfile
# Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/api

FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/

COPY --from=builder /app/main .
COPY --from=builder /app/migrations ./migrations

EXPOSE 8080
CMD ["./main"]
```

```yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_USER=knowledge
      - DB_PASSWORD=secret
      - DB_NAME=knowledge_db
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      - postgres
      - redis
    networks:
      - knowledge-network

  postgres:
    image: postgres:14
    environment:
      - POSTGRES_USER=knowledge
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=knowledge_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./migrations:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"
    networks:
      - knowledge-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - knowledge-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app
    networks:
      - knowledge-network

volumes:
  postgres_data:
  redis_data:

networks:
  knowledge-network:
    driver: bridge
```

### 8.2 Kubernetes 部署配置

```yaml
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: knowledge-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: knowledge-api
  template:
    metadata:
      labels:
        app: knowledge-api
    spec:
      containers:
      - name: api
        image: knowledge-api:latest
        ports:
        - containerPort: 8080
        env:
        - name: DB_HOST
          value: "postgres-service"
        - name: REDIS_HOST
          value: "redis-service"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
  name: knowledge-api-service
spec:
  selector:
    app: knowledge-api
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer
```

### 8.3 监控配置

```go
package monitoring

import (
    "context"
    "net/http"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    // HTTP 请求指标
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "endpoint", "status"},
    )

    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "http_request_duration_seconds",
            Help: "HTTP request duration in seconds",
        },
        []string{"method", "endpoint"},
    )

    // 业务指标
    learningRecordsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "learning_records_total",
            Help: "Total number of learning records created",
        },
        []string{"student_id", "knowledge_id"},
    )

    masteryScoreDistribution = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "mastery_score_distribution",
            Help: "Distribution of mastery scores",
            Buckets: []float64{0, 20, 40, 60, 80, 100},
        },
        []string{"knowledge_id"},
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal)
    prometheus.MustRegister(httpRequestDuration)
    prometheus.MustRegister(learningRecordsTotal)
    prometheus.MustRegister(masteryScoreDistribution)
}

type MonitoringMiddleware struct{}

func (m *MonitoringMiddleware) Measure(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}

        next.ServeHTTP(wrapped, r)

        duration := time.Since(start).Seconds()

        httpRequestsTotal.WithLabelValues(
            r.Method,
            r.URL.Path,
            string(rune(wrapped.statusCode)),
        ).Inc()

        httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// Prometheus metrics endpoint
func MetricsHandler() http.Handler {
    return promhttp.Handler()
}
```

### 8.4 日志配置

```go
package logger

import (
    "os"

    "github.com/sirupsen/logrus"
    "gopkg.in/natefinch/lumberjack.v2"
)

func NewLogger() *logrus.Logger {
    log := logrus.New()

    // 设置日志格式
    log.SetFormatter(&logrus.JSONFormatter{
        TimestampFormat: "2006-01-02 15:04:05",
    })

    // 设置日志级别
    level := os.Getenv("LOG_LEVEL")
    if level == "" {
        level = "info"
    }
    logLevel, err := logrus.ParseLevel(level)
    if err != nil {
        logLevel = logrus.InfoLevel
    }
    log.SetLevel(logLevel)

    // 设置日志输出
    log.SetOutput(&lumberjack.Logger{
        Filename:   "/var/log/knowledge-api/app.log",
        MaxSize:    100, // MB
        MaxBackups: 3,
        MaxAge:     28, // days
        Compress:   true,
    })

    return log
}
```

### 8.5 健康检查

```go
package health

import (
    "context"
    "net/http"

    "github.com/gin-gonic/gin"
)

type HealthChecker struct {
    db    *sqlx.DB
    redis *redis.Client
}

func NewHealthChecker(db *sqlx.DB, redis *redis.Client) *HealthChecker {
    return &HealthChecker{db: db, redis: redis}
}

func (h *HealthChecker) CheckHealth(c *gin.Context) {
    status := map[string]interface{}{
        "status": "healthy",
        "timestamp": time.Now(),
        "version": os.Getenv("APP_VERSION"),
    }

    // 检查数据库连接
    if err := h.db.Ping(); err != nil {
        status["status"] = "unhealthy"
        status["database"] = "disconnected"
        c.JSON(503, status)
        return
    }
    status["database"] = "connected"

    // 检查 Redis 连接
    if _, err := h.redis.Ping().Result(); err != nil {
        status["status"] = "degraded"
        status["redis"] = "disconnected"
        c.JSON(200, status)
        return
    }
    status["redis"] = "connected"

    c.JSON(200, status)
}

func (h *HealthChecker) CheckReadiness(c *gin.Context) {
    // 检查应用是否准备好接收请求
    if err := h.db.Ping(); err != nil {
        c.JSON(503, gin.H{"ready": false})
        return
    }

    c.JSON(200, gin.H{"ready": true})
}
```

### 8.6 监控指标说明

- **应用性能指标**：
  - `http_requests_total`: HTTP 请求总数
  - `http_request_duration_seconds`: HTTP 请求响应时间
  - `http_requests_errors_total`: HTTP 错误请求数

- **业务指标**：
  - `learning_records_total`: 学习记录总数
  - `mastery_score_distribution`: 掌握度分布
  - `recommendations_generated_total`: 推荐生成总数

- **数据库指标**：
  - `db_connections_active`: 活跃数据库连接数
  - `db_query_duration_seconds`: 数据库查询时间
  - `db_slow_queries_total`: 慢查询总数

- **缓存指标**：
  - `redis_hits_total`: Redis 缓存命中数
  - `redis_misses_total`: Redis 缓存未命中数
  - `cache_hit_ratio`: 缓存命中率

---

## 九、开发里程碑

### Phase 1: 基础功能 (4周)
- [ ] 数据库表结构创建
- [ ] 基础CRUD API
- [ ] 学习记录录入
- [ ] 基础掌握度计算

### Phase 2: 核心业务 (6周)  
- [ ] 个性化推荐引擎
- [ ] 学习路径规划
- [ ] 数据统计分析
- [ ] 缓存和性能优化

### Phase 3: 高级功能 (4周)
- [ ] 批量数据处理
- [ ] 报表导出
- [ ] 系统监控
- [ ] 压力测试

---

## 十、技术风险与应对

### 10.1 数据量风险
- **风险**: learning_record表数据量快速增长
- **应对**: 分区表、归档策略、读写分离

### 10.2 计算复杂度
- **风险**: 实时推荐计算资源消耗大
- **应对**: 缓存策略、异步计算、定时预计算

### 10.3 数据一致性
- **风险**: 掌握度数据与学习记录不一致
- **应对**: 事务控制、数据校验、定期修复任务

---

**此设计概要为开发团队提供了完整的技术方案，可直接用于项目开发。** 🚀