Compare commits

..

No commits in common. 'main' and 'master' have entirely different histories.
main ... master

2
.gitignore vendored

@ -0,0 +1,2 @@
node_modules
.env

@ -1,2 +0,0 @@
# feynman-platform-backend

@ -0,0 +1,131 @@
const axios = require('axios');
const AipSpeechClient = require('baidu-aip-sdk').speech;
const APP_ID = process.env.BAIDU_APP_ID;
const API_KEY = process.env.BAIDU_API_KEY;
const SECRET_KEY = process.env.BAIDU_SECRET_KEY;
const QIANFAN_API_KEY = process.env.QIANFAN_API_KEY;
const QIANFAN_SECRET_KEY = process.env.QIANFAN_SECRET_KEY;
const client = new AipSpeechClient(APP_ID, API_KEY, SECRET_KEY);
async function transcribeAudio(req, res) {
if (!req.file) {
return res.status(400).json({ msg: 'No audio file uploaded.' });
}
const audioBuffer = req.file.buffer;
try {
const result = await client.recognize(audioBuffer, 'wav', 16000, {
dev_pid: 1537,
});
if (result.err_no === 0) {
return res.json({ result: result.result[0] });
}
console.error('Baidu ASR service error:', result);
return res.status(502).json({ msg: 'Baidu ASR service error', error: result });
} catch (error) {
console.error('Error calling Baidu ASR API:', error);
return res.status(500).json({ msg: 'Server error during transcription.' });
}
}
async function getQianfanAccessToken() {
if (!QIANFAN_API_KEY || !QIANFAN_SECRET_KEY) {
throw new Error('QIANFAN credentials are not configured');
}
const url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${QIANFAN_API_KEY}&client_secret=${QIANFAN_SECRET_KEY}`;
try {
const response = await axios.get(url);
if (!response.data?.access_token) {
throw new Error('Invalid access token response');
}
return response.data.access_token;
} catch (error) {
const message = error.response?.data || error.message;
console.error('Error getting Qianfan access token:', message);
throw new Error('Failed to get Qianfan access token');
}
}
async function evaluateFeynmanAttempt(req, res) {
const { originalContent, transcribedText } = req.body || {};
if (!originalContent || !transcribedText) {
return res.status(400).json({ msg: 'Original content and transcribed text are required.' });
}
try {
const accessToken = await getQianfanAccessToken();
const url = `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${accessToken}`;
const prompt = `你是一个严格而友善的计算机科学学习教练。你的任务是评估学生对一个知识点的复述,并给出反馈。
原始知识点:
\`\`\`
${originalContent}
\`\`\`
学生的口头复述文本:
\`\`\`
${transcribedText}
\`\`\`
请你完成以下三项任务:
1. **文本润色**: 将学生的复述文本润色成一段通顺专业书面化的文字修正明显的语法错误和口语化表达但保持其核心观点不变
2. **综合评价**: 基于原始知识点对学生的复述进行评价指出其优点和可以改进的地方
3. **评分**: 综合考虑准确性完整性逻辑性和流畅性给出一个0到100的整数分数
请严格按照以下JSON格式返回你的结果不要包含任何额外的解释或文字
{
"polishedText": "这里是润色后的文本",
"evaluation": "这里是你的综合评价",
"strengths": ["优点1", "优点2"],
"weaknesses": ["可以改进的地方1", "可以改进的地方2"],
"score": 85
}`;
const response = await axios.post(
url,
{
messages: [
{
role: 'user',
content: prompt,
},
],
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 20_000,
},
);
const rawResult = response.data?.result || response.data?.body?.result;
if (!rawResult) {
throw new Error('Empty result from Qianfan');
}
try {
const parsed = JSON.parse(rawResult);
return res.json(parsed);
} catch (parseError) {
console.error('Failed to parse Qianfan result:', rawResult);
throw new Error('Invalid JSON returned by Qianfan');
}
} catch (error) {
const message = error.response?.data || error.message;
console.error('Error calling LLM API:', message);
return res.status(500).json({ msg: 'AI evaluation failed', detail: message });
}
}
module.exports = {
transcribeAudio,
evaluateFeynmanAttempt,
};

