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

1771 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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