feat: 星域故事汇小游戏初始版本
This commit is contained in:
13
server/.env.example
Normal file
13
server/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
|
||||
# MySQL数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password
|
||||
DB_NAME=stardom_story
|
||||
|
||||
# 微信小游戏配置
|
||||
WX_APPID=your_appid
|
||||
WX_SECRET=your_secret
|
||||
35
server/app.js
Normal file
35
server/app.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
require('dotenv').config();
|
||||
|
||||
const storyRoutes = require('./routes/story');
|
||||
const userRoutes = require('./routes/user');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// 路由
|
||||
app.use('/api/stories', storyRoutes);
|
||||
app.use('/api/user', userRoutes);
|
||||
|
||||
// 健康检查
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', message: '星域故事汇服务运行中' });
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ code: 500, message: '服务器内部错误' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`星域故事汇服务器运行在 http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
14
server/config/db.js
Normal file
14
server/config/db.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'stardom_story',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
module.exports = pool;
|
||||
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;
|
||||
1269
server/package-lock.json
generated
Normal file
1269
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
server/package.json
Normal file
20
server/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "stardom-story-server",
|
||||
"version": "1.0.0",
|
||||
"description": "星域故事汇小游戏后端服务",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"dev": "nodemon app.js",
|
||||
"init-db": "node sql/init.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mysql2": "^3.6.5",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
110
server/routes/story.js
Normal file
110
server/routes/story.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const StoryModel = require('../models/story');
|
||||
|
||||
// 获取故事列表
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { category, featured, limit, offset } = req.query;
|
||||
const stories = await StoryModel.getList({
|
||||
category,
|
||||
featured: featured === 'true',
|
||||
limit: parseInt(limit) || 20,
|
||||
offset: parseInt(offset) || 0
|
||||
});
|
||||
res.json({ code: 0, data: stories });
|
||||
} catch (err) {
|
||||
console.error('获取故事列表失败:', err);
|
||||
res.status(500).json({ code: 500, message: '获取故事列表失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取热门故事
|
||||
router.get('/hot', async (req, res) => {
|
||||
try {
|
||||
const { limit } = req.query;
|
||||
const stories = await StoryModel.getHotStories(parseInt(limit) || 10);
|
||||
res.json({ code: 0, data: stories });
|
||||
} catch (err) {
|
||||
console.error('获取热门故事失败:', err);
|
||||
res.status(500).json({ code: 500, message: '获取热门故事失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取分类列表
|
||||
router.get('/categories', async (req, res) => {
|
||||
try {
|
||||
const categories = await StoryModel.getCategories();
|
||||
res.json({ code: 0, data: categories });
|
||||
} catch (err) {
|
||||
console.error('获取分类列表失败:', err);
|
||||
res.status(500).json({ code: 500, message: '获取分类列表失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取故事详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const storyId = parseInt(req.params.id);
|
||||
const story = await StoryModel.getDetail(storyId);
|
||||
if (!story) {
|
||||
return res.status(404).json({ code: 404, message: '故事不存在' });
|
||||
}
|
||||
res.json({ code: 0, data: story });
|
||||
} catch (err) {
|
||||
console.error('获取故事详情失败:', err);
|
||||
res.status(500).json({ code: 500, message: '获取故事详情失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 记录游玩
|
||||
router.post('/:id/play', async (req, res) => {
|
||||
try {
|
||||
const storyId = parseInt(req.params.id);
|
||||
await StoryModel.incrementPlayCount(storyId);
|
||||
res.json({ code: 0, message: '记录成功' });
|
||||
} catch (err) {
|
||||
console.error('记录游玩失败:', err);
|
||||
res.status(500).json({ code: 500, message: '记录游玩失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 点赞
|
||||
router.post('/:id/like', async (req, res) => {
|
||||
try {
|
||||
const storyId = parseInt(req.params.id);
|
||||
const { like } = req.body; // true: 点赞, false: 取消点赞
|
||||
await StoryModel.toggleLike(storyId, like);
|
||||
res.json({ code: 0, message: like ? '点赞成功' : '取消点赞成功' });
|
||||
} catch (err) {
|
||||
console.error('点赞操作失败:', err);
|
||||
res.status(500).json({ code: 500, message: '点赞操作失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// AI改写结局
|
||||
router.post('/:id/rewrite', async (req, res) => {
|
||||
try {
|
||||
const storyId = parseInt(req.params.id);
|
||||
const { ending_name, ending_content, prompt } = req.body;
|
||||
|
||||
if (!prompt) {
|
||||
return res.status(400).json({ code: 400, message: '请输入改写指令' });
|
||||
}
|
||||
|
||||
// 调用AI服务生成新内容
|
||||
const result = await StoryModel.aiRewriteEnding({
|
||||
storyId,
|
||||
endingName: ending_name,
|
||||
endingContent: ending_content,
|
||||
prompt
|
||||
});
|
||||
|
||||
res.json({ code: 0, data: result });
|
||||
} catch (err) {
|
||||
console.error('AI改写失败:', err);
|
||||
res.status(500).json({ code: 500, message: 'AI改写失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
124
server/routes/user.js
Normal file
124
server/routes/user.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const UserModel = require('../models/user');
|
||||
|
||||
// 模拟微信登录(实际环境需要调用微信API)
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { code, userInfo } = req.body;
|
||||
|
||||
// 实际项目中需要用code换取openid
|
||||
// 这里为了开发测试,暂时用code作为openid
|
||||
const openid = code || `test_user_${Date.now()}`;
|
||||
|
||||
const user = await UserModel.findOrCreate(openid, userInfo);
|
||||
res.json({
|
||||
code: 0,
|
||||
data: {
|
||||
userId: user.id,
|
||||
openid: user.openid,
|
||||
nickname: user.nickname,
|
||||
avatarUrl: user.avatar_url
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('登录失败:', err);
|
||||
res.status(500).json({ code: 500, message: '登录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新用户信息
|
||||
router.post('/profile', async (req, res) => {
|
||||
try {
|
||||
const { userId, nickname, avatarUrl, gender } = req.body;
|
||||
await UserModel.updateProfile(userId, { nickname, avatarUrl, gender });
|
||||
res.json({ code: 0, message: '更新成功' });
|
||||
} catch (err) {
|
||||
console.error('更新用户信息失败:', err);
|
||||
res.status(500).json({ code: 500, message: '更新用户信息失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取用户进度
|
||||
router.get('/progress', async (req, res) => {
|
||||
try {
|
||||
const { userId, storyId } = req.query;
|
||||
const progress = await UserModel.getProgress(
|
||||
parseInt(userId),
|
||||
storyId ? parseInt(storyId) : null
|
||||
);
|
||||
res.json({ code: 0, data: progress });
|
||||
} catch (err) {
|
||||
console.error('获取进度失败:', err);
|
||||
res.status(500).json({ code: 500, message: '获取进度失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 保存用户进度
|
||||
router.post('/progress', async (req, res) => {
|
||||
try {
|
||||
const { userId, storyId, currentNodeKey, isCompleted, endingReached } = req.body;
|
||||
await UserModel.saveProgress(parseInt(userId), parseInt(storyId), {
|
||||
currentNodeKey,
|
||||
isCompleted,
|
||||
endingReached
|
||||
});
|
||||
res.json({ code: 0, message: '保存成功' });
|
||||
} catch (err) {
|
||||
console.error('保存进度失败:', err);
|
||||
res.status(500).json({ code: 500, message: '保存进度失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 点赞操作
|
||||
router.post('/like', async (req, res) => {
|
||||
try {
|
||||
const { userId, storyId, isLiked } = req.body;
|
||||
await UserModel.toggleLike(parseInt(userId), parseInt(storyId), isLiked);
|
||||
res.json({ code: 0, message: isLiked ? '点赞成功' : '取消点赞' });
|
||||
} catch (err) {
|
||||
console.error('点赞操作失败:', err);
|
||||
res.status(500).json({ code: 500, message: '点赞操作失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 收藏操作
|
||||
router.post('/collect', async (req, res) => {
|
||||
try {
|
||||
const { userId, storyId, isCollected } = req.body;
|
||||
await UserModel.toggleCollect(parseInt(userId), parseInt(storyId), isCollected);
|
||||
res.json({ code: 0, message: isCollected ? '收藏成功' : '取消收藏' });
|
||||
} catch (err) {
|
||||
console.error('收藏操作失败:', err);
|
||||
res.status(500).json({ code: 500, message: '收藏操作失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取收藏列表
|
||||
router.get('/collections', async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.query;
|
||||
const collections = await UserModel.getCollections(parseInt(userId));
|
||||
res.json({ code: 0, data: collections });
|
||||
} catch (err) {
|
||||
console.error('获取收藏列表失败:', err);
|
||||
res.status(500).json({ code: 500, message: '获取收藏列表失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取已解锁结局
|
||||
router.get('/endings', async (req, res) => {
|
||||
try {
|
||||
const { userId, storyId } = req.query;
|
||||
const endings = await UserModel.getUnlockedEndings(
|
||||
parseInt(userId),
|
||||
storyId ? parseInt(storyId) : null
|
||||
);
|
||||
res.json({ code: 0, data: endings });
|
||||
} catch (err) {
|
||||
console.error('获取结局列表失败:', err);
|
||||
res.status(500).json({ code: 500, message: '获取结局列表失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
69
server/sql/init.js
Normal file
69
server/sql/init.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 数据库初始化脚本
|
||||
* 运行: npm run init-db
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
async function initDatabase() {
|
||||
console.log('开始初始化数据库...');
|
||||
|
||||
// 先连接到MySQL(不指定数据库)
|
||||
const connection = await mysql.createConnection({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
multipleStatements: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 读取并执行schema.sql
|
||||
console.log('创建数据库表结构...');
|
||||
const schemaSQL = fs.readFileSync(
|
||||
path.join(__dirname, 'schema.sql'),
|
||||
'utf8'
|
||||
);
|
||||
await connection.query(schemaSQL);
|
||||
console.log('表结构创建成功!');
|
||||
|
||||
// 读取并执行种子数据
|
||||
console.log('导入种子故事数据(第1部分)...');
|
||||
const seedSQL1 = fs.readFileSync(
|
||||
path.join(__dirname, 'seed_stories_part1.sql'),
|
||||
'utf8'
|
||||
);
|
||||
await connection.query(seedSQL1);
|
||||
console.log('种子数据第1部分导入成功!');
|
||||
|
||||
console.log('导入种子故事数据(第2部分)...');
|
||||
const seedSQL2 = fs.readFileSync(
|
||||
path.join(__dirname, 'seed_stories_part2.sql'),
|
||||
'utf8'
|
||||
);
|
||||
await connection.query(seedSQL2);
|
||||
console.log('种子数据第2部分导入成功!');
|
||||
|
||||
console.log('\n数据库初始化完成!');
|
||||
console.log('共创建10个种子故事,包含66个剧情节点和多个结局分支。');
|
||||
|
||||
} catch (error) {
|
||||
console.error('数据库初始化失败:', error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
// 运行初始化
|
||||
initDatabase()
|
||||
.then(() => {
|
||||
console.log('\n可以启动服务器了: npm start');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('初始化过程中出现错误:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
107
server/sql/schema.sql
Normal file
107
server/sql/schema.sql
Normal file
@@ -0,0 +1,107 @@
|
||||
-- 星域故事汇数据库初始化脚本
|
||||
-- 创建数据库
|
||||
CREATE DATABASE IF NOT EXISTS stardom_story DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE stardom_story;
|
||||
|
||||
-- 故事主表
|
||||
CREATE TABLE IF NOT EXISTS stories (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL COMMENT '故事标题',
|
||||
cover_url VARCHAR(255) DEFAULT '' COMMENT '封面图URL',
|
||||
description TEXT COMMENT '故事简介',
|
||||
author_id INT DEFAULT 0 COMMENT '作者ID,0表示官方',
|
||||
category VARCHAR(50) NOT NULL COMMENT '故事分类',
|
||||
play_count INT DEFAULT 0 COMMENT '游玩次数',
|
||||
like_count INT DEFAULT 0 COMMENT '点赞数',
|
||||
is_featured BOOLEAN DEFAULT FALSE COMMENT '是否精选',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:0下架 1上架',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_featured (is_featured),
|
||||
INDEX idx_play_count (play_count)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事主表';
|
||||
|
||||
-- 故事节点表
|
||||
CREATE TABLE IF NOT EXISTS story_nodes (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
story_id INT NOT NULL COMMENT '故事ID',
|
||||
node_key VARCHAR(50) NOT NULL COMMENT '节点唯一标识',
|
||||
content TEXT NOT NULL COMMENT '节点内容文本',
|
||||
speaker VARCHAR(50) DEFAULT '' COMMENT '说话角色名',
|
||||
background_image VARCHAR(255) DEFAULT '' COMMENT '背景图URL',
|
||||
character_image VARCHAR(255) DEFAULT '' COMMENT '角色立绘URL',
|
||||
bgm VARCHAR(255) DEFAULT '' COMMENT '背景音乐',
|
||||
is_ending BOOLEAN DEFAULT FALSE COMMENT '是否为结局节点',
|
||||
ending_name VARCHAR(100) DEFAULT '' COMMENT '结局名称',
|
||||
ending_score INT DEFAULT 0 COMMENT '结局评分',
|
||||
ending_type VARCHAR(20) DEFAULT '' COMMENT '结局类型:good/bad/normal/hidden',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_story_id (story_id),
|
||||
INDEX idx_node_key (story_id, node_key),
|
||||
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事节点表';
|
||||
|
||||
-- 故事选项表
|
||||
CREATE TABLE IF NOT EXISTS story_choices (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
node_id INT NOT NULL COMMENT '所属节点ID',
|
||||
story_id INT NOT NULL COMMENT '故事ID(冗余,便于查询)',
|
||||
text VARCHAR(200) NOT NULL COMMENT '选项文本',
|
||||
next_node_key VARCHAR(50) NOT NULL COMMENT '下一个节点key',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||
is_locked BOOLEAN DEFAULT FALSE COMMENT '是否锁定(需广告解锁)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_node_id (node_id),
|
||||
INDEX idx_story_id (story_id),
|
||||
FOREIGN KEY (node_id) REFERENCES story_nodes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事选项表';
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
openid VARCHAR(100) UNIQUE NOT NULL COMMENT '微信openid',
|
||||
nickname VARCHAR(100) DEFAULT '' COMMENT '昵称',
|
||||
avatar_url VARCHAR(255) DEFAULT '' COMMENT '头像URL',
|
||||
gender TINYINT DEFAULT 0 COMMENT '性别:0未知 1男 2女',
|
||||
total_play_count INT DEFAULT 0 COMMENT '总游玩次数',
|
||||
total_endings INT DEFAULT 0 COMMENT '解锁结局数',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_openid (openid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
||||
|
||||
-- 用户进度表
|
||||
CREATE TABLE IF NOT EXISTS user_progress (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
story_id INT NOT NULL COMMENT '故事ID',
|
||||
current_node_key VARCHAR(50) DEFAULT 'start' COMMENT '当前节点',
|
||||
is_completed BOOLEAN DEFAULT FALSE COMMENT '是否完成',
|
||||
ending_reached VARCHAR(100) DEFAULT '' COMMENT '达成的结局',
|
||||
is_liked BOOLEAN DEFAULT FALSE COMMENT '是否点赞',
|
||||
is_collected BOOLEAN DEFAULT FALSE COMMENT '是否收藏',
|
||||
play_count INT DEFAULT 1 COMMENT '游玩次数',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_user_story (user_id, story_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_story_id (story_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户进度表';
|
||||
|
||||
-- 用户结局收集表
|
||||
CREATE TABLE IF NOT EXISTS user_endings (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id INT NOT NULL,
|
||||
story_id INT NOT NULL,
|
||||
ending_name VARCHAR(100) NOT NULL,
|
||||
ending_score INT DEFAULT 0,
|
||||
unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_user_ending (user_id, story_id, ending_name),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户结局收集表';
|
||||
566
server/sql/schema_v2.sql
Normal file
566
server/sql/schema_v2.sql
Normal file
@@ -0,0 +1,566 @@
|
||||
-- ============================================
|
||||
-- 星域故事汇 完整数据库结构 V2.0
|
||||
-- 支持:AI改写/续写/创作、UGC、社交、计费
|
||||
-- ============================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS stardom_story DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE stardom_story;
|
||||
|
||||
-- ============================================
|
||||
-- 一、用户体系
|
||||
-- ============================================
|
||||
|
||||
-- 用户主表
|
||||
CREATE TABLE users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
openid VARCHAR(100) UNIQUE NOT NULL COMMENT '微信openid',
|
||||
unionid VARCHAR(100) DEFAULT '' COMMENT '微信unionid',
|
||||
nickname VARCHAR(100) DEFAULT '' COMMENT '昵称',
|
||||
avatar_url VARCHAR(500) DEFAULT '' COMMENT '头像',
|
||||
gender TINYINT DEFAULT 0 COMMENT '0未知 1男 2女',
|
||||
phone VARCHAR(20) DEFAULT '' COMMENT '手机号',
|
||||
level INT DEFAULT 1 COMMENT '用户等级',
|
||||
exp INT DEFAULT 0 COMMENT '经验值',
|
||||
vip_type TINYINT DEFAULT 0 COMMENT '0普通 1月卡 2年卡 3永久',
|
||||
vip_expire_at DATETIME DEFAULT NULL COMMENT 'VIP过期时间',
|
||||
coin_balance INT DEFAULT 0 COMMENT '金币余额',
|
||||
is_creator BOOLEAN DEFAULT FALSE COMMENT '是否认证创作者',
|
||||
is_banned BOOLEAN DEFAULT FALSE COMMENT '是否封禁',
|
||||
ban_reason VARCHAR(255) DEFAULT '',
|
||||
last_login_at DATETIME DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_openid (openid),
|
||||
INDEX idx_unionid (unionid),
|
||||
INDEX idx_creator (is_creator)
|
||||
) ENGINE=InnoDB COMMENT='用户主表';
|
||||
|
||||
-- 用户统计表(高频更新分离)
|
||||
CREATE TABLE user_stats (
|
||||
user_id BIGINT PRIMARY KEY,
|
||||
total_play_count INT DEFAULT 0 COMMENT '总游玩次数',
|
||||
total_endings INT DEFAULT 0 COMMENT '解锁结局数',
|
||||
total_creations INT DEFAULT 0 COMMENT '创作数',
|
||||
total_ai_uses INT DEFAULT 0 COMMENT 'AI使用次数',
|
||||
followers_count INT DEFAULT 0 COMMENT '粉丝数',
|
||||
following_count INT DEFAULT 0 COMMENT '关注数',
|
||||
likes_received INT DEFAULT 0 COMMENT '获赞数',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB COMMENT='用户统计表';
|
||||
|
||||
-- 用户关注关系表
|
||||
CREATE TABLE user_follows (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
follower_id BIGINT NOT NULL COMMENT '关注者',
|
||||
following_id BIGINT NOT NULL COMMENT '被关注者',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_follow (follower_id, following_id),
|
||||
INDEX idx_follower (follower_id),
|
||||
INDEX idx_following (following_id)
|
||||
) ENGINE=InnoDB COMMENT='用户关注关系';
|
||||
|
||||
-- ============================================
|
||||
-- 二、故事内容体系
|
||||
-- ============================================
|
||||
|
||||
-- 故事主表(官方+UGC统一)
|
||||
CREATE TABLE stories (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
author_id BIGINT DEFAULT 0 COMMENT '作者ID,0为官方',
|
||||
title VARCHAR(100) NOT NULL COMMENT '标题',
|
||||
cover_url VARCHAR(500) DEFAULT '' COMMENT '封面',
|
||||
description TEXT COMMENT '简介',
|
||||
category VARCHAR(50) NOT NULL COMMENT '分类',
|
||||
tags VARCHAR(255) DEFAULT '' COMMENT '标签,逗号分隔',
|
||||
word_count INT DEFAULT 0 COMMENT '字数',
|
||||
node_count INT DEFAULT 0 COMMENT '节点数',
|
||||
ending_count INT DEFAULT 0 COMMENT '结局数',
|
||||
difficulty TINYINT DEFAULT 2 COMMENT '难度 1简单 2普通 3困难',
|
||||
|
||||
-- 来源标记
|
||||
source_type ENUM('official','ugc','ai_generated','ai_assisted') DEFAULT 'official' COMMENT '来源类型',
|
||||
base_story_id BIGINT DEFAULT NULL COMMENT '基于哪个故事改编',
|
||||
ai_generation_id BIGINT DEFAULT NULL COMMENT '关联的AI生成记录',
|
||||
|
||||
-- 状态控制
|
||||
status TINYINT DEFAULT 0 COMMENT '0草稿 1审核中 2已发布 3已下架 4已拒绝',
|
||||
review_note VARCHAR(500) DEFAULT '' COMMENT '审核备注',
|
||||
is_featured BOOLEAN DEFAULT FALSE COMMENT '是否精选',
|
||||
is_hot BOOLEAN DEFAULT FALSE COMMENT '是否热门',
|
||||
is_new BOOLEAN DEFAULT TRUE COMMENT '是否新作',
|
||||
|
||||
-- 付费设置
|
||||
price_type TINYINT DEFAULT 0 COMMENT '0免费 1付费 2广告解锁',
|
||||
price_coin INT DEFAULT 0 COMMENT '价格(金币)',
|
||||
free_chapters INT DEFAULT 0 COMMENT '免费章节数',
|
||||
|
||||
-- 统计(读多写少可放主表)
|
||||
play_count INT DEFAULT 0,
|
||||
like_count INT DEFAULT 0,
|
||||
collect_count INT DEFAULT 0,
|
||||
comment_count INT DEFAULT 0,
|
||||
share_count INT DEFAULT 0,
|
||||
|
||||
published_at DATETIME DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_author (author_id),
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_source (source_type),
|
||||
INDEX idx_featured (is_featured),
|
||||
INDEX idx_play (play_count),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB COMMENT='故事主表';
|
||||
|
||||
-- 故事节点表
|
||||
CREATE TABLE story_nodes (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
story_id BIGINT NOT NULL,
|
||||
node_key VARCHAR(50) NOT NULL COMMENT '节点标识',
|
||||
parent_node_key VARCHAR(50) DEFAULT '' COMMENT '父节点(用于树形结构)',
|
||||
chapter_num INT DEFAULT 1 COMMENT '章节号',
|
||||
|
||||
-- 内容
|
||||
content TEXT NOT NULL COMMENT '文本内容',
|
||||
speaker VARCHAR(50) DEFAULT '' COMMENT '说话角色',
|
||||
emotion VARCHAR(30) DEFAULT '' COMMENT '情绪标签',
|
||||
|
||||
-- 媒体资源
|
||||
background_image VARCHAR(500) DEFAULT '',
|
||||
character_images JSON DEFAULT NULL COMMENT '角色立绘数组',
|
||||
bgm VARCHAR(500) DEFAULT '',
|
||||
sound_effect VARCHAR(500) DEFAULT '',
|
||||
voice_url VARCHAR(500) DEFAULT '' COMMENT '语音URL',
|
||||
|
||||
-- 结局设置
|
||||
is_ending BOOLEAN DEFAULT FALSE,
|
||||
ending_name VARCHAR(100) DEFAULT '',
|
||||
ending_type ENUM('good','bad','normal','hidden','secret') DEFAULT 'normal',
|
||||
ending_score INT DEFAULT 0,
|
||||
ending_cg VARCHAR(500) DEFAULT '' COMMENT '结局CG图',
|
||||
|
||||
-- AI标记
|
||||
is_ai_generated BOOLEAN DEFAULT FALSE,
|
||||
ai_generation_id BIGINT DEFAULT NULL,
|
||||
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_story (story_id),
|
||||
INDEX idx_node_key (story_id, node_key),
|
||||
INDEX idx_chapter (story_id, chapter_num),
|
||||
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB COMMENT='故事节点表';
|
||||
|
||||
-- 故事选项表
|
||||
CREATE TABLE story_choices (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
story_id BIGINT NOT NULL,
|
||||
node_id BIGINT NOT NULL,
|
||||
text VARCHAR(200) NOT NULL COMMENT '选项文本',
|
||||
next_node_key VARCHAR(50) NOT NULL,
|
||||
|
||||
-- 条件解锁
|
||||
condition_type ENUM('none','item','attr','ending','vip','ad') DEFAULT 'none',
|
||||
condition_value VARCHAR(100) DEFAULT '' COMMENT '条件值JSON',
|
||||
|
||||
-- 效果
|
||||
effect_type ENUM('none','attr','item','achievement') DEFAULT 'none',
|
||||
effect_value VARCHAR(100) DEFAULT '' COMMENT '效果值JSON',
|
||||
|
||||
-- AI标记
|
||||
is_ai_generated BOOLEAN DEFAULT FALSE,
|
||||
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_story (story_id),
|
||||
INDEX idx_node (node_id),
|
||||
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (node_id) REFERENCES story_nodes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB COMMENT='故事选项表';
|
||||
|
||||
-- 故事版本表(支持版本回退)
|
||||
CREATE TABLE story_versions (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
story_id BIGINT NOT NULL,
|
||||
version_num INT NOT NULL COMMENT '版本号',
|
||||
nodes_snapshot LONGTEXT NOT NULL COMMENT '节点快照JSON',
|
||||
change_log VARCHAR(500) DEFAULT '' COMMENT '变更说明',
|
||||
created_by BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_version (story_id, version_num),
|
||||
INDEX idx_story (story_id)
|
||||
) ENGINE=InnoDB COMMENT='故事版本表';
|
||||
|
||||
-- ============================================
|
||||
-- 三、AI生成体系
|
||||
-- ============================================
|
||||
|
||||
-- AI生成记录表
|
||||
CREATE TABLE ai_generations (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
story_id BIGINT DEFAULT NULL COMMENT '关联故事',
|
||||
|
||||
-- 生成类型
|
||||
gen_type ENUM('rewrite_ending','rewrite_node','continue','create_outline','create_full','polish','translate') NOT NULL,
|
||||
|
||||
-- 输入
|
||||
source_node_key VARCHAR(50) DEFAULT '',
|
||||
source_content TEXT COMMENT '原内容',
|
||||
user_prompt TEXT NOT NULL COMMENT '用户指令',
|
||||
system_prompt TEXT COMMENT '系统提示词',
|
||||
|
||||
-- 输出
|
||||
generated_content LONGTEXT COMMENT '生成内容',
|
||||
generated_nodes JSON COMMENT '生成的节点结构',
|
||||
|
||||
-- 模型信息
|
||||
model_provider VARCHAR(30) DEFAULT '' COMMENT 'openai/anthropic/local',
|
||||
model_name VARCHAR(50) DEFAULT '',
|
||||
temperature DECIMAL(3,2) DEFAULT 0.70,
|
||||
|
||||
-- 消耗统计
|
||||
input_tokens INT DEFAULT 0,
|
||||
output_tokens INT DEFAULT 0,
|
||||
total_tokens INT DEFAULT 0,
|
||||
latency_ms INT DEFAULT 0 COMMENT '响应耗时',
|
||||
cost_cents INT DEFAULT 0 COMMENT '成本(分)',
|
||||
|
||||
-- 状态
|
||||
status TINYINT DEFAULT 1 COMMENT '0失败 1成功 2处理中',
|
||||
error_code VARCHAR(50) DEFAULT '',
|
||||
error_msg TEXT,
|
||||
|
||||
-- 用户操作
|
||||
is_accepted BOOLEAN DEFAULT NULL COMMENT '用户是否采纳',
|
||||
is_edited BOOLEAN DEFAULT FALSE COMMENT '是否编辑后使用',
|
||||
is_published BOOLEAN DEFAULT FALSE COMMENT '是否发布',
|
||||
|
||||
-- 评价
|
||||
user_rating TINYINT DEFAULT NULL COMMENT '1-5星评价',
|
||||
user_feedback TEXT COMMENT '用户反馈',
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_story (story_id),
|
||||
INDEX idx_type (gen_type),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB COMMENT='AI生成记录表';
|
||||
|
||||
-- AI提示词模板表
|
||||
CREATE TABLE ai_prompt_templates (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
gen_type VARCHAR(30) NOT NULL,
|
||||
category VARCHAR(50) DEFAULT '' COMMENT '适用分类',
|
||||
system_prompt TEXT NOT NULL,
|
||||
user_prompt_template TEXT NOT NULL COMMENT '用户提示词模板',
|
||||
variables JSON COMMENT '可用变量说明',
|
||||
model_recommend VARCHAR(50) DEFAULT '',
|
||||
temperature_recommend DECIMAL(3,2) DEFAULT 0.70,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_type (gen_type),
|
||||
INDEX idx_active (is_active)
|
||||
) ENGINE=InnoDB COMMENT='AI提示词模板';
|
||||
|
||||
-- ============================================
|
||||
-- 四、用户行为体系
|
||||
-- ============================================
|
||||
|
||||
-- 用户游玩进度表
|
||||
CREATE TABLE user_progress (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
story_id BIGINT NOT NULL,
|
||||
current_node_key VARCHAR(50) DEFAULT 'start',
|
||||
|
||||
-- 状态
|
||||
status TINYINT DEFAULT 0 COMMENT '0进行中 1已完成',
|
||||
play_count INT DEFAULT 1,
|
||||
play_duration INT DEFAULT 0 COMMENT '游玩时长(秒)',
|
||||
|
||||
-- 达成结局
|
||||
endings_reached JSON COMMENT '达成的结局列表',
|
||||
best_ending_score INT DEFAULT 0,
|
||||
|
||||
-- 存档数据
|
||||
save_data JSON COMMENT '游戏存档(属性/道具等)',
|
||||
choices_history JSON COMMENT '选择历史',
|
||||
|
||||
-- 互动
|
||||
is_liked BOOLEAN DEFAULT FALSE,
|
||||
is_collected BOOLEAN DEFAULT FALSE,
|
||||
|
||||
first_play_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_play_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME DEFAULT NULL,
|
||||
|
||||
UNIQUE KEY uk_user_story (user_id, story_id),
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_story (story_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB COMMENT='用户游玩进度';
|
||||
|
||||
-- 用户结局收集表
|
||||
CREATE TABLE user_endings (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
story_id BIGINT NOT NULL,
|
||||
node_id BIGINT NOT NULL,
|
||||
ending_name VARCHAR(100) NOT NULL,
|
||||
ending_type VARCHAR(20) DEFAULT 'normal',
|
||||
ending_score INT DEFAULT 0,
|
||||
choices_path JSON COMMENT '达成路径',
|
||||
play_duration INT DEFAULT 0 COMMENT '本次游玩时长',
|
||||
unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_user_ending (user_id, story_id, ending_name),
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_story (story_id)
|
||||
) ENGINE=InnoDB COMMENT='用户结局收集';
|
||||
|
||||
-- 用户收藏表
|
||||
CREATE TABLE user_collections (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
story_id BIGINT NOT NULL,
|
||||
folder_name VARCHAR(50) DEFAULT '默认' COMMENT '收藏夹',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_user_story (user_id, story_id),
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_folder (user_id, folder_name)
|
||||
) ENGINE=InnoDB COMMENT='用户收藏';
|
||||
|
||||
-- 评论表
|
||||
CREATE TABLE comments (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
story_id BIGINT NOT NULL,
|
||||
parent_id BIGINT DEFAULT NULL COMMENT '父评论ID',
|
||||
reply_to_user_id BIGINT DEFAULT NULL COMMENT '回复谁',
|
||||
content TEXT NOT NULL,
|
||||
like_count INT DEFAULT 0,
|
||||
status TINYINT DEFAULT 1 COMMENT '0隐藏 1正常 2置顶',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_story (story_id),
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_parent (parent_id)
|
||||
) ENGINE=InnoDB COMMENT='评论表';
|
||||
|
||||
-- ============================================
|
||||
-- 五、经济体系
|
||||
-- ============================================
|
||||
|
||||
-- 用户AI配额表
|
||||
CREATE TABLE user_ai_quota (
|
||||
user_id BIGINT PRIMARY KEY,
|
||||
|
||||
-- 每日免费额度
|
||||
daily_free_total INT DEFAULT 5 COMMENT '每日免费总次数',
|
||||
daily_free_used INT DEFAULT 0 COMMENT '今日已用',
|
||||
daily_reset_date DATE COMMENT '重置日期',
|
||||
|
||||
-- 购买/赠送额度
|
||||
purchased_quota INT DEFAULT 0 COMMENT '购买的次数',
|
||||
gift_quota INT DEFAULT 0 COMMENT '赠送的次数',
|
||||
|
||||
-- VIP额度
|
||||
vip_daily_bonus INT DEFAULT 0 COMMENT 'VIP每日额外次数',
|
||||
|
||||
-- 总使用统计
|
||||
total_used INT DEFAULT 0,
|
||||
total_tokens_used BIGINT DEFAULT 0,
|
||||
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB COMMENT='用户AI配额';
|
||||
|
||||
-- 金币流水表
|
||||
CREATE TABLE coin_transactions (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
amount INT NOT NULL COMMENT '正为收入负为支出',
|
||||
balance_after INT NOT NULL COMMENT '交易后余额',
|
||||
|
||||
tx_type ENUM('recharge','purchase','reward','refund','gift','withdraw') NOT NULL,
|
||||
ref_type VARCHAR(30) DEFAULT '' COMMENT '关联类型',
|
||||
ref_id BIGINT DEFAULT NULL COMMENT '关联ID',
|
||||
|
||||
description VARCHAR(255) DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_type (tx_type),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB COMMENT='金币流水';
|
||||
|
||||
-- 订单表
|
||||
CREATE TABLE orders (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_no VARCHAR(64) UNIQUE NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
|
||||
product_type ENUM('coin','vip','story','ai_quota') NOT NULL,
|
||||
product_id VARCHAR(50) DEFAULT '',
|
||||
product_name VARCHAR(100) NOT NULL,
|
||||
|
||||
amount_cents INT NOT NULL COMMENT '金额(分)',
|
||||
pay_channel VARCHAR(30) DEFAULT '' COMMENT '支付渠道',
|
||||
|
||||
status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已取消 3已退款',
|
||||
paid_at DATETIME DEFAULT NULL,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_order_no (order_no),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB COMMENT='订单表';
|
||||
|
||||
-- ============================================
|
||||
-- 六、运营与审核
|
||||
-- ============================================
|
||||
|
||||
-- 内容审核表
|
||||
CREATE TABLE content_reviews (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
content_type ENUM('story','comment','ai_generation','avatar','nickname') NOT NULL,
|
||||
content_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL COMMENT '提交者',
|
||||
|
||||
content_snapshot TEXT COMMENT '内容快照',
|
||||
|
||||
-- 机审
|
||||
auto_result TINYINT DEFAULT NULL COMMENT '0通过 1疑似 2违规',
|
||||
auto_labels JSON COMMENT '机审标签',
|
||||
auto_score DECIMAL(5,2) DEFAULT NULL,
|
||||
|
||||
-- 人审
|
||||
review_status TINYINT DEFAULT 0 COMMENT '0待审 1通过 2拒绝',
|
||||
reviewer_id BIGINT DEFAULT NULL,
|
||||
review_note VARCHAR(500) DEFAULT '',
|
||||
reviewed_at DATETIME DEFAULT NULL,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_type_status (content_type, review_status),
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB COMMENT='内容审核表';
|
||||
|
||||
-- 举报表
|
||||
CREATE TABLE reports (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
reporter_id BIGINT NOT NULL,
|
||||
target_type ENUM('story','comment','user') NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
reason_type VARCHAR(30) NOT NULL COMMENT '举报类型',
|
||||
reason_detail TEXT,
|
||||
|
||||
status TINYINT DEFAULT 0 COMMENT '0待处理 1已处理 2已忽略',
|
||||
handler_id BIGINT DEFAULT NULL,
|
||||
handle_result VARCHAR(255) DEFAULT '',
|
||||
handled_at DATETIME DEFAULT NULL,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_target (target_type, target_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB COMMENT='举报表';
|
||||
|
||||
-- ============================================
|
||||
-- 七、数据分析
|
||||
-- ============================================
|
||||
|
||||
-- 行为埋点表(按天分区)
|
||||
CREATE TABLE event_logs (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT DEFAULT NULL,
|
||||
device_id VARCHAR(100) DEFAULT '',
|
||||
session_id VARCHAR(64) DEFAULT '',
|
||||
|
||||
event_name VARCHAR(50) NOT NULL,
|
||||
event_params JSON,
|
||||
|
||||
page_name VARCHAR(50) DEFAULT '',
|
||||
ref_page VARCHAR(50) DEFAULT '',
|
||||
|
||||
platform VARCHAR(20) DEFAULT '' COMMENT 'weapp/h5/app',
|
||||
app_version VARCHAR(20) DEFAULT '',
|
||||
os VARCHAR(20) DEFAULT '',
|
||||
device_model VARCHAR(50) DEFAULT '',
|
||||
|
||||
ip VARCHAR(50) DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
event_date DATE AS (DATE(created_at)) STORED,
|
||||
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_event (event_name),
|
||||
INDEX idx_date (event_date),
|
||||
INDEX idx_session (session_id)
|
||||
) ENGINE=InnoDB COMMENT='行为埋点表'
|
||||
PARTITION BY RANGE (TO_DAYS(event_date)) (
|
||||
PARTITION p_default VALUES LESS THAN MAXVALUE
|
||||
);
|
||||
|
||||
-- AI调用统计日表
|
||||
CREATE TABLE ai_daily_stats (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
stat_date DATE NOT NULL,
|
||||
gen_type VARCHAR(30) NOT NULL,
|
||||
model_name VARCHAR(50) DEFAULT '',
|
||||
|
||||
call_count INT DEFAULT 0,
|
||||
success_count INT DEFAULT 0,
|
||||
fail_count INT DEFAULT 0,
|
||||
|
||||
total_input_tokens BIGINT DEFAULT 0,
|
||||
total_output_tokens BIGINT DEFAULT 0,
|
||||
total_cost_cents INT DEFAULT 0,
|
||||
|
||||
avg_latency_ms INT DEFAULT 0,
|
||||
p99_latency_ms INT DEFAULT 0,
|
||||
|
||||
unique_users INT DEFAULT 0,
|
||||
|
||||
UNIQUE KEY uk_date_type (stat_date, gen_type, model_name),
|
||||
INDEX idx_date (stat_date)
|
||||
) ENGINE=InnoDB COMMENT='AI调用日统计';
|
||||
|
||||
-- ============================================
|
||||
-- 八、系统配置
|
||||
-- ============================================
|
||||
|
||||
-- 分类配置表
|
||||
CREATE TABLE categories (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
icon VARCHAR(100) DEFAULT '',
|
||||
color VARCHAR(20) DEFAULT '',
|
||||
sort_order INT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
story_count INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB COMMENT='分类配置';
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE system_configs (
|
||||
config_key VARCHAR(100) PRIMARY KEY,
|
||||
config_value TEXT NOT NULL,
|
||||
description VARCHAR(255) DEFAULT '',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB COMMENT='系统配置';
|
||||
|
||||
-- 敏感词表
|
||||
CREATE TABLE sensitive_words (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
word VARCHAR(50) NOT NULL,
|
||||
category VARCHAR(30) DEFAULT 'general',
|
||||
level TINYINT DEFAULT 1 COMMENT '1警告 2禁止',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
UNIQUE KEY uk_word (word),
|
||||
INDEX idx_category (category)
|
||||
) ENGINE=InnoDB COMMENT='敏感词表';
|
||||
132
server/sql/seed_stories_part1.sql
Normal file
132
server/sql/seed_stories_part1.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
-- 10个种子故事数据
|
||||
USE stardom_story;
|
||||
|
||||
-- 1. 都市言情:《总裁的替身新娘》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(1, '总裁的替身新娘', '', '一场阴差阳错的婚礼,让平凡的你成为了霸道总裁的替身新娘。他冷漠、神秘,却在深夜对你展现出不一样的温柔...', '都市言情', 15680, 3420, TRUE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(1, 1, 'start', '婚礼当天,你穿着洁白的婚纱站在镜子前。这本该是你姐姐的婚礼,但她在昨晚逃婚了,留下一封信让你代替她嫁给陆氏集团的继承人——陆景深。\n\n"小姐,时间到了。"管家的声音从门外传来。\n\n你深吸一口气,推开了门。婚礼大厅里,那个男人背对着你站在红毯的尽头,高大的身影透着不可一世的气势。', '旁白', FALSE, '', 0, '', 1),
|
||||
(2, 1, 'choice1_a', '你缓缓走向他,每一步都像踩在云端。当你站定在他身侧,他终于转过身来——剑眉星目,薄唇紧抿,眼神冰冷得像是能将人冻结。\n\n"你不是林诗韵。"他低沉的声音只有你能听见。\n\n你的心猛地一跳,他竟然认出来了!', '旁白', FALSE, '', 0, '', 2),
|
||||
(3, 1, 'choice1_b', '你转身想逃,但刚迈出一步,一只有力的手就抓住了你的手腕。\n\n"想逃?"陆景深不知何时已经走到你身后,他的声音低沉而危险,"婚礼都准备好了,你觉得你能逃到哪里去?"\n\n他的眼神冰冷,却带着一丝玩味。', '旁白', FALSE, '', 0, '', 3),
|
||||
(4, 1, 'choice2_a', '"是的,我是她的妹妹林诗语。"你决定坦白,"她...她逃婚了,我是被迫来的。"\n\n陆景深眯起眼睛,审视着你。良久,他嘴角勾起一抹意味不明的笑。\n\n"有趣。既然来了,那就继续演下去吧。"他握住你的手,转向宾客,"婚礼继续。"', '旁白', FALSE, '', 0, '', 4),
|
||||
(5, 1, 'choice2_b', '"我不知道你在说什么。"你故作镇定地否认。\n\n陆景深冷笑一声:"你的耳垂没有耳洞,林诗韵左耳有三个。"他凑近你耳边,"别以为我什么都不知道。"\n\n你浑身一僵,原来从一开始,你就没骗过他。', '旁白', FALSE, '', 0, '', 5),
|
||||
(6, 1, 'ending_good', '三个月后——\n\n"老婆,该吃药了。"陆景深端着一杯温水走进来,眼神里满是宠溺。\n\n你看着这个曾经冷漠如冰的男人,如今却甘愿为你放下所有骄傲。原来那场替身婚姻,竟成了你们爱情的开始。\n\n"陆景深,你什么时候开始喜欢我的?"\n\n他俯身在你额头落下一吻:"从你穿着婚纱走向我的那一刻。"\n\n【达成结局:真心换真情】', '旁白', TRUE, '真心换真情', 100, 'good', 6),
|
||||
(7, 1, 'ending_normal', '一年后,合约婚姻到期。\n\n你站在陆家门口,手里提着行李。这一年,他对你不好不坏,你们像最熟悉的陌生人。\n\n"林诗语。"身后传来他的声音。\n\n你没有回头:"合约到期了,谢谢你这一年的照顾。"\n\n"...保重。"\n\n你笑了笑,迈步走向新的人生。有些人,注定只是生命中的过客。\n\n【达成结局:契约终结】', '旁白', TRUE, '契约终结', 60, 'normal', 7),
|
||||
(8, 1, 'ending_bad', '你被陆家赶出了门。\n\n原来陆景深从始至终都在利用你,他需要一场婚姻来稳定公司股价,而你只是一颗棋子。\n\n"你以为我会爱上一个替身?"他的话像刀子一样扎进你的心。\n\n雨夜中,你孤独地走在街头,不知何去何从。\n\n【达成结局:棋子的悲哀】', '旁白', TRUE, '棋子的悲哀', 20, 'bad', 8);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(1, 1, '深吸一口气,走向红毯尽头', 'choice1_a', 1),
|
||||
(1, 1, '转身想要逃离', 'choice1_b', 2),
|
||||
(2, 1, '承认身份,坦白一切', 'choice2_a', 1),
|
||||
(2, 1, '否认到底,继续伪装', 'choice2_b', 2),
|
||||
(3, 1, '顺从地跟他完成婚礼', 'choice2_a', 1),
|
||||
(3, 1, '挣扎反抗,坚持要走', 'ending_bad', 2),
|
||||
(4, 1, '认真扮演妻子角色', 'ending_good', 1),
|
||||
(4, 1, '保持距离,等合约结束', 'ending_normal', 2),
|
||||
(5, 1, '向他道歉,请求原谅', 'ending_normal', 1),
|
||||
(5, 1, '恼羞成怒,甩开他离去', 'ending_bad', 2);
|
||||
|
||||
-- 2. 悬疑推理:《密室中的第四个人》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(2, '密室中的第四个人', '', '暴风雪夜,你和三个陌生人被困在一座古老的山庄里。第二天早晨,其中一人死在了密室中。凶手,就在剩下的人当中...', '悬疑推理', 12350, 2890, TRUE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(9, 2, 'start', '暴风雪已经持续了整整一夜。\n\n你和另外三个人被困在这座偏僻的山庄里——戴眼镜的中年男人王教授、穿着名牌的富家女苏小姐、还有沉默寡言的青年李明。\n\n凌晨三点,一声尖叫划破夜空。\n\n你们冲向声音来源,发现王教授倒在书房的地板上,门从里面反锁,窗户紧闭。他已经没有了呼吸。\n\n一把沾血的匕首就在他身边,但诡异的是——密室里,不可能有第四个人进入。', '旁白', FALSE, '', 0, '', 1),
|
||||
(10, 2, 'investigate_room', '你仔细检查了书房。\n\n发现几个关键线索:\n1. 王教授的手表停在2:47\n2. 窗户确实从内部锁死,没有破坏痕迹\n3. 书桌上有一杯已经凉透的咖啡,还剩半杯\n4. 地毯上有淡淡的香水味,不属于王教授\n\n"这是不可能的犯罪..."你喃喃自语。', '旁白', FALSE, '', 0, '', 2),
|
||||
(11, 2, 'question_su', '你找到了苏小姐。\n\n"昨晚你在哪里?"你问道。\n\n她的眼眶微红:"我一直在房间里。王教授...他是个好人,我们曾经是师生关系。"她的声音微微颤抖。\n\n你注意到她身上有淡淡的香水味,和书房里的一模一样。', '旁白', FALSE, '', 0, '', 3),
|
||||
(12, 2, 'question_li', '李明站在走廊尽头,表情阴沉。\n\n"你昨晚听到什么了吗?"你试探着问。\n\n"什么都没有。"他冷冷地回答,"我睡得很死。"\n\n但你注意到他的袖口有一小块暗红色的痕迹,像是没洗干净的血迹。', '旁白', FALSE, '', 0, '', 4),
|
||||
(13, 2, 'accuse_su', '"凶手是你,苏小姐。"\n\n你指向她:"书房里的香水味就是证据。你和王教授的关系不只是师生,对吗?"\n\n苏小姐脸色惨白,突然崩溃大哭:"他...他要把我们的关系公开!我不能让他毁了我的婚约!但是...但是密室是怎么形成的,我真的不知道!"', '旁白', FALSE, '', 0, '', 5),
|
||||
(14, 2, 'accuse_li', '"李明,血迹就是证据。"\n\n你直视他的眼睛。李明的表情终于出现了裂痕。\n\n"王教授...他十年前撞死了我妹妹,然后用钱摆平了一切!"他的声音充满恨意,"我等这个机会等了十年!"\n\n"但密室呢?你是怎么做到的?"\n\n他露出诡异的笑:"你难道没发现吗?这扇门的锁,是可以从外面用铁丝复位的老式锁..."', '旁白', FALSE, '', 0, '', 6),
|
||||
(15, 2, 'ending_truth', '真相大白。\n\n李明利用老式门锁的漏洞制造了密室假象,而苏小姐的到来只是巧合——她确实去找过王教授,但那时他还活着。\n\n"你很聪明。"李明被铐上手铐前,看着你说,"但即使重来一次,我也会这么做。有些仇,不能不报。"\n\n暴风雪终于停了,警车的鸣笛声由远及近。\n\n【达成结局:真相猎人】', '旁白', TRUE, '真相猎人', 100, 'good', 7),
|
||||
(16, 2, 'ending_wrong', '你指控了苏小姐。\n\n当警察到来后,真正的凶手李明趁乱逃走了。三天后,他在另一座城市落网,但已经又制造了一起"密室杀人案"。\n\n"如果你当时再仔细一点..."警探惋惜地说。\n\n你沉默了。错误的推理,让无辜的人背负嫌疑,也让真凶逍遥法外。\n\n【达成结局:错误的指控】', '旁白', TRUE, '错误的指控', 40, 'bad', 8);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(9, 2, '检查书房现场', 'investigate_room', 1),
|
||||
(9, 2, '询问苏小姐', 'question_su', 2),
|
||||
(9, 2, '询问李明', 'question_li', 3),
|
||||
(10, 2, '香水味很关键,去问苏小姐', 'question_su', 1),
|
||||
(10, 2, '继续调查其他人', 'question_li', 2),
|
||||
(11, 2, '她就是凶手!', 'accuse_su', 1),
|
||||
(11, 2, '先去问问李明', 'question_li', 2),
|
||||
(12, 2, '血迹说明一切,他是凶手', 'accuse_li', 1),
|
||||
(12, 2, '证据不足,先指控苏小姐', 'accuse_su', 2),
|
||||
(13, 2, '她在撒谎,坚持指控', 'ending_wrong', 1),
|
||||
(13, 2, '也许另有隐情,重新调查', 'question_li', 2),
|
||||
(14, 2, '确认他就是凶手', 'ending_truth', 1);
|
||||
|
||||
-- 3. 古风宫廷:《凤临天下》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(3, '凤临天下', '', '你是将军府的嫡女,一道圣旨将你送入了深宫。后宫如战场,尔虞我诈步步惊心,你能否在这深宫中存活,甚至问鼎凤位?', '古风宫廷', 18920, 4560, TRUE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(17, 3, 'start', '永安十三年,春。\n\n一道圣旨,将你从将军府送入了这座金碧辉煌却暗藏杀机的皇宫。\n\n"林家嫡女林清婉,册封为婉嫔,赐居长春宫。"\n\n你跪在殿前,锦绣华服加身,却感受不到半分暖意。父亲战死沙场的消息传来不过三日,皇上便下了这道旨意。\n\n"婉嫔娘娘,请随奴婢来。"一个小太监恭敬地说。\n\n你站起身,望向那巍峨的宫门。前路未知,步步惊心。', '旁白', FALSE, '', 0, '', 1),
|
||||
(18, 3, 'meet_emperor', '御花园中,你"偶遇"了当今圣上。\n\n他负手而立,龙袍加身,眉宇间带着天生的威严。\n\n"你就是林将军的女儿?"他的目光在你身上停留,"倒是比传闻中更出色。"\n\n你低眉顺目:"陛下谬赞。"\n\n"林将军为国捐躯,朕心甚痛。"他走近一步,"你可有什么心愿?"\n\n这是一个机会,也可能是一个陷阱。', '旁白', FALSE, '', 0, '', 2),
|
||||
(19, 3, 'meet_consort', '你在宫道上遇到了皇后的仪仗。\n\n皇后端坐轿中,珠翠环绕,气度雍容。她掀开帘子,打量着你。\n\n"你就是新来的婉嫔?"她的声音不辨喜怒,"生得倒是标致。本宫听说你父亲是林大将军?可惜了..."\n\n"谢皇后娘娘关心。"你福身行礼。\n\n"长春宫偏僻,若有什么需要,尽管来坤宁宫找本宫。"她意味深长地笑了笑,便放下了帘子。', '旁白', FALSE, '', 0, '', 3),
|
||||
(20, 3, 'choice_emperor', '"臣妾别无所求。"你温婉地回答,"只愿陛下龙体安康,江山永固。"\n\n皇帝眼中闪过一丝意外,随即笑了:"难得。宫中女子,大多急于求宠,你倒是不同。"\n\n他从袖中取出一块玉佩递给你:"这是朕的贴身之物,你且收着。"\n\n接过玉佩的那一刻,你感受到了远处无数道注视的目光,有嫉妒,有审视,更有杀意。', '旁白', FALSE, '', 0, '', 4),
|
||||
(21, 3, 'choice_queen', '你主动去了坤宁宫。\n\n"皇后娘娘,臣妾初入宫闱,不懂规矩,还请娘娘多多指点。"你恭敬地跪下。\n\n皇后打量着你,似乎在评估你的价值。\n\n"你是个聪明人。"她终于开口,"在这宫里,聪明人才能活得久。本宫缺一个能用的人,你可愿意?"\n\n这是投靠,也是站队。', '旁白', FALSE, '', 0, '', 5),
|
||||
(22, 3, 'ending_empress', '十年后——\n\n"皇后驾到!"\n\n你身着凤袍,步入太和殿。百官跪拜,三千佳丽俯首。\n\n从婉嫔到贵妃,从贵妃到皇后,这条路你走了整整十年。多少腥风血雨,多少尔虞我诈,都已成为过眼云烟。\n\n"皇后,今日是大喜之日。"皇帝握住你的手,眼中是多年来从未改变的深情。\n\n你微微一笑。凤临天下,母仪天下,这一切终于实现。\n\n【达成结局:凤临天下】', '旁白', TRUE, '凤临天下', 100, 'good', 6),
|
||||
(23, 3, 'ending_concubine', '你选择了安稳度日。\n\n长春宫虽偏僻,却也清静。你不争不抢,在这后宫的惊涛骇浪中保全了自己。\n\n二十年后,你依旧是婉嫔,看着一代代新人入宫,又看着她们在争斗中陨落。\n\n"娘娘,您不后悔吗?"宫女问你。\n\n你望着天边的晚霞,轻轻摇头:"能活着,已是幸事。"\n\n【达成结局:深宫余生】', '旁白', TRUE, '深宫余生', 60, 'normal', 7),
|
||||
(24, 3, 'ending_tragic', '你被卷入了一场宫变。\n\n无论是皇帝的宠爱还是皇后的拉拢,最终都成了你的催命符。\n\n"婉嫔通敌叛国,赐白绫。"\n\n你跪在冰冷的宫殿里,手中握着那块曾经的定情玉佩。原来从一开始,你就只是一颗棋子。\n\n"父亲..."你闭上了眼睛,"女儿来陪您了。"\n\n【达成结局:红颜薄命】', '旁白', TRUE, '红颜薄命', 20, 'bad', 8);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(17, 3, '在御花园偶遇皇上', 'meet_emperor', 1),
|
||||
(17, 3, '在宫道上遇到皇后', 'meet_consort', 2),
|
||||
(18, 3, '说只愿陛下安康', 'choice_emperor', 1),
|
||||
(18, 3, '请求为父亲平反', 'ending_tragic', 2),
|
||||
(19, 3, '主动去拜见皇后', 'choice_queen', 1),
|
||||
(19, 3, '保持距离,安分守己', 'ending_concubine', 2),
|
||||
(20, 3, '收下玉佩,争取圣宠', 'ending_empress', 1),
|
||||
(20, 3, '婉拒御赐,保持低调', 'ending_concubine', 2),
|
||||
(21, 3, '答应皇后,成为她的人', 'ending_empress', 1),
|
||||
(21, 3, '婉拒,不想卷入纷争', 'ending_concubine', 2);
|
||||
|
||||
-- 4. 校园青春:《暗恋那件小事》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(4, '暗恋那件小事', '', '高三那年,你暗恋着班上最耀眼的那个人。毕业季来临,你是否有勇气说出那句"我喜欢你"?', '校园青春', 22450, 5280, TRUE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(25, 4, 'start', '高三下学期,距离高考还有三个月。\n\n教室里弥漫着紧张的气氛,而你的目光却总是不自觉地飘向窗边第三排的位置——那里坐着沈昼,全年级第一,校篮球队队长,无数女生心中的白月光。\n\n也是你暗恋了整整三年的人。\n\n"喂,又在看沈昼?"同桌用胳膊肘捅了捅你,"你什么时候才敢表白啊?"\n\n"别闹..."你赶紧低下头,心跳却已经漏了一拍。\n\n这时,沈昼突然回过头,目光与你相撞。你慌乱地移开视线,脸烧得发烫。', '旁白', FALSE, '', 0, '', 1),
|
||||
(26, 4, 'library', '放学后,你在图书馆复习。\n\n"这里有人吗?"一个熟悉的声音在头顶响起。\n\n你抬起头,发现沈昼就站在你面前,手里拿着几本书。整个图书馆只剩你们两个人。\n\n"没...没有。"你的声音小得像蚊子叫。\n\n他在你对面坐下,专注地翻开书本。你完全看不进去任何东西,满脑子都是他的睫毛好长、他闻起来好香...\n\n突然,他抬起头:"你数学是不是不太好?我看你那道题写了很久。"', '旁白', FALSE, '', 0, '', 2),
|
||||
(27, 4, 'basketball', '篮球赛上,你被同桌拉来当啦啦队。\n\n沈昼在场上奔跑跳跃,汗水浸湿了他的球衣。每一次投篮,都引来女生们的尖叫。\n\n比赛结束,他们班赢了。沈昼朝这边走来,你下意识想躲,却被同桌按住。\n\n"这个给你。"他递过来一瓶水,"刚才听到你喊加油了。"\n\n"我...我没有..."你语无伦次。\n\n他笑了,眼睛弯成了月牙:"骗人,我明明看到了。"', '旁白', FALSE, '', 0, '', 3),
|
||||
(28, 4, 'rooftop', '毕业典礼后,你收到一张字条:\n\n"天台见。——沈昼"\n\n你的心跳快得像要爆炸。踏上天台的那一刻,夕阳正好洒满整个城市。沈昼背对着你,校服被风吹起。\n\n"你...找我有事吗?"你小心翼翼地开口。\n\n他转过身,认真地看着你:"有件事,我想了三年,今天必须告诉你。"', '旁白', FALSE, '', 0, '', 4),
|
||||
(29, 4, 'confess_yes', '"我也...我也喜欢你!"你鼓起勇气,抢先说出了口。\n\n沈昼愣了一秒,然后露出了你从未见过的灿烂笑容。\n\n"笨蛋,我还没说完呢。"他走近,轻轻牵起你的手,"我喜欢你,从高一第一次看到你在图书馆睡着开始。"\n\n夕阳下,两个人的影子交叠在一起。\n\n"我们在一起吧。"\n\n【达成结局:双向奔赴】', '旁白', TRUE, '双向奔赴', 100, 'good', 5),
|
||||
(30, 4, 'confess_no', '你没有说话,沉默地低下了头。\n\n"我知道你喜欢我。"沈昼说,"其实,我也..."\n\n"别说了。"你打断他,"我们...还是做普通同学吧。高考要紧。"\n\n你转身跑下了天台,泪水模糊了视线。你告诉自己,这是为了你们两个好。\n\n多年后,你总会想起那个夕阳下的天台,和那个没能说出口的"我喜欢你"。\n\n【达成结局:错过的心动】', '旁白', TRUE, '错过的心动', 40, 'normal', 6),
|
||||
(31, 4, 'ending_friends', '高考结束后,你们考上了不同的大学。\n\n那三年的暗恋,最终还是没能说出口。你们成了普通朋友,偶尔在同学群里聊几句,仅此而已。\n\n十年后的同学聚会,你看到他带着妻子出现。他还是那么耀眼,而你只能在心里默默说一句:\n\n"我曾经很喜欢你。"\n\n【达成结局:藏在心底的秘密】', '旁白', TRUE, '藏在心底的秘密', 30, 'bad', 7);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(25, 4, '放学去图书馆学习', 'library', 1),
|
||||
(25, 4, '去看篮球比赛', 'basketball', 2),
|
||||
(26, 4, '请他教你数学', 'rooftop', 1),
|
||||
(26, 4, '说不用,自己能行', 'ending_friends', 2),
|
||||
(27, 4, '接过水,道谢', 'rooftop', 1),
|
||||
(27, 4, '害羞跑开', 'ending_friends', 2),
|
||||
(28, 4, '鼓起勇气说出喜欢', 'confess_yes', 1),
|
||||
(28, 4, '沉默不语', 'confess_no', 2);
|
||||
|
||||
-- 5. 修仙玄幻:《废柴逆袭录》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(5, '废柴逆袭录', '', '身为青云宗的废柴弟子,你意外获得上古传承。从此开始了一段热血逆袭之路!', '修仙玄幻', 16780, 3890, TRUE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(32, 5, 'start', '青云宗,外门弟子居住的杂役院。\n\n你叫陈风,入宗三年,修为停滞在炼气一层,被同门嘲笑为"废柴"。今日是宗门大比,你照例被师兄们呼来喝去地搬运物资。\n\n"陈风,把这些灵石搬去演武场!动作快点!"\n\n你默默扛起沉重的箱子,路过一处山洞时,脚下突然踩空,整个人滚落洞中。\n\n黑暗中,一道金光直冲你的眉心——\n\n"小子,你的机缘到了。"一个苍老的声音在脑海中响起。', '旁白', FALSE, '', 0, '', 1),
|
||||
(33, 5, 'inheritance', '你发现自己得到了一位上古强者的传承!\n\n"老夫是万年前的天元大帝,这部《混元诀》是我毕生心血。"那声音说道,"你资质平平,但心性不错。好好修炼,或可成就大道。"\n\n你感到一股浑厚的力量涌入体内,三年停滞的修为瞬间突破,炼气二层...三层...五层!\n\n"小子,收敛气息,别让人发现。"天元大帝提醒道。', '旁白', FALSE, '', 0, '', 2),
|
||||
(34, 5, 'challenge', '宗门大比上,你的死对头——内门弟子赵天龙公然挑衅。\n\n"废柴陈风,敢不敢和我打一场?"他嚣张地笑着,"你要是赢了,我给你当一个月奴仆!"\n\n周围响起阵阵哄笑。所有人都在等着看你的笑话。\n\n天元大帝的声音响起:"小子,要不要教训教训他?以你现在的实力,轻松碾压。"', '旁白', FALSE, '', 0, '', 3),
|
||||
(35, 5, 'show_power', '"我接受挑战!"\n\n你踏上擂台,赵天龙根本没把你放在眼里,漫不经心地出了一拳。\n\n你侧身躲过,反手一掌!\n\n轰!\n\n赵天龙直接飞出擂台,撞碎了三根石柱才停下。全场鸦雀无声。\n\n"这...这怎么可能!"赵天龙满脸不可置信。\n\n"还有谁?"你负手而立,气势如虹。', '旁白', FALSE, '', 0, '', 4),
|
||||
(36, 5, 'hide_power', '你装作害怕的样子,转身就跑。\n\n"哈哈哈!看到没,就是个怂包!"赵天龙大笑。\n\n但天元大帝却赞许道:"不错,懂得韬光养晦。真正的强者,不需要逞一时之勇。等你筑基之后,再让他们看看什么叫脱胎换骨。"\n\n你暗暗握拳,总有一天,你会让所有人刮目相看。', '旁白', FALSE, '', 0, '', 5),
|
||||
(37, 5, 'ending_immortal', '百年后——\n\n你已成为修真界的传奇,"天元真君"的名号响彻九州。\n\n青云宗早已被你踩在脚下,当年嘲笑你的人,如今只能仰望你的背影。\n\n"师尊,您当年为何愿意收我这个废柴为徒?"弟子问道。\n\n你望着云海,想起了那个山洞里改变命运的夜晚。\n\n"因为,成大事者,从来不在起点,而在终点。"\n\n【达成结局:问鼎仙途】', '旁白', TRUE, '问鼎仙途', 100, 'good', 6),
|
||||
(38, 5, 'ending_mortal', '你选择放弃修炼,回归凡尘。\n\n天元大帝的传承太过沉重,你只想做一个普通人。\n\n多年后,你成为了一个小镇上的郎中,治病救人,娶妻生子。\n\n偶尔,你会抬头望向天空中划过的剑光,想起那段短暂的修仙岁月。\n\n"平凡,也是一种幸福吧。"你笑着给孙子讲起了那些关于仙人的故事。\n\n【达成结局:归于平凡】', '旁白', TRUE, '归于平凡', 50, 'normal', 7);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(32, 5, '接受传承', 'inheritance', 1),
|
||||
(32, 5, '拒绝传承', 'ending_mortal', 2),
|
||||
(33, 5, '参加宗门大比', 'challenge', 1),
|
||||
(33, 5, '低调修炼不张扬', 'ending_immortal', 2),
|
||||
(34, 5, '接受挑战,展示实力', 'show_power', 1),
|
||||
(34, 5, '装怂跑路', 'hide_power', 2),
|
||||
(35, 5, '继续高调挑战强者', 'ending_immortal', 1),
|
||||
(35, 5, '见好就收,低调修炼', 'ending_immortal', 2),
|
||||
(36, 5, '潜心修炼,等待时机', 'ending_immortal', 1),
|
||||
(36, 5, '受不了嘲讽,主动退宗', 'ending_mortal', 2);
|
||||
|
||||
-- 6-10故事继续插入...
|
||||
105
server/sql/seed_stories_part2.sql
Normal file
105
server/sql/seed_stories_part2.sql
Normal file
@@ -0,0 +1,105 @@
|
||||
-- 继续插入6-10号种子故事
|
||||
USE stardom_story;
|
||||
|
||||
-- 6. 穿越重生:《回到高考前一天》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(6, '回到高考前一天', '', '车祸瞬间,你睁开眼发现自己回到了十年前——高考前一天。这一次,你能改变命运吗?', '穿越重生', 19870, 4230, FALSE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(39, 6, 'start', '刺眼的白光之后,你猛然睁开眼。\n\n熟悉的天花板,熟悉的旧风扇,熟悉的高中宿舍...床头的日历赫然写着:2016年6月6日。\n\n高考前一天。\n\n你愣住了。十年前的自己,因为一场失误与985失之交臂,从此走上了完全不同的人生轨迹。\n\n而现在...你回来了。\n\n手机震动,是妈妈的短信:"儿子,明天好好考,妈相信你!"\n\n你握紧手机,这一次,绝不能重蹈覆辙。', '旁白', FALSE, '', 0, '', 1),
|
||||
(40, 6, 'choice_study', '你决定抓紧最后的时间复习。\n\n记忆中,理综有一道大题你当年没做出来,白白丢了15分。那道题,是关于电磁感应的...\n\n你翻开物理课本,十年后的你已经是一名工程师,这种题目早已烂熟于心。你把解题思路和关键公式重新梳理了一遍。\n\n"这次,一定能做出来。"\n\n室友探头问:"都考前一天了你还看书?不如出去放松放松?"', '旁白', FALSE, '', 0, '', 2),
|
||||
(41, 6, 'choice_relax', '你决定出去走走,顺便见一些重要的人。\n\n十年后,你最后悔的不是高考成绩,而是失去的人和错过的机会。\n\n你想起了隔壁班的林小雨——她后来因为抑郁症退学了。那时候如果有人关心她一下,也许结局会不同。\n\n还有爷爷,高考后的那个暑假,他突发心梗去世了。你连最后一面都没见到。\n\n"我该先去见谁?"', '旁白', FALSE, '', 0, '', 3),
|
||||
(42, 6, 'visit_girl', '你在校门口找到了林小雨。\n\n她正一个人坐在长椅上发呆,表情木然。你记得,原本的历史中,你从未注意过她。\n\n"林小雨?"你走过去,"明天就高考了,你还好吗?"\n\n她抬起头,眼眶微红:"我...我觉得我考不好。我爸妈说,考不上好大学就断绝关系..."\n\n你看着她,心里一阵酸楚。当年她就是因为压力太大才崩溃的。', '旁白', FALSE, '', 0, '', 4),
|
||||
(43, 6, 'ending_perfect', '十年后——\n\n你坐在自己的公司里,看着窗外的车水马龙。\n\n高考那天,你超常发挥,考上了清华。但更重要的是,你改变了很多人的命运。\n\n林小雨后来成了心理医生,专门帮助有心理问题的青少年。你在她的朋友圈里看到她阳光灿烂的笑容。\n\n爷爷在你的提醒下去做了体检,及时发现了心脏问题,现在已经八十多岁,身体依然硬朗。\n\n"重来一次,真好。"\n\n【达成结局:完美逆转】', '旁白', TRUE, '完美逆转', 100, 'good', 5),
|
||||
(44, 6, 'ending_changed', '你改变了一些事情,但代价是高考失利。\n\n为了开导林小雨,你熬夜陪她聊天,第二天考试时昏昏沉沉。你的分数比原本的历史还低了20分。\n\n但林小雨考上了大学,后来成了你最好的朋友。\n\n"后悔吗?"多年后她问你。\n\n你笑着摇头:"分数只是一时的,朋友是一辈子的。"\n\n【达成结局:无悔的选择】', '旁白', TRUE, '无悔的选择', 70, 'normal', 6);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(39, 6, '抓紧时间复习', 'choice_study', 1),
|
||||
(39, 6, '出去见重要的人', 'choice_relax', 2),
|
||||
(40, 6, '继续复习到深夜', 'ending_perfect', 1),
|
||||
(40, 6, '出去见见朋友', 'choice_relax', 2),
|
||||
(41, 6, '去找林小雨', 'visit_girl', 1),
|
||||
(41, 6, '回去继续复习', 'ending_perfect', 2),
|
||||
(42, 6, '陪她聊天开导她', 'ending_changed', 1),
|
||||
(42, 6, '简单鼓励后回去复习', 'ending_perfect', 2);
|
||||
|
||||
-- 7. 职场商战:《逆风翻盘》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(7, '逆风翻盘', '', '你被陷害失去了一切,公司、名誉、爱人。三年后,你带着复仇之心重返商场...', '职场商战', 14560, 3120, FALSE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(45, 7, 'start', '三年前,你是年轻有为的创业者,星辰科技的创始人。\n\n直到那一天——你的合伙人兼好友周明背叛了你。他联合董事会,以莫须有的罪名将你踢出公司,还抢走了你的未婚妻。\n\n三年后的今天,你改头换面,以"陈墨"的身份重返商界。你的新公司"破晓资本"已经估值百亿。\n\n"周总,您的三点有人预约了见面。"\n\n你看着周明办公室的方向,嘴角勾起一抹冷笑。\n\n游戏,开始了。', '旁白', FALSE, '', 0, '', 1),
|
||||
(46, 7, 'meeting', '会议室里,周明正襟危坐。他比三年前胖了不少,眼神里多了几分世故和贪婪。\n\n"陈总,久仰大名。"他殷勤地握住你的手,"破晓资本愿意投资我们,实在是星辰的荣幸。"\n\n他没认出你。三年的风霜和精心的改变,让你判若两人。\n\n"周总客气了。"你微微一笑,"不过投资之前,我想先了解一下贵公司的核心业务...以及一些历史问题。"\n\n周明的笑容僵了一秒:"历史问题?"', '旁白', FALSE, '', 0, '', 2),
|
||||
(47, 7, 'expose', '你将一份文件扔在桌上。\n\n"周明,还记得三年前你是怎么踢走原创始人的吗?"你摘下眼镜,直视他的眼睛。\n\n周明瞳孔骤缩:"你...你是..."\n\n"对,我就是被你陷害的那个人。"你站起身,"这份文件是你当年伪造证据、贿赂董事的全部证据。明天,它会出现在所有媒体的头条上。"\n\n周明脸色惨白,双腿发软跪倒在地:"求你...给我一条活路..."', '旁白', FALSE, '', 0, '', 3),
|
||||
(48, 7, 'ending_revenge', '周明入狱了,星辰科技被你收购。\n\n你坐在曾经属于自己的办公室里,看着窗外的夜景。复仇成功了,但你并没有想象中那么开心。\n\n"值得吗?"秘书问你。\n\n你沉默良久:"三年的仇恨终于放下了。但我也明白了,最好的复仇不是毁掉别人,而是让自己过得比他们好。"\n\n【达成结局:王者归来】', '旁白', TRUE, '王者归来', 100, 'good', 4),
|
||||
(49, 7, 'ending_forgive', '你看着跪在地上的周明,最终收回了那份文件。\n\n"滚吧。"你冷冷地说,"这次放过你,是为了曾经的兄弟情。但如果你再敢做伤害别人的事,我不会再手软。"\n\n周明狼狈地逃走了。你没有毁掉他,而是选择了放手。\n\n三年后,你的公司成为行业龙头。而周明的公司因为经营不善倒闭了。\n\n"最好的复仇,是活得比他好。"你笑着说。\n\n【达成结局:格局之上】', '旁白', TRUE, '格局之上', 80, 'good', 5);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(45, 7, '主动约见周明', 'meeting', 1),
|
||||
(45, 7, '先暗中收集更多证据', 'meeting', 2),
|
||||
(46, 7, '直接摊牌,公布证据', 'expose', 1),
|
||||
(46, 7, '继续伪装,慢慢渗透', 'expose', 2),
|
||||
(47, 7, '彻底毁掉他', 'ending_revenge', 1),
|
||||
(47, 7, '放他一马', 'ending_forgive', 2);
|
||||
|
||||
-- 8. 科幻未来:《2099最后一班地铁》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(8, '2099最后一班地铁', '', '2099年的最后一天,你登上了末班地铁。车厢里只有你和一个神秘的女孩。她说,这趟地铁会带你去任何你想去的地方...', '科幻未来', 11230, 2670, FALSE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(50, 8, 'start', '2099年12月31日,23:45。\n\n你独自站在空荡荡的地铁站台,霓虹灯闪烁着"末班车"的字样。这是人类在地球上的最后一年——明天,最后一批移民船将驶向火星。\n\n地铁门打开,车厢里空无一人,只有一个穿白裙的女孩坐在角落,看着窗外。\n\n"你好。"她转头看你,眼睛明亮得不像这个时代的人,"你是最后一个乘客了。"\n\n"这趟地铁...去哪?"你问。\n\n她微微一笑:"你想去哪,它就去哪。"', '旁白', FALSE, '', 0, '', 1),
|
||||
(51, 8, 'talk_girl', '"你是谁?"你在她对面坐下。\n\n"我叫Zero。"她说,"我是这趟地铁的管理员。它已经运行了五百年,送走了无数乘客。"\n\n"五百年?那岂不是从2599年..."\n\n"不,是从1599年。"她平静地说,"这趟地铁可以穿越时间。每一个迷失的灵魂,都能在这里找到他想去的时代。"\n\n你愣住了。\n\n"所以..."她看着你,"你想去哪里?过去,还是未来?"', '旁白', FALSE, '', 0, '', 2),
|
||||
(52, 8, 'choose_past', '"我想回到过去。"你说,"回到一切还没有毁灭的时候。"\n\n地铁开始加速,窗外的灯光变成了流动的光线。\n\n"你想回到哪一年?"Zero问。\n\n你想起了很多:2050年,地球最后一片森林消失的那天;2030年,你第一次失去亲人的那天;还是更早,人类还充满希望的21世纪初?\n\n"我想...回到还能改变什么的时候。"', '旁白', FALSE, '', 0, '', 3),
|
||||
(53, 8, 'ending_past', '地铁停下了。\n\n车门打开,阳光照进来。你看到了蓝天白云,看到了绿色的树木,看到了街上熙熙攘攘的人群。\n\n"这是2024年。"Zero说,"人类还有机会的时候。"\n\n"我...真的能改变什么吗?"\n\n"一个人的力量很小,但蝴蝶效应是真实的。"她微微一笑,"去吧,做你觉得对的事。"\n\n你踏出车门,带着来自未来的记忆,开始了新的人生。\n\n【达成结局:时间旅人】', '旁白', TRUE, '时间旅人', 100, 'good', 4),
|
||||
(54, 8, 'ending_future', '"我想去未来。"你说,"去看看人类最终会变成什么样。"\n\n地铁穿越星海,最终停在一个你从未见过的世界。\n\n这里的人类已经不再是人类——他们与机器融合,与宇宙合一,成为了新的生命形式。\n\n"欢迎来到永恒。"Zero说,"这是十万年后的未来。"\n\n你望着这个陌生而美丽的世界,第一次感受到了真正的希望。\n\n【达成结局:永恒之旅】', '旁白', TRUE, '永恒之旅', 90, 'good', 5);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(50, 8, '和女孩交谈', 'talk_girl', 1),
|
||||
(50, 8, '保持沉默,看向窗外', 'talk_girl', 2),
|
||||
(51, 8, '选择回到过去', 'choose_past', 1),
|
||||
(51, 8, '选择去往未来', 'ending_future', 2),
|
||||
(52, 8, '回到2024年', 'ending_past', 1),
|
||||
(52, 8, '回到更早的时候', 'ending_past', 2);
|
||||
|
||||
-- 9. 恐怖惊悚:《第七夜》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(9, '第七夜', '', '你入住了一家偏僻的旅馆,老板娘警告你:千万不要在第七天午夜离开房间...', '恐怖惊悚', 13450, 2980, FALSE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(55, 9, 'start', '暴雨如注的夜晚,你的车在山路上抛锚了。\n\n你艰难地走到一家看起来很古老的旅馆。门口的老板娘用奇怪的眼神打量着你。\n\n"只剩最后一间房了。"她递过来一把生锈的钥匙,"307房间。记住,住满七天之前,千万不要在午夜离开房间。"\n\n"为什么?"你问。\n\n她没有回答,只是转身消失在走廊深处。\n\n你推开307的门,一股霉味扑面而来。墙上的日历停在了七年前...', '旁白', FALSE, '', 0, '', 1),
|
||||
(56, 9, 'night_sound', '第三天午夜,你被一阵脚步声惊醒。\n\n沙沙...沙沙...\n\n脚步声在走廊里徘徊,时而远,时而近。然后,它停在了你的门口。\n\n咚,咚,咚。\n\n有人敲门。\n\n"有人在吗?"一个女孩的声音传来,"我迷路了...好冷...能让我进去吗?"\n\n你屏住呼吸。透过门缝,你隐约看到门外站着一个穿白裙的身影...', '旁白', FALSE, '', 0, '', 2),
|
||||
(57, 9, 'open_door', '你打开了门。\n\n门外空无一人。走廊里漆黑一片,只有尽头的灯在忽明忽暗地闪烁。\n\n然后,你听到了背后传来的声音。\n\n"谢谢你...开门..."\n\n你猛然回头,看到床上躺着一个苍白的女孩,她的脸上挂着诡异的微笑。\n\n"终于...有人愿意陪我了..."', '旁白', FALSE, '', 0, '', 3),
|
||||
(58, 9, 'not_open', '你没有开门,死死地盯着那道门。\n\n敲门声持续了整整一个小时,然后消失了。你一夜未眠。\n\n第七天终于到了。当第一缕阳光照进房间的那一刻,你几乎是冲出了旅馆。\n\n老板娘站在门口,表情复杂地看着你:"你是第一个活着离开的人。"\n\n"那个女孩...是什么?"\n\n"七年前,一个女孩在这里被杀了。从此每隔七天,她就会出现..."', '旁白', FALSE, '', 0, '', 4),
|
||||
(59, 9, 'ending_death', '你感到一阵刺骨的寒冷。\n\n女孩的手穿过你的胸膛,你看到自己的身体变得透明...\n\n"不要怕...和我一起留在这里吧...永远..."\n\n第二天早晨,老板娘打开307的门,发现床上躺着你冰冷的尸体,脸上挂着恐惧的表情。\n\n而日历上,又多了一道血红的记号。\n\n【达成结局:永恒的旅客】', '旁白', TRUE, '永恒的旅客', 20, 'bad', 5),
|
||||
(60, 9, 'ending_escape', '你跑出了旅馆,再也没有回头。\n\n后来你查到,那家旅馆在一年后被大火烧毁了。废墟中发现了数十具白骨——都是历年来失踪的旅客。\n\n那个女孩的诅咒,终于随着大火一起消散了。\n\n但每当午夜梦回,你依然会想起那个声音:\n\n"能让我进去吗?"\n\n【达成结局:劫后余生】', '旁白', TRUE, '劫后余生', 80, 'good', 6);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(55, 9, '躺下休息', 'night_sound', 1),
|
||||
(55, 9, '检查一下房间', 'night_sound', 2),
|
||||
(56, 9, '打开门看看', 'open_door', 1),
|
||||
(56, 9, '不开门,假装没听到', 'not_open', 2),
|
||||
(57, 9, '试图逃跑', 'ending_death', 1),
|
||||
(57, 9, '放弃挣扎', 'ending_death', 2),
|
||||
(58, 9, '赶紧离开这里', 'ending_escape', 1);
|
||||
|
||||
-- 10. 搞笑轻喜:《我的室友是只猫》
|
||||
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
|
||||
(10, '我的室友是只猫', '', '某天醒来,你发现你的室友变成了一只会说话的猫!更可怕的是,你们还得一起完成期末考试...', '搞笑轻喜', 20560, 5670, TRUE);
|
||||
|
||||
INSERT INTO story_nodes (id, story_id, node_key, content, speaker, is_ending, ending_name, ending_score, ending_type, sort_order) VALUES
|
||||
(61, 10, 'start', '"喵~"\n\n你迷迷糊糊地睁开眼,发现一只橘色的大猫正趴在你脸上。\n\n"你干嘛呢!"你一把把它推开,"我室友的猫怎么跑我床上来了...等等,我室友好像没养猫?"\n\n"笨蛋,我就是你室友啊!"猫开口说话了。\n\n你愣了三秒,然后发出了杀猪般的尖叫。\n\n"吵死了!"猫翻了个白眼,"我也不知道怎么回事,早上起来就变成这样了。而且明天就是期末考试,你说怎么办!"', '旁白', FALSE, '', 0, '', 1),
|
||||
(62, 10, 'panic', '"你...你...你怎么变成猫了!"你抓狂地来回走动。\n\n"我怎么知道!"猫跳上桌子,焦躁地甩尾巴,"可能是昨晚吃的那个外卖有问题?我就说那家新开的黑暗料理店不靠谱..."\n\n"所以你是吃了什么变成猫的?"\n\n"好像是...猫咪意面?"猫心虚地舔了舔爪子。\n\n你无语地看着它。你的室友,真的是个大冤种。', '旁白', FALSE, '', 0, '', 2),
|
||||
(63, 10, 'exam_plan', '"算了,先想想怎么应对期末考试吧。"你叹了口气。\n\n"我有个主意!"猫兴奋地跳起来,"你把我藏在衣服里带进考场,我给你念答案!"\n\n"你一只猫能念什么答案啊!"\n\n"我虽然变成了猫,但脑子还是人类的脑子!高数我可是年级前十!"\n\n你思考了一下...这似乎是个好主意?但被抓到就完蛋了。', '旁白', FALSE, '', 0, '', 3),
|
||||
(64, 10, 'bring_cat', '考试当天,你把室友(猫)藏在外套里。\n\n"《高等数学》考试现在开始!"\n\n你打开试卷,第一题就懵了。猫在你怀里小声说:"第一题选B...第二题是求导..."\n\n一切都很顺利,直到监考老师走过来。\n\n"这位同学,你衣服里怎么有声音?"', '旁白', FALSE, '', 0, '', 4),
|
||||
(65, 10, 'ending_funny', '"喵~"猫突然跳出来,假装是一只普通的猫,在监考老师腿上蹭来蹭去。\n\n"哎呀,哪来的猫,好可爱~"监考老师完全被萌化了,忘记了刚才的疑问。\n\n猫冲你眨了眨眼。你憋着笑继续答题。\n\n期末考试,你拿了满分。\n\n三天后,室友恢复了人形。他决定:以后再也不吃黑暗料理了。\n\n【达成结局:作弊猫的胜利】', '旁白', TRUE, '作弊猫的胜利', 100, 'good', 5),
|
||||
(66, 10, 'ending_caught', '"同学,你在作弊!"\n\n监考老师一把抓住了从你衣服里钻出来的猫。\n\n"等等,我没有!这只是...呃...我的情感支持猫!"\n\n"情感支持猫?会说话的情感支持猫?"\n\n全考场的人都看向你们。猫一脸生无可恋:"喵...我只是一只普通的猫...喵..."\n\n你被记了处分,室友(猫)被没收关了三天。\n\n【达成结局:全剧终,作弊必被抓】', '旁白', TRUE, '作弊的代价', 30, 'bad', 6);
|
||||
|
||||
INSERT INTO story_choices (node_id, story_id, text, next_node_key, sort_order) VALUES
|
||||
(61, 10, '冷静下来问清楚', 'panic', 1),
|
||||
(61, 10, '继续尖叫', 'panic', 2),
|
||||
(62, 10, '想办法应对期末考试', 'exam_plan', 1),
|
||||
(62, 10, '先带它去看兽医', 'exam_plan', 2),
|
||||
(63, 10, '接受室友的主意', 'bring_cat', 1),
|
||||
(63, 10, '老老实实自己考', 'ending_caught', 2),
|
||||
(64, 10, '让猫装作普通猫', 'ending_funny', 1),
|
||||
(64, 10, '慌张解释', 'ending_caught', 2);
|
||||
Reference in New Issue
Block a user