61 KiB
镭射卡功能重构技术方案(总方案)
文档版本: v3.3
状态: 待评审
创建日期: 2026-05-25
更新日期: 2026-05-27
所属项目: TopFans 铸爱 — 镭射卡(Laser Card)
关联服务:assetService(Dubbo-go)、gateway(Gin)、frontend(uni-app)文档定位: 本文档为镭射卡重构唯一总方案。实施、评审、DDL、接口均以本文为准。
辅助材料(非总方案,不重复维护表结构):
docs/specs/2026-05-15-lenticular-card-multi-material-architecture-design.md— 光栅多素材规范基线(只读参考)docs/specs/2026-05-24-rembg-segmentation-feasibility-study.md— 通用抠图后续调研(本期不实施)docs/specs/2026-05-18-laser-card-refactor-design.md— 已废止,仅作历史 diff
文档目录
| 章节 | 内容 |
|---|---|
| §一~二 | 背景、现状与目标路径 |
| §三 | 魔搭抠图、Dify、aiWorkflowService、MiniMax 边界 |
| §四 | 整体逻辑架构 |
| §五 | 前端目录、组件隔离、五图生成(无后端) |
| §六 | 数据库完整设计(含 §6.3.1 tags 约定,不新增列) |
| §七 | 后端 API、铸造、页面职责、死代码 |
| §八~十 | 计划、风险、验收 |
| §十一 | 附录(预设种子、代码索引、组件迁移步骤) |
版本变更摘要
v3.3(相对 v3.2)
| 变更项 | 说明 |
|---|---|
| 品类 / 工艺标识 | 不在 assets / asset_registry 新增列;铸爱大类(星卡/吧唧/海报)与工艺小类(镭射/光栅等)统一写入既有 assets.tags(JSONB 数组) 约定字符串,见 §6.3.1 |
| 镭射铸造 tags | 铸造成功时 tags 必须含 cast:star_card + craft:laser(与光栅 craft:lenticular 对称) |
| 前端常量 | castloveMintForm.js 增加 CAST_TAG_* / CRAFT_TAG_LASER;buildCastloveFormSnapshot / useLaserMint 按工艺写入 |
| Phase 1 进度 | 前端已落地 laser-thinking、laser-result、LaserVariantPyramid、useLaserBatchGenerate 等;tags 双写与多素材铸造仍待 §十一附录 C 步 6 收尾 |
v3.2(相对 v3.1)
| 变更项 | 说明 |
|---|---|
| 前端组件隔离 | 新增 §5.4 组件目录、职责、数据流、迁移步骤 |
| 五图生成说明 | 新增 §5.5:无后端接口、JPG 格式、Storage 协议 |
| AI 平台服务 | 新增 §3.5 aiWorkflowService(Dify/魔搭统一编排,供多模块接入) |
| Dify 工作流 | 新增 §3.6 镭射五图工作流 laser_card_variants_v1 节点级设计 |
v3.0 相对 v2.0 的核心变更
| 变更项 | v2.0 | v3.0(本版) |
|---|---|---|
| 用户主路径 | create → 镭射工坊 laser-card-studio → 保存铸造 |
create → Thinking → 五图选卡 → 铸造(与产品截图一致) |
| 五图生成 | 计划废弃 laserBatchExport |
保留并强化五图批量合成;Phase 2 可迁 Dify 工作流 |
| 抠图 | 阿里云 IVPD | 魔搭社区 RMBG-2.0(服务端代理) |
| 生成编排 | 未定义 | Dify 工作流(Phase 2 服务端五图生成;Phase 1 仍用客户端合成) |
| 铸造 | craftMintSubmit 多素材 |
同左;选卡后与光栅卡一致走 estimateMintCost + ConfirmModal |
| 草稿 / 转赠 | 已舍弃 | 继续舍弃 |
| 前端目录 | pages/castlove/laser/laser-studio.vue |
pages/castlove/laser/laser-thinking.vue + laser-result.vue(对齐光栅 lenticular/*) |
一、背景与目标
1.1 项目背景
当前镭射卡存在以下问题:
| 问题 | 说明 |
|---|---|
| 双轨入口混乱 | 线上同时存在「工坊保存」与「生成五图选卡」两套入口,产品只保留后者,前者造成维护与安全风险 |
| 铸造链路落后 | 选卡结果页仍走单图 material_url + createMintOrderApi,未对齐光栅卡多素材模型 |
| 抠图不安全 | 客户端直连阿里云 IVPD,存在 AK 泄露风险 |
| 目录不规范 | 镭射逻辑散落在 discover/generation-* 与 laser-card-studio,未与光栅 lenticular/* 对仗 |
| 服务端无编排 | 五图合成纯客户端 Canvas,无法统一审计、限流与 A/B 预设 |
1.2 业务目标
- 用户路径与产品稿一致:上传照片 → Thinking → 五图金字塔选卡 → 确认铸造
- 铸造后资产可回溯 source / cutout / backdrop / composite 分层素材
- 与光栅卡体验一致:同一套「生成 → 选卡 → 费用确认 → 铸造成功」心智
- 为吧唧生成器等后续场景预留
scene路由(魔搭抠图)
1.3 技术目标
| 目标 | 说明 |
|---|---|
| 路径收敛 | 下线镭射工坊;create.vue 镭射仅走 startCraftGenerationFlow(STUDIO_LASER) |
| 规范对齐 | 页面 pages/castlove/laser/*、组件 components/laser/*、铸造走 craftMintSubmit.js |
| 魔搭抠图 | 服务端代理魔搭 RMBG-2.0,客户端无密钥 |
| Dify 编排 | Phase 2 将五图生成迁入 Dify Workflow;Phase 1 客户端 laserBatchExport 快速上线 |
| 2 周交付 | Phase 1 优先打通选卡 + 多素材铸造 + 魔搭抠图 |
1.4 本期明确不做
- ❌ 镭射工坊(
laser-card-studio.vue及入口handleSingleCraftLaserEntry) - ❌ 相册导出:不提供「保存到相册 / 下载到本地」;禁止
uni.saveImageToPhotosAlbum及任何面向用户的导出能力(现工坊laser-card-studio中的保存相册逻辑一并删除) - ❌ 草稿实例、转赠、领取中心、管理端模板 CRUD
- ❌ rembg 自建部署(见独立调研文档)
- ❌ 阿里云 IVPD 直连(客户端 AK 全部删除)
- ❌ 陀螺仪驱动镭射预览(晃动手机变光效)
仍允许(非「相册导出」):
| 能力 | 说明 |
|---|---|
| 从相册选图 | create 页 chooseImage 选用户照片作为输入,属于上传源,不是导出 |
| 离屏 Canvas 临时文件 | laserBatchExport → canvasToTempFilePath 仅用于五图候选与 OSS 上传 / 铸造,不对用户暴露「保存」 |
| WebGL 屏上动画 | LaserPreviewCanvas 仅页面内实时预览,不落相册 |
二、现状与目标路径对比
2.1 现状(As-Is)
flowchart TB
subgraph online [线上实际可触达]
A[create.vue 点生成] --> B[startCraftGenerationFlow laser]
B --> C[generation-loading.vue]
C --> D[laserBatchExport 客户端5图]
D --> E[generation-result.vue 金字塔选卡]
E --> F[单图 OSS + createMintOrderApi]
end
subgraph dead [应下线]
G[create 或旧入口] --> H[laser-card-studio.vue 3600行]
H --> I[castloveAfterLaserMint 单图]
end
| 路径 | 入口 | 行为 | 处置 |
|---|---|---|---|
| A. 五图选卡(保留) | create → startCraftGenerationFlow |
客户端批量 5 预设 → generation-result |
主路径,重构增强 |
| B. 镭射工坊(废弃) | handleSingleCraftLaserEntry → laser-card-studio |
WebGL 预览 + 单图铸造 | 删除 |
2.2 目标(To-Be)
sequenceDiagram
participant U as 用户
participant C as create.vue
participant T as laser-thinking.vue
participant R as laser-result.vue
participant API as Gateway
participant MS as 魔搭 RMBG
participant OSS as OSS
participant Mint as craftMintSubmit
U->>C: 上传照片 + 藏品信息 + 点「生成」
C->>T: startCraftGenerationFlow(STUDIO_LASER)
opt 智能抠图 Phase1可选
T->>API: POST /api/v1/segment scene=portrait
API->>MS: RMBG-2.0
MS-->>T: cutout_url
end
T->>T: 生成5张镭射变体 Phase1客户端 / Phase2 Dify
T->>R: 金字塔选卡 UI
U->>R: 选一张 + 开始铸造
R->>API: estimateMintCost
R->>U: ConfirmModal 水晶确认
U->>R: 确认
R->>OSS: 上传 source/cutout/backdrop/composite
R->>API: POST /laser/instances
R->>Mint: submitCraftMintFromPath 多素材
Mint-->>U: success 页
三、Dify + 魔搭 技术架构
3.1 职责划分
| 平台 | 职责 | 本期 |
|---|---|---|
| 魔搭社区 ModelScope | 人像抠图(RMBG-2.0 等),输入 HTTP 图 URL,输出透明 PNG | Phase 1 落地(POST /api/v1/segment) |
| Dify | 镭射卡五图生成工作流编排:接收用户图 + 预设参数 → 调用合成/风格节点 → 输出 5 张图 OSS URL | Phase 2 落地;Phase 1 用客户端 laserBatchExport |
| TopFans Gateway | 鉴权、限流、审计、统一错误码;不向客户端暴露魔搭/Dify 密钥 | Phase 1 |
| assetService | 实例 CRUD、铸造编排、操作流水 | Phase 1 |
3.2 魔搭抠图(Segment Service)
接口: POST /api/v1/segment
本期仅实现: scene=portrait → 魔搭 RMBG-2.0(或团队已申请的同系列模型)
推荐调用链:
客户端 multipart 上传
→ Gateway segment_controller
→ 临时上传 OSS(或内存限制 3MB 直传魔搭)
→ ModelScope Inference API(服务端 AK)
→ 结果写入用户 OSS:laser-card/{star_id}/{user_id}/{date}/{uuid}_cutout.png
→ 返回 cutout_oss_key + cutout_url_signed
降级: 魔搭失败 / 超时 → LC_SEGMENT_FAILED → 前端椭圆 mask(与现 laser-card-studio 注释一致,逻辑迁到 laser-thinking)
限流: 单用户 10 次/分钟;单 IP 30 次/分钟
scene 预留(本期返回 400): bajis / sticker / portrait_hd — 见 rembg 调研文档
3.3 Dify 镭射五图生成工作流(Phase 2)
目标: 将 laserBatchExport.js 的 5 预设合成迁到服务端,便于统一预设、监控与后续 AI 增强。
建议工作流(Dify Workflow):
flowchart LR
IN[输入: image_url, preset_codes, user_id] --> SEG{需要抠图?}
SEG -->|是| MS[HTTP: 魔搭 RMBG]
SEG -->|否| COMP[合成节点 x5]
MS --> COMP
COMP --> OSS[上传 OSS x5]
OSS --> OUT[输出: variant_urls 数组]
| 节点 | 说明 |
|---|---|
| 开始 | image_url、preset_codes(默认 5 个)、use_cutout(bool) |
| 条件分支 | use_cutout=true 时调魔搭(与 Segment 服务复用同一 HTTP 封装) |
| 代码/HTTP 节点 | 调用 TopFans 内部 laser-compositor 服务(Go 复刻 PRESET_VARIANTS)或 GPU 容器 |
| 结束 | variants: [{ preset_id, oss_key, signed_url, width, height }] |
对外 API(Phase 2 新增):
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/v1/laser/generate |
创建五图生成任务(异步) |
| GET | /api/v1/laser/generate/{job_id} |
轮询状态;完成返回 5 张图 URL |
Phase 1 不实现上述 API,laser-thinking 继续调用 generateLaserVariantBatch(imagePath, canvasId),与现网一致。
3.4 与现有 MiniMax 图生图接口的关系
| 接口 | 用途 | 镭射卡是否使用 |
|---|---|---|
POST /api/v1/assets/mints/image/generation |
星卡等 AI 文生图/图生图 四宫格 | 否 |
| 镭射五图 | 本地 Canvas 合成(Phase 1)或 Dify(Phase 2) | 是 |
镭射卡不走 MiniMax imageGenerationApi,避免与星卡共用 Loading 页时逻辑混淆;Phase 1 镭射不再进入 discover/generation-loading(见 §5.4)。
3.5 AI 编排服务(aiWorkflowService,Phase 2 起)
定位: 平台级 AI 能力微服务,与
laserService(镭射业务实例)、assetService(铸造/素材)分离。Dify、魔搭等密钥仅在此服务内;业务方只认workflow_key+run_id。
| 职责 | 说明 |
|---|---|
| 工作流注册 | workflow_key → Dify app_id / 版本 / 默认 inputs |
| 异步运行 | CreateRun / GetRun / CancelRun |
| Provider 适配 | Dify(编排)、魔搭(Segment / 可选图生图)、后续可收敛 MiniMax |
| 统一限流与审计 | 按 user_id、workflow_key 配额 |
不负责: 镭射实例 CRUD、水晶扣费、materials 写入(仍属 laserService / assetService)。
Gateway 对外(建议):
| 方法 | 路径 | Phase |
|---|---|---|
| POST | /api/v1/ai/runs |
P2 |
| GET | /api/v1/ai/runs/:run_id |
P2 |
| POST | /api/v1/segment |
P1(实现可暂放 Gateway,逻辑归属 ai 服务) |
镭射五图 Phase 2: workflow_key = laser_card_variants_v1,由 useLaserBatchGenerate 在开关打开时改为轮询 /ai/runs,关闭时仍走 §5.5 客户端 batch。
flowchart LR
subgraph biz [业务]
LT[laser-thinking]
LS[laserService]
AS[assetService]
end
subgraph ai [aiWorkflowService]
RUN[workflow_runs]
DIFY[Dify Adapter]
MS[ModelScope Adapter]
end
LT -->|P2 CreateRun| ai
LT -->|P1 batch 本地| LT
LS --> AS
ai --> DIFY
ai --> MS
3.6 Dify 工作流详设:laser_card_variants_v1(Phase 2)
原则: Dify 只做编排;像素级镭射合成调内部 laser-compositor HTTP(Go 复刻
laserBatchExport),不用魔搭通用图生图替代 compositor(效果不可控,见评审结论)。
工作流类型: Workflow(API 触发,非 Chatbot)
开始节点 — 输入变量:
| 变量 | 类型 | 必填 | 说明 |
|---|---|---|---|
source_image_url |
string | ✅ | 用户原图可访问 URL(OSS 签名) |
use_cutout |
boolean | ✅ | 是否人像抠图 |
preset_codes |
array[string] | 否 | 默认 ["dream","classic","holoFull","ice","sunset"] |
star_id / user_id |
number | ✅ | 隔离与审计 |
export_width / export_height |
number | 否 | 默认 1080 / 1440 |
节点链路:
flowchart TB
START[开始] --> NORM[代码: 补全 preset + render_config]
NORM --> BR{use_cutout?}
BR -->|是| SEG[HTTP POST TopFans /segment]
BR -->|否| LOOP[循环 preset_codes]
SEG --> SEGF{成功?}
SEGF -->|否| WARN[warnings += SEGMENT_FAILED]
SEGF -->|是| LOOP
WARN --> LOOP
LOOP --> COMP[HTTP POST laser-compositor /compose]
COMP --> AGG[代码: 聚合 variants]
AGG --> END[结束 JSON]
| 步骤 | 节点 | 说明 |
|---|---|---|
| 1 | 代码 | preset_codes 为空则填默认 5 个;展开为带 render_config 的列表(与 laserPresets.js / DB 模板一致) |
| 2 | 条件 | use_cutout=true → HTTP POST /api/v1/segment(scene=portrait),失败不中断,cutout_url="" 并记 warning |
| 3 | 循环 | 对每个 preset 调 POST /v1/compose(或一次 POST /v1/compose/batch) |
| 4 | 结束 | 输出统一 JSON(见下) |
结束节点 — 输出 JSON:
{
"workflow_key": "laser_card_variants_v1",
"status": "succeeded",
"cutout_oss_key": "",
"variants": [
{
"preset_id": "holoFull",
"oss_key": "laser-card/.../variant_holoFull.jpg",
"signed_url": "https://...",
"width": 1080,
"height": 1440
}
],
"warnings": [],
"engine_version": "compositor-1.0.0"
}
aiWorkflowService 将 outputs 写入 ai_workflow_runs,laser-thinking / useLaserBatchGenerate 轮询完成后写入 GENERATED_IMAGES_KEY(URL 数组,与 Phase 1 本地 path 数组对前端透明)。
四、整体逻辑架构
┌─────────────────────────────────────────────────────────────┐
│ 客户端 (uni-app) │
│ create.vue ──► laser-thinking.vue ──► laser-result.vue │
│ │ │ │ │
│ │ laserBatchExport (P1) │ craftMintSubmit │
│ │ Dify job poll (P2) │ │
└───────┼──────────────┼──────────────────────┼─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ API Gateway (Gin) │
│ /api/v1/segment 魔搭抠图代理 │
│ /api/v1/laser/templates 模板/预设只读 │
│ /api/v1/laser/instances 实例创建/查询 │
│ /api/v1/laser/instances/:id/mint │
│ /api/v1/assets/* 复用 OSS 签名、素材、铸造估价 │
└───────────────────────────────┬─────────────────────────────┘
│ Dubbo
┌───────────────────────────────▼─────────────────────────────┐
│ assetService │
│ LaserCardService │ MaterialService │ MintService │
└───────────────────────────────┬─────────────────────────────┘
│
┌───────────────┬───────────────┬───────────────────┐
▼ ▼ ▼
PostgreSQL 阿里云 OSS aiWorkflowService (P2)
laser_* 三表 素材与成品图 Dify + 魔搭
五、前端架构(目录、组件隔离、五图生成)
5.1 目录对仗(页面 + 组件 + composable)
| 光栅卡(基线) | 镭射卡(目标) |
|---|---|
pages/castlove/create.vue(分流) |
同左,studioKind=laser → laser-thinking |
pages/castlove/lenticular/lenticular-thinking.vue |
pages/castlove/laser/laser-thinking.vue |
pages/castlove/lenticular/lenticular-result.vue |
pages/castlove/laser/laser-result.vue |
components/lenticular/LenticularCard.vue |
components/laser/LaserPreviewCanvas.vue(动态预览) |
| — | components/laser/LaserVariantPyramid.vue(五图金字塔) |
| — | components/laser/LaserVariantThumb.vue(静态缩略图) |
| — | components/laser/LaserBatchCanvasHost.vue(离屏 batch canvas) |
composables/useLenticularCraftTiltPreview.js |
composables/useLaserPreview.js(WebGL/2D 动画) |
| — | composables/useLaserBatchGenerate.js(五图 batch + Storage) |
| — | composables/useLaserResultSelection.js(选中 ↔ preset ↔ 铸造 path) |
| — | composables/useLaserMint.js(估价 + ConfirmModal + craftMintSubmit) |
| — | composables/useLaserSegment.js(魔搭抠图,P1 可选) |
utils/craftMintSubmit.js |
直接复用,扩展 material_type |
utils/lenticular-engine.js |
utils/laser-card/laserBatchExport.js、laserPreviewWebgl.js、laserPresets.js |
5.2 路由与 Storage 约定
| Key / 常量 | 说明 |
|---|---|
GENERATION_FLOW_KEY + FLOW_MODE_LASER |
不变;navigateToStudioLoading 改为跳转 /pages/castlove/laser/laser-thinking |
GENERATED_IMAGES_KEY |
存 5 张变体本地路径或 URL |
GENERATION_RESULT_META_KEY |
{ displayMode: 'laser', imageCount: 5, selectedPresetId } |
CASTLOVE_FORM_KEY |
与光栅共用表单快照 |
CASTLOVE_LASER_ENTRY_KEY |
删除(工坊专用) |
5.3 强制原则
- 镭射禁止新增
laser-card-studio.vue路由与 pages.json 注册 discover/generation-loading、generation-result不得再含镭射v-if分支;镭射仅走pages/castlove/laser/*- 页面薄编排:
laser-thinking/laser-result各 < 200 行,业务在components/laser+ composable - 素材绑定必须经
uploadMaterialApi+bindAssetMaterialsApi - 选卡页铸造必须与光栅一致:
estimateMintCostApi→ConfirmModal→submitCraftMintFromPath - 禁止
uni.saveImageToPhotosAlbum(见 §1.4) - 铸造时
assets.tags写入cast:star_card(铸爱大类)+craft:laser(工艺小类);禁止为品类/工艺新增cast_category/craft_type列(见 §6.3.1) material_type仍为表单「素材类型」文案(如粉丝自制),不等于 工艺;工艺只认tags中craft:*
5.4 前端组件隔离设计
5.4.1 隔离原则
| 原则 | 做法 |
|---|---|
| 按工艺分包 | 镭射 UI 仅在 components/laser/,不 import components/lenticular/* |
| 引擎与 UI 分离 | Canvas/WebGL 在 utils/laser-card/;组件只通过 composable 调用 |
| 预览动 / 铸造静 | 动画仅 LaserPreviewCanvas;铸造用 batch 静态 JPG(§5.5) |
| 与 discover 脱钩 | 星卡/AI 四图仍用 generation-loading;镭射已迁出 |
5.4.2 组件与 composable 职责
| 模块 | 职责 | 禁止包含 |
|---|---|---|
| LaserBatchCanvasHost | 隐藏 canvas-id="laserBatchCanvas" |
业务逻辑、进度 UI |
| useLaserBatchGenerate | 调 generateLaserVariantBatch → persistLaserPreviewImages |
HTTP 五图接口(P1 无) |
| LaserPreviewCanvas | 单 WebGL/2D 实例,u_time 驱动动画 |
陀螺仪、保存相册 |
| useLaserPreview | RAF 生命周期、preset 切换、降级 | 铸造、OSS |
| LaserVariantPyramid | 金字塔布局、getCardStyle、选中态;中央 slot 放 Preview |
batch 合成 |
| LaserVariantThumb | 单张静态 <image> |
动画 |
| useLaserResultSelection | selectedIndex ↔ presetId ↔ paths[i] |
扣费 |
| useLaserMint | estimateMintCost + ConfirmModal + submitCraftMintFromPath |
五图生成 |
| useLaserSegment | POST /segment(可选) |
实例写库 |
5.4.3 页面编排(薄层)
laser-thinking.vue
LaserThinkingShell(礼盒 + 进度 + "Thinking",从 generation-loading 抽取)
└── LaserBatchCanvasHost
onMounted → useLaserBatchGenerate().run() → navigateTo laser-result
laser-result.vue
LaserCraftRewardBanner(可选)
LaserVariantPyramid(:paths :selected @select)
└── #hero → LaserPreviewCanvas(:source-path :preset :paused)
底部:重新生成 | 选择作品
ConfirmModal ← useLaserMint
5.4.4 组件数据流
flowchart TB
subgraph pages [pages/castlove/laser]
TH[laser-thinking]
RS[laser-result]
end
subgraph comp [components/laser]
BH[LaserBatchCanvasHost]
PV[LaserPreviewCanvas]
PY[LaserVariantPyramid]
end
subgraph hooks [composables]
BG[useLaserBatchGenerate]
LP[useLaserPreview]
SEL[useLaserResultSelection]
MT[useLaserMint]
end
subgraph util [utils/laser-card]
BE[laserBatchExport]
WG[laserPreviewWebgl]
end
TH --> BG --> BE
TH --> BH
RS --> PY --> PV
PV --> LP --> WG
RS --> SEL --> MT
5.4.5 与共享模块边界
| 模块 | 关系 |
|---|---|
castloveGenerationFlow.js |
navigateToStudioLoading('laser') → /pages/castlove/laser/laser-thinking |
create.vue |
仅 startCraftGenerationFlow(STUDIO_LASER),不 import components/laser |
craftMintSubmit.js |
扩展多素材;由 useLaserMint 调用 |
5.5 五图生成(Phase 1:无后端接口)
结论: 五图不调用 TopFans 后端生成接口;与星卡
POST /api/v1/assets/mints/image/generation(MiniMax)完全分离。
5.5.1 调用链
create.vue → startCraftGenerationFlow(STUDIO_LASER)
→ CASTLOVE_FORM_KEY + GENERATION_FLOW_KEY { mode: 'laser' }
→ laser-thinking.vue
→ useLaserBatchGenerate.run()
→ generateLaserVariantBatch(imagePath, 'laserBatchCanvas')
→ persistLaserPreviewImages(paths)
→ laser-result.vue 读取 GENERATED_IMAGES_KEY
输入: formData.image 或 formData.uploadedImage(chooseImage 本地路径,非 URL)
核心实现: frontend/utils/laser-card/laserBatchExport.js — 循环 5 次 PRESET_VARIANTS,2D Canvas 合成(底纹 + 人像 + 镭射叠层)
5.5.2 输出格式
| 项 | 值 |
|---|---|
| API | 无;仅 uni.canvasToTempFilePath |
| 文件类型 | JPEG(fileType: 'jpg', quality: 0.96) |
| 逻辑尺寸 | 450 × 600 |
| 输出像素 | 900 × 1200(destWidth/Height = 2×) |
| 返回类型 | string[] 本地临时路径 |
5.5.3 Storage 协议
| Key | 内容 |
|---|---|
generated_images |
JSON.stringify(["path0","path1",...]) |
generation_result_meta |
{"displayMode":"laser","imageCount":5} |
castlove_form_data |
铸造前保留;含 image / uploadedImage 等 |
五图阶段不上传 OSS;选卡确认铸造后再 getOssSignatureApi 上传。
5.5.4 与动画预览的关系
| 用途 | 实现 |
|---|---|
| 用户看到的「会动的镭射」 | LaserPreviewCanvas(WebGL,laserPreviewWebgl.js) |
| 选卡/铸造用的图 | batch 生成的 静态 JPG(paths[selectedIndex]) |
切换缩略图时:更新 Preview 的 preset(光效变),铸造仍绑定对应静态文件。
5.5.5 Phase 2 切换点
useLaserBatchGenerate 内根据 feature_laser_dify_compose:
false(默认):§5.5.1 客户端 batchtrue:POST /api/v1/ai/runs+ 轮询 →variants[].signed_url写入GENERATED_IMAGES_KEY
六、数据库设计(完整)
设计原则
- 本期仅新增 3 张镭射业务表;素材主数据与资产绑定完全复用既有
materials+asset_material_relations。- 禁止新增
laser_card_instance_materials中间表;铸造前临时素材仅存laser_card_instances.materials_snapshot(JSONB)。- 禁止在
laser_card_service内私写materials/asset_material_relationsSQL;铸造成功后统一走既有MaterialService.UploadMaterial+BindAssetMaterialsRPC。- 旧版单图镭射资产仍可读
assets.material_url;新资产以asset_material_relations为准。- 品类(星卡/吧唧/海报)与工艺(镭射/光栅等)本期不新增 DDL 列;统一用既有
assets.tags(JSONB) 约定前缀cast:*/craft:*(§6.3.1)。若未来按大类高频筛选成为瓶颈,再单独立项增加cast_category列并自tags回填。
6.1 表清单与职责
| 表名 | 类型 | 职责 |
|---|---|---|
laser_card_templates |
新增 | 镭射预设模板(5 条种子,只读) |
laser_card_instances |
新增 | 用户一次「选卡→铸造」业务实例 |
laser_card_operation_logs |
新增 | 实例写操作审计流水 |
materials |
复用 | 素材文件元数据(OSS key、hash、尺寸) |
asset_material_relations |
复用 | 资产 ↔ 素材角色绑定(main/source/cutout/backdrop) |
assets |
复用 | 铸造产出的藏品主表 |
mint_orders |
复用 | 铸造订单、水晶扣费 |
users |
复用 | owner_user_id 逻辑关联(库内可不建物理 FK) |
stars |
复用 | star_id 多星隔离 |
明确不建表: laser_card_instance_materials、laser_card_drafts、laser_card_transfers。
6.2 全库 ER 关系
erDiagram
laser_card_templates ||--o{ laser_card_instances : "1:N template_id"
laser_card_instances ||--o{ laser_card_operation_logs : "1:N instance_id"
laser_card_instances }o--o| assets : "0..1 asset_id"
laser_card_instances }o--o| mint_orders : "0..1 mint_order_id"
assets ||--o{ asset_material_relations : "1:N asset_id"
materials ||--o{ asset_material_relations : "1:N material_id"
laser_card_templates {
bigint id PK
varchar template_code UK
jsonb render_config
jsonb backdrop_options
}
laser_card_instances {
bigint id PK
varchar instance_no UK
varchar instance_ulid UK
bigint template_id FK
bigint owner_user_id
bigint star_id
varchar status
jsonb materials_snapshot
bigint asset_id FK
varchar mint_order_id FK
}
laser_card_operation_logs {
bigint id PK
bigint instance_id FK
varchar action
}
assets {
bigint id PK
bigint owner_uid
varchar cover_url
varchar material_url
jsonb tags
}
materials {
bigint id PK
varchar oss_key UK
varchar hash
}
asset_material_relations {
bigint id PK
bigint asset_id FK
bigint material_id FK
varchar material_type UK
int layer_order UK
}
mint_orders {
varchar order_id PK
bigint user_id
bigint asset_id
}
6.3 表间关系说明(基数 + 关联键 + 时机)
| 关系 | 基数 | 关联字段 | 写入时机 | 说明 |
|---|---|---|---|---|
templates → instances |
1 : N | instances.template_id → templates.id |
POST /laser/instances |
用户选中的预设;template_code / template_version 冗余便于列表查询 |
instances → operation_logs |
1 : N | logs.instance_id → instances.id |
每次写实例 | 只追加,不更新 |
instances → assets |
N : 0..1 | instances.asset_id → assets.id |
POST .../mint 成功 |
铸造前 asset_id 为 NULL |
instances → mint_orders |
N : 0..1 | instances.mint_order_id → mint_orders.order_id |
发起铸造时 | 与光栅共用 InitMintOrder 返回的 order_id |
assets → asset_material_relations |
1 : N | amr.asset_id → assets.id |
铸造成功事务末 | 仅在 status=minted 后写入 |
materials → asset_material_relations |
1 : N | amr.material_id → materials.id |
同上 | 同一 oss_key 可对应一条 materials 行 |
instances.materials_snapshot |
— | JSONB 内 oss_key |
POST /instances |
铸造前唯一素材暂存;不指向 materials.id |
instances → users |
N : 1 | owner_user_id |
创建实例 | 鉴权:owner_user_id = JWT user_id |
instances → stars |
N : 1 | star_id |
创建实例 | 与 Attachment star_id 一致 |
与旧字段兼容:
| 字段 | 新镭射资产 | 旧镭射资产(单图) |
|---|---|---|
assets.cover_url |
= 选中 composite 的 CDN URL |
同 |
assets.material_url |
可写 composite URL 作降级 | 唯一素材来源 |
assets.tags |
含 cast:star_card + craft:laser |
可能仅 craft:laser 或为空;读路径需兼容 |
GetAssetMaterials |
4 条 amr |
空则降级读 material_url |
6.3.1 assets.tags 品类与工艺约定(本期不新增列)
决策(v3.3): 铸爱「大类」与「工艺小类」均写入
assets.tags,与现网光栅craft:lenticular一致;不在assets/asset_registry增加cast_category、craft_type等列。
6.3.1.1 与 star_id / material_type 的区分
| 字段 / 概念 | 含义 | 镭射卡示例 |
|---|---|---|
assets.star_id |
所属明星/星球(多星隔离) | 1001 |
assets.material_type(可选列) |
表单「素材类型」文案(粉丝自制等) | 粉丝自制 |
tags 中 cast:* |
铸爱大类:星卡 / 吧唧 / 海报 | cast:star_card |
tags 中 craft:* |
工艺小类:镭射 / 光栅 / 拍立得等 | craft:laser |
laser_card_instances.template_code |
五套预设(dream/classic/…) | holoFull |
6.3.1.2 约定枚举(常量须在前后端统一定义)
大类 cast:*(与 craft-select.vue 左侧分类对齐):
| tag 值 | 产品名 |
|---|---|
cast:star_card |
星卡 |
cast:badge |
吧唧 |
cast:poster |
海报 |
工艺 craft:*(与工艺卡片对齐):
| tag 值 | 产品名 | 现网写入情况 |
|---|---|---|
craft:laser |
镭射卡 | Phase 1 铸造时必须写入 |
craft:lenticular |
光栅卡 | 已写入(castloveMintForm.js) |
craft:polaroid |
拍立得 | 待产品开通时写入 |
craft:tear_off |
撕拉片 | 待产品开通时写入 |
craft:plain |
普通单图星卡(无特殊工艺) | 可选 |
镭射卡铸造成功时 tags 示例:
["cast:star_card", "craft:laser"]
光栅卡对照:
["cast:star_card", "craft:lenticular"]
6.3.1.3 写入时机与链路
| 阶段 | 写入位置 | 说明 |
|---|---|---|
| create / 选卡页 | CASTLOVE_FORM_KEY 快照字段 tags: string[] |
buildCastloveFormSnapshot 或镭射专用快照按 pageName / 工艺注入 |
POST /api/v1/assets/mints |
CreateMintOrderRequest.tags → mint_service → assets.tags |
与光栅相同,铸造事务内落库 |
laser_card_instances |
不重复存 tags | 工艺由业务表 + 关联 assets 表达;实例表只存 template_code、materials_snapshot |
前端常量(frontend/utils/castloveMintForm.js):
export const CAST_TAG_STAR_CARD = 'cast:star_card'
export const CAST_TAG_BADGE = 'cast:badge'
export const CAST_TAG_POSTER = 'cast:poster'
export const CRAFT_TAG_LASER = 'craft:laser'
// 已有:export const CRAFT_TAG_LENTICULAR = 'craft:lenticular'
buildCastloveFormSnapshot 规则(摘要):
- 从
craft-select进入且大类为星卡、工艺为镭射:tags = [CAST_TAG_STAR_CARD, CRAFT_TAG_LASER] - 光栅:
tags = [CAST_TAG_STAR_CARD, CRAFT_TAG_LENTICULAR](在现有仅craft:lenticular基础上补cast:star_card) - 禁止把
craft_name中文名(如「镭射卡」)直接写入tags
6.3.1.4 查询与索引
推荐 SQL(与画廊光栅判断一致):
-- 是否镭射工艺
(a.tags @> '["craft:laser"]') AS is_laser
-- 是否星卡大类下的藏品(可选,用于广场/星册筛大类)
(a.tags @> '["cast:star_card"]') AS is_star_card_cast
本期不强制 为 tags 建 GIN 索引;若 cast:* / craft:* 筛选 QPS 升高,再评估:
CREATE INDEX idx_assets_tags_gin ON assets USING GIN (tags) WHERE deleted_at IS NULL;
明确不做:
- 不在
asset_registry冗余cast_category(避免双写);星册按大类筛时 JOINassets或读 API 层聚合。 - 不用
tags存五图预设 id(预设走laser_card_instances.template_code/GENERATION_RESULT_META_KEY.selectedPresetId)。
6.3.1.5 历史数据与兼容
| 数据 | 读逻辑 |
|---|---|
仅有 craft:lenticular |
视为工艺光栅;大类可推断为 cast:star_card(仅展示层,不回填库表除非做迁移脚本) |
tags 为空的老镭射单图 |
GetAssetMaterials 空则降级 material_url;不根据 tags 阻断展示 |
| 回填(可选运维脚本) | UPDATE assets SET tags = tags || '"cast:star_card"'::jsonb WHERE ... 需谨慎去重 |
6.4 复用表结构(实施须对齐现网)
6.4.0 assets.tags(已上线,本期扩展约定即可)
-- 来源:docker/init-db.sql
tags jsonb -- 字符串数组,如 ["cast:star_card","craft:laser"]
| 属性 | 说明 |
|---|---|
| 类型 | JSONB,元素为 string |
| 本期变更 | 无 DDL;仅扩展写入约定(§6.3.1) |
| 与迁移关系 | migrate_laser_card_v3_tables.sql 不包含 ALTER TABLE assets ADD COLUMN |
6.4.1 materials(素材主表,已上线)
-- 来源:docker/init-db.sql V6 / supabase/migrations/20260515_*
CREATE TABLE materials (
id BIGSERIAL PRIMARY KEY,
oss_key VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(100) NOT NULL,
width INT,
height INT,
hash VARCHAR(64) NOT NULL,
created_by BIGINT NOT NULL,
star_id BIGINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
deleted_at BIGINT
);
CREATE UNIQUE INDEX uk_materials_oss_key ON materials(oss_key);
CREATE INDEX idx_materials_hash ON materials(hash);
CREATE INDEX idx_materials_created_by ON materials(created_by);
CREATE INDEX idx_materials_star_id ON materials(star_id);
| 字段 | 类型 | 镭射卡用途 |
|---|---|---|
oss_key |
VARCHAR(255) UK | laser-card/{star_id}/{user_id}/{date}/{uuid}_{role}.ext |
hash |
VARCHAR(64) | 客户端 SHA256,用于去重与幂等 |
created_by |
BIGINT | = owner_user_id |
star_id |
BIGINT | 多星隔离 |
6.4.2 asset_material_relations(资产-素材关联,已上线)
CREATE TABLE asset_material_relations (
id BIGSERIAL PRIMARY KEY,
asset_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
material_type VARCHAR(50) NOT NULL,
layer_order INT NOT NULL DEFAULT 0,
pos_x DOUBLE PRECISION,
pos_y DOUBLE PRECISION,
opacity DOUBLE PRECISION DEFAULT 1.0,
rotation DOUBLE PRECISION DEFAULT 0,
scale_x DOUBLE PRECISION DEFAULT 1.0,
scale_y DOUBLE PRECISION DEFAULT 1.0,
version INT NOT NULL DEFAULT 1,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
deleted_at BIGINT
);
ALTER TABLE asset_material_relations
ADD CONSTRAINT fk_amr_material
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT;
-- asset_id → assets(id) ON DELETE CASCADE 以现网 migration 为准
CREATE INDEX idx_amr_asset_id ON asset_material_relations(asset_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_amr_material_id ON asset_material_relations(material_id) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uk_amr_asset_type_active
ON asset_material_relations(asset_id, material_type) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uk_amr_asset_layer_active
ON asset_material_relations(asset_id, layer_order) WHERE deleted_at IS NULL;
镭射卡 material_type 与 layer_order(铸造成功后写入):
| material_type | layer_order | 对应 snapshot.role | 说明 |
|---|---|---|---|
backdrop |
0 | backdrop |
最底层:液态底纹 |
source |
1 | source |
用户原图(可选展示) |
cutout |
2 | cutout |
魔搭抠图 PNG;无抠图时可省略该行 |
main |
3 | composite |
用户选中的五图之一,封面 |
光栅卡使用
main(1) +bg(0);镭射与光栅 material_type 不混用于同一资产。
常量须定义在backend/pkg/models/material.go,禁止业务层硬编码字符串。
6.4.3 assets / mint_orders(逻辑关联)
| 表 | 镭射关联字段 | 说明 |
|---|---|---|
assets |
laser_card_instances.asset_id |
铸造成功后回填;cover_url = composite 签名 URL |
mint_orders |
laser_card_instances.mint_order_id = order_id |
status: PENDING → PROCESSING → SUCCESS/FAILED |
6.5 新增表:laser_card_templates
职责: 内置 5 套预设(与 laserBatchExport.js → PRESET_VARIANTS 一一对应),本期仅种子数据,无管理端 CRUD。
CREATE TABLE laser_card_templates (
id BIGSERIAL PRIMARY KEY,
template_code VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'published',
version INT NOT NULL DEFAULT 1,
thumbnail_oss_key VARCHAR(255),
backdrop_options JSONB NOT NULL DEFAULT '[]',
render_config JSONB NOT NULL,
engine_min_version VARCHAR(32) NOT NULL DEFAULT 'compositor-1.0.0',
sort_order INT NOT NULL DEFAULT 0,
star_id BIGINT NOT NULL DEFAULT 0,
created_by BIGINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
deleted_at BIGINT
);
CREATE UNIQUE INDEX uk_lct_code_version
ON laser_card_templates(template_code, version)
WHERE deleted_at IS NULL;
CREATE INDEX idx_lct_status_star
ON laser_card_templates(status, star_id)
WHERE deleted_at IS NULL;
| 字段 | 类型 | 空 | 说明 |
|---|---|---|---|
id |
BIGSERIAL | N | 主键 |
template_code |
VARCHAR(64) | N | 业务编码:dream/classic/holoFull/ice/sunset |
name |
VARCHAR(100) | N | 展示名 |
description |
TEXT | Y | 描述 |
status |
VARCHAR(20) | N | published | archived |
version |
INT | N | 模板版本;与 uk_lct_code_version 联合唯一 |
thumbnail_oss_key |
VARCHAR(255) | Y | 列表缩略图 OSS |
backdrop_options |
JSONB | N | [{ "id": "liquidBlue", "label": "...", "oss_key": "static/laser-bg/..." }] |
render_config |
JSONB | N | 默认滑杆、beam、style_preset_id 等,< 16KB |
engine_min_version |
VARCHAR(32) | N | 最低合成器版本 |
sort_order |
INT | N | 列表排序 |
star_id |
BIGINT | N | 0 = 全站通用 |
created_by |
BIGINT | N | 种子填 0 |
created_at / updated_at |
BIGINT | N | 毫秒时间戳 |
deleted_at |
BIGINT | Y | 软删除 |
种子数据(5 条): 见 §十附录 A。
6.6 新增表:laser_card_instances
职责: 一次「上传 → 五图选卡 → 铸造」的业务实体;铸造前素材在 materials_snapshot,铸造后回填 asset_id 并写 asset_material_relations。
CREATE TABLE laser_card_instances (
id BIGSERIAL PRIMARY KEY,
instance_no VARCHAR(32) NOT NULL,
instance_ulid VARCHAR(40) NOT NULL,
template_id BIGINT NOT NULL,
template_code VARCHAR(64) NOT NULL,
template_version INT NOT NULL,
owner_user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'rendered',
client_request_id VARCHAR(64),
render_config JSONB,
materials_snapshot JSONB NOT NULL DEFAULT '[]',
composite_oss_key VARCHAR(255),
composite_material_id BIGINT,
asset_id BIGINT,
mint_order_id VARCHAR(100),
idempotency_key VARCHAR(64),
version INT NOT NULL DEFAULT 1,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
deleted_at BIGINT,
CONSTRAINT fk_lci_template
FOREIGN KEY (template_id) REFERENCES laser_card_templates(id),
CONSTRAINT fk_lci_asset
FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE SET NULL,
CONSTRAINT fk_lci_mint_order
FOREIGN KEY (mint_order_id) REFERENCES mint_orders(order_id) ON DELETE SET NULL,
CONSTRAINT fk_lci_composite_material
FOREIGN KEY (composite_material_id) REFERENCES materials(id) ON DELETE SET NULL,
CONSTRAINT chk_lci_status
CHECK (status IN ('rendered', 'minting', 'minted'))
);
CREATE UNIQUE INDEX uk_lci_instance_no ON laser_card_instances(instance_no);
CREATE UNIQUE INDEX uk_lci_instance_ulid ON laser_card_instances(instance_ulid);
CREATE UNIQUE INDEX uk_lci_user_client_req
ON laser_card_instances(owner_user_id, client_request_id)
WHERE deleted_at IS NULL AND client_request_id IS NOT NULL;
CREATE INDEX idx_lci_owner_status
ON laser_card_instances(owner_user_id, status)
WHERE deleted_at IS NULL;
CREATE INDEX idx_lci_asset
ON laser_card_instances(asset_id)
WHERE asset_id IS NOT NULL AND deleted_at IS NULL;
CREATE INDEX idx_lci_star_created
ON laser_card_instances(star_id, created_at DESC)
WHERE deleted_at IS NULL;
| 字段 | 类型 | 空 | 说明 |
|---|---|---|---|
id |
BIGSERIAL | N | 主键;对外可用 instance_ulid |
instance_no |
VARCHAR(32) | N | LC + yyyyMMddHHmmss + 6 位随机;UK |
instance_ulid |
VARCHAR(40) | N | 对外 ID,如 lc_inst_01HXYZ...;UK |
template_id |
BIGINT | N | FK → laser_card_templates.id |
template_code |
VARCHAR(64) | N | 冗余,避免 JOIN |
template_version |
INT | N | 创建时模板版本快照 |
owner_user_id |
BIGINT | N | 持有者 |
star_id |
BIGINT | N | 多星隔离 |
status |
VARCHAR(20) | N | 见下表状态机 |
client_request_id |
VARCHAR(64) | Y | 客户端 UUID;与 owner_user_id 联合幂等 |
render_config |
JSONB | Y | 用户最终渲染参数快照 |
materials_snapshot |
JSONB | N | 铸造前素材 OSS 清单(不存 material_id) |
composite_oss_key |
VARCHAR(255) | Y | 选中合成图 OSS key(= snapshot 中 composite) |
composite_material_id |
BIGINT | Y | 铸造后回填 materials.id(main 层) |
asset_id |
BIGINT | Y | 铸造成功后回填 |
mint_order_id |
VARCHAR(100) | Y | FK → mint_orders.order_id |
idempotency_key |
VARCHAR(64) | Y | 最后一次写操作幂等键 |
version |
INT | N | 乐观锁 |
created_at / updated_at / deleted_at |
BIGINT | — | 审计 |
状态机:
| status | 含义 | 允许操作 |
|---|---|---|
rendered |
已选卡、素材已上传、实例已创建,未铸造 | POST .../mint |
minting |
铸造进行中 | 幂等查询;失败可回到 rendered |
minted |
铸造完成,asset_id 已回填 |
只读 |
stateDiagram-v2
[*] --> rendered: POST /laser/instances
rendered --> minting: POST /mint
minting --> minted: CreateMintOrder SUCCESS
minting --> rendered: CreateMintOrder FAILED
6.7 新增表:laser_card_operation_logs
CREATE TABLE laser_card_operation_logs (
id BIGSERIAL PRIMARY KEY,
instance_id BIGINT NOT NULL,
instance_no VARCHAR(32) NOT NULL,
operator_user_id BIGINT NOT NULL,
action VARCHAR(50) NOT NULL,
status_before VARCHAR(20),
status_after VARCHAR(20),
request_id VARCHAR(64),
payload_json JSONB,
result_json JSONB,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
latency_ms INT,
err_code VARCHAR(32),
created_at BIGINT NOT NULL,
CONSTRAINT fk_lclog_instance
FOREIGN KEY (instance_id) REFERENCES laser_card_instances(id) ON DELETE CASCADE
);
CREATE INDEX idx_lclog_instance_time
ON laser_card_operation_logs(instance_id, created_at DESC);
CREATE INDEX idx_lclog_operator_time
ON laser_card_operation_logs(operator_user_id, created_at DESC);
CREATE INDEX idx_lclog_action_time
ON laser_card_operation_logs(action, created_at DESC);
| action | 触发接口 | status 变化 |
|---|---|---|
create_instance |
POST /laser/instances |
→ rendered |
generate_variants |
Thinking 页五图完成(可选记日志) | — |
mint_start |
POST .../mint |
rendered → minting |
mint_success |
铸造事务提交成功 | minting → minted |
mint_fail |
铸造失败 | minting → rendered |
6.8 materials_snapshot JSON Schema
铸造前写入 laser_card_instances.materials_snapshot,数组长度 3–4(无抠图时无 cutout 项):
[
{
"role": "source",
"oss_key": "laser-card/1001/42/20260525/uuid_source.jpg",
"hash": "sha256...",
"width": 1080,
"height": 1440,
"mime_type": "image/jpeg",
"file_size": 245760,
"preset_id": null
},
{
"role": "cutout",
"oss_key": "laser-card/1001/42/20260525/uuid_cutout.png",
"hash": "sha256...",
"width": 1080,
"height": 1440,
"mime_type": "image/png",
"file_size": 180000,
"preset_id": null
},
{
"role": "backdrop",
"oss_key": "static/laser-bg/laser-bg-1.png",
"hash": "static...",
"preset_id": "liquidBlue"
},
{
"role": "composite",
"oss_key": "laser-card/1001/42/20260525/uuid_variant_holoFull.jpg",
"hash": "sha256...",
"width": 1080,
"height": 1440,
"preset_id": "holoFull"
}
]
| snapshot 字段 | 必填 | 说明 |
|---|---|---|
role |
Y | source | cutout | backdrop | composite |
oss_key |
Y | OSS 对象键 |
hash |
Y | 文件哈希 |
width / height |
图必填 | 像素尺寸 |
preset_id |
N | 仅 backdrop / composite 关联模板预设 |
6.9 跨表生命周期(数据落库顺序)
sequenceDiagram
participant FE as 前端
participant OSS as OSS
participant LCI as laser_card_instances
participant MAT as materials
participant AMR as asset_material_relations
participant AST as assets
participant MO as mint_orders
FE->>OSS: 上传 source/cutout/backdrop/composite
FE->>LCI: INSERT status=rendered, materials_snapshot=[...]
Note over LCI: materials 表尚无记录
FE->>MO: InitMintOrder (既有)
FE->>LCI: UPDATE status=minting, mint_order_id
FE->>AST: CreateMintOrder (既有)
loop 每条 snapshot.role
FE->>MAT: UploadMaterial(oss_key) → material_id
FE->>AMR: BindAssetMaterials(asset_id, type, layer_order)
end
FE->>LCI: UPDATE status=minted, asset_id, composite_material_id
| 阶段 | 写入表 | 关键字段 |
|---|---|---|
| 选卡确认 | laser_card_instances |
status=rendered, materials_snapshot, composite_oss_key, render_config |
| 发起铸造 | laser_card_instances + mint_orders |
status=minting, mint_order_id |
| 铸造成功 | assets + materials × N + asset_material_relations × N |
instances.asset_id, instances.composite_material_id |
| 完成 | laser_card_instances |
status=minted |
| 全程 | laser_card_operation_logs |
每个写操作追加一行 |
6.10 OSS 路径与表字段映射
| 文件角色 | OSS 路径模式 | 写入 snapshot.role | 铸造后 material_type |
|---|---|---|---|
| 用户原图 | laser-card/{star_id}/{user_id}/{yyyyMMdd}/{uuid}_source.jpg |
source |
source |
| 抠图 PNG | laser-card/.../{uuid}_cutout.png |
cutout |
cutout |
| 内置底纹 | static/laser-bg/laser-bg-{n}.png |
backdrop |
backdrop |
| 选中合成图 | laser-card/.../{uuid}_composite.jpg |
composite |
main |
| 模板缩略图 | laser-card/templates/{code}/thumb.jpg |
— | — |
禁止: 在 PostgreSQL 存 BLOB / base64;payload_json 禁止存大图。
6.11 迁移脚本与索引汇总
迁移文件(已落库):
| 文件 | 用途 |
|---|---|
backend/scripts/migrations/migrate_laser_card_v3_tables.sql |
三表 DDL + 全量 COMMENT + 5 条模板种子 |
backend/scripts/migrations/migrate_laser_card_v3_comments.sql |
仅备注片段(被主迁移 \ir 引用) |
backend/scripts/migrations/migrate_laser_card_v3_comments_only.sql |
已建表时单独补备注 |
backend/scripts/migrations/migrate_laser_card_tags_optional.sql |
可选 历史 assets.tags 补 cast:star_card(无 DDL) |
-- 1. laser_card_templates
-- 2. laser_card_instances (+ FK + CHECK)
-- 3. laser_card_operation_logs (+ FK)
-- 4. INSERT 5 rows INTO laser_card_templates (附录 A)
-- 5. pkg/models 新增 MaterialTypeSource / Cutout / Backdrop 常量
-- 注意:本期不对 assets / asset_registry 做 ALTER(品类与工艺走 tags 约定,§6.3.1)
本期新增索引一览:
| 表 | 索引名 | 列 | 类型 |
|---|---|---|---|
laser_card_templates |
uk_lct_code_version |
template_code, version |
UNIQUE (partial) |
laser_card_templates |
idx_lct_status_star |
status, star_id |
INDEX (partial) |
laser_card_instances |
uk_lci_instance_no |
instance_no |
UNIQUE |
laser_card_instances |
uk_lci_instance_ulid |
instance_ulid |
UNIQUE |
laser_card_instances |
uk_lci_user_client_req |
owner_user_id, client_request_id |
UNIQUE (partial) |
laser_card_instances |
idx_lci_owner_status |
owner_user_id, status |
INDEX (partial) |
laser_card_instances |
idx_lci_asset |
asset_id |
INDEX (partial) |
laser_card_operation_logs |
idx_lclog_instance_time |
instance_id, created_at DESC |
INDEX |
6.12 Phase 2 可选表(Dify 五图任务,本期不建)
若服务端生成五图上线,可另立迁移增加 laser_card_generate_jobs(job_id、owner_user_id、status、variant_urls JSONB),与 instances 通过 generate_job_id 关联。本期不实施,不写入迁移脚本。
七、后端 API 与其它详细设计
7.1 接口一览(6 个,Phase 1)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/laser/templates |
预设列表(含 render_config) |
| GET | /api/v1/laser/templates/:code |
预设详情 |
| POST | /api/v1/laser/instances |
选卡并确认素材后创建实例 |
| POST | /api/v1/laser/instances/:id/mint |
铸造 |
| GET | /api/v1/laser/instances/:id |
实例详情 |
| POST | /api/v1/segment |
魔搭抠图代理 |
Phase 2 追加: POST/GET /api/v1/laser/generate[/{job_id}](Dify 五图任务)
7.2 选卡页铸造流程(对齐光栅 lenticular-result)
sequenceDiagram
participant R as laser-result.vue
participant API as Gateway
participant Mint as craftMintSubmit
R->>R: 用户选中 variant[index]
R->>API: estimateMintCostApi
R->>R: ConfirmModal 展示水晶
R->>R: 上传 source + cutout + backdrop + composite
R->>API: POST /laser/instances
R->>Mint: submitCraftMintFromPath 扩展多素材
Note over Mint: main=composite<br/>source/cutout/backdrop 额外 bind
Mint->>API: createMintOrder + bindAssetMaterials
submitCraftMintFromPath 扩展要点:
- 新增参数
laserMaterials: [{ role, oss_key, material_id? }] main= 用户选中的合成图- 铸造成功后
bindAssetMaterialsApi绑定 4 层 - 复用现有水晶扣费与
order_id幂等 createMintOrderApi请求体tags:必须含cast:star_card+craft:laser(来自CASTLOVE_FORM_KEY快照,§6.3.1);mint_service已支持req.Tags→asset.Tags,无需改表结构
7.3 页面职责摘要(细节见 §5.4、§5.5)
| 页面 | 职责 |
|---|---|
laser-thinking.vue |
useLaserBatchGenerate + LaserBatchCanvasHost;礼盒进度 UI;完成后跳 result |
laser-result.vue |
LaserVariantPyramid + LaserPreviewCanvas + useLaserMint;删除 createMintOrderApi 单图路径 |
展示原则(§5.4): 预览可动(WebGL)、铸造用静图(batch JPG)、无相册导出、无陀螺仪。
7.4 死代码清理(实施时必须删除)
| 项 | 处置 |
|---|---|
pages/castlove/laser-card-studio.vue |
删除(含 saveImageToPhotosAlbum 保存相册逻辑) |
pages.json 中 laser-card-studio 路由 |
删除 |
handleSingleCraftLaserEntry、CASTLOVE_LASER_ENTRY_KEY 写入 |
删除 |
castloveAfterLaserMint.js |
Phase 1 末删除(由扩展后的 craftMintSubmit 替代) |
aliyunPortraitUni.js、segmentApi.js AK、segmentationCloud.js direct 分支 |
删除 |
components/lenticular/HolographicCard.vue |
删除 |
lenticular-thinking.vue 误引 laserBatchExport |
删除误引 |
discover/generation-loading 镭射分支 |
迁出后删除分支,仅保留 API/星卡模式 |
八、分阶段实施计划(14 天)
| 阶段 | 时间 | 交付 |
|---|---|---|
| Phase 1 | 第 1–10 天 | 魔搭 Segment 代理;三表 + 实例/铸造 API;laser-thinking / laser-result 迁目录;craftMintSubmit 多素材 + tags 双写(§6.3.1);下线工坊;客户端五图 |
| Phase 2 | 第 11–14 天 | Dify 五图工作流 + /laser/generate 接口;灰度切换客户端/服务端生成;死代码清零;监控告警 |
| 里程碑 | 日期 |
|---|---|
| 后端接口可联调 | D5 |
| 选卡 → 多素材铸造 E2E | D10 |
| 灰度 10% | D12 |
| 全量 + 删工坊 | D14 |
灰度: feature_flag_laser_v3 按 user_id 尾号;关闭时回退旧五图+单图铸造(仅灰度期,全量后删旧逻辑)。
九、风险与应对
| 风险 | 等级 | 应对 |
|---|---|---|
| 魔搭额度/稳定性 | 中 | 椭圆降级;购买额度包;超时 2s 重试 |
| 客户端/服务端五图不一致 | 中(Phase 2) | Phase 2 灰度对比;验收截图 diff |
| 选卡页改造引入回归 | 中 | 与光栅共用 ConfirmModal;E2E 用例 |
| 删除工坊误伤入口 | 低 | 全局搜 laser-card-studio;产品确认无工坊入口 |
| Dify 工作流延迟 | 低(P2) | 异步 job + 轮询;Loading 最短展示 1.6s |
十、验收标准
10.1 功能
- create 上传 → Thinking → 五图展示 → 选卡 → 费用确认 → 铸造成功
- 魔搭抠图成功/失败椭圆降级
- 铸造后
GetAssetMaterials含 main/source/cutout/backdrop - 铸造成功后
assets.tags含cast:star_card与craft:laser - 无
laser-card-studio路由可访问 - 光栅流程无回归
10.2 性能
- 五图生成(Phase 1 中端机)< 8s
- 抠图 P95 < 3s
- 选卡页滑动流畅
10.3 安全
- 正式包无 AK
/segment鉴权 + 限流生效
10.4 规范
- 页面位于
pages/castlove/laser/*,单页 < 200 行 - 镭射逻辑仅在
components/laser/+composables/useLaser*,discover/generation-*无镭射分支 - 无
saveImageToPhotosAlbum、无laser-card-studio路由 - 五图 Phase 1 无后端生成接口(仅
canvasToTempFilePath) - 无
laser_card_instance_materials表 - 无私写
materialsSQL - 未 为品类/工艺新增
cast_category/craft_type列;仅用tags约定(§6.3.1)
十一、附录
A. Phase 1 五图预设与模板种子映射
| 序号 | PRESET_VARIANTS.style | template_code | 底纹 backdrop |
|---|---|---|---|
| 0 | dream | dream | liquidBlue |
| 1 | classic | classic | liquidLavender |
| 2 | holoFull | holoFull | liquidPearl |
| 3 | ice | ice | liquidBlue |
| 4 | sunset | sunset | liquidLavender |
种子 SQL 示例(每条需补全 render_config / backdrop_options JSON):
INSERT INTO laser_card_templates (
template_code, name, status, version,
backdrop_options, render_config, engine_min_version,
sort_order, star_id, created_by, created_at, updated_at
) VALUES
('dream', '梦幻', 'published', 1, '[{"id":"liquidBlue","oss_key":"static/laser-bg/laser-bg-1.png"}]', '{}', 'compositor-1.0.0', 0, 0, 0, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('classic', '经典', 'published', 1, '[{"id":"liquidLavender","oss_key":"static/laser-bg/laser-bg-2.png"}]', '{}', 'compositor-1.0.0', 1, 0, 0, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('holoFull', '全息', 'published', 1, '[{"id":"liquidPearl","oss_key":"static/laser-bg/laser-bg-3.png"}]', '{}', 'compositor-1.0.0', 2, 0, 0, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('ice', '冰蓝', 'published', 1, '[{"id":"liquidBlue","oss_key":"static/laser-bg/laser-bg-1.png"}]', '{}', 'compositor-1.0.0', 3, 0, 0, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('sunset', '晚霞', 'published', 1, '[{"id":"liquidLavender","oss_key":"static/laser-bg/laser-bg-2.png"}]', '{}', 'compositor-1.0.0', 4, 0, 0, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000);
B. 参考代码索引
| 文件 | 说明 |
|---|---|
frontend/utils/castloveGenerationFlow.js |
startCraftGenerationFlow、persistLaserPreviewImages |
frontend/utils/laser-card/laserBatchExport.js |
Phase 1 五图合成(§5.5) |
frontend/utils/laser-card/laserPreviewWebgl.js |
动态预览着色器 |
frontend/pages/discover/generation-loading.vue |
迁出 runLaserFlow → laser-thinking |
frontend/pages/discover/generation-result.vue |
迁出 金字塔 UI → LaserVariantPyramid |
frontend/pages/castlove/laser-card-studio.vue |
删除(WebGL 逻辑迁入 useLaserPreview) |
frontend/pages/castlove/lenticular/lenticular-result.vue |
铸造确认交互基线 |
frontend/utils/craftMintSubmit.js |
多素材铸造基线 |
frontend/utils/castloveMintForm.js |
CAST_TAG_* / CRAFT_TAG_*、buildCastloveFormSnapshot(§6.3.1) |
frontend/composables/useLaserMint.js |
镭射估价 + 铸造;须透传 tags |
docs/specs/2026-04-07-minimax-image-generation-design.md |
星卡 AI 四图(镭射不用) |
C. 前端组件迁移步骤(建议顺序)
| 步 | 任务 | 产出 |
|---|---|---|
| 1 | 抽出 utils/laser-card/laserPresets.js(PRESET_VARIANTS) |
预设单源 |
| 2 | useLaserBatchGenerate + LaserBatchCanvasHost |
Thinking 可生成五图 |
| 3 | 新建 laser-thinking.vue、pages.json;castloveGenerationFlow 改跳转 |
脱离 discover/loading |
| 4 | useLaserPreview + LaserPreviewCanvas(从 studio 迁移 RAF/WebGL) |
动态预览组件 |
| 5 | LaserVariantPyramid + useLaserResultSelection |
金字塔选卡 |
| 6 | laser-result.vue + useLaserMint |
多素材铸造 E2E;tags: cast:star_card + craft:laser(§6.3.1) |
| 6b | castloveMintForm.js:补 CAST_TAG_STAR_CARD、CRAFT_TAG_LASER;光栅快照补 cast:star_card |
tags 双轨对齐 |
| 7 | 删除 generation-* 镭射分支、laser-card-studio、死代码 |
单轨维护 |
| 8 | Phase 2:aiWorkflowService + useLaserBatchGenerate Dify 分支 |
可选服务端五图 |
文档结束。 评审通过后,实施任务可拆至 docs/superpowers/plans/2026-05-25-laser-card-refactor-plan.md。