修改经验问题和光栅卡铸造bug修改
This commit is contained in:
parent
d30eda151e
commit
cf8671b5bb
@ -1532,10 +1532,15 @@ func (ctrl *AssetController) BindAssetMaterials(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, exists := c.Get("user_id"); !exists {
|
||||
userIDVal, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, http.StatusUnauthorized, "未授权")
|
||||
return
|
||||
}
|
||||
uid, _ := userIDVal.(int64)
|
||||
starIDVal, _ := c.Get("star_id")
|
||||
sid, _ := starIDVal.(int64)
|
||||
_ = uid; _ = sid // suppress unused
|
||||
|
||||
var req dto.BindAssetMaterialsRequestDTO
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@ -1546,6 +1551,11 @@ func (ctrl *AssetController) BindAssetMaterials(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
|
||||
"user_id": strconv.FormatInt(uid, 10),
|
||||
"star_id": strconv.FormatInt(sid, 10),
|
||||
})
|
||||
|
||||
materials := make([]*pbAsset.AssetMaterialRelation, 0, len(req.Materials))
|
||||
for _, item := range req.Materials {
|
||||
m := &pbAsset.AssetMaterialRelation{
|
||||
|
||||
@ -213,7 +213,7 @@ type UploadMaterialRequestDTO struct {
|
||||
MimeType string `json:"mime_type" binding:"required"`
|
||||
Width *int `json:"width"`
|
||||
Height *int `json:"height"`
|
||||
Hash string `json:"hash" binding:"required"`
|
||||
Hash string `json:"hash"` // 可选,用于去重
|
||||
MaterialType string `json:"material_type"`
|
||||
}
|
||||
|
||||
|
||||
@ -60,3 +60,15 @@ type AssetMaterialRelation struct {
|
||||
}
|
||||
|
||||
func (AssetMaterialRelation) TableName() string { return "asset_material_relations" }
|
||||
|
||||
func (r *AssetMaterialRelation) BeforeCreate(tx *gorm.DB) error {
|
||||
now := time.Now().UnixMilli()
|
||||
r.CreatedAt = now
|
||||
r.UpdatedAt = now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AssetMaterialRelation) BeforeUpdate(tx *gorm.DB) error {
|
||||
r.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -232,14 +232,18 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
|
||||
var asset *models.Asset
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 3.0 取出阶段一订单,并校验状态/所有者
|
||||
logger.Logger.Info("[MintOrder] Step 3.0: 获取订单", zap.String("order_id", req.OrderId), zap.Int64("user_id", userID), zap.Int64("star_id", starID))
|
||||
existing, err := s.mintOrderRepo.GetByOrderIDAndUser(req.OrderId, userID, starID)
|
||||
if err != nil {
|
||||
logger.Logger.Error("[MintOrder] Step 3.0 失败: 获取订单错误", zap.String("order_id", req.OrderId), zap.Error(err))
|
||||
return fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
logger.Logger.Info("[MintOrder] Step 3.0 查询成功", zap.String("order_id", existing.OrderID), zap.String("status", existing.Status))
|
||||
if existing.Status != models.MintOrderStatusPending {
|
||||
return fmt.Errorf("订单状态为%s,不能继续铸造", existing.Status)
|
||||
}
|
||||
mintOrder = existing
|
||||
logger.Logger.Info("[MintOrder] Step 3.0 完成: 订单状态", zap.String("status", existing.Status))
|
||||
|
||||
// 若阶段二传了字段,则覆盖阶段一的存储值(允许再次编辑元数据)
|
||||
if req.MaterialUrl != "" {
|
||||
@ -276,34 +280,27 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
|
||||
|
||||
// 3.1 获取铸造消耗配置(阶梯计价)
|
||||
// 本次铸造是第 currentMintCount+1 次
|
||||
logger.Logger.Info("[MintOrder] Step 3.1: 获取铸造配置", zap.Int32("current_mint_count", currentMintCount))
|
||||
mintCost, err := s.GetMintCost(currentMintCount + 1)
|
||||
if err != nil {
|
||||
logger.Logger.Error("[MintOrder] Step 3.1 失败", zap.Error(err))
|
||||
return fmt.Errorf("获取铸造消耗配置失败: %w", err)
|
||||
}
|
||||
logger.Logger.Info("[MintOrder] Step 3.1 完成", zap.Int64("cost_crystal", mintCost.CostCrystal))
|
||||
|
||||
// 3.2 扣除水晶余额(调用 User Service RPC)
|
||||
logger.Logger.Info("[MintOrder] Step 3.2: 扣除水晶", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("delta", -mintCost.CostCrystal))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
newBalance, err := s.userClient.UpdateCrystalBalance(ctx, userID, starID, -mintCost.CostCrystal,
|
||||
"mint_cost", req.OrderId, fmt.Sprintf("铸造藏品 #%s", req.OrderId))
|
||||
if err != nil {
|
||||
logger.Logger.Error("Failed to deduct crystal balance",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Int64("star_id", starID),
|
||||
zap.Int64("cost", mintCost.CostCrystal),
|
||||
zap.Error(err),
|
||||
)
|
||||
logger.Logger.Error("[MintOrder] Step 3.2 失败: 扣除水晶错误", zap.Error(err))
|
||||
return fmt.Errorf("水晶余额不足或扣除失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Logger.Info("Crystal balance deducted with tiered cost",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Int64("star_id", starID),
|
||||
zap.Int64("cost", mintCost.CostCrystal),
|
||||
zap.Int32("mint_count", currentMintCount+1),
|
||||
zap.Int64("new_balance", newBalance),
|
||||
)
|
||||
logger.Logger.Info("[MintOrder] Step 3.2 完成", zap.Int64("new_balance", newBalance))
|
||||
|
||||
// 3.3 检查是否触发保底(概率触发)
|
||||
var boostBps int32 = 0
|
||||
@ -322,19 +319,19 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
|
||||
}
|
||||
|
||||
// 3.4 更新用户铸爱次数和收益提升
|
||||
logger.Logger.Info("[MintOrder] Step 3.4: 更新铸爱次数", zap.Int64("user_id", userID), zap.Int64("star_id", starID))
|
||||
if err := s.UpdateMintCountAndBoost(ctx, tx, userID, starID, boostBps); err != nil {
|
||||
logger.Logger.Error("Failed to update mint count and boost",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Int64("star_id", starID),
|
||||
zap.Error(err))
|
||||
// 不阻断主流程,只记录错误
|
||||
logger.Logger.Error("[MintOrder] Step 3.4 失败: 更新铸爱次数错误,将回滚事务", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err))
|
||||
return fmt.Errorf("failed to update mint count: %w", err) // 回滚事务,不继续
|
||||
}
|
||||
|
||||
// 3.5 创建资产记录(状态:Active,CoverURL 直接使用 MaterialURL)
|
||||
logger.Logger.Info("[MintOrder] Step 3.5: 创建资产")
|
||||
materialURLValue := getStringValue(mintOrder.MaterialURL)
|
||||
mintedAt := time.Now().UnixMilli()
|
||||
mockTxHash := fmt.Sprintf("0x%x", sha256.Sum256([]byte(fmt.Sprintf("%s-%d-%d-%d", mintOrder.OrderID, userID, starID, time.Now().UnixNano()))))
|
||||
mockBlockNumber := int64(time.Now().Unix()) // 使用当前时间戳作为模拟区块号
|
||||
logger.Logger.Info("[MintOrder] 创建资产参数", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.String("name", getStringValue(mintOrder.Name)), zap.String("material_url", materialURLValue))
|
||||
asset = &models.Asset{
|
||||
OwnerUID: userID,
|
||||
StarID: starID,
|
||||
@ -359,20 +356,15 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
|
||||
asset.Tags = models.StringArray(req.Tags)
|
||||
}
|
||||
|
||||
logger.Logger.Info("[MintOrder] 执行 tx.Create(asset)")
|
||||
if err := tx.Create(asset).Error; err != nil {
|
||||
logger.Logger.Error("Failed to create asset",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
logger.Logger.Error("[MintOrder] Step 3.5 失败: 创建资产错误", zap.String("material_url", materialURLValue), zap.Error(err))
|
||||
return fmt.Errorf("failed to create asset: %w", err)
|
||||
}
|
||||
|
||||
logger.Logger.Info("Asset created",
|
||||
zap.Int64("asset_id", asset.ID),
|
||||
zap.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
// 3.3 同步写入 asset_registry(普通藏品纳入星册体系)
|
||||
grade := int32(1) // 普通藏品初始等级为1
|
||||
registry := &models.AssetRegistry{
|
||||
AssetID: asset.ID,
|
||||
@ -817,7 +809,7 @@ func (s *mintService) GetUserMintCount(userID, starID int64) (int32, error) {
|
||||
// 在事务内调用,tx 为nil时会创建新事务
|
||||
func (s *mintService) UpdateMintCountAndBoost(ctx context.Context, tx *gorm.DB, userID, starID int64, boostBps int32) error {
|
||||
// 获取或创建用户铸爱累计记录
|
||||
record, isNew, err := s.userMintCountRepo.GetOrCreate(tx, userID, starID)
|
||||
record, _, err := s.userMintCountRepo.GetOrCreate(tx, userID, starID)
|
||||
if err != nil {
|
||||
logger.Logger.Error("Failed to get or create user mint count",
|
||||
zap.Int64("user_id", userID),
|
||||
@ -840,14 +832,10 @@ func (s *mintService) UpdateMintCountAndBoost(ctx context.Context, tx *gorm.DB,
|
||||
}
|
||||
record.UpdatedAt = time.Now().UnixMilli()
|
||||
|
||||
if isNew {
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return fmt.Errorf("创建用户铸爱累计记录失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.Save(record).Error; err != nil {
|
||||
return fmt.Errorf("更新用户铸爱累计记录失败: %w", err)
|
||||
}
|
||||
// 注意:GetOrCreate 已经插入了一条记录(isNew=true),所以这里直接 Save 更新即可
|
||||
// 不需要再 Create,否则会违反 user_id+star_id 唯一索引
|
||||
if err := tx.Save(record).Error; err != nil {
|
||||
return fmt.Errorf("更新用户铸爱累计记录失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Logger.Info("Updated mint count and boost",
|
||||
|
||||
@ -336,6 +336,42 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
|
||||
return &pb.OnExhibitionCompletedResponse{Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR}}, err
|
||||
}
|
||||
|
||||
// 增加用户累计上架时长(展位主人获得上架时长累计)
|
||||
// 计算实际上架时长(毫秒转小时)
|
||||
startTime := req.StartTime
|
||||
expireAt := req.ExpireAt
|
||||
actualHours := (expireAt - startTime) / 3600000
|
||||
if actualHours < 1 {
|
||||
actualHours = 1
|
||||
}
|
||||
|
||||
// sourceID 用于去重,避免重复累计
|
||||
sourceID := fmt.Sprintf("exhibition_%d", req.ExhibitionId)
|
||||
|
||||
newLevel, levelDelta, crystalReward, err := s.userRPCClient.AddExhibitionHours(
|
||||
ctx,
|
||||
req.SlotOwnerUid,
|
||||
req.OccupierStarId,
|
||||
actualHours,
|
||||
sourceID,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Logger.Error("OnExhibitionCompleted: AddExhibitionHours failed",
|
||||
zap.Int64("slot_owner_uid", req.SlotOwnerUid),
|
||||
zap.Int64("hours", actualHours),
|
||||
zap.Error(err))
|
||||
// 不返回错误,因为收益记录已创建
|
||||
} else if levelDelta > 0 {
|
||||
logger.Logger.Info("OnExhibitionCompleted: 展位主人累计上架时长触发升级",
|
||||
zap.Int64("slot_owner_uid", req.SlotOwnerUid),
|
||||
zap.Int64("exhibition_id", req.ExhibitionId),
|
||||
zap.Int64("hours", actualHours),
|
||||
zap.Int32("old_level", newLevel-levelDelta),
|
||||
zap.Int32("new_level", newLevel),
|
||||
zap.Int32("level_delta", levelDelta),
|
||||
zap.Int64("crystal_reward", crystalReward))
|
||||
}
|
||||
|
||||
logger.Logger.Info("OnExhibitionCompleted: success",
|
||||
zap.Int64("exhibition_id", req.ExhibitionId),
|
||||
zap.Int64("revenue_record_id", createdRecord.ID))
|
||||
|
||||
@ -753,6 +753,12 @@ const handleCraftMint = async () => {
|
||||
const bgImagePath = isCraftLenticular.value
|
||||
? lenticularLayers.value.find((l) => l.id === 'base')?.src || ''
|
||||
: undefined;
|
||||
console.log('[asset-detail] handleCraftMint', {
|
||||
isCraftLenticular: isCraftLenticular.value,
|
||||
bgImagePath: bgImagePath,
|
||||
lenticularLayers: lenticularLayers.value.map(l => ({ id: l.id, src: l.src ? l.src.substring(0, 30) : '' })),
|
||||
craftFormDataKeys: Object.keys(craftFormData.value || {})
|
||||
})
|
||||
craftMinting.value = true;
|
||||
uni.showLoading({ title: '铸造中…', mask: true });
|
||||
try {
|
||||
@ -781,7 +787,15 @@ onLoad((options) => {
|
||||
assetIdParam.value = options?.asset_id || '';
|
||||
orderIdParam.value = options?.order_id || '';
|
||||
fromParam.value = options?.from || '';
|
||||
studioKindParam.value = options?.studio_kind || '';
|
||||
loadCurrentUser();
|
||||
|
||||
// craft_confirm 模式:不需要调用 loadData,而是加载本地表单数据
|
||||
if (fromParam.value === 'craft_confirm') {
|
||||
console.log('[asset-detail] craft_confirm 模式,加载本地数据');
|
||||
craftConfirmMode.value = true;
|
||||
loadCraftConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
>
|
||||
<text class="button-text">重新生成</text>
|
||||
</view>
|
||||
<view class="action-button" @click="selectAsset" @tap="selectAsset">
|
||||
<view class="action-button" @tap="selectAsset">
|
||||
<text class="button-text">{{ primaryActionLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -717,8 +717,10 @@ const selectAsset = async () => {
|
||||
const formDataStr = uni.getStorageSync(CASTLOVE_FORM_KEY);
|
||||
const orderValue = formDataStr ? JSON.parse(formDataStr) : craftFormData.value || {};
|
||||
const selectedImage = getSelectedImageUrl();
|
||||
|
||||
console.log('1')
|
||||
if (isDetailAfterSelect(orderValue)) {
|
||||
console.log('1')
|
||||
|
||||
isUploading.value = true;
|
||||
uni.showLoading({ title: '准备中…', mask: true });
|
||||
try {
|
||||
|
||||
@ -558,6 +558,24 @@ export function getMintOrderDetailApi(orderId) {
|
||||
})
|
||||
}
|
||||
|
||||
// 上传素材(返回 material_id)
|
||||
export function uploadMaterialApi(data) {
|
||||
return request({
|
||||
url: '/api/v1/assets/materials/upload',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 绑定资产素材关联
|
||||
export function bindAssetMaterialsApi(assetId, materials) {
|
||||
return request({
|
||||
url: `/api/v1/assets/${assetId}/materials`,
|
||||
method: 'POST',
|
||||
data: { materials }
|
||||
})
|
||||
}
|
||||
|
||||
// 图生图
|
||||
export function imageGenerationApi(params) {
|
||||
return request({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js'
|
||||
import { getOssSignatureApi, createMintOrderApi, uploadMaterialApi, bindAssetMaterialsApi } from '@/utils/api.js'
|
||||
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js'
|
||||
import { buildCastloveFormSnapshot } from '@/utils/castloveMintForm.js'
|
||||
|
||||
@ -32,26 +32,96 @@ function uploadFileToOss(tempFilePath, ossData) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 铸爱确认页:上传选中图并创建铸造订单
|
||||
* @param {{ imagePath: string, bgImagePath?: string, formData: object }} opts
|
||||
* 上传文件到 OSS 并返回文件大小(用于注册素材)
|
||||
*/
|
||||
export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData }) {
|
||||
const path = String(imagePath || '').trim()
|
||||
if (!path) {
|
||||
throw new Error('缺少作品图片')
|
||||
}
|
||||
const signRes = await getOssSignatureApi('asset')
|
||||
if (!signRes || signRes.code !== 200 || !signRes.data) {
|
||||
throw new Error(signRes?.message || '获取签名失败')
|
||||
}
|
||||
const ossData = signRes.data
|
||||
const orderId = ossData.order_id || ''
|
||||
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,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let imageUrl = path
|
||||
if (!path.startsWith('http')) {
|
||||
/**
|
||||
* 计算文件 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 (path.startsWith('data:')) {
|
||||
const blob = await fetch(path).then((r) => r.blob())
|
||||
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)
|
||||
@ -64,18 +134,73 @@ export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData
|
||||
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 {
|
||||
imageUrl = await uploadFileToOss(path, ossData)
|
||||
console.log('[craftMintSubmit] 小程序/非H5上传')
|
||||
const uploadResult = await uploadFileToOssWithInfo(imagePath, ossData)
|
||||
imageUrl = uploadResult.url
|
||||
uploadFileSize = uploadResult.size || 0
|
||||
uploadHash = uploadResult.hash || ''
|
||||
}
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
imageUrl = await uploadFileToOss(path, ossData)
|
||||
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 || '',
|
||||
@ -85,23 +210,32 @@ export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData
|
||||
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)
|
||||
|
||||
let bgUrl = ''
|
||||
// 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 && !bgPath.startsWith('http')) {
|
||||
bgUrl = await uploadFileToOss(bgPath, ossData)
|
||||
} else if (bgPath) {
|
||||
bgUrl = bgPath
|
||||
if (bgPath) {
|
||||
console.log('[craftMintSubmit] 3. 上传背景图并注册')
|
||||
bgMaterialId = (await uploadImageAndRegisterMaterial(bgPath, ossData, originalFileName(bgPath), 'bg')).materialId
|
||||
}
|
||||
}
|
||||
|
||||
const materialUrl = isLenticular && bgUrl
|
||||
? JSON.stringify({ main: imageUrl, bg: bgUrl })
|
||||
: imageUrl
|
||||
// 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,
|
||||
@ -112,8 +246,10 @@ export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData
|
||||
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 || '创建订单失败')
|
||||
}
|
||||
@ -124,8 +260,34 @@ export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData
|
||||
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: imageUrl,
|
||||
image: mainResult.ossKey, // oss_key
|
||||
name: snap.name,
|
||||
description: snap.description || '',
|
||||
material_type: snap.material_type,
|
||||
@ -134,9 +296,26 @@ export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData
|
||||
asset_id: assetId,
|
||||
info: snap.info,
|
||||
event: snap.info,
|
||||
...(isLenticular && bgUrl ? { bg_image: bgUrl } : {}),
|
||||
...(bgMaterialId ? { bg_material_id: bgMaterialId } : {}),
|
||||
}
|
||||
uni.setStorageSync('temp_nft_data', JSON.stringify(nftData))
|
||||
uni.removeStorageSync('castlove_form_data')
|
||||
return { imageUrl, orderId }
|
||||
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'
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user