1. 为什么我会用 PHP + MySQL + Uniapp 来做 AI 短剧系统

先说个背景。我这两年一直在做内容创作工具类项目,短视频生态火起来之后,短剧成了流量洼地,但传统短剧制作成本高、周期长,根本不是普通团队玩得转的。于是市面上开始出现"AI 短剧"这个概念——用 AI 生成脚本、分镜、配音、甚至直接生成视频素材,再通过程序化手段批量组装成成品短剧。方向没问题,但真正落地时,你会发现最大的瓶颈不是 AI 本身,而是怎么把 AI 能力"流程化"和"产品化"。

我最初也考虑过用 Python FastAPI 或者 Node.js 来做后端,毕竟 AI 生态圈里 Python 是主流。但实际调研之后,我放弃了这个方案,原因有三点。

第一,这个系统的目标用户是短剧创作者和中小型内容工作室,他们普遍没有复杂的运维能力,部署环境大多是阿里云、腾讯云的轻量服务器,甚至是一台 2G 内存的小水管。PHP 在这个场景下有天然优势,扔上去就能跑,不用配 uwsgi、不用管 gunicorn、不用折腾虚拟环境。

第二,AI 短剧系统的核心业务是:剧本管理、角色配置、分镜生成、视频素材管理、成片合成、发布渠道管理。这些本质上是 CRUD + 业务流程编排,PHP 的 Laravel 框架做这类业务太顺手了,开发效率比 Python 快不少。

第三,Uniapp 作为前端选型,是因为短剧创作工具的使用者大量依赖手机端操作——导演在现场要改剧本、剪辑师要上传素材、运营要实时盯数据。Uniapp 一套代码同时输出微信小程序、H5、安卓 App、iOS App,省掉了四套前端团队的维护成本。

这套系统的核心价值可以浓缩成一句话: 把 AI 生成能力从零散的"调用接口"升级成一条可配置、可复用、可协作的创作流水线 。

下面我会按我实际开发的路径,把这套"AI短剧创作系统源码(PHP+MySQL+Uniapp)"的实现方案逐步拆开来讲。这么说吧,你拿到这套实现思路,不是拿到一个玩具 Demo,而是一套可以直接拿去给客户演示、接商单的完整工程骨架。

2. 从需求到数据库表设计:AI 短剧系统到底在管理什么"对象"

很多人在做这类系统时,上来就写代码,结果写到一半发现表结构撑不住业务,回头重构,浪费时间不说,心气也磨没了。我建议你开工前先把领域模型想清楚。

AI 短剧系统里,最核心的领域对象有六个: 剧集(Drama)、剧本(Script)、角色(Character)、分镜(Shot)、成片(Video)、创作任务(Task) 。这六者之间的关系不是简单的父子级,而是"一个剧集有多个剧本版本,一个剧本包含多个角色,一个剧本被拆分成多个分镜,多个分镜最终渲染成一个成片,整个过程由创作任务驱动"。

我用一个实战中常用的表结构设计来说明,这套设计经过了商单项目的验证,字段不冗余,查询效率也不差。

2.1 剧集表与剧本表:版本管理的设计思路

CREATE TABLE `drama` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '创作者ID',
  `title` varchar(200) NOT NULL DEFAULT '' COMMENT '剧集名称',
  `category` tinyint(4) NOT NULL DEFAULT '0' COMMENT '分类:1甜宠 2悬疑 3逆袭 4都市',
  `cover_url` varchar(500) NOT NULL DEFAULT '' COMMENT '封面图',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0草稿 1创作中 2已发布',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='剧集表';

剧集表就是整个系统的最顶层容器。这里的 user_id 一定要建索引,因为系统一旦上线,用户量和作品量增长非常快,用联合查询时 user_id 是最高频的筛选条件。

