From c49ff72f1efd600134d8477416c726e0fc4ed6f6 Mon Sep 17 00:00:00 2001 From: obroccolio Date: Thu, 13 Aug 2026 01:39:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(wordcloud):=20=E8=B4=B4=E7=BA=B8=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=EF=BC=88=E5=86=B3=E7=AD=96#4=EF=BC=89+=20dis?= =?UTF-8?q?patchOrder/persistDesignMedia=20=E8=BE=85=E5=8A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/diy/index.tsx | 12 +++++------ src/utils/api/upload.ts | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/pages/diy/index.tsx b/src/pages/diy/index.tsx index d0fed59..9455ed3 100644 --- a/src/pages/diy/index.tsx +++ b/src/pages/diy/index.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react' import './index.scss' import { getProductById } from '../../utils/productConfig' import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../utils/store' +import { persistDesignMedia } from '../../utils/api' import { useThemeContext } from '../../context/ThemeContext' import { useSafeArea } from '../../hooks/useSafeArea' import { useStatusBar } from '../../hooks/useStatusBar' @@ -241,18 +242,15 @@ export default function DIYPage() { return base } - const handleComplete = () => { + const handleComplete = async () => { if (hasOverlap) { Taro.showToast({ title: '贴纸不能重叠', icon: 'none' }) return } if (designId) { - updateDesign(designId, { - designData: { - stickers, - category - } - }) + // 贴纸/底图持久化(决策#4,R4 负责):本地图 → COS 持久 URL,保证后端 WCD 打包可下载素材 + const data = await persistDesignMedia({ stickers, category, version: 1 }) + updateDesign(designId, { designData: data }) } setPreviewMode(false) Taro.navigateTo({ url: `/pages/checkout/index?designId=${designId}` }) diff --git a/src/utils/api/upload.ts b/src/utils/api/upload.ts index af59b59..4c58ee3 100644 --- a/src/utils/api/upload.ts +++ b/src/utils/api/upload.ts @@ -2,7 +2,10 @@ import Taro from '@tarojs/taro' import http, { BASE_URL, getToken } from '../request' import type { CosCredentials, + DesignDataV1, SketchResult, + StickerItem, + WordCloudDispatchResult, WordCloudJob, WordCloudStatus, } from '../../types' @@ -101,4 +104,49 @@ export function getWordCloudResult(jobId: string): Promise<{ imageUrl: string; s return http.get<{ imageUrl: string; svgUrl?: string }>(`/api/wordcloud/jobs/${jobId}/result`) } +/** 是否为设备本地临时路径(后端/wordcloud 无法消费,需先转 COS 持久 URL) */ +function isLocalPath(url?: string): boolean { + return !!url && /^(wxfile:|http:\/\/tmp\/|tmp\/)/i.test(url) +} + +/** 单张本地图 → COS 持久 URL;COS 未配置等失败时保留原路径,不阻断保存 */ +async function persistLocalImage(localPath: string): Promise { + try { + const key = `uploads/sticker/${Date.now()}_${Math.floor(Math.random() * 1e6)}.jpg` + const cred = await getUploadCredentials(key) + return await uploadToCos(localPath, cred) + } catch { + return localPath + } +} + +/** + * 设计媒体持久化(契约 design-data-contract-v1.md 决策#4:贴纸持久化由 R4 完成)。 + * 保存/下单前调用,把 designData 里的本地贴纸/底图上传为 COS 持久 URL 写回 src, + * 保证 WCD 打包时后端能下载到素材字节。 + */ +export async function persistDesignMedia(designData: DesignDataV1): Promise { + if (!designData) return designData + const next: DesignDataV1 = { ...designData } + if (designData.background && isLocalPath(designData.background.src)) { + next.background = { + ...designData.background, + src: await persistLocalImage(designData.background.src), + } + } + if (designData.stickers && designData.stickers.length) { + const list: StickerItem[] = [] + for (const s of designData.stickers) { + list.push(isLocalPath(s.src) ? { ...s, src: await persistLocalImage(s.src) } : s) + } + next.stickers = list + } + return next +} + +/** 下单后 WCD 派单(幂等触发;WORDCLOUD 未配置返回 not_configured,见 api-contract §8) */ +export function dispatchOrder(orderId: string): Promise { + return http.post(`/api/orders/${orderId}/dispatch`) +} + export type { WordCloudStatus }