feat: 星域故事汇小游戏初始版本

This commit is contained in:
2026-03-03 16:57:49 +08:00
commit cc0e39cccc
34 changed files with 6556 additions and 0 deletions

157
server/models/story.js Normal file
View File

@@ -0,0 +1,157 @@
const pool = require('../config/db');
const StoryModel = {
// 获取故事列表
async getList(options = {}) {
const { category, featured, limit = 20, offset = 0 } = options;
let sql = `SELECT id, title, cover_url, description, category, play_count, like_count, is_featured
FROM stories WHERE status = 1`;
const params = [];
if (category) {
sql += ' AND category = ?';
params.push(category);
}
if (featured) {
sql += ' AND is_featured = 1';
}
sql += ' ORDER BY is_featured DESC, play_count DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
const [rows] = await pool.query(sql, params);
return rows;
},
// 获取故事详情(含节点和选项)
async getDetail(storyId) {
// 获取故事基本信息
const [stories] = await pool.query(
'SELECT * FROM stories WHERE id = ? AND status = 1',
[storyId]
);
if (stories.length === 0) return null;
const story = stories[0];
// 获取所有节点
const [nodes] = await pool.query(
`SELECT id, node_key, content, speaker, background_image, character_image, bgm,
is_ending, ending_name, ending_score, ending_type
FROM story_nodes WHERE story_id = ? ORDER BY sort_order`,
[storyId]
);
// 获取所有选项
const [choices] = await pool.query(
'SELECT id, node_id, text, next_node_key, is_locked FROM story_choices WHERE story_id = ? ORDER BY sort_order',
[storyId]
);
// 组装节点和选项
const nodesMap = {};
nodes.forEach(node => {
nodesMap[node.node_key] = {
...node,
choices: []
};
});
choices.forEach(choice => {
const node = nodes.find(n => n.id === choice.node_id);
if (node && nodesMap[node.node_key]) {
nodesMap[node.node_key].choices.push({
text: choice.text,
nextNodeKey: choice.next_node_key,
isLocked: choice.is_locked
});
}
});
story.nodes = nodesMap;
return story;
},
// 增加游玩次数
async incrementPlayCount(storyId) {
await pool.query(
'UPDATE stories SET play_count = play_count + 1 WHERE id = ?',
[storyId]
);
},
// 点赞/取消点赞
async toggleLike(storyId, increment) {
const delta = increment ? 1 : -1;
await pool.query(
'UPDATE stories SET like_count = like_count + ? WHERE id = ?',
[delta, storyId]
);
},
// 获取热门故事
async getHotStories(limit = 10) {
const [rows] = await pool.query(
`SELECT id, title, cover_url, description, category, play_count, like_count
FROM stories WHERE status = 1 ORDER BY play_count DESC LIMIT ?`,
[limit]
);
return rows;
},
// 获取分类列表
async getCategories() {
const [rows] = await pool.query(
'SELECT DISTINCT category FROM stories WHERE status = 1'
);
return rows.map(r => r.category);
},
// AI改写结局
async aiRewriteEnding({ storyId, endingName, endingContent, prompt }) {
// 获取故事信息用于上下文
const [stories] = await pool.query(
'SELECT title, category, description FROM stories WHERE id = ?',
[storyId]
);
const story = stories[0];
// TODO: 接入真实AI服务OpenAI/Claude/自建模型)
// 这里先返回模拟结果后续替换为真实AI调用
const aiContent = await this.callAIService({
storyTitle: story?.title,
storyCategory: story?.category,
originalEnding: endingContent,
userPrompt: prompt
});
return {
content: aiContent,
speaker: '旁白',
is_ending: true,
ending_name: `${endingName}(改写版)`,
ending_type: 'rewrite'
};
},
// AI服务调用模拟/真实)
async callAIService({ storyTitle, storyCategory, originalEnding, userPrompt }) {
// 模拟AI生成内容
// 实际部署时替换为真实API调用
const templates = [
`根据你的愿望「${userPrompt}」,故事有了新的发展...\n\n`,
`命运的齿轮开始转动,${userPrompt}...\n\n`,
`在另一个平行世界里,${userPrompt}成为了现实...\n\n`
];
const template = templates[Math.floor(Math.random() * templates.length)];
const newContent = template +
`原本的结局被改写,新的故事在这里展开。\n\n` +
`【AI改写提示】这是基于「${userPrompt}」生成的新结局。\n` +
`实际部署时这里将由AI大模型根据上下文生成更精彩的内容。`;
return newContent;
}
};
module.exports = StoryModel;