topfans/docs/superpowers/plans/2026-05-16-lenticular-card-mint-asset-registry.md
2026-05-17 19:03:34 +08:00

11 KiB
Raw Blame History

光栅卡铸造后写入 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 背景图)都注册到 materials2) asset_registry 有记录3) asset_material_relations 正确绑定两组图片

Architecture:

  • 前端在 lenticular-result.vueselectAsset 中调用 submitCraftMintFromPath 完成上传和铸造
  • submitCraftMintFromPath 已经支持:上传主图(main) + 背景图(bg)、注册素材、创建订单、绑定多素材
  • 问题:lenticular-result.vue 引用了未定义的 isCraftLenticularcraftCoverUrl,导致铸造流程断裂

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_urlmain 素材的 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 codereturn 后的逻辑)
    if (!isLenticularDisplay.value && selectedIndex.value === -1) { ... }
    if (isUploading.value) { ... }
    // ... 上传到 OSS 并创建订单的旧代码 ...
};

现有正确流程(craftMintSubmit.jssubmitCraftMintFromPath

// 第 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.vueselectAsset 函数

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 codeline 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 函数:

  1. isCraftLenticularcraftCoverUrl 已正确定义
  2. bgImagePath 正确取 base 层图片
  3. submitCraftMintFromPath 调用参数正确
  4. 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_idlayer_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 函数引用了未定义的 isCraftLenticularcraftCoverUrl,且 return 后存在大量 dead code。

修复方案:

  1. 添加 isCraftLenticularcraftCoverUrl computed
  2. 清理 dead codereturn 后的旧上传/订单代码)
  3. 确保 bgImagePath 传入 base 层图片给 submitCraftMintFromPath

已正确部分: submitCraftMintFromPath 已完整实现:上传主图 + 背景图 → 注册素材 → 创建订单 → 绑定多素材,后端自动写 asset_registry