# 资产铸造(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 前端: {
"name": "资产名称",
"material_url": "...",
"description": "描述"
}
网关->>资产服务: 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 = `
- `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 的核心功能实现,优先完成前后端完整对接流程。