# 二期：广告制作管理平台 — 细化方案 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`：**

```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` 的唯一数据入口：

```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 用）

```ts
// 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 _ad_config_.ts（平台构建时动态生成，gitignored）

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

```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 素材路径。

**重构后**：

```ts
// 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 改动

```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 建表语句

```sql
-- 模板注册表
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` 响应：**
```json
{
  "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` 请求：**
```json
{
  "name": "萌宠填色-谷歌渠道",
  "templateId": "coloring"
}
```

**`GET /creatives/:id` 响应：**
```json
{
  "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`

**响应：**
```json
{
  "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` 请求：**
```json
{
  "platforms": ["google", "applovin", "unity"],
  "theme": {
    "bgGradient": "linear-gradient(160deg, #fff9f2, #fed)",
    "ctaGradient": "linear-gradient(135deg, #ff6b6b, #ee5a24)",
    "ctaText": "PLAY NOW",
    "progressColor": "#07ce07"
  }
}
```

**响应：**
```json
{
  "data": {
    "id": "uuid-b1",
    "status": "pending",
    "platforms": ["google", "applovin", "unity"]
  }
}
```

**`GET /builds/:id/status` 响应（轮询用）：**
```json
{
  "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 核心逻辑

```ts
// 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 构建串行化

```ts
// 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` 自身管理创意列表
- `CreativeDetail` 用 `useReducer` 管理一个页面的复杂状态：
  - 创意基本信息
  - 素材文件列表
  - 主题表单值
  - 构建状态 / 历史

API 请求封装为 hooks：
```ts
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 | 错误处理 + 日志 | 稳定性 |

**验收**：
```bash
# 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 通知（构建完成通知到企业微信/钉钉）
