1390 lines
61 KiB
Markdown
1390 lines
61 KiB
Markdown
# 镭射卡功能重构技术方案(总方案)
|
||
|
||
> **文档版本:** 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)
|
||
|
||
```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)
|
||
→ 结果写入用户 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):**
|
||
|
||
```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 }]` |
|
||
|
||
**对外 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。
|
||
|
||
```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** 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 |
|
||
|
||
**节点链路:**
|
||
|
||
```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 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 关系
|
||
|
||
```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`,**数组长度 3–4**(无抠图时无 `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** | 第 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` 表
|
||
- [ ] 无私写 `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`。
|