chore: 清理后端冗余代码和数据库
- 移除 pinyin 拼音探索模块(routes/controllers/models) - 移除 gallery、bgm、share、Level 等旧 WDKJ 模块 - 清理数据库 9 个无用集合(galleries/bgms/pinyin*等) - 清理 38 条旧 loginlogs - 移除旧脚本(initPinyinData/generateAudioUrls/init-knowledge-ai) - 新增 enrich-content.js 内容丰富脚本 - 优化前端 16 页面文案,改为用户友好风格 - 修复 CSS 变量全局加载问题 - 修复排行榜公开访问问题
This commit is contained in:
@@ -1,288 +0,0 @@
|
||||
const { PinyinAchievement, PinyinProgress } = require('../../models/pinyin');
|
||||
|
||||
/**
|
||||
* 获取成就列表
|
||||
* GET /api/pinyin/achievements
|
||||
*/
|
||||
exports.getAchievements = async (req, res) => {
|
||||
try {
|
||||
const { type } = req.query;
|
||||
|
||||
const query = { isActive: true };
|
||||
if (type) query.type = type;
|
||||
|
||||
const achievements = await PinyinAchievement.find(query)
|
||||
.sort({ order: 1, createdAt: 1 })
|
||||
.select('-__v');
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
list: achievements
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取成就列表失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取成就列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户已获得的成就
|
||||
* GET /api/pinyin/achievements/my
|
||||
*/
|
||||
exports.getMyAchievements = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
|
||||
const progress = await PinyinProgress.findOne({ userId });
|
||||
|
||||
if (!progress || !progress.achievements || progress.achievements.length === 0) {
|
||||
return res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
total: 0,
|
||||
list: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取成就详情
|
||||
const achievementCodes = progress.achievements.map(a => a.code);
|
||||
const achievements = await PinyinAchievement.find({
|
||||
code: { $in: achievementCodes }
|
||||
}).select('-__v');
|
||||
|
||||
// 合并获得时间
|
||||
const achievementsWithTime = achievements.map(ach => {
|
||||
const userAch = progress.achievements.find(a => a.code === ach.code);
|
||||
return {
|
||||
...ach.toObject(),
|
||||
obtainedAt: userAch ? userAch.obtainedAt : null
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
total: achievementsWithTime.length,
|
||||
list: achievementsWithTime
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户成就失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取用户成就失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查成就达成
|
||||
* POST /api/pinyin/achievements/check
|
||||
*/
|
||||
exports.checkAchievements = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
|
||||
const progress = await PinyinProgress.findOne({ userId });
|
||||
if (!progress) {
|
||||
return res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
newAchievements: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有启用的成就
|
||||
const achievements = await PinyinAchievement.find({ isActive: true });
|
||||
|
||||
const newAchievements = [];
|
||||
|
||||
for (const achievement of achievements) {
|
||||
// 检查是否已获得
|
||||
if (progress.achievements.some(a => a.code === achievement.code)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查条件
|
||||
const isAchieved = checkCondition(progress, achievement.condition);
|
||||
|
||||
if (isAchieved) {
|
||||
progress.achievements.push({
|
||||
code: achievement.code,
|
||||
obtainedAt: new Date()
|
||||
});
|
||||
|
||||
newAchievements.push({
|
||||
code: achievement.code,
|
||||
name: achievement.name,
|
||||
description: achievement.description,
|
||||
icon: achievement.icon,
|
||||
reward: achievement.reward
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
await progress.save();
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
newAchievements
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查成就失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '检查成就失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建成就(管理员)
|
||||
* POST /api/pinyin/achievements
|
||||
*/
|
||||
exports.createAchievement = async (req, res) => {
|
||||
try {
|
||||
const achievementData = req.body;
|
||||
|
||||
// 检查code是否已存在
|
||||
const existing = await PinyinAchievement.findOne({ code: achievementData.code });
|
||||
if (existing) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '该成就代码已存在'
|
||||
});
|
||||
}
|
||||
|
||||
const achievement = new PinyinAchievement(achievementData);
|
||||
await achievement.save();
|
||||
|
||||
res.status(201).json({
|
||||
code: 0,
|
||||
message: '创建成功',
|
||||
data: achievement
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建成就失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '创建成就失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新成就(管理员)
|
||||
* PUT /api/pinyin/achievements/:id
|
||||
*/
|
||||
exports.updateAchievement = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
// 不允许修改code
|
||||
delete updateData.code;
|
||||
|
||||
const achievement = await PinyinAchievement.findByIdAndUpdate(
|
||||
id,
|
||||
{ $set: updateData },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!achievement) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '成就不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data: achievement
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新成就失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '更新成就失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除成就(管理员)
|
||||
* DELETE /api/pinyin/achievements/:id
|
||||
*/
|
||||
exports.deleteAchievement = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const achievement = await PinyinAchievement.findByIdAndDelete(id);
|
||||
|
||||
if (!achievement) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '成就不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除成就失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '删除成就失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查成就条件
|
||||
* @param {Object} progress - 用户进度
|
||||
* @param {Object} condition - 条件
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function checkCondition(progress, condition) {
|
||||
switch (condition.type) {
|
||||
case 'explore_count':
|
||||
return progress.totalExplored >= condition.value;
|
||||
|
||||
case 'collect_count':
|
||||
return progress.totalStones >= condition.value;
|
||||
|
||||
case 'streak_days':
|
||||
return progress.streakDays >= condition.value;
|
||||
|
||||
case 'complete_symbol':
|
||||
return progress.isSymbolCompleted(condition.symbol);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
const { PinyinContent } = require('../../models/pinyin');
|
||||
|
||||
/**
|
||||
* 获取拼音内容列表
|
||||
* GET /api/pinyin/contents
|
||||
*/
|
||||
exports.getContents = async (req, res) => {
|
||||
try {
|
||||
const { type, isFree, page = 1, limit = 20 } = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const query = { status: 'active' };
|
||||
if (type) query.type = type;
|
||||
if (isFree !== undefined) query.isFree = isFree === 'true';
|
||||
|
||||
// 分页
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
// 查询数据
|
||||
const contents = await PinyinContent.find(query)
|
||||
.select('-__v')
|
||||
.sort({ order: 1, symbol: 1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit));
|
||||
|
||||
// 获取总数
|
||||
const total = await PinyinContent.countDocuments(query);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
list: contents,
|
||||
total,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
totalPages: Math.ceil(total / parseInt(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取拼音内容列表失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取拼音内容列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取单个拼音详情
|
||||
* GET /api/pinyin/contents/:symbol
|
||||
*/
|
||||
exports.getContentBySymbol = async (req, res) => {
|
||||
try {
|
||||
const { symbol } = req.params;
|
||||
|
||||
const content = await PinyinContent.findOne({
|
||||
symbol: symbol.toLowerCase(),
|
||||
status: 'active'
|
||||
}).select('-__v');
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '拼音内容不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: content
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取拼音详情失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取拼音详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取拼音音频
|
||||
* GET /api/pinyin/contents/:symbol/audio
|
||||
*/
|
||||
exports.getContentAudio = async (req, res) => {
|
||||
try {
|
||||
const { symbol } = req.params;
|
||||
|
||||
const content = await PinyinContent.findOne({
|
||||
symbol: symbol.toLowerCase(),
|
||||
status: 'active'
|
||||
}).select('symbol audioUrl name');
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '拼音内容不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
symbol: content.symbol,
|
||||
audioUrl: content.audioUrl,
|
||||
name: content.name
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取拼音音频失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取拼音音频失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建拼音内容(管理员)
|
||||
* POST /api/pinyin/contents
|
||||
*/
|
||||
exports.createContent = async (req, res) => {
|
||||
try {
|
||||
const contentData = req.body;
|
||||
|
||||
// 检查是否已存在
|
||||
const existing = await PinyinContent.findOne({ symbol: contentData.symbol });
|
||||
if (existing) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '该拼音内容已存在'
|
||||
});
|
||||
}
|
||||
|
||||
const content = new PinyinContent(contentData);
|
||||
await content.save();
|
||||
|
||||
res.status(201).json({
|
||||
code: 0,
|
||||
message: '创建成功',
|
||||
data: content
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建拼音内容失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '创建拼音内容失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新拼音内容(管理员)
|
||||
* PUT /api/pinyin/contents/:id
|
||||
*/
|
||||
exports.updateContent = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
// 不允许修改symbol
|
||||
delete updateData.symbol;
|
||||
|
||||
const content = await PinyinContent.findByIdAndUpdate(
|
||||
id,
|
||||
{ $set: updateData },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '拼音内容不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data: content
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新拼音内容失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '更新拼音内容失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除拼音内容(管理员)
|
||||
* DELETE /api/pinyin/contents/:id
|
||||
*/
|
||||
exports.deleteContent = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const content = await PinyinContent.findByIdAndDelete(id);
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '拼音内容不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除拼音内容失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '删除拼音内容失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传拼音音频
|
||||
* POST /api/pinyin/contents/:symbol/audio
|
||||
*/
|
||||
exports.uploadAudio = async (req, res) => {
|
||||
try {
|
||||
const { symbol } = req.params;
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '请上传音频文件'
|
||||
});
|
||||
}
|
||||
|
||||
// 构建文件URL(假设使用静态文件服务)
|
||||
const audioUrl = `/uploads/audio/${req.file.filename}`;
|
||||
|
||||
// 更新拼音内容的音频URL
|
||||
const content = await PinyinContent.findOneAndUpdate(
|
||||
{ symbol: symbol.toLowerCase() },
|
||||
{ $set: { audioUrl } },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '拼音内容不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '音频上传成功',
|
||||
data: { audioUrl }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('上传音频失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '上传音频失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传口型图
|
||||
* POST /api/pinyin/contents/:symbol/mouth-image
|
||||
*/
|
||||
exports.uploadMouthImage = async (req, res) => {
|
||||
try {
|
||||
const { symbol } = req.params;
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '请上传图片文件'
|
||||
});
|
||||
}
|
||||
|
||||
// 构建文件URL
|
||||
const mouthImage = `/uploads/images/${req.file.filename}`;
|
||||
|
||||
// 更新拼音内容的口型图URL
|
||||
const content = await PinyinContent.findOneAndUpdate(
|
||||
{ symbol: symbol.toLowerCase() },
|
||||
{ $set: { mouthImage } },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '拼音内容不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '口型图上传成功',
|
||||
data: { mouthImage }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('上传口型图失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '上传口型图失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新音频URL(从外部TTS服务)
|
||||
* POST /api/pinyin/contents/batch-update-audio
|
||||
*/
|
||||
exports.batchUpdateAudio = async (req, res) => {
|
||||
try {
|
||||
const { audioBaseUrl } = req.body;
|
||||
|
||||
if (!audioBaseUrl) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '请提供音频基础URL'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有拼音内容
|
||||
const contents = await PinyinContent.find({});
|
||||
|
||||
// 批量更新音频URL
|
||||
const updatePromises = contents.map(content => {
|
||||
const audioUrl = `${audioBaseUrl}/${content.symbol}.mp3`;
|
||||
return PinyinContent.findByIdAndUpdate(content._id, { $set: { audioUrl } });
|
||||
});
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: `成功更新 ${contents.length} 个拼音的音频URL`,
|
||||
data: { updatedCount: contents.length }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量更新音频失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '批量更新音频失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,320 +0,0 @@
|
||||
const { PinyinGameRecord, PinyinProgress, PinyinContent } = require('../../models/pinyin');
|
||||
|
||||
/**
|
||||
* 游戏配置
|
||||
*/
|
||||
const GAME_CONFIG = {
|
||||
match: {
|
||||
name: '拼音配对',
|
||||
description: '找出相同的拼音卡片',
|
||||
difficulty: {
|
||||
easy: { pairs: 4, time: 60 },
|
||||
normal: { pairs: 6, time: 90 },
|
||||
hard: { pairs: 8, time: 120 }
|
||||
}
|
||||
},
|
||||
tone: {
|
||||
name: '声调过山车',
|
||||
description: '根据声调高低控制轨道',
|
||||
difficulty: {
|
||||
easy: { questions: 5, time: 60 },
|
||||
normal: { questions: 8, time: 90 },
|
||||
hard: { questions: 10, time: 120 }
|
||||
}
|
||||
},
|
||||
find: {
|
||||
name: '找拼音',
|
||||
description: '在场景中找到指定拼音',
|
||||
difficulty: {
|
||||
easy: { targets: 3, distractors: 6, time: 60 },
|
||||
normal: { targets: 5, distractors: 10, time: 90 },
|
||||
hard: { targets: 7, distractors: 14, time: 120 }
|
||||
}
|
||||
},
|
||||
puzzle: {
|
||||
name: '拼音拼图',
|
||||
description: '拖拽拼音组成完整音节',
|
||||
difficulty: {
|
||||
easy: { pieces: 2, time: 60 },
|
||||
normal: { pieces: 3, time: 90 },
|
||||
hard: { pieces: 4, time: 120 }
|
||||
}
|
||||
},
|
||||
mimic: {
|
||||
name: '语音模仿',
|
||||
description: '模仿发音,获得反馈',
|
||||
difficulty: {
|
||||
easy: { targets: 3, time: 60 },
|
||||
normal: { targets: 5, time: 90 },
|
||||
hard: { targets: 7, time: 120 }
|
||||
}
|
||||
},
|
||||
runner: {
|
||||
name: '拼音跑酷',
|
||||
description: '躲避障碍,收集正确拼音',
|
||||
difficulty: {
|
||||
easy: { distance: 100, obstacles: 5, time: 60 },
|
||||
normal: { distance: 200, obstacles: 10, time: 90 },
|
||||
hard: { distance: 300, obstacles: 15, time: 120 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取游戏配置
|
||||
* GET /api/pinyin/games/config
|
||||
*/
|
||||
exports.getGameConfig = async (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
games: GAME_CONFIG
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取游戏配置失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取游戏配置失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定游戏配置
|
||||
* GET /api/pinyin/games/config/:type
|
||||
*/
|
||||
exports.getGameConfigByType = async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
|
||||
if (!GAME_CONFIG[type]) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '游戏类型不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
type,
|
||||
config: GAME_CONFIG[type]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取游戏配置失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取游戏配置失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 记录游戏结果
|
||||
* POST /api/pinyin/games/record
|
||||
*/
|
||||
exports.recordGame = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const {
|
||||
gameType,
|
||||
difficulty = 'normal',
|
||||
score,
|
||||
duration,
|
||||
correctCount,
|
||||
wrongCount,
|
||||
details = {}
|
||||
} = req.body;
|
||||
|
||||
// 验证参数
|
||||
if (!gameType || !GAME_CONFIG[gameType]) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '无效的游戏类型'
|
||||
});
|
||||
}
|
||||
|
||||
if (score === undefined || duration === undefined) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '缺少必要参数:score 或 duration'
|
||||
});
|
||||
}
|
||||
|
||||
// 计算准确率
|
||||
const total = (correctCount || 0) + (wrongCount || 0);
|
||||
const accuracy = total > 0 ? Math.round((correctCount / total) * 100) : 0;
|
||||
|
||||
// 创建游戏记录
|
||||
const record = new PinyinGameRecord({
|
||||
userId,
|
||||
gameType,
|
||||
difficulty,
|
||||
score,
|
||||
duration,
|
||||
correctCount: correctCount || 0,
|
||||
wrongCount: wrongCount || 0,
|
||||
accuracy,
|
||||
details,
|
||||
playedAt: new Date()
|
||||
});
|
||||
|
||||
await record.save();
|
||||
|
||||
// 更新用户进度中的游戏统计
|
||||
let progress = await PinyinProgress.findOne({ userId });
|
||||
if (!progress) {
|
||||
progress = new PinyinProgress({ userId });
|
||||
}
|
||||
progress.updateDailyStats('game', duration);
|
||||
await progress.save();
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
record: {
|
||||
id: record._id,
|
||||
gameType,
|
||||
score,
|
||||
accuracy,
|
||||
playedAt: record.playedAt
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录游戏结果失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '记录游戏结果失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户游戏记录
|
||||
* GET /api/pinyin/games/records
|
||||
*/
|
||||
exports.getGameRecords = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { gameType, page = 1, limit = 20 } = req.query;
|
||||
|
||||
const query = { userId };
|
||||
if (gameType) query.gameType = gameType;
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const records = await PinyinGameRecord.find(query)
|
||||
.sort({ playedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit))
|
||||
.select('-__v');
|
||||
|
||||
const total = await PinyinGameRecord.countDocuments(query);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
list: records,
|
||||
total,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取游戏记录失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取游戏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户游戏统计
|
||||
* GET /api/pinyin/games/stats
|
||||
*/
|
||||
exports.getGameStats = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
|
||||
const stats = await PinyinGameRecord.getUserStats(userId);
|
||||
|
||||
// 格式化统计数据
|
||||
const formattedStats = {};
|
||||
Object.keys(GAME_CONFIG).forEach(type => {
|
||||
const stat = stats.find(s => s._id === type);
|
||||
formattedStats[type] = {
|
||||
name: GAME_CONFIG[type].name,
|
||||
totalGames: stat ? stat.totalGames : 0,
|
||||
totalScore: stat ? stat.totalScore : 0,
|
||||
avgScore: stat ? Math.round(stat.avgScore) : 0,
|
||||
maxScore: stat ? stat.maxScore : 0,
|
||||
avgAccuracy: stat ? Math.round(stat.avgAccuracy) : 0
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
stats: formattedStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取游戏统计失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取游戏统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取游戏排行榜
|
||||
* GET /api/pinyin/games/leaderboard/:type
|
||||
*/
|
||||
exports.getLeaderboard = async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
if (!GAME_CONFIG[type]) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '游戏类型不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const leaderboard = await PinyinGameRecord.getLeaderboard(type, parseInt(limit));
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
gameType: type,
|
||||
gameName: GAME_CONFIG[type].name,
|
||||
leaderboard
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取游戏排行榜失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取游戏排行榜失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,351 +0,0 @@
|
||||
const { PinyinProgress, PinyinContent, PinyinAchievement } = require('../../models/pinyin');
|
||||
|
||||
/**
|
||||
* 获取用户探索进度
|
||||
* GET /api/pinyin/progress
|
||||
*/
|
||||
exports.getProgress = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
|
||||
let progress = await PinyinProgress.findOne({ userId });
|
||||
|
||||
// 如果没有进度记录,创建新记录
|
||||
if (!progress) {
|
||||
progress = new PinyinProgress({ userId });
|
||||
await progress.save();
|
||||
}
|
||||
|
||||
// 获取所有拼音内容数量
|
||||
const totalSymbols = await PinyinContent.countDocuments({ status: 'active' });
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
progress: {
|
||||
totalExplored: progress.totalExplored,
|
||||
totalStones: progress.totalStones,
|
||||
totalSymbols: totalSymbols,
|
||||
exploredSymbols: progress.exploredSymbols,
|
||||
currentSymbol: progress.currentSymbol,
|
||||
achievements: progress.achievements,
|
||||
streakDays: progress.streakDays
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取探索进度失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取探索进度失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 记录探索行为
|
||||
* POST /api/pinyin/progress/explore
|
||||
*/
|
||||
exports.recordExplore = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { symbol, area, duration = 0 } = req.body;
|
||||
|
||||
if (!symbol || !area) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '缺少必要参数:symbol 或 area'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证拼音是否存在
|
||||
const content = await PinyinContent.findOne({
|
||||
symbol: symbol.toLowerCase(),
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '拼音内容不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取或创建进度记录
|
||||
let progress = await PinyinProgress.findOne({ userId });
|
||||
if (!progress) {
|
||||
progress = new PinyinProgress({ userId });
|
||||
}
|
||||
|
||||
// 检查是否首次探索该拼音
|
||||
const isFirstExplore = !progress.hasExplored(symbol);
|
||||
|
||||
// 添加探索记录
|
||||
progress.addExplore(symbol, area);
|
||||
|
||||
// 更新每日统计
|
||||
progress.updateDailyStats('explore', duration);
|
||||
|
||||
// 检查是否完成所有区域
|
||||
const isCompleted = progress.isSymbolCompleted(symbol);
|
||||
|
||||
// 检查成就
|
||||
const newAchievements = await checkAchievements(progress);
|
||||
|
||||
await progress.save();
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
isFirstExplore,
|
||||
isCompleted,
|
||||
newAchievements,
|
||||
progress: {
|
||||
totalExplored: progress.totalExplored,
|
||||
totalStones: progress.totalStones,
|
||||
currentSymbol: progress.currentSymbol
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录探索行为失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '记录探索行为失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 收集能量石
|
||||
* POST /api/pinyin/progress/collect
|
||||
*/
|
||||
exports.collectStone = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { symbol } = req.body;
|
||||
|
||||
if (!symbol) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '缺少必要参数:symbol'
|
||||
});
|
||||
}
|
||||
|
||||
let progress = await PinyinProgress.findOne({ userId });
|
||||
if (!progress) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '探索进度不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否已完成该拼音的所有区域
|
||||
if (!progress.isSymbolCompleted(symbol)) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '请先完成该拼音的所有探索区域'
|
||||
});
|
||||
}
|
||||
|
||||
// 收集能量石
|
||||
const isCollected = progress.collectStone(symbol);
|
||||
|
||||
if (!isCollected) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '该能量石已收集'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查成就
|
||||
const newAchievements = await checkAchievements(progress);
|
||||
|
||||
await progress.save();
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
isCollected: true,
|
||||
totalStones: progress.totalStones,
|
||||
newAchievements
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('收集能量石失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '收集能量石失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户探索统计
|
||||
* GET /api/pinyin/progress/stats
|
||||
*/
|
||||
exports.getStats = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
|
||||
const progress = await PinyinProgress.findOne({ userId });
|
||||
|
||||
if (!progress) {
|
||||
return res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
totalExplored: 0,
|
||||
totalStones: 0,
|
||||
streakDays: 0,
|
||||
achievements: [],
|
||||
dailyStats: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取最近7天的统计
|
||||
const last7Days = progress.dailyStats
|
||||
.sort((a, b) => b.date - a.date)
|
||||
.slice(0, 7);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
totalExplored: progress.totalExplored,
|
||||
totalStones: progress.totalStones,
|
||||
streakDays: progress.streakDays,
|
||||
achievements: progress.achievements,
|
||||
dailyStats: last7Days
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取探索统计失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取探索统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取探索排行榜
|
||||
* GET /api/pinyin/progress/leaderboard
|
||||
*/
|
||||
exports.getLeaderboard = async (req, res) => {
|
||||
try {
|
||||
const { type = 'stones', limit = 10 } = req.query;
|
||||
|
||||
let sortField = 'totalStones';
|
||||
if (type === 'explored') sortField = 'totalExplored';
|
||||
if (type === 'achievements') sortField = 'achievements';
|
||||
|
||||
const leaderboard = await PinyinProgress.find()
|
||||
.sort({ [sortField]: -1 })
|
||||
.limit(parseInt(limit))
|
||||
.populate('userId', 'nickname avatar')
|
||||
.select('totalStones totalExplored achievements streakDays');
|
||||
|
||||
const formattedLeaderboard = leaderboard.map((item, index) => ({
|
||||
rank: index + 1,
|
||||
userId: item.userId?._id,
|
||||
nickname: item.userId?.nickname || '匿名用户',
|
||||
avatar: item.userId?.avatar || '',
|
||||
totalStones: item.totalStones,
|
||||
totalExplored: item.totalExplored,
|
||||
achievements: item.achievements.length,
|
||||
streakDays: item.streakDays
|
||||
}));
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
type,
|
||||
leaderboard: formattedLeaderboard
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取排行榜失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取排行榜失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查成就达成
|
||||
* @param {Object} progress - 用户进度对象
|
||||
* @returns {Array} - 新获得的成就列表
|
||||
*/
|
||||
async function checkAchievements(progress) {
|
||||
const newAchievements = [];
|
||||
|
||||
// 获取所有启用的成就
|
||||
const achievements = await PinyinAchievement.find({ isActive: true });
|
||||
|
||||
for (const achievement of achievements) {
|
||||
// 检查是否已获得
|
||||
if (progress.achievements.some(a => a.code === achievement.code)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查条件
|
||||
const isAchieved = checkAchievementCondition(progress, achievement.condition);
|
||||
|
||||
if (isAchieved) {
|
||||
progress.addAchievement(achievement.code);
|
||||
newAchievements.push({
|
||||
code: achievement.code,
|
||||
name: achievement.name,
|
||||
icon: achievement.icon
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newAchievements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个成就条件
|
||||
* @param {Object} progress - 用户进度
|
||||
* @param {Object} condition - 成就条件
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function checkAchievementCondition(progress, condition) {
|
||||
switch (condition.type) {
|
||||
case 'explore_count':
|
||||
return progress.totalExplored >= condition.value;
|
||||
|
||||
case 'collect_count':
|
||||
return progress.totalStones >= condition.value;
|
||||
|
||||
case 'streak_days':
|
||||
return progress.streakDays >= condition.value;
|
||||
|
||||
case 'complete_symbol':
|
||||
return progress.isSymbolCompleted(condition.symbol);
|
||||
|
||||
case 'complete_type':
|
||||
// 需要查询该类型的所有拼音是否都已完成
|
||||
// 这里简化处理,实际应该查询数据库
|
||||
return false;
|
||||
|
||||
case 'explore_all':
|
||||
// 需要查询所有拼音数量
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const BGMschema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
dimension: {
|
||||
type: Number,
|
||||
required: true,
|
||||
enum: [1, 2, 3, 4, 5],
|
||||
index: true
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
loop: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
volume: {
|
||||
type: Number,
|
||||
default: 0.5,
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
index: true
|
||||
},
|
||||
sortOrder: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
})
|
||||
|
||||
// 更新时自动修改 updatedAt
|
||||
BGMschema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now()
|
||||
next()
|
||||
})
|
||||
|
||||
// 复合索引:维度 + 激活状态 + 排序
|
||||
BGMschema.index({ dimension: 1, isActive: 1, sortOrder: 1 })
|
||||
|
||||
module.exports = mongoose.model('BGM', BGMschema)
|
||||
@@ -1,106 +0,0 @@
|
||||
const mongoose = require('mongoose')
|
||||
const { Schema } = mongoose
|
||||
|
||||
/**
|
||||
* 画廊作品模型
|
||||
*/
|
||||
const GallerySchema = new Schema({
|
||||
// 用户信息
|
||||
openid: {
|
||||
type: String,
|
||||
required: false,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true
|
||||
},
|
||||
authorName: {
|
||||
type: String,
|
||||
default: '星辰旅者'
|
||||
},
|
||||
authorAvatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 作品信息
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 100
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
|
||||
// 图片数据(Base64 或 URL)
|
||||
imageData: String,
|
||||
imageUrl: String,
|
||||
|
||||
// 作品属性
|
||||
tags: [{
|
||||
type: String,
|
||||
maxlength: 20
|
||||
}],
|
||||
dim: {
|
||||
type: Number,
|
||||
enum: [1, 2, 3, 4, 5],
|
||||
default: 1
|
||||
},
|
||||
|
||||
// 统计
|
||||
likeCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 审核状态
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'approved', 'rejected'],
|
||||
default: 'pending',
|
||||
index: true
|
||||
},
|
||||
reviewComment: String,
|
||||
reviewedAt: Date,
|
||||
reviewerId: String,
|
||||
|
||||
// 时间戳
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
index: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
})
|
||||
|
||||
// 索引
|
||||
GallerySchema.index({ openid: 1, createdAt: -1 })
|
||||
GallerySchema.index({ status: 1, createdAt: -1 })
|
||||
GallerySchema.index({ dim: 1, createdAt: -1 })
|
||||
GallerySchema.index({ likeCount: -1 })
|
||||
|
||||
// 静态方法:获取今日作品数
|
||||
GallerySchema.statics.getTodayCount = async function() {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
return this.countDocuments({
|
||||
createdAt: { $gte: today },
|
||||
status: 'approved'
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = mongoose.model('Gallery', GallerySchema)
|
||||
@@ -1,79 +0,0 @@
|
||||
const mongoose = require('mongoose')
|
||||
const { Schema } = mongoose
|
||||
|
||||
/**
|
||||
* 关卡配置模型
|
||||
*/
|
||||
const LevelSchema = new Schema({
|
||||
// 关卡基本信息
|
||||
dimension: {
|
||||
type: Number,
|
||||
required: true,
|
||||
enum: [1, 2, 3, 4, 5],
|
||||
index: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
|
||||
// 关卡难度配置
|
||||
difficulty: {
|
||||
type: String,
|
||||
enum: ['easy', 'normal', 'hard', 'expert'],
|
||||
default: 'normal'
|
||||
},
|
||||
|
||||
// 通关条件
|
||||
requirements: {
|
||||
score: { type: Number, default: 0 },
|
||||
timeLimit: { type: Number, default: 0 }, // 秒,0表示无限制
|
||||
collectibles: { type: Number, default: 0 } // 需要收集的物品数量
|
||||
},
|
||||
|
||||
// 奖励配置
|
||||
rewards: {
|
||||
exp: { type: Number, default: 100 },
|
||||
coins: { type: Number, default: 50 },
|
||||
unlockNext: { type: Boolean, default: true }
|
||||
},
|
||||
|
||||
// 关卡描述
|
||||
title: String,
|
||||
description: String,
|
||||
tips: [String], // 游戏提示
|
||||
|
||||
// 状态
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isLocked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 排序
|
||||
sortOrder: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 时间戳
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
})
|
||||
|
||||
// 复合索引:维度 + 关卡等级
|
||||
LevelSchema.index({ dimension: 1, level: 1 }, { unique: true })
|
||||
LevelSchema.index({ dimension: 1, isActive: 1, sortOrder: 1 })
|
||||
|
||||
module.exports = mongoose.model('Level', LevelSchema)
|
||||
@@ -1,25 +0,0 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const ShareRecordSchema = new mongoose.Schema({
|
||||
openid: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true
|
||||
},
|
||||
shareType: {
|
||||
type: String,
|
||||
enum: ['app', 'dim1', 'dim2', 'dim3', 'dim4', 'dim5'],
|
||||
default: 'app'
|
||||
},
|
||||
score: { type: Number, default: 0 },
|
||||
sharedAt: { type: Date, default: Date.now }
|
||||
})
|
||||
|
||||
ShareRecordSchema.index({ sharedAt: -1 })
|
||||
|
||||
module.exports = mongoose.model('ShareRecord', ShareRecordSchema)
|
||||
@@ -1,38 +1,25 @@
|
||||
const User = require('./User')
|
||||
const Order = require('./Order')
|
||||
const Gallery = require('./Gallery')
|
||||
const Knowledge = require('./Knowledge')
|
||||
const ShopItem = require('./ShopItem')
|
||||
const Admin = require('./Admin')
|
||||
const AdminLog = require('./AdminLog')
|
||||
const ShareRecord = require('./ShareRecord')
|
||||
const BGM = require('./BGM')
|
||||
const Level = require('./Level')
|
||||
const AIChatQuota = require('./AIChatQuota')
|
||||
const AIModel = require('./AIModel')
|
||||
const LoginLog = require('./LoginLog')
|
||||
const Feedback = require('./Feedback')
|
||||
const Trend = require('./Trend')
|
||||
|
||||
// 拼音探索模块模型
|
||||
const pinyinModels = require('./pinyin')
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Order,
|
||||
Gallery,
|
||||
Knowledge,
|
||||
ShopItem,
|
||||
Admin,
|
||||
AdminLog,
|
||||
ShareRecord,
|
||||
BGM,
|
||||
Level,
|
||||
AIChatQuota,
|
||||
AIModel,
|
||||
LoginLog,
|
||||
Feedback,
|
||||
Trend,
|
||||
// 拼音探索模型
|
||||
...pinyinModels
|
||||
Trend
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 拼音探索成就模型
|
||||
* 定义可获得的成就及其条件
|
||||
*/
|
||||
const pinyinAchievementSchema = new mongoose.Schema({
|
||||
// 成就代码
|
||||
code: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 成就名称
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 成就描述
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
// 成就图标
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 成就类型
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['explore', 'game', 'collection', 'streak', 'special'],
|
||||
default: 'explore'
|
||||
},
|
||||
|
||||
// 达成条件
|
||||
condition: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: [
|
||||
'explore_count', // 探索数量
|
||||
'collect_count', // 收集数量
|
||||
'game_count', // 游戏次数
|
||||
'game_score', // 游戏分数
|
||||
'streak_days', // 连续天数
|
||||
'complete_symbol', // 完成指定拼音
|
||||
'complete_type', // 完成某类型所有拼音
|
||||
'explore_all' // 探索所有内容
|
||||
]
|
||||
},
|
||||
value: { type: Number, required: true }, // 条件值
|
||||
symbol: { type: String }, // 特定拼音(可选)
|
||||
symbolType: { type: String } // 特定类型(可选)
|
||||
},
|
||||
|
||||
// 奖励
|
||||
reward: {
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['stone', 'badge', 'theme', 'none'],
|
||||
default: 'none'
|
||||
},
|
||||
value: { type: Number, default: 0 },
|
||||
item: { type: String, default: '' }
|
||||
},
|
||||
|
||||
// 排序
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 是否启用
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinAchievementSchema.index({ type: 1, order: 1 });
|
||||
pinyinAchievementSchema.index({ isActive: 1 });
|
||||
pinyinAchievementSchema.index({ code: 1 });
|
||||
|
||||
// 更新中间件
|
||||
pinyinAchievementSchema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now();
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('PinyinAchievement', pinyinAchievementSchema);
|
||||
@@ -1,120 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 拼音内容模型
|
||||
* 存储拼音字母的基础信息、音频、口型图、关联词语等
|
||||
*/
|
||||
const pinyinContentSchema = new mongoose.Schema({
|
||||
// 拼音符号,如 "b"
|
||||
symbol: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 拼音类型:initial(声母)/final(韵母)/overall(整体认读)
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['initial', 'final', 'overall']
|
||||
},
|
||||
|
||||
// 拼音名称,如 "玻"
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 发音音频URL
|
||||
audioUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 口型示意图URL
|
||||
mouthImage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 发音方法描述
|
||||
pronunciation: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 显示顺序
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 是否免费
|
||||
isFree: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 状态:active(启用)/inactive(禁用)
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['active', 'inactive'],
|
||||
default: 'active'
|
||||
},
|
||||
|
||||
// 相关词语
|
||||
words: [{
|
||||
word: { type: String, required: true }, // 汉字
|
||||
pinyin: { type: String, required: true }, // 拼音
|
||||
image: { type: String, default: '' }, // 图片URL
|
||||
audioUrl: { type: String, default: '' }, // 音频URL
|
||||
meaning: { type: String, default: '' } // 释义
|
||||
}],
|
||||
|
||||
// 关联游戏配置
|
||||
games: [{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['match', 'tone', 'find', 'puzzle', 'mimic', 'runner']
|
||||
},
|
||||
config: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
isEnabled: { type: Boolean, default: true }
|
||||
}],
|
||||
|
||||
// 探索区域配置
|
||||
exploreAreas: {
|
||||
audio: { type: Boolean, default: true }, // 声音发现
|
||||
mouth: { type: Boolean, default: true }, // 口型观察
|
||||
speak: { type: Boolean, default: true }, // 语音互动
|
||||
write: { type: Boolean, default: true }, // 书写体验
|
||||
game: { type: Boolean, default: true }, // 趣味互动
|
||||
words: { type: Boolean, default: true } // 词语发现
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinContentSchema.index({ type: 1, order: 1 });
|
||||
pinyinContentSchema.index({ status: 1 });
|
||||
pinyinContentSchema.index({ isFree: 1 });
|
||||
|
||||
// 更新中间件
|
||||
pinyinContentSchema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now();
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('PinyinContent', pinyinContentSchema);
|
||||
@@ -1,142 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 拼音游戏记录模型
|
||||
* 记录用户的游戏行为和成绩
|
||||
*/
|
||||
const pinyinGameRecordSchema = new mongoose.Schema({
|
||||
// 用户ID
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
|
||||
// 游戏类型
|
||||
gameType: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['match', 'tone', 'find', 'puzzle', 'mimic', 'runner']
|
||||
},
|
||||
|
||||
// 游戏难度
|
||||
difficulty: {
|
||||
type: String,
|
||||
enum: ['easy', 'normal', 'hard'],
|
||||
default: 'normal'
|
||||
},
|
||||
|
||||
// 得分
|
||||
score: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 游戏时长(秒)
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 正确数
|
||||
correctCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 错误数
|
||||
wrongCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 准确率
|
||||
accuracy: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 游戏详情
|
||||
details: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: {}
|
||||
},
|
||||
|
||||
// 是否完成
|
||||
isCompleted: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 游戏时间
|
||||
playedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinGameRecordSchema.index({ userId: 1, gameType: 1 });
|
||||
pinyinGameRecordSchema.index({ userId: 1, playedAt: -1 });
|
||||
pinyinGameRecordSchema.index({ gameType: 1, score: -1 });
|
||||
|
||||
// 静态方法:获取用户游戏统计
|
||||
pinyinGameRecordSchema.statics.getUserStats = async function(userId) {
|
||||
const stats = await this.aggregate([
|
||||
{ $match: { userId: mongoose.Types.ObjectId(userId) } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$gameType',
|
||||
totalGames: { $sum: 1 },
|
||||
totalScore: { $sum: '$score' },
|
||||
avgScore: { $avg: '$score' },
|
||||
maxScore: { $max: '$score' },
|
||||
totalDuration: { $sum: '$duration' },
|
||||
avgAccuracy: { $avg: '$accuracy' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
return stats;
|
||||
};
|
||||
|
||||
// 静态方法:获取排行榜
|
||||
pinyinGameRecordSchema.statics.getLeaderboard = async function(gameType, limit = 10) {
|
||||
const leaderboard = await this.aggregate([
|
||||
{ $match: { gameType: gameType } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId',
|
||||
maxScore: { $max: '$score' },
|
||||
totalGames: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{ $sort: { maxScore: -1 } },
|
||||
{ $limit: limit },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'users',
|
||||
localField: '_id',
|
||||
foreignField: '_id',
|
||||
as: 'user'
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
userId: '$_id',
|
||||
maxScore: 1,
|
||||
totalGames: 1,
|
||||
nickname: { $arrayElemAt: ['$user.nickname', 0] },
|
||||
avatar: { $arrayElemAt: ['$user.avatar', 0] }
|
||||
}
|
||||
}
|
||||
]);
|
||||
return leaderboard;
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('PinyinGameRecord', pinyinGameRecordSchema);
|
||||
@@ -1,189 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 用户拼音探索进度模型
|
||||
* 记录用户的探索进度、收集的能量石、成就等
|
||||
*/
|
||||
const pinyinProgressSchema = new mongoose.Schema({
|
||||
// 用户ID
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
|
||||
// 已探索的拼音
|
||||
exploredSymbols: [{
|
||||
symbol: { type: String, required: true }, // 拼音符号
|
||||
exploredAt: { type: Date, default: Date.now }, // 首次探索时间
|
||||
completedAreas: [{ type: String }], // 完成的探索区域
|
||||
isCollected: { type: Boolean, default: false }, // 是否收集能量石
|
||||
collectedAt: { type: Date }, // 收集时间
|
||||
lastExploredAt: { type: Date } // 最后探索时间
|
||||
}],
|
||||
|
||||
// 当前正在探索的拼音
|
||||
currentSymbol: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 总探索数
|
||||
totalExplored: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 收集的能量石数量
|
||||
totalStones: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 获得的成就
|
||||
achievements: [{
|
||||
code: { type: String, required: true }, // 成就代码
|
||||
obtainedAt: { type: Date, default: Date.now } // 获得时间
|
||||
}],
|
||||
|
||||
// 每日统计
|
||||
dailyStats: [{
|
||||
date: { type: Date, required: true }, // 日期
|
||||
exploreCount: { type: Number, default: 0 }, // 探索次数
|
||||
gameCount: { type: Number, default: 0 }, // 游戏次数
|
||||
duration: { type: Number, default: 0 } // 时长(分钟)
|
||||
}],
|
||||
|
||||
// 连续探索天数
|
||||
streakDays: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 最后探索日期
|
||||
lastExploreDate: {
|
||||
type: Date
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinProgressSchema.index({ userId: 1 });
|
||||
pinyinProgressSchema.index({ totalExplored: -1 });
|
||||
|
||||
// 更新中间件
|
||||
pinyinProgressSchema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now();
|
||||
next();
|
||||
});
|
||||
|
||||
// 方法:获取指定拼音的探索记录
|
||||
pinyinProgressSchema.methods.getSymbolProgress = function(symbol) {
|
||||
return this.exploredSymbols.find(s => s.symbol === symbol);
|
||||
};
|
||||
|
||||
// 方法:检查是否已探索指定拼音
|
||||
pinyinProgressSchema.methods.hasExplored = function(symbol) {
|
||||
return this.exploredSymbols.some(s => s.symbol === symbol);
|
||||
};
|
||||
|
||||
// 方法:检查是否完成指定拼音的所有区域
|
||||
pinyinProgressSchema.methods.isSymbolCompleted = function(symbol) {
|
||||
const record = this.getSymbolProgress(symbol);
|
||||
if (!record) return false;
|
||||
return record.completedAreas.length >= 6; // 6个探索区域
|
||||
};
|
||||
|
||||
// 方法:添加探索记录
|
||||
pinyinProgressSchema.methods.addExplore = function(symbol, area) {
|
||||
let record = this.getSymbolProgress(symbol);
|
||||
|
||||
if (!record) {
|
||||
record = {
|
||||
symbol: symbol,
|
||||
exploredAt: new Date(),
|
||||
completedAreas: [],
|
||||
isCollected: false
|
||||
};
|
||||
this.exploredSymbols.push(record);
|
||||
this.totalExplored += 1;
|
||||
}
|
||||
|
||||
if (!record.completedAreas.includes(area)) {
|
||||
record.completedAreas.push(area);
|
||||
}
|
||||
|
||||
record.lastExploredAt = new Date();
|
||||
this.currentSymbol = symbol;
|
||||
this.lastExploreDate = new Date();
|
||||
};
|
||||
|
||||
// 方法:收集能量石
|
||||
pinyinProgressSchema.methods.collectStone = function(symbol) {
|
||||
const record = this.getSymbolProgress(symbol);
|
||||
if (record && !record.isCollected) {
|
||||
record.isCollected = true;
|
||||
record.collectedAt = new Date();
|
||||
this.totalStones += 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 方法:添加成就
|
||||
pinyinProgressSchema.methods.addAchievement = function(code) {
|
||||
if (!this.achievements.some(a => a.code === code)) {
|
||||
this.achievements.push({
|
||||
code: code,
|
||||
obtainedAt: new Date()
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 方法:更新每日统计
|
||||
pinyinProgressSchema.methods.updateDailyStats = function(type, duration = 0) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
let dailyStat = this.dailyStats.find(d => {
|
||||
const statDate = new Date(d.date);
|
||||
statDate.setHours(0, 0, 0, 0);
|
||||
return statDate.getTime() === today.getTime();
|
||||
});
|
||||
|
||||
if (!dailyStat) {
|
||||
dailyStat = {
|
||||
date: today,
|
||||
exploreCount: 0,
|
||||
gameCount: 0,
|
||||
duration: 0
|
||||
};
|
||||
this.dailyStats.push(dailyStat);
|
||||
}
|
||||
|
||||
if (type === 'explore') {
|
||||
dailyStat.exploreCount += 1;
|
||||
} else if (type === 'game') {
|
||||
dailyStat.gameCount += 1;
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
dailyStat.duration += duration;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('PinyinProgress', pinyinProgressSchema);
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* 拼音探索模块模型导出
|
||||
*/
|
||||
module.exports = {
|
||||
PinyinContent: require('./PinyinContent'),
|
||||
PinyinProgress: require('./PinyinProgress'),
|
||||
PinyinAchievement: require('./PinyinAchievement'),
|
||||
PinyinGameRecord: require('./PinyinGameRecord')
|
||||
};
|
||||
@@ -1,201 +0,0 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const { BGM } = require('../models')
|
||||
const ApiResponse = require('../utils/response')
|
||||
const { authMiddleware } = require('../middleware/auth')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
/**
|
||||
* 获取指定维度的随机BGM(公开接口,移动端调用)
|
||||
* GET /api/bgm/random/:dimension
|
||||
*/
|
||||
router.get('/random/:dimension', async (req, res) => {
|
||||
try {
|
||||
const dimension = parseInt(req.params.dimension)
|
||||
|
||||
if (![1, 2, 3, 4, 5].includes(dimension)) {
|
||||
return ApiResponse.badRequest(res, '无效的维度参数')
|
||||
}
|
||||
|
||||
// 查询该维度下所有激活的BGM
|
||||
const bgms = await BGM.find({
|
||||
dimension,
|
||||
isActive: true
|
||||
}).sort({ sortOrder: 1 })
|
||||
|
||||
if (!bgms || bgms.length === 0) {
|
||||
return ApiResponse.success(res, null)
|
||||
}
|
||||
|
||||
// 随机选择一个
|
||||
const randomIndex = Math.floor(Math.random() * bgms.length)
|
||||
const selectedBGM = bgms[randomIndex]
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
id: selectedBGM._id,
|
||||
name: selectedBGM.name,
|
||||
url: selectedBGM.url,
|
||||
duration: selectedBGM.duration,
|
||||
loop: selectedBGM.loop,
|
||||
volume: selectedBGM.volume,
|
||||
dimension: selectedBGM.dimension
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('获取随机BGM失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取所有BGM列表(管理员)
|
||||
* GET /api/admin/bgm/list
|
||||
*/
|
||||
router.get('/list', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { dimension, page = 1, limit = 20 } = req.query
|
||||
|
||||
const query = {}
|
||||
if (dimension && [1, 2, 3, 4, 5].includes(parseInt(dimension))) {
|
||||
query.dimension = parseInt(dimension)
|
||||
}
|
||||
|
||||
const pageNum = parseInt(page)
|
||||
const limitNum = parseInt(limit)
|
||||
const skip = (pageNum - 1) * limitNum
|
||||
|
||||
const [bgms, total] = await Promise.all([
|
||||
BGM.find(query)
|
||||
.sort({ dimension: 1, sortOrder: 1, createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limitNum),
|
||||
BGM.countDocuments(query)
|
||||
])
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
list: bgms,
|
||||
total,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
totalPages: Math.ceil(total / limitNum)
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('获取BGM列表失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 创建BGM(管理员)
|
||||
* POST /api/admin/bgm/create
|
||||
*/
|
||||
router.post('/create', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { name, dimension, url, duration, loop, volume, sortOrder, description } = req.body
|
||||
|
||||
if (!name || !dimension || !url) {
|
||||
return ApiResponse.badRequest(res, '名称、维度和URL为必填项')
|
||||
}
|
||||
|
||||
if (![1, 2, 3, 4, 5].includes(parseInt(dimension))) {
|
||||
return ApiResponse.badRequest(res, '无效的维度参数')
|
||||
}
|
||||
|
||||
const bgm = new BGM({
|
||||
name,
|
||||
dimension: parseInt(dimension),
|
||||
url,
|
||||
duration: duration || 0,
|
||||
loop: loop !== undefined ? loop : true,
|
||||
volume: volume !== undefined ? volume : 0.5,
|
||||
sortOrder: sortOrder || 0,
|
||||
description: description || ''
|
||||
})
|
||||
|
||||
await bgm.save()
|
||||
|
||||
logger.info(`管理员创建了BGM: ${name} (维度${dimension})`)
|
||||
return ApiResponse.success(res, { message: 'BGM创建成功', data: bgm })
|
||||
} catch (error) {
|
||||
logger.error('创建BGM失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新BGM(管理员)
|
||||
* PUT /api/admin/bgm/update/:id
|
||||
*/
|
||||
router.put('/update/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const updates = req.body
|
||||
|
||||
const bgm = await BGM.findByIdAndUpdate(
|
||||
id,
|
||||
{ $set: updates },
|
||||
{ new: true, runValidators: true }
|
||||
)
|
||||
|
||||
if (!bgm) {
|
||||
return ApiResponse.notFound(res, 'BGM不存在')
|
||||
}
|
||||
|
||||
logger.info(`管理员更新了BGM: ${bgm.name}`)
|
||||
return ApiResponse.success(res, { message: 'BGM更新成功', data: bgm })
|
||||
} catch (error) {
|
||||
logger.error('更新BGM失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 删除BGM(管理员)
|
||||
* DELETE /api/admin/bgm/delete/:id
|
||||
*/
|
||||
router.delete('/delete/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
const bgm = await BGM.findByIdAndDelete(id)
|
||||
|
||||
if (!bgm) {
|
||||
return ApiResponse.notFound(res, 'BGM不存在')
|
||||
}
|
||||
|
||||
logger.info(`管理员删除了BGM: ${bgm.name}`)
|
||||
return ApiResponse.success(res, { message: 'BGM删除成功' })
|
||||
} catch (error) {
|
||||
logger.error('删除BGM失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 切换BGM激活状态(管理员)
|
||||
* PUT /api/admin/bgm/toggle/:id
|
||||
*/
|
||||
router.put('/toggle/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
const bgm = await BGM.findById(id)
|
||||
|
||||
if (!bgm) {
|
||||
return ApiResponse.notFound(res, 'BGM不存在')
|
||||
}
|
||||
|
||||
bgm.isActive = !bgm.isActive
|
||||
await bgm.save()
|
||||
|
||||
logger.info(`管理员${bgm.isActive ? '激活' : '禁用'}了BGM: ${bgm.name}`)
|
||||
return ApiResponse.success(res, {
|
||||
message: `BGM已${bgm.isActive ? '激活' : '禁用'}`,
|
||||
data: bgm
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('切换BGM状态失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,209 +0,0 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const { Gallery } = require('../models')
|
||||
const ApiResponse = require('../utils/response')
|
||||
const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
// 应用用户认证中间件到需要登录的路由
|
||||
// 注意:POST /api/gallery 需要认证
|
||||
router.post('/', userAuthMiddleware)
|
||||
router.use('/like', userAuthMiddleware)
|
||||
// 作品列表接口使用可选认证,这样未登录用户也能访问
|
||||
router.get('/works', optionalAuthMiddleware)
|
||||
|
||||
/**
|
||||
* 获取作品列表(兼容旧版API)
|
||||
* GET /api/gallery/works
|
||||
*/
|
||||
router.get('/works', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 12, filter = 'all' } = req.query
|
||||
|
||||
const query = { status: 'approved' }
|
||||
|
||||
// 处理筛选条件
|
||||
if (filter === 'dim2') {
|
||||
query.dim = 2
|
||||
} else if (filter === 'dim3') {
|
||||
query.dim = 3
|
||||
} else if (filter === 'my') {
|
||||
// 显示当前用户的作品,需要认证
|
||||
if (!req.user) {
|
||||
return ApiResponse.error(res, '需要登录才能查看我的作品', 401)
|
||||
}
|
||||
query.openid = req.user.openid
|
||||
}
|
||||
|
||||
const total = await Gallery.countDocuments(query)
|
||||
const works = await Gallery.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((parseInt(page) - 1) * parseInt(limit))
|
||||
.limit(parseInt(limit))
|
||||
.lean()
|
||||
|
||||
// 格式化返回数据
|
||||
const formattedWorks = works.map(work => ({
|
||||
_id: work._id,
|
||||
fileURL: work.imageUrl || work.imageData,
|
||||
authorAvatar: work.authorAvatar || '/static/images/default_avatar.png',
|
||||
authorName: work.authorName || '星辰旅者',
|
||||
likes: work.likeCount || 0,
|
||||
views: work.viewCount || 0,
|
||||
dim: work.dim || 2,
|
||||
type: work.dim === 2 ? 'dim2' : 'dim3',
|
||||
description: work.description || '',
|
||||
createdAt: work.createdAt
|
||||
}))
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
list: formattedWorks,
|
||||
total,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取作品列表失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取作品列表
|
||||
* GET /api/gallery
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, status = 'approved', dim } = req.query
|
||||
|
||||
const query = { status }
|
||||
if (dim) query.dim = parseInt(dim)
|
||||
|
||||
const total = await Gallery.countDocuments(query)
|
||||
const works = await Gallery.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((parseInt(page) - 1) * parseInt(limit))
|
||||
.limit(parseInt(limit))
|
||||
.lean()
|
||||
|
||||
return ApiResponse.paginated(res, works, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取作品列表失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 上传作品
|
||||
* POST /api/gallery
|
||||
*/
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const user = req.user
|
||||
const { title, description, imageData, imageUrl, tags, dim } = req.body
|
||||
|
||||
if (!title || !dim) {
|
||||
return ApiResponse.error(res, '标题和维度为必填项', 400)
|
||||
}
|
||||
|
||||
const work = new Gallery({
|
||||
openid: user.openid,
|
||||
userId: user._id,
|
||||
authorName: user.nickName,
|
||||
authorAvatar: user.avatarUrl,
|
||||
title,
|
||||
description,
|
||||
imageData,
|
||||
imageUrl,
|
||||
tags: tags || [],
|
||||
dim,
|
||||
status: 'approved' // 自动审核通过,用户可以立即看到自己的作品
|
||||
})
|
||||
|
||||
await work.save()
|
||||
|
||||
return ApiResponse.success(res, work, '作品上传成功,等待审核')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('上传作品失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取作品详情
|
||||
* GET /api/gallery/:id
|
||||
*/
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const work = await Gallery.findById(req.params.id)
|
||||
|
||||
if (!work) {
|
||||
return ApiResponse.notFound(res, '作品不存在')
|
||||
}
|
||||
|
||||
// 增加浏览量
|
||||
work.viewCount += 1
|
||||
await work.save()
|
||||
|
||||
return ApiResponse.success(res, work)
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取作品详情失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 点赞作品(兼容移动端API)
|
||||
* POST /api/gallery/like/:id
|
||||
*/
|
||||
router.post('/like/:id', async (req, res) => {
|
||||
try {
|
||||
const work = await Gallery.findById(req.params.id)
|
||||
|
||||
if (!work) {
|
||||
return ApiResponse.notFound(res, '作品不存在')
|
||||
}
|
||||
|
||||
work.likeCount += 1
|
||||
await work.save()
|
||||
|
||||
return ApiResponse.success(res, { likeCount: work.likeCount }, '点赞成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('点赞失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 点赞作品
|
||||
* POST /api/gallery/:id/like
|
||||
*/
|
||||
router.post('/:id/like', async (req, res) => {
|
||||
try {
|
||||
const work = await Gallery.findById(req.params.id)
|
||||
|
||||
if (!work) {
|
||||
return ApiResponse.notFound(res, '作品不存在')
|
||||
}
|
||||
|
||||
work.likeCount += 1
|
||||
await work.save()
|
||||
|
||||
return ApiResponse.success(res, { likeCount: work.likeCount }, '点赞成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('点赞失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,15 +1,11 @@
|
||||
const authRoutes = require('./auth')
|
||||
const userRoutes = require('./user')
|
||||
const paymentRoutes = require('./payment')
|
||||
const galleryRoutes = require('./gallery')
|
||||
const knowledgeRoutes = require('./knowledge')
|
||||
const adminRoutes = require('./admin')
|
||||
const shareRoutes = require('./share')
|
||||
const bgmRoutes = require('./bgm')
|
||||
const aiChatRoutes = require('./aiChat')
|
||||
const aiModelRoutes = require('./aiModel')
|
||||
const feedbackRoutes = require('./feedback')
|
||||
const pinyinRoutes = require('./pinyin')
|
||||
const trendRoutes = require('./trend')
|
||||
|
||||
module.exports = (app) => {
|
||||
@@ -22,9 +18,6 @@ module.exports = (app) => {
|
||||
// 支付路由
|
||||
app.use('/api/payment', paymentRoutes)
|
||||
|
||||
// 画廊路由
|
||||
app.use('/api/gallery', galleryRoutes)
|
||||
|
||||
// 知识库路由
|
||||
app.use('/api/knowledge', knowledgeRoutes)
|
||||
|
||||
@@ -34,24 +27,12 @@ module.exports = (app) => {
|
||||
// 管理后台路由
|
||||
app.use('/api/admin', adminRoutes)
|
||||
|
||||
// 分享记录路由
|
||||
app.use('/api/share', shareRoutes)
|
||||
|
||||
// BGM音效路由(公开接口 + 管理员接口)
|
||||
app.use('/api/bgm', bgmRoutes)
|
||||
|
||||
// AI问答路由
|
||||
app.use('/api/ai-chat', aiChatRoutes)
|
||||
|
||||
// AI模型配置路由
|
||||
app.use('/api/ai-model', aiModelRoutes)
|
||||
|
||||
// 拼音探索路由
|
||||
app.use('/api/pinyin/contents', pinyinRoutes.contents)
|
||||
app.use('/api/pinyin/progress', pinyinRoutes.progress)
|
||||
app.use('/api/pinyin/games', pinyinRoutes.games)
|
||||
app.use('/api/pinyin/achievements', pinyinRoutes.achievements)
|
||||
|
||||
// AI趋势路由
|
||||
app.use('/api/trend', trendRoutes)
|
||||
}
|
||||
// AI趋势路由
|
||||
app.use('/api/trend', trendRoutes)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware } = require('../../middleware/auth');
|
||||
const achievementController = require('../../controllers/pinyin/achievementController');
|
||||
|
||||
// 获取成就列表(公开)
|
||||
router.get('/', achievementController.getAchievements);
|
||||
|
||||
// 获取我的成就(需要登录)
|
||||
router.get('/my', authMiddleware, achievementController.getMyAchievements);
|
||||
|
||||
// 检查成就达成(需要登录)
|
||||
router.post('/check', authMiddleware, achievementController.checkAchievements);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,16 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware } = require('../../middleware/auth');
|
||||
const contentController = require('../../controllers/pinyin/contentController');
|
||||
|
||||
// 公开接口
|
||||
router.get('/', contentController.getContents);
|
||||
router.get('/:symbol', contentController.getContentBySymbol);
|
||||
router.get('/:symbol/audio', contentController.getContentAudio);
|
||||
|
||||
// 管理员接口
|
||||
router.post('/', authMiddleware, contentController.createContent);
|
||||
router.put('/:id', authMiddleware, contentController.updateContent);
|
||||
router.delete('/:id', authMiddleware, contentController.deleteContent);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,24 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { userAuthMiddleware } = require('../../middleware/auth');
|
||||
const gameController = require('../../controllers/pinyin/gameController');
|
||||
|
||||
// 获取游戏配置
|
||||
router.get('/config', gameController.getGameConfig);
|
||||
|
||||
// 获取游戏配置(按类型)
|
||||
router.get('/config/:type', gameController.getGameConfigByType);
|
||||
|
||||
// 获取用户游戏统计(需要登录)
|
||||
router.get('/stats', userAuthMiddleware, gameController.getGameStats);
|
||||
|
||||
// 获取用户游戏记录(需要登录)
|
||||
router.get('/records', userAuthMiddleware, gameController.getGameRecords);
|
||||
|
||||
// 记录游戏结果(需要登录)
|
||||
router.post('/record', userAuthMiddleware, gameController.recordGame);
|
||||
|
||||
// 获取游戏排行榜
|
||||
router.get('/leaderboard/:type', gameController.getLeaderboard);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 拼音探索模块路由导出
|
||||
*/
|
||||
const contentsRouter = require('./contents');
|
||||
const progressRouter = require('./progress');
|
||||
const gamesRouter = require('./games');
|
||||
const achievementsRouter = require('./achievements');
|
||||
|
||||
module.exports = {
|
||||
contents: contentsRouter,
|
||||
progress: progressRouter,
|
||||
games: gamesRouter,
|
||||
achievements: achievementsRouter
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { userAuthMiddleware } = require('../../middleware/auth');
|
||||
const progressController = require('../../controllers/pinyin/progressController');
|
||||
|
||||
// 获取用户探索进度
|
||||
router.get('/', userAuthMiddleware, progressController.getProgress);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,67 +0,0 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const { ShareRecord, User } = require('../models')
|
||||
const ApiResponse = require('../utils/response')
|
||||
const { userAuthMiddleware, optionalAuthMiddleware, authMiddleware } = require('../middleware/auth')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
/**
|
||||
* 记录分享行为(支持匿名)
|
||||
* POST /api/share/record
|
||||
*/
|
||||
router.post('/record', optionalAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { shareType, score } = req.body
|
||||
const openid = req.user ? req.user.openid : 'anonymous'
|
||||
const userId = req.user ? req.user._id : null
|
||||
|
||||
const record = new ShareRecord({
|
||||
openid,
|
||||
userId,
|
||||
shareType: shareType || 'app',
|
||||
score: score || 0
|
||||
})
|
||||
await record.save()
|
||||
|
||||
return ApiResponse.success(res, { message: '分享已记录' })
|
||||
} catch (error) {
|
||||
logger.error('记录分享失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取分享记录列表(管理员)
|
||||
* GET /api/share/list
|
||||
*/
|
||||
router.get('/list', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, openid } = req.query
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const query = {}
|
||||
if (openid && openid !== 'all') {
|
||||
query.openid = openid
|
||||
}
|
||||
|
||||
const records = await ShareRecord.find(query)
|
||||
.sort({ sharedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit))
|
||||
.populate('userId', 'nickname avatar')
|
||||
|
||||
const total = await ShareRecord.countDocuments(query)
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
list: records,
|
||||
total,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('获取分享记录失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -5,7 +5,39 @@ const ApiResponse = require('../utils/response')
|
||||
const { userAuthMiddleware } = require('../middleware/auth')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
// 应用用户认证中间件到所有路由
|
||||
/**
|
||||
* 获取排行榜(公开接口,无需登录)
|
||||
* GET /api/user/leaderboard
|
||||
*/
|
||||
router.get('/leaderboard', async (req, res) => {
|
||||
try {
|
||||
const { limit = 50, dim } = req.query
|
||||
|
||||
const users = await User.getLeaderboard(dim ? parseInt(dim) : null, parseInt(limit))
|
||||
|
||||
const data = users.map((u, i) => ({
|
||||
rank: i + 1,
|
||||
openid: u.openid,
|
||||
nickname: u.nickName || '星辰旅者',
|
||||
avatar: u.avatarUrl || '',
|
||||
totalScore: u.totalScore || 0,
|
||||
dim1Score: u.dim1Score || 0,
|
||||
dim2Score: u.dim2Score || 0,
|
||||
dim3Score: u.dim3Score || 0,
|
||||
dim4Score: u.dim4Score || 0,
|
||||
dim5Score: u.dim5Score || 0,
|
||||
unlockedDims: u.unlockedDims || [1]
|
||||
}))
|
||||
|
||||
return ApiResponse.success(res, data)
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取排行榜失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
// 应用用户认证中间件到所有路由(排行榜已在上方公开)
|
||||
router.use(userAuthMiddleware)
|
||||
|
||||
/**
|
||||
@@ -162,38 +194,6 @@ router.post('/progress', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取排行榜
|
||||
* GET /api/user/leaderboard
|
||||
*/
|
||||
router.get('/leaderboard', async (req, res) => {
|
||||
try {
|
||||
const { limit = 50, dim } = req.query
|
||||
|
||||
const users = await User.getLeaderboard(dim ? parseInt(dim) : null, parseInt(limit))
|
||||
|
||||
const data = users.map((u, i) => ({
|
||||
rank: i + 1,
|
||||
openid: u.openid,
|
||||
nickname: u.nickName || '星辰旅者',
|
||||
avatar: u.avatarUrl || '',
|
||||
totalScore: u.totalScore || 0,
|
||||
dim1Score: u.dim1Score || 0,
|
||||
dim2Score: u.dim2Score || 0,
|
||||
dim3Score: u.dim3Score || 0,
|
||||
dim4Score: u.dim4Score || 0,
|
||||
dim5Score: u.dim5Score || 0,
|
||||
unlockedDims: u.unlockedDims || [1]
|
||||
}))
|
||||
|
||||
return ApiResponse.success(res, data)
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取排行榜失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取用户成就
|
||||
* GET /api/user/achievements
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 内容丰富脚本 — 清理旧 WDKJ 数据,插入 5 维度 AI 知识 + 扩充趋势
|
||||
* 执行: node src/scripts/enrich-content.js
|
||||
*/
|
||||
const mongoose = require('mongoose')
|
||||
const path = require('path')
|
||||
|
||||
// 加载环境变量
|
||||
const dotenv = require('dotenv')
|
||||
dotenv.config({ path: path.join(__dirname, '..', '..', '.env') })
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj'
|
||||
|
||||
async function main() {
|
||||
await mongoose.connect(MONGODB_URI)
|
||||
console.log('✅ 已连接 MongoDB:', MONGODB_URI)
|
||||
|
||||
const db = mongoose.connection.db
|
||||
|
||||
// ============================================================
|
||||
// 1. 清理旧 WDKJ 知识(几何空间类内容)
|
||||
// ============================================================
|
||||
const oldKeywords = ['一维', '二维', '三维', '四维', '五维', '六维', '七维', '高维',
|
||||
'多维空间', '坐标系', '几何特性', '几何空间', '数学表示', '物理意义',
|
||||
'弦理论', '卡鲁扎-克莱因', '紧致维度', '可视化技术', '投影法', '切片法']
|
||||
|
||||
// Build regex for old content
|
||||
const oldRegex = oldKeywords.map(k => `.*${k}.*`).join('|')
|
||||
const deleteResult = await db.collection('knowledges').deleteMany({
|
||||
$or: oldKeywords.map(k => ({ title: { $regex: k } }))
|
||||
})
|
||||
console.log(`🗑️ 已删除 ${deleteResult.deletedCount} 条旧 WDKJ 知识`)
|
||||
|
||||
// Also delete any remaining non-AI content
|
||||
const extraDelete = await db.collection('knowledges').deleteMany({
|
||||
title: { $regex: /(空间|坐标|几何|弦理论|可视化|投影)/ }
|
||||
})
|
||||
if (extraDelete.deletedCount > 0) {
|
||||
console.log(`🗑️ 额外删除 ${extraDelete.deletedCount} 条几何内容`)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2. 插入 5 维度 AI 知识
|
||||
// ============================================================
|
||||
const knowledgeItems = [
|
||||
// ===== D1: AI 起源 =====
|
||||
{ dim: 1, title: '图灵测试:人工智能的哲学起点',
|
||||
content: '1950 年,英国数学家艾伦·图灵发表了划时代论文《计算机器与智能》,提出了著名的"图灵测试":如果一台机器能够在对话中让人类无法分辨它是机器还是人,那么这台机器就具备智能。图灵还预言了创造真正智能机器的可能性,这一思想成为 AI 领域的精神源头。图灵测试不仅是一个测试标准,更引发了"机器能否思考"的终极哲学追问,至今仍是 AI 研究中的核心议题。',
|
||||
category: 'history', tags: ['图灵测试', 'AI起源', '艾伦·图灵'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
|
||||
{ dim: 1, title: '达特茅斯会议:AI 的诞生时刻',
|
||||
content: '1956 年夏季,约翰·麦卡锡、马文·明斯基、克劳德·香农等科学家在美国达特茅斯学院召开了一场具有历史意义的研讨会。正是在这次会议上,"人工智能(Artificial Intelligence)"这个术语被首次正式提出,标志着 AI 作为一门独立学科的诞生。与会者乐观地预测"人类级别的机器智能将在一代人的时间内实现"。虽然这个预测过于乐观,但达特茅斯会议点燃了 AI 研究的火种。',
|
||||
category: 'history', tags: ['达特茅斯会议', 'AI诞生', '麦卡锡', '香农'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
|
||||
{ dim: 1, title: 'ELIZA 与感知机:AI 的童年玩具',
|
||||
content: '1966 年,MIT 的约瑟夫·魏泽鲍姆创造了 ELIZA,史上第一个聊天机器人。它通过简单的模式匹配模拟心理治疗师的对话,让人们首次直观感受到 AI 的交互潜力。同年,弗兰克·罗森布拉特的感知机(Perceptron)成为最早的人工神经网络之一。然而,马文·明斯基随后证明了感知机的局限性,导致神经网络研究陷入低谷,这为后来的"AI 寒冬"埋下了伏笔。',
|
||||
category: 'history', tags: ['ELIZA', '感知机', '聊天机器人', '神经网络'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
|
||||
{ dim: 1, title: '深蓝与 AI 寒冬:技术的曲折前行',
|
||||
content: '1997 年,IBM 的深蓝(Deep Blue)击败国际象棋世界冠军卡斯帕罗夫,成为首个在标准比赛时限内战胜棋王的计算机系统。这一成就震惊世界,证明了 AI 在复杂战略领域超越人类的潜力。但在此之前,AI 经历了长达十年的"寒冬":1970 年代,由于计算能力不足和符号主义 AI 难以处理现实问题,各国科研经费大幅削减。深蓝的胜利标志着 AI 从寒冬中复苏,开启了新的发展周期。',
|
||||
category: 'history', tags: ['深蓝', 'IBM', '卡斯帕罗夫', 'AI寒冬'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
|
||||
{ dim: 1, title: '反向传播算法:神经网络的复兴之匙',
|
||||
content: '1986 年,大卫·鲁姆哈特等人重新发现并推广了反向传播(Backpropagation)算法,解决了多层神经网络的训练难题。此前,神经网络只能处理简单的线性问题,而 BP 算法让网络能够通过"误差反向传播"来调整各层参数。这一突破使连接主义(神经网络方法)重新成为 AI 研究的主流方向,为 2010 年代的深度学习革命奠定了理论基础。',
|
||||
category: 'concept', tags: ['反向传播', 'BP算法', '神经网络', '深度学习'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
|
||||
{ dim: 1, title: 'AlexNet 与深度学习元年',
|
||||
content: '2012 年,亚历克斯·克里泽夫斯基等人提出的 AlexNet 在 ImageNet 图像识别大赛中以远超传统方法的精度夺冠,错误率从 26% 骤降至 15%。AlexNet 使用了 GPU 加速的深度卷积神经网络,标志着深度学习正式成为计算机视觉的核心技术。这一年被称为"深度学习元年",开启了 AI 全面爆发的时代。此后,更深的 VGG、残差网络 ResNet 等模型不断刷新纪录。',
|
||||
category: 'concept', tags: ['AlexNet', 'ImageNet', '卷积神经网络', 'GPU'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
|
||||
{ dim: 1, title: 'AlphaGo:AI 征服围棋之路',
|
||||
content: '2016 年,谷歌 DeepMind 的 AlphaGo 以 4:1 击败围棋世界冠军李世石,这一成就被视为 AI 发展的里程碑。围棋的可能性远超国际象棋(棋盘状态数超过宇宙原子数),传统方法无法解决。AlphaGo 融合了深度学习和强化学习——先通过人类棋谱学习,再通过自我对弈不断进化。后续的 AlphaZero 更实现了"从零自学",掌握多种棋类游戏。AlphaGo 证明了 AI 在直觉性、创造性领域的巨大潜力。',
|
||||
category: 'history', tags: ['AlphaGo', 'DeepMind', '围棋', '强化学习', '李世石'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
|
||||
|
||||
// ===== D2: AI 发展 =====
|
||||
{ dim: 2, title: 'Transformer 架构:AI 的基石',
|
||||
content: '2017 年,Google 研究团队在论文《Attention is All You Need》中提出了 Transformer 架构,彻底改变了 AI 的发展轨迹。Transformer 完全摒弃了传统的循环神经网络结构,完全基于自注意力机制(Self-Attention)来捕捉序列中元素之间的关系。它的优势在于:可并行计算(训练速度大幅提升)、可捕捉长距离依赖关系、可扩展到海量参数。如今,所有主流大模型(GPT、BERT、Claude、Gemini、DeepSeek 等)都建立在 Transformer 架构之上。',
|
||||
category: 'concept', tags: ['Transformer', '注意力机制', 'Google', 'Attention'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
|
||||
{ dim: 2, title: 'GPT 系列演进:从 GPT-1 到 GPT-5',
|
||||
content: 'OpenAI 的 GPT 系列是 AI 发展史上最具影响力的模型家族之一。2018 年的 GPT-1(1.17 亿参数)证明了大规模无监督预训练的有效性。2019 年的 GPT-2(15 亿参数)因"太危险"一度推迟发布,其文本生成能力令人震惊。2020 年的 GPT-3(1750 亿参数)确立了"规模即能力"的范式。2022 年的 ChatGPT 将 GPT-3.5 带入大众视野,引爆全球 AI 热潮。2023-2026 年,GPT-4、GPT-5、GPT-5.5 持续迭代,在推理、多模态、长上下文等方面不断突破,5.5 版本更是从零重训,性能全面提升。',
|
||||
category: 'history', tags: ['GPT', 'OpenAI', 'ChatGPT', '大模型'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
|
||||
{ dim: 2, title: 'BERT 与预训练-微调范式',
|
||||
content: '2018 年,Google 提出的 BERT(Bidirectional Encoder Representations from Transformers)开启了一种全新的 AI 模型范式:先在海量无标注数据上预训练,再针对具体任务进行微调。BERT 是双向编码器模型,能同时从上下文中理解每个词的含义。这一范式彻底改变了 NLP 领域,后来被广泛应用于各种 AI 任务,成为现代大模型的基础方法论。',
|
||||
category: 'concept', tags: ['BERT', '预训练', '微调', 'Google', 'NLP'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
|
||||
{ dim: 2, title: '生成对抗网络 GAN:AI 创造力的起点',
|
||||
content: '2014 年,伊恩·古德费洛提出了生成对抗网络(GAN),由生成器和判别器两个网络相互博弈:生成器努力生成逼真的假数据,判别器努力分辨真假。这种"对抗训练"机制使 AI 能够生成极为逼真的图像、音频和视频。GAN 开启了生成式 AI 的时代,为后来的扩散模型(DALL·E、Stable Diffusion 等)奠定了基础。虽然如今扩散模型在某些任务上超越了 GAN,但 GAN 的思想深刻影响了整个 AI 领域。',
|
||||
category: 'concept', tags: ['GAN', '生成对抗网络', '生成式AI', '古德费洛'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
|
||||
{ dim: 2, title: '扩散模型与 AI 绘画革命',
|
||||
content: '2022 年,Stable Diffusion 和 DALL·E 2 的发布引爆了 AI 绘画热潮。这些模型基于扩散(Diffusion)技术:先学习将图像逐步加噪的过程,再学会从纯噪声中逐步还原出清晰的图像。扩散模型在图像质量、多样性和创意性上远超 GAN,让"一句话生成专业级图片"成为现实。此后,Midjourney 以其独特的艺术风格成为设计师的新宠,Adobe 将 Firefly 集成到 Photoshop,AI 绘画正式进入主流创作工具链。',
|
||||
category: 'concept', tags: ['扩散模型', 'Stable Diffusion', 'DALL·E', 'Midjourney', 'AI绘画'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
|
||||
{ dim: 2, title: 'MoE 混合专家模型:效率的革命',
|
||||
content: '混合专家模型(Mixture of Experts, MoE)是一种将大模型分解为多个"专家"子网络的架构。每次推理时,MoE 只激活其中少数专家,而非全量参数。例如 GPT-4 传言有 1.8 万亿参数,但每次只激活约 3700 亿。这种设计大幅降低了推理成本,同时保留了海量参数的知识容量。DeepSeek V4 等国产模型也采用了 MoE 架构,以极低的成本实现接近顶级闭源模型的性能。MoE 被认为是未来大模型规模化的重要方向。',
|
||||
category: 'concept', tags: ['MoE', '混合专家模型', 'GPT-4', 'DeepSeek', '效率'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
|
||||
{ dim: 2, title: 'DeepSeek 开源之路:从 V2 到 R1 的技术跃迁',
|
||||
content: 'DeepSeek 是近年来最受瞩目的国产大模型之一。2024 年的 DeepSeek V2 以创新的 MoE 架构和极低的推理成本引起关注。2025 年初的 DeepSeek R1 更是颠覆性模型——通过强化学习训练出强大的推理能力,在数学、代码等任务上媲美 OpenAI o1。R1 展示了一个重要方向:不必追求最大参数,而是通过创新的训练方法和架构设计,以更低的成本实现更强的智能。DeepSeek 坚持开源路线,为全球 AI 社区贡献了宝贵的开放权重模型。',
|
||||
category: 'history', tags: ['DeepSeek', 'R1', '开源', '推理', '国产模型'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
|
||||
|
||||
// ===== D3: AI 当前 =====
|
||||
{ dim: 3, title: '2026 年大模型格局:四强争霸',
|
||||
content: '2026 年,AI 大模型竞争进入白热化阶段,形成 GPT、Claude、Gemini、DeepSeek 四足鼎立的格局。OpenAI 的 GPT-5.5 是首个从零重训的基础模型,均衡性最强;Anthropic 的 Claude Opus 4.7 在编码(SWE-bench 87.6%)领域领先,代码质量行业第一;Google 的 Gemini 3.5 Flash 免费开放,200 万 Token 上下文窗口独步天下;DeepSeek V4 以极低的价格(仅为 Claude 的 1/432)进入全球前十。选择哪个模型取决于场景:编程用 Claude,通用选 GPT,长文本用 Gemini,成本敏感选 DeepSeek。',
|
||||
category: 'application', tags: ['大模型', 'GPT-5', 'Claude', 'Gemini', 'DeepSeek', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
|
||||
{ dim: 3, title: 'AI Agent 智能体生态:2026 爆发元年',
|
||||
content: '2026 年被业界称为"智能体爆发年"。AI Agent 不再只是聊天机器人,而是能独立感知环境、拆解任务、调用工具、执行操作并持续学习的智能实体。从 Cursor(年收入 5 亿美元)到 Devin,从 OpenAI Operator 到 Manus,AI Agent 已从实验品转变为企业优先事项。MCP(模型上下文协议)和 A2A(Agent to Agent 协议)的标准化,让智能体能够互联互通。82% 的企业计划在未来 12 个月内将 Agent 应用于客户支持。Agent 正在重塑软件开发的每一个环节。',
|
||||
category: 'application', tags: ['Agent', '智能体', 'MCP', 'A2A', 'Cursor', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
|
||||
{ dim: 3, title: '多模态大模型:全能 AI 时代',
|
||||
content: '多模态 AI 能够同时处理文本、图像、音频、视频等多种类型的数据。2026 年,多模态已成为大模型的标配能力。GPT-5.5 支持图像理解和生成,Gemini 3 Pro 直接打通 Gmail/Docs/Drive,豆包 Seed 2.0 Pro 进入全球前十。MiniMax M3 多模态大模型将在 WAIC 2026 首发。多模态技术的核心挑战在于如何在不同模态间建立统一的理解——让模型既"看懂"图片又"听懂"语音。这一能力的突破使 AI 更接近人类的感知方式。',
|
||||
category: 'application', tags: ['多模态', '视觉', '语音', 'GPT-5', 'Gemini'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
|
||||
{ dim: 3, title: '具身智能与人形机器人:从实验室到工厂',
|
||||
content: '具身智能(Embodied AI)是让 AI 拥有物理身体并与世界交互的前沿方向。2026 年,中国工信部预计全年人形机器人整机产量突破 10 万台。WAIC 2026 上,多款人形机器人将现场演示"进厂上岗"。蚂蚁灵波开源 LingBot-World 2.0,实现了世界模型的小时级实时生成。具身智能的核心突破在于"世界模型"——让机器人理解物理世界的规律,而不只是执行预编程的动作。从仓库搬运到家庭服务,人形机器人正在从概念走向规模化商用。',
|
||||
category: 'application', tags: ['具身智能', '人形机器人', '世界模型', '蚂蚁灵波'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
|
||||
{ dim: 3, title: 'AI 编程革命:Cursor、Claude Code 与 Vibe Coding',
|
||||
content: 'AI 编程工具正在重新定义软件开发。Cursor 在 2022 年成立,年收入突破 5 亿美元,每名员工创造 140 万美元收入。Claude Code 实现代理式编程,周限额提升 50%。"氛围编程"(Vibe Coding)成为新趋势——开发者只需描述高层次目标,AI 自动完成多步骤执行。但推理成本暴涨 20 倍也带来了挑战,促使企业探索速率限制和效率优化。AI 编程正从简单的代码补全,演进到测试、QA、代码审查和调试的全链路智能化。',
|
||||
category: 'application', tags: ['编程', 'Cursor', 'Claude Code', 'Vibe Coding', 'AI开发'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
|
||||
{ dim: 3, title: '开源 vs 闭源:AI 模型的路线之争',
|
||||
content: '2026 年,开源与闭源模型的分化愈加明显。闭源阵营(OpenAI、Anthropic、Google)保持性能领先,但在定价上居高不下。开源阵营(DeepSeek、Llama、Qwen、GLM)通过 MoE 架构和高效的训练方法,以 1/400 的成本实现了接近顶级闭源的性能。智谱 GLM-5 在 OpenRouter 上开源并登顶榜首。开源的优势在于透明度、可定制和低成本,但闭源产品在安全性和生态完整性上更有保障。这场拉锯战正在重塑 AI 的产业格局。',
|
||||
category: 'concept', tags: ['开源', '闭源', 'DeepSeek', 'Llama', 'GLM'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
|
||||
{ dim: 3, title: '100 万到 200 万 Token:上下文窗口之战',
|
||||
content: '上下文窗口(Context Window)是决定大模型能力的关键指标之一。2026 年,上下文窗口的竞赛进入白热化:Gemini 3.5 Flash 支持 200 万 Token(约 150 万字),GPT-5.5 支持 128K Token,Claude Opus 4.7 支持 200K Token。更大的上下文窗口意味着模型可以一次处理整本书、整个代码库或完整的对话历史。Google 凭借 TPU 优势在上下文窗口上遥遥领先,但长上下文也带来推理成本的急剧上升。如何平衡长度与效率,是当前研究的重点。',
|
||||
category: 'concept', tags: ['上下文窗口', 'Token', 'Gemini', 'GPT-5', 'Claude'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
|
||||
|
||||
// ===== D4: AI 学习 =====
|
||||
{ dim: 4, title: '提示词工程入门:从 Prompt 到 Context Engineering',
|
||||
content: '提示词工程(Prompt Engineering)是使用 AI 大模型的核心技能。2026 年,这门技术已从简单的"告诉 AI 做什么"进化为"指导 AI 如何思考"的 Context Engineering。核心原则包括:明确角色("你是一个资深 Python 工程师")、结构化输出(JSON/表格)、提供示例(Few-shot)、负面约束("不要使用全局变量")。进阶技巧包括:使用思考链(Chain-of-Thought)引导推理、用 XML 标签组织上下文、分配"思考 Token"。好的提示词可以让模型输出质量提升数倍。',
|
||||
category: 'concept', tags: ['提示词工程', 'Prompt', 'Context Engineering', '入门'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
|
||||
{ dim: 4, title: 'RAG 系统搭建指南:检索增强生成',
|
||||
content: 'RAG(Retrieval-Augmented Generation)是让大模型"接入"企业私有数据的核心技术。它的工作原理是:用户提问 → 从知识库检索相关文档 → 将文档和问题一起发给大模型 → 生成准确回答。RAG 的优势在于:知识实时更新、幻觉大幅降低、成本可控。一个典型的 RAG 系统包含:向量数据库(Milvus/Pinecone)、嵌入模型(Embedding)、分块策略、检索排序。2026 年的 RAG 已进化为 Agentic RAG——智能体自动决策何时检索、如何合并多源信息。',
|
||||
category: 'concept', tags: ['RAG', '检索增强生成', '知识库', '向量数据库', 'LangChain'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
|
||||
{ dim: 4, title: '模型微调实战:LoRA 与参数高效微调',
|
||||
content: '微调(Fine-tuning)是让通用大模型适应特定领域任务的关键技术。全参数微调需要大量 GPU 资源,而参数高效微调(PEFT)大幅降低了门槛。其中最流行的是 LoRA(Low-Rank Adaptation),它在原始模型权重旁添加少量可训练参数(通常为原参数的 0.1-1%),显著降低显存需求。一张 A10(24G 显存)即可微调 7B 模型。常用的微调框架有 LLaMA-Factory(专业全面)和 Unsloth(快速省显存)。微调适用于:使模型掌握特定领域的术语和风格。',
|
||||
category: 'concept', tags: ['微调', 'LoRA', 'PEFT', 'LLaMA-Factory', 'Unsloth'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
|
||||
{ dim: 4, title: '大模型工具链全景:LangChain、LlamaIndex 与框架生态',
|
||||
content: '大模型应用开发有一整套工具链。LangChain 是最流行的应用框架,提供了链式调用、工具集成、记忆管理、Agent 编排等功能。LlamaIndex 专注数据索引和 RAG,帮助开发者高效连接私域数据。LangGraph 用于构建有状态的多步骤 AI 工作流。此外还有:Chroma/Pinecone(向量数据库)、Ollama/vLLM(本地模型部署)、Dify/Coze(低代码平台)。掌握这些工具,是 AI 应用开发者的基本功。2026 年,MCP 协议的出现进一步简化了工具集成。',
|
||||
category: 'application', tags: ['LangChain', 'LlamaIndex', 'LangGraph', '工具链', 'MCP'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
|
||||
{ dim: 4, title: 'Agent 开发框架:从 LangGraph 到 MCP 协议',
|
||||
content: '开发一个 AI Agent 需要综合运用多项技术。主流的 Agent 框架包括:LangGraph(状态化多步骤工作流)、CrewAI(多 Agent 协作)、AutoGen(微软多 Agent 框架)、OpenClaw(开源 Agent)。2026 年 Agent 开发的核心范式是"模型 + 工具 + 记忆 + 规划"。MCP(模型上下文协议)的标准化让 Agent 能够统一接入各种外部工具——从数据库到浏览器,从 API 到文件系统。A2A 协议则让不同 Agent 之间能够通信协作,构建真正的"Agent 互联网"。',
|
||||
category: 'application', tags: ['Agent', 'LangGraph', 'MCP', 'A2A', '开发框架'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
|
||||
{ dim: 4, title: 'AI 编程工具实战:从 Copilot 到 Claude Code',
|
||||
content: 'AI 编程工具已成为开发者不可或缺的伙伴。GitHub Copilot 是最普及的 AI 代码补全工具,Claude Code 擅长深度代码分析和重构,Cursor 提供完整的 AI 原生 IDE 体验。2026 年的进阶用法包括:用 AI 生成完整的单元测试、自动代码审查、数据库查询优化、微服务架构设计。最佳实践是:先给 AI 提供项目结构和上下文("这个项目是 React + Go + MySQL"),明确约束条件("不要引入新依赖"),再逐步迭代。AI 编程不是取代开发者,而是让开发者的效率提升 10 倍。',
|
||||
category: 'application', tags: ['Copilot', 'Claude Code', 'Cursor', 'AI编程', 'IDE'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
|
||||
{ dim: 4, title: '模型评估与基准测试:如何选择最优模型',
|
||||
content: '面对众多 AI 模型,如何做出正确选择?2026 年的标准评测体系包括:SWE-bench Verified(编程能力)、ARC-AGI-2(抽象推理)、AIME(数学能力)、LMArena(人类偏好评分)、GPQA(科学推理)。Claude Opus 4.7 在 SWE-bench 上以 87.6% 领先,GPT-5.5 在 ARC-AGI-2 上以 52.9% 拔得头筹,Gemini 3 Pro 在科学推理 GPQA 上得分 91.9%。选择模型的核心原则是:先明确需求场景(编程/推理/长文档/成本敏感),再参考对应基准测试,最后用实际场景验证。性价比也是关键——DeepSeek V4 以极低成本提供了实用级别的性能。',
|
||||
category: 'concept', tags: ['基准测试', 'SWE-bench', 'ARC-AGI', '模型评估', '选型'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
|
||||
|
||||
// ===== D5: AI 趋势 =====
|
||||
{ dim: 5, title: 'WAIC 2026:世界人工智能大会即将开幕',
|
||||
content: '2026 世界人工智能大会(WAIC 2026)将于 7 月 17-20 日在上海举办,展览面积首次突破 10 万平方米,1100 余家企业参展,超 300 款产品全球首发。华为 Atlas 950 超节点、MiniMax M3 多模态大模型、全球首款 AI 智能体手机、多款人形机器人等重磅新品将集中亮相。大会还将举办首届 WAIC Academic 学术会议,9 位图灵奖、诺贝尔奖得主确认参会。',
|
||||
category: 'application', tags: ['WAIC', '世界人工智能大会', '上海', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
|
||||
{ dim: 5, title: '中国 AI 产业规模破万亿',
|
||||
content: '2025 年,中国 AI 相关产业规模突破 1 万亿元,2026 年预计增速超 30%。国家发改委已布局 30 余个国家 AI 应用中试基地,央国企开放 1000 余个应用场景,重点行业 AI 渗透率突破 80%。中国日均 Token 调用量从 2024 年初的 1000 亿飙升至 2026 年 3 月的 140 万亿,增长 1400 倍。同时,中国企业正在从英伟达芯片转向国产 AI 芯片,未来 12 个月预计 46% 的 AI 加速器采购将投向国产产品。',
|
||||
category: 'company', tags: ['产业规模', '万亿', '国产芯片', '渗透率', 'Token'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
|
||||
{ dim: 5, title: '全球首款 AI 智能体手机将在 WAIC 首秀',
|
||||
content: '据悉,全球首款 AI 智能体手机将在 WAIC 2026 上首次公开亮相。这款手机内置 Agent 操作系统,用户可以通过自然语言指令让手机自动完成复杂任务——比如"帮我订下周五去北京的机票和酒店"或"整理相册中最近三个月的照片并做成旅行视频"。这标志着端侧 AI 从"语音助手"进化到"智能体管家",AI 不再只是被动回答问题,而是能够主动规划并执行多步骤任务。',
|
||||
category: 'product', tags: ['AI手机', '智能体', 'WAIC', '端侧AI'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
|
||||
{ dim: 5, title: 'DeepSeek 秘密造芯:推理芯片项目已启动',
|
||||
content: '据报道,DeepSeek 正在秘密自研推理芯片,项目已启动超过一年,旨在降低对英伟达的依赖。这一战略与华为、百度、阿里巴巴等中国科技巨头的自研芯片路线一致。在当前的全球芯片供应格局下,自研芯片不仅是降低成本的手段,更是保障技术自主可控的关键举措。DeepSeek 若能成功推出用于 AI 推理的自研芯片,将在成本上获得更大的竞争优势。',
|
||||
category: 'company', tags: ['DeepSeek', '芯片', '推理', '英伟达', '国产'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
|
||||
{ dim: 5, title: '联合国呼吁全球 AI 治理体系建设',
|
||||
content: '联合国秘书长古特雷斯在首届 AI 治理全球对话会上呼吁对 AI 实施全球管控,表示"我们可能是最后一代能够设定人类与机器共存条件的人"。与此同时,AI 安全与治理成为 WAIC 2026 的重要主题,数十个国家将围绕 AI 伦理、数据安全、产业规则展开协商。各国正加速制定 AI 法规:欧盟 AI 法案已生效,中国持续推进算法备案与大模型登记制度,全球 AI 治理框架正在形成。',
|
||||
category: 'company', tags: ['AI治理', '联合国', '法规', '安全', '伦理'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
|
||||
{ dim: 5, title: '2026 AI Agent 六大趋势:从编程到行业落地',
|
||||
content: 'CB Insights 发布的《AI Agent 报告》指出 2026 年六大趋势:语音 AI 加速崛起、AI 并购潮席卷智能体领域、利润压力从编程扩展到其他领域、智能体商业模式基础正在巩固、数据护城河之战重塑企业软件、智能体监控工具成为必需品。报告显示,82% 的企业将在 12 个月内把 Agent 应用于客户支持,AI Agent 初创公司平均成立仅 3.8 年即实现规模化收入。在医疗、金融等受监管行业,垂直 Agent 正在快速渗透。',
|
||||
category: 'paper', tags: ['Agent', '趋势', 'CB Insights', '行业报告', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
|
||||
{ dim: 5, title: '国家发改委发布"人工智能合作发展行动计划"',
|
||||
content: '国家发改委将在 WAIC 2026 上发布"人工智能合作发展行动计划",涵盖智能算力普惠、开源生态共享、人工智能赋能、安全治理协作等 8 个方面。同时还将发布《中国智·惠世界》案例集,覆盖 20 多个国家的人工智能应用实践。这一系列举措表明,中国正在从 AI 技术应用者转向全球 AI 治理的重要参与者和贡献者。',
|
||||
category: 'company', tags: ['发改委', '行动计划', '算力', '开源', '治理'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
|
||||
]
|
||||
|
||||
const insertResult = await db.collection('knowledges').insertMany(knowledgeItems)
|
||||
console.log(`📚 已插入 ${insertResult.insertedCount} 条 AI 知识内容`)
|
||||
console.log(` D1(AI起源): ${knowledgeItems.filter(k => k.dim === 1).length} 条`)
|
||||
console.log(` D2(AI发展): ${knowledgeItems.filter(k => k.dim === 2).length} 条`)
|
||||
console.log(` D3(AI当前): ${knowledgeItems.filter(k => k.dim === 3).length} 条`)
|
||||
console.log(` D4(AI学习): ${knowledgeItems.filter(k => k.dim === 4).length} 条`)
|
||||
console.log(` D5(AI趋势): ${knowledgeItems.filter(k => k.dim === 5).length} 条`)
|
||||
|
||||
// ============================================================
|
||||
// 3. 扩充趋势数据
|
||||
// ============================================================
|
||||
const now = new Date()
|
||||
const trends = [
|
||||
{ title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年', summary: '据路透社报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖,项目已启动超过一年。', source: '路透社', sourceUrl: 'https://reuters.com', category: '公司', tags: ['DeepSeek', '芯片'], hot: true, newsDate: new Date(now.getTime() - 1 * 3600000) },
|
||||
{ title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成', summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。', source: '量子位', sourceUrl: 'https://qbitai.com', category: '产品', tags: ['蚂蚁灵波', '世界模型'], hot: false, newsDate: new Date(now.getTime() - 3 * 3600000) },
|
||||
{ title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷', summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。', source: '机器之心', sourceUrl: 'https://jiqizhixin.com', category: '论文', tags: ['阿里', 'ACL', 'Agent'], hot: false, newsDate: new Date(now.getTime() - 5 * 3600000) },
|
||||
{ title: 'WAIC 2026 即将开幕:超 300 款 AI 新品全球首发', summary: '2026 世界人工智能大会将于 7 月 17-20 日在上海举办,超 300 款产品全球首发,华为 Atlas 950、全球首款 AI 智能体手机将亮相。', source: '经济参考报', sourceUrl: 'https://jjckb.xinhuanet.com', category: '公司', tags: ['WAIC', '上海', 'AI大会'], hot: true, newsDate: new Date(now.getTime() - 6 * 3600000) },
|
||||
{ title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%', summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。', source: '腾讯云', sourceUrl: 'https://cloud.tencent.com', category: '产品', tags: ['腾讯', '混元', 'Agent'], hot: false, newsDate: new Date(now.getTime() - 8 * 3600000) },
|
||||
{ title: '联合国呼吁全球 AI 治理:首届 AI 治理全球对话会召开', summary: '联合国秘书长古特雷斯呼吁对 AI 实施全球管控,强调"设定人类与机器共存的条件"刻不容缓。', source: 'IT之家', sourceUrl: 'https://ithome.com', category: '公司', tags: ['联合国', 'AI治理', '古特雷斯'], hot: false, newsDate: new Date(now.getTime() - 10 * 3600000) },
|
||||
{ title: '2026 年中国人形机器人整机产量有望突破 10 万台', summary: '工信部表示,今年人形机器人全年整机产量有望突破 10 万台,规上工业企业 AI 应用普及率超 30%。', source: '新华社', sourceUrl: 'https://xinhuanet.com', category: '公司', tags: ['人形机器人', '工信部', '产量'], hot: true, newsDate: new Date(now.getTime() - 12 * 3600000) },
|
||||
{ title: '国家发改委将发布"人工智能合作发展行动计划"', summary: '行动计划涵盖智能算力普惠、开源生态共享、安全治理协作等 8 个方面,将在 WAIC 2026 上正式发布。', source: '经济参考报', sourceUrl: 'https://jjckb.xinhuanet.com', category: '公司', tags: ['发改委', '行动计划', '算力'], hot: false, newsDate: new Date(now.getTime() - 14 * 3600000) },
|
||||
{ title: '中国企业转向本土 AI 芯片,英伟达份额持续萎缩', summary: '彭博社报告显示,46% 的企业 AI 加速器采购将投向国产产品,80% 高管表示 AI 基础设施投入超出预算。', source: '彭博社', sourceUrl: 'https://bloomberg.com', category: '公司', tags: ['芯片', '英伟达', '国产替代'], hot: false, newsDate: new Date(now.getTime() - 16 * 3600000) },
|
||||
{ title: '云从科技推出政务大模型一体机', summary: '云从科技发布政务大模型一体机,内置 DeepSeek R1 蒸馏模型,助力政府部门本地化部署 AI 能力。', source: '36氪', sourceUrl: 'https://36kr.com', category: '产品', tags: ['云从科技', '政务', '一体机'], hot: false, newsDate: new Date(now.getTime() - 18 * 3600000) },
|
||||
{ title: '全球首款 AI 智能体手机将在 WAIC 2026 首秀', summary: '这款手机内置 Agent 操作系统,用户可通过自然语言指令让手机自动完成复杂任务,标志着端侧 AI 从助手进化为管家。', source: '界面新闻', sourceUrl: 'https://jiemian.com', category: '产品', tags: ['AI手机', '智能体', 'WAIC'], hot: false, newsDate: new Date(now.getTime() - 20 * 3600000) },
|
||||
{ title: '2026 AI Agent 六大趋势发布:语音 AI 加速崛起', summary: 'CB Insights 报告显示 82% 企业将在 12 个月内将 Agent 用于客户支持,Agent 初创公司平均成立 3.8 年即实现规模化收入。', source: '36氪', sourceUrl: 'https://36kr.com', category: '论文', tags: ['Agent', '趋势', '报告'], hot: false, newsDate: new Date(now.getTime() - 22 * 3600000) },
|
||||
]
|
||||
|
||||
// Clear old trends (optional)
|
||||
const oldTrends = await db.collection('trends').countDocuments()
|
||||
if (oldTrends > 0) {
|
||||
await db.collection('trends').deleteMany({})
|
||||
console.log(`🗑️ 已清空 ${oldTrends} 条旧趋势数据`)
|
||||
}
|
||||
|
||||
const trendResult = await db.collection('trends').insertMany(trends.map(t => ({
|
||||
...t,
|
||||
picked: false,
|
||||
viewCount: 0,
|
||||
status: 'published',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})))
|
||||
console.log(`📡 已插入 ${trendResult.insertedCount} 条趋势数据`)
|
||||
|
||||
// ============================================================
|
||||
// 4. 最终统计
|
||||
// ============================================================
|
||||
const knowledgeTotal = await db.collection('knowledges').countDocuments()
|
||||
const trendTotal = await db.collection('trends').countDocuments()
|
||||
console.log(`\n📊 最终统计:`)
|
||||
console.log(` 知识库: ${knowledgeTotal} 条`)
|
||||
console.log(` 趋势: ${trendTotal} 条`)
|
||||
|
||||
// Per-dimension count
|
||||
for (let d = 1; d <= 5; d++) {
|
||||
const n = await db.collection('knowledges').countDocuments({ dim: d })
|
||||
console.log(` D${d}: ${n} 条`)
|
||||
}
|
||||
|
||||
await mongoose.disconnect()
|
||||
console.log('\n✅ 内容丰富完成!')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ 脚本执行失败:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* 批量生成拼音音频URL脚本
|
||||
* 支持多种TTS服务:有道词典、百度语音、科大讯飞等
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 拼音列表
|
||||
const initials = ['b','p','m','f','d','t','n','l','g','k','h','j','q','x','zh','ch','sh','r','z','c','s','y','w'];
|
||||
const finals = ['a','o','e','i','u','ü','ai','ei','ui','ao','ou','iu','ie','üe','er','an','en','in','un','ün','ang','eng','ing','ong'];
|
||||
const overalls = ['zhi','chi','shi','ri','zi','ci','si','yi','wu','yu','ye','yue','yuan','yin','yun','ying'];
|
||||
|
||||
/**
|
||||
* 有道词典TTS服务
|
||||
* 优点:免费、无需注册、支持中文
|
||||
* 缺点:音质一般
|
||||
*/
|
||||
function getYoudaoUrl(text) {
|
||||
// 从环境变量读取TTS基础URL
|
||||
const ttsBaseUrl = process.env.TTS_BASE_URL || 'https://dict.youdao.com/dictvoice';
|
||||
return `${ttsBaseUrl}?audio=${encodeURIComponent(text)}&type=1`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成所有拼音的音频URL配置
|
||||
*/
|
||||
function generateAudioConfig() {
|
||||
// 从环境变量读取配置
|
||||
const ttsProvider = process.env.TTS_PROVIDER || 'youdao';
|
||||
const ttsBaseUrl = process.env.TTS_BASE_URL || 'https://dict.youdao.com/dictvoice';
|
||||
|
||||
const config = {
|
||||
// TTS服务提供商
|
||||
provider: ttsProvider,
|
||||
// 基础URL
|
||||
baseUrl: ttsBaseUrl,
|
||||
// 所有拼音的音频URL
|
||||
audios: {}
|
||||
};
|
||||
|
||||
// 声母
|
||||
initials.forEach(symbol => {
|
||||
config.audios[symbol] = {
|
||||
url: getYoudaoUrl(symbol),
|
||||
name: getPinyinName(symbol),
|
||||
type: 'initial'
|
||||
};
|
||||
});
|
||||
|
||||
// 韵母
|
||||
finals.forEach(symbol => {
|
||||
config.audios[symbol] = {
|
||||
url: getYoudaoUrl(symbol),
|
||||
name: getPinyinName(symbol),
|
||||
type: 'final'
|
||||
};
|
||||
});
|
||||
|
||||
// 整体认读
|
||||
overalls.forEach(symbol => {
|
||||
config.audios[symbol] = {
|
||||
url: getYoudaoUrl(symbol),
|
||||
name: getPinyinName(symbol),
|
||||
type: 'overall'
|
||||
};
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拼音名称
|
||||
*/
|
||||
function getPinyinName(symbol) {
|
||||
const nameMap = {
|
||||
'b': '玻', 'p': '坡', 'm': '摸', 'f': '佛',
|
||||
'd': '得', 't': '特', 'n': '讷', 'l': '勒',
|
||||
'g': '哥', 'k': '科', 'h': '喝',
|
||||
'j': '基', 'q': '欺', 'x': '希',
|
||||
'zh': '知', 'ch': '蚩', 'sh': '诗', 'r': '日',
|
||||
'z': '资', 'c': '雌', 's': '思',
|
||||
'y': '医', 'w': '巫',
|
||||
'a': '啊', 'o': '喔', 'e': '鹅', 'i': '衣', 'u': '乌', 'ü': '迂',
|
||||
'ai': '哀', 'ei': '诶', 'ui': '威', 'ao': '熬', 'ou': '欧',
|
||||
'iu': '优', 'ie': '耶', 'üe': '约', 'er': '儿',
|
||||
'an': '安', 'en': '恩', 'in': '因', 'un': '温', 'ün': '晕',
|
||||
'ang': '昂', 'eng': '亨', 'ing': '英', 'ong': '雍',
|
||||
'zhi': '织', 'chi': '吃', 'shi': '狮', 'ri': '日',
|
||||
'zi': '资', 'ci': '疵', 'si': '丝',
|
||||
'yi': '衣', 'wu': '乌', 'yu': '迂',
|
||||
'ye': '耶', 'yue': '约', 'yuan': '冤',
|
||||
'yin': '因', 'yun': '晕', 'ying': '英'
|
||||
};
|
||||
return nameMap[symbol] || symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成SQL更新语句(用于直接更新数据库)
|
||||
*/
|
||||
function generateSqlUpdates() {
|
||||
const config = generateAudioConfig();
|
||||
const sqls = [];
|
||||
|
||||
Object.entries(config.audios).forEach(([symbol, data]) => {
|
||||
const sql = `UPDATE pinyin_contents SET audio_url = '${data.url}' WHERE symbol = '${symbol}';`;
|
||||
sqls.push(sql);
|
||||
});
|
||||
|
||||
return sqls.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MongoDB更新脚本
|
||||
*/
|
||||
function generateMongoScript() {
|
||||
const config = generateAudioConfig();
|
||||
const updates = [];
|
||||
|
||||
Object.entries(config.audios).forEach(([symbol, data]) => {
|
||||
updates.push({
|
||||
updateOne: {
|
||||
filter: { symbol: symbol },
|
||||
update: { $set: { audioUrl: data.url } }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return `db.pinyincontents.bulkWrite(${JSON.stringify(updates, null, 2)});`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置文件
|
||||
*/
|
||||
function saveConfig() {
|
||||
const config = generateAudioConfig();
|
||||
const outputPath = path.join(__dirname, '../../config/pinyin-audio-config.json');
|
||||
|
||||
// 确保目录存在
|
||||
const dir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2));
|
||||
console.log(`✅ 音频配置已保存到: ${outputPath}`);
|
||||
|
||||
// 生成SQL文件
|
||||
const sqlPath = path.join(__dirname, '../../config/update-audio-urls.sql');
|
||||
fs.writeFileSync(sqlPath, generateSqlUpdates());
|
||||
console.log(`✅ SQL更新脚本已保存到: ${sqlPath}`);
|
||||
|
||||
// 生成MongoDB脚本
|
||||
const mongoPath = path.join(__dirname, '../../config/update-audio-urls.js');
|
||||
fs.writeFileSync(mongoPath, generateMongoScript());
|
||||
console.log(`✅ MongoDB更新脚本已保存到: ${mongoPath}`);
|
||||
|
||||
// 输出统计
|
||||
console.log('\n📊 生成统计:');
|
||||
console.log(` - 声母: ${initials.length} 个`);
|
||||
console.log(` - 韵母: ${finals.length} 个`);
|
||||
console.log(` - 整体认读: ${overalls.length} 个`);
|
||||
console.log(` - 总计: ${initials.length + finals.length + overalls.length} 个拼音`);
|
||||
}
|
||||
|
||||
// 运行
|
||||
if (require.main === module) {
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateAudioConfig,
|
||||
generateSqlUpdates,
|
||||
generateMongoScript,
|
||||
getYoudaoUrl
|
||||
};
|
||||
@@ -1,210 +0,0 @@
|
||||
/**
|
||||
* AI 知识库初始化脚本
|
||||
* 为 5 个 AI 维度填充知识内容
|
||||
*
|
||||
* 用法:
|
||||
* cd /root/Hermes-workspace/yuzhiran-ai-dimension/backend/wdkj-server
|
||||
* node src/scripts/init-knowledge-ai.js
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const mongoose = require('mongoose');
|
||||
const Knowledge = require('../models/Knowledge');
|
||||
|
||||
// 数据库连接
|
||||
const DB_URI = process.env.MONGODB_URI || process.env.DB_URI || 'mongodb://127.0.0.1:27017/wdkj';
|
||||
|
||||
async function main() {
|
||||
console.log('[AI知识初始化] 开始连接数据库...');
|
||||
await mongoose.connect(DB_URI);
|
||||
console.log('[AI知识初始化] 数据库已连接:', DB_URI);
|
||||
|
||||
// 检查现有数据
|
||||
const existing = await Knowledge.countDocuments();
|
||||
if (existing > 0) {
|
||||
console.log(`[AI知识初始化] 发现 ${existing} 条现有记录`);
|
||||
console.log('[AI知识初始化] 继续追加(如需清空请先手动删除)');
|
||||
}
|
||||
|
||||
// 维度数据:key = dim 编号 (1-5)
|
||||
const dimensionData = {
|
||||
1: [
|
||||
{
|
||||
title: '图灵测试:人工智能的哲学起点',
|
||||
content: '1950 年,英国数学家艾伦·图灵发表了划时代的论文《计算机器与智能》,提出了著名的"图灵测试"——如果一台机器能与人类进行对话而不被辨别出机器身份,就可以认为它具备智能。图灵测试不是技术标准,而是哲学思辨:"机器能思考吗?"这个问题开启了 AI 研究的序幕。',
|
||||
dim: 1, category: 'history', tags: ['图灵测试', '艾伦·图灵', 'AI 哲学'],
|
||||
isPremium: false, sortOrder: 1, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '达特茅斯会议:AI 的诞生',
|
||||
content: '1956 年夏天,约翰·麦卡锡、马文·明斯基、克劳德·香农等科学家在达特茅斯学院聚会,首次提出"Artificial Intelligence"(人工智能)一词,标志着 AI 作为独立学科的正式诞生。会议上定义了 AI 的核心目标:让机器模拟人类智能行为——学习、推理、感知、决策。',
|
||||
dim: 1, category: 'history', tags: ['达特茅斯', '麦卡锡', 'AI 起源'],
|
||||
isPremium: false, sortOrder: 2, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '深蓝击败卡斯帕罗夫(1997)',
|
||||
content: '1997 年 5 月,IBM 的深蓝(Deep Blue)在国际象棋比赛中以 3.5:2.5 击败世界冠军加里·卡斯帕罗夫。这是 AI 第一次在正式比赛中战胜人类世界冠军,具有里程碑意义。深蓝的胜利展示了暴力搜索和硬编码知识的威力,也引发了对 AI 威胁论的激烈讨论。',
|
||||
dim: 1, category: 'history', tags: ['深蓝', 'IBM', '卡斯帕罗夫'],
|
||||
isPremium: false, sortOrder: 3, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'AlphaGo 与深度学习革命(2016)',
|
||||
content: '2016 年 3 月,Google DeepMind 的 AlphaGo 以 4:1 击败围棋世界冠军李世石。围棋曾被视为人类智力的最后堡垒,AlphaGo 的胜利震惊世界。AlphaGo 结合了深度神经网络和蒙特卡洛树搜索,开创了 AI 的新纪元,直接推动了深度学习在各行各业的爆发式应用。',
|
||||
dim: 1, category: 'history', tags: ['AlphaGo', '深度学习', 'DeepMind'],
|
||||
isPremium: true, sortOrder: 4, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '从 GPT 到 DeepSeek:大语言模型时代',
|
||||
content: '2018 年 OpenAI 发布 GPT-1,开启了基于 Transformer 的预训练范式。2022 年 ChatGPT 引爆全球,展示了生成式 AI 的惊人能力。2024-2026 年,以 DeepSeek、Llama、Qwen 为代表的开源大模型迅速崛起,AI 真正走进千家万户,成为生产力工具。',
|
||||
dim: 1, category: 'history', tags: ['GPT', 'DeepSeek', '大语言模型'],
|
||||
isPremium: true, sortOrder: 5, status: 'approved'
|
||||
}
|
||||
],
|
||||
|
||||
2: [
|
||||
{
|
||||
title: '机器学习与深度学习基础',
|
||||
content: '机器学习是 AI 的核心分支,让计算机从数据中自动学习规律,而不需要显式编程。深度学习是机器学习的一个子集,使用多层神经网络来处理复杂模式。从 2012 年 AlexNet 在 ImageNet 夺冠开始,深度学习席卷所有领域。',
|
||||
dim: 2, category: 'concept', tags: ['机器学习', '深度学习', '神经网络'],
|
||||
isPremium: false, sortOrder: 1, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'Transformer:改变一切的架构',
|
||||
content: '2017 年,Google 在论文《Attention Is All You Need》中提出 Transformer 架构,核心是自注意力机制(Self-Attention),让模型可以并行处理序列,突破 RNN 的瓶颈。Transformer 成为后来所有大模型(GPT、BERT、T5)的基础,是 AI 历史上最重要的架构创新之一。',
|
||||
dim: 2, category: 'concept', tags: ['Transformer', '注意力机制', 'Google'],
|
||||
isPremium: false, sortOrder: 2, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '从 BERT 到 GPT:预训练模型的演进',
|
||||
content: 'BERT(2018)展示了双向编码器的强大,GPT(2018)展示了自回归生成的能力。随着模型规模指数级增长(参数从亿到万亿),涌现出前所未有的能力——这就是规模定律(Scaling Law)。GPT 系列代表了一条路:预训练 + 指令微调 + RLHF,成为今天大语言模型的标准配方。',
|
||||
dim: 2, category: 'application', tags: ['BERT', 'GPT', '预训练'],
|
||||
isPremium: false, sortOrder: 3, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '多模态 AI:从文本到全感官',
|
||||
content: '2022 年后,AI 不再局限于文本。DALL·E、Midjourney、Stable Diffusion 让 AI 绘画普及;Sora、Kling 让 AI 生成视频成为现实;Suno 让 AI 创作音乐。多模态(Multimodal)成为大模型的新标准:一个模型同时理解文本、图像、声音,打通感官壁垒。',
|
||||
dim: 2, category: 'application', tags: ['多模态', '文生图', 'AI 视频'],
|
||||
isPremium: true, sortOrder: 4, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'MoE 架构:万亿参数的经济之选',
|
||||
content: '混合专家模型(Mixture of Experts, MoE)让模型参数规模达到万亿级别,但推理时只激活部分参数,显著提升性能同时控制成本。GPT-4、Mixtral、DeepSeek 等均已采用 MoE,成为大模型追求性能的终极架构。',
|
||||
dim: 2, category: 'concept', tags: ['MoE', '万亿参数', '架构'],
|
||||
isPremium: true, sortOrder: 5, status: 'approved'
|
||||
}
|
||||
],
|
||||
|
||||
3: [
|
||||
{
|
||||
title: 'OpenAI vs Anthropic:闭源双雄争霸',
|
||||
content: 'OpenAI 的 GPT-4/4o 系列占据高端市场,Anthropic 的 Claude 3 系列以安全和推理见长。两者代表了闭源大模型的最高水平,但商业化策略不同:OpenAI 走 B2B2C,Anthropic 专注企业市场。',
|
||||
dim: 3, category: 'application', tags: ['OpenAI', 'Anthropic', '闭源模型'],
|
||||
isPremium: false, sortOrder: 1, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'DeepSeek:中国开源力量的崛起',
|
||||
content: 'DeepSeek(深度求索)是中国 AI 公司的代表,其 DeepSeek-V3 在多项评测中逼近 GPT-4,同时大力开源(DeepSeek-V2、Coder 系列)。DeepSeek 展示了中国团队在大模型领域的技术实力。',
|
||||
dim: 3, category: 'application', tags: ['DeepSeek', '中国 AI', '开源'],
|
||||
isPremium: false, sortOrder: 2, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '开源 vs 闭源:生态之争',
|
||||
content: '闭源模型(OpenAI、Anthropic)性能领先但成本高、可控性差;开源模型(Llama 3、DeepSeek、Qwen)灵活部署、可定制,生态快速繁荣。2026 年,开源生态已形成完整工具链(vLLM、Ollama、Hugging Face),成为中小企业首选。',
|
||||
dim: 3, category: 'concept', tags: ['开源', '闭源', '生态'],
|
||||
isPremium: false, sortOrder: 3, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'Agent 智能体:从 Chatbot 到自主智能',
|
||||
content: 'Agent 是新一代 AI 形态,不仅能对话,还能规划、使用工具、执行任务。AutoGPT、MetaGPT 展示了潜力;Claude 的 Tool Use、OpenAI 的 GPTs 让 Agent 更实用。Agent 是通向 AGI 的关键路径。',
|
||||
dim: 3, category: 'application', tags: ['Agent', '自主智能'],
|
||||
isPremium: true, sortOrder: 4, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '具身智能与世界模型',
|
||||
content: '具身智能(Embodied AI)让 AI 拥有"身体",能够感知和操作物理世界。Google 的 RT-2、DeepMind 的 RoboCat 展示了机器人与大模型结合的前景。世界模型(World Model)让 AI 理解物理规律,模拟环境,是具身智能的核心。',
|
||||
dim: 3, category: 'concept', tags: ['具身智能', '世界模型', '机器人'],
|
||||
isPremium: true, sortOrder: 5, status: 'approved'
|
||||
}
|
||||
],
|
||||
|
||||
4: [
|
||||
{
|
||||
title: '提示词工程入门',
|
||||
content: '好的 prompt 能显著提升 AI 输出质量。提示词工程包括:角色设定、任务描述、输出格式、few-shot 示例、思维链(Chain-of-Thought)。学会写 prompt,是使用大模型的基本功。',
|
||||
dim: 4, category: 'concept', tags: ['提示词', 'Prompt Engineering'],
|
||||
isPremium: false, sortOrder: 1, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '检索增强生成(RAG)详解',
|
||||
content: 'RAG (Retrieval-Augmented Generation) 将外部知识库与 LLM 结合,解决大模型知识滞后和幻觉问题。流程:用户提问 → 向量检索相关文档 → 拼接 prompt → LLM 生成答案。RAG 是企业知识库 AI 化的核心技术。',
|
||||
dim: 4, category: 'concept', tags: ['RAG', '检索增强', '知识库'],
|
||||
isPremium: false, sortOrder: 2, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: '模型微调(Fine-tuning)实战',
|
||||
content: '微调是在预训练模型基础上用领域数据进一步训练,让模型适应特定任务。常用方法:全量微调、LoRA(低秩适应)、QLoRA(量化 LoRA)。小数据也能做微调,但要注意过拟合和灾难性遗忘。',
|
||||
dim: 4, category: 'application', tags: ['Fine-tuning', 'LoRA', '微调'],
|
||||
isPremium: true, sortOrder: 3, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'LangChain:AI 应用开发框架',
|
||||
content: 'LangChain 是最流行的 LLM 应用框架,提供模型调用、提示模板、记忆管理、工具链、Agent 等模块,大大降低开发难度。用它快速搭建 RAG 应用、聊天机器人、数据分析工具。',
|
||||
dim: 4, category: 'application', tags: ['LangChain', '开发框架'],
|
||||
isPremium: true, sortOrder: 4, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'Dify:无代码 AI 平台',
|
||||
content: 'Dify 是开源的 LLMOps 平台,可视化编排 AI 工作流,支持 RAG、Agent、知识库,无需写代码即可构建企业级 AI 应用。适合业务人员快速落地 AI 解决方案。',
|
||||
dim: 4, category: 'application', tags: ['Dify', '低代码'],
|
||||
isPremium: true, sortOrder: 5, status: 'approved'
|
||||
}
|
||||
],
|
||||
|
||||
5: [
|
||||
{
|
||||
title: 'WAIC 2026:世界人工智能大会即将开幕',
|
||||
content: '2026 年世界人工智能大会(WAIC)将于 7 月 17 日在上海开幕,预计将有 1100+ 企业参展,发布众多前沿技术和产品。WAIC 是了解 AI 趋势的绝佳窗口。',
|
||||
dim: 5, category: 'concept', tags: ['WAIC', '会议'],
|
||||
isPremium: false, sortOrder: 1, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'AI 法规与伦理:全球监管趋势',
|
||||
content: '各国正在加快 AI 立法:欧盟 AI Act 已生效,美国 NIST AI RMF 发布,中国《生成式 AI 服务管理暂行办法》持续完善。安全、可控、可信成为 AI 发展的重要议题。',
|
||||
dim: 5, category: 'concept', tags: ['AI 伦理', '法规'],
|
||||
isPremium: false, sortOrder: 2, status: 'approved'
|
||||
},
|
||||
{
|
||||
title: 'Agent 将如何改变软件交互方式?',
|
||||
content: 'Agent 让软件从"人找功能"变为"AI 帮我做事"。未来的办公软件、设计工具、数据分析平台都将以 Agent 为核心交互。一些专家预测:2027 年 Agent 将替代 30% 的重复性办公任务。',
|
||||
dim: 5, category: 'application', tags: ['Agent', '未来趋势'],
|
||||
isPremium: true, sortOrder: 3, status: 'approved'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const dims = [1, 2, 3, 4, 5];
|
||||
let totalInserted = 0;
|
||||
|
||||
for (const dim of dims) {
|
||||
const entries = dimensionData[dim];
|
||||
if (!entries) continue;
|
||||
console.log(`[维度${dim}] 开始插入 ${entries.length} 条知识...`);
|
||||
try {
|
||||
const result = await Knowledge.insertMany(entries);
|
||||
totalInserted += result.length;
|
||||
console.log(`[维度${dim}] 已插入 ${result.length} 条`);
|
||||
} catch (err) {
|
||||
console.error(`[维度${dim}] 插入失败:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const total = await Knowledge.countDocuments();
|
||||
console.log(`✅ 完成!Knowledge 库现有 ${total} 条记录(本次新增 ${totalInserted} 条)`);
|
||||
await mongoose.disconnect();
|
||||
console.log('数据库连接已关闭');
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ 初始化失败:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,395 +0,0 @@
|
||||
/**
|
||||
* 拼音数据初始化脚本
|
||||
* 创建所有63个拼音的基础数据
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const mongoose = require('mongoose');
|
||||
const { PinyinContent } = require('../models/pinyin');
|
||||
const { PinyinAchievement } = require('../models/pinyin');
|
||||
|
||||
// 声母表 (23个)
|
||||
const initials = [
|
||||
{ symbol: 'b', name: '玻', order: 1 },
|
||||
{ symbol: 'p', name: '坡', order: 2 },
|
||||
{ symbol: 'm', name: '摸', order: 3 },
|
||||
{ symbol: 'f', name: '佛', order: 4 },
|
||||
{ symbol: 'd', name: '得', order: 5 },
|
||||
{ symbol: 't', name: '特', order: 6 },
|
||||
{ symbol: 'n', name: '讷', order: 7 },
|
||||
{ symbol: 'l', name: '勒', order: 8 },
|
||||
{ symbol: 'g', name: '哥', order: 9 },
|
||||
{ symbol: 'k', name: '科', order: 10 },
|
||||
{ symbol: 'h', name: '喝', order: 11 },
|
||||
{ symbol: 'j', name: '基', order: 12 },
|
||||
{ symbol: 'q', name: '欺', order: 13 },
|
||||
{ symbol: 'x', name: '希', order: 14 },
|
||||
{ symbol: 'zh', name: '知', order: 15 },
|
||||
{ symbol: 'ch', name: '蚩', order: 16 },
|
||||
{ symbol: 'sh', name: '诗', order: 17 },
|
||||
{ symbol: 'r', name: '日', order: 18 },
|
||||
{ symbol: 'z', name: '资', order: 19 },
|
||||
{ symbol: 'c', name: '雌', order: 20 },
|
||||
{ symbol: 's', name: '思', order: 21 },
|
||||
{ symbol: 'y', name: '医', order: 22 },
|
||||
{ symbol: 'w', name: '巫', order: 23 }
|
||||
];
|
||||
|
||||
// 韵母表 (24个)
|
||||
const finals = [
|
||||
{ symbol: 'a', name: '啊', order: 1 },
|
||||
{ symbol: 'o', name: '喔', order: 2 },
|
||||
{ symbol: 'e', name: '鹅', order: 3 },
|
||||
{ symbol: 'i', name: '衣', order: 4 },
|
||||
{ symbol: 'u', name: '乌', order: 5 },
|
||||
{ symbol: 'ü', name: '迂', order: 6 },
|
||||
{ symbol: 'ai', name: '哀', order: 7 },
|
||||
{ symbol: 'ei', name: '诶', order: 8 },
|
||||
{ symbol: 'ui', name: '威', order: 9 },
|
||||
{ symbol: 'ao', name: '熬', order: 10 },
|
||||
{ symbol: 'ou', name: '欧', order: 11 },
|
||||
{ symbol: 'iu', name: '优', order: 12 },
|
||||
{ symbol: 'ie', name: '耶', order: 13 },
|
||||
{ symbol: 'üe', name: '约', order: 14 },
|
||||
{ symbol: 'er', name: '儿', order: 15 },
|
||||
{ symbol: 'an', name: '安', order: 16 },
|
||||
{ symbol: 'en', name: '恩', order: 17 },
|
||||
{ symbol: 'in', name: '因', order: 18 },
|
||||
{ symbol: 'un', name: '温', order: 19 },
|
||||
{ symbol: 'ün', name: '晕', order: 20 },
|
||||
{ symbol: 'ang', name: '昂', order: 21 },
|
||||
{ symbol: 'eng', name: '亨', order: 22 },
|
||||
{ symbol: 'ing', name: '英', order: 23 },
|
||||
{ symbol: 'ong', name: '雍', order: 24 }
|
||||
];
|
||||
|
||||
// 整体认读音节 (16个)
|
||||
const overalls = [
|
||||
{ symbol: 'zhi', name: '织', order: 1 },
|
||||
{ symbol: 'chi', name: '吃', order: 2 },
|
||||
{ symbol: 'shi', name: '狮', order: 3 },
|
||||
{ symbol: 'ri', name: '日', order: 4 },
|
||||
{ symbol: 'zi', name: '资', order: 5 },
|
||||
{ symbol: 'ci', name: '疵', order: 6 },
|
||||
{ symbol: 'si', name: '丝', order: 7 },
|
||||
{ symbol: 'yi', name: '衣', order: 8 },
|
||||
{ symbol: 'wu', name: '乌', order: 9 },
|
||||
{ symbol: 'yu', name: '迂', order: 10 },
|
||||
{ symbol: 'ye', name: '耶', order: 11 },
|
||||
{ symbol: 'yue', name: '约', order: 12 },
|
||||
{ symbol: 'yuan', name: '冤', order: 13 },
|
||||
{ symbol: 'yin', name: '因', order: 14 },
|
||||
{ symbol: 'yun', name: '晕', order: 15 },
|
||||
{ symbol: 'ying', name: '英', order: 16 }
|
||||
];
|
||||
|
||||
// 示例词语数据
|
||||
const sampleWords = {
|
||||
'b': [
|
||||
{ word: '爸爸', pinyin: 'bàba', meaning: '父亲' },
|
||||
{ word: '杯子', pinyin: 'bēizi', meaning: '装水的器具' },
|
||||
{ word: '白云', pinyin: 'báiyún', meaning: '白色的云' }
|
||||
],
|
||||
'a': [
|
||||
{ word: '妈妈', pinyin: 'māma', meaning: '母亲' },
|
||||
{ word: '阿姨', pinyin: 'āyí', meaning: '母亲的姐妹' }
|
||||
],
|
||||
'zhi': [
|
||||
{ word: '知道', pinyin: 'zhīdào', meaning: '了解' },
|
||||
{ word: '蜘蛛', pinyin: 'zhīzhū', meaning: '一种昆虫' }
|
||||
]
|
||||
};
|
||||
|
||||
// 发音方法说明
|
||||
const pronunciationGuides = {
|
||||
'b': '双唇紧闭,阻碍气流,然后双唇突然放开,让气流冲出,读音轻短。',
|
||||
'p': '双唇紧闭,阻碍气流,然后双唇突然放开,气流较强地冲出。',
|
||||
'm': '双唇紧闭,软腭下降,气流从鼻腔出来,声带振动。',
|
||||
'f': '上齿接触下唇,形成缝隙,气流从缝隙中摩擦出来。',
|
||||
'a': '嘴巴张大,舌头放平,舌位低,声音响亮。',
|
||||
'o': '嘴巴圆圆,舌头后缩,舌位半高。'
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成音频URL(从环境变量读取TTS配置)
|
||||
*/
|
||||
function getAudioUrl(symbol) {
|
||||
// 从环境变量读取TTS基础URL,默认使用有道词典
|
||||
const ttsBaseUrl = process.env.TTS_BASE_URL || 'https://dict.youdao.com/dictvoice';
|
||||
const ttsProvider = process.env.TTS_PROVIDER || 'youdao';
|
||||
|
||||
// 如果使用自定义CDN
|
||||
if (ttsProvider === 'custom' && process.env.TTS_CUSTOM_CDN_URL) {
|
||||
return `${process.env.TTS_CUSTOM_CDN_URL}/${encodeURIComponent(symbol)}.mp3`;
|
||||
}
|
||||
|
||||
// 默认使用有道词典格式
|
||||
return `${ttsBaseUrl}?audio=${encodeURIComponent(symbol)}&type=1`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成口型图URL(从环境变量读取CDN配置)
|
||||
*/
|
||||
function getMouthImage(symbol) {
|
||||
// 从环境变量读取口型图CDN
|
||||
const mouthImageCdn = process.env.MOUTH_IMAGE_CDN_URL;
|
||||
|
||||
if (mouthImageCdn) {
|
||||
return `${mouthImageCdn}/mouth_${encodeURIComponent(symbol)}.png`;
|
||||
}
|
||||
|
||||
// 默认返回空,由管理后台上传
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建拼音内容数据
|
||||
*/
|
||||
async function createPinyinContents() {
|
||||
const contents = [];
|
||||
|
||||
// 处理声母
|
||||
for (const item of initials) {
|
||||
const words = sampleWords[item.symbol] || [
|
||||
{ word: item.name, pinyin: item.symbol, meaning: '示例词语' }
|
||||
];
|
||||
|
||||
contents.push({
|
||||
symbol: item.symbol,
|
||||
type: 'initial',
|
||||
name: item.name,
|
||||
order: item.order,
|
||||
audioUrl: getAudioUrl(item.symbol),
|
||||
mouthImage: getMouthImage(item.symbol),
|
||||
pronunciation: pronunciationGuides[item.symbol] || `${item.name}的发音`,
|
||||
isFree: item.order <= 5, // 前5个免费
|
||||
words: words.map(w => ({
|
||||
...w,
|
||||
audioUrl: getAudioUrl(w.word),
|
||||
image: ''
|
||||
})),
|
||||
exploreAreas: {
|
||||
audio: true,
|
||||
mouth: true,
|
||||
speak: true,
|
||||
write: true,
|
||||
game: true,
|
||||
words: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理韵母
|
||||
for (const item of finals) {
|
||||
const words = sampleWords[item.symbol] || [
|
||||
{ word: item.name, pinyin: item.symbol, meaning: '示例词语' }
|
||||
];
|
||||
|
||||
contents.push({
|
||||
symbol: item.symbol,
|
||||
type: 'final',
|
||||
name: item.name,
|
||||
order: item.order,
|
||||
audioUrl: getAudioUrl(item.symbol),
|
||||
mouthImage: getMouthImage(item.symbol),
|
||||
pronunciation: pronunciationGuides[item.symbol] || `${item.name}的发音`,
|
||||
isFree: item.order <= 3, // 前3个免费
|
||||
words: words.map(w => ({
|
||||
...w,
|
||||
audioUrl: getAudioUrl(w.word),
|
||||
image: ''
|
||||
})),
|
||||
exploreAreas: {
|
||||
audio: true,
|
||||
mouth: true,
|
||||
speak: true,
|
||||
write: true,
|
||||
game: true,
|
||||
words: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理整体认读
|
||||
for (const item of overalls) {
|
||||
const words = sampleWords[item.symbol] || [
|
||||
{ word: item.name, pinyin: item.symbol, meaning: '示例词语' }
|
||||
];
|
||||
|
||||
contents.push({
|
||||
symbol: item.symbol,
|
||||
type: 'overall',
|
||||
name: item.name,
|
||||
order: item.order,
|
||||
audioUrl: getAudioUrl(item.symbol),
|
||||
mouthImage: getMouthImage(item.symbol),
|
||||
pronunciation: pronunciationGuides[item.symbol] || `${item.name}的发音`,
|
||||
isFree: item.order <= 2, // 前2个免费
|
||||
words: words.map(w => ({
|
||||
...w,
|
||||
audioUrl: getAudioUrl(w.word),
|
||||
image: ''
|
||||
})),
|
||||
exploreAreas: {
|
||||
audio: true,
|
||||
mouth: true,
|
||||
speak: true,
|
||||
write: true,
|
||||
game: true,
|
||||
words: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成就数据
|
||||
*/
|
||||
async function createAchievements() {
|
||||
const achievements = [
|
||||
{
|
||||
code: 'first_explore',
|
||||
name: '初次探索',
|
||||
description: '完成第一次拼音探索',
|
||||
type: 'explore',
|
||||
icon: 'rocket',
|
||||
condition: { type: 'explore_count', value: 1 },
|
||||
reward: { type: 'stone', value: 1 },
|
||||
order: 1,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
code: 'explore_10',
|
||||
name: '探索新手',
|
||||
description: '累计探索10个拼音',
|
||||
type: 'explore',
|
||||
icon: 'star',
|
||||
condition: { type: 'explore_count', value: 10 },
|
||||
reward: { type: 'stone', value: 5 },
|
||||
order: 2,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
code: 'explore_50',
|
||||
name: '探索达人',
|
||||
description: '累计探索50个拼音',
|
||||
type: 'explore',
|
||||
icon: 'trophy',
|
||||
condition: { type: 'explore_count', value: 50 },
|
||||
reward: { type: 'stone', value: 20 },
|
||||
order: 3,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
code: 'collect_10',
|
||||
name: '收集者',
|
||||
description: '收集10个能量石',
|
||||
type: 'collection',
|
||||
icon: 'gem',
|
||||
condition: { type: 'collect_count', value: 10 },
|
||||
reward: { type: 'stone', value: 5 },
|
||||
order: 4,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
code: 'collect_all',
|
||||
name: '收集大师',
|
||||
description: '收集所有63个能量石',
|
||||
type: 'collection',
|
||||
icon: 'crown',
|
||||
condition: { type: 'collect_count', value: 63 },
|
||||
reward: { type: 'stone', value: 50 },
|
||||
order: 5,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
code: 'streak_7',
|
||||
name: '坚持一周',
|
||||
description: '连续7天进行拼音探索',
|
||||
type: 'streak',
|
||||
icon: 'fire',
|
||||
condition: { type: 'streak_days', value: 7 },
|
||||
reward: { type: 'stone', value: 10 },
|
||||
order: 6,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
code: 'complete_symbol_b',
|
||||
name: 'b的探索者',
|
||||
description: '完成拼音b的所有探索',
|
||||
type: 'special',
|
||||
icon: 'check-circle',
|
||||
condition: { type: 'complete_symbol', value: 1, symbol: 'b' },
|
||||
reward: { type: 'stone', value: 2 },
|
||||
order: 7,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
code: 'game_master',
|
||||
name: '游戏高手',
|
||||
description: '趣味互动累计获得1000分',
|
||||
type: 'special',
|
||||
icon: 'gamepad',
|
||||
condition: { type: 'game_score', value: 1000 },
|
||||
reward: { type: 'stone', value: 15 },
|
||||
order: 8,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
return achievements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化拼音数据
|
||||
*/
|
||||
async function initPinyinData() {
|
||||
try {
|
||||
// 连接数据库
|
||||
const mongoURI = process.env.MONGODB_URI ||
|
||||
`mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_HOST}:${process.env.MONGODB_PORT}/${process.env.MONGODB_DB}?authSource=${process.env.MONGODB_DB}`;
|
||||
await mongoose.connect(mongoURI);
|
||||
console.log('数据库连接成功');
|
||||
|
||||
// 清空现有数据(可选,生产环境慎用)
|
||||
const clearExisting = process.env.CLEAR_EXISTING === 'true';
|
||||
if (clearExisting) {
|
||||
await PinyinContent.deleteMany({});
|
||||
await PinyinAchievement.deleteMany({});
|
||||
console.log('已清空现有数据');
|
||||
}
|
||||
|
||||
// 检查是否已有数据
|
||||
const existingCount = await PinyinContent.countDocuments();
|
||||
if (existingCount > 0 && !clearExisting) {
|
||||
console.log(`数据库中已有 ${existingCount} 条拼音数据,跳过初始化`);
|
||||
console.log('如需重新初始化,请设置环境变量 CLEAR_EXISTING=true');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 创建拼音内容
|
||||
const contents = await createPinyinContents();
|
||||
await PinyinContent.insertMany(contents);
|
||||
console.log(`成功创建 ${contents.length} 个拼音内容`);
|
||||
|
||||
// 创建成就
|
||||
const achievements = await createAchievements();
|
||||
await PinyinAchievement.insertMany(achievements);
|
||||
console.log(`成功创建 ${achievements.length} 个成就`);
|
||||
|
||||
console.log('拼音数据初始化完成!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行初始化
|
||||
if (require.main === module) {
|
||||
initPinyinData();
|
||||
}
|
||||
|
||||
module.exports = { initPinyinData, createPinyinContents, createAchievements };
|
||||
Reference in New Issue
Block a user