Files
ai_game/server/models/story.js

158 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;