guoziyun 3 هفته پیش
والد
کامیت
3f6a0fa093

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 8
platform/client/dist/assets/index-0G6wIYVf.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
platform/client/dist/assets/index-Bd4owCos.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 8 - 0
platform/client/dist/assets/index-CsY2SnZf.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
platform/client/dist/assets/index-DrsA2kn0.css


+ 2 - 2
platform/client/dist/index.html

@@ -4,8 +4,8 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>Playable Ads Platform</title>
-    <script type="module" crossorigin src="/ads/assets/index-0G6wIYVf.js"></script>
-    <link rel="stylesheet" crossorigin href="/ads/assets/index-DrsA2kn0.css">
+    <script type="module" crossorigin src="/ads/assets/index-CsY2SnZf.js"></script>
+    <link rel="stylesheet" crossorigin href="/ads/assets/index-Bd4owCos.css">
   </head>
   <body>
     <div id="root"></div>

+ 1 - 1
platform/server/dist/routes/assets.d.ts.map

@@ -1 +1 @@
-{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/routes/assets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAqGtC,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAiI9E"}
+{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/routes/assets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAkHtC,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA+L9E"}

+ 85 - 8
platform/server/dist/routes/assets.js

@@ -24,6 +24,19 @@ const upload = (0, multer_1.default)({
         }
     },
 });
