phase2-plan.md 44 KB

二期:广告制作管理平台 — 细化方案 v2

继承自 v1 规划,基于最新讨论更新:Creative 命名、zip 素材包、模板下沉、manifest 扩展性设计。


0. 与 v1 的关键变更

维度 v1 v2
业务概念 Project(项目) Creative(创意)——广告行业标准术语
素材上传 单文件逐个上传 zip 包上传(config.json + page.png + map.png ± special.jpeg)
模板代码位置 留在项目根 src/ 下沉到 templates/coloring/
CSS 参数化 全面 var() 非重点,聚焦主流程
storeUrls 外部注入 不参数化,保持 adapter 内硬编码
模板元信息 manifest.json,驱动前后端 UI + 校验
optional 素材 未明确 构建时检查文件存在性,动态生成 import

1. 总体架构

浏览器 (React SPA)
    │
    ▼
Express API (:3001)
    │  ┌── 静态文件服务 (React build)
    │  ├── REST API (/api/v1/*)
    │  └── Build Service (调用 Vite 程序化构建)
    │
    ├── SQLite (creatives, builds, templates 元数据)
    │
    ├── storage/creatives/<id>/
    │     ├── assets/       # 用户上传的 zip 解压产物
    │     └── builds/<uuid>/# 构建产物
    │
    └── templates/coloring/  # 模板 Vite 项目
            │
            ▼
         npx vite build --mode <platform>

2. 项目目录结构

playableads-platform/
│
├── templates/                          # 模板根目录(扩展点)
│   └── coloring/                       #   填色玩法模板
│       ├── manifest.json               #     模板元信息 → 驱动前后端
│       ├── index.html                  #     模板入口 HTML
│       ├── vite.config.js              #     构建配置(含 #ad-config alias)
│       ├── tsconfig.json
│       ├── package.json                #     模板依赖(js-confetti, vite-plugin-singlefile…)
│       ├── src/
│       │   ├── base/                   #     自研 WebGL 2 引擎(不变)
│       │   │   ├── Scene.ts
│       │   │   ├── Gesture.ts
│       │   │   ├── Animator.ts
│       │   │   ├── m4.ts
│       │   │   ├── BgLayer.ts / BoxLayer.ts / BorderLayer.ts / FrameLayer.ts
│       │   │   ├── ImageLayer.ts / TextureLayer.ts
│       │   │   ├── ImageShaders.ts
│       │   │   ├── Triangle.ts / 2d.ts / utils.ts
│       │   │   └── glsl/              # GLSL shader 文件
│       │   ├── filler/                 #     填色玩法业务逻辑
│       │   │   ├── index.ts            #       ★ 重构:从 #ad-config 导入素材+主题
│       │   │   ├── ad-config.ts        #       NEW:默认配置(dev / CLI 构建用,提交到 repo)
│       │   │   ├── _ad_config_.ts      #       .gitignore,平台构建时动态生成
│       │   │   ├── FillerData.ts       #       状态核心
│       │   │   ├── FillerScene.ts      #       WebGL 场景
│       │   │   ├── WorkLayer.ts
│       │   │   ├── AnimatableMask.ts
│       │   │   ├── LineArtLayer.ts
│       │   │   ├── NumberLayer.ts
│       │   │   ├── HintLayer.ts
│       │   │   ├── Mask.ts
│       │   │   ├── Audio.ts
│       │   │   ├── cta.ts
│       │   │   ├── FingerHint.ts
│       │   │   ├── explosion.ts
│       │   │   ├── common.ts
│       │   │   ├── createColored.ts
│       │   │   ├── LoadingController.ts
│       │   │   ├── play.ts             #       share.html 回放逻辑
│       │   │   ├── mraid.ts
│       │   │   └── ad-platform/
│       │   │       ├── types.ts        #       AdPlatformAdapter 接口
│       │   │       ├── current.ts      #       构建时 Vite alias 替换
│       │   │       └── adapters/
│       │   │           ├── google.ts
│       │   │           ├── applovin.ts
│       │   │           ├── unity.ts
│       │   │           ├── playturbo.ts
│       │   │           ├── helpers.ts
│       │   │           └── storeUrls.ts
│       │   └── utils/
│       │       └── random.ts
│       ├── assets/
│       │   ├── res/                    #     默认填色素材(dev 用)
│       │   │   └── 6a18f7d9957ac783bad75479/
│       │   │       ├── config.json
│       │   │       ├── page.png
│       │   │       ├── map.png
│       │   │       └── special.jpeg
│       │   ├── user/                   #     .gitignore,平台构建时 symlink → storage
│       │   ├── css/
│       │   │   ├── tools.css           #       主样式
│       │   │   └── loading.css         #       加载动画
│       │   ├── img/
│       │   │   ├── logo.png / logo-txt.png
│       │   │   ├── coloring-pages.png / slogon.png
│       │   │   ├── finger.png / finger2.png
│       │   │   └── icon/
│       │   ├── fonts/
│       │   │   └── numbers_roboto_500.png
│       │   └── sound/
│       │       ├── color_done_02.mp3
│       │       ├── section_done.mp3
│       │       └── sound_hint.mp3
│       ├── typings/                    #     TypeScript 声明
│       └── dist/                       #     .gitignore
│
├── platform/                           # 二期平台代码
│   ├── server/
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── src/
│   │       ├── index.ts                #     Express 入口
│   │       ├── routes/
│   │       │   ├── templates.ts        #     GET /api/v1/templates
│   │       │   ├── creatives.ts        #     CRUD /api/v1/creatives
│   │       │   ├── assets.ts           #     POST/DELETE /api/v1/creatives/:id/assets
│   │       │   └── builds.ts           #     POST/GET /api/v1/creatives/:id/builds
│   │       ├── services/
│   │       │   ├── storageService.ts   #     素材存取、zip 解压、文件扫描
│   │       │   ├── buildService.ts     #     构建编排(队列 + mutex)
│   │       │   └── configGenerator.ts  #     生成 _ad_config_.ts + symlink
│   │       ├── db/
│   │       │   ├── database.ts         #     SQLite 初始化 + 迁移
│   │       │   └── seed.ts             #     种子数据(coloring 模板注册)
│   │       └── middleware/
│   │           └── errorHandler.ts
│   │
│   └── client/
│       ├── package.json
│       ├── vite.config.ts
│       ├── index.html
│       └── src/
│           ├── main.tsx                #     React 入口
│           ├── App.tsx                 #     路由
│           ├── api/
│           │   └── client.ts           #     API 请求封装
│           ├── pages/
│           │   ├── Dashboard.tsx       #     创意列表 + 新建入口
│           │   ├── NewCreative.tsx     #     选模板 → 填名称
│           │   └── CreativeDetail.tsx  #     核心工作页(上传+配置+构建)
│           ├── components/
│           │   ├── Layout.tsx
│           │   ├── AssetUploader.tsx   #     zip 上传 + 已上传文件列表
│           │   ├── ThemeEditor.tsx     #     动态表单(由 manifest.theme 驱动)
│           │   ├── PlatformSelector.tsx#     平台勾选
│           │   ├── BuildPanel.tsx      #     构建按钮 + spinner + 产物下载
│           │   ├── BuildHistory.tsx    #     历史构建记录
│           │   └── PreviewModal.tsx    #     HTML 预览 iframe
│           └── types/
│               └── index.ts            #     Template, Creative, Build 类型
│
├── storage/                            # gitignored
│   ├── data.db                         #   SQLite 数据库文件
│   └── creatives/<uuid>/
│       ├── assets/                     #   用户上传 zip 解压产物
│       │   ├── config.json
│       │   ├── page.png
│       │   ├── map.png
│       │   └── special.jpeg            #   可选
│       └── builds/<build_uuid>/
│           ├── google/
│           │   └── index.html
│           ├── applovin/
│           │   └── index.html
│           ├── unity/
│           │   └── index.html
│           ├── playturbo/
│           │   └── index.html
│           ├── mintegral/
│           │   └── index.html
│           └── all.zip                 #   全部产物打包
│
├── docs/
│   └── GIT_GUIDELINES.md
│
├── share.html                          # 分享落地页(现有,保留)
├── agent.md                            # Agent 工作文档
├── README.md                           # 项目说明
├── package.json                        # Root workspace orchestration
├── .gitignore
└── .claude/
    ├── settings.local.json
    └── plans/

.gitignore 关键条目

# 平台构建时动态生成
templates/*/src/filler/_ad_config_.ts
templates/*/assets/user/

# 运行时数据
storage/
platform/server/dist/
platform/client/dist/
dist/
node_modules/

3. 模板参数化

3.1 manifest.json 规范

每个模板目录根部的 manifest.json 是平台了解模板的唯一元数据来源。新增模板 = 新增目录 + 写一个 manifest,无需改平台代码。

templates/coloring/manifest.json

{
  "id": "coloring",
  "name": "填色互动广告",
  "description": "WebGL 填色核心玩法,点击区域上色,完成后展示宣传界面",
  "version": "1.0.0",
  "assets": {
    "uploadFormat": "zip",
    "required": [
      { "key": "config", "file": "config.json", "label": "区域配置文件", "accept": ".json" },
      { "key": "page",    "file": "page.png",    "label": "线稿图",       "accept": ".png,.jpg,.jpeg" },
      { "key": "map",     "file": "map.png",     "label": "映射图",       "accept": ".png" }
    ],
    "optional": [
      { "key": "special", "file": "special.jpeg", "label": "着色参考图(推荐)", "accept": ".jpeg,.jpg,.png" }
    ]
  },
  "theme": {
    "properties": [
      { "key": "bgGradient",   "label": "背景渐变",      "type": "css-gradient", "default": "linear-gradient(160deg, #fff9f2, #fed)" },
      { "key": "ctaGradient",  "label": "CTA 按钮渐变",  "type": "css-gradient", "default": "linear-gradient(135deg, #ff6b6b, #ee5a24)" },
      { "key": "ctaText",      "label": "CTA 按钮文案",  "type": "text",         "default": "PLAY NOW", "maxLength": 30 },
      { "key": "progressColor", "label": "进度条颜色",    "type": "color",        "default": "#07ce07" }
    ]
  },
  "platforms": {
    "available": ["google", "applovin", "unity", "playturbo", "mintegral"],
    "defaults": ["google", "applovin"]
  },
  "build": {
    "command": "npx vite build",
    "cwd": "templates/coloring",
    "outputPatterns": [
      { "platform": "google",    "path": "dist/google/index.html" },
      { "platform": "applovin",  "path": "dist/applovin/index.html" },
      { "platform": "unity",     "path": "dist/unity/index.html" },
      { "platform": "playturbo", "path": "dist/playturbo/index.html" },
      { "platform": "mintegral", "path": "dist/mintegral/index.html" }
    ],
    "adConfigAlias": "#ad-config",
    "adConfigPath": "src/filler/_ad_config_.ts",
    "assetsSymlink": {
      "source": "assets/user",
      "target": "../../storage/creatives/{creativeId}/assets"
    }
  }
}

manifest 字段说明

字段 用途
id 模板唯一标识,用于路由和数据库关联
assets.uploadFormat "zip" 表示素材通过 zip 上传;未来可扩展 "files" 多文件
assets.required[*] 必填素材清单——前端据此渲染上传区,后端据此校验
assets.optional[*] 选填素材——前端标注"(选填)",后端检查存在性来生成条件 import
theme.properties[*] 主题参数——前端据此动态渲染表单控件
platforms.available 此模板支持的目标平台
platforms.defaults 默认勾选的平台
build.outputPatterns 告诉平台构建产物在哪里,复制到哪里

3.2 ad-config 接口

模板 index.ts 的唯一数据入口:

// 类型定义(模板内)
interface AdAssets {
  configRaw: string;          // config.json 原始文本
  pageUrl: string;            // 线稿图 URL
  mapUrl: string;             // 映射图 URL
  specialUrl?: string;        // 着色参考图(可选)
  numberFontUrl: string;      // 数字字体图
  fingerUrl: string;          // 手指引导图
  logoUrl: string;            // Logo 图标
  logoTxtUrl: string;         // Logo 文字
  coloringPagesUrl: string;   // 宣传图1
  slogonUrl: string;          // 宣传图2
}

interface AdTheme {
  bgGradient: string;
  ctaGradient: string;
  ctaText: string;
  progressColor: string;
}

3.3 ad-config-default.ts(提交到仓库,dev/CLI 用)

// templates/coloring/src/filler/ad-config.ts
import configRaw from "/assets/res/6a18f7d9957ac783bad75479/config.json?raw";
import pageUrl from "/assets/res/6a18f7d9957ac783bad75479/page.png?url";
import mapUrl from "/assets/res/6a18f7d9957ac783bad75479/map.png?url";
import specialUrl from "/assets/res/6a18f7d9957ac783bad75479/special.jpeg?url";
import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";
import fingerUrl from "/assets/img/finger.png?url";
import logoUrl from "/assets/img/logo.png?url";
import logoTxtUrl from "/assets/img/logo-txt.png?url";
import coloringPagesUrl from "/assets/img/coloring-pages.png?url";
import slogonUrl from "/assets/img/slogon.png?url";

export const adAssets: AdAssets = {
  configRaw, pageUrl, mapUrl, specialUrl,
  numberFontUrl, fingerUrl, logoUrl, logoTxtUrl,
  coloringPagesUrl, slogonUrl,
};

export const adTheme: AdTheme = {
  bgGradient: "linear-gradient(160deg, #fff9f2, #fed)",
  ctaGradient: "linear-gradient(135deg, #ff6b6b, #ee5a24)",
  ctaText: "PLAY NOW",
  progressColor: "#07ce07",
};

3.4 _adconfig.ts(平台构建时动态生成,gitignored)

平台 configGenerator.ts 根据"用户上传了哪些文件" + "用户选择的主题参数"来生成:

// ↓ 以下为 configGenerator.ts 拼接生成 ↓

// ==== 用户素材(从 storage/creatives/<id>/assets/ → symlink → assets/user/) ====
import configRaw from "/assets/user/config.json?raw";
import pageUrl from "/assets/user/page.png?url";
import mapUrl from "/assets/user/map.png?url";
// 仅当 special.jpeg 存在于用户上传包中时,才生成此行:
import specialUrl from "/assets/user/special.jpeg?url";

// ==== 模板自有素材 ====
import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";
import fingerUrl from "/assets/img/finger.png?url";
import logoUrl from "/assets/img/logo.png?url";
import logoTxtUrl from "/assets/img/logo-txt.png?url";
import coloringPagesUrl from "/assets/img/coloring-pages.png?url";
import slogonUrl from "/assets/img/slogon.png?url";

export const adAssets = {
  configRaw, pageUrl, mapUrl,
  // 仅当 special.jpeg 存在时才有此字段:
  specialUrl,
  numberFontUrl, fingerUrl, logoUrl, logoTxtUrl,
  coloringPagesUrl, slogonUrl,
};

export const adTheme = {
  bgGradient: "linear-gradient(160deg, #fff9f2, #fed)",
  ctaGradient: "linear-gradient(135deg, #ff6b6b, #ee5a24)",
  ctaText: "PLAY NOW",
  progressColor: "#07ce07",
};

3.5 index.ts 重构要点

当前 loadResource() (index.ts:251-275):11 行分散 import,hardcode 素材路径。

重构后

// templates/coloring/src/filler/index.ts
import { adAssets, adTheme } from "#ad-config";

async function loadResource(): Promise<FillerResource> {
  const config = JSON.parse(adAssets.configRaw) as AreaGroups;
  const [page, map, numberImage] = await Promise.all([
    loadImage(adAssets.pageUrl),
    loadImage(adAssets.mapUrl),
    loadImage(adAssets.numberFontUrl),
  ]);

  // optional: 仅当用户上传了 special 时才尝试加载
  let special: HTMLImageElement | undefined;
  if ("specialUrl" in adAssets && adAssets.specialUrl) {
    try {
      special = await loadImage(adAssets.specialUrl);
    } catch { /* fallback to createColored */ }
  }

  return new FillerResource(config, page, map, numberImage, [], special);
}

