🎉 feat: initialize 宇之然AI维度 project

- uni-app Vue3 frontend with dark glassmorphism theme
- 5 AI dimensions: Origin / Development / Current / Learning / Trend
- AI Chat system prompt updated from geometry to AI
- 23 AI knowledge articles initialized in DB
- Trend API + cron job for daily news
- Product pricing (Pro ¥19.9, VIP ¥39.9)
- Pinia stores + API utilities
- AGENTS.md documentation
This commit is contained in:
Yuzhiran Dev
2026-07-10 13:26:04 +08:00
commit 24cd9cef53
107 changed files with 20665 additions and 0 deletions
@@ -0,0 +1,363 @@
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
});
}
};