公开分享一个AI番茄短故事模块技术方案(含代码)

AI番茄短故事模块技术方案

1. 技术架构概览

1.1 整体架构

markdown 复制代码
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   前端应用      │    │   后端API      │    │   AI服务        │
│   React + TS    │◄──►│   Node.js       │◄──►│   GPT-4/Claude  │
│   Ant Design    │    │   Express       │    │   通义千问      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   状态管理      │    │   数据存储      │    │   缓存服务      │
│   Context API   │    │   MongoDB       │    │   Redis         │
│   useReducer    │    │   Mongoose      │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘

1.2 技术选型理由

前端技术栈

  • React 18: 与现有系统保持一致,支持并发特性
  • TypeScript: 提供类型安全,提高代码质量
  • Ant Design: 丰富的组件库,快速开发
  • Context API: 轻量级状态管理,避免过度工程化

后端技术栈

  • Node.js: 与现有系统技术栈一致
  • Express.js: 成熟稳定的Web框架
  • MongoDB: 文档型数据库,适合存储非结构化内容
  • Redis: 高性能缓存,提升响应速度

2. 前端详细设计

2.1 项目结构

bash 复制代码
src/
├── pages/
│   └── TomatoStory/
│       ├── Create/                 # 创建故事页面
│       │   ├── index.tsx
│       │   ├── StoryForm.tsx      # 故事配置表单
│       │   ├── TemplateSelector.tsx # 模板选择器
│       │   └── styles.css
│       ├── List/                   # 故事列表页面
│       │   ├── index.tsx
│       │   ├── StoryCard.tsx      # 故事卡片
│       │   ├── FilterBar.tsx      # 筛选栏
│       │   └── styles.css
│       ├── Detail/                 # 故事详情页面
│       │   ├── index.tsx
│       │   ├── ChapterList.tsx    # 章节列表
│       │   ├── StoryViewer.tsx    # 故事阅读器
│       │   └── styles.css
│       ├── Edit/                   # 编辑故事页面
│       │   ├── index.tsx
│       │   ├── ChapterEditor.tsx  # 章节编辑器
│       │   ├── OutlineEditor.tsx  # 大纲编辑器
│       │   └── styles.css
│       └── Generate/               # 生成进度页面
│           ├── index.tsx
│           ├── ProgressBar.tsx    # 进度条
│           ├── GenerationLog.tsx  # 生成日志
│           └── styles.css
├── components/
│   └── TomatoStory/
│       ├── Common/
│       │   ├── StoryStyleSelector.tsx # 风格选择器
│       │   ├── CharacterForm.tsx      # 人物表单
│       │   ├── WordCountSlider.tsx    # 字数滑块
│       │   └── QualityIndicator.tsx   # 质量指示器
│       ├── Templates/
│       │   ├── TemplateCard.tsx       # 模板卡片
│       │   ├── TemplatePreview.tsx    # 模板预览
│       │   └── CustomTemplate.tsx     # 自定义模板
│       └── Generation/
│           ├── GenerationStatus.tsx   # 生成状态
│           ├── ChapterProgress.tsx    # 章节进度
│           └── ErrorHandler.tsx       # 错误处理
├── hooks/
│   └── useTomatoStory/
│       ├── index.ts
│       ├── useStoryGeneration.ts      # 故事生成钩子
│       ├── useStoryList.ts            # 故事列表钩子
│       ├── useTemplates.ts            # 模板钩子
│       └── useStoryEditor.ts          # 编辑器钩子
├── context/
│   └── TomatoStoryContext.tsx         # 全局状态管理
├── services/
│   └── tomatoStoryApi.ts              # API服务
├── types/
│   └── tomatoStory.ts                 # 类型定义
└── utils/
    ├── storyValidation.ts             # 故事验证
    ├── contentFilter.ts               # 内容过滤
    └── exportUtils.ts                 # 导出工具

2.2 核心组件实现

2.2.1 故事创建表单组件
typescript 复制代码
// src/components/TomatoStory/Common/StoryForm.tsx
import React, { useState } from 'react';
import { Form, Input, Select, Slider, Button, Card, Space } from 'antd';
import { StoryConfig, StoryStyle, Character } from '../../../types/tomatoStory';
import StoryStyleSelector from './StoryStyleSelector';
import CharacterForm from './CharacterForm';
import WordCountSlider from './WordCountSlider';

interface StoryFormProps {
  onSubmit: (config: StoryConfig) => void;
  loading?: boolean;
  initialValues?: Partial<StoryConfig>;
}

const StoryForm: React.FC<StoryFormProps> = ({ onSubmit, loading, initialValues }) => {
  const [form] = Form.useForm();
  const [characters, setCharacters] = useState<Character[]>([
    { name: '', role: 'female_lead', personality: '', background: '' },
    { name: '', role: 'male_lead', personality: '', background: '' }
  ]);

  const handleSubmit = (values: any) => {
    const config: StoryConfig = {
      theme: values.theme,
      description: values.description,
      wordCount: values.wordCount,
      style: values.style,
      characters: characters.filter(c => c.name.trim()),
      templateId: values.templateId
    };
    onSubmit(config);
  };

  return (
    <Form
      form={form}
      layout="vertical"
      onFinish={handleSubmit}
      initialValues={{
        wordCount: 20000,
        style: 'sweet',
        ...initialValues
      }}
    >
      <Card title="基本信息" className="mb-4">
        <Form.Item
          name="theme"
          label="故事主题"
          rules={[{ required: true, message: '请输入故事主题' }]}
        >
          <Input
            placeholder="如:霸道总裁爱上我、重生复仇记等"
            maxLength={50}
            showCount
          />
        </Form.Item>

        <Form.Item
          name="description"
          label="故事简介"
          rules={[
            { required: true, message: '请输入故事简介' },
            { min: 50, message: '简介至少50字' },
            { max: 500, message: '简介不超过500字' }
          ]}
        >
          <Input.TextArea
            placeholder="请详细描述故事背景、主要情节和人物关系..."
            rows={4}
            maxLength={500}
            showCount
          />
        </Form.Item>

        <Form.Item name="style" label="故事风格">
          <StoryStyleSelector />
        </Form.Item>

        <Form.Item name="wordCount" label="目标字数">
          <WordCountSlider min={15000} max={30000} />
        </Form.Item>
      </Card>

      <Card title="人物设定" className="mb-4">
        <CharacterForm
          characters={characters}
          onChange={setCharacters}
        />
      </Card>

      <Form.Item>
        <Space>
          <Button type="primary" htmlType="submit" loading={loading} size="large">
            开始生成故事
          </Button>
          <Button onClick={() => form.resetFields()}>重置</Button>
        </Space>
      </Form.Item>
    </Form>
  );
};

