# 知识图谱系统 - 后端项目设计概要 ## 项目概述 基于三层架构(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 数据一致性 - **风险**: 掌握度数据与学习记录不一致 - **应对**: 事务控制、数据校验、定期修复任务 --- **此设计概要为开发团队提供了完整的技术方案,可直接用于项目开发。** 🚀