3.6 vite.config.js 改动

// templates/coloring/vite.config.js
const { defineConfig } = require("vite");
const { viteSingleFile } = require("vite-plugin-singlefile");
const path = require("path");

module.exports = defineConfig(({ mode }) => {
  const platformBuild = platformBuilds[mode];
  const adapter = platformBuild?.adapter || "google";
  const output = platformBuild?.output;
  const outDir = output ? `dist/${output}` : "dist";

  // ★ 新增:#ad-config alias,通过环境变量切换
  const adConfigPath = process.env.AD_CONFIG_PATH
    ? path.resolve(__dirname, process.env.AD_CONFIG_PATH)
    : path.resolve(__dirname, "src/filler/ad-config.ts");

  return {
    plugins: [viteSingleFile(), finalizeHtmlPlugin(outDir, adapter)],
    resolve: {
      alias: {
        "#ad-config": adConfigPath,                // ★ 新增
        "./ad-platform/current": path.resolve(
          __dirname,
          `src/filler/ad-platform/adapters/${adapter}.ts`,
        ),
      },
    },
    build: {
      assetsInlineLimit: 100 * 1024 * 1024,
      target: "es2017",
      outDir,
      emptyOutDir: true,
      rollupOptions: {
        input: { main: "./index.html" },
        output: {
          entryFileNames: "assets/[name].js",
          chunkFileNames: "assets/[name].js",
          assetFileNames: "assets/[name][extname]",
        },
      },
    },
  };
});

