topfans/frontend/utils/craftMintSubmit.js
2026-06-03 22:19:22 +08:00

745 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getOssSignatureApi, createMintOrderApi, uploadMaterialApi, bindAssetMaterialsApi } from '@/utils/api.js'
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js'
import { buildCastloveFormSnapshot, CRAFT_TAG_LASER, CRAFT_TAG_LENTICULAR } from '@/utils/castloveMintForm.js'
function uploadFileToOss(tempFilePath, ossData) {
return new Promise((resolve, reject) => {
const fileName = `${Date.now()}.jpg`
const 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-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version,
}
// STS 模式下才携带 security_token直连 AK/SK 模式下会多余导致签名不匹配 403
if (ossData.security_token) {
formData['x-oss-security-token'] = ossData.security_token
}
uni.uploadFile({
url: ossData.host,
filePath: tempFilePath,
name: 'file',
formData,
success: (res) => {
if (res.statusCode === 200 || res.statusCode === 204) {
resolve(`${ossData.host}/${ossData.dir}${fileName}`)
} else {
reject(new Error(`上传失败 ${res.statusCode}`))
}
},
fail: reject,
})
})
}
/**
* 上传文件到 OSS 并返回文件大小和 hash用于注册素材
*/
function uploadFileToOssWithInfo(tempFilePath, ossData) {
return new Promise((resolve, reject) => {
console.log('[craftMintSubmit] uploadFileToOssWithInfo tempFilePath:', tempFilePath)
const fileName = `${Date.now()}.jpg`
const 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-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version,
}
if (ossData.security_token) {
formData['x-oss-security-token'] = ossData.security_token
}
uni.uploadFile({
url: resolveH5OssPostUrl(ossData.host),
filePath: tempFilePath,
name: 'file',
formData,
success: async (res) => {
console.log('[craftMintSubmit] uploadFileToOssWithInfo statusCode:', res.statusCode)
if (res.statusCode === 200 || res.statusCode === 204) {
const url = `${ossData.host}/${ossData.dir}${fileName}`
// 获取文件大小 — H5 下 uni.getFileInfo 对 blob URL 返回 0改用 fetch
let size = 0
try {
if (typeof window !== 'undefined' && String(tempFilePath).startsWith('blob:')) {
const resp = await fetch(tempFilePath)
const blob = await resp.blob()
size = blob.size
console.log('[craftMintSubmit] H5 blob size:', size, 'from:', String(tempFilePath).substring(0, 40))
} else {
const fileInfo = await new Promise((res2, rej) => {
uni.getFileInfo({
filePath: tempFilePath,
success: (info) => res2(info.size),
fail: rej
})
})
size = fileInfo || 0
}
// 兜底:如果 size 仍为 0用合理默认值避免后端校验失败
if (size <= 0) {
size = 500000
console.warn('[craftMintSubmit] size=0, using fallback 500KB')
}
} catch (e) {
console.warn('[craftMintSubmit] getFileInfo failed', e)
size = 500000
}
// 计算文件 hash
const hash = await computeHashFromPath(tempFilePath)
console.log('[craftMintSubmit] uploadFileToOssWithInfo hash:', hash ? hash.substring(0, 10) + '...' : 'empty')
resolve({ url, size, hash })
} else {
reject(new Error(`上传失败 ${res.statusCode}`))
}
},
fail: (err) => {
console.error('[craftMintSubmit] uploadFileToOssWithInfo fail:', err)
reject(err)
},
})
})
}
/**
* 纯 JavaScript SHA256 实现(用于 App 等不支持 crypto.subtle 的环境)
*/
function sha256Sync(str) {
// SHA256 实现
const rotateRight = (n, s) => (n >>> s) | ((n << (32 - s)) >>> 0)
const choice = (x, y, z) => (x & y) ^ (~x & z)
const majority = (x, y, z) => (x & y) ^ (x & z) ^ (y & z)
const maj = (x, y, z) => (x & y) ^ (x & z) ^ (y & z)
const ch = (x, y, z) => (x & y) ^ (~x & z)
const K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0x49b40821, 0xfef9e05c, 0x4fd9c4a7, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814,
0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
]
// 转换为 bytes
const msgBytes = []
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i)
if (code < 0x80) {
msgBytes.push(code)
} else if (code < 0x800) {
msgBytes.push(0xc0 | (code >> 6))
msgBytes.push(0x80 | (code & 0x3f))
} else if (code < 0xd800 || code >= 0xe000) {
msgBytes.push(0xe0 | (code >> 12))
msgBytes.push(0x80 | ((code >> 6) & 0x3f))
msgBytes.push(0x80 | (code & 0x3f))
} else {
const cp = 0x10000 + ((code - 0xd800) << 10) | (str.charCodeAt(++i) - 0xdc00)
msgBytes.push(0xf0 | (cp >> 18))
msgBytes.push(0x80 | ((cp >> 12) & 0x3f))
msgBytes.push(0x80 | ((cp >> 6) & 0x3f))
msgBytes.push(0x80 | (cp & 0x3f))
}
}
// Padding
const msgLen = msgBytes.length
const bitLen = msgLen * 8
msgBytes.push(0x80)
while ((msgBytes.length % 64) !== 56) msgBytes.push(0)
for (let i = 7; i >= 0; i--) msgBytes.push((bitLen >>> (i * 8)) & 0xff)
// Initialize hash values
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a
let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19
// Process blocks
for (let chunk = 0; chunk < msgBytes.length / 64; chunk++) {
const w = new Array(64)
for (let j = 0; j < 16; j++) {
w[j] = (msgBytes[chunk * 64 + j * 4] << 24) | (msgBytes[chunk * 64 + j * 4 + 1] << 16) |
(msgBytes[chunk * 64 + j * 4 + 2] << 8) | msgBytes[chunk * 64 + j * 4 + 3]
}
for (let j = 16; j < 64; j++) {
const s0 = rotateRight(w[j - 15], 7) ^ rotateRight(w[j - 15], 18) ^ (w[j - 15] >>> 3)
const s1 = rotateRight(w[j - 2], 17) ^ rotateRight(w[j - 2], 19) ^ (w[j - 2] >>> 10)
w[j] = (w[j - 16] + s0 + w[j - 7] + s1) >>> 0
}
let [ah0, ah1, ah2, ah3, ah4, ah5, ah6, ah7] = [h0, h1, h2, h3, h4, h5, h6, h7]
for (let j = 0; j < 64; j++) {
const S1 = rotateRight(ah6, 6) ^ rotateRight(ah6, 11) ^ rotateRight(ah6, 25)
const ch2 = ch(ah6, ah5, ah4)
const temp1 = (ah7 + S1 + ch2 + K[j] + w[j]) >>> 0
const S0 = rotateRight(ah0, 2) ^ rotateRight(ah0, 13) ^ rotateRight(ah0, 22)
const maj2 = maj(ah0, ah1, ah2)
const temp2 = (S0 + maj2) >>> 0
ah7 = (ah6 + temp1) >>> 0
ah6 = (ah5 + temp1) >>> 0
ah5 = (ah4 + temp1) >>> 0
ah4 = (ah3 + temp1) >>> 0
ah3 = (ah2 + temp1) >>> 0
ah2 = (ah1 + temp1) >>> 0
ah1 = (ah0 + temp1) >>> 0
ah0 = (temp1 + temp2) >>> 0
}
h0 = (h0 + ah0) >>> 0; h1 = (h1 + ah1) >>> 0; h2 = (h2 + ah2) >>> 0; h3 = (h3 + ah3) >>> 0
h4 = (h4 + ah4) >>> 0; h5 = (h5 + ah5) >>> 0; h6 = (h6 + ah6) >>> 0; h7 = (h7 + ah7) >>> 0
}
const toHex = (n) => n.toString(16).padStart(8, '0')
return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7)
}
/**
* 计算文件 SHA256 哈希(使用 Web Crypto API
*/
async function computeFileHash(file) {
console.log('[craftMintSubmit] computeFileHash start, file type:', typeof file, file instanceof Blob ? `Blob(size=${file.size})` : '')
try {
if (typeof crypto !== 'undefined' && crypto.subtle) {
const buffer = await file.arrayBuffer()
console.log('[craftMintSubmit] computeFileHash arrayBuffer length:', buffer.byteLength)
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
console.log('[craftMintSubmit] computeFileHash result:', hash.substring(0, 20) + '...')
return hash
} else {
console.warn('[craftMintSubmit] computeFileHash: crypto.subtle not available')
}
} catch (e) {
console.error('[craftMintSubmit] computeFileHash failed:', e)
}
// Fallback: 返回空字符串,后端会重新计算
return ''
}
/**
* 从文件路径读取内容并计算 hash支持各端
*/
async function computeHashFromPath(filePath) {
console.log('[craftMintSubmit] computeHashFromPath start, path:', filePath ? filePath.substring(0, 80) : 'empty')
try {
// 处理 data:URL 格式
if (filePath && filePath.startsWith('data:')) {
console.log('[craftMintSubmit] computeHashFromPath data:URL format')
try {
const base64Data = filePath.split(',')[1] || ''
const binaryStr = atob(base64Data)
// 优先用 crypto.subtle失败则用纯JS
if (typeof crypto !== 'undefined' && crypto.subtle) {
const bytes = new Uint8Array(binaryStr.length)
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i)
}
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
console.log('[craftMintSubmit] computeHashFromPath data:URL hash:', hash.substring(0, 20) + '...')
return hash
} else {
// Fallback 纯JS SHA256
const hash = sha256Sync(binaryStr)
console.log('[craftMintSubmit] computeHashFromPath data:URL sha256Sync hash:', hash.substring(0, 20) + '...')
return hash
}
} catch (e) {
console.error('[craftMintSubmit] data:URL hash failed:', e)
}
return ''
}
let content = ''
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
console.log('[craftMintSubmit] computeHashFromPath in miniprogram')
try {
const fs = uni.getFileSystemManager()
content = await new Promise((resolve, reject) => {
fs.readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
console.log('[craftMintSubmit] fs.readFile success, data length:', res.data?.length)
resolve(res.data)
},
fail: (err) => {
console.error('[craftMintSubmit] fs.readFile fail:', err)
reject(err)
}
})
})
} catch (e) {
console.error('[craftMintSubmit] 小程序读取文件失败:', e)
}
// #endif
// #ifdef APP-PLUS
if (!content) {
console.log('[craftMintSubmit] computeHashFromPath in app-plus')
try {
content = await new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.file((file) => {
const reader = new plus.io.FileReader()
reader.onloadend = (e) => {
console.log('[craftMintSubmit] FileReader onloadend, result length:', e.target.result?.length)
resolve(e.target.result.split(',')[1] || '')
}
reader.onerror = (e) => {
console.error('[craftMintSubmit] FileReader error:', e)
reject(e)
}
reader.readAsDataURL(file)
}, (err) => {
console.error('[craftMintSubmit] getFile error:', err)
reject(err)
})
}, (err) => {
console.error('[craftMintSubmit] resolveLocalFileSystemURL error:', err)
reject(err)
})
})
} catch (e) {
console.error('[craftMintSubmit] App读取文件失败:', e)
}
}
// #endif
console.log('[craftMintSubmit] computeHashFromPath content length:', content?.length || 0)
console.log('[craftMintSubmit] computeHashFromPath crypto:', typeof crypto, crypto ? 'exists' : 'null', ', subtle:', crypto?.subtle ? 'exists' : 'null')
if (content && typeof crypto !== 'undefined' && crypto && crypto.subtle) {
try {
// 将 base64 转换为 ArrayBuffer 并计算 hash
const binary = atob(content)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
console.log('[craftMintSubmit] computeHashFromPath hash:', hash.substring(0, 20) + '...')
return hash
} catch (e) {
console.error('[craftMintSubmit] hash计算失败:', e)
}
} else {
// Fallback: 使用纯JS SHA256 实现 (App 环境 crypto.subtle 可能不可用)
try {
console.log('[craftMintSubmit] computeHashFromPath 使用纯JS SHA256 fallback, content length:', content.length)
// content 是 base64 字符串,需要先解码为 binary string
const binaryStr = atob(content)
const hash = sha256Sync(binaryStr)
console.log('[craftMintSubmit] computeHashFromPath sha256Sync hash:', hash.substring(0, 20) + '...')
return hash
} catch (e) {
console.error('[craftMintSubmit] SHA256 fallback failed:', e)
}
}
} catch (e) {
console.error('[craftMintSubmit] computeHashFromPath failed:', e)
}
console.log('[craftMintSubmit] computeHashFromPath return empty')
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, ossKeyHint = '') {
console.log('[craftMintSubmit] uploadImageAndRegisterMaterial', { materialType, imagePath: String(imagePath || '').substring(0, 50) })
// 1. 上传图片到 OSS如果不在 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 base64Data = imagePath.split(',')[1] || ''
const binaryStr = atob(base64Data)
const bytes = new Uint8Array(binaryStr.length)
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i)
}
const blob = new Blob([bytes], { type: 'image/jpeg' })
uploadFileSize = blob.size
uploadHash = await computeFileHash(blob)
console.log('[craftMintSubmit] H5 base64 hash:', uploadHash.substring(0, 20) + '...')
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)
if (ossData.security_token) 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 if (imagePath.startsWith('blob:')) {
console.log('[craftMintSubmit] H5 blob upload, fetching blob...')
const resp = await fetch(imagePath)
const blob = await resp.blob()
uploadFileSize = blob.size
uploadHash = await computeFileHash(blob)
console.log('[craftMintSubmit] H5 blob size:', uploadFileSize, 'hash:', uploadHash.substring(0, 20) + '...')
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)
if (ossData.security_token) 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 blob 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
} else {
if (ossKeyHint) {
// oss_key 由生成阶段携带,直接使用,跳过费时的 HEAD 请求
uploadFileSize = 500000
console.log('[craftMintSubmit] Using provided oss_key:', ossKeyHint)
} else {
// 图片已在 OSS 上:通过 HEAD 请求获取文件大小
try {
const headRes = await fetch(imagePath, { method: 'HEAD' })
const cl = headRes.headers.get('content-length')
uploadFileSize = cl ? parseInt(cl, 10) : 500000
console.log('[craftMintSubmit] OSS existing image size via HEAD:', uploadFileSize)
} catch (e) {
console.warn('[craftMintSubmit] HEAD failed, using fallback size:', e)
uploadFileSize = 500000
}
}
}
console.log('[craftMintSubmit] imageUrl:', imageUrl, 'fileSize:', uploadFileSize, 'hash:', uploadHash.substring(0, 20))
// 2. 构建 oss_key优先使用传入的 oss_key否则从 URL 提取(去掉 host 和查询参数)
const ossKey = ossKeyHint || imageUrl.replace(/^https?:\/\/[^/]+\/(.+?)(\?.*)?$/, '$1')
// 3. 获取 MIME 类型
const mimeType = getFileMimeType(imagePath)
// 4. 注册素材到后端
console.log('[craftMintSubmit] uploadMaterialApi ossKey:', ossKey, 'mimeType:', mimeType, 'fileSize:', uploadFileSize)
const materialRes = await uploadMaterialApi({
oss_key: ossKey,
original_name: originalName || `${materialType}.jpg`,
file_size: uploadFileSize,
mime_type: mimeType,
hash: uploadHash,
material_type: materialType,
})
console.log('[craftMintSubmit] uploadMaterialApi response:', materialRes)
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, laserBackdropPath?: string, formData: object }} opts
*/
export async function submitCraftMintFromPath({ imagePath, bgImagePath, laserBackdropPath, formData, laserPresetIndex, ossKey, instanceNo }) {
console.log('[craftMintSubmit] start', {
imagePath: String(imagePath || '').trim().substring(0, 50),
bgImagePath: !!bgImagePath,
laserBackdropPath: !!laserBackdropPath,
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 })
const isLenticular = Array.isArray(snap.tags) && snap.tags.includes(CRAFT_TAG_LENTICULAR)
const isLaser = Array.isArray(snap.tags) && snap.tags.includes(CRAFT_TAG_LASER)
console.log('[craftMintSubmit] isLenticular:', isLenticular, 'isLaser:', isLaser)
let mainResult
let bgMaterialId = null
let backdropMaterialId = null
let sourceMaterialId = null
let cutoutMaterialId = null
if (isLaser) {
// 镭射多素材:铸造时上传 OSS五图生成阶段仅存本地临时文件
// layer_order: backdrop(0) / source(1) / cutout(2 可选) / main(3 选中合成图)
const compositePath = path
const sourcePath = String(formData?.image || formData?.uploadedImage || '').trim()
const backdropPath = String(
laserBackdropPath || formData?.laser_backdrop_path || ''
).trim()
const cutoutPath = String(formData?.cutoutImage || formData?.cutout_image || '').trim()
// backdropPath 为空字符串或 "undefined" 时不传
if (backdropPath && backdropPath !== 'undefined') {
console.log('[craftMintSubmit] 镭射:上传底纹 backdrop')
backdropMaterialId = (
await uploadImageAndRegisterMaterial(
backdropPath,
ossData,
originalFileName(backdropPath),
'backdrop'
)
).materialId
}
if (sourcePath) {
console.log('[craftMintSubmit] 镭射:上传原图 source')
sourceMaterialId = (
await uploadImageAndRegisterMaterial(
sourcePath,
ossData,
originalFileName(sourcePath),
'source'
)
).materialId
}
if (cutoutPath) {
console.log('[craftMintSubmit] 镭射:上传抠图 cutout')
cutoutMaterialId = (
await uploadImageAndRegisterMaterial(
cutoutPath,
ossData,
originalFileName(cutoutPath),
'cutout'
)
).materialId
}
console.log('[craftMintSubmit] 镭射:上传选中合成图 main, ossKey=', ossKey)
mainResult = await uploadImageAndRegisterMaterial(
compositePath,
ossData,
originalFileName(compositePath),
'main',
ossKey || ''
)
} else {
// 2. 上传主图并注册为主素材
console.log('[craftMintSubmit] 2. 上传主图并注册')
mainResult = await uploadImageAndRegisterMaterial(path, ossData, originalFileName(path), 'main')
console.log('[craftMintSubmit] mainResult:', mainResult)
// 3. 光栅卡:上传背景图并注册为 bg 素材
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 || '',
}
if (instanceNo) orderData.instance_no = instanceNo
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 = []
if (isLaser) {
if (backdropMaterialId) {
materialsToBind.push({
material_id: backdropMaterialId,
material_type: 'backdrop',
layer_order: 0,
})
}
if (sourceMaterialId) {
materialsToBind.push({
material_id: sourceMaterialId,
material_type: 'source',
layer_order: 1,
})
}
if (cutoutMaterialId) {
materialsToBind.push({
material_id: cutoutMaterialId,
material_type: 'cutout',
layer_order: 2,
})
}
materialsToBind.push({
material_id: mainResult.materialId,
material_type: 'main',
layer_order: 3,
})
} else {
materialsToBind.push({
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: `${ossData.host}/${mainResult.ossKey}`, // 完整OSS URL
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,
}
// 如果是光栅卡,存储背景图路径供 success 页面展示
if (isLenticular && bgImagePath) {
nftData.bg_image = bgImagePath
nftData.is_lenticular = true
}
if (isLaser) {
nftData.is_laser = true
if (typeof laserPresetIndex === 'number' && laserPresetIndex >= 0) {
nftData.laser_preset_index = laserPresetIndex
}
const cutoutPath = String(formData?.cutoutImage || formData?.cutout_image || '').trim()
if (cutoutPath) {
nftData.laser_cutout_path = cutoutPath
}
}
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'
}
}