feat: 星域故事汇小游戏初始版本
This commit is contained in:
157
server/models/story.js
Normal file
157
server/models/story.js
Normal 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;
|
||||
134
server/models/user.js
Normal file
134
server/models/user.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const pool = require('../config/db');
|
||||
|
||||
const UserModel = {
|
||||
// 通过openid查找或创建用户
|
||||
async findOrCreate(openid, userInfo = {}) {
|
||||
const [existing] = await pool.query(
|
||||
'SELECT * FROM users WHERE openid = ?',
|
||||
[openid]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return existing[0];
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO users (openid, nickname, avatar_url, gender) VALUES (?, ?, ?, ?)',
|
||||
[openid, userInfo.nickname || '', userInfo.avatarUrl || '', userInfo.gender || 0]
|
||||
);
|
||||
|
||||
return {
|
||||
id: result.insertId,
|
||||
openid,
|
||||
nickname: userInfo.nickname || '',
|
||||
avatar_url: userInfo.avatarUrl || '',
|
||||
gender: userInfo.gender || 0
|
||||
};
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
async updateProfile(userId, userInfo) {
|
||||
await pool.query(
|
||||
'UPDATE users SET nickname = ?, avatar_url = ?, gender = ? WHERE id = ?',
|
||||
[userInfo.nickname, userInfo.avatarUrl, userInfo.gender, userId]
|
||||
);
|
||||
},
|
||||
|
||||
// 获取用户进度
|
||||
async getProgress(userId, storyId = null) {
|
||||
let sql = `SELECT up.*, s.title as story_title, s.cover_url
|
||||
FROM user_progress up
|
||||
JOIN stories s ON up.story_id = s.id
|
||||
WHERE up.user_id = ?`;
|
||||
const params = [userId];
|
||||
|
||||
if (storyId) {
|
||||
sql += ' AND up.story_id = ?';
|
||||
params.push(storyId);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY up.updated_at DESC';
|
||||
const [rows] = await pool.query(sql, params);
|
||||
return storyId ? rows[0] : rows;
|
||||
},
|
||||
|
||||
// 保存用户进度
|
||||
async saveProgress(userId, storyId, data) {
|
||||
const { currentNodeKey, isCompleted, endingReached } = data;
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO user_progress (user_id, story_id, current_node_key, is_completed, ending_reached)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
current_node_key = VALUES(current_node_key),
|
||||
is_completed = VALUES(is_completed),
|
||||
ending_reached = VALUES(ending_reached),
|
||||
play_count = play_count + 1`,
|
||||
[userId, storyId, currentNodeKey, isCompleted || false, endingReached || '']
|
||||
);
|
||||
|
||||
// 如果完成了,记录结局
|
||||
if (isCompleted && endingReached) {
|
||||
await pool.query(
|
||||
`INSERT IGNORE INTO user_endings (user_id, story_id, ending_name) VALUES (?, ?, ?)`,
|
||||
[userId, storyId, endingReached]
|
||||
);
|
||||
|
||||
// 更新用户统计
|
||||
await pool.query(
|
||||
'UPDATE users SET total_play_count = total_play_count + 1, total_endings = (SELECT COUNT(*) FROM user_endings WHERE user_id = ?) WHERE id = ?',
|
||||
[userId, userId]
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// 点赞/取消点赞
|
||||
async toggleLike(userId, storyId, isLiked) {
|
||||
await pool.query(
|
||||
`INSERT INTO user_progress (user_id, story_id, is_liked)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE is_liked = ?`,
|
||||
[userId, storyId, isLiked, isLiked]
|
||||
);
|
||||
},
|
||||
|
||||
// 收藏/取消收藏
|
||||
async toggleCollect(userId, storyId, isCollected) {
|
||||
await pool.query(
|
||||
`INSERT INTO user_progress (user_id, story_id, is_collected)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE is_collected = ?`,
|
||||
[userId, storyId, isCollected, isCollected]
|
||||
);
|
||||
},
|
||||
|
||||
// 获取收藏列表
|
||||
async getCollections(userId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT s.id, s.title, s.cover_url, s.description, s.category, s.play_count, s.like_count
|
||||
FROM user_progress up
|
||||
JOIN stories s ON up.story_id = s.id
|
||||
WHERE up.user_id = ? AND up.is_collected = 1
|
||||
ORDER BY up.updated_at DESC`,
|
||||
[userId]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 获取用户解锁的结局
|
||||
async getUnlockedEndings(userId, storyId = null) {
|
||||
let sql = 'SELECT * FROM user_endings WHERE user_id = ?';
|
||||
const params = [userId];
|
||||
|
||||
if (storyId) {
|
||||
sql += ' AND story_id = ?';
|
||||
params.push(storyId);
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = UserModel;
|
||||
Reference in New Issue
Block a user