topfans/frontend/utils/craftMintSubmit.js
2026-05-16 04:32:43 +08:00

321 lines
11 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 } 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'
}
}