feat: 添加测试用户到种子数据, AI改写功能优化, 前端联调修复

This commit is contained in:
2026-03-09 23:00:15 +08:00
parent 5e931424ab
commit 9948ccba8f
12 changed files with 1082 additions and 151 deletions

View File

@@ -32,6 +32,24 @@ class CreateDraftRequest(BaseModel):
prompt: str
class CreateEndingDraftRequest(BaseModel):
"""结局改写请求"""
userId: int
storyId: int
endingName: str
endingContent: str
prompt: str
class ContinueEndingDraftRequest(BaseModel):
"""结局续写请求"""
userId: int
storyId: int
endingName: str
endingContent: str
prompt: str
class DraftResponse(BaseModel):
id: int
storyId: int
@@ -120,6 +138,174 @@ async def process_ai_rewrite(draft_id: int):
pass
async def process_ai_rewrite_ending(draft_id: int):
"""后台异步处理AI改写结局"""
from app.database import async_session_factory
from app.services.ai import ai_service
import json
import re
async with async_session_factory() as db:
try:
# 获取草稿
result = await db.execute(select(StoryDraft).where(StoryDraft.id == draft_id))
draft = result.scalar_one_or_none()
if not draft:
return
# 更新状态为处理中
draft.status = DraftStatus.processing
await db.commit()
# 获取故事信息
story_result = await db.execute(select(Story).where(Story.id == draft.story_id))
story = story_result.scalar_one_or_none()
if not story:
draft.status = DraftStatus.failed
draft.error_message = "故事不存在"
draft.completed_at = datetime.now()
await db.commit()
return
# 从 path_history 获取结局信息
ending_info = draft.path_history or {}
ending_name = ending_info.get("endingName", "未知结局")
ending_content = ending_info.get("endingContent", "")
# 调用AI服务改写结局
ai_result = await ai_service.rewrite_ending(
story_title=story.title,
story_category=story.category or "未知",
ending_name=ending_name,
ending_content=ending_content,
user_prompt=draft.user_prompt
)
if ai_result and ai_result.get("content"):
content = ai_result["content"]
new_ending_name = f"{ending_name}AI改写"
# 尝试解析 JSON 格式的返回
try:
json_match = re.search(r'\{[^{}]*"ending_name"[^{}]*"content"[^{}]*\}', content, re.DOTALL)
if json_match:
parsed = json.loads(json_match.group())
new_ending_name = parsed.get("ending_name", new_ending_name)
content = parsed.get("content", content)
else:
parsed = json.loads(content)
new_ending_name = parsed.get("ending_name", new_ending_name)
content = parsed.get("content", content)
except (json.JSONDecodeError, AttributeError):
pass
# 成功 - 存储为单节点结局格式
draft.status = DraftStatus.completed
draft.ai_nodes = [{
"nodeKey": "ending_rewrite",
"content": content,
"speaker": "旁白",
"isEnding": True,
"endingName": new_ending_name,
"endingType": "rewrite"
}]
draft.entry_node_key = "ending_rewrite"
draft.tokens_used = ai_result.get("tokens_used", 0)
draft.title = f"{story.title}-{new_ending_name}"
else:
draft.status = DraftStatus.failed
draft.error_message = "AI服务暂时不可用"
draft.completed_at = datetime.now()
await db.commit()
except Exception as e:
print(f"[process_ai_rewrite_ending] 异常: {e}")
import traceback
traceback.print_exc()
try:
draft.status = DraftStatus.failed
draft.error_message = str(e)[:500]
draft.completed_at = datetime.now()
await db.commit()
except:
pass
async def process_ai_continue_ending(draft_id: int):
"""后台异步处理AI续写结局"""
from app.database import async_session_factory
from app.services.ai import ai_service
async with async_session_factory() as db:
try:
# 获取草稿
result = await db.execute(select(StoryDraft).where(StoryDraft.id == draft_id))
draft = result.scalar_one_or_none()
if not draft:
return
# 更新状态为处理中
draft.status = DraftStatus.processing
await db.commit()
# 获取故事信息
story_result = await db.execute(select(Story).where(Story.id == draft.story_id))
story = story_result.scalar_one_or_none()
if not story:
draft.status = DraftStatus.failed
draft.error_message = "故事不存在"
draft.completed_at = datetime.now()
await db.commit()
return
# 从 path_history 获取结局信息
ending_info = draft.path_history or {}
ending_name = ending_info.get("endingName", "未知结局")
ending_content = ending_info.get("endingContent", "")
# 调用AI服务续写结局
ai_result = await ai_service.continue_ending(
story_title=story.title,
story_category=story.category or "未知",
ending_name=ending_name,
ending_content=ending_content,
user_prompt=draft.user_prompt
)
if ai_result and ai_result.get("nodes"):
# 成功 - 存储多节点分支格式
draft.status = DraftStatus.completed
draft.ai_nodes = ai_result["nodes"]
draft.entry_node_key = ai_result.get("entryNodeKey", "continue_1")
draft.tokens_used = ai_result.get("tokens_used", 0)
draft.title = f"{story.title}-{ending_name}续写"
else:
draft.status = DraftStatus.failed
draft.error_message = "AI服务暂时不可用"
draft.completed_at = datetime.now()
await db.commit()
except Exception as e:
print(f"[process_ai_continue_ending] 异常: {e}")
import traceback
traceback.print_exc()
try:
draft.status = DraftStatus.failed
draft.error_message = str(e)[:500]
draft.completed_at = datetime.now()
await db.commit()
except:
pass
# ============ API 路由 ============
@router.post("")
@@ -173,6 +359,96 @@ async def create_draft(
}
@router.post("/ending")
async def create_ending_draft(
request: CreateEndingDraftRequest,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
"""提交AI改写结局任务异步处理"""
if not request.prompt:
raise HTTPException(status_code=400, detail="请输入改写指令")
# 获取故事信息
result = await db.execute(select(Story).where(Story.id == request.storyId))
story = result.scalar_one_or_none()
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
# 创建草稿记录,将结局信息存在 path_history
draft = StoryDraft(
user_id=request.userId,
story_id=request.storyId,
title=f"{story.title}-结局改写",
path_history={"endingName": request.endingName, "endingContent": request.endingContent},
current_node_key="ending",
current_content=request.endingContent,
user_prompt=request.prompt,
status=DraftStatus.pending
)
db.add(draft)
await db.commit()
await db.refresh(draft)
# 添加后台任务
background_tasks.add_task(process_ai_rewrite_ending, draft.id)
return {
"code": 0,
"data": {
"draftId": draft.id,
"message": "已提交AI正在生成新结局..."
}
}
@router.post("/continue-ending")
async def create_continue_ending_draft(
request: ContinueEndingDraftRequest,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
"""提交AI续写结局任务异步处理"""
if not request.prompt:
raise HTTPException(status_code=400, detail="请输入续写指令")
# 获取故事信息
result = await db.execute(select(Story).where(Story.id == request.storyId))
story = result.scalar_one_or_none()
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
# 创建草稿记录,将结局信息存在 path_history
draft = StoryDraft(
user_id=request.userId,
story_id=request.storyId,
title=f"{story.title}-结局续写",
path_history={"endingName": request.endingName, "endingContent": request.endingContent},
current_node_key="ending",
current_content=request.endingContent,
user_prompt=request.prompt,
status=DraftStatus.pending
)
db.add(draft)
await db.commit()
await db.refresh(draft)
# 添加后台任务
background_tasks.add_task(process_ai_continue_ending, draft.id)
return {
"code": 0,
"data": {
"draftId": draft.id,
"message": "已提交AI正在续写故事..."
}
}
@router.get("")
async def get_drafts(
userId: int,

View File

@@ -274,6 +274,139 @@ class AIService:
traceback.print_exc()
return None
async def continue_ending(
self,
story_title: str,
story_category: str,
ending_name: str,
ending_content: str,
user_prompt: str
) -> Optional[Dict[str, Any]]:
"""
AI续写结局从结局开始续写新的剧情分支
"""
print(f"\n[continue_ending] ========== 开始调用 ==========")
print(f"[continue_ending] story_title={story_title}, category={story_category}")
print(f"[continue_ending] ending_name={ending_name}")
print(f"[continue_ending] user_prompt={user_prompt}")
print(f"[continue_ending] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
if not self.enabled or not self.api_key:
print(f"[continue_ending] 服务未启用或API Key为空返回None")
return None
# 构建系统提示词
system_prompt = """你是一个专业的互动故事续写专家。用户已经到达故事的某个结局,现在想要从这个结局继续故事发展。
【任务】
请从这个结局开始,创作故事的延续。新剧情必须紧接结局内容,仿佛故事并没有在此结束,而是有了新的发展。
【写作要求】
1. 第一个节点必须紧密衔接原结局,像是结局之后自然发生的事
2. 生成 4-6 个新节点,形成有层次的剧情发展(起承转合)
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\\n\\n分隔包含场景描写、人物对话、心理活动
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
5. 严格符合用户的续写意图,围绕用户指令展开剧情
6. 保持原故事的人物性格、语言风格和世界观
7. 对话要自然生动,描写要有画面感
【关于新结局 - 极其重要!】
★★★ 每一条分支路径的尽头必须是新结局节点 ★★★
- 结局节点必须设置 "is_ending": true
- 结局内容要 200-400 字,分 2-3 段,有情感冲击力
- 结局名称 4-8 字,体现剧情走向
- 如果有2个选项分支最终必须有2个不同的结局
- 每个结局必须有 "ending_score" 评分0-100
【输出格式】严格JSON不要有任何额外文字
{
"nodes": {
"continue_1": {
"content": "续写剧情第一段150-300字...",
"speaker": "旁白",
"choices": [
{"text": "选项A5-15字", "nextNodeKey": "continue_2a"},
{"text": "选项B5-15字", "nextNodeKey": "continue_2b"}
]
},
"continue_2a": {
"content": "...",
"speaker": "旁白",
"choices": [
{"text": "选项C", "nextNodeKey": "continue_ending_good"},
{"text": "选项D", "nextNodeKey": "continue_ending_bad"}
]
},
"continue_ending_good": {
"content": "新好结局内容200-400字...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "新结局名称",
"ending_type": "good",
"ending_score": 90
},
"continue_ending_bad": {
"content": "新坏结局内容...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "新结局名称",
"ending_type": "bad",
"ending_score": 40
}
},
"entryNodeKey": "continue_1"
}"""
# 构建用户提示词
user_prompt_text = f"""【原故事信息】
故事标题:{story_title}
故事分类:{story_category}
【已达成的结局】
结局名称:{ending_name}
结局内容:{ending_content[:800]}
【用户续写指令】
{user_prompt}
请从这个结局开始续写新的剧情分支输出JSON格式"""
print(f"[continue_ending] 提示词构建完成开始调用AI...")
try:
result = None
if self.provider == "openai":
result = await self._call_openai_long(system_prompt, user_prompt_text)
elif self.provider == "claude":
result = await self._call_claude(f"{system_prompt}\n\n{user_prompt_text}")
elif self.provider == "qwen":
result = await self._call_qwen_long(system_prompt, user_prompt_text)
elif self.provider == "deepseek":
result = await self._call_deepseek_long(system_prompt, user_prompt_text)
print(f"[continue_ending] AI调用完成result存在={result is not None}")
if result and result.get("content"):
print(f"[continue_ending] AI返回内容长度={len(result.get('content', ''))}")
# 解析JSON响应复用 rewrite_branch 的解析方法)
parsed = self._parse_branch_json(result["content"])
print(f"[continue_ending] JSON解析结果: parsed存在={parsed is not None}")
if parsed:
parsed["tokens_used"] = result.get("tokens_used", 0)
print(f"[continue_ending] 成功! nodes数量={len(parsed.get('nodes', {}))}")
return parsed
else:
print(f"[continue_ending] JSON解析失败!")
return None
except Exception as e:
print(f"[continue_ending] 异常: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
return None
def _parse_branch_json(self, content: str) -> Optional[Dict]:
"""解析AI返回的分支JSON"""
print(f"[_parse_branch_json] 开始解析,内容长度={len(content)}")

View File

@@ -0,0 +1,29 @@
"""添加测试用户"""
import os
import pymysql
from pathlib import Path
SERVER_DIR = Path(__file__).parent.parent
env_file = SERVER_DIR / '.env'
if env_file.exists():
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ.setdefault(key.strip(), value.strip())
conn = pymysql.connect(
host=os.getenv('DB_HOST', 'localhost'),
port=int(os.getenv('DB_PORT', 3306)),
user=os.getenv('DB_USER', 'root'),
password=os.getenv('DB_PASSWORD', ''),
database='stardom_story',
charset='utf8mb4'
)
cur = conn.cursor()
cur.execute("INSERT IGNORE INTO users (id, openid, nickname) VALUES (1, 'test_user', '测试用户')")
conn.commit()
print('测试用户创建成功')
cur.close()
conn.close()

View File

@@ -9,12 +9,25 @@ from pathlib import Path
# 获取当前脚本所在目录
SQL_DIR = Path(__file__).parent
# 数据库配置(从环境变量或默认值)
# 从.env文件读取配置
def load_env():
env_file = SQL_DIR.parent / '.env'
config = {}
if env_file.exists():
for line in env_file.read_text(encoding='utf-8').splitlines():
if '=' in line and not line.startswith('#'):
k, v = line.split('=', 1)
config[k.strip()] = v.strip()
return config
env_config = load_env()
# 数据库配置(优先从.env读取
DB_CONFIG = {
'host': os.getenv('DB_HOST', 'localhost'),
'port': int(os.getenv('DB_PORT', 3306)),
'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', '123456'),
'host': env_config.get('DB_HOST', os.getenv('DB_HOST', 'localhost')),
'port': int(env_config.get('DB_PORT', os.getenv('DB_PORT', 3306))),
'user': env_config.get('DB_USER', os.getenv('DB_USER', 'root')),
'password': env_config.get('DB_PASSWORD', os.getenv('DB_PASSWORD', '')),
'charset': 'utf8mb4'
}

74
server/sql/rebuild_db.py Normal file
View File

@@ -0,0 +1,74 @@
"""删库重建脚本"""
import os
import sys
import pymysql
from pathlib import Path
SQL_DIR = Path(__file__).parent
SERVER_DIR = SQL_DIR.parent
# 加载 .env 文件
env_file = SERVER_DIR / '.env'
if env_file.exists():
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ.setdefault(key.strip(), value.strip())
DB_CONFIG = {
'host': os.getenv('DB_HOST', 'localhost'),
'port': int(os.getenv('DB_PORT', 3306)),
'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', '123456'),
'charset': 'utf8mb4'
}
def read_sql_file(filename):
with open(SQL_DIR / filename, 'r', encoding='utf-8') as f:
return f.read()
def execute_sql(cursor, sql, desc):
print(f'{desc}...')
for stmt in [s.strip() for s in sql.split(';') if s.strip()]:
try:
cursor.execute(stmt)
except pymysql.Error as e:
if e.args[0] not in [1007, 1050]:
print(f' 警告: {e.args[1]}')
print(f' {desc}完成!')
def rebuild():
print('=' * 50)
print('星域故事汇 - 删库重建')
print('=' * 50)
conn = pymysql.connect(**DB_CONFIG)
cur = conn.cursor()
# 删库
print('删除旧数据库...')
cur.execute('DROP DATABASE IF EXISTS stardom_story')
conn.commit()
print(' 删除完成!')
# 重建
schema_sql = read_sql_file('schema.sql')
execute_sql(cur, schema_sql, '创建数据库表结构')
conn.commit()
seed1 = read_sql_file('seed_stories_part1.sql')
execute_sql(cur, seed1, '导入种子数据第1部分')
conn.commit()
seed2 = read_sql_file('seed_stories_part2.sql')
execute_sql(cur, seed2, '导入种子数据第2部分')
conn.commit()
print('\n数据库重建完成!')
cur.close()
conn.close()
if __name__ == '__main__':
rebuild()

View File

@@ -1,107 +1,158 @@
-- 星域故事汇数据库初始化脚本
-- 创建数据库
CREATE DATABASE IF NOT EXISTS stardom_story DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- ============================================
-- 星域故事汇数据库结构
-- 基于实际数据库导出共7张表
-- ============================================
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)
-- ============================================
-- 1. 用户表
-- ============================================
CREATE TABLE IF NOT EXISTS `users` (
`id` INT NOT NULL AUTO_INCREMENT,
`openid` VARCHAR(100) 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,
PRIMARY KEY (`id`),
UNIQUE KEY `openid` (`openid`),
KEY `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
-- ============================================
-- 2. 故事主表
-- ============================================
CREATE TABLE IF NOT EXISTS `stories` (
`id` INT NOT NULL 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` TINYINT(1) DEFAULT 0 COMMENT '是否精选',
`status` TINYINT DEFAULT 1 COMMENT '状态0下架 1上架',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category`),
KEY `idx_featured` (`is_featured`),
KEY `idx_play_count` (`play_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事主表';
-- ============================================
-- 3. 故事节点表
-- ============================================
CREATE TABLE IF NOT EXISTS `story_nodes` (
`id` INT NOT NULL 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` TINYINT(1) DEFAULT 0 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,
PRIMARY KEY (`id`),
KEY `idx_story_id` (`story_id`),
KEY `idx_node_key` (`story_id`, `node_key`),
CONSTRAINT `story_nodes_ibfk_1` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事节点表';
-- ============================================
-- 4. 故事选项表
-- ============================================
CREATE TABLE IF NOT EXISTS `story_choices` (
`id` INT NOT NULL 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` TINYINT(1) DEFAULT 0 COMMENT '是否锁定(需广告解锁)',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_node_id` (`node_id`),
KEY `idx_story_id` (`story_id`),
CONSTRAINT `story_choices_ibfk_1` FOREIGN KEY (`node_id`) REFERENCES `story_nodes` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事选项表';
-- ============================================
-- 5. 用户进度表
-- ============================================
CREATE TABLE IF NOT EXISTS `user_progress` (
`id` INT NOT NULL 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` TINYINT(1) DEFAULT 0 COMMENT '是否完成',
`ending_reached` VARCHAR(100) DEFAULT '' COMMENT '达成的结局',
`is_liked` TINYINT(1) DEFAULT 0 COMMENT '是否点赞',
`is_collected` TINYINT(1) DEFAULT 0 COMMENT '是否收藏',
`play_count` INT DEFAULT 1 COMMENT '游玩次数',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_story` (`user_id`, `story_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_story_id` (`story_id`),
CONSTRAINT `user_progress_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `user_progress_ibfk_2` 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
-- ============================================
-- 6. 用户结局收集表
-- ============================================
CREATE TABLE IF NOT EXISTS `user_endings` (
`id` INT NOT NULL 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,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_ending` (`user_id`, `story_id`, `ending_name`),
KEY `story_id` (`story_id`),
CONSTRAINT `user_endings_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `user_endings_ibfk_2` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户结局收集表';
-- ============================================
-- 7. AI改写草稿表
-- ============================================
CREATE TABLE IF NOT EXISTS `story_drafts` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL COMMENT '用户ID',
`story_id` INT NOT NULL COMMENT '原故事ID',
`title` VARCHAR(100) DEFAULT '' COMMENT '草稿标题',
`path_history` JSON DEFAULT NULL COMMENT '用户之前的选择路径',
`current_node_key` VARCHAR(50) DEFAULT '' COMMENT '改写起始节点',
`current_content` TEXT COMMENT '当前节点内容',
`user_prompt` VARCHAR(500) NOT NULL COMMENT '用户改写指令',
`ai_nodes` JSON DEFAULT NULL COMMENT 'AI生成的新节点',
`entry_node_key` VARCHAR(50) DEFAULT '' COMMENT '入口节点',
`tokens_used` INT DEFAULT 0 COMMENT '消耗token数',
`status` ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending' COMMENT '状态',
`error_message` VARCHAR(500) DEFAULT '' COMMENT '失败原因',
`is_read` TINYINT(1) DEFAULT 0 COMMENT '用户是否已查看',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间',
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_story` (`story_id`),
KEY `idx_status` (`status`),
KEY `idx_user_unread` (`user_id`, `is_read`),
CONSTRAINT `story_drafts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `story_drafts_ibfk_2` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI改写草稿表';

View File

@@ -1,7 +1,15 @@
-- 10个种子故事数据
-- 种子数据:测试用户 + 10个故事
USE stardom_story;
-- ============================================
-- 测试用户
-- ============================================
INSERT INTO `users` (`id`, `openid`, `nickname`, `avatar_url`, `gender`, `total_play_count`, `total_endings`) VALUES
(1, 'test_user', '测试用户', '', 0, 0, 0);
-- ============================================
-- 1. 都市言情:《总裁的替身新娘》
-- ============================================
INSERT INTO stories (id, title, cover_url, description, category, play_count, like_count, is_featured) VALUES
(1, '总裁的替身新娘', '', '一场阴差阳错的婚礼,让平凡的你成为了霸道总裁的替身新娘。他冷漠、神秘,却在深夜对你展现出不一样的温柔...', '都市言情', 15680, 3420, TRUE);