@ -0,0 +1,27 @@
// index.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 3000;
// --- 核心中间件 ---
app.use(cors());
app.use(express.json());
// --- 数据库连接 ---
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB connected successfully!'))
.catch(err => console.error('MongoDB connection error:', err));
// --- API 路由 ---
app.use('/api/users', require('./routes/users'));
app.use('/api/knowledge-points', require('./routes/knowledgePoints')); // 新增
app.use('/api/audio', require('./routes/audio'));
app.use('/api/attempts', require('./routes/attempts'));
app.listen(port, () => {
console.log(`Feynman Platform backend is running at http://localhost:${port}`);
});

@ -0,0 +1,26 @@
// middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = function(req, res, next) {
// 1. 从请求头中获取token
const token = req.header('x-auth-token');
// 2. 检查token是否存在
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' }); // 401: 未授权
}
// 3. 验证token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 将解码后的用户信息特别是user.id附加到请求对象上
req.user = decoded.user;
// 调用next(),将控制权交给下一个中间件或路由处理器
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};

@ -0,0 +1,15 @@
const mongoose = require('mongoose');
const FeynmanAttemptSchema = new mongoose.Schema(
{
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
knowledgePointId: { type: mongoose.Schema.Types.ObjectId, ref: 'KnowledgePoint', required: true },
text: { type: String, required: true },
durationMs: { type: Number, default: 0 },
},
{ timestamps: true }
);
module.exports = mongoose.model('FeynmanAttempt', FeynmanAttemptSchema);

@ -0,0 +1,27 @@
// models/KnowledgePoint.js
const mongoose = require('mongoose');
const KnowledgePointSchema = new mongoose.Schema({
user: { // 关联到用户
type: mongoose.Schema.Types.ObjectId,
ref: 'User' // 引用User模型
},
title: {
type: String,
required: true
},
content: { // 存放Markdown, LaTeX, Mermaid等原始内容
type: String,
required: true
},
status: { // 学习状态: 'not_started', 'in_progress', 'mastered'
type: String,
default: 'not_started'
},
reviewList: { // 是否在复习列表中
type: Boolean,
default: false
}
}, { timestamps: true });
module.exports = mongoose.model('KnowledgePoint', KnowledgePointSchema);

@ -0,0 +1,10 @@
// models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
}, { timestamps: true }); // timestamps会自动添加createdAt和updatedAt字段
module.exports = mongoose.model('User', UserSchema);

2335
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,24 @@
{
"name": "feynman-platform-backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.7.9",
"baidu-aip-sdk": "^4.16.16",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.20.0",
"mongoose": "^8.18.2",
"multer": "^2.0.2"
}
}

@ -0,0 +1,30 @@
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
const FeynmanAttempt = require('../models/FeynmanAttempt');
// @route POST /api/attempts
// @desc 保存一次复述结果
// @access Private
router.post('/', auth, async (req, res) => {
try {
const { knowledgePointId, text, durationMs } = req.body || {};
if (!knowledgePointId || !text) {
return res.status(400).json({ msg: 'knowledgePointId 与 text 为必填项' });
}
const attempt = await FeynmanAttempt.create({
userId: req.user.id,
knowledgePointId,
text,
durationMs: typeof durationMs === 'number' ? durationMs : 0,
});
return res.json(attempt);
} catch (err) {
console.error('Create attempt failed:', err);
return res.status(500).json({ msg: 'Server error' });
}
});
module.exports = router;

@ -0,0 +1,19 @@
const express = require('express');
const multer = require('multer');
const auth = require('../middleware/auth');
const { transcribeAudio, evaluateFeynmanAttempt } = require('../controllers/baiduAiController');
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
// @route POST /api/audio/transcribe
// @desc 上传音频并进行语音识别
// @access Private
router.post('/transcribe', auth, upload.single('audio'), transcribeAudio);
// @route POST /api/audio/evaluate
// @desc AI润色与评分
// @access Private
router.post('/evaluate', auth, evaluateFeynmanAttempt);
module.exports = router;

