11 KiB
光栅卡铸造后写入 asset_registry 方案
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 确保光栅卡铸造完成后:1) 两张图片(main 主图 + bg 背景图)都注册到 materials 表;2) asset_registry 有记录;3) asset_material_relations 正确绑定两组图片
Architecture:
- 前端在
lenticular-result.vue的selectAsset中调用submitCraftMintFromPath完成上传和铸造 submitCraftMintFromPath已经支持:上传主图(main) + 背景图(bg)、注册素材、创建订单、绑定多素材- 问题:
lenticular-result.vue引用了未定义的isCraftLenticular和craftCoverUrl,导致铸造流程断裂
Tech Stack: Vue3 + uni-app 前端,Go 后端,Supabase/PostgreSQL
数据库设计解析
核心表关系
asset_registry ← 资产索引表(用户有哪些藏品)
│ asset_id
▼
assets ← 资产表(名称、封面、描述)
│ id
▼
asset_material_relations ← 资产-素材关联表(同一资产有哪些图片)
│ asset_id, material_id
▼
materials ← 素材表(OSS key、MIME、尺寸)
asset_registry 表结构(不存图片)
CREATE TABLE public.asset_registry (
id bigint,
asset_id bigint, -- 关联 assets.ID
asset_type varchar(20), -- 'regular'|'collection'|'activity'
owner_uid bigint,
star_id bigint,
grade integer,
status integer, -- 0=待处理, 1=已激活
like_count integer,
display_status integer,
created_at bigint,
updated_at bigint
);
注意: asset_registry 只存储资产索引和元数据,不存储任何图片 URL。
图片通过 asset_material_relations 关联
光栅卡有两张图片(main 主图 + bg 背景图),通过 asset_material_relations 表实现多对一关联:
-- 同一个 asset_id = 123 的光栅卡,有两条素材记录
INSERT INTO asset_material_relations (asset_id, material_id, material_type, layer_order) VALUES
(123, 456, 'main', 0), -- 主图(人物层)
(123, 789, 'bg', 1); -- 背景图
-- materials 表存储实际图片信息
INSERT INTO materials (id, oss_key, original_name, file_size, mime_type, hash, ...) VALUES
(456, 'asset/7/88/main/xxx.jpg', 'subject.jpg', 102400, 'image/jpeg', 'abc123', ...),
(789, 'asset/7/88/bg/xxx.jpg', 'bg.png', 204800, 'image/png', 'def456', ...);
封面图来源
铸造时,前端传入的 material_url(main 素材的 oss_key)会被设为 assets.CoverURL,用于列表页展示:
craftMintSubmit.js → createMintOrderApi({ material_url: main_oss_key })
↓
mint_service.go:339 assets.CoverURL = materialURLValue
↓
前端列表页通过 assets.CoverURL 获取封面图
完整数据流
submitCraftMintFromPath({ imagePath, bgImagePath, formData })
│
├─ uploadImageAndRegisterMaterial(imagePath, 'main') → materials.id=456
├─ uploadImageAndRegisterMaterial(bgImagePath, 'bg') → materials.id=789
│
├─ createMintOrderApi({ material_url: main_oss_key })
│ ↓
│ mint_service.go
│ ├── tx.Create(asset) → CoverURL = main_oss_key
│ ├── tx.Create(registry) → asset_registry (status=1)
│ └── 更新订单状态为 SUCCESS
│
└─ bindAssetMaterialsApi(asset_id, [
{ material_id: 456, material_type: 'main', layer_order: 0 },
{ material_id: 789, material_type: 'bg', layer_order: 1 }
])
↓
asset_material_relations 写入两条记录
问题分析
当前代码问题(lenticular-result.vue 第 596-623 行)
const selectAsset = async () => {
// line 596-625 是完整代码块
const imagePath =
isCraftLenticular.value // ❌ isCraftLenticular 未定义
? lenticularLayers.value.find((l) => l.id === 'mid')?.src || craftCoverUrl.value
: craftCoverUrl.value; // ❌ craftCoverUrl 未定义
if (!imagePath) { ... }
const bgImagePath = isCraftLenticular.value // ❌ isCraftLenticular 未定义
? lenticularLayers.value.find((l) => l.id === 'base')?.src || ''
: undefined;
try {
await submitCraftMintFromPath({ imagePath, bgImagePath, formData: craftFormData.value });
uni.navigateTo({ url: '/pages/castlove/success' });
return; // line 623 return 后不会执行
} catch (e) { ... }
// 以下代码 dead code(return 后的逻辑)
if (!isLenticularDisplay.value && selectedIndex.value === -1) { ... }
if (isUploading.value) { ... }
// ... 上传到 OSS 并创建订单的旧代码 ...
};
现有正确流程(craftMintSubmit.js 的 submitCraftMintFromPath)
// 第 187-306 行
export async function submitCraftMintFromPath({ imagePath, bgImagePath, formData }) {
// 1. 获取 OSS 签名
// 2. 上传主图并注册为 'main' 素材
// 3. 如果是光栅卡,上传背景图并注册为 'bg' 素材
// 4. 创建铸造订单(后端自动写入 asset_registry)
// 5. 绑定多素材到资产(main + bg)
// 6. 返回
}
铸造流程本身是正确的,问题在前端调用方。
文件结构
| 文件 | 修改类型 | 职责 |
|---|---|---|
frontend/pages/castlove/lenticular/lenticular-result.vue |
修改 | 修复 selectAsset 函数,添加缺失的 computed |
frontend/utils/craftMintSubmit.js |
已有 | 铸造流程(已正确,无需修改) |
Task 1: 修复 lenticular-result.vue 的 selectAsset 函数
Files:
-
Modify:
frontend/pages/castlove/lenticular/lenticular-result.vue:595-625 -
Step 1: 确认缺失的 import 并添加
在 <script setup> 块顶部已有:
import { submitCraftMintFromPath } from '@/utils/craftMintSubmit.js';
import { CASTLOVE_FORM_KEY, GENERATED_IMAGES_KEY, ... } from '@/utils/castloveGenerationFlow.js';
需要检查并补充以下 import(参考 asset-detail.vue 第 371 行):
import { CRAFT_LENTICULAR_CN, CRAFT_TAG_LENTICULAR } from '@/utils/castloveMintForm.js';
import { STUDIO_LENTICULAR } from '@/utils/castloveGenerationFlow.js';
- Step 2: 添加缺失的 computed 属性
在 craftFormData 之后添加:
// 光栅卡类型判断
const isCraftLenticular = computed(() =>
craftFormData.value?.studio_kind === STUDIO_LENTICULAR
);
// 光栅封面 URL(优先取中间层)
const craftCoverUrl = computed(() =>
lenticularLayers.value.find((l) => l.id === 'mid')?.src || ''
);
- Step 3: 清理 dead code(line 625-719)
selectAsset 函数的 return 后存在大量永不执行的代码(旧的上传 + 创建订单流程),需要删除:
- 删除
if (!isLenticularDisplay.value && selectedIndex.value === -1)分支 - 删除
if (isUploading.value)分支 - 删除
uni.getStorageSync(CASTLOVE_FORM_KEY)及后续所有旧代码 - 保留 try-catch 和错误处理
修复后 selectAsset 函数结构:
const selectAsset = async () => {
// 1. 获取主图路径
const imagePath =
isCraftLenticular.value
? craftCoverUrl.value
: craftCoverUrl.value;
if (!imagePath) {
uni.showToast({ title: '缺少作品图', icon: 'none' });
return;
}
// 2. 获取背景图路径(仅光栅卡)
const bgImagePath = isCraftLenticular.value
? lenticularLayers.value.find((l) => l.id === 'base')?.src || ''
: undefined;
console.log('[lenticular-result] handleCraftMint', {
isCraftLenticular: isCraftLenticular.value,
imagePath: imagePath?.substring(0, 50),
bgImagePath: bgImagePath?.substring(0, 50),
});
// 3. 调用铸造流程
uni.showLoading({ title: '铸造中…', mask: true });
try {
await submitCraftMintFromPath({ imagePath, bgImagePath, formData: craftFormData.value });
uni.hideLoading();
uni.navigateTo({ url: '/pages/castlove/success' });
} catch (e) {
uni.hideLoading();
uni.showToast({ title: e.message || '铸造失败', icon: 'none' });
}
};
- Step 4: 运行验证
检查 selectAsset 函数:
isCraftLenticular和craftCoverUrl已正确定义bgImagePath正确取base层图片submitCraftMintFromPath调用参数正确- dead code 已删除
Task 2: 验证铸造全链路
Files:
-
Test: 手动测试
lenticular-result.vue的铸造流程 -
Step 1: 检查后端是否自动写入 asset_registry
后端 mint_service.go 第 369-387 行在创建订单时已自动写入 asset_registry:
registry := &models.AssetRegistry{
AssetID: asset.ID,
AssetType: models.AssetTypeRegular,
OwnerUID: userID,
StarID: starID,
Grade: &grade,
Status: models.AssetRegistryStatusActive,
// ...
}
tx.Create(registry)
- Step 2: 确认 bindAssetMaterialsApi 调用后素材正确绑定
craftMintSubmit.js 第 268-286 行已调用 bindAssetMaterialsApi:
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 })
}
bindAssetMaterialsApi(assetId, materialsToBind)
Task 3: 检查清单确认
asset_registry有记录(asset_type = 'regular',status = 1)assets.CoverURL= main 素材的 oss_key(用于列表封面)materials表有两条记录(material_type = 'main' 和 'bg')asset_material_relations有两条绑定记录(同一个 asset_id,layer_order 0 和 1)- 前端
selectAsset无编译错误(isCraftLenticular、craftCoverUrl 已定义) - 铸造后跳转成功页
/pages/castlove/success
总结
核心理解:
asset_registry是索引表,不存图片 — 它通过asset_id关联到assets,再通过asset_material_relations关联到materials- 同一组光栅卡图片靠
asset_material_relations.asset_id相同 +material_type不同('main'/'bg')来区分 - 封面图来自
assets.CoverURL(即 main 素材的 oss_key)
问题根因: lenticular-result.vue 第 596-623 行的 selectAsset 函数引用了未定义的 isCraftLenticular 和 craftCoverUrl,且 return 后存在大量 dead code。
修复方案:
- 添加
isCraftLenticular和craftCoverUrlcomputed - 清理 dead code(return 后的旧上传/订单代码)
- 确保
bgImagePath传入base层图片给submitCraftMintFromPath
已正确部分: submitCraftMintFromPath 已完整实现:上传主图 + 背景图 → 注册素材 → 创建订单 → 绑定多素材,后端自动写 asset_registry。