改动极小:只加了一个 #ad-config alias,其他不变。


4. 数据库设计

SQLite,五张表。

4.1 ER 概览

templates  1──N  creatives  1──N  builds
  │                                  │
  └── manifest (JSON)                └── results (JSON)
                  
creatives.theme (JSON)
creatives 1──N creative_assets (上传文件清单)

4.2 建表语句

-- 模板注册表
CREATE TABLE templates (
  id          TEXT PRIMARY KEY,          -- 'coloring'
  name        TEXT NOT NULL,
  manifest    TEXT NOT NULL,             -- manifest.json 全文
  created_at  TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- 创意表
CREATE TABLE creatives (
  id          TEXT PRIMARY KEY,          -- UUID
  name        TEXT NOT NULL,             -- 运营人员命名的名称
  template_id TEXT NOT NULL REFERENCES templates(id),
  theme       TEXT NOT NULL DEFAULT '{}',-- JSON: 用户配置的主题参数
  status      TEXT NOT NULL DEFAULT 'draft', -- draft | assets_ready | building | built | failed
  created_at  TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- 素材文件记录(从 zip 解压出来的文件清单)
CREATE TABLE creative_assets (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  creative_id TEXT NOT NULL REFERENCES creatives(id),
  file_key    TEXT NOT NULL,             -- manifest 中定义的 key: 'config' | 'page' | 'map' | 'special'
  file_name   TEXT NOT NULL,             -- 原始文件名
  file_path   TEXT NOT NULL,             -- 相对 storage 的路径
  file_size   INTEGER,                  -- 字节
  is_required INTEGER NOT NULL DEFAULT 1,
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- 构建记录表
CREATE TABLE builds (
  id          TEXT PRIMARY KEY,          -- UUID
  creative_id TEXT NOT NULL REFERENCES creatives(id),
  status      TEXT NOT NULL DEFAULT 'pending', -- pending | building | completed | failed
  platforms   TEXT NOT NULL,             -- JSON: ["google","applovin"]
  theme_snapshot TEXT NOT NULL,          -- JSON: 构建时的 theme 快照
  results     TEXT,                      -- JSON: [{platform, filePath, fileSize}]
  error_log   TEXT,
  started_at  TEXT,
  finished_at TEXT,
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- 索引
CREATE INDEX idx_creatives_template ON creatives(template_id);
CREATE INDEX idx_builds_creative ON builds(creative_id);
CREATE INDEX idx_creative_assets_creative ON creative_assets(creative_id);

4.3 状态流转

creatives:
  draft ──→ assets_ready ──→ building ──→ built
               │                 │
               └──→ (re-upload)  └──→ failed (可重试)

builds:
  pending ──→ building ──→ completed
                │
                └──→ failed

5. API 设计

Base: /api/v1

5.1 模板

方法 路径 说明
GET /templates 模板列表(含 manifest 中的 assets/theme/platforms 摘要)
GET /templates/:id 单个模板详情(含完整 manifest)

GET /templates 响应:

{
  "data": [{
    "id": "coloring",
    "name": "填色互动广告",
    "description": "WebGL 填色核心玩法",
    "version": "1.0.0",
    "platforms": ["google","applovin","unity","playturbo","mintegral"],
    "assetCount": { "required": 3, "optional": 1 }
  }]
}

5.2 创意 CRUD

方法 路径 说明
GET /creatives 创意列表(支持 ?status= 筛选,?page=&limit= 分页)
POST /creatives 新建创意
GET /creatives/:id 创意详情(含 assets 列表、builds 摘要)
PATCH /creatives/:id 更新创意名称或 theme
DELETE /creatives/:id 删除创意及其所有 assets 和 builds

POST /creatives 请求:

{
  "name": "萌宠填色-谷歌渠道",
  "templateId": "coloring"
}

GET /creatives/:id 响应:

{
  "data": {
    "id": "uuid-1",
    "name": "萌宠填色-谷歌渠道",
    "templateId": "coloring",
    "template": { "id": "coloring", "name": "填色互动广告", "version": "1.0.0" },
    "status": "assets_ready",
    "theme": { "bgGradient": "...", "ctaText": "PLAY NOW" },
    "assets": [
      { "key": "config", "fileName": "config.json", "fileSize": 2048, "isRequired": true },
      { "key": "page",    "fileName": "page.png",    "fileSize": 302000, "isRequired": true },
      { "key": "map",     "fileName": "map.png",     "fileSize": 46000, "isRequired": true },
      { "key": "special", "fileName": "special.jpeg","fileSize": 150000, "isRequired": false }
    ],
    "recentBuilds": [
      { "id": "uuid-b1", "status": "completed", "platforms": ["google"], "createdAt": "..." }
    ],
    "createdAt": "2026-06-03T10:00:00Z",
    "updatedAt": "2026-06-03T10:30:00Z"
  }
}

5.3 素材管理

方法 路径 说明
POST /creatives/:id/assets/upload 上传素材 zip(multipart/form-data)
DELETE /creatives/:id/assets 清除当前创意所有素材

POST /creatives/:id/assets/upload

  • Content-Type: multipart/form-data
  • Field: file.zip 文件
  • 后端解压 → 校验(对照 manifest.assets.required)→ 存入 storage/creatives/:id/assets/
  • 成功后创意状态 → assets_ready

响应:

{
  "data": {
    "files": [
      { "key": "config", "fileName": "config.json", "fileSize": 2048, "valid": true },
      { "key": "page",    "fileName": "page.png",    "fileSize": 302000, "valid": true },
      { "key": "map",     "fileName": "map.png",     "fileSize": 46000, "valid": true }
    ],
    "warnings": []
  }
}

5.4 构建

方法 路径 说明
POST /creatives/:id/builds 触发构建
GET /creatives/:id/builds 构建历史
GET /builds/:id/status 构建状态(轮询)
GET /builds/:id/download/:platform 下载单个平台 HTML
GET /builds/:id/download/all 下载全部产物 ZIP

POST /creatives/:id/builds 请求:

{
  "platforms": ["google", "applovin", "unity"],
  "theme": {
    "bgGradient": "linear-gradient(160deg, #fff9f2, #fed)",
    "ctaGradient": "linear-gradient(135deg, #ff6b6b, #ee5a24)",
    "ctaText": "PLAY NOW",
    "progressColor": "#07ce07"
  }
}

响应:

{
  "data": {
    "id": "uuid-b1",
    "status": "pending",
    "platforms": ["google", "applovin", "unity"]
  }
}

GET /builds/:id/status 响应(轮询用):

{
  "data": {
    "id": "uuid-b1",
    "status": "completed",
    "results": [
      { "platform": "google",    "fileSize": 1024000 },
      { "platform": "applovin",  "fileSize": 1025000 },
      { "platform": "unity",     "fileSize": 1023000 }
    ]
  }
}

6. 构建服务

6.1 构建流程

buildService.build(creativeId, buildId, platforms, theme)
        │
        ▼
  ┌─ 1. 预检查 ─────────────────────────────────────┐
  │  - 读取 manifest.json                            │
  │  - 校验必填素材文件均存在                          │
  │  - 校验 creative.status ∈ {assets_ready, built}   │
  └──────────────────────────────────────────────────┘
        │
        ▼
  ┌─ 2. 准备构建环境 ────────────────────────────────┐
  │  - 创建/更新 symlink:                             │
  │    templates/coloring/assets/user/                │
  │    → storage/creatives/<creativeId>/assets/       │
  │  - configGenerator.generate(creativeId, theme):  │
  │    扫描 assets/ 目录,生成 _ad_config_.ts         │
  │    包含条件 import(special 存在则导入)           │
  └──────────────────────────────────────────────────┘
        │
        ▼
  ┌─ 3. 构建循环(串行)─────────────────────────────┐
  │  for each platform in platforms:                  │
  │    $ cd templates/coloring                        │
  │    $ AD_CONFIG_PATH=src/filler/_ad_config_.ts \   │
  │      npx vite build --mode <platform>             │
  │    → 生成 dist/<platform>/index.html              │
  │    → 复制到 storage/creatives/<id>/builds/<bid>/  │
  └──────────────────────────────────────────────────┘
        │
        ▼
  ┌─ 4. 收尾 ───────────────────────────────────────┐
  │  - 打包 all.zip(archiver)                       │
  │  - 更新 builds 表 (status=completed, results)      │
  │  - 更新 creatives 表 (status=built)               │
  │  - 删除 _ad_config_.ts(可选保留 symlink)        │
  └──────────────────────────────────────────────────┘

6.2 configGenerator 核心逻辑

// platform/server/src/services/configGenerator.ts
import fs from "fs";
import path from "path";

interface GenerateConfigInput {
  creativeId: string;
  theme: Record<string, string>;
  manifest: Manifest;
}

function generate(input: GenerateConfigInput): string {
  const userAssetsDir = path.resolve(
    __dirname, `../../../storage/creatives/${input.creativeId}/assets`
  );
  const files = fs.readdirSync(userAssetsDir);
  const hasSpecial = files.includes("special.jpeg") || files.includes("special.jpg");

  let imports = "";
  let assetFields = "";

  // Required assets (always present after validation)
  imports += `import configRaw from "/assets/user/config.json?raw";\n`;
  imports += `import pageUrl from "/assets/user/page.png?url";\n`;
  imports += `import mapUrl from "/assets/user/map.png?url";\n`;
  assetFields += "  configRaw, pageUrl, mapUrl,\n";

  // Optional: special image
  if (hasSpecial) {
    const ext = files.find(f => f.startsWith("special."))!.split(".").pop();
    imports += `import specialUrl from "/assets/user/special.${ext}?url";\n`;
    assetFields += "  specialUrl,\n";
  }

  // Template-provided assets
  imports += `import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";\n`;
  imports += `import fingerUrl from "/assets/img/finger.png?url";\n`;
  imports += `import logoUrl from "/assets/img/logo.png?url";\n`;
  // ... etc

  return `${imports}
export const adAssets = {
${assetFields}  numberFontUrl, fingerUrl, logoUrl,
  logoTxtUrl, coloringPagesUrl, slogonUrl,
};

export const adTheme = ${JSON.stringify(input.theme, null, 2)};
`;
}

6.3 构建串行化

// platform/server/src/services/buildService.ts
class BuildService {
  private queue: Array<() => Promise<void>> = [];
  private running = false;

  async enqueue(fn: () => Promise<void>) {
    this.queue.push(fn);
    if (!this.running) this.processQueue();
  }

  private async processQueue() {
    this.running = true;
    while (this.queue.length > 0) {
      const task = this.queue.shift()!;
      await task();
    }
    this.running = false;
  }
}

Vite build 自身就是单线程的,队列只需保证不并发写 _ad_config_.ts

6.4 超时与错误处理

  • 单次 vite build 超时:120s(正常构建 < 30s)
  • 构建失败:写 error_log,状态 → failed,不清除 symlink(方便排查)
  • 构建失败后允许用户在 Web UI 点"重试"

7. 前端设计

7.1 路由

/                     Dashboard      创意列表卡片 + 新建入口
/creatives/new        NewCreative    选模板 → 填名称
/creatives/:id        CreativeDetail 上传素材 + 主题配置 + 构建

7.2 页面结构

Dashboard(/):

┌─────────────────────────────────────────┐
│  Playable Ads Platform          [+新建创意] │
├─────────────────────────────────────────┤
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│  │ 创意1     │ │ 创意2     │ │ 创意3     │ │
│  │ 模板:填色  │ │ 模板:填色  │ │ 模板:填色  │ │
│  │ 状态:已就绪│ │ 状态:已构建│ │ 状态:草稿  │ │
│  │ 3 files   │ │ 4 files   │ │ 0 files   │ │
│  └──────────┘ └──────────┘ └──────────┘ │
│                                         │
│  (empty state: "暂无创意,点击新建")     │
└─────────────────────────────────────────┘

NewCreative(/creatives/new):

┌─────────────────────────────────────────┐
│  ← 返回                                  │
│  新建创意                                │
│                                         │
│  选择模板:                              │
│  ┌──────────────────────────┐ (选中✓)   │
│  │ 🎨 填色互动广告            │          │
│  │ WebGL 填色核心玩法         │          │
│  └──────────────────────────┘           │
│                                         │
│  创意名称:[萌宠填色-谷歌渠道____]        │
│                                         │
│  [取消]  [创建]                          │
└─────────────────────────────────────────┘

CreativeDetail(/creatives/:id)—— 核心工作页:

┌──────────────────────────────────────────────────────────┐
│  ← 返回    萌宠填色-谷歌渠道    状态: ● 素材已就绪        │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  ┌─ 左栏:素材上传 ──────────┐ ┌─ 右栏:主题配置 ───────┐ │
│  │                           │ │                        │ │
│  │  📦 上传素材 zip           │ │  背景渐变              │ │
│  │  [拖拽或点击上传]          │ │  [████████░░░░]        │ │
│  │                           │ │                        │ │
│  │  已上传文件 (3/3 必填):    │ │  CTA 渐变              │ │
│  │  ✅ config.json   2 KB    │ │  [████████░░░░]        │ │
│  │  ✅ page.png    302 KB    │ │                        │ │
│  │  ✅ map.png      46 KB    │ │  CTA 文案              │ │
│  │  ⬜ special.jpeg (选填)   │ │  [PLAY NOW       ]     │ │
│  │                           │ │                        │ │
│  │  [清除素材]                │ │  进度条颜色            │ │
│  │                           │ │  [■] #07ce07           │ │
│  └───────────────────────────┘ └────────────────────────┘ │
│                                                          │
│  ┌─ 构建 ───────────────────────────────────────────────┐ │
│  │  目标平台:                                           │ │
│  │  ☑ Google  ☑ Applovin  ☑ Unity  ☐ Playturbo  ☐ Mintegral │
│  │                                                      │ │
│  │  [🚀 开始构建]  (素材未上传时 disabled)               │ │
│  │                                                      │ │
│  │  ┌─ 构建历史 ───────────────────────────────────┐    │ │
│  │  │ #3  2026-06-03 14:30  ✅ 完成                │    │ │
│  │  │   [Google ↓] [Applovin ↓] [Unity ↓] [全部ZIP ↓] │   │ │
│  │  │                                              │    │ │
│  │  │ #2  2026-06-03 11:00  ❌ 失败                │    │ │
│  │  │   错误: Required file 'map.png' missing      │    │ │
│  │  └──────────────────────────────────────────────┘    │ │
│  └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

7.3 组件状态覆盖

组件 Loading Empty Error Success
Dashboard Skeleton cards "暂无创意,点击新建" "加载失败,点击重试" 卡片列表
AssetUploader 上传进度条 "拖拽 zip 到此处" "解压失败:缺少 config.json" 文件清单
ThemeEditor (manifest 无 theme 时隐藏整栏) 表单控件
BuildPanel 按钮 disabled+spinner "构建失败:xxx" + 重试按钮 下载按钮
BuildHistory Spinner 占位 "暂无构建记录" 构建列表

7.4 状态管理

前端状态简单,不需要 Redux 等状态库。使用 React hooks + 页面级 state:

  • Dashboard 自身管理创意列表
  • CreativeDetailuseReducer 管理一个页面的复杂状态:
    • 创意基本信息
    • 素材文件列表
    • 主题表单值
    • 构建状态 / 历史

API 请求封装为 hooks:

useCreatives()           // → { data, loading, error, refetch }
useCreative(id)          // → { data, loading, error }
useBuilds(creativeId)    // → { data, loading, error, trigger }
useBuildStatus(buildId)  // → { status, results }  轮询

8. 实施阶段

阶段 1:基础搭建(预计 2-3 天)

目标:Express 启动、React 可访问、DB 初始化。

# 任务 产出
1.1 重构目录结构:src/templates/coloring/,创建 platform/ 目录就位
1.2 创建 templates/coloring/manifest.json manifest 文件
1.3 platform/server/:Express + SQLite + better-sqlite3 初始化 server 可启动
1.4 建表 + 种子数据(coloring 模板注册) DB 就绪
1.5 platform/client/:Vite React + react-router-dom 脚手架 dev server 可访问
1.6 清理:删除 setting.css 引用(index.html)、标记 Loader.ts 为待移除 代码整洁

验收

  • cd platform/server && npm run dev → :3001 响应
  • cd platform/client && npm run dev → :5173 React 页面可访问
  • GET /api/v1/templates 返回 coloring 模板

阶段 2:模板参数化(预计 1.5-2 天)

目标index.ts 改为从 #ad-config 导入,dev/CLI 构建行为不变。

# 任务 产出
2.1 创建 ad-config.ts(默认配置,内容 = 当前 11 行 import) 默认配置就位
2.2 重构 index.ts:分散 import → import { adAssets, adTheme } from "#ad-config" index.ts 重构完成
2.3 loadResource() 适配:检查 "specialUrl" in adAssets optional 处理
2.4 vite.config.js#ad-config alias (+ 环境变量切换) alias 就位
2.5 验证:npm run dev(模板目录)行为不变 dev 正常
2.6 验证:npm run build:all(模板目录)产物不变 构建正常
2.7 验证:AD_CONFIG_PATH=src/filler/ad-config.ts npx vite build 产物一致 环境变量切换正常
2.8 移除 Loader.ts(未被任何代码引用) 死代码清理

验收:日常开发和 CLI 构建行为与重构前完全一致。dist/ 下所有平台产物大小、功能无差异。


阶段 3:后端 API(预计 2-3 天)

目标:curl 可触发完整"上传 zip → 构建 → 下载"流程。

# 任务 产出
3.1 storageService:zip 解压(adm-zip)、文件扫描、校验(对照 manifest) 素材存取
3.2 configGenerator:根据文件列表 + theme 生成 _ad_config_.ts + 创建 symlink 配置生成
3.3 buildService:构建队列 + mutex + npx vite build 调用 + 产物复制 + ZIP 打包 构建服务
3.4 GET /templates / GET /templates/:id 模板 API
3.5 GET/POST /creatives / GET/PATCH/DELETE /creatives/:id 创意 CRUD
3.6 POST /creatives/:id/assets/upload / DELETE /creatives/:id/assets 素材 API
3.7 POST/GET /creatives/:id/builds / GET /builds/:id/status / GET /builds/:id/download/:platform 构建 API
3.8 错误处理 + 日志 稳定性

验收

# 1. 创建创意
curl -X POST http://localhost:3001/api/v1/creatives \
  -H "Content-Type: application/json" \
  -d '{"name":"测试创意","templateId":"coloring"}'

# 2. 上传素材
curl -X POST http://localhost:3001/api/v1/creatives/<id>/assets/upload \
  -F "file=@test-assets.zip"

# 3. 触发构建
curl -X POST http://localhost:3001/api/v1/creatives/<id>/builds \
  -H "Content-Type: application/json" \
  -d '{"platforms":["google","applovin"],"theme":{...}}'

# 4. 轮询状态
curl http://localhost:3001/api/v1/builds/<bid>/status

# 5. 下载
curl -O http://localhost:3001/api/v1/builds/<bid>/download/all

阶段 4:前端 UI(预计 2-3 天)

目标:浏览器端可完整体验"新建创意 → 上传素材 → 配置 → 构建 → 下载"。

# 任务 产出
4.1 API client 封装 + React Query hooks 数据层
4.2 Layout + Dashboard(列表 + 空态 + 新建入口) 首页
4.3 NewCreative(模板选择 + 名称输入) 新建页
4.4 CreativeDetail — 左栏 AssetUploader(拖拽上传 zip + 文件列表 + 清除) 素材区
4.5 CreativeDetail — 右栏 ThemeEditor(由 manifest.theme 驱动的动态表单) 配置区
4.6 CreativeDetail — 底部 PlatformSelector + 构建按钮 + BuildHistory 构建区
4.7 BuildPanel:触发构建 → 轮询状态(spinner)→ 完成显示下载按钮 构建交互
4.8 各组件 loading / empty / error 状态覆盖 完整性
4.9 响应式适配(Desktop 双栏,Mobile 上下堆叠) 移动端可用

验收:浏览器全流程可操作,各状态覆盖无白屏。


阶段 5:部署(预计 1-2 天)

目标:ECS 可访问完整平台。

# 任务 产出
5.1 Client 生产构建(vite build)→ platform/client/dist/ 静态文件
5.2 Server 编译(tsc)→ Express 同时服务 API + 静态文件 单体部署
5.3 Dockerfile(Node 20 + 模板依赖预装) Docker 化
5.4 ECS 部署 + 端口映射(3001) 公网可访问
5.5 端到端手动测试(上传 zip → 构建 → 下载 → 真机验证产物) 质量确认

验收http://42.193.231.145:3001 可访问,完整流程无报错。


9. 风险清单

风险 概率 影响 缓解措施
用户上传的 zip 文件命名不规范(大小写、扩展名) 文件匹配时忽略大小写和扩展名变体(.jpeg/.jpg.png/.PNG
大素材包导致 Vite build 超时(>120s) 设置合理超时,前端提示"构建时间较长请耐心等待"
并发构建写 _ad_config_.ts 冲突 Mutex 队列串行化,每 creative 同一时间只有 1 个构建
storage/ 磁盘积累 保留最近 10 次构建产物;后续可加自动清理 cron
WebGL 2 在低端 Android WebView 不支持 模板层面检测 + 静态降级画面(一期已有基础)
Playturbo 扫描拒绝动态生成的 HTML 构建后 finalize plugin 已处理 type="module" 移除(一期已验证)
模板目录结构变动后 vite alias 失效 vite.config.js 使用 path.resolve(__dirname, ...) 绝对路径
optional 文件判断逻辑错误导致构建失败 configGenerator 单元测试覆盖"有/无 special"两种场景

10. 后续扩展预留

当前只实现 coloring 模板,但架构已预留多模板支持:

  1. 新增模板步骤

    • templates/<new-id>/ 下放置完整 Vite 项目
    • 编写 manifest.json
    • 在 DB templates 表中插入一行(或 seed 脚本中添加)
    • 前端自动读取 manifest 渲染对应 UI
  2. manifest 未来可扩展字段

    • previewImage:模板缩略图,Dashboard 展示
    • build.env:构建时需要注入的额外环境变量
    • assets.previewHint:上传区的示例图
    • theme.groups:主题参数分组(基础/高级)
  3. 平台功能后续可加

    • 模板在线预览(iframe 嵌入模板 dev server)
    • 构建产物在线预览(iframe 嵌入生成的 HTML)
    • 批量构建(一个创意选多个平台一键构建)
    • WebHook 通知(构建完成通知到企业微信/钉钉)