# 二期:广告制作管理平台 — 细化方案 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// │ ├── assets/ # 用户上传的 zip 解压产物 │ └── builds//# 构建产物 │ └── templates/coloring/ # 模板 Vite 项目 │ ▼ npx vite build --mode ``` ## 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// │ ├── assets/ # 用户上传 zip 解压产物 │ │ ├── config.json │ │ ├── page.png │ │ ├── map.png │ │ └── special.jpeg # 可选 │ └── builds// │ ├── 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//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 { 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//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 │ │ → 生成 dist//index.html │ │ → 复制到 storage/creatives//builds// │ └──────────────────────────────────────────────────┘ │ ▼ ┌─ 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; 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> = []; private running = false; async enqueue(fn: () => Promise) { 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//assets/upload \ -F "file=@test-assets.zip" # 3. 触发构建 curl -X POST http://localhost:3001/api/v1/creatives//builds \ -H "Content-Type: application/json" \ -d '{"platforms":["google","applovin"],"theme":{...}}' # 4. 轮询状态 curl http://localhost:3001/api/v1/builds//status # 5. 下载 curl -O http://localhost:3001/api/v1/builds//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//` 下放置完整 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 通知(构建完成通知到企业微信/钉钉)