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