821 lines
30 KiB
Markdown
821 lines
30 KiB
Markdown
# 资产铸造(AI 生成版)流程设计文档
|
||
|
||
## 📋 文档信息
|
||
|
||
- **版本**: v1.0
|
||
- **创建日期**: 2026-01-20
|
||
- **状态**: 待确认
|
||
- **作者**: 开发团队
|
||
|
||
---
|
||
|
||
## 📊 文档状态总结
|
||
|
||
### ✅ 已确认的设计
|
||
- ✅ 采用异步处理机制(AI 生成耗时,不阻塞用户请求)
|
||
- ✅ AI 生成的图片必须存储到 OSS(确保持久性)
|
||
- ✅ 使用预签名 URL 机制访问私有 OSS 资源
|
||
- ✅ 复用现有的 `Asset` 和 `MintOrder` 数据模型
|
||
- ✅ **AI 服务先使用 Mock 版本**,实现完整的前后端对接流程
|
||
- ✅ **铸造订单失败时自动退回水晶费用**(无论 AI 问题还是链问题)
|
||
- ✅ **所有图片 URL 自动生成预签名 URL**(在查询接口中)
|
||
- ✅ **前端轮询频率为 3 秒**
|
||
- ✅ **AI 生成失败时,前端查询返回失败消息**
|
||
- ✅ **暂不支持 WebSocket、进度提示、批量铸造**
|
||
|
||
### ⚠️ 后续优化项(暂不实现)
|
||
1. WebSocket 实时通知(当前使用轮询)
|
||
2. AI 生成进度百分比显示
|
||
3. 批量铸造功能(一次提交多个素材)
|
||
|
||
---
|
||
|
||
## 1. 业务概述
|
||
|
||
### 1.1 功能描述
|
||
用户通过上传原始图片(素材)并填写元数据信息,发起“铸造”请求。系统将:
|
||
1. 扣除用户的水晶余额作为铸造费用
|
||
2. 异步调用 AI 服务,基于原始素材生成一张新的艺术图片
|
||
3. 将 AI 生成的图片上传到 OSS 存储
|
||
4. 模拟区块链上链过程(生成交易哈希和模拟地址)
|
||
5. 更新资产状态为“已激活”,用户可在“我的藏品”中查看
|
||
|
||
### 1.2 核心特点
|
||
- **异步处理**:AI 生成耗时较长(通常 10-60 秒),采用后台异步处理,不阻塞用户请求
|
||
- **持久化存储**:AI 生成的图片必须转存到自己的 OSS,避免临时链接失效
|
||
- **状态可追踪**:用户可通过 `order_id` 实时查询铸造进度
|
||
- **安全访问**:私有 OSS 资源通过预签名 URL 机制访问
|
||
|
||
---
|
||
|
||
## 2. 系统架构
|
||
|
||
### 2.1 参与角色
|
||
| 角色 | 职责 |
|
||
| :--- | :--- |
|
||
| **前端 (FE)** | 用户交互、图片上传、状态展示、结果呈现 |
|
||
| **网关 (GW)** | 鉴权、限流、请求转发 |
|
||
| **资产服务 (AssetService)** | 订单管理、资产状态更新、异步任务调度 |
|
||
| **用户服务 (UserService)** | 水晶余额校验与扣减 |
|
||
| **AI 代理层 (AI Task)** | 对接 AI 接口、图片转存 OSS、模拟上链 |
|
||
| **阿里云 OSS** | 存储原始素材图和 AI 生成的目标图 |
|
||
|
||
### 2.2 数据流向
|
||
```
|
||
用户上传素材 → OSS (material_url)
|
||
↓
|
||
创建铸造订单 → 数据库 (Asset + MintOrder)
|
||
↓
|
||
异步 AI 处理 → AI 服务 → 下载图片 → 上传 OSS (cover_url)
|
||
↓
|
||
更新数据库 → 状态变更 (Pending → Active)
|
||
↓
|
||
前端轮询查询 → 返回结果(含预签名 URL)
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 详细交互流程
|
||
|
||
### 3.1 完整流程图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
participant 用户
|
||
participant 前端
|
||
participant 网关
|
||
participant 资产服务
|
||
participant 用户服务
|
||
participant 异步任务(AI+OSS)
|
||
participant AI服务
|
||
participant OSS存储
|
||
|
||
Note over 用户, OSS存储: 第一阶段:素材准备
|
||
用户->>前端: 选择图片并填写信息
|
||
前端->>网关: GET /api/v1/assets/oss/upload-signature?type=asset
|
||
网关-->>前端: 返回STS临时凭证 + 目录路径
|
||
前端->>OSS存储: 直传素材图 (POST Object)
|
||
OSS存储-->>前端: 上传成功,返回 material_url
|
||
|
||
Note over 用户, OSS存储: 第二阶段:发起铸造 (同步,立即返回)
|
||
前端->>网关: POST /api/v1/assets/mints
|
||
Note right of 前端: {<br/> "name": "资产名称",<br/> "material_url": "...",<br/> "description": "描述"<br/>}
|
||
网关->>资产服务: CreateMintOrder (RPC)
|
||
资产服务->>用户服务: 扣除铸造费用 (UpdateCrystalBalance)
|
||
用户服务-->>资产服务: 扣费成功
|
||
资产服务->>资产服务: DB事务:创建 Asset (Status=0) + Order (Status=PROCESSING)
|
||
资产服务-->>网关: 返回 order_id
|
||
网关-->>前端: 返回 200 OK { order_id, status: "PROCESSING" }
|
||
前端->>用户: 界面显示 "AI创作中,请稍候..."
|
||
|
||
Note over 用户, OSS存储: 第三阶段:异步生成 (后台处理)
|
||
rect rgb(240, 240, 240)
|
||
资产服务->>异步任务(AI+OSS): 启动 Goroutine 处理
|
||
alt AI处理成功
|
||
异步任务(AI+OSS)->>AI服务: 调用AI接口 (输入: material_url) [Mock版本]
|
||
AI服务-->>异步任务(AI+OSS): 返回AI生成图片 (临时URL或Base64)
|
||
异步任务(AI+OSS)->>异步任务(AI+OSS): 下载AI图片到内存
|
||
异步任务(AI+OSS)->>OSS存储: 上传到 OSS (asset/{user_id}/{star_id}/covers/{asset_id}.png)
|
||
OSS存储-->>异步任务(AI+OSS): 返回 cover_url
|
||
异步任务(AI+OSS)->>异步任务(AI+OSS): 生成模拟 TxHash (0x...)
|
||
异步任务(AI+OSS)->>资产服务: 更新 DB: Asset (Status=1, CoverURL, TxHash)
|
||
资产服务->>资产服务: 更新 Order 为 SUCCESS
|
||
else AI处理失败
|
||
异步任务(AI+OSS)->>资产服务: 更新 Order 为 FAILED + error_message
|
||
资产服务->>用户服务: 退回水晶费用 (UpdateCrystalBalance)
|
||
用户服务-->>资产服务: 退费成功
|
||
end
|
||
end
|
||
|
||
Note over 用户, OSS存储: 第四阶段:结果获取 (轮询/展示)
|
||
loop 状态轮询 (每3秒)
|
||
前端->>网关: GET /api/v1/assets/mints/:order_id
|
||
网关->>资产服务: 查询订单状态 (GetMintOrder)
|
||
资产服务-->>前端: 返回 Status=SUCCESS, cover_url=...
|
||
end
|
||
alt 状态为SUCCESS
|
||
资产服务-->>前端: 返回 Status=SUCCESS, cover_url_signed=... (已自动生成预签名URL)
|
||
前端->>用户: 动画展示生成的藏品 & 模拟链上地址
|
||
else 状态为FAILED
|
||
资产服务-->>前端: 返回 Status=FAILED, error_message="AI生成失败,已退回费用"
|
||
前端->>用户: 显示失败提示 & 费用已退回消息
|
||
end
|
||
```
|
||
|
||
### 3.2 阶段说明
|
||
|
||
#### 阶段一:素材准备(前端完成)
|
||
1. 用户在前端选择图片并填写元数据(名称、描述等)
|
||
2. 前端调用 `GET /api/v1/assets/oss/upload-signature?type=asset` 获取 OSS 上传凭证
|
||
3. 前端使用临时凭证直接上传图片到 OSS(路径:`asset/{user_id}/{star_id}/materials/{filename}`)
|
||
4. 前端获得 `material_url`(原始素材的 OSS 地址)
|
||
|
||
#### 阶段二:发起铸造(同步,立即返回)
|
||
1. 前端调用 `POST /api/v1/assets/mints`,传入 `material_url` 和元数据
|
||
2. 后端在事务中完成:
|
||
- 扣除用户水晶余额(调用 UserService)
|
||
- 创建 `Asset` 记录(`Status = 0 (Pending)`,`MaterialURL` 已设置,`CoverURL` 为空)
|
||
- 创建 `MintOrder` 记录(`Status = PROCESSING`)
|
||
3. 立即返回 `order_id`,不等待 AI 处理
|
||
|
||
#### 阶段三:异步 AI 处理(后台 Goroutine)
|
||
1. 启动独立的 Goroutine 处理 AI 生成任务
|
||
2. 调用 AI 服务接口(输入:`material_url`)
|
||
- **Mock 版本**:随机选择预设图片或直接复制 `material_url` 的图片
|
||
3. AI 服务返回生成的图片(可能是临时 URL 或 Base64)
|
||
4. 下载 AI 生成的图片到内存
|
||
5. 上传到 OSS(路径:`asset/{user_id}/{star_id}/covers/{asset_id}_{timestamp}.png`)
|
||
6. 生成模拟链上信息:
|
||
- `TxHash`: `0x` + 64位随机16进制字符串
|
||
- `BlockNumber`: 随机生成
|
||
- `MintedAt`: 当前时间戳
|
||
7. 更新数据库:
|
||
- `Asset.Status = 1 (Active)`
|
||
- `Asset.CoverURL = <OSS地址>`
|
||
- `Asset.TxHash = <模拟哈希>`
|
||
- `MintOrder.Status = SUCCESS`
|
||
|
||
**失败处理**(AI 超时、AI 调用失败、OSS 上传失败等):
|
||
1. 记录错误信息到 `MintOrder.error_message`
|
||
2. 更新订单状态为 `FAILED`
|
||
3. **自动退回水晶费用**:调用 `UserService.UpdateCrystalBalance`,退回 `cost_crystal` 金额
|
||
4. 记录日志,便于后续排查
|
||
|
||
#### 阶段四:结果获取(前端轮询)
|
||
1. 前端每 3 秒轮询一次 `GET /api/v1/assets/mints/:order_id`
|
||
2. **成功场景**:当 `status = SUCCESS` 时
|
||
- 前端获取到 `cover_url_signed`(后端已自动生成预签名 URL)
|
||
- 展示生成的藏品图片和模拟链上地址
|
||
3. **失败场景**:当 `status = FAILED` 时
|
||
- 前端获取到 `error_message` 字段
|
||
- 展示失败提示:"AI生成失败,已退回水晶费用"
|
||
- 用户可重新提交订单
|
||
|
||
---
|
||
|
||
## 4. API 接口定义
|
||
|
||
### 4.1 创建铸造订单
|
||
|
||
**Endpoint**: `POST /api/v1/assets/mints`
|
||
|
||
**认证**: 需要 JWT Token(从 Header 中获取 `user_id` 和 `star_id`)
|
||
|
||
**Request Body**:
|
||
```json
|
||
{
|
||
"name": "我的数字艺术藏品",
|
||
"material_url": "https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/7/87/materials/original.jpg",
|
||
"description": "这是由AI生成的数字艺术品",
|
||
"rarity": 1,
|
||
"tags": ["AI", "Art", "Digital"]
|
||
}
|
||
```
|
||
|
||
**字段说明**:
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| `name` | string | ✅ | 资产名称 |
|
||
| `material_url` | string | ✅ | 原始素材的 OSS URL(前端已上传到 `asset/{user_id}/{star_id}/materials/` 目录) |
|
||
| `description` | string | ❌ | 资产描述 |
|
||
| `rarity` | int32 | ❌ | 稀有度(预留字段) |
|
||
| `tags` | string[] | ❌ | 标签数组 |
|
||
|
||
**重要说明**:
|
||
- ✅ `material_url` 是必填字段,前端必须在调用此接口前完成素材上传
|
||
- ✅ `cover_url` 不需要前端传入,由后端 AI 处理完成后自动生成
|
||
- ✅ 创建订单时,`Asset.CoverURL` 字段为空,等待异步任务完成后回填
|
||
|
||
**Response (200 OK)**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"order_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"asset_id": 12345,
|
||
"status": "PROCESSING",
|
||
"created_at": 1705747200000
|
||
}
|
||
}
|
||
```
|
||
|
||
**错误响应**:
|
||
- `400 Bad Request`: 参数错误(缺少必填字段、URL 格式错误等)
|
||
- `401 Unauthorized`: 未授权(Token 无效)
|
||
- `402 Payment Required`: 水晶余额不足
|
||
- `500 Internal Server Error`: 服务器内部错误
|
||
|
||
---
|
||
|
||
### 4.2 查询铸造订单状态
|
||
|
||
**Endpoint**: `GET /api/v1/assets/mints/:order_id`
|
||
|
||
**认证**: 需要 JWT Token(只能查询自己的订单)
|
||
|
||
**URL 参数**:
|
||
- `order_id`: 订单 ID(UUID 格式)
|
||
|
||
**Response (200 OK) - 成功状态**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"order_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"asset_id": 12345,
|
||
"status": "SUCCESS",
|
||
"cover_url": "https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/7/87/covers/12345_1705747200.png",
|
||
"cover_url_signed": "https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/7/87/covers/12345_1705747200.png?Expires=...&Signature=...",
|
||
"tx_hash": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
|
||
"minted_at": 1705747260000,
|
||
"created_at": 1705747200000,
|
||
"updated_at": 1705747260000
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response (200 OK) - 处理中状态**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"order_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"asset_id": 12345,
|
||
"status": "PROCESSING",
|
||
"cover_url": null,
|
||
"cover_url_signed": null,
|
||
"tx_hash": null,
|
||
"minted_at": null,
|
||
"created_at": 1705747200000,
|
||
"updated_at": 1705747200000
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response (200 OK) - 失败状态**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"order_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"asset_id": 12345,
|
||
"status": "FAILED",
|
||
"error_message": "AI生成超时,已自动退回水晶费用",
|
||
"cover_url": null,
|
||
"cover_url_signed": null,
|
||
"tx_hash": null,
|
||
"minted_at": null,
|
||
"created_at": 1705747200000,
|
||
"updated_at": 1705747300000
|
||
}
|
||
}
|
||
```
|
||
|
||
**状态值说明**:
|
||
- `PROCESSING`: AI 生成中(前端继续轮询)
|
||
- `SUCCESS`: 生成成功(前端展示结果)
|
||
- `FAILED`: 生成失败(前端显示 `error_message`,提示用户费用已退回)
|
||
|
||
**关键设计点**:
|
||
- ✅ **自动预签名**:`status = SUCCESS` 时,自动返回 `cover_url_signed` 预签名 URL
|
||
- ✅ **失败信息返回**:`status = FAILED` 时,返回 `error_message` 字段,前端直接展示
|
||
- ✅ **费用已退回**:失败时,后端已自动退回水晶费用,前端无需额外处理
|
||
|
||
**错误响应**:
|
||
- `404 Not Found`: 订单不存在或不属于当前用户
|
||
- `401 Unauthorized`: 未授权
|
||
|
||
---
|
||
|
||
### 4.3 获取我的藏品列表
|
||
|
||
**Endpoint**: `GET /api/v1/assets/me`
|
||
|
||
**认证**: 需要 JWT Token
|
||
|
||
**Query 参数**:
|
||
- `page`: 页码(默认 1)
|
||
- `page_size`: 每页数量(默认 20,最大 100)
|
||
- `status`: 筛选状态(可选:`pending`, `active`,默认返回全部)
|
||
|
||
**Response (200 OK)**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"total": 50,
|
||
"page": 1,
|
||
"page_size": 20,
|
||
"items": [
|
||
{
|
||
"asset_id": 12345,
|
||
"name": "我的数字艺术藏品",
|
||
"cover_url": "https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/7/87/covers/12345_1705747200.png",
|
||
"cover_url_signed": "https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/7/87/covers/12345_1705747200.png?Expires=...&Signature=...",
|
||
"status": "active",
|
||
"tx_hash": "0x1a2b3c4d...",
|
||
"minted_at": 1705747260000,
|
||
"like_count": 10,
|
||
"created_at": 1705747200000
|
||
},
|
||
{
|
||
"asset_id": 12346,
|
||
"name": "另一个藏品",
|
||
"cover_url": null,
|
||
"cover_url_signed": null,
|
||
"status": "pending",
|
||
"tx_hash": null,
|
||
"minted_at": null,
|
||
"like_count": 0,
|
||
"created_at": 1705747300000
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键设计点**:
|
||
- ✅ **自动预签名**:后端在返回数据前,自动为所有 `cover_url` 和 `material_url` 生成预签名 URL,放入 `*_signed` 字段
|
||
- ✅ **状态区分**:`status = pending` 时,`cover_url` 为 `null`,前端显示“生成中”占位图
|
||
- ✅ **状态筛选**:支持按状态筛选,方便用户查看“进行中”或“已完成”的藏品
|
||
- ✅ **失败状态展示**:`status = failed` 的藏品也会出现在列表中,`cover_url` 为 `null`,前端可显示“生成失败”提示
|
||
|
||
---
|
||
|
||
### 4.4 获取藏品详情
|
||
|
||
**Endpoint**: `GET /api/v1/assets/:asset_id`
|
||
|
||
**认证**: 需要 JWT Token(只能查看自己的藏品或公开藏品)
|
||
|
||
**Response (200 OK)**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"asset_id": 12345,
|
||
"name": "我的数字艺术藏品",
|
||
"description": "这是由AI生成的数字艺术品",
|
||
"material_url": "https://.../materials/original.jpg",
|
||
"material_url_signed": "https://...?Expires=...&Signature=...",
|
||
"cover_url": "https://.../covers/12345_1705747200.png",
|
||
"cover_url_signed": "https://...?Expires=...&Signature=...",
|
||
"status": "active",
|
||
"tx_hash": "0x1a2b3c4d...",
|
||
"block_number": 12345678,
|
||
"minted_at": 1705747260000,
|
||
"like_count": 10,
|
||
"is_liked": false,
|
||
"created_at": 1705747200000
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 数据模型设计
|
||
|
||
### 5.1 Asset 表(资产表)
|
||
|
||
**现有字段复用**:
|
||
| 字段 | 类型 | 用途 | 说明 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| `id` | int64 | 资产ID | 主键 |
|
||
| `owner_uid` | int64 | 所有者ID | 外键关联 users 表 |
|
||
| `star_id` | int64 | 明星ID | 用于数据隔离 |
|
||
| `name` | string | 资产名称 | 用户填写 |
|
||
| `cover_url` | string | **AI生成图URL** | 异步处理完成后回填 |
|
||
| `material_url` | string | **原始素材URL** | 前端上传时设置 |
|
||
| `description` | text | 描述 | 用户填写 |
|
||
| `status` | int32 | 状态 | `0=Pending`, `1=Active` |
|
||
| `tx_hash` | string | 交易哈希 | 模拟生成(0x...) |
|
||
| `block_number` | int64 | 区块号 | 模拟生成 |
|
||
| `minted_at` | int64 | 上链时间 | 毫秒时间戳 |
|
||
| `like_count` | int32 | 点赞数 | 默认 0 |
|
||
| `created_at` | int64 | 创建时间 | 毫秒时间戳 |
|
||
| `updated_at` | int64 | 更新时间 | 毫秒时间戳 |
|
||
|
||
**关键字段映射**:
|
||
- ✅ `MaterialURL` → 用户上传的原始素材(前端已上传到 OSS)
|
||
- ✅ `CoverURL` → AI 生成的最终图片(异步任务完成后回填)
|
||
- ✅ `Status` → `0` 表示处理中,`1` 表示已完成
|
||
|
||
### 5.2 MintOrder 表(铸造订单表)
|
||
|
||
**现有字段复用**:
|
||
| 字段 | 类型 | 用途 | 说明 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| `order_id` | string | 订单ID | UUID,主键 |
|
||
| `user_id` | int64 | 用户ID | 外键关联 users 表 |
|
||
| `star_id` | int64 | 明星ID | 用于数据隔离 |
|
||
| `asset_id` | int64 | 资产ID | 外键关联 assets 表 |
|
||
| `status` | string | 订单状态 | `PROCESSING`, `SUCCESS`, `FAILED` |
|
||
| `cost_crystal` | int64 | 消耗水晶 | 铸造费用 |
|
||
| `error_message` | text | 错误信息 | 失败时记录原因 |
|
||
| `retry_count` | int32 | 重试次数 | 默认 0 |
|
||
| `created_at` | int64 | 创建时间 | 毫秒时间戳 |
|
||
| `updated_at` | int64 | 更新时间 | 毫秒时间戳 |
|
||
| `minted_at` | int64 | 完成时间 | 毫秒时间戳 |
|
||
|
||
---
|
||
|
||
## 6. 状态机设计
|
||
|
||
### 6.1 Asset 状态流转
|
||
|
||
```
|
||
[Pending (0)] ──AI处理成功──> [Active (1)]
|
||
│ │
|
||
└──AI处理失败──> [Pending (0)] ──┘
|
||
(保持Pending,记录错误信息)
|
||
```
|
||
|
||
**状态说明**:
|
||
- **Pending (0)**: 资产创建后,等待 AI 处理或处理失败
|
||
- **Active (1)**: AI 处理成功,资产可用(`CoverURL` 已设置)
|
||
|
||
### 6.2 MintOrder 状态流转
|
||
|
||
```
|
||
[PROCESSING] ──AI处理成功──> [SUCCESS]
|
||
│ │
|
||
└──AI处理失败──> [FAILED] ────┘
|
||
(记录 error_message)
|
||
```
|
||
|
||
**状态说明**:
|
||
- **PROCESSING**: 订单创建后,AI 处理中
|
||
- **SUCCESS**: AI 处理成功,资产已激活
|
||
- **FAILED**: AI 处理失败(超时、API 错误等)
|
||
|
||
---
|
||
|
||
## 7. 关键设计决策
|
||
|
||
### 7.1 异步处理机制
|
||
|
||
**决策**: 采用 Goroutine 异步处理 AI 生成任务,不阻塞用户请求。
|
||
|
||
**理由**:
|
||
- AI 生成通常需要 10-60 秒,同步等待会导致请求超时
|
||
- 用户体验更好:用户提交后立即获得 `order_id`,可继续其他操作
|
||
- 系统可扩展:后续可引入消息队列(如 RocketMQ)进行任务调度
|
||
|
||
**实现方式**:
|
||
```go
|
||
// 在 CreateMintOrder 中,创建订单后立即启动异步任务
|
||
go s.processAIGeneration(mintOrder.OrderID, asset.ID, req.MaterialUrl)
|
||
```
|
||
|
||
### 7.2 AI 图片必须存储到 OSS
|
||
|
||
**决策**: AI 服务返回的图片(临时 URL 或 Base64)必须下载并上传到自己的 OSS。
|
||
|
||
**理由**:
|
||
- AI 服务的临时 URL 通常有效期仅 1 小时,不转存会导致链接失效
|
||
- 统一管理:所有资产图片都在自己的 OSS 中,便于权限控制和 CDN 加速
|
||
- 数据安全:避免依赖第三方服务的稳定性
|
||
|
||
**实现方式**:
|
||
1. 调用 AI 服务,获取生成的图片(临时 URL 或 Base64)
|
||
2. 下载图片到内存(如果是 URL,使用 HTTP GET;如果是 Base64,解码)
|
||
3. 上传到 OSS(路径:`asset/{user_id}/{star_id}/covers/{asset_id}_{timestamp}.png`)
|
||
4. 将 OSS URL 回填到 `Asset.CoverURL`
|
||
|
||
### 7.3 预签名 URL 自动生成
|
||
|
||
**决策**: 在 `GetMyAssets`、`GetAssetDetail` 和 `GetMintOrder` 接口中,后端自动为**所有图片 URL** 生成预签名 URL。
|
||
|
||
**理由**:
|
||
- 减少前端请求次数:前端不需要为每张图片单独调用预签名接口
|
||
- 统一管理:后端统一控制访问权限和有效期
|
||
- 性能优化:批量生成预签名 URL,减少 OSS API 调用
|
||
- 用户体验:前端拿到数据即可直接展示图片,无需额外请求
|
||
|
||
**实现方式**:
|
||
```go
|
||
// 在 DTO 转换层,自动为所有图片 URL 生成预签名 URL
|
||
func ToAssetDTO(asset *models.Asset) *dto.AssetDTO {
|
||
dto := &dto.AssetDTO{
|
||
AssetID: asset.ID,
|
||
CoverURL: asset.CoverURL,
|
||
MaterialURL: asset.MaterialURL,
|
||
}
|
||
|
||
// 为 cover_url 生成预签名 URL(如果存在)
|
||
if asset.CoverURL != "" {
|
||
signedURL, _ := generatePresignedURL(asset.CoverURL, 3600)
|
||
dto.CoverURLSigned = signedURL
|
||
}
|
||
|
||
// 为 material_url 生成预签名 URL(如果存在)
|
||
if asset.MaterialURL != nil && *asset.MaterialURL != "" {
|
||
signedURL, _ := generatePresignedURL(*asset.MaterialURL, 3600)
|
||
dto.MaterialURLSigned = &signedURL
|
||
}
|
||
|
||
return dto
|
||
}
|
||
```
|
||
|
||
**适用范围**:
|
||
- ✅ `GET /api/v1/assets/me` - 藏品列表
|
||
- ✅ `GET /api/v1/assets/:asset_id` - 藏品详情
|
||
- ✅ `GET /api/v1/assets/mints/:order_id` - 订单状态查询(成功时)
|
||
|
||
### 7.4 模拟链上信息生成
|
||
|
||
**决策**: 在前期不引入链端 API 的情况下,后端模拟生成 `TxHash` 和 `BlockNumber`。
|
||
|
||
**实现方式**:
|
||
```go
|
||
// 生成模拟交易哈希
|
||
txHash := fmt.Sprintf("0x%x", rand.Int63()) // 0x + 64位16进制
|
||
|
||
// 生成模拟区块号
|
||
blockNumber := rand.Int63n(1000000) + 1000000
|
||
|
||
// 设置上链时间
|
||
mintedAt := time.Now().UnixMilli()
|
||
```
|
||
|
||
**后续扩展**: 当引入真实区块链时,只需替换此处的模拟逻辑为真实的链端 SDK 调用。
|
||
|
||
---
|
||
|
||
## 8. 错误处理与异常场景
|
||
|
||
### 8.1 AI 生成超时
|
||
|
||
**场景**: AI 服务响应时间超过 60 秒。
|
||
|
||
**处理**:
|
||
1. 设置超时时间(60 秒,可配置)
|
||
2. 超时后,将订单状态更新为 `FAILED`
|
||
3. 记录错误信息:`error_message = "AI生成超时"`
|
||
4. **✅ 自动回退水晶费用**:调用 `UserService.UpdateCrystalBalance`,退回 `cost_crystal` 金额
|
||
5. 记录日志,便于后续排查
|
||
|
||
### 8.2 AI 服务调用失败
|
||
|
||
**场景**: AI 服务返回错误(API 限流、服务不可用等)。
|
||
|
||
**处理**:
|
||
1. 记录错误信息到 `MintOrder.error_message`(如:"AI服务调用失败: API限流")
|
||
2. 更新订单状态为 `FAILED`
|
||
3. **✅ 自动回退水晶费用**:调用 `UserService.UpdateCrystalBalance`,退回 `cost_crystal` 金额
|
||
4. **暂不重试**:当前版本不自动重试,失败即退回费用(后续版本可考虑重试机制)
|
||
|
||
### 8.3 OSS 上传失败
|
||
|
||
**场景**: AI 图片下载成功,但上传到 OSS 失败。
|
||
|
||
**处理**:
|
||
1. 记录错误信息:`error_message = "OSS上传失败: <具体错误>"`
|
||
2. 订单状态更新为 `FAILED`
|
||
3. **✅ 自动回退水晶费用**:调用 `UserService.UpdateCrystalBalance`,退回 `cost_crystal` 金额
|
||
4. **建议**:保留 AI 服务的临时 URL(如果还在有效期内),记录到日志,便于后续手动重试
|
||
|
||
### 8.4 链上模拟失败(预留)
|
||
|
||
**场景**: 模拟链上信息生成失败(当前版本为模拟,此场景理论上不会发生,但预留处理逻辑)。
|
||
|
||
**处理**:
|
||
1. 记录错误信息
|
||
2. 订单状态更新为 `FAILED`
|
||
3. **✅ 自动回退水晶费用**:调用 `UserService.UpdateCrystalBalance`,退回 `cost_crystal` 金额
|
||
|
||
### 8.5 用户余额不足
|
||
|
||
**场景**: 创建订单时,用户水晶余额不足。
|
||
|
||
**处理**:
|
||
1. 在事务中先检查余额,不足则直接返回错误
|
||
2. 不创建 `Asset` 和 `MintOrder` 记录
|
||
3. 返回 HTTP 402 状态码,前端提示“余额不足”
|
||
|
||
### 8.6 费用回退统一处理
|
||
|
||
**决策**: 所有失败场景(AI 超时、AI 调用失败、OSS 上传失败、链上失败)都必须自动退回水晶费用。
|
||
|
||
**实现方式**:
|
||
```go
|
||
// 在异步任务失败时,统一调用费用回退
|
||
func (s *mintService) refundCrystalBalance(order *models.MintOrder) error {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
// 退回水晶费用
|
||
_, err := s.userClient.UpdateCrystalBalance(ctx, order.UserID, order.StarID, order.CostCrystal)
|
||
if err != nil {
|
||
logger.Logger.Error("Failed to refund crystal balance",
|
||
zap.String("order_id", order.OrderID),
|
||
zap.Error(err),
|
||
)
|
||
return err
|
||
}
|
||
|
||
logger.Logger.Info("Crystal balance refunded",
|
||
zap.String("order_id", order.OrderID),
|
||
zap.Int64("amount", order.CostCrystal),
|
||
)
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
**注意事项**:
|
||
- 费用回退必须在订单状态更新为 `FAILED` 之前完成(确保原子性)
|
||
- 如果费用回退失败,需要记录告警日志,并可能需要人工介入
|
||
|
||
---
|
||
|
||
## 9. 性能优化建议
|
||
|
||
### 9.1 异步任务并发控制
|
||
|
||
**建议**: 限制同时处理的 AI 任务数量,避免过载。
|
||
|
||
**实现方式**:
|
||
```go
|
||
// 使用 channel 作为信号量,限制并发数
|
||
var semaphore = make(chan struct{}, 10) // 最多10个并发任务
|
||
|
||
func (s *mintService) processAIGeneration(...) {
|
||
semaphore <- struct{}{} // 获取信号量
|
||
defer func() { <-semaphore }() // 释放信号量
|
||
|
||
// AI 处理逻辑
|
||
}
|
||
```
|
||
|
||
### 9.2 预签名 URL 批量生成
|
||
|
||
**建议**: 在 `GetMyAssets` 接口中,批量生成预签名 URL,减少 OSS API 调用。
|
||
|
||
**实现方式**:
|
||
```go
|
||
// 一次性为所有 cover_url 生成预签名 URL
|
||
for i := range assets {
|
||
if assets[i].CoverURL != "" {
|
||
signedURL, _ := generatePresignedURL(assets[i].CoverURL, 3600)
|
||
assets[i].CoverURLSigned = signedURL
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.3 数据库索引优化
|
||
|
||
**建议**: 确保以下字段已建立索引:
|
||
- `assets.owner_uid + star_id`(复合索引,用于查询用户藏品)
|
||
- `assets.status`(用于筛选状态)
|
||
- `mint_orders.user_id + star_id`(复合索引,用于查询用户订单)
|
||
- `mint_orders.status`(用于筛选订单状态)
|
||
|
||
---
|
||
|
||
## 10. 已确认问题清单
|
||
|
||
### ✅ 已确认的设计决策
|
||
|
||
1. **AI 服务选择** ✅
|
||
- ✅ **先使用 Mock 版本**:实现完整的前后端对接流程,AI 服务暂时模拟(返回一张预设图片或随机选择)
|
||
- ✅ **后续集成真实 AI 服务**:Phase 4 再集成真实的 AI 服务 API
|
||
- ✅ **Mock 实现方式**:可以随机选择一张预设图片,或直接复制 `material_url` 作为 `cover_url`(仅用于测试)
|
||
|
||
2. **AI 生成超时与重试** ✅
|
||
- ✅ **超时时间**:60 秒(可配置)
|
||
- ✅ **失败处理**:不自动重试,失败即退回费用(后续版本可考虑重试机制)
|
||
|
||
3. **费用回退策略** ✅
|
||
- ✅ **自动回退**:所有失败场景(AI 超时、AI 调用失败、OSS 上传失败、链上失败)都必须自动退回水晶费用
|
||
- ✅ **回退时机**:在订单状态更新为 `FAILED` 时,立即调用 `UserService.UpdateCrystalBalance` 退回费用
|
||
- ✅ **回退金额**:退回 `MintOrder.cost_crystal` 的完整金额
|
||
|
||
4. **藏品列表预签名** ✅
|
||
- ✅ **自动生成**:在 `GetMyAssets`、`GetAssetDetail` 和 `GetMintOrder` 接口中,自动为所有图片 URL 生成预签名 URL
|
||
- ✅ **有效期**:3600 秒(1 小时)
|
||
- ✅ **适用范围**:`cover_url` 和 `material_url` 都需要生成预签名 URL
|
||
|
||
5. **状态轮询频率** ✅
|
||
- ✅ **轮询频率**:3 秒(前端每 3 秒轮询一次 `GET /api/v1/assets/mints/:order_id`)
|
||
|
||
6. **错误信息展示** ✅
|
||
- ✅ **失败消息返回**:AI 生成失败时,`GET /api/v1/assets/mints/:order_id` 接口返回 `status = FAILED` 和 `error_message` 字段
|
||
- ✅ **前端展示**:前端直接展示 `error_message`,提示用户费用已退回
|
||
- ✅ **暂不提供重新生成**:当前版本不提供“重新生成”功能,用户需要重新提交订单
|
||
|
||
### 🟢 暂不实现的功能(后续优化)
|
||
|
||
7. **WebSocket 实时通知** ❌
|
||
- ❌ **暂不实现**:当前版本使用轮询机制,后续版本可考虑 WebSocket
|
||
|
||
8. **AI 生成进度** ❌
|
||
- ❌ **暂不实现**:当前版本不显示进度百分比,后续版本可考虑(需要 AI 服务支持)
|
||
|
||
9. **批量铸造** ❌
|
||
- ❌ **暂不实现**:当前版本不支持一次提交多个素材,后续版本可考虑
|
||
|
||
---
|
||
|
||
## 11. 实现计划
|
||
|
||
### Phase 1: 核心功能实现(预计 3-5 天)
|
||
- [ ] 修改 `CreateMintOrder` 接口,支持 `material_url` 参数(替换 `cover_url` 为必填)
|
||
- [ ] 实现异步 AI 处理 Goroutine(**Mock 版本**:随机选择预设图片或复制 `material_url`)
|
||
- [ ] 实现 AI 图片下载与 OSS 上传逻辑(Mock 版本:直接上传 `material_url` 的图片到 `covers` 目录)
|
||
- [ ] 实现模拟链上信息生成(`TxHash`、`BlockNumber`、`MintedAt`)
|
||
- [ ] 实现订单状态查询接口 `GET /api/v1/assets/mints/:order_id`
|
||
- [ ] 实现费用回退逻辑(所有失败场景自动退回水晶)
|
||
|
||
### Phase 2: 藏品查询优化(预计 1-2 天)
|
||
- [ ] 优化 `GetMyAssets` 接口,自动为所有图片 URL 生成预签名 URL
|
||
- [ ] 实现 `GetAssetDetail` 接口,自动生成预签名 URL
|
||
- [ ] 优化 `GetMintOrder` 接口,成功时自动生成 `cover_url_signed`
|
||
- [ ] 添加状态筛选功能(`status` 参数)
|
||
|
||
### Phase 3: 错误处理与测试(预计 2-3 天)
|
||
- [ ] 实现超时处理机制(60 秒超时)
|
||
- [ ] 实现失败场景的统一错误处理(AI 超时、AI 调用失败、OSS 上传失败)
|
||
- [ ] 实现费用回退的统一处理逻辑
|
||
- [ ] 编写单元测试和集成测试
|
||
- [ ] 编写 HTTP 完整测试流程文档
|
||
|
||
### Phase 4: 真实 AI 服务集成(预计 2-3 天,后续版本)
|
||
- [ ] 集成真实的 AI 服务 API(OpenAI DALL-E、Stable Diffusion 或其他)
|
||
- [ ] 配置 AI 服务的 API Key 和 Endpoint
|
||
- [ ] 实现真实 AI 图片下载与转存逻辑
|
||
- [ ] 测试真实场景下的性能与稳定性
|
||
- [ ] 替换 Mock 版本的 AI 处理逻辑
|
||
|
||
---
|
||
|
||
## 12. 参考文档
|
||
|
||
- [资产服务设计文档](./资产服务设计文档.md)
|
||
- [OSS 预签名 URL 指南](../gateway/OSS_PRESIGNED_URL_GUIDE.md)
|
||
- [OSS 批量预签名 URL 指南](../gateway/OSS_BATCH_PRESIGNED_URL_GUIDE.md)
|
||
- [API 测试快速指南](../gateway/API_TEST_QUICK.md)
|
||
|
||
---
|
||
|
||
## 13. 更新日志
|
||
|
||
| 版本 | 日期 | 更新内容 | 作者 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| v1.0 | 2026-01-20 | 初始版本,包含完整流程设计和待确认问题 | 开发团队 |
|
||
| v1.1 | 2026-01-20 | 根据确认结果更新:AI Mock 版本、费用自动回退、预签名 URL 自动生成、轮询频率 3 秒、失败消息返回、暂不实现的功能 | 开发团队 |
|
||
|
||
---
|
||
|
||
**文档状态**: ✅ 已确认,可开始实现
|
||
|
||
**下一步**: 开始 Phase 1 的核心功能实现,优先完成前后端完整对接流程。
|