# 镭射卡功能重构技术方案(总方案) > **文档版本:** 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** | 单张静态 `` | 动画 | | **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
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`。