topfans/docs/specs/2026-05-25-laser-card-refactor-design.md

1390 lines
61 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.

# 镭射卡功能重构技术方案(总方案)
> **文档版本:** 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 WorkflowPhase 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
```mermaid
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
```mermaid
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
→ 结果写入用户 OSSlaser-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**
```mermaid
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 }]` |
**对外 APIPhase 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或 DifyPhase 2 | **是** |
镭射卡**不**走 MiniMax `imageGenerationApi`,避免与星卡共用 Loading 页时逻辑混淆Phase 1 镭射**不再进入** `discover/generation-loading`(见 §5.4)。
### 3.5 AI 编排服务aiWorkflowServicePhase 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。
```mermaid
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** HTTPGo 复刻 `laserBatchExport`**不用**魔搭通用图生图替代 compositor效果不可控见评审结论
**工作流类型:** WorkflowAPI 触发,非 Chatbot
**开始节点 — 输入变量:**
| 变量 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `source_image_url` | string | ✅ | 用户原图可访问 URLOSS 签名) |
| `use_cutout` | boolean | ✅ | 是否人像抠图 |
| `preset_codes` | array[string] | 否 | 默认 `["dream","classic","holoFull","ice","sunset"]` |
| `star_id` / `user_id` | number | ✅ | 隔离与审计 |
| `export_width` / `export_height` | number | 否 | 默认 1080 / 1440 |
**节点链路:**
```mermaid
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**
```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 强制原则
1. 镭射**禁止**新增 `laser-card-studio.vue` 路由与 pages.json 注册
2. **`discover/generation-loading`、`generation-result` 不得再含镭射 `v-if` 分支**;镭射仅走 `pages/castlove/laser/*`
3. 页面**薄编排**`laser-thinking` / `laser-result`**< 200 **业务在 `components/laser` + composable
4. 素材绑定**必须** `uploadMaterialApi` + `bindAssetMaterialsApi`
5. 选卡页铸造**必须**与光栅一致`estimateMintCostApi` `ConfirmModal` `submitCraftMintFromPath`
6. **禁止** `uni.saveImageToPhotosAlbum` §1.4
7. 铸造时 `assets.tags` 写入 **`cast:star_card`**铸爱大类+ **`craft:laser`**工艺小类**禁止**为品类/工艺新增 `cast_category` / `craft_type` §6.3.1
8. `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 组件数据流
```mermaid
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 客户端 batch
- `true``POST /api/v1/ai/runs` + 轮询 `variants[].signed_url` 写入 `GENERATED_IMAGES_KEY`
---
## 六、数据库设计(完整)
> **设计原则**
> 1. **本期仅新增 3 张镭射业务表**;素材主数据与资产绑定**完全复用**既有 `materials` + `asset_material_relations`。
> 2. **禁止**新增 `laser_card_instance_materials` 中间表;铸造前临时素材仅存 `laser_card_instances.materials_snapshot`JSONB
> 3. **禁止**在 `laser_card_service` 内私写 `materials` / `asset_material_relations` SQL铸造成功后统一走既有 `MaterialService.UploadMaterial` + `BindAssetMaterials` RPC。
> 4. 旧版单图镭射资产仍可读 `assets.material_url`;新资产以 `asset_material_relations` 为准。
> 5. **品类(星卡/吧唧/海报)与工艺(镭射/光栅等)本期不新增 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 keyhash尺寸 |
| `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 关系
```mermaid
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` 示例:**
```json
["cast:star_card", "craft:laser"]
```
**光栅卡对照:**
```json
["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`**
```javascript
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与画廊光栅判断一致**
```sql
-- 是否镭射工艺
(a.tags @> '["craft:laser"]') AS is_laser
-- 是否星卡大类下的藏品(可选,用于广场/星册筛大类)
(a.tags @> '["cast:star_card"]') AS is_star_card_cast
```
**本期不强制** `tags` GIN 索引 `cast:*` / `craft:*` 筛选 QPS 升高再评估
```sql
CREATE INDEX idx_assets_tags_gin ON assets USING GIN (tags) WHERE deleted_at IS NULL;
```
**明确不做:**
- 不在 `asset_registry` 冗余 `cast_category`避免双写星册按大类筛时 JOIN `assets` 或读 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`(已上线,本期扩展约定即可)
```sql
-- 来源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`(素材主表,已上线)
```sql
-- 来源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`(资产-素材关联,已上线)
```sql
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
```sql
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`
```sql
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` 已回填 | 只读 |
```mermaid
stateDiagram-v2
[*] --> rendered: POST /laser/instances
rendered --> minting: POST /mint
minting --> minted: CreateMintOrder SUCCESS
minting --> rendered: CreateMintOrder FAILED
```
### 6.7 新增表:`laser_card_operation_logs`
```sql
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`**数组长度 34**无抠图时无 `cutout`
```json
[
{
"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 跨表生命周期(数据落库顺序)
```mermaid
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 |
```sql
-- 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`
```mermaid
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** | 110 | 魔搭 Segment 代理三表 + 实例/铸造 API`laser-thinking` / `laser-result` 迁目录`craftMintSubmit` 多素材 + **`tags` 双写**(§6.3.1下线工坊客户端五图 |
| **Phase 2** | 1114 | Dify 五图工作流 + `/laser/generate` 接口灰度切换客户端/服务端生成死代码清零监控告警 |
| 里程碑 | 日期 |
|---|---|
| 后端接口可联调 | D5 |
| 选卡 多素材铸造 E2E | D10 |
| 灰度 10% | D12 |
| 全量 + 删工坊 | D14 |
**灰度:** `feature_flag_laser_v3` `user_id` 尾号关闭时回退旧五图+单图铸造仅灰度期全量后删旧逻辑)。
---
## 九、风险与应对
| 风险 | 等级 | 应对 |
|---|---|---|
| 魔搭额度/稳定性 | | 椭圆降级购买额度包超时 2s 重试 |
| 客户端/服务端五图不一致 | Phase 2 | Phase 2 灰度对比验收截图 diff |
| 选卡页改造引入回归 | | 与光栅共用 ConfirmModalE2E 用例 |
| 删除工坊误伤入口 | | 全局搜 `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`
- [ ] 无私写 `materials` SQL
- [ ] **未** 为品类/工艺新增 `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**
```sql
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`