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

13
server/.env.example Normal file
View 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
View 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
View 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
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;

134
server/models/user.js Normal file
View 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

File diff suppressed because it is too large Load Diff

20
server/package.json Normal file
View 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
View 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
View 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
View 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
View 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 '作者ID0表示官方',
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
View 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 '作者ID0为官方',
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='敏感词表';

View 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故事继续插入...

View 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);