import { getOssSignatureApi, createMintOrderApi, uploadMaterialApi, bindAssetMaterialsApi } from '@/utils/api.js' import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js' import { buildCastloveFormSnapshot } from '@/utils/castloveMintForm.js' function uploadFileToOss(tempFilePath, ossData) { return new Promise((resolve, reject) => { const fileName = `${Date.now()}.jpg` uni.uploadFile({ url: ossData.host, filePath: tempFilePath, name: 'file', formData: { key: ossData.dir + fileName, policy: ossData.policy, success_action_status: '200', 'x-oss-credential': ossData.x_oss_credential, 'x-oss-date': ossData.x_oss_date, 'x-oss-security-token': ossData.security_token, 'x-oss-signature': ossData.signature, 'x-oss-signature-version': ossData.x_oss_signature_version, }, success: (res) => { if (res.statusCode === 200 || res.statusCode === 204) { resolve(`${ossData.host}/${ossData.dir}${fileName}`) } else { reject(new Error(`上传失败 ${res.statusCode}`)) } }, fail: reject, }) }) } /** * 上传文件到 OSS 并返回文件大小(用于注册素材) */ function uploadFileToOssWithInfo(tempFilePath, ossData) { return new Promise((resolve, reject) => { const fileName = `${Date.now()}.jpg` uni.uploadFile({ url: ossData.host, filePath: tempFilePath, name: 'file', formData: { key: ossData.dir + fileName, policy: ossData.policy, success_action_status: '200', 'x-oss-credential': ossData.x_oss_credential, 'x-oss-date': ossData.x_oss_date, 'x-oss-security-token': ossData.security_token, 'x-oss-signature': ossData.signature, 'x-oss-signature-version': ossData.x_oss_signature_version, }, success: async (res) => { if (res.statusCode === 200 || res.statusCode === 204) { const url = `${ossData.host}/${ossData.dir}${fileName}` // 获取文件大小 let size = 0 try { const fileInfo = await new Promise((res2, rej) => { uni.getFileInfo({ filePath: tempFilePath, success: (info) => res2(info.size), fail: rej }) }) size = fileInfo || 0 } catch (e) { console.warn('[craftMintSubmit] getFileInfo failed', e) } resolve({ url, size, hash: '' }) } else { reject(new Error(`上传失败 ${res.statusCode}`)) } }, fail: reject, }) }) } /** * 计算文件 SHA256 哈希(使用 Web Crypto API) */ async function computeFileHash(file) { if (typeof crypto !== 'undefined' && crypto.subtle) { const buffer = await file.arrayBuffer() const hashBuffer = await crypto.subtle.digest('SHA-256', buffer) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') } // Fallback: 返回空字符串,后端会重新计算 return '' } /** * 获取文件 MIME 类型 */ function getFileMimeType(filePath) { const ext = filePath?.toLowerCase().split('.').pop() if (ext === 'png') return 'image/png' return 'image/jpeg' } /** * 上传图片到 OSS 并注册为素材 * @param {string} imagePath - 图片路径或 base64 * @param {object} ossData - OSS 签名数据 * @param {string} originalName - 原始文件名 * @param {string} materialType - 素材类型 (main/bg/sticker/patch 等) * @returns {Promise<{ ossKey: string, materialId: number }>} */ async function uploadImageAndRegisterMaterial(imagePath, ossData, originalName, materialType) { console.log('[craftMintSubmit] uploadImageAndRegisterMaterial', { materialType, imagePath: String(imagePath || '').substring(0, 50) }) // 1. 上传图片到 OSS let imageUrl = imagePath let uploadFileSize = 0 let uploadHash = '' if (!imagePath.startsWith('http')) { // #ifdef H5 if (imagePath.startsWith('data:')) { console.log('[craftMintSubmit] H5 base64 upload') const blob = await fetch(imagePath).then((r) => r.blob()) uploadFileSize = blob.size uploadHash = await computeFileHash(blob) const fd = new FormData() const fileName = `${Date.now()}.jpg` fd.append('key', ossData.dir + fileName) fd.append('policy', ossData.policy) fd.append('success_action_status', '200') fd.append('x-oss-credential', ossData.x_oss_credential) fd.append('x-oss-date', ossData.x_oss_date) fd.append('x-oss-security-token', ossData.security_token) fd.append('x-oss-signature', ossData.signature) fd.append('x-oss-signature-version', ossData.x_oss_signature_version) fd.append('file', blob, fileName) const res = await fetch(resolveH5OssPostUrl(ossData.host), { method: 'POST', body: fd }) console.log('[craftMintSubmit] OSS upload res:', res.status, res.ok) if (!res.ok && res.status !== 204) { throw new Error('上传失败') } imageUrl = `${ossData.host}/${ossData.dir}${fileName}` } else { console.log('[craftMintSubmit] 小程序/非H5上传') const uploadResult = await uploadFileToOssWithInfo(imagePath, ossData) imageUrl = uploadResult.url uploadFileSize = uploadResult.size || 0 uploadHash = uploadResult.hash || '' } // #endif // #ifndef H5 const uploadResult = await uploadFileToOssWithInfo(imagePath, ossData) imageUrl = uploadResult.url uploadFileSize = uploadResult.size || 0 uploadHash = uploadResult.hash || '' // #endif } console.log('[craftMintSubmit] imageUrl:', imageUrl, 'fileSize:', uploadFileSize, 'hash:', uploadHash.substring(0, 20)) // 2. 构建 oss_key(去掉 host 前缀) const ossKey = imageUrl.replace(/^https?:\/\/[^/]+\/(.+)$/, '$1') // 3. 获取 MIME 类型 const mimeType = getFileMimeType(imagePath) // 4. 注册素材到后端 const materialRes = await uploadMaterialApi({ oss_key: ossKey, original_name: originalName || `${materialType}.jpg`, file_size: uploadFileSize, mime_type: mimeType, hash: uploadHash, material_type: materialType, }) if (!materialRes || materialRes.code !== 200 || !materialRes.data) { throw new Error(materialRes?.message || '素材注册失败') } const materialId = materialRes.data.material_id return { ossKey, materialId } } /** * 铸爱确认页:上传选中图并创建铸造订单(支持多素材) * @param {{ imagePath: string, bgImagePath?: string, formData: object }} opts */ export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData }) { console.log('[craftMintSubmit] start', { imagePath: String(imagePath || '').trim().substring(0, 50), bgImagePath: !!bgImagePath, formDataKeys: Object.keys(formData || {}) }) const path = String(imagePath || '').trim() if (!path) { throw new Error('缺少作品图片') } // 1. 获取 OSS 签名 console.log('[craftMintSubmit] 1. 获取 OSS 签名') const signRes = await getOssSignatureApi('asset') console.log('[craftMintSubmit] OSS 签名响应:', signRes) if (!signRes || signRes.code !== 200 || !signRes.data) { throw new Error(signRes?.message || '获取签名失败') } const ossData = signRes.data const orderId = ossData.order_id || '' const snap = buildCastloveFormSnapshot({ nftInfo: formData.info || formData.nftInfo || '', materialTypes: formData.materialTypes || ['粉丝自制'], materialTypeIndex: formData.materialTypeIndex ?? 0, pageName: formData.craft_name || formData.typeName || '', uploadedImage: path, uploadedImageBase64: formData.imageBase64 || '', }) console.log('[craftMintSubmit] snap:', { name: snap.name, tags: snap.tags, isLenticular: snap.tags?.includes('craft:lenticular') }) const isLenticular = Array.isArray(snap.tags) && snap.tags.includes('craft:lenticular') console.log('[craftMintSubmit] isLenticular:', isLenticular) // 2. 上传主图并注册为主素材 console.log('[craftMintSubmit] 2. 上传主图并注册') const mainResult = await uploadImageAndRegisterMaterial(path, ossData, originalFileName(path), 'main') console.log('[craftMintSubmit] mainResult:', mainResult) // 3. 如果是光栅卡,上传背景图并注册为 bg 素材 let bgMaterialId = null if (isLenticular && bgImagePath) { const bgPath = String(bgImagePath || '').trim() if (bgPath) { console.log('[craftMintSubmit] 3. 上传背景图并注册') bgMaterialId = (await uploadImageAndRegisterMaterial(bgPath, ossData, originalFileName(bgPath), 'bg')).materialId } } // 4. 构建 material_url(使用主素材 oss_key 作为封面) // 后端铸造时会自动使用此 URL 生成封面图 const materialUrl = mainResult.ossKey // 5. 创建铸造订单(后端自动扣除水晶消耗) console.log('[craftMintSubmit] 5. 创建铸造订单', { orderId, materialUrl }) const orderData = { order_id: orderId, name: snap.name, material_url: materialUrl, description: snap.description || '', grade: snap.grade ?? 0, tags: Array.isArray(snap.tags) ? snap.tags : [], material_type: snap.material_type, info: snap.info || '', } console.log('[craftMintSubmit] orderData:', orderData) const response = await createMintOrderApi(orderData) console.log('[craftMintSubmit] createMintOrderApi 响应:', response) if (!response || response.code !== 200) { throw new Error(response?.message || '创建订单失败') } const assetId = response.data?.asset?.asset_id || response.data?.asset_id || response.data?.assetId || '' if (!assetId) { throw new Error('创建订单成功但未返回资产 ID') } console.log('[craftMintSubmit] assetId:', assetId) // 6. 绑定多素材到资产 const materialsToBind = [ { material_id: mainResult.materialId, material_type: 'main', layer_order: 0 } ] if (bgMaterialId) { materialsToBind.push({ material_id: bgMaterialId, material_type: 'bg', layer_order: 1 }) } console.log('[craftMintSubmit] 6. 绑定多素材:', materialsToBind) try { const bindRes = await bindAssetMaterialsApi(assetId, materialsToBind) console.log('[craftMintSubmit] bindRes:', bindRes) if (!bindRes || bindRes.code !== 200) { console.warn('[craftMintSubmit] bind materials failed, but mint order created:', bindRes?.message) } } catch (e) { console.warn('[craftMintSubmit] bind materials error:', e) // 不阻断铸造流程,资产已创建成功 } // 7. 构建 NFT 数据并跳转 const nftData = { image: mainResult.ossKey, // oss_key name: snap.name, description: snap.description || '', material_type: snap.material_type, tags: snap.tags, order_id: orderId, asset_id: assetId, info: snap.info, event: snap.info, ...(bgMaterialId ? { bg_material_id: bgMaterialId } : {}), } uni.setStorageSync('temp_nft_data', JSON.stringify(nftData)) uni.removeStorageSync('castlove_form_data') console.log('[craftMintSubmit] 完成') return { imageUrl: mainResult.ossKey, orderId } } /** * 从文件路径提取原始文件名 */ function originalFileName(filePath) { if (!filePath) return '' try { if (filePath.startsWith('data:')) { return 'image.jpg' } return filePath.split('/').pop() || 'image.jpg' } catch { return 'image.jpg' } }