export default StoryForm;
2.2.2 故事生成进度组件
typescript 复制代码
// src/components/TomatoStory/Generation/GenerationStatus.tsx
import React, { useEffect, useState } from 'react';
import { Progress, Card, Steps, Alert, Button, Space } from 'antd';
import { CheckCircleOutlined, LoadingOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { GenerationProgress, GenerationStep } from '../../../types/tomatoStory';

interface GenerationStatusProps {
  storyId: string;
  onComplete: () => void;
  onError: (error: string) => void;
}

const GenerationStatus: React.FC<GenerationStatusProps> = ({ storyId, onComplete, onError }) => {
  const [progress, setProgress] = useState<GenerationProgress>({
    currentChapter: 0,
    totalChapters: 0,
    currentStep: 'outline',
    progress: 0,
    status: 'running',
    message: '正在初始化...'
  });

  const steps: GenerationStep[] = [
    { key: 'outline', title: '生成大纲', description: '分析主题,构建故事框架' },
    { key: 'content', title: '生成内容', description: '逐章节生成故事内容' },
    { key: 'polish', title: '优化润色', description: '检查逻辑,优化表达' },
    { key: 'complete', title: '生成完成', description: '故事生成完毕' }
  ];

  useEffect(() => {
    const eventSource = new EventSource(`/api/tomato-story/${storyId}/progress`);
    
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setProgress(data);
      
      if (data.status === 'completed') {
        eventSource.close();
        onComplete();
      } else if (data.status === 'error') {
        eventSource.close();
        onError(data.error);
      }
    };

    eventSource.onerror = () => {
      eventSource.close();
      onError('连接中断,请刷新页面重试');
    };

    return () => eventSource.close();
  }, [storyId, onComplete, onError]);

  const getCurrentStepIndex = () => {
    return steps.findIndex(step => step.key === progress.currentStep);
  };

  const getStepStatus = (stepIndex: number) => {
    const currentIndex = getCurrentStepIndex();
    if (stepIndex < currentIndex) return 'finish';
    if (stepIndex === currentIndex) {
      return progress.status === 'error' ? 'error' : 'process';
    }
    return 'wait';
  };

  return (
    <div className="generation-status">
      <Card title="故事生成进度" className="mb-4">
        <Steps
          current={getCurrentStepIndex()}
          status={progress.status === 'error' ? 'error' : 'process'}
          items={steps.map((step, index) => ({
            title: step.title,
            description: step.description,
            status: getStepStatus(index),
            icon: getStepStatus(index) === 'process' ? <LoadingOutlined /> : 
                  getStepStatus(index) === 'error' ? <ExclamationCircleOutlined /> :
                  getStepStatus(index) === 'finish' ? <CheckCircleOutlined /> : undefined
          }))}
        />
      </Card>

      <Card title="详细进度" className="mb-4">
        <div className="mb-3">
          <div className="flex justify-between mb-2">
            <span>总体进度</span>
            <span>{Math.round(progress.progress)}%</span>
          </div>
          <Progress 
            percent={progress.progress} 
            status={progress.status === 'error' ? 'exception' : 'active'}
            strokeColor={{
              '0%': '#108ee9',
              '100%': '#87d068',
            }}
          />
        </div>

        {progress.currentStep === 'content' && progress.totalChapters > 0 && (
          <div className="mb-3">
            <div className="flex justify-between mb-2">
              <span>章节进度</span>
              <span>{progress.currentChapter}/{progress.totalChapters}</span>
            </div>
            <Progress 
              percent={(progress.currentChapter / progress.totalChapters) * 100}
              showInfo={false}
            />
          </div>
        )}

        <Alert
          message={progress.message}
          type={progress.status === 'error' ? 'error' : 'info'}
          showIcon
        />
      </Card>

      {progress.status === 'error' && (
        <Card>
          <Space>
            <Button type="primary" onClick={() => window.location.reload()}>
              重新生成
            </Button>
            <Button onClick={() => window.history.back()}>
              返回
            </Button>
          </Space>
        </Card>
      )}
    </div>
  );
};

export default GenerationStatus;

2.3 状态管理设计

typescript 复制代码
// src/context/TomatoStoryContext.tsx
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
import { TomatoStory, StoryConfig, Template } from '../types/tomatoStory';
import * as api from '../services/tomatoStoryApi';

interface TomatoStoryState {
  stories: TomatoStory[];
  templates: Template[];
  currentStory: TomatoStory | null;
  loading: boolean;
  error: string | null;
  pagination: {
    current: number;
    pageSize: number;
    total: number;
  };
}

type TomatoStoryAction =
  | { type: 'SET_LOADING'; payload: boolean }
  | { type: 'SET_ERROR'; payload: string | null }
  | { type: 'SET_STORIES'; payload: TomatoStory[] }
  | { type: 'SET_TEMPLATES'; payload: Template[] }
  | { type: 'SET_CURRENT_STORY'; payload: TomatoStory | null }
  | { type: 'ADD_STORY'; payload: TomatoStory }
  | { type: 'UPDATE_STORY'; payload: { id: string; updates: Partial<TomatoStory> } }
  | { type: 'DELETE_STORY'; payload: string }
  | { type: 'SET_PAGINATION'; payload: Partial<TomatoStoryState['pagination']> };

const initialState: TomatoStoryState = {
  stories: [],
  templates: [],
  currentStory: null,
  loading: false,
  error: null,
  pagination: {
    current: 1,
    pageSize: 10,
    total: 0
  }
};

function tomatoStoryReducer(state: TomatoStoryState, action: TomatoStoryAction): TomatoStoryState {
  switch (action.type) {
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload, loading: false };
    case 'SET_STORIES':
      return { ...state, stories: action.payload, loading: false };
    case 'SET_TEMPLATES':
      return { ...state, templates: action.payload };
    case 'SET_CURRENT_STORY':
      return { ...state, currentStory: action.payload };
    case 'ADD_STORY':
      return { ...state, stories: [action.payload, ...state.stories] };
    case 'UPDATE_STORY':
      return {
        ...state,
        stories: state.stories.map(story =>
          story.id === action.payload.id
            ? { ...story, ...action.payload.updates }
            : story
        ),
        currentStory: state.currentStory?.id === action.payload.id
          ? { ...state.currentStory, ...action.payload.updates }
          : state.currentStory
      };
    case 'DELETE_STORY':
      return {
        ...state,
        stories: state.stories.filter(story => story.id !== action.payload),
        currentStory: state.currentStory?.id === action.payload ? null : state.currentStory
      };
    case 'SET_PAGINATION':
      return {
        ...state,
        pagination: { ...state.pagination, ...action.payload }
      };
    default:
      return state;
  }
}

interface TomatoStoryContextType {
  state: TomatoStoryState;
  actions: {
    createStory: (config: StoryConfig) => Promise<TomatoStory>;
    loadStories: (params?: any) => Promise<void>;
    loadStory: (id: string) => Promise<void>;
    updateStory: (id: string, updates: Partial<TomatoStory>) => Promise<void>;
    deleteStory: (id: string) => Promise<void>;
    loadTemplates: () => Promise<void>;
    clearError: () => void;
  };
}

const TomatoStoryContext = createContext<TomatoStoryContextType | undefined>(undefined);