+// 单图片上传(logo/slogon 等手动替换)
+const uploadImage = (0, multer_1.default)({
+    storage: multer_1.default.memoryStorage(),
+    limits: { fileSize: 5 * 1024 * 1024 },
+    fileFilter: (_req, file, cb) => {
+        if (file.mimetype.startsWith("image/")) {
+            cb(null, true);
+        }
+        else {
+            cb(new Error("Only image files are allowed"));
+        }
+    },
+});
 /**
  * 从 zip buffer 中提取素材文件、校验、写入磁盘、更新 DB。
  * 文件上传和 URL 导入共用此逻辑。
@@ -136,18 +149,38 @@ function assetsRouter(db, storageDir) {
     router.get("/creatives/:id/assets/:key", (req, res) => {
         const creativeId = req.params.id;
         const fileKey = req.params.key;
+        // 1. 先查用户上传的素材
         const asset = db
             .prepare("SELECT file_path, file_name FROM creative_assets WHERE creative_id = ? AND file_key = ?")
             .get(creativeId, fileKey);
-        if (!asset) {
-            res.status(404).json({ error: { message: "Asset not found" } });
-            return;
+        let filePath;
+        let fileName;
+        if (asset && fs_1.default.existsSync(asset.file_path)) {
+            filePath = asset.file_path;
+            fileName = asset.file_name;
         }
-        if (!fs_1.default.existsSync(asset.file_path)) {
-            res.status(404).json({ error: { message: "File not found on disk" } });
-            return;
+        else {
+            // 2. 未上传 → 尝试回退到模板默认图
+            const TEMPLATE_IMG_DIR = path_1.default.resolve(__dirname, "../../../../templates/coloring/assets/img");
+            const defaultMap = {
+                logo: "logo.png",
+                logoTxt: "logo-txt.png",
+                slogon: "slogon.png",
+                coloringPages: "coloring-pages.png",
+            };
+            const defaultFile = defaultMap[fileKey];
+            if (!defaultFile) {
+                res.status(404).json({ error: { message: "Asset not found" } });
+                return;
+            }
+            filePath = path_1.default.join(TEMPLATE_IMG_DIR, defaultFile);
+            fileName = defaultFile;
+            if (!fs_1.default.existsSync(filePath)) {
+                res.status(404).json({ error: { message: "Default asset not found" } });
+                return;
+            }
         }
-        const ext = path_1.default.extname(asset.file_name).toLowerCase();
+        const ext = path_1.default.extname(fileName).toLowerCase();
         const mimeTypes = {
             ".png": "image/png",
             ".jpg": "image/jpeg",
@@ -159,7 +192,51 @@ function assetsRouter(db, storageDir) {
         const contentType = mimeTypes[ext] || "application/octet-stream";
         res.setHeader("Content-Type", contentType);
         res.setHeader("Cache-Control", "public, max-age=3600");
-        fs_1.default.createReadStream(asset.file_path).pipe(res);
+        fs_1.default.createReadStream(filePath).pipe(res);
+    });
+    // POST /api/v1/creatives/:id/assets/:key — 上传单个素材文件(手动替换)
+    router.post("/creatives/:id/assets/:key", uploadImage.single("file"), (req, res) => {
+        try {
+            const creativeId = req.params.id;
+            const fileKey = req.params.key;
+            const creative = db
+                .prepare("SELECT c.*, t.manifest FROM creatives c JOIN templates t ON c.template_id = t.id WHERE c.id = ?")
+                .get(creativeId);
+            if (!creative) {
+                res.status(404).json({ error: { message: "Creative not found" } });
+                return;
+            }
+            const manifest = JSON.parse(creative.manifest);
+            const requiredAssets = manifest.assets?.required ?? [];
+            const optionalAssets = manifest.assets?.optional ?? [];
+            const def = [...requiredAssets, ...optionalAssets].find((d) => d.key === fileKey);
+            if (!def) {
+                res.status(400).json({ error: { message: `Unknown asset key: ${fileKey}` } });
+                return;
+            }
+            if (!req.file) {
+                res.status(400).json({ error: { message: "请选择文件" } });
+                return;
+            }
+            const assetsDir = path_1.default.join(storageDir, "creatives", creativeId, "assets");
+            if (!fs_1.default.existsSync(assetsDir))
+                fs_1.default.mkdirSync(assetsDir, { recursive: true });
+            // 删除同 key 的旧文件
+            const oldAsset = db.prepare("SELECT file_path FROM creative_assets WHERE creative_id = ? AND file_key = ?").get(creativeId, fileKey);
+            if (oldAsset && fs_1.default.existsSync(oldAsset.file_path))
+                fs_1.default.unlinkSync(oldAsset.file_path);
+            const filePath = path_1.default.join(assetsDir, def.file);
+            fs_1.default.writeFileSync(filePath, req.file.buffer);
+            const stat = fs_1.default.statSync(filePath);
+            db.prepare("DELETE FROM creative_assets WHERE creative_id = ? AND file_key = ?").run(creativeId, fileKey);
+            db.prepare("INSERT INTO creative_assets (creative_id, file_key, file_name, file_path, file_size, is_required) VALUES (?, ?, ?, ?, ?, ?)").run(creativeId, fileKey, def.file, filePath, stat.size, requiredAssets.some((r) => r.key === fileKey) ? 1 : 0);
+            console.log(`[assets] Single upload: ${fileKey} -> ${filePath} (${stat.size} bytes)`);
+            res.json({ data: { key: fileKey, fileName: def.file, fileSize: stat.size, valid: true } });
+        }
+        catch (err) {
+            console.error("[assets] Single upload error:", err.message);
+            res.status(500).json({ error: { message: err.message } });
+        }
     });
     // DELETE /api/v1/creatives/:id/assets — 清除素材
     router.delete("/creatives/:id/assets", (req, res) => {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
platform/server/dist/routes/assets.js.map


+ 1 - 1
platform/server/dist/services/configGenerator.d.ts.map

@@ -1 +1 @@
-{"version":3,"file":"configGenerator.d.ts","sourceRoot":"","sources":["../../src/services/configGenerator.ts"],"names":[],"mappings":"AAMA,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAwD7D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAkBhF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAO5C"}
+{"version":3,"file":"configGenerator.d.ts","sourceRoot":"","sources":["../../src/services/configGenerator.ts"],"names":[],"mappings":"AAMA,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAqE7D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAkBhF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAO5C"}

+ 18 - 5
platform/server/dist/services/configGenerator.js

@@ -32,15 +32,27 @@ function generateAdConfig(input) {
         hasSpecial = true;
         lines.push(`import specialUrl from "/assets/user/${files.special}?url";`);
     }
-    // == 模板自有素材 ==
+    // == 模板自有素材(用户上传则优先使用)==
     lines.push("");
     lines.push("// ==== 模板自有素材 ====");
     lines.push(`import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";`);
     lines.push(`import fingerUrl from "/assets/img/finger.png?url";`);
-    lines.push(`import logoUrl from "/assets/img/logo.png?url";`);
-    lines.push(`import logoTxtUrl from "/assets/img/logo-txt.png?url";`);
-    lines.push(`import coloringPagesUrl from "/assets/img/coloring-pages.png?url";`);
-    lines.push(`import slogonUrl from "/assets/img/slogon.png?url";`);
+    // 可替换素材:优先用用户上传的,否则用模板默认
+    const replaceableAssets = [
+        { key: "logo", file: "logo.png", var: "logoUrl", defaultPath: "/assets/img/logo.png" },
+        { key: "logoTxt", file: "logo-txt.png", var: "logoTxtUrl", defaultPath: "/assets/img/logo-txt.png" },
+        { key: "slogon", file: "slogon.png", var: "slogonUrl", defaultPath: "/assets/img/slogon.png" },
+        { key: "coloringPages", file: "coloring-pages.png", var: "coloringPagesUrl", defaultPath: "/assets/img/coloring-pages.png" },
+    ];
+    for (const a of replaceableAssets) {
+        const userFile = files[a.key];
+        if (userFile) {
+            lines.push(`import ${a.var} from "/assets/user/${userFile}?url";`);
+        }
+        else {
+            lines.push(`import ${a.var} from "${a.defaultPath}?url";`);
+        }
+    }
     // == adAssets 导出 ==
     lines.push("");
     lines.push("export const adAssets = {");
@@ -62,6 +74,7 @@ function generateAdConfig(input) {
     lines.push("export const adTheme = {");
     lines.push(`  bgGradient: ${JSON.stringify(input.theme.bgGradient || "linear-gradient(160deg, #fff9f2 0%, #ffeedd 100%)")},`);
     lines.push(`  ctaGradient: ${JSON.stringify(input.theme.ctaGradient || "linear-gradient(135deg, #ff5f1f 0%, #ffb300 100%)")},`);
+    lines.push(`  ctaGlowColor: ${JSON.stringify(input.theme.ctaGlowColor || "#ff5f1f")},`);
     lines.push(`  ctaText: ${JSON.stringify(input.theme.ctaText || "PLAY NOW")},`);
     lines.push(`  frameColor: ${JSON.stringify(input.theme.frameColor || "#edce9b")},`);
     lines.push(`  frameLineWidth: ${JSON.stringify(Number(input.theme.frameLineWidth) || 16)},`);

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
platform/server/dist/services/configGenerator.js.map


+ 7 - 2
platform/server/dist/services/storageService.d.ts

@@ -22,10 +22,15 @@ export declare function downloadFile(url: string): Promise<Buffer>;
 export declare function xorDecryptBuffer(encrypted: Buffer, key: string): Buffer;
 export declare function getCreativeAssetsDir(storageDir: string, creativeId: string): string;
 export declare function getBuildOutputDir(storageDir: string, creativeId: string, buildId: string): string;
-export declare function scanAssetFiles(assetsDir: string): {
+export interface ScannedAssets {
     config: boolean;
     page: boolean;
     map: boolean;
     special: string | null;
-};
+    logo: string | null;
+    logoTxt: string | null;
+    slogon: string | null;
+    coloringPages: string | null;
+}
+export declare function scanAssetFiles(assetsDir: string): ScannedAssets;
 //# sourceMappingURL=storageService.d.ts.map

+ 1 - 1
platform/server/dist/services/storageService.d.ts.map

@@ -1 +1 @@
-{"version":3,"file":"storageService.d.ts","sourceRoot":"","sources":["../../src/services/storageService.ts"],"names":[],"mappings":"AAKA,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAavF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAczD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAUvE;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGnF;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,MAAM,CAIR;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,GAChB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAgB1E"}
+{"version":3,"file":"storageService.d.ts","sourceRoot":"","sources":["../../src/services/storageService.ts"],"names":[],"mappings":"AAKA,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAavF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAczD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAUvE;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGnF;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,MAAM,CAIR;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAwB/D"}

+ 13 - 1
platform/server/dist/services/storageService.js

@@ -83,7 +83,11 @@ function getBuildOutputDir(storageDir, creativeId, buildId) {
     return dir;
 }
 function scanAssetFiles(assetsDir) {
-    const result = { config: false, page: false, map: false, special: null };
+    const result = {
+        config: false, page: false, map: false,
+        special: null, logo: null, logoTxt: null,
+        slogon: null, coloringPages: null,
+    };
     if (!fs_1.default.existsSync(assetsDir))
         return result;
     const files = fs_1.default.readdirSync(assetsDir);
@@ -97,6 +101,14 @@ function scanAssetFiles(assetsDir) {
             result.map = true;
         else if (lower.startsWith("special."))
             result.special = file;
+        else if (lower === "logo.png")
+            result.logo = file;
+        else if (lower === "logo-txt.png")
+            result.logoTxt = file;
+        else if (lower === "slogon.png")
+            result.slogon = file;
+        else if (lower === "coloring-pages.png")
+            result.coloringPages = file;
     }
     return result;
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
platform/server/dist/services/storageService.js.map


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است