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