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