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

315 lines
11 KiB
Markdown
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.

# 光栅卡铸造后写入 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` 表结构(不存图片)
```sql
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` 表实现多对一关联:
```sql
-- 同一个 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 行)
```javascript
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.js` 的 `submitCraftMintFromPath`
```javascript
// 第 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>` 块顶部已有:
```javascript
import { submitCraftMintFromPath } from '@/utils/craftMintSubmit.js';
import { CASTLOVE_FORM_KEY, GENERATED_IMAGES_KEY, ... } from '@/utils/castloveGenerationFlow.js';
```
需要检查并补充以下 import参考 `asset-detail.vue` 第 371 行):
```javascript
import { CRAFT_LENTICULAR_CN, CRAFT_TAG_LENTICULAR } from '@/utils/castloveMintForm.js';
import { STUDIO_LENTICULAR } from '@/utils/castloveGenerationFlow.js';
```
- [ ] **Step 2: 添加缺失的 computed 属性**
`craftFormData` 之后添加:
```javascript
// 光栅卡类型判断
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` 函数结构:
```javascript
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. `isCraftLenticular``craftCoverUrl` 已正确定义
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`
```go
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`
```javascript
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` 函数引用了未定义的 `isCraftLenticular``craftCoverUrl`,且 return 后存在大量 dead code。
**修复方案:**
1. 添加 `isCraftLenticular``craftCoverUrl` computed
2. 清理 dead codereturn 后的旧上传/订单代码)
3. 确保 `bgImagePath` 传入 `base` 层图片给 `submitCraftMintFromPath`
**已正确部分:** `submitCraftMintFromPath` 已完整实现:上传主图 + 背景图 → 注册素材 → 创建订单 → 绑定多素材,后端自动写 `asset_registry`