@ -0,0 +1,128 @@
// routes/knowledgePoints.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth'); // 引入认证中间件
const KnowledgePoint = require('../models/KnowledgePoint');
// @route POST /api/knowledge-points
// @desc 创建一个新的知识点
// @access Private (需要登录)
router.post('/', auth, async (req, res) => { // 在这里使用auth中间件
try {
const { title, content } = req.body;
const newKp = new KnowledgePoint({
title,
content,
user: req.user.id // 从auth中间件附加的req.user中获取用户ID
});
const kp = await newKp.save();
res.json(kp);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route GET /api/knowledge-points
// @desc 获取当前用户的所有知识点
// @access Private
router.get('/', auth, async (req, res) => {
try {
const kps = await KnowledgePoint.find({ user: req.user.id }).sort({ createdAt: -1 });
res.json(kps);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route GET /api/knowledge-points/:id
// @desc 获取单个知识点详情
// @access Private
router.get('/:id', auth, async (req, res) => {
try {
const kp = await KnowledgePoint.findById(req.params.id);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
return res.json(kp);
} catch (err) {
console.error(err.message);
return res.status(500).send('Server Error');
}
});
// @route PUT /api/knowledge-points/:id
// @desc 更新一个知识点
// @access Private
router.put('/:id', auth, async (req, res) => {
try {
let kp = await KnowledgePoint.findById(req.params.id);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
// 确保是该用户自己的知识点
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
const { title, content, status, reviewList } = req.body;
kp = await KnowledgePoint.findByIdAndUpdate(
req.params.id,
{ $set: { title, content, status, reviewList } },
{ new: true } // 返回更新后的文档
);
res.json(kp);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route POST /api/knowledge-points/:id/append-attempt
// @desc 将一次复述文本追加到知识点内容以Markdown段落追加
// @access Private
router.post('/:id/append-attempt', auth, async (req, res) => {
try {
const { text } = req.body || {};
if (!text) {
return res.status(400).json({ msg: 'text 为必填项' });
}
const kp = await KnowledgePoint.findById(req.params.id);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
const now = new Date();
const pad2 = (n) => String(n).padStart(2, '0');
const timestamp = `${now.getFullYear()}-${pad2(now.getMonth()+1)}-${pad2(now.getDate())} ${pad2(now.getHours())}:${pad2(now.getMinutes())}`;
const block = `\n\n## 复述记录(${timestamp}\n\n${text}\n`;
kp.content = (kp.content || '') + block;
await kp.save();
return res.json(kp);
} catch (err) {
console.error(err.message);
return res.status(500).send('Server Error');
}
});
// @route DELETE /api/knowledge-points/:id
// @desc 删除一个知识点
// @access Private
router.delete('/:id', auth, async (req, res) => {
try {
console.log('[DEBUG] DELETE request for id:', req.params.id, 'user:', req.user.id);
let kp = await KnowledgePoint.findById(req.params.id);
console.log('[DEBUG] found kp:', !!kp);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
console.log('[DEBUG] deleting kp, calling findByIdAndDelete...');
await KnowledgePoint.findByIdAndDelete(req.params.id);
console.log('[DEBUG] delete successful');
res.json({ msg: 'Knowledge point removed' });
} catch (err) {
console.error('[DEBUG] delete error:', err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;

@ -0,0 +1,111 @@
// routes/users.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs'); // 引入加密库
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// --- 用户注册 API (已集成密码加密) ---
// @route POST /api/users/register
// @desc 注册一个新用户
// @access Public
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// 1. 检查用户是否已存在
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'User already exists' });
}
// 2. 创建新用户实例
user = new User({
username,
email,
password
});
// 3. 【安全核心】对密码进行哈希加密
// 讲解:我们使用 bcrypt 库。它会先生成一个“盐”salt这是一个随机字符串
// 然后将盐和原始密码混合在一起进行哈希计算。
// 这样做可以确保即使两个用户设置了相同的密码,它们在数据库中的哈希值也完全不同。
const salt = await bcrypt.genSalt(10); // 10是安全强度数值越大越安全但越耗时
user.password = await bcrypt.hash(password, salt); // 生成加密后的密码
// 4. 保存用户到数据库
await user.save();
// 5. 注册成功直接生成JWT并返回实现注册后自动登录
const payload = {
user: {
id: user.id,
username: user.username,
email: user.email
}
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '5h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// --- 用户登录 API ---
// @route POST /api/users/login
// @desc 用户登录并获取token
// @access Public
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// 1. 检查用户是否存在
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// 2. 【安全核心】比较密码
// 这里使用 bcrypt.compare 来安全地比较客户端传来的原始密码和数据库中存储的哈希密码。
// 它会自动处理盐值我们无需关心。只有密码匹配才会返回true。
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// 3. 登录成功生成JWT
const payload = {
user: {
id: user.id,
username: user.username,
email: user.email
}
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '5h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
Loading…
Cancel
Save