note/知识图谱/后端/知识图谱系统 - 后端项目设计概要.md
2025-11-19 10:16:05 +08:00

51 KiB
Raw Permalink Blame History

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

项目概述

基于三层架构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知识点表

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方法表

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题目表

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学生表

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学习记录表 - 核心流水表)

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知识点掌握度表

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方法掌握度表

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

知识点管理

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 # 获取相关题目

方法管理

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

题目管理

GET    /api/problems                   # 获取题目列表(支持筛选)
POST   /api/problems                   # 创建题目
GET    /api/problems/{id}              # 获取题目详情
PUT    /api/problems/{id}              # 更新题目
GET    /api/problems/recommendations   # 个性化题目推荐

3.2 学习记录API

答题记录

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

批量记录处理

POST   /api/learning-records/batch     # 批量提交答题记录

3.3 掌握度分析API

知识点掌握度

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

方法掌握度

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

3.4 个性化推荐API

题目推荐

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

四、核心业务逻辑

4.1 掌握度计算服务

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 个性化推荐引擎

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 定时任务配置

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 缓存策略

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 异步处理

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 权限控制

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 数据验证

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
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"]
# 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 部署配置

# 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 监控配置

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 日志配置

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 健康检查

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 数据一致性

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

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