CREATE TABLE `script` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `drama_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属剧集ID',
  `version` int(11) NOT NULL DEFAULT '1' COMMENT '版本号',
  `title` varchar(200) NOT NULL DEFAULT '' COMMENT '剧本标题',
  `content` longtext COMMENT '剧本文本内容(AI生成的原始JSON或Markdown)',
  `word_count` int(11) NOT NULL DEFAULT '0' COMMENT '字数',
  `is_active` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否当前生效版本',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_drama_id` (`drama_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='剧本表';

剧本表这里的 content 字段类型我用的是 longtext ,因为 AI 生成的剧本可能是一段结构化 JSON,包含场次、对白、旁白、机位建议等,单条数据轻松超过 64KB,用 text 类型会截断。 version 字段是实现"剧本版本管理"的关键,AI 每次重新生成我们不是覆盖原记录,而是插入新版本,这样创作者可以随时对比不同版本之间的差异。

这个设计思路背后的逻辑是:AI 创作不等于"一次性生成一个完美成品",而是"多轮试探、反复修正"。没有版本管理,这个系统在导演眼里就是一个玩具。

2.2 角色表与分镜表:面向 AI 生成的标准化存储

CREATE TABLE `character` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `script_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属剧本ID',
  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '角色名',
  `gender` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1男 2女',
  `personality` varchar(500) NOT NULL DEFAULT '' COMMENT '人设关键词,逗号分隔',
  `avatar_url` varchar(500) NOT NULL DEFAULT '' COMMENT '角色定妆照',
  `voice_url` varchar(500) NOT NULL DEFAULT '' COMMENT 'AI配音音色样本',
  `description` text COMMENT '角色详细描述',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_script_id` (`script_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

角色表是整个系统里跟 AI 结合最紧密的一张表。 personality 字段存的是人设关键词,比如"傲娇、毒舌、外冷内热",这些关键词在后续调用 AI 接口时会被拼进 Prompt 模板里; voice_url 是 AI 配音的音色样本地址,每个角色说话的风格要统一,通过声音克隆类接口做人声锁定。

CREATE TABLE `shot` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `script_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属剧本ID',
  `sequence` int(11) NOT NULL DEFAULT '0' COMMENT '分镜序号',
  `scene_desc` varchar(1000) NOT NULL DEFAULT '' COMMENT '场景描述',
  `action_desc` varchar(1000) NOT NULL DEFAULT '' COMMENT '动作描述',
  `dialogue` text COMMENT '对白内容',
  `camera_move` varchar(100) NOT NULL DEFAULT '' COMMENT '运镜方式,如推进/拉远/平移',
  `duration` int(11) NOT NULL DEFAULT '3' COMMENT '预计时长(秒)',
  `image_url` varchar(500) NOT NULL DEFAULT '' COMMENT 'AI生成的参考画面',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_script_id` (`script_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分镜表';

分镜表的设计灵感来自动画制作的"分镜脚本"流程——把剧本拆成最小的可视化单元,AI 出图能力就能逐镜生成画面,真人导演也能按这个表安排拍摄计划。

2.3 创作任务表:让 AI 异步生成不再"卡死"

创作任务表是整个系统稳定性的底座。刚开始做这套系统时,我犯过一个低级错误:在 PHP 里直接同步调用 AI 生成接口,用户在浏览器上等 60 秒,最后网关超时,请求断了,AI 还在后台跑,用户不知道结果,只能再点一次,结果生成了两份,浪费 API 额度。后来我改用"任务队列 + 异步回调"模式,才彻底解决。

CREATE TABLE `task` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL DEFAULT '0',
  `drama_id` bigint(20) NOT NULL DEFAULT '0',
  `task_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '1生成剧本 2生成分镜 3生成图片 4合成视频',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0等待中 1执行中 2成功 3失败 4取消',
  `params` json DEFAULT NULL COMMENT '任务参数,JSON格式',
  `result_data` longtext COMMENT '任务结果,JSON格式',
  `error_msg` varchar(500) DEFAULT '' COMMENT '错误信息',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id_status` (`user_id`, `status`),
  KEY `idx_status_type` (`status`, `task_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='创作任务表';

params 字段用 MySQL 5.7+ 的 JSON 类型,这是 MySQL 原生支持,比存序列化字符串更规范。 idx_user_id_status 这个联合索引非常关键,用户打开 App 看到的"我的创作任务列表"就是查这张表,没有索引的话,用户量和任务量上来后必卡。

3. 后端服务分层:PHP 代码里的 AI 生成链路如何编排

表结构定好之后,接下来是 PHP 后端的架构设计。我用的是 Laravel 框架(PHP 8.1+),项目结构分成 Controller、Service、Repository 三层。后端最复杂的部分在于 AI 生成链路的编排——同一个创作流程里要调用多个 AI 服务,而且这些服务有先后依赖关系。

3.1 服务分层与 AI 接入的适配器模式

先看一个核心问题:AI 服务商有很多,OpenAI、百度文心、讯飞星火、智谱、通义千问,每家都有对话生成接口,但参数格式、响应结构各不相同。如果直接在业务代码里写死各家 SDK,后期切换或者增加服务商时,改动量巨大。

我的方案是引入 适配器模式 :

<?php

namespace App\Services\AI;

interface AIProviderInterface
{
    // 生成文本内容(剧本、人设等)
    public function generateText(string $prompt, array $options = []): array;
    
    // 生成图片
    public function generateImage(string $prompt, array $options = []): array;
    
    // 生成配音
    public function generateVoice(string $text, array $options = []): array;
}

然后每个服务商实现这个接口:

<?php

namespace App\Services\AI\Providers;

use App\Services\AI\AIProviderInterface;

class OpenAIService implements AIProviderInterface
{
    protected $apiKey;
    protected $httpClient;

    public function __construct()
    {
        $this->apiKey = config('ai.providers.openai.api_key');
        $this->httpClient = new \GuzzleHttp\Client();
    }

    public function generateText(string $prompt, array $options = []): array
    {
        $response = $this->httpClient->post('https://api.openai.com/v1/chat/completions', [
            'headers' => [
                'Authorization' => 'Bearer ' . $this->apiKey,
            ],
            'json' => [
                'model' => $options['model'] ?? 'gpt-4o-mini',
                'messages' => [
                    ['role' => 'user', 'content' => $prompt],
                ],
                'temperature' => $options['temperature'] ?? 0.8,
                'max_tokens' => $options['max_tokens'] ?? 2048,
            ],
            'timeout' => 120,
        ]);

        $data = json_decode($response->getBody()->getContents(), true);

        return [
            'content' => $data['choices'][0]['message']['content'] ?? '',
            'raw' => $data,
        ];
    }
}

接着用一个工厂类来统一获取服务实例:

<?php

namespace App\Services\AI;

use App\Services\AI\Providers\OpenAIService;
use App\Services\AI\Providers\BaiduWenxinService;
use App\Services\AI\Providers\SparkService;

class AIProviderManager
{
    public static function getProvider(string $name): AIProviderInterface
    {
        return match ($name) {
            'openai' => app(OpenAIService::class),
            'wenxin' => app(BaiduWenxinService::class),
            'spark' => app(SparkService::class),
            default => throw new \InvalidArgumentException("未知的AI服务商: {$name}"),
        };
    }
}

为什么我要花这么大篇幅讲这个适配层?因为很多半路出家的开发者会直接写 $openai = new OpenAI(...) ,把服务商绑死在业务代码里。一旦某个服务商涨价、限流、接口变更,你就要满项目去搜 openai 这个字符串。有了适配器层,换服务商只是改一个配置文件的事。这在接商单时尤其重要——客户的需求经常是"哪个便宜用哪个"。

3.2 剧本生成的 Prompt 模板与流程编排

AI 短剧系统最有技术含量的部分是 Prompt 模板设计。直接写"帮我写一个短剧剧本",AI 产出的内容天马行空,没法用。我设计了一套 结构化 Prompt 模板 ,让 AI 的输出被约束在固定 JSON 结构里。

<?php

namespace App\Services\ShortDrama;

class ScriptGenerator
{
    protected $aiProvider;

    public function __construct()
    {
        $this->aiProvider = AIProviderManager::getProvider(config('ai.default_provider'));
    }

    public function generate(array $params): int
    {
        // 1. 组装 Prompt
        $prompt = $this->buildPrompt($params);

        // 2. 调用 AI 生成
        $result = $this->aiProvider->generateText($prompt, [
            'model' => 'gpt-4o-mini',
            'temperature' => 0.9,
            'max_tokens' => 4096,
        ]);

        // 3. 解析 AI 返回的 JSON
        $scriptData = json_decode($result['content'], true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \RuntimeException('AI返回内容不是有效JSON');
        }

        // 4. 复核关键字段
        if (empty($scriptData['scenes']) || count($scriptData['scenes']) < 5) {
            throw new \RuntimeException('生成场景数不足5个,需要重试');
        }

        // 5. 保存剧本数据
        return $this->saveScript($params['drama_id'], $scriptData);
    }

    protected function buildPrompt(array $params): string
    {
        $categoryMap = [
            1 => '甜宠', 
            2 => '悬疑', 
            3 => '逆袭', 
            4 => '都市'
        ];

        $category = $categoryMap[$params['category']] ?? '都市';

        // 从模型训练时的最佳实践出发,这里需要一个严格的结构化输出约束
        return <<<PROMPT
你是短剧编剧专家。请为用户创作一部 {$category} 类别的短剧剧本。

【剧集名称】{$params['title']}
【剧情概要】{$params['summary']}
【主要角色】{$params['characters']}
【集数要求】{$params['episodes']}集
【每集时长】约60秒

【输出要求】
请严格按照以下JSON结构输出(注意:只输出JSON,不要包含任何其他文字):

{
  "title": "剧集标题",
  "logline": "一句话故事梗概",
  "characters": [
    {"name":"角色名","gender":"男/女","personality":"角色性格","mission":"角色目标"}
  ],
  "episodes": [
    {
      "episode_no": 1,
      "title": "本集标题",
      "scenes": [
        {
          "scene_no": 1,
          "location": "场景地点",
          "time": "日/夜",
          "action_desc": "场景动作描述",
          "dialogue": "对白内容",
          "camera_move": "运镜方式",
          "duration": 8
        }
      ]
    }
  ]
}

要求每集至少包含3个场景,每个场景要有明确的冲突或转折。
PROMPT;
    }

    protected function saveScript(int $dramaId, array $scriptData): int
    {
        // 保存到 script 表,返回自增ID
        // 代码从略
    }
}

这里有个关键点:AI 一次生成的 token 有限,不可能生成整部 30 集短剧。实际操作中,我采用的是" 先生成剧情大纲 + 角色设定 + 前三集完整脚本 ",然后用一个"续写"接口,基于已有内容继续生成后续集数,每续写一次生成 5 集,直到集数补满。这样避免了超长上下文导致的质量退化,也保持了角色设定的一致性。

3.3 异步任务处理:用 Redis 队列解决超时与重试难题

前面我提到同步调用会超时,这里展开讲讲我最终的异步方案。

在 Laravel 里,我们使用队列处理 AI 生成任务。用户在前端点"开始创作",先把任务数据写入 task 表(status=0),同时 dispatch 一个 Job 到 Redis 队列。Workers 进程消费 Job,调用 AI 接口,完成后回写 task 表。

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Task;
use App\Services\ShortDrama\ScriptGenerator;
use Illuminate\Support\Facades\Log;

class GenerateScriptJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;
    public $timeout = 300;
    public $backoff = [10, 60, 300];

    protected $taskId;

    public function __construct(int $taskId)
    {
        $this->taskId = $taskId;
    }

    public function handle(): void
    {
        $task = Task::find($this->taskId);
        if (!$task) {
            return;
        }

        // 更新任务状态为执行中
        $task->update(['status' => 1]);

        try {
            $params = $task->params;
            $generator = new ScriptGenerator();
            $scriptId = $generator->generate($params);

            $task->update([
                'status' => 2,
                'result_data' => json_encode(['script_id' => $scriptId]),
            ]);
        } catch (\Throwable $e) {
            Log::error('剧本生成失败', [
                'task_id' => $task->id,
                'error' => $e->getMessage(),
            ]);

            $task->update([
                'status' => 3,
                'error_msg' => $e->getMessage(),
            ]);

            throw $e;
        }
    }
}

Laravel 队列的重试机制( $tries = 3 和 $backoff )非常实用。AI 接口的失败原因通常是限流或网络抖动,第一次失败等 10 秒重试,第二次失败等 60 秒,第三次失败等 300 秒,第三次还失败就放弃并把错误写进 error_msg 。用户端看到任务状态为"失败"时,可以点"重新生成",新任务会基于原参数重新跑一遍。这是一套健壮的任务闭环。

4. 分镜生成与 AI 配图:一张图一个镜头的生产流水线

剧本搞定之后,下一个核心环节是 分镜生成与 AI 配图 。短剧是视觉内容,每一集观众看到的都是"镜头画面 + 对白配音"的组合。这里我做了两件事:一是把剧本文本解析成结构化的分镜列表,二是为每个分镜调用 AI 绘图接口生成参考画面。

4.1 分镜解析算法:从剧本 JSON 到 shot 表

其实上一步 AI 输出的剧本 JSON 里已经包含了 scenes 数组,分镜解析就是把 episodes 里的 scenes 拆解并写入 shot 表。但这里有一个隐藏的难点: 每个场景的 "duration" 字段加起来要等于 60 秒左右(一集短剧),但 AI 生成时不一定精确 。

我的做法是写一个"时长归一化"逻辑:

public function normalizeDuration(array $scenes, int $targetDuration = 60, int $minSceneDuration = 3): array
{
    $total = array_sum(array_column($scenes, 'duration'));
    
    if ($total <= 0) {
        $total = $targetDuration;
    }

    $ratio = $targetDuration / $total;

    foreach ($scenes as $index => $scene) {
        $scenes[$index]['duration'] = max($minSceneDuration, (int) round($scene['duration'] * $ratio));
    }

    return $scenes;
}

正常运行时,AI 输出的每集总时长可能只有 45 或 80 秒,我们用这个归一化函数把它缩放到 60 秒左右,同时保证每个分镜最少 3 秒——这个值是从短剧观看习惯里总结出来的:太长的镜头显得拖沓,太短则画面看不清。3-8 秒是一个比较舒适的镜头节奏。

4.2 配图 Prompt 的"一致性"设计

做 AI 短剧最难的是 角色一致性 。用 Midjourney 或 Stable Diffusion 生成角色图,第一张生成一个"穿红色衣服的女孩",第二张同样描述可能就生成另一个完全不同长相的女孩。观众会立刻觉得"这不是同一个人,出戏了"。

我的方案是:给每个角色建立一个"视觉锚点"描述,然后把它拼进每张配图的 Prompt 里。这个锚点在角色创建时生成,通常是一个固定格式:

"固定锚点": 25岁东亚女性,瓜子脸,眼角有泪痣,黑色长发,身高165cm,穿衣风格以白色连衣裙为主

生成分镜配图时,Prompt 模板会加入这个锚点:

protected function buildImagePrompt(array $shot, array $character): string
{
    return "电影级短剧画面,"
        . "角色:" . $character['visual_anchor'] . ","
        . "动作:" . $shot['action_desc'] . ","
        . "场景:" . $shot['scene_desc'] . ","
        . "运镜:" . $shot['camera_move'] . ","
        . "画面比例9:16,竖屏构图,电影感,高细节,3D渲染风格";
}

这里要注意, visual_anchor 是从角色表里读出来的,每次生成配图都带上它,才能保证 AIGC 出来的多张图在"人设"上不跑偏——这是通过 工程手段解决模型不可控问题 的典型做法。

4.3 图片素材的上传与压缩处理

AI 生成的图片可能单张 3-5MB,直接存云存储成本高不说,前端加载也慢。我封装了一个 ImageProcessor 服务,自动处理上传:

<?php

namespace App\Services\Media;

use Intervention\Image\ImageManager;

class ImageProcessor
{
    protected $manager;

    public function __construct()
    {
        // 具体驱动的选择以环境实际支持的扩展为准,这里以GD为主
        $this->manager = new ImageManager(['driver' => 'gd']);
    }

    public function process(string $sourcePath, string $targetPath): array
    {
        $image = $this->manager->make($sourcePath);

        $width = $image->width();
        $height = $image->height();

        // 竖屏短剧,强制裁剪到 9:16 比例
        $image->resize(720, 1280, function ($constraint) {
            $constraint->aspectRatio();
            $constraint->upsize();
        });

        $image->crop(720, 1280, 0, 0);
        $image->save($targetPath, 80);

        return [
            'width' => 720,
            'height' => 1280,
            'size' => filesize($targetPath),
        ];
    }
}

这里有几个细节值得说。第一,短剧最终的呈现载体是手机竖屏,所以统一做 9:16 裁剪,避免画面出现黑边;第二,压到 720 宽足够清晰,不会对平台审核有压力;第三,JPG 质量设为 80,人眼基本看不出压缩痕迹,但体积能小 60% 以上。

5. Uniapp 前端落地:多端适配下的核心页面与状态管理

前端我用的是 Uniapp + Vue3 组合。选 Vue3 是因为它的组合式 API 写业务逻辑更清爽,尤其是创作这种多步流程。

5.1 页面结构与路由设计

整个 App 分五块:首页(作品列表)、创作中心(创建短剧)、剧本编辑器(剧本与分镜管理)、任务中心(异步任务进度)、我的(个人中心)。

Uniapp 里有一个容易踩坑的点: 路由参数传递 。短剧详情页要接收 drama_id ,很多新手会这样做:

uni.navigateTo({
    url: '/pages/drama/detail?id=' + dramaId
})

然后在详情页用 onLoad(options) 接收,这没错,但当参数特别多、比如从列表页带整套筛选条件过去时,URL 会被撑爆或者被转义搞乱。这时候建议这样处理:

// 传递复杂参数
const data = { dramaId: 123, from: 'list', filters: { category: 1, page: 2 } };
uni.navigateTo({
    url: '/pages/drama/detail?data=' + encodeURIComponent(JSON.stringify(data))
});
// 接收复杂参数
onLoad(options) {
    const data = JSON.parse(decodeURIComponent(options.data));
    console.log(data)
}

这套"JSON 序列化 + URL 编码"的方案在处理带嵌套对象的参数时特别稳。单页面内状态共享则用 Pinia,跨页面传递尽量走前端本地存储或请求后端接口,不要依赖 URL 传参——URL 只传 ID。

5.2 创作中心的"多步向导"组件设计

创作中心是用户创建 AI 短剧的入口,我设计成三步向导:

  1. 第一步:填写剧集基本信息(名称、分类、剧情概要)
  2. 第二步:配置角色(名字、人设、性别,可添加多个)
  3. 第三步:选择生成参数(集数、每集时长)并提交任务

每一步的状态存在 Pinia store 里,最后一步点击"开始创作"时统一提交到后端接口并创建任务。

// store/drama.js
import { defineStore } from 'pinia'

export const useDramaStore = defineStore('drama', {
    state: () => ({
        draftId: null,
        title: '',
        category: 1,
        summary: '',
        characters: [],
        episodes: 10,
        durationPerEpisode: 60,
    }),
    
    actions: {
        reset() {
            this.draftId = null
            this.title = ''
            this.category = 1
            this.summary = ''
            this.characters = []
            this.episodes = 10
            this.durationPerEpisode = 60
        }
    }
})

这个三步向导看起来简单,但正是"把 AI 能力产品化"的缩影——用户不需要理解 Prompt 是什么,只需要填几个中文选项,系统就能帮他跑完整个创作流程。

5.3 任务进度的实时反馈

任务中心页面要展示每个创作任务的状态。Uniapp 里我使用 setInterval 每 5 秒轮询一次后端接口获取任务状态,当状态变为 2(成功)时,前端自动跳转到剧本详情页。这个轮询方案在并发量不大时完全够用,而且比 WebSocket 实现简单得多。

轮询要注意一个细节:页面离开时一定要清除定时器,否则用户从任务中心跳到详情页,定时器还在后台跑,浪费流量也消耗用户电量。

onUnload() {
    if (this.timer) {
        clearInterval(this.timer)
        this.timer = null
    }
}

5.4 自定义分享好友与隐私政策适配

短剧系统天然需要分享功能,创作者把自己生成的作品分享给朋友或发布到社群。Uniapp 在微信小程序里的分享需要单独配置。因为整套源码是多个页面结构,建议在 App.vue 的 onLaunch 里统一设置分享逻辑,也可以在 onShareAppMessage 生命周期里针对每个页面定制分享卡片。这里有一个最新搜索里反复出现的坑: onShareAppMessage 被全局方法覆盖 。如果你的 main.js 里挂载了全局分享逻辑,页面又自己定义了 onShareAppMessage ,两者可能互相覆盖。

我的解法是:在页面实例中主动定义分享,并在 onLoad 里把当前页面的分享参数写入 Pinia,由统一模块读取:

// pages/drama/detail.vue
onShareAppMessage() {
    const dramaStore = useDramaStore()
    return {
        title: `推荐一部短剧《${dramaStore.title}》`,
        path: `/pages/drama/detail?id=${dramaStore.draftId}`,
        imageUrl: dramaStore.coverUrl
    }
}

另外,App 上架安卓应用市场和 iOS 时,涉及隐私政策弹窗。如果用户不同意隐私政策,需要退出 App。Uniapp 里可以这样处理:

// 用户点击不同意的回调
function handleDisagree() {
    // 小程序里不能直接退出,引导用户主动关闭
    // App端可调用 plus.runtime.quit()
    // #ifdef APP-PLUS
    plus.runtime.quit();
    // #endif
    
    // #ifdef MP-WEIXIN
    uni.showModal({
        title: '提示',
        content: '需要同意隐私政策后才能继续使用',
        showCancel: false,
        confirmText: '知道了'
    })
    // #endif
}

这个适配点很多人不重视,结果提审时被卡,反复打回。不同平台的策略差异很大:小程序不能主动退出(这是微信的规则),App 里可以用 plus.runtime.quit() 强制退出。

6. AI 创作者后台:PHP 管理端的审核与发布联动

系统不是做完用户端就完了,一个完整商单还包括管理后台。创作者在 App 里生成的短剧,要经过后台审核之后才能正式发布到各大渠道。

6.1 基于角色的权限控制

用户表里我加了一个 role 字段: 1 普通用户、 2 创作者、 3 管理员。管理员登录后,通过中间件拦截路由,只允许 role=3 的用户访问 /admin/* 路径。

public function handle(Request $request, Closure $next): Response
{
    if ($request->user() && $request->user()->role !== 3) {
        return response()->json(['message' => '无权限访问'], 403);
    }

    return $next($request);
}

这套方案在 API 驱动的架构里是最常见的权限控制方式,简单、有效,PHP 和 Laravel 生态里也有一堆现成包可以用。

6.2 审核流与分发

管理员进入后台后,看到的是待审核作品列表,点进详情页可以看到剧本内容、分镜列表、AI 生成图。审核通过后,系统自动把成片信息同步到各视频平台的发布队列——这里我用了一个 publish_channel 表来维护分发渠道:

渠道 对接方式 状态
抖音 开放平台 API 待点击授权
快手 开放平台 API 待点击授权
微信小程序 内部上架 已完成
自有 H5 直接发布 已完成

分发这块的难点在于各平台 API 要求不一致,目前我的做法是先做"复制文案 + 保存视频到相册"的半自动方案,让运营人员手动复制到对应平台发布。全套自动分发需要申请各个平台的开发者权限,流程比较长,作为源码版本可以预留扩展接口。

7. 环境部署与典型问题排查:从 0 到 1 跑通整套系统

最后这部分是实战经验总结,也是很多开发者拿到源码后卡住的地方。我会按部署顺序来说,每一步都列出我实际碰到的坑。

7.1 PHP 与 MySQL 环境的安装配置

本地开发我建议用 PHPStudy、XAMPP 或者 Docker 都行。在 Linux 服务器上,我习惯用 Docker 装 MySQL 8.0。这里有一点必须提醒: MySQL 8.0 的默认认证插件是 caching_sha2_password,而 PHP 5.x 的老项目可能连接不上 ,需要手动改认证方式。如果你用的是 PHP 8.1 + PDO 方式连接,这个坑基本不存在。

docker run --name mysql8 \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=your_password \
  -e MYSQL_DATABASE=short_drama \
  -e MYSQL_USER=drama_user \
  -e MYSQL_PASSWORD=drama_password \
  -d mysql:8.0

PHP 侧的 PDO 连接:

$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=short_drama;charset=utf8mb4';
$pdo = new PDO($dsn, 'drama_user', 'drama_password', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

7.2 Uniapp 打包上线实用要点

之前好几个人问过我:Uniapp 怎么打包?这里统一回答。

  • 微信小程序 :用 HBuilderX 打开工程,选"运行 → 运行到小程序模拟器",微信开发者工具里打开生成的 dist/dev/mp-weixin 目录即可。
  • App 安卓包 :HBuilderX 里点"发行 → 原生App-云打包",在后台配置包名和证书(Android 需要签名证书,可以在 HBuilderX 中生成通用证书),等几分钟出包。
  • iOS 上架 :需要 Apple 开发者账号,云打包生成 ipa 后用 Transporter 上传到 App Store Connect。

打包时还有个细节:小程序端如果要用 chooseMedia 上传视频,需要在 manifest.json 的 mp-weixin 节点里配置 permission 字段,否则真机调试会报"chooseMedia:fail api scope is not declared"。

7.3 系统上线后的性能瓶颈与优化方向

如果你是用这套源码去做商单,上线后最先会遇到两个性能瓶颈:一是 AI 接口的并发调用速度,二是 MySQL 的查询压力。

AI 接口的并发我很推荐用 Laravel Horizon 来管理 Redis 队列。Horizon 能实时监控队列吞吐量、失败任务、worker 数量,还能按任务类型设置不同的并发数。生成剧本任务的 API 调用频率是每分钟 60 次,配图任务可能是每分钟 30 次,分开控制可以避免一种任务把额度耗尽。

MySQL 的优化主要靠索引和缓存。高频查询如"首页推荐剧集列表""任务列表"都可以加一层 Redis 缓存:查询时先读缓存,没有再查数据库,回填缓存并设置 5 分钟过期。这能撑住三到五万日活完全没问题。

8. 源码版的定位思考与二次开发建议

最后聊聊这套源码该怎么用。如果你是一个独立开发者或小团队负责人,拿到这套 PHP + MySQL + Uniapp 的 AI 短剧创作系统源码,我的建议是不要一上来就大改架构,先把它跑起来,让导演或运营同事试用,收集真实的挫败点,再针对性地迭代。

目前整个系统里,AI 生成剧本、生成分镜、生成配图这条主链路已经完整跑通,属于"能用"的水平。但如果你要让客户掏钱,还值得在几个方向加强:

一个是增加智能校验模块——AI 生成的内容不一定符合短剧平台审核规则,比如"未成年角色形象""医疗用语"等高风险词库可以做成内置敏感词过滤,生成完成后自动预警。

另一个是增加"二次编辑"能力。AI 不是全部,短剧创作的最后一公里必须有人工参与。在剧本编辑页面提供"每个场景可以手动修改对白、重新生成配图、调整时长"这类能力,会大幅提升用户满意度。

还有一个方向是多语言。短剧出海是热门话题,中文短剧在海外平台也火。你可以在角色表、剧本表里预留 language 字段,再接入翻译 API,把整个生产链路做成中英双语——这又是另一套产品了。

AI 短剧这个赛道,代码只是底座,真正值钱的是这套流程编排能力和内容运营体系。先把技术底座做扎实,后面往上叠加的都是溢价空间。希望这篇实现方案能给你提供一条清晰的路径,少走些我踩过的弯路。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