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;