export const TomatoStoryProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [state, dispatch] = useReducer(tomatoStoryReducer, initialState);

  const actions = {
    createStory: async (config: StoryConfig): Promise<TomatoStory> => {
      try {
        dispatch({ type: 'SET_LOADING', payload: true });
        dispatch({ type: 'SET_ERROR', payload: null });
        
        const story = await api.createStory(config);
        dispatch({ type: 'ADD_STORY', payload: story });
        
        return story;
      } catch (error) {
        const message = error instanceof Error ? error.message : '创建故事失败';
        dispatch({ type: 'SET_ERROR', payload: message });
        throw error;
      }
    },

    loadStories: async (params = {}) => {
      try {
        dispatch({ type: 'SET_LOADING', payload: true });
        dispatch({ type: 'SET_ERROR', payload: null });
        
        const response = await api.getStories({
          page: state.pagination.current,
          limit: state.pagination.pageSize,
          ...params
        });
        
        dispatch({ type: 'SET_STORIES', payload: response.stories });
        dispatch({ 
          type: 'SET_PAGINATION', 
          payload: { 
            total: response.total,
            current: response.page 
          } 
        });
      } catch (error) {
        const message = error instanceof Error ? error.message : '加载故事列表失败';
        dispatch({ type: 'SET_ERROR', payload: message });
      }
    },

    loadStory: async (id: string) => {
      try {
        dispatch({ type: 'SET_LOADING', payload: true });
        dispatch({ type: 'SET_ERROR', payload: null });
        
        const story = await api.getStory(id);
        dispatch({ type: 'SET_CURRENT_STORY', payload: story });
      } catch (error) {
        const message = error instanceof Error ? error.message : '加载故事详情失败';
        dispatch({ type: 'SET_ERROR', payload: message });
      }
    },

    updateStory: async (id: string, updates: Partial<TomatoStory>) => {
      try {
        dispatch({ type: 'SET_ERROR', payload: null });
        
        const updatedStory = await api.updateStory(id, updates);
        dispatch({ type: 'UPDATE_STORY', payload: { id, updates: updatedStory } });
      } catch (error) {
        const message = error instanceof Error ? error.message : '更新故事失败';
        dispatch({ type: 'SET_ERROR', payload: message });
        throw error;
      }
    },

    deleteStory: async (id: string) => {
      try {
        dispatch({ type: 'SET_ERROR', payload: null });
        
        await api.deleteStory(id);
        dispatch({ type: 'DELETE_STORY', payload: id });
      } catch (error) {
        const message = error instanceof Error ? error.message : '删除故事失败';
        dispatch({ type: 'SET_ERROR', payload: message });
        throw error;
      }
    },

    loadTemplates: async () => {
      try {
        dispatch({ type: 'SET_ERROR', payload: null });
        
        const templates = await api.getTemplates();
        dispatch({ type: 'SET_TEMPLATES', payload: templates });
      } catch (error) {
        const message = error instanceof Error ? error.message : '加载模板失败';
        dispatch({ type: 'SET_ERROR', payload: message });
      }
    },

    clearError: () => {
      dispatch({ type: 'SET_ERROR', payload: null });
    }
  };

  return (
    <TomatoStoryContext.Provider value={{ state, actions }}>
      {children}
    </TomatoStoryContext.Provider>
  );
};

export const useTomatoStory = () => {
  const context = useContext(TomatoStoryContext);
  if (!context) {
    throw new Error('useTomatoStory must be used within a TomatoStoryProvider');
  }
  return context;
};

3. 后端详细设计

3.1 项目结构

bash 复制代码
src/
├── controllers/
│   └── tomatoStoryController.js       # 番茄故事控制器
├── models/
│   ├── TomatoStory.js                 # 故事模型
│   └── StoryTemplate.js               # 模板模型
├── services/
│   ├── tomatoStoryService.js          # 故事业务逻辑
│   ├── aiGenerationService.js         # AI生成服务
│   ├── templateService.js             # 模板服务
│   └── contentFilterService.js        # 内容过滤服务
├── routes/
│   └── tomatoStory.js                 # 路由定义
├── middleware/
│   ├── validation.js                  # 参数验证
│   └── rateLimit.js                   # 频率限制
├── utils/
│   ├── promptBuilder.js               # Prompt构建器
│   ├── contentParser.js               # 内容解析器
│   └── qualityChecker.js              # 质量检查器
└── config/
    ├── ai.js                          # AI配置
    └── constants.js                   # 常量定义

3.2 数据模型设计

javascript 复制代码
// src/models/TomatoStory.js
const mongoose = require('mongoose');

const characterSchema = new mongoose.Schema({
  name: { type: String, required: true },
  role: { 
    type: String, 
    enum: ['male_lead', 'female_lead', 'supporting', 'antagonist'],
    required: true 
  },
  personality: { type: String, required: true },
  background: String,
  appearance: String
});

const chapterSchema = new mongoose.Schema({
  title: { type: String, required: true },
  content: { type: String, required: true },
  wordCount: { type: Number, required: true },
  order: { type: Number, required: true },
  summary: String,
  keyPoints: [String], // 关键情节点
  qualityScore: { type: Number, min: 0, max: 100 },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const tomatoStorySchema = new mongoose.Schema({
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  title: { type: String, required: true },
  theme: { type: String, required: true },
  description: { type: String, required: true },
  wordCount: { type: Number, required: true, min: 15000, max: 30000 },
  style: { 
    type: String, 
    enum: ['sweet', 'abuse', 'cool', 'suspense', 'fantasy', 'urban', 'ancient'],
    required: true 
  },
  status: { 
    type: String, 
    enum: ['draft', 'generating', 'completed', 'error', 'paused'],
    default: 'draft' 
  },
  outline: String,
  chapters: [chapterSchema],
  characters: [characterSchema],
  templateId: { type: mongoose.Schema.Types.ObjectId, ref: 'StoryTemplate' },
  
  // 生成配置
  generationConfig: {
    aiModel: { type: String, default: 'gpt-4' },
    temperature: { type: Number, default: 0.8, min: 0, max: 2 },
    maxTokens: { type: Number, default: 4000 },
    language: { type: String, default: 'zh-CN' }
  },
  
  // 质量指标
  qualityMetrics: {
    overallScore: { type: Number, min: 0, max: 100 },
    plotCoherence: { type: Number, min: 0, max: 100 }, // 情节连贯性
    characterConsistency: { type: Number, min: 0, max: 100 }, // 人物一致性
    languageQuality: { type: Number, min: 0, max: 100 }, // 语言质量
    paceRating: { type: Number, min: 0, max: 100 } // 节奏评分
  },
  
  // 生成进度
  generationProgress: {
    currentChapter: { type: Number, default: 0 },
    totalChapters: { type: Number, default: 0 },
    currentStep: { 
      type: String, 
      enum: ['outline', 'content', 'polish', 'complete'],
      default: 'outline' 
    },
    progress: { type: Number, default: 0, min: 0, max: 100 },
    startTime: Date,
    estimatedEndTime: Date,
    actualEndTime: Date
  },
  
  // 统计信息
  stats: {
    viewCount: { type: Number, default: 0 },
    likeCount: { type: Number, default: 0 },
    shareCount: { type: Number, default: 0 },
    downloadCount: { type: Number, default: 0 }
  },
  
  tags: [String],
  isPublic: { type: Boolean, default: false },
  publishedAt: Date,
  
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

// 索引
tomatoStorySchema.index({ userId: 1, createdAt: -1 });
tomatoStorySchema.index({ style: 1, status: 1 });
tomatoStorySchema.index({ tags: 1 });
tomatoStorySchema.index({ 'qualityMetrics.overallScore': -1 });

// 中间件
tomatoStorySchema.pre('save', function(next) {
  this.updatedAt = new Date();
  
  // 计算总字数
  if (this.chapters && this.chapters.length > 0) {
    this.wordCount = this.chapters.reduce((total, chapter) => total + chapter.wordCount, 0);
  }
  
  next();
});

// 虚拟字段
tomatoStorySchema.virtual('averageChapterLength').get(function() {
  if (!this.chapters || this.chapters.length === 0) return 0;
  return Math.round(this.wordCount / this.chapters.length);
});

tomatoStorySchema.virtual('estimatedReadingTime').get(function() {
  // 假设每分钟阅读300字
  return Math.ceil(this.wordCount / 300);
});

module.exports = mongoose.model('TomatoStory', tomatoStorySchema);

3.3 AI生成服务实现

javascript 复制代码
// src/services/aiGenerationService.js
const OpenAI = require('openai');
const PromptBuilder = require('../utils/promptBuilder');
const ContentParser = require('../utils/contentParser');
const QualityChecker = require('../utils/qualityChecker');
const TomatoStory = require('../models/TomatoStory');
const EventEmitter = require('events');

class AIGenerationService extends EventEmitter {
  constructor() {
    super();
    this.openai = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY
    });
    this.promptBuilder = new PromptBuilder();
    this.contentParser = new ContentParser();
    this.qualityChecker = new QualityChecker();
  }

  async generateStory(storyId, config) {
    try {
      // 更新状态为生成中
      await this.updateStoryStatus(storyId, {
        status: 'generating',
        'generationProgress.startTime': new Date(),
        'generationProgress.currentStep': 'outline'
      });

      // 第一步:生成故事大纲
      this.emit('progress', storyId, {
        currentStep: 'outline',
        progress: 10,
        message: '正在分析主题,生成故事大纲...'
      });

      const outline = await this.generateOutline(config);
      
      await this.updateStoryStatus(storyId, {
        outline: outline.content,
        'generationProgress.totalChapters': outline.chapters.length
      });

      // 第二步:生成章节内容
      this.emit('progress', storyId, {
        currentStep: 'content',
        progress: 20,
        message: '开始生成章节内容...'
      });

      const chapters = [];
      for (let i = 0; i < outline.chapters.length; i++) {
        const chapterConfig = {
          ...config,
          outline,
          chapterIndex: i,
          previousChapters: chapters
        };

        const chapter = await this.generateChapter(chapterConfig);
        chapters.push(chapter);

        // 更新进度
        const progress = 20 + (i + 1) / outline.chapters.length * 60;
        await this.updateStoryStatus(storyId, {
          $push: { chapters: chapter },
          'generationProgress.currentChapter': i + 1,
          'generationProgress.progress': progress
        });

        this.emit('progress', storyId, {
          currentStep: 'content',
          currentChapter: i + 1,
          totalChapters: outline.chapters.length,
          progress,
          message: `正在生成第${i + 1}章:${chapter.title}`
        });

        // 避免API频率限制
        await this.delay(2000);
      }

      // 第三步:内容优化和质量检查
      this.emit('progress', storyId, {
        currentStep: 'polish',
        progress: 85,
        message: '正在优化内容,检查质量...'
      });

      const qualityMetrics = await this.checkQuality(storyId, chapters);
      
      await this.updateStoryStatus(storyId, {
        status: 'completed',
        qualityMetrics,
        'generationProgress.currentStep': 'complete',
        'generationProgress.progress': 100,
        'generationProgress.actualEndTime': new Date()
      });

      this.emit('progress', storyId, {
        currentStep: 'complete',
        progress: 100,
        message: '故事生成完成!',
        status: 'completed'
      });

      return await TomatoStory.findById(storyId);

    } catch (error) {
      console.error('Story generation error:', error);
      
      await this.updateStoryStatus(storyId, {
        status: 'error',
        'generationProgress.progress': 0
      });

      this.emit('progress', storyId, {
        status: 'error',
        error: error.message,
        message: '生成过程中出现错误,请重试'
      });

      throw error;
    }
  }

  async generateOutline(config) {
    const prompt = this.promptBuilder.buildOutlinePrompt(config);
    
    const response = await this.openai.chat.completions.create({
      model: config.generationConfig?.aiModel || 'gpt-4',
      messages: [{ role: 'user', content: prompt }],
      temperature: config.generationConfig?.temperature || 0.8,
      max_tokens: 2000
    });

    const content = response.choices[0].message.content;
    return this.contentParser.parseOutline(content);
  }

  async generateChapter(config) {
    const prompt = this.promptBuilder.buildChapterPrompt(config);
    
    const response = await this.openai.chat.completions.create({
      model: config.generationConfig?.aiModel || 'gpt-4',
      messages: [{ role: 'user', content: prompt }],
      temperature: config.generationConfig?.temperature || 0.8,
      max_tokens: config.generationConfig?.maxTokens || 4000
    });

    const content = response.choices[0].message.content;
    const chapter = this.contentParser.parseChapter(content, config.chapterIndex);
    
    // 计算字数
    chapter.wordCount = this.countWords(chapter.content);
    
    return chapter;
  }

  async checkQuality(storyId, chapters) {
    const story = await TomatoStory.findById(storyId);
    
    const metrics = {
      overallScore: 0,
      plotCoherence: 0,
      characterConsistency: 0,
      languageQuality: 0,
      paceRating: 0
    };

    // 情节连贯性检查
    metrics.plotCoherence = await this.qualityChecker.checkPlotCoherence(chapters);
    
    // 人物一致性检查
    metrics.characterConsistency = await this.qualityChecker.checkCharacterConsistency(
      story.characters, 
      chapters
    );
    
    // 语言质量检查
    metrics.languageQuality = await this.qualityChecker.checkLanguageQuality(chapters);
    
    // 节奏评分
    metrics.paceRating = await this.qualityChecker.checkPacing(chapters, story.style);
    
    // 计算总分
    metrics.overallScore = Math.round(
      (metrics.plotCoherence + metrics.characterConsistency + 
       metrics.languageQuality + metrics.paceRating) / 4
    );

    return metrics;
  }

  async updateStoryStatus(storyId, updates) {
    return await TomatoStory.findByIdAndUpdate(storyId, updates, { new: true });
  }

  countWords(text) {
    // 中文字符计数
    const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
    // 英文单词计数
    const englishWords = (text.match(/[a-zA-Z]+/g) || []).length;
    return chineseChars + englishWords;
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

module.exports = AIGenerationService;

3.4 API控制器实现

javascript 复制代码
// src/controllers/tomatoStoryController.js
const TomatoStory = require('../models/TomatoStory');
const StoryTemplate = require('../models/StoryTemplate');
const AIGenerationService = require('../services/aiGenerationService');
const ContentFilterService = require('../services/contentFilterService');
const { validationResult } = require('express-validator');

class TomatoStoryController {
  constructor() {
    this.aiService = new AIGenerationService();
    this.contentFilter = new ContentFilterService();
    this.activeGenerations = new Map(); // 存储活跃的生成任务
  }

  // 创建故事
  async createStory(req, res) {
    try {
      // 验证输入
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(400).json({
          success: false,
          message: '输入参数错误',
          errors: errors.array()
        });
      }

      const { theme, description, wordCount, style, characters, templateId } = req.body;
      const userId = req.user.id;

      // 内容过滤
      const filterResult = await this.contentFilter.checkContent({
        theme,
        description,
        characters: characters.map(c => c.name + c.personality).join(' ')
      });

      if (!filterResult.passed) {
        return res.status(400).json({
          success: false,
          message: '内容包含敏感信息,请修改后重试',
          details: filterResult.issues
        });
      }

      // 创建故事记录
      const story = new TomatoStory({
        userId,
        title: theme,
        theme,
        description,
        wordCount,
        style,
        characters,
        templateId,
        status: 'draft'
      });

      await story.save();

      // 异步开始生成
      this.startGeneration(story._id, {
        theme,
        description,
        wordCount,
        style,
        characters,
        templateId,
        generationConfig: {
          aiModel: req.body.aiModel || 'gpt-4',
          temperature: req.body.temperature || 0.8,
          maxTokens: req.body.maxTokens || 4000
        }
      });

      res.status(201).json({
        success: true,
        message: '故事创建成功,正在生成中',
        data: {
          id: story._id,
          status: story.status,
          generationProgress: story.generationProgress
        }
      });

    } catch (error) {
      console.error('Create story error:', error);
      res.status(500).json({
        success: false,
        message: '创建故事失败',
        error: error.message
      });
    }
  }

  // 获取生成进度(SSE)
  async getGenerationProgress(req, res) {
    const { id } = req.params;
    
    // 设置SSE头
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'Access-Control-Allow-Origin': '*'
    });

    // 发送初始状态
    const story = await TomatoStory.findById(id);
    if (!story) {
      res.write(`data: ${JSON.stringify({ error: '故事不存在' })}\n\n`);
      res.end();
      return;
    }

    res.write(`data: ${JSON.stringify({
      ...story.generationProgress,
      status: story.status,
      message: this.getStatusMessage(story.status, story.generationProgress)
    })}\n\n`);

    // 监听进度更新
    const progressHandler = (storyId, progress) => {
      if (storyId === id) {
        res.write(`data: ${JSON.stringify(progress)}\n\n`);
        
        // 如果完成或出错,关闭连接
        if (progress.status === 'completed' || progress.status === 'error') {
          res.end();
        }
      }
    };

    this.aiService.on('progress', progressHandler);

    // 客户端断开连接时清理
    req.on('close', () => {
      this.aiService.removeListener('progress', progressHandler);
    });
  }

  // 获取故事列表
  async getStories(req, res) {
    try {
      const {
        page = 1,
        limit = 10,
        style,
        status,
        search,
        sortBy = 'createdAt',
        sortOrder = 'desc'
      } = req.query;

      const userId = req.user.id;
      const skip = (page - 1) * limit;

      // 构建查询条件
      const query = { userId };
      if (style) query.style = style;
      if (status) query.status = status;
      if (search) {
        query.$or = [
          { title: { $regex: search, $options: 'i' } },
          { theme: { $regex: search, $options: 'i' } },
          { description: { $regex: search, $options: 'i' } }
        ];
      }

      // 构建排序
      const sort = {};
      sort[sortBy] = sortOrder === 'desc' ? -1 : 1;

      const [stories, total] = await Promise.all([
        TomatoStory.find(query)
          .sort(sort)
          .skip(skip)
          .limit(parseInt(limit))
          .select('-chapters.content') // 列表不返回章节内容
          .populate('templateId', 'name category'),
        TomatoStory.countDocuments(query)
      ]);

      res.json({
        success: true,
        data: {
          stories,
          pagination: {
            current: parseInt(page),
            pageSize: parseInt(limit),
            total,
            totalPages: Math.ceil(total / limit)
          }
        }
      });

    } catch (error) {
      console.error('Get stories error:', error);
      res.status(500).json({
        success: false,
        message: '获取故事列表失败',
        error: error.message
      });
    }
  }

  // 获取故事详情
  async getStory(req, res) {
    try {
      const { id } = req.params;
      const userId = req.user.id;

      const story = await TomatoStory.findOne({ _id: id, userId })
        .populate('templateId')
        .populate('userId', 'username avatar');

      if (!story) {
        return res.status(404).json({
          success: false,
          message: '故事不存在'
        });
      }

      // 增加查看次数
      await TomatoStory.findByIdAndUpdate(id, {
        $inc: { 'stats.viewCount': 1 }
      });

      res.json({
        success: true,
        data: story
      });

    } catch (error) {
      console.error('Get story error:', error);
      res.status(500).json({
        success: false,
        message: '获取故事详情失败',
        error: error.message
      });
    }
  }

  // 更新故事
  async updateStory(req, res) {
    try {
      const { id } = req.params;
      const userId = req.user.id;
      const updates = req.body;

      // 验证权限
      const story = await TomatoStory.findOne({ _id: id, userId });
      if (!story) {
        return res.status(404).json({
          success: false,
          message: '故事不存在或无权限'
        });
      }

      // 不允许修改某些字段
      delete updates._id;
      delete updates.userId;
      delete updates.createdAt;
      delete updates.stats;

      const updatedStory = await TomatoStory.findByIdAndUpdate(
        id,
        { ...updates, updatedAt: new Date() },
        { new: true, runValidators: true }
      );

      res.json({
        success: true,
        message: '故事更新成功',
        data: updatedStory
      });

    } catch (error) {
      console.error('Update story error:', error);
      res.status(500).json({
        success: false,
        message: '更新故事失败',
        error: error.message
      });
    }
  }

  // 删除故事
  async deleteStory(req, res) {
    try {
      const { id } = req.params;
      const userId = req.user.id;

      const story = await TomatoStory.findOneAndDelete({ _id: id, userId });
      if (!story) {
        return res.status(404).json({
          success: false,
          message: '故事不存在或无权限'
        });
      }

      res.json({
        success: true,
        message: '故事删除成功'
      });

    } catch (error) {
      console.error('Delete story error:', error);
      res.status(500).json({
        success: false,
        message: '删除故事失败',
        error: error.message
      });
    }
  }

  // 开始生成任务
  async startGeneration(storyId, config) {
    try {
      // 避免重复生成
      if (this.activeGenerations.has(storyId)) {
        return;
      }

      this.activeGenerations.set(storyId, true);
      
      await this.aiService.generateStory(storyId, config);
      
    } catch (error) {
      console.error(`Generation failed for story ${storyId}:`, error);
    } finally {
      this.activeGenerations.delete(storyId);
    }
  }

  // 获取状态消息
  getStatusMessage(status, progress) {
    switch (status) {
      case 'draft':
        return '准备开始生成';
      case 'generating':
        switch (progress.currentStep) {
          case 'outline':
            return '正在生成故事大纲...';
          case 'content':
            return `正在生成第${progress.currentChapter}章内容...`;
          case 'polish':
            return '正在优化和润色内容...';
          default:
            return '正在生成中...';
        }
      case 'completed':
        return '故事生成完成';
      case 'error':
        return '生成过程中出现错误';
      case 'paused':
        return '生成已暂停';
      default:
        return '未知状态';
    }
  }
}

module.exports = new TomatoStoryController();

4. 部署和集成方案

4.1 前端集成

typescript 复制代码
// src/App.tsx 中添加路由
import { Routes, Route } from 'react-router-dom';
import { TomatoStoryProvider } from './context/TomatoStoryContext';
import TomatoStoryRoutes from './pages/TomatoStory/routes';

function App() {
  return (
    <BrowserRouter>
      <TomatoStoryProvider>
        <Routes>
          {/* 现有路由 */}
          <Route path="/novel/*" element={<NovelRoutes />} />
          <Route path="/chapter-generation" element={<ChapterGeneration />} />
          
          {/* 新增番茄故事路由 */}
          <Route path="/tomato-story/*" element={<TomatoStoryRoutes />} />
        </Routes>
      </TomatoStoryProvider>
    </BrowserRouter>
  );
}
typescript 复制代码
// src/pages/TomatoStory/routes.tsx
import { Routes, Route } from 'react-router-dom';
import CreateStory from './Create';
import StoryList from './List';
import StoryDetail from './Detail';
import EditStory from './Edit';
import GenerateProgress from './Generate';

const TomatoStoryRoutes = () => {
  return (
    <Routes>
      <Route index element={<StoryList />} />
      <Route path="create" element={<CreateStory />} />
      <Route path="list" element={<StoryList />} />
      <Route path="detail/:id" element={<StoryDetail />} />
      <Route path="edit/:id" element={<EditStory />} />
      <Route path="generate/:id" element={<GenerateProgress />} />
    </Routes>
  );
};

export default TomatoStoryRoutes;

4.2 后端集成

javascript 复制代码
// src/routes/index.js 中添加路由
const express = require('express');
const router = express.Router();

// 现有路由
router.use('/api/novels', require('./novels'));
router.use('/api/chapters', require('./chapters'));

// 新增番茄故事路由
router.use('/api/tomato-story', require('./tomatoStory'));

module.exports = router;
javascript 复制代码
// src/routes/tomatoStory.js
const express = require('express');
const router = express.Router();
const tomatoStoryController = require('../controllers/tomatoStoryController');
const { body } = require('express-validator');
const auth = require('../middleware/auth');
const rateLimit = require('../middleware/rateLimit');

// 创建故事验证规则
const createStoryValidation = [
  body('theme').notEmpty().withMessage('主题不能为空').isLength({ max: 50 }).withMessage('主题不超过50字'),
  body('description').isLength({ min: 50, max: 500 }).withMessage('简介长度50-500字'),
  body('wordCount').isInt({ min: 15000, max: 30000 }).withMessage('字数范围15000-30000'),
  body('style').isIn(['sweet', 'abuse', 'cool', 'suspense', 'fantasy', 'urban', 'ancient']).withMessage('无效的故事风格'),
  body('characters').isArray({ min: 1, max: 5 }).withMessage('人物数量1-5个')
];

// 路由定义
router.post('/create', auth, rateLimit.createStory, createStoryValidation, tomatoStoryController.createStory);
router.get('/list', auth, tomatoStoryController.getStories);
router.get('/:id', auth, tomatoStoryController.getStory);
router.put('/:id', auth, tomatoStoryController.updateStory);
router.delete('/:id', auth, tomatoStoryController.deleteStory);
router.get('/:id/progress', auth, tomatoStoryController.getGenerationProgress);

// 模板相关路由
router.get('/templates/list', auth, tomatoStoryController.getTemplates);
router.get('/templates/:id', auth, tomatoStoryController.getTemplate);
router.post('/templates', auth, tomatoStoryController.createTemplate);

module.exports = router;

4.3 导航集成

typescript 复制代码
// src/components/Layout/Sidebar.tsx 中添加导航项
const menuItems = [
  {
    key: 'novel',
    icon: <BookOutlined />,
    label: '小说管理',
    children: [
      { key: 'novel-list', label: '我的小说', path: '/novel/list' },
      { key: 'novel-create', label: '创建小说', path: '/novel/create' }
    ]
  },
  {
    key: 'chapter-generation',
    icon: <EditOutlined />,
    label: '章节生成',
    path: '/chapter-generation'
  },
  // 新增番茄故事导航
  {
    key: 'tomato-story',
    icon: <ThunderboltOutlined />,
    label: '番茄短故事',
    children: [
      { key: 'tomato-story-list', label: '我的故事', path: '/tomato-story/list' },
      { key: 'tomato-story-create', label: '创建故事', path: '/tomato-story/create' }
    ]
  }
];

5. 性能优化方案

5.1 前端优化

  1. 代码分割
typescript 复制代码
// 懒加载番茄故事模块
const TomatoStoryRoutes = lazy(() => import('./pages/TomatoStory/routes'));

// 在App.tsx中使用Suspense
<Suspense fallback={<Loading />}>
  <Route path="/tomato-story/*" element={<TomatoStoryRoutes />} />
</Suspense>
  1. 状态优化
typescript 复制代码
// 使用useMemo优化计算
const filteredStories = useMemo(() => {
  return stories.filter(story => {
    if (filters.style && story.style !== filters.style) return false;
    if (filters.status && story.status !== filters.status) return false;
    return true;
  });
}, [stories, filters]);

// 使用useCallback优化函数
const handleStoryCreate = useCallback(async (config: StoryConfig) => {
  try {
    const story = await actions.createStory(config);
    navigate(`/tomato-story/generate/${story.id}`);
  } catch (error) {
    message.error(error.message);
  }
}, [actions, navigate]);

5.2 后端优化

  1. 数据库优化
javascript 复制代码
// 添加复合索引
tomatoStorySchema.index({ userId: 1, status: 1, createdAt: -1 });
tomatoStorySchema.index({ style: 1, 'qualityMetrics.overallScore': -1 });

// 分页查询优化
const getStoriesOptimized = async (query, options) => {
  const { page, limit, sortBy, sortOrder } = options;
  
  // 使用聚合管道优化查询
  const pipeline = [
    { $match: query },
    { $sort: { [sortBy]: sortOrder === 'desc' ? -1 : 1 } },
    { $skip: (page - 1) * limit },
    { $limit: limit },
    {
      $project: {
        'chapters.content': 0, // 排除大字段
        'generationProgress.logs': 0
      }
    }
  ];
  
  return await TomatoStory.aggregate(pipeline);
};
  1. 缓存策略
javascript 复制代码
const Redis = require('redis');
const redis = Redis.createClient();

// 缓存模板数据
const getTemplatesWithCache = async () => {
  const cacheKey = 'story_templates';
  const cached = await redis.get(cacheKey);
  
  if (cached) {
    return JSON.parse(cached);
  }
  
  const templates = await StoryTemplate.find({ isPublic: true });
  await redis.setex(cacheKey, 3600, JSON.stringify(templates)); // 1小时缓存
  
  return templates;
};

// 缓存用户故事列表
const getUserStoriesWithCache = async (userId, options) => {
  const cacheKey = `user_stories:${userId}:${JSON.stringify(options)}`;
  const cached = await redis.get(cacheKey);
  
  if (cached) {
    return JSON.parse(cached);
  }
  
  const stories = await getStoriesOptimized({ userId }, options);
  await redis.setex(cacheKey, 300, JSON.stringify(stories)); // 5分钟缓存
  
  return stories;
};
  1. AI服务优化
javascript 复制代码
// 请求队列管理
class AIRequestQueue {
  constructor(maxConcurrent = 3) {
    this.queue = [];
    this.running = 0;
    this.maxConcurrent = maxConcurrent;
  }
  
  async add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.process();
    });
  }
  
  async process() {
    if (this.running >= this.maxConcurrent || this.queue.length === 0) {
      return;
    }
    
    this.running++;
    const { task, resolve, reject } = this.queue.shift();
    
    try {
      const result = await task();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.process();
    }
  }
}

const aiQueue = new AIRequestQueue();

// 使用队列管理AI请求
const generateWithQueue = async (prompt, config) => {
  return aiQueue.add(() => openai.chat.completions.create({
    model: config.model,
    messages: [{ role: 'user', content: prompt }],
    ...config
  }));
};

6. 监控和日志

6.1 性能监控

javascript 复制代码
// 生成性能监控
class GenerationMonitor {
  constructor() {
    this.metrics = {
      totalGenerations: 0,
      successfulGenerations: 0,
      failedGenerations: 0,
      averageGenerationTime: 0,
      totalTokensUsed: 0
    };
  }
  
  startGeneration(storyId) {
    this.metrics.totalGenerations++;
    return {
      storyId,
      startTime: Date.now(),
      tokensUsed: 0
    };
  }
  
  endGeneration(session, success, tokensUsed = 0) {
    const duration = Date.now() - session.startTime;
    
    if (success) {
      this.metrics.successfulGenerations++;
    } else {
      this.metrics.failedGenerations++;
    }
    
    this.metrics.totalTokensUsed += tokensUsed;
    
    // 更新平均生成时间
    this.metrics.averageGenerationTime = 
      (this.metrics.averageGenerationTime * (this.metrics.totalGenerations - 1) + duration) / 
      this.metrics.totalGenerations;
    
    // 记录日志
    console.log(`Generation ${session.storyId}: ${success ? 'SUCCESS' : 'FAILED'}, Duration: ${duration}ms, Tokens: ${tokensUsed}`);
  }
  
  getMetrics() {
    return {
      ...this.metrics,
      successRate: this.metrics.successfulGenerations / this.metrics.totalGenerations * 100,
      averageCost: this.calculateCost(this.metrics.totalTokensUsed)
    };
  }
  
  calculateCost(tokens) {
    // 基于GPT-4定价计算
    const inputCost = 0.03 / 1000; // $0.03 per 1K tokens
    const outputCost = 0.06 / 1000; // $0.06 per 1K tokens
    return (tokens * inputCost + tokens * outputCost).toFixed(4);
  }
}

const monitor = new GenerationMonitor();

6.2 错误处理和重试机制

javascript 复制代码
// 智能重试机制
class RetryHandler {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }
  
  async executeWithRetry(fn, context = {}) {
    let lastError;
    
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error;
        
        // 不重试的错误类型
        if (this.isNonRetryableError(error)) {
          throw error;
        }
        
        if (attempt < this.maxRetries) {
          const delay = this.calculateDelay(attempt);
          console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms:`, error.message);
          await this.delay(delay);
        }
      }
    }
    
    throw lastError;
  }
  
  isNonRetryableError(error) {
    // 内容违规、认证失败等不应重试
    const nonRetryableCodes = ['content_filter', 'invalid_api_key', 'insufficient_quota'];
    return nonRetryableCodes.some(code => error.message.includes(code));
  }
  
  calculateDelay(attempt) {
    // 指数退避
    return this.baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

const retryHandler = new RetryHandler();

7. 安全和合规

7.1 内容安全

javascript 复制代码
// 内容过滤服务
class ContentFilterService {
  constructor() {
    this.sensitiveWords = this.loadSensitiveWords();
    this.patterns = this.loadPatterns();
  }
  
  async checkContent(content) {
    const issues = [];
    
    // 敏感词检查
    const sensitiveWordIssues = this.checkSensitiveWords(content);
    if (sensitiveWordIssues.length > 0) {
      issues.push(...sensitiveWordIssues);
    }
    
    // 模式匹配检查
    const patternIssues = this.checkPatterns(content);
    if (patternIssues.length > 0) {
      issues.push(...patternIssues);
    }
    
    // AI内容检查
    const aiIssues = await this.checkWithAI(content);
    if (aiIssues.length > 0) {
      issues.push(...aiIssues);
    }
    
    return {
      passed: issues.length === 0,
      issues,
      score: this.calculateSafetyScore(content, issues)
    };
  }
  
  checkSensitiveWords(content) {
    const issues = [];
    const text = typeof content === 'string' ? content : JSON.stringify(content);
    
    for (const word of this.sensitiveWords) {
      if (text.includes(word)) {
        issues.push({
          type: 'sensitive_word',
          word,
          severity: 'high'
        });
      }
    }
    
    return issues;
  }
  
  async checkWithAI(content) {
    try {
      const prompt = `请检查以下内容是否包含不当信息(政治敏感、暴力、色情等):\n\n${content}\n\n请回答:安全/不安全,并说明原因。`;
      
      const response = await openai.chat.completions.create({
        model: 'gpt-3.5-turbo',
        messages: [{ role: 'user', content: prompt }],
        temperature: 0.1
      });
      
      const result = response.choices[0].message.content;
      
      if (result.includes('不安全')) {
        return [{
          type: 'ai_detection',
          reason: result,
          severity: 'high'
        }];
      }
      
      return [];
    } catch (error) {
      console.error('AI content check failed:', error);
      return []; // 检查失败时不阻止
    }
  }
}

7.2 API安全

javascript 复制代码
// API频率限制
const rateLimit = {
  createStory: require('express-rate-limit')({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 5, // 最多5个故事
    message: {
      success: false,
      message: '创建故事过于频繁,请稍后再试'
    }
  }),
  
  generateContent: require('express-rate-limit')({
    windowMs: 60 * 1000, // 1分钟
    max: 10, // 最多10次请求
    message: {
      success: false,
      message: '请求过于频繁,请稍后再试'
    }
  })
};

// 输入验证中间件
const validateInput = (req, res, next) => {
  const { body } = req;
  
  // 检查必需字段
  const requiredFields = ['theme', 'description', 'wordCount', 'style'];
  for (const field of requiredFields) {
    if (!body[field]) {
      return res.status(400).json({
        success: false,
        message: `缺少必需字段: ${field}`
      });
    }
  }
  
  // 字符串长度检查
  if (body.theme.length > 50) {
    return res.status(400).json({
      success: false,
      message: '主题长度不能超过50字符'
    });
  }
  
  // 数值范围检查
  if (body.wordCount < 15000 || body.wordCount > 30000) {
    return res.status(400).json({
      success: false,
      message: '字数必须在15000-30000之间'
    });
  }
  
  next();
};

8. 测试策略

8.1 前端测试

typescript 复制代码
// 组件测试示例
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { TomatoStoryProvider } from '../context/TomatoStoryContext';
import StoryForm from '../components/TomatoStory/Common/StoryForm';

describe('StoryForm', () => {
  const mockOnSubmit = jest.fn();
  
  beforeEach(() => {
    mockOnSubmit.mockClear();
  });
  
  const renderWithProvider = (component: React.ReactElement) => {
    return render(
      <TomatoStoryProvider>
        {component}
      </TomatoStoryProvider>
    );
  };
  
  test('应该正确渲染表单字段', () => {
    renderWithProvider(<StoryForm onSubmit={mockOnSubmit} />);
    
    expect(screen.getByLabelText('故事主题')).toBeInTheDocument();
    expect(screen.getByLabelText('故事简介')).toBeInTheDocument();
    expect(screen.getByLabelText('故事风格')).toBeInTheDocument();
    expect(screen.getByLabelText('目标字数')).toBeInTheDocument();
  });
  
  test('应该验证必填字段', async () => {
    renderWithProvider(<StoryForm onSubmit={mockOnSubmit} />);
    
    const submitButton = screen.getByText('开始生成故事');
    fireEvent.click(submitButton);
    
    await waitFor(() => {
      expect(screen.getByText('请输入故事主题')).toBeInTheDocument();
    });
    
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });
  
  test('应该正确提交表单数据', async () => {
    renderWithProvider(<StoryForm onSubmit={mockOnSubmit} />);
    
    // 填写表单
    fireEvent.change(screen.getByLabelText('故事主题'), {
      target: { value: '霸道总裁爱上我' }
    });
    
    fireEvent.change(screen.getByLabelText('故事简介'), {
      target: { value: '一个平凡女孩意外成为总裁秘书,从此开始了一段甜蜜的爱情故事。女主角善良坚强,男主角冷酷霸道但内心深情。' }
    });
    
    const submitButton = screen.getByText('开始生成故事');
    fireEvent.click(submitButton);
    
    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        theme: '霸道总裁爱上我',
        description: expect.stringContaining('平凡女孩'),
        wordCount: 20000,
        style: 'sweet',
        characters: expect.any(Array)
      });
    });
  });
});

8.2 后端测试

javascript 复制代码
// API测试示例
const request = require('supertest');
const app = require('../app');
const TomatoStory = require('../models/TomatoStory');
const { generateToken } = require('../utils/auth');

describe('Tomato Story API', () => {
  let authToken;
  let userId;
  
  beforeAll(async () => {
    // 创建测试用户
    const user = await User.create({
      username: 'testuser',
      email: 'test@example.com',
      password: 'password123'
    });
    
    userId = user._id;
    authToken = generateToken(user._id);
  });
  
  afterAll(async () => {
    // 清理测试数据
    await TomatoStory.deleteMany({ userId });
    await User.findByIdAndDelete(userId);
  });
  
  describe('POST /api/tomato-story/create', () => {
    test('应该成功创建故事', async () => {
      const storyData = {
        theme: '重生复仇记',
        description: '女主角重生回到十年前,决心报复那些伤害过她的人,同时收获真爱。',
        wordCount: 25000,
        style: 'cool',
        characters: [
          {
            name: '苏晚晚',
            role: 'female_lead',
            personality: '聪明、坚强、有仇必报'
          }
        ]
      };
      
      const response = await request(app)
        .post('/api/tomato-story/create')
        .set('Authorization', `Bearer ${authToken}`)
        .send(storyData)
        .expect(201);
      
      expect(response.body.success).toBe(true);
      expect(response.body.data.id).toBeDefined();
      expect(response.body.data.status).toBe('draft');
    });
    
    test('应该验证输入参数', async () => {
      const invalidData = {
        theme: '', // 空主题
        description: '太短', // 描述太短
        wordCount: 5000, // 字数太少
        style: 'invalid' // 无效风格
      };
      
      const response = await request(app)
        .post('/api/tomato-story/create')
        .set('Authorization', `Bearer ${authToken}`)
        .send(invalidData)
        .expect(400);
      
      expect(response.body.success).toBe(false);
      expect(response.body.errors).toBeDefined();
    });
  });
  
  describe('GET /api/tomato-story/list', () => {
    test('应该返回用户的故事列表', async () => {
      // 先创建一些测试故事
      await TomatoStory.create({
        userId,
        title: '测试故事1',
        theme: '测试主题1',
        description: '测试描述1',
        wordCount: 20000,
        style: 'sweet',
        status: 'completed'
      });
      
      const response = await request(app)
        .get('/api/tomato-story/list')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);
      
      expect(response.body.success).toBe(true);
      expect(response.body.data.stories).toBeInstanceOf(Array);
      expect(response.body.data.pagination).toBeDefined();
    });
  });
});

9. 部署配置

9.1 环境配置

bash 复制代码
# .env.production
NODE_ENV=production
PORT=3001

# 数据库
MONGODB_URI=mongodb://localhost:27017/novel_ai_prod
REDIS_URL=redis://localhost:6379

# AI服务
OPENAI_API_KEY=your_openai_api_key
OPENAI_ORG_ID=your_org_id

# 安全
JWT_SECRET=your_jwt_secret
ENCRYPTION_KEY=your_encryption_key

# 限制
MAX_CONCURRENT_GENERATIONS=5
MAX_STORIES_PER_USER=100
MAX_DAILY_GENERATIONS=10

# 监控
LOG_LEVEL=info
MONITOR_ENABLED=true

9.2 Docker配置

dockerfile 复制代码
# Dockerfile
FROM node:18-alpine

WORKDIR /app

# 复制package文件
COPY package*.json ./
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建前端
RUN npm run build

# 暴露端口
EXPOSE 3001

# 启动应用
CMD ["npm", "start"]
yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3001:3001"
    environment:
      - NODE_ENV=production
      - MONGODB_URI=mongodb://mongo:27017/novel_ai
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis
    volumes:
      - ./logs:/app/logs
  
  mongo:
    image: mongo:6
    ports:
      - "27017:27017"
    volumes:
      - mongo_data:/data/db
  
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  mongo_data:
  redis_data:

10. 总结

本技术方案详细描述了AI番茄短故事模块的完整实现方案,包括:

  1. 前端架构:基于React + TypeScript的现代化前端架构
  2. 后端架构:Node.js + Express + MongoDB的稳定后端架构
  3. AI集成:完整的AI生成服务和质量控制机制
  4. 性能优化:数据库优化、缓存策略、请求队列管理
  5. 安全保障:内容过滤、API安全、输入验证
  6. 监控测试:完整的监控体系和测试策略
  7. 部署方案:Docker化部署和环境配置

该方案确保了系统的可扩展性、稳定性和安全性,为用户提供高质量的AI番茄短故事生成服务。

相关推荐
晴殇i3 小时前
为什么现代 JavaScript 代码规范开始建议禁止使用 else ?
前端·javascript·前端框架
源力祁老师3 小时前
OWL与VUE3 的高级组件通信全解析
前端·javascript·vue.js
花开月正圆3 小时前
遇见docker-compose
前端
护国神蛙3 小时前
自动翻译插件中的智能字符串切割方案
前端·javascript·babel
TechFrank3 小时前
浏览器云端写代码,远程开发 Next.js 应用的简易教程
前端
PaytonD3 小时前
LoopBack 2 如何设置静态资源缓存时间
前端·javascript·node.js
snow@li3 小时前
d3.js:学习积累
开发语言·前端·javascript
vincention3 小时前
JavaScript 中 this 指向完全指南
前端
qyresearch_4 小时前
射频前端MMIC:5G时代的技术引擎与市场机遇
前端·5g