1541 lines
48 KiB
Markdown
1541 lines
48 KiB
Markdown
# 铸爱创作模块 技术设计文档
|
||
|
||
> **文档版本:** V1.0
|
||
> **创建日期:** 2026-04-09
|
||
> **关联产品文档:** 铸爱创作模块PRD.md
|
||
> **状态:** 初稿
|
||
|
||
---
|
||
|
||
## 1. 产品需求概述
|
||
|
||
### 1.1 核心功能
|
||
|
||
| 功能模块 | 说明 |
|
||
|----------|------|
|
||
| 顶部运营轮播图 | 展示运营活动,点击跳转H5或站内页面 |
|
||
| 主Tab(星卡/吧唧/海报) | 三大创作分类,点击直接进入铸造页面 |
|
||
| 分类标签 | 筛选用户已铸造的藏品(热门/最新/各分类) |
|
||
| 创作网格列表 | 双列瀑布流展示藏品卡片 |
|
||
| 铸造页面 | 选择素材 → 输入描述词 → AI生成 |
|
||
| AI处理中页面 | 展示创作进度 |
|
||
| AI创作结果预览 | 预览结果 → 重新生成/确认铸造 |
|
||
| 创作详情页 | 展示藏品信息、用户名、点赞、身份证编号 |
|
||
| 身份证编号(上链) | 每份藏品全球唯一编号,用于确权追溯 |
|
||
|
||
### 1.2 现有系统能力复用
|
||
|
||
| 现有服务 | 可复用能力 |
|
||
|----------|-----------|
|
||
| AssetService | 数字藏品铸造、资产查询、OSS存储、上链 |
|
||
| ActivityService | 运营活动管理(轮播图配置) |
|
||
| GalleryService | 展馆展示(可参考布局) |
|
||
| UserService | 用户信息、身份验证 |
|
||
| SocialService | 点赞功能 |
|
||
|
||
---
|
||
|
||
## 2. 技术架构设计
|
||
|
||
### 2.1 服务职责划分
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ API Gateway (8080) │
|
||
│ 统一入口 / 认证 / 协议转换 / 请求路由 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
│ AssetService │ │ ActivityService │ │ SocialService │
|
||
│ (tri:20003) │ │ (tri:20005?) │ │ (tri:20001) │
|
||
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
||
│ 铸造订单管理 │ │ 运营活动配置 │ │ 藏品点赞 │
|
||
│ 资产CRUD │ │ 轮播图管理 │ │ 评论(扩展) │
|
||
│ AI任务调度 │ │ 活动跳转配置 │ │ │
|
||
│ 上链状态 │ │ │ │ │
|
||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||
```
|
||
|
||
### 2.2 新增/扩展接口
|
||
|
||
| 接口 | 方法 | 归属服务 | 说明 | 状态 |
|
||
|------|------|----------|------|------|
|
||
| `GET /api/v1/activities/banners` | GET | ActivityService | 获取轮播图列表 | **新增** |
|
||
| `GET /api/v1/assets/cast/items` | GET | AssetService | 获取铸爱创作列表(按分类筛选) | **新增** |
|
||
| `GET /api/v1/assets/cast/options` | GET | AssetService | 获取铸爱类型选项(吧唧装饰/海报风格) | **新增** |
|
||
| `GET /api/v1/assets/:asset_id` | GET | AssetService | 获取藏品详情(**复用现有接口**) | 已存在 |
|
||
| `POST /api/v1/social/assets/:asset_id/like` | POST | SocialService | 藏品点赞(**复用现有接口**) | 已存在 |
|
||
| `POST /api/v1/assets/cast/mints` | POST | AssetService | 创建铸爱铸造订单 | **新增** |
|
||
| `GET /api/v1/assets/cast/mints/:order_id` | GET | AssetService | 查询铸造订单状态 | **新增** |
|
||
|
||
**接口复用说明:**
|
||
- `GET /api/v1/assets/:asset_id` - 现有接口,可查询藏品详情
|
||
- `POST /api/v1/social/assets/:asset_id/like` - 现有接口,可用于点赞
|
||
|
||
---
|
||
|
||
## 3. 数据模型设计
|
||
|
||
### 3.1 铸爱藏品表(新建)
|
||
|
||
**表名:** `cast_assets`
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS cast_assets (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
owner_uid BIGINT NOT NULL COMMENT '所有者ID',
|
||
star_id BIGINT NOT NULL COMMENT '明星ID,用于数据隔离',
|
||
name VARCHAR(255) NOT NULL COMMENT '藏品名称',
|
||
material_url TEXT COMMENT '原始素材URL(用户相册图片)',
|
||
cover_url TEXT COMMENT 'AI生成结果图URL',
|
||
description TEXT COMMENT '藏品描述',
|
||
cast_type INT NOT NULL DEFAULT 1 COMMENT '藏品类型:1=星卡, 2=吧唧, 3=海报',
|
||
identity_no VARCHAR(50) UNIQUE COMMENT '身份证编号(全局唯一)',
|
||
tx_hash VARCHAR(100) COMMENT '上链交易哈希',
|
||
block_number BIGINT COMMENT '区块高度',
|
||
status INT NOT NULL DEFAULT 0 COMMENT '状态:0=处理中, 1=成功, 2=失败',
|
||
like_count INT NOT NULL DEFAULT 0 COMMENT '点赞数',
|
||
user_id BIGINT NOT NULL COMMENT '创作者ID',
|
||
minted_at BIGINT COMMENT '上链时间(毫秒)',
|
||
created_at BIGINT NOT NULL DEFAULT 0,
|
||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||
|
||
-- 索引
|
||
INDEX idx_cast_assets_star_id (star_id),
|
||
INDEX idx_cast_assets_owner_uid_star_id (owner_uid, star_id),
|
||
INDEX idx_cast_assets_cast_type (cast_type),
|
||
INDEX idx_cast_assets_identity_no (identity_no),
|
||
INDEX idx_cast_assets_user_id (user_id),
|
||
INDEX idx_cast_assets_status (status),
|
||
INDEX idx_cast_assets_like_count (like_count),
|
||
INDEX idx_cast_assets_created_at (created_at)
|
||
) COMMENT '铸爱藏品表';
|
||
```
|
||
|
||
### 3.2 铸爱铸造订单表(新建)
|
||
|
||
**表名:** `cast_mint_orders`
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS cast_mint_orders (
|
||
order_id VARCHAR(50) PRIMARY KEY COMMENT '订单ID(UUID)',
|
||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||
star_id BIGINT NOT NULL COMMENT '明星ID',
|
||
asset_id BIGINT NOT NULL COMMENT '关联藏品ID',
|
||
cast_type INT NOT NULL COMMENT '藏品类型:1=星卡, 2=吧唧, 3=海报',
|
||
prompt_text TEXT COMMENT '用户输入的描述词',
|
||
material_url TEXT COMMENT '原始素材URL',
|
||
cover_url TEXT COMMENT 'AI生成结果URL(完成后回填)',
|
||
identity_no VARCHAR(50) COMMENT '身份证编号',
|
||
tx_hash VARCHAR(100) COMMENT '上链哈希',
|
||
status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS/FAILED',
|
||
error_message TEXT COMMENT '失败原因',
|
||
cost_crystal BIGINT DEFAULT 0 COMMENT '消耗水晶',
|
||
retry_count INT DEFAULT 0 COMMENT '重试次数',
|
||
-- 吧唧专属参数
|
||
decoration_type VARCHAR(50) COMMENT '吧唧装饰类型:cream_glue/metal_frame/shaped',
|
||
-- 海报专属参数
|
||
aspect_ratio VARCHAR(20) COMMENT '海报比例:landscape/portrait/square',
|
||
style_options TEXT COMMENT '海报风格选项:JSON数组,bg_replace/style_transfer/lighting',
|
||
created_at BIGINT NOT NULL DEFAULT 0,
|
||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||
minted_at BIGINT COMMENT '完成时间',
|
||
|
||
-- 索引
|
||
INDEX idx_cast_mint_orders_user_star (user_id, star_id),
|
||
INDEX idx_cast_mint_orders_status (status),
|
||
INDEX idx_cast_mint_orders_asset_id (asset_id),
|
||
INDEX idx_cast_mint_orders_created_at (created_at)
|
||
) COMMENT '铸爱铸造订单表';
|
||
```
|
||
|
||
### 3.3 铸爱点赞表(扩展现有 asset_likes)
|
||
|
||
**扩展现有表,新增字段:**
|
||
|
||
```sql
|
||
ALTER TABLE asset_likes ADD COLUMN IF NOT EXISTS asset_type INT DEFAULT 1 COMMENT '资产类型:1=原有资产, 2=铸爱藏品';
|
||
ALTER TABLE asset_likes ADD COLUMN IF NOT EXISTS cast_asset_id BIGINT COMMENT '铸爱藏品ID(asset_type=2时使用)';
|
||
|
||
-- 添加联合唯一索引,防止重复点赞
|
||
ALTER TABLE asset_likes ADD CONSTRAINT uk_asset_like UNIQUE (user_id, asset_id, asset_type);
|
||
```
|
||
|
||
### 3.4 铸爱类型选项配置表(新建)
|
||
|
||
**表名:** `cast_type_options`
|
||
|
||
**说明:** 运维后台上传和管理各类型的可选项(吧唧装饰类型、海报风格选项等),前端下发给用户选择,同时传给AI生成。
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS cast_type_options (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
cast_type INT NOT NULL COMMENT '藏品类型:1=星卡, 2=吧唧, 3=海报',
|
||
option_key VARCHAR(50) NOT NULL COMMENT '选项标识,如 cream_glue / metal_frame / bg_replace',
|
||
option_name VARCHAR(100) NOT NULL COMMENT '选项显示名称,如 奶油胶 / 金属边框 / 背景替换',
|
||
option_type VARCHAR(30) NOT NULL COMMENT '选项类别:decoration / style',
|
||
icon_url TEXT COMMENT '选项图标URL',
|
||
sort_order INT DEFAULT 0 COMMENT '排序',
|
||
status INT DEFAULT 1 COMMENT '状态:0=禁用, 1=启用',
|
||
created_at BIGINT NOT NULL DEFAULT 0,
|
||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||
|
||
-- 索引
|
||
INDEX idx_cast_type_options_type (cast_type),
|
||
INDEX idx_cast_type_options_key (cast_type, option_key),
|
||
UNIQUE KEY uk_cast_type_option (cast_type, option_key)
|
||
) COMMENT '铸爱类型选项配置表';
|
||
```
|
||
|
||
**示例数据:**
|
||
|
||
| cast_type | option_key | option_name | option_type |
|
||
|-----------|------------|-------------|-------------|
|
||
| 2 (吧唧) | cream_glue | 奶油胶 | decoration |
|
||
| 2 (吧唧) | metal_frame | 金属边框 | decoration |
|
||
| 2 (吧唧) | shaped | 异形裁切 | decoration |
|
||
| 3 (海报) | bg_replace | 背景替换 | style |
|
||
| 3 (海报) | style_transfer | 风格迁移 | style |
|
||
| 3 (海报) | lighting | 光影调整 | style |
|
||
|
||
**运维后台功能:**
|
||
- 新增/编辑/删除选项
|
||
- 上传选项图标
|
||
- 调整排序
|
||
|
||
---
|
||
|
||
## 4. 接口详细设计与代码实现
|
||
|
||
### 4.1 获取轮播图列表(新增接口)
|
||
|
||
**Endpoint:** `GET /api/v1/activities/banners`
|
||
|
||
**归属服务:** ActivityService
|
||
|
||
#### 4.1.1 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| position | string | 否 | 位置标识,默认 "cast_home" |
|
||
|
||
#### 4.1.2 响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"items": [
|
||
{
|
||
"id": 1,
|
||
"image_url": "https://xxx/banner1.jpg",
|
||
"title": "铸爱联名活动",
|
||
"description": "参与铸爱创作赢好礼",
|
||
"link_type": "h5",
|
||
"link_value": "https://xxx/activity"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.3 DTO 定义
|
||
|
||
```go
|
||
// BannerDTO 轮播图响应
|
||
type BannerDTO struct {
|
||
ID int64 `json:"id"`
|
||
ImageURL string `json:"image_url"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
LinkType string `json:"link_type"` // h5 / activity / internal
|
||
LinkValue string `json:"link_value"` // 跳转目标
|
||
}
|
||
|
||
// BannerListResponse 轮播图列表响应
|
||
type BannerListResponse struct {
|
||
Items []BannerDTO `json:"items"`
|
||
}
|
||
```
|
||
|
||
#### 4.1.4 Service 层实现
|
||
|
||
```go
|
||
// ActivityService/service/activity_service.go
|
||
|
||
// GetBanners 获取轮播图列表
|
||
func (s *ActivityService) GetBanners(ctx context.Context, position string) ([]*BannerDTO, error) {
|
||
// 默认位置
|
||
if position == "" {
|
||
position = "cast_home"
|
||
}
|
||
|
||
// 查询数据库
|
||
banners, err := s.bannerRepo.FindByPosition(ctx, position)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 转换为DTO
|
||
dtos := make([]*BannerDTO, 0, len(banners))
|
||
for _, banner := range banners {
|
||
dtos = append(dtos, &BannerDTO{
|
||
ID: banner.ID,
|
||
ImageURL: banner.ImageURL,
|
||
Title: banner.Title,
|
||
Description: banner.Description,
|
||
LinkType: banner.LinkType,
|
||
LinkValue: banner.LinkValue,
|
||
})
|
||
}
|
||
|
||
return dtos, nil
|
||
}
|
||
```
|
||
|
||
#### 4.1.5 Controller 层实现
|
||
|
||
```go
|
||
// Gateway/controller/activity_controller.go
|
||
|
||
// GetBanners 获取轮播图列表
|
||
// @Summary 获取轮播图列表
|
||
// @Tags activity
|
||
// @Param position query string false "位置标识"
|
||
// @Success 200 {object} BannerListResponse
|
||
// @Router /api/v1/activities/banners [get]
|
||
func (c *ActivityController) GetBanners(ctx *gin.Context) {
|
||
position := ctx.DefaultQuery("position", "cast_home")
|
||
|
||
banners, err := c.activityClient.GetBanners(ctx, position)
|
||
if err != nil {
|
||
response.Fail(ctx, 500, "获取轮播图失败")
|
||
return
|
||
}
|
||
|
||
response.Success(ctx, gin.H{
|
||
"items": banners,
|
||
})
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.2 获取铸爱创作列表(新增接口)
|
||
|
||
**Endpoint:** `GET /api/v1/assets/cast/items`
|
||
|
||
**归属服务:** AssetService
|
||
|
||
#### 4.2.1 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| cast_type | int32 | 否 | 藏品类型:1=星卡, 2=吧唧, 3=海报 |
|
||
| sort_by | string | 否 | 排序方式:hot(热门) / latest(最新),默认hot |
|
||
| page | int32 | 否 | 页码,默认1 |
|
||
| page_size | int32 | 否 | 每页数量,默认20 |
|
||
|
||
#### 4.2.2 响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"total": 100,
|
||
"page": 1,
|
||
"page_size": 20,
|
||
"items": [
|
||
{
|
||
"asset_id": 12345,
|
||
"name": "我的铸爱作品",
|
||
"cover_url": "https://xxx/cover.jpg",
|
||
"cover_url_signed": "https://xxx/cover.jpg?Expires=...&Signature=...",
|
||
"identity_no": "ZUA-20260409-00001",
|
||
"cast_type": 1,
|
||
"user_id": 10001,
|
||
"user_nickname": "用户昵称",
|
||
"user_avatar": "https://xxx/avatar.jpg",
|
||
"like_count": 999,
|
||
"is_liked": false,
|
||
"created_at": 1705747200000
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.2.3 DTO 定义
|
||
|
||
```go
|
||
// CastAssetItemDTO 铸爱藏品列表项
|
||
type CastAssetItemDTO struct {
|
||
AssetID int64 `json:"asset_id"`
|
||
Name string `json:"name"`
|
||
CoverURL string `json:"cover_url"`
|
||
CoverURLSigned string `json:"cover_url_signed"`
|
||
IdentityNo string `json:"identity_no"`
|
||
CastType int32 `json:"cast_type"`
|
||
UserID int64 `json:"user_id"`
|
||
UserNickname string `json:"user_nickname"`
|
||
UserAvatar string `json:"user_avatar"`
|
||
LikeCount int32 `json:"like_count"`
|
||
IsLiked bool `json:"is_liked"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
}
|
||
|
||
// CastAssetListResponse 铸爱藏品列表响应
|
||
type CastAssetListResponse struct {
|
||
Total int64 `json:"total"`
|
||
Page int32 `json:"page"`
|
||
PageSize int32 `json:"page_size"`
|
||
Items []CastAssetItemDTO `json:"items"`
|
||
}
|
||
```
|
||
|
||
#### 4.2.4 Service 层实现
|
||
|
||
```go
|
||
// AssetService/service/cast_service.go
|
||
|
||
// GetCastItems 获取铸爱创作列表
|
||
func (s *CastService) GetCastItems(ctx context.Context, req *GetCastItemsRequest) (*CastAssetListResponse, error) {
|
||
// 参数校验与默认值
|
||
if req.Page <= 0 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize <= 0 || req.PageSize > 100 {
|
||
req.PageSize = 20
|
||
}
|
||
if req.SortBy == "" {
|
||
req.SortBy = "hot"
|
||
}
|
||
|
||
// 构建查询条件
|
||
query := &CastAssetQuery{
|
||
StarID: req.StarID,
|
||
CastType: req.CastType,
|
||
SortBy: req.SortBy,
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
Status: 1, // 只查询成功的
|
||
}
|
||
|
||
// 查询列表
|
||
assets, total, err := s.castRepo.FindList(ctx, query)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 获取当前用户已点赞的藏品ID列表
|
||
likedMap, err := s.getUserLikedMap(ctx, req.UserID, assets)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 转换为DTO
|
||
items := make([]CastAssetItemDTO, 0, len(assets))
|
||
for _, asset := range assets {
|
||
// 生成预签名URL
|
||
coverURLSigned, _ := s.generatePresignedURL(asset.CoverURL, 3600)
|
||
|
||
// 获取用户信息
|
||
user, _ := s.userClient.GetUserInfo(ctx, asset.UserID)
|
||
|
||
items = append(items, CastAssetItemDTO{
|
||
AssetID: asset.ID,
|
||
Name: asset.Name,
|
||
CoverURL: asset.CoverURL,
|
||
CoverURLSigned: coverURLSigned,
|
||
IdentityNo: asset.IdentityNo,
|
||
CastType: asset.CastType,
|
||
UserID: asset.UserID,
|
||
UserNickname: user.Nickname,
|
||
UserAvatar: user.Avatar,
|
||
LikeCount: asset.LikeCount,
|
||
IsLiked: likedMap[asset.ID],
|
||
CreatedAt: asset.CreatedAt,
|
||
})
|
||
}
|
||
|
||
return &CastAssetListResponse{
|
||
Total: total,
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
Items: items,
|
||
}, nil
|
||
}
|
||
|
||
// generatePresignedURL 生成预签名URL
|
||
func (s *CastService) generatePresignedURL(url string, expireSeconds int64) (string, error) {
|
||
if url == "" {
|
||
return "", nil
|
||
}
|
||
// 调用OSS生成预签名URL
|
||
return s.ossClient.GetPresignedURL(ctx, url, expireSeconds)
|
||
}
|
||
|
||
// getUserLikedMap 获取用户已点赞的藏品Map
|
||
func (s *CastService) getUserLikedMap(ctx context.Context, userID int64, assets []*CastAsset) (map[int64]bool, error) {
|
||
assetIDs := make([]int64, 0, len(assets))
|
||
for _, asset := range assets {
|
||
assetIDs = append(assetIDs, asset.ID)
|
||
}
|
||
|
||
likedAssetIDs, err := s.socialClient.GetUserLikedCastAssets(ctx, userID, assetIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
likedMap := make(map[int64]bool)
|
||
for _, id := range likedAssetIDs {
|
||
likedMap[id] = true
|
||
}
|
||
return likedMap, nil
|
||
}
|
||
```
|
||
|
||
#### 4.2.5 Repository 层实现
|
||
|
||
```go
|
||
// AssetService/repository/cast_asset_repo.go
|
||
|
||
// FindList 查询铸爱藏品列表
|
||
func (r *CastAssetRepository) FindList(ctx context.Context, query *CastAssetQuery) ([]*CastAsset, int64, error) {
|
||
db := r.db.WithContext(ctx)
|
||
|
||
// 条件构建
|
||
if query.StarID > 0 {
|
||
db = db.Where("star_id = ?", query.StarID)
|
||
}
|
||
if query.CastType > 0 {
|
||
db = db.Where("cast_type = ?", query.CastType)
|
||
}
|
||
if query.Status > 0 {
|
||
db = db.Where("status = ?", query.Status)
|
||
}
|
||
|
||
// 排序
|
||
switch query.SortBy {
|
||
case "latest":
|
||
db = db.Order("created_at DESC")
|
||
case "hot":
|
||
db = db.Order("like_count DESC, created_at DESC")
|
||
default:
|
||
db = db.Order("like_count DESC, created_at DESC")
|
||
}
|
||
|
||
// 查询总数
|
||
var total int64
|
||
if err := db.Model(&CastAsset{}).Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 分页
|
||
offset := (query.Page - 1) * query.PageSize
|
||
db = db.Offset(offset).Limit(query.PageSize)
|
||
|
||
// 执行查询
|
||
var assets []*CastAsset
|
||
if err := db.Find(&assets).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return assets, total, nil
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.3 获取铸爱藏品详情(复用现有接口)
|
||
|
||
**Endpoint:** `GET /api/v1/assets/:asset_id`
|
||
|
||
**归属服务:** AssetService
|
||
|
||
**接口说明:** 复用现有的 `GetAsset` 接口,无需新增。铸爱藏品使用 `cast_assets` 表,扩展 `AssetService.GetAsset` 方法支持查询铸爱藏品。
|
||
|
||
#### 4.3.1 响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"asset_id": 12345,
|
||
"name": "我的铸爱作品",
|
||
"cover_url": "https://xxx/cover.jpg",
|
||
"cover_url_signed": "https://xxx/cover.jpg?Expires=...&Signature=...",
|
||
"identity_no": "ZUA-20260409-00001",
|
||
"identity_no_chain_url": "https://xxx/chain/query?no=ZUA-20260409-00001",
|
||
"cast_type": 1,
|
||
"description": "藏品描述",
|
||
"user_id": 10001,
|
||
"user_nickname": "用户昵称",
|
||
"user_avatar": "https://xxx/avatar.jpg",
|
||
"like_count": 999,
|
||
"is_liked": false,
|
||
"created_at": 1705747200000
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.3.2 DTO 定义
|
||
|
||
```go
|
||
// CastAssetDetailDTO 铸爱藏品详情
|
||
type CastAssetDetailDTO struct {
|
||
AssetID int64 `json:"asset_id"`
|
||
Name string `json:"name"`
|
||
CoverURL string `json:"cover_url"`
|
||
CoverURLSigned string `json:"cover_url_signed"`
|
||
IdentityNo string `json:"identity_no"`
|
||
IdentityNoChainURL string `json:"identity_no_chain_url"`
|
||
CastType int32 `json:"cast_type"`
|
||
Description string `json:"description"`
|
||
UserID int64 `json:"user_id"`
|
||
UserNickname string `json:"user_nickname"`
|
||
UserAvatar string `json:"user_avatar"`
|
||
LikeCount int32 `json:"like_count"`
|
||
IsLiked bool `json:"is_liked"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
}
|
||
```
|
||
|
||
#### 4.3.3 Service 层实现
|
||
|
||
```go
|
||
// GetCastAssetDetail 获取铸爱藏品详情
|
||
func (s *CastService) GetCastAssetDetail(ctx context.Context, assetID, userID int64) (*CastAssetDetailDTO, error) {
|
||
// 查询藏品
|
||
asset, err := s.castRepo.FindByID(ctx, assetID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if asset == nil {
|
||
return nil, errors.New("藏品不存在")
|
||
}
|
||
|
||
// 生成预签名URL
|
||
coverURLSigned, _ := s.generatePresignedURL(asset.CoverURL, 3600)
|
||
|
||
// 获取用户信息
|
||
user, _ := s.userClient.GetUserInfo(ctx, asset.UserID)
|
||
|
||
// 查询用户是否已点赞
|
||
isLiked, _ := s.socialClient.IsUserLikedCastAsset(ctx, userID, assetID)
|
||
|
||
// 链上查询URL(预留)
|
||
chainURL := s.buildChainQueryURL(asset.IdentityNo)
|
||
|
||
return &CastAssetDetailDTO{
|
||
AssetID: asset.ID,
|
||
Name: asset.Name,
|
||
CoverURL: asset.CoverURL,
|
||
CoverURLSigned: coverURLSigned,
|
||
IdentityNo: asset.IdentityNo,
|
||
IdentityNoChainURL: chainURL,
|
||
CastType: asset.CastType,
|
||
Description: asset.Description,
|
||
UserID: asset.UserID,
|
||
UserNickname: user.Nickname,
|
||
UserAvatar: user.Avatar,
|
||
LikeCount: asset.LikeCount,
|
||
IsLiked: isLiked,
|
||
CreatedAt: asset.CreatedAt,
|
||
}, nil
|
||
}
|
||
|
||
// buildChainQueryURL 构建链上查询URL
|
||
func (s *CastService) buildChainQueryURL(identityNo string) string {
|
||
// 预留链上查询功能
|
||
return fmt.Sprintf("https://xxx/chain/query?no=%s", identityNo)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.4 创建铸爱铸造订单(新增接口)
|
||
|
||
**Endpoint:** `POST /api/v1/assets/cast/mints`
|
||
|
||
**归属服务:** AssetService
|
||
|
||
**页面说明:**
|
||
- 用户点击主Tab(星卡/吧唧/海报)后进入对应类型的铸造页面
|
||
- 铸造页面是同一个页面,通过 `cast_type` 参数区分类型
|
||
-铸造流程:选择素材 → 输入描述词 → 发送 → AI处理中 → 结果预览
|
||
|
||
**三种铸造类型:**
|
||
|
||
| cast_type | 类型 | 素材形态 | 尺寸规格 | 创作特点 | 专属参数 |
|
||
|-----------|------|----------|----------|----------|----------|
|
||
| 1 | 星卡 | 卡片形态,类似明星小卡 | 1:1 或 3:4 | 适合人物特写、半身照 | 无 |
|
||
| 2 | 吧唧 | 徽章/贴纸形态 | 圆形或异形 | 适合可爱风格、二次元、奶油胶/金属边框/异形裁切 | decoration_type |
|
||
| 3 | 海报 | 大尺寸图片 | 16:9、4:3 或 2:3 | 适合全身照、场景图、背景替换/风格迁移/光影调整 | aspect_ratio, style_options |
|
||
|
||
#### 4.4.1 请求格式
|
||
|
||
```json
|
||
{
|
||
"name": "我的铸爱作品",
|
||
"material_url": "https://xxx/material.jpg",
|
||
"description": "藏品描述",
|
||
"cast_type": 1,
|
||
"prompt_text": "请生成一张温暖风格的图片",
|
||
"decoration_type": "",
|
||
"aspect_ratio": "",
|
||
"style_options": []
|
||
}
|
||
```
|
||
|
||
**请求参数说明:**
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| name | string | 是 | 藏品名称 |
|
||
| material_url | string | 是 | 用户选择的相册图片URL |
|
||
| description | string | 否 | 藏品描述 |
|
||
| cast_type | int32 | 是 | 藏品类型:1=星卡, 2=吧唧, 3=海报 |
|
||
| prompt_text | string | 是 | AI描述词 |
|
||
| decoration_type | string | 否 | **吧唧专属**:装饰类型,记录用户选择,从 `/cast/options` 获取,如 cream_glue/metal_frame/shaped,可组合逗号分隔 |
|
||
| aspect_ratio | string | 否 | **海报专属**:比例,landscape(横向16:9) / portrait(竖向2:3) / square(方形4:3) |
|
||
| style_options | string[] | 否 | **海报专属**:风格选项,记录用户选择,从 `/cast/options` 获取,如 bg_replace/style_transfer/lighting,可多选 |
|
||
|
||
**参数说明:**
|
||
- `decoration_type` 和 `style_options` 仅记录用户选择,**不参与AI生成**,AI会根据cast_type自动生成对应效果
|
||
- `aspect_ratio` 为海报比例参数,AI生成时使用
|
||
|
||
**不同类型的参数组合:**
|
||
|
||
| cast_type | 必须参数 | 可选参数 |
|
||
|-----------|----------|----------|
|
||
| 1 (星卡) | name, material_url, cast_type, prompt_text | description |
|
||
| 2 (吧唧) | name, material_url, cast_type, prompt_text | description, decoration_type |
|
||
| 3 (海报) | name, material_url, cast_type, prompt_text | description, aspect_ratio, style_options |
|
||
|
||
#### 4.4.2 响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"order_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"asset_id": 12345,
|
||
"status": "PROCESSING",
|
||
"identity_no": "ZUA-20260409-00002",
|
||
"created_at": 1705747200000
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.4.3 DTO 定义
|
||
|
||
```go
|
||
// CreateCastMintRequest 创建铸造订单请求
|
||
type CreateCastMintRequest struct {
|
||
Name string `json:"name" binding:"required"`
|
||
MaterialURL string `json:"material_url" binding:"required,url"`
|
||
Description string `json:"description"`
|
||
CastType int32 `json:"cast_type" binding:"required,oneof=1 2 3"`
|
||
PromptText string `json:"prompt_text" binding:"required"`
|
||
// 吧唧专属参数
|
||
DecorationType string `json:"decoration_type"` // cream_glue, metal_frame, shaped
|
||
// 海报专属参数
|
||
AspectRatio string `json:"aspect_ratio"` // landscape, portrait, square
|
||
StyleOptions []string `json:"style_options"` // bg_replace, style_transfer, lighting
|
||
}
|
||
|
||
// CreateCastMintResponse 创建铸造订单响应
|
||
type CreateCastMintResponse struct {
|
||
OrderID string `json:"order_id"`
|
||
AssetID int64 `json:"asset_id"`
|
||
Status string `json:"status"`
|
||
IdentityNo string `json:"identity_no"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
}
|
||
```
|
||
|
||
#### 4.4.4 Service 层实现(核心流程)
|
||
|
||
```go
|
||
// CreateCastMint 创建铸爱铸造订单
|
||
func (s *CastService) CreateCastMint(ctx context.Context, userID, starID int64, req *CreateCastMintRequest) (*CreateCastMintResponse, error) {
|
||
// 1. 参数校验
|
||
if req.Name == "" || req.MaterialURL == "" || req.PromptText == "" {
|
||
return nil, errors.New("参数不完整")
|
||
}
|
||
if req.CastType < 1 || req.CastType > 3 {
|
||
return nil, errors.New("无效的藏品类型")
|
||
}
|
||
|
||
// 2. 生成唯一订单ID
|
||
orderID := uuid.New().String()
|
||
|
||
// 3. 生成唯一身份证编号
|
||
identityNo, err := s.generateIdentityNo(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("生成身份证编号失败: %w", err)
|
||
}
|
||
|
||
// 4. 创建藏品记录(初始状态:处理中)
|
||
asset := &CastAsset{
|
||
OwnerUID: userID,
|
||
StarID: starID,
|
||
Name: req.Name,
|
||
MaterialURL: req.MaterialURL,
|
||
Description: req.Description,
|
||
CastType: req.CastType,
|
||
IdentityNo: identityNo,
|
||
Status: 0, // 处理中
|
||
UserID: userID,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
UpdatedAt: time.Now().UnixMilli(),
|
||
}
|
||
if err := s.castRepo.Create(ctx, asset); err != nil {
|
||
return nil, fmt.Errorf("创建藏品记录失败: %w", err)
|
||
}
|
||
|
||
// 5. 创建订单记录
|
||
order := &CastMintOrder{
|
||
OrderID: orderID,
|
||
UserID: userID,
|
||
StarID: starID,
|
||
AssetID: asset.ID,
|
||
CastType: req.CastType,
|
||
PromptText: req.PromptText,
|
||
MaterialURL: req.MaterialURL,
|
||
IdentityNo: identityNo,
|
||
Status: "PROCESSING",
|
||
DecorationType: req.DecorationType, // 吧唧专属
|
||
AspectRatio: req.AspectRatio, // 海报专属
|
||
StyleOptions: req.StyleOptions, // 海报专属
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
UpdatedAt: time.Now().UnixMilli(),
|
||
}
|
||
if err := s.mintOrderRepo.Create(ctx, order); err != nil {
|
||
return nil, fmt.Errorf("创建订单记录失败: %w", err)
|
||
}
|
||
|
||
// 6. 启动异步AI生成任务(decoration_type/style_options仅记录,不传给AI)
|
||
go s.processCastAIGeneration(orderID, asset.ID, req.CastType, req.PromptText, req.MaterialURL, req.AspectRatio)
|
||
|
||
// 7. 返回响应
|
||
return &CreateCastMintResponse{
|
||
OrderID: orderID,
|
||
AssetID: asset.ID,
|
||
Status: "PROCESSING",
|
||
IdentityNo: identityNo,
|
||
CreatedAt: asset.CreatedAt,
|
||
}, nil
|
||
}
|
||
```
|
||
|
||
#### 4.4.5 异步AI生成任务(核心代码)
|
||
|
||
```go
|
||
// processCastAIGeneration 异步处理AI生成
|
||
func (s *CastService) processCastAIGeneration(orderID string, assetID int64, castType int32, promptText, materialURL, aspectRatio string) {
|
||
// 创建独立的context,避免请求超时
|
||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||
defer cancel()
|
||
|
||
logger.Info("开始处理铸爱AI生成",
|
||
zap.String("order_id", orderID),
|
||
zap.Int64("asset_id", assetID),
|
||
zap.Int32("cast_type", castType),
|
||
zap.String("aspect_ratio", aspectRatio),
|
||
zap.String("prompt", promptText),
|
||
)
|
||
|
||
// 1. 构建AI请求参数
|
||
aiReq := &AIGenerateRequest{
|
||
Model: "minimax",
|
||
InputImage: materialURL,
|
||
Prompt: promptText,
|
||
AspectRatio: aspectRatio,
|
||
}
|
||
|
||
// 2. 根据cast_type设置AI生成参数
|
||
// decoration_type和style_options仅记录到订单,不参与AI生成
|
||
switch castType {
|
||
case 1: // 星卡
|
||
aiReq.AspectRatio = "1:1"
|
||
case 2: // 吧唧
|
||
// AI根据cast_type自动生成吧唧装饰效果
|
||
case 3: // 海报
|
||
// AI根据aspectRatio确定输出图片的尺寸比例
|
||
}
|
||
|
||
// 3. 调用MiniMax AI服务进行图生图
|
||
aiResult, err := s.aiClient.GenerateImage(ctx, aiReq)
|
||
if err != nil {
|
||
s.handleAIGenerationFailed(ctx, orderID, assetID, "AI服务调用失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
// 5. 下载AI生成的图片
|
||
generatedImage, err := s.downloadImage(ctx, aiResult.ImageURL)
|
||
if err != nil {
|
||
s.handleAIGenerationFailed(ctx, orderID, assetID, "下载AI图片失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
// 6. 上传到OSS
|
||
coverURL, err := s.uploadToOSS(ctx, generatedImage, assetID)
|
||
if err != nil {
|
||
s.handleAIGenerationFailed(ctx, orderID, assetID, "上传OSS失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
// 7. 生成模拟上链信息
|
||
txHash := s.generateMockTxHash()
|
||
blockNumber := s.generateMockBlockNumber()
|
||
mintedAt := time.Now().UnixMilli()
|
||
|
||
// 8. 更新藏品状态
|
||
if err := s.updateAssetSuccess(ctx, assetID, coverURL, txHash, blockNumber, mintedAt); err != nil {
|
||
s.handleAIGenerationFailed(ctx, orderID, assetID, "更新藏品状态失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
// 9. 更新订单状态
|
||
if err := s.updateOrderSuccess(ctx, orderID, coverURL, txHash); err != nil {
|
||
logger.Error("更新订单状态失败", zap.String("order_id", orderID), zap.Error(err))
|
||
return
|
||
}
|
||
|
||
logger.Info("铸爱AI生成完成",
|
||
zap.String("order_id", orderID),
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("cover_url", coverURL),
|
||
zap.String("tx_hash", txHash),
|
||
)
|
||
}
|
||
|
||
// handleAIGenerationFailed 处理AI生成失败
|
||
func (s *CastService) handleAIGenerationFailed(ctx context.Context, orderID string, assetID int64, errorMsg string) {
|
||
logger.Error("铸爱AI生成失败",
|
||
zap.String("order_id", orderID),
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("error", errorMsg),
|
||
)
|
||
|
||
// 1. 更新订单状态为失败
|
||
order, _ := s.mintOrderRepo.FindByOrderID(ctx, orderID)
|
||
if order != nil {
|
||
order.Status = "FAILED"
|
||
order.ErrorMessage = errorMsg
|
||
order.UpdatedAt = time.Now().UnixMilli()
|
||
s.mintOrderRepo.Update(ctx, order)
|
||
|
||
// 2. 退回水晶费用
|
||
if order.CostCrystal > 0 {
|
||
s.refundCrystalBalance(ctx, order.UserID, order.StarID, order.CostCrystal)
|
||
}
|
||
}
|
||
|
||
// 3. 更新藏品状态
|
||
s.castRepo.UpdateStatus(ctx, assetID, 2) // 2=失败
|
||
}
|
||
```
|
||
|
||
#### 4.4.6 身份证编号生成
|
||
|
||
```go
|
||
// generateIdentityNo 生成唯一身份证编号
|
||
// 格式: ZUA-YYYYMMDD-XXXXX
|
||
func (s *CastService) generateIdentityNo(ctx context.Context) (string, error) {
|
||
// 获取当前日期
|
||
dateStr := time.Now().Format("20060102")
|
||
|
||
// 使用Redis INCR获取当日序号(按日期key)
|
||
key := fmt.Sprintf("cast:identity:seq:%s", dateStr)
|
||
seq, err := s.redisClient.Incr(ctx, key)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 设置过期时间(次日凌晨)
|
||
s.redisClient.ExpireAt(ctx, key, s.getNextDayMidnight())
|
||
|
||
// 格式化为5位序号
|
||
seqStr := fmt.Sprintf("%05d", seq)
|
||
return fmt.Sprintf("ZUA-%s-%s", dateStr, seqStr), nil
|
||
}
|
||
|
||
// getNextDayMidnight 获取次日凌晨时间
|
||
func (s *CastService) getNextDayMidnight() time.Time {
|
||
tomorrow := time.Now().AddDate(0, 0, 1)
|
||
return time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, tomorrow.Location())
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.5 查询铸造订单状态(新增接口)
|
||
|
||
**Endpoint:** `GET /api/v1/assets/cast/mints/:order_id`
|
||
|
||
**归属服务:** AssetService
|
||
|
||
#### 4.5.1 响应格式
|
||
|
||
```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,
|
||
"identity_no": "ZUA-20260409-00002",
|
||
"tx_hash": null,
|
||
"error_message": null,
|
||
"created_at": 1705747200000,
|
||
"updated_at": 1705747200000
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.5.2 DTO 定义
|
||
|
||
```go
|
||
// MintOrderStatusDTO 铸造订单状态
|
||
type MintOrderStatusDTO struct {
|
||
OrderID string `json:"order_id"`
|
||
AssetID int64 `json:"asset_id"`
|
||
Status string `json:"status"` // PROCESSING / SUCCESS / FAILED
|
||
CoverURL string `json:"cover_url"`
|
||
CoverURLSigned string `json:"cover_url_signed"`
|
||
IdentityNo string `json:"identity_no"`
|
||
TxHash string `json:"tx_hash"`
|
||
ErrorMessage string `json:"error_message,omitempty"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
}
|
||
```
|
||
|
||
#### 4.5.3 Service 层实现
|
||
|
||
```go
|
||
// GetMintOrderStatus 查询铸造订单状态
|
||
func (s *CastService) GetMintOrderStatus(ctx context.Context, orderID string) (*MintOrderStatusDTO, error) {
|
||
// 查询订单
|
||
order, err := s.mintOrderRepo.FindByOrderID(ctx, orderID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if order == nil {
|
||
return nil, errors.New("订单不存在")
|
||
}
|
||
|
||
dto := &MintOrderStatusDTO{
|
||
OrderID: order.OrderID,
|
||
AssetID: order.AssetID,
|
||
Status: order.Status,
|
||
IdentityNo: order.IdentityNo,
|
||
ErrorMessage: order.ErrorMessage,
|
||
CreatedAt: order.CreatedAt,
|
||
UpdatedAt: order.UpdatedAt,
|
||
}
|
||
|
||
// 如果成功,填充图片URL
|
||
if order.Status == "SUCCESS" && order.CoverURL != "" {
|
||
dto.CoverURL = order.CoverURL
|
||
dto.CoverURLSigned, _ = s.generatePresignedURL(order.CoverURL, 3600)
|
||
dto.TxHash = order.TxHash
|
||
}
|
||
|
||
return dto, nil
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.6 铸爱藏品点赞(复用现有接口)
|
||
|
||
**Endpoint:** `POST /api/v1/social/assets/:asset_id/like`
|
||
|
||
**归属服务:** SocialService
|
||
|
||
**接口说明:** 复用现有的 `LikeAsset` 接口。扩展 `asset_likes` 表的 `asset_type` 字段,支持区分原有资产(1)和铸爱藏品(2)。
|
||
|
||
#### 4.6.1 请求格式
|
||
|
||
```json
|
||
{
|
||
"asset_id": 12345
|
||
}
|
||
```
|
||
|
||
#### 4.6.2 响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"asset_id": 12345,
|
||
"is_liked": true,
|
||
"like_count": 1000
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.6.3 DTO 定义
|
||
|
||
```go
|
||
// LikeCastAssetRequest 点赞请求
|
||
type LikeCastAssetRequest struct {
|
||
AssetID int64 `json:"asset_id" binding:"required"`
|
||
}
|
||
|
||
// LikeCastAssetResponse 点赞响应
|
||
type LikeCastAssetResponse struct {
|
||
AssetID int64 `json:"asset_id"`
|
||
IsLiked bool `json:"is_liked"`
|
||
LikeCount int32 `json:"like_count"`
|
||
}
|
||
```
|
||
|
||
#### 4.6.4 Service 层实现
|
||
|
||
```go
|
||
// LikeCastAsset 铸爱藏品点赞/取消点赞
|
||
func (s *SocialService) LikeCastAsset(ctx context.Context, userID, assetID int64) (*LikeCastAssetResponse, error) {
|
||
// 查询是否已点赞
|
||
existing, err := s.assetLikeRepo.FindByUserAndAsset(ctx, userID, assetID, 2) // asset_type=2
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var isLiked bool
|
||
if existing != nil {
|
||
// 取消点赞
|
||
if err := s.assetLikeRepo.Delete(ctx, existing.ID); err != nil {
|
||
return nil, err
|
||
}
|
||
// 减少点赞数
|
||
s.castAssetRepo.DecrLikeCount(ctx, assetID)
|
||
isLiked = false
|
||
} else {
|
||
// 添加点赞
|
||
like := &AssetLike{
|
||
UserID: userID,
|
||
AssetID: assetID,
|
||
AssetType: 2, // 铸爱藏品
|
||
CastAssetID: assetID,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
}
|
||
if err := s.assetLikeRepo.Create(ctx, like); err != nil {
|
||
return nil, err
|
||
}
|
||
// 增加点赞数
|
||
s.castAssetRepo.IncrLikeCount(ctx, assetID)
|
||
isLiked = true
|
||
}
|
||
|
||
// 获取最新点赞数
|
||
likeCount, _ := s.castAssetRepo.GetLikeCount(ctx, assetID)
|
||
|
||
return &LikeCastAssetResponse{
|
||
AssetID: assetID,
|
||
IsLiked: isLiked,
|
||
LikeCount: likeCount,
|
||
}, nil
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.7 获取铸爱类型选项(新增接口)
|
||
|
||
**Endpoint:** `GET /api/v1/assets/cast/options`
|
||
|
||
**归属服务:** AssetService
|
||
|
||
**接口说明:** 获取各类型的可选项(吧唧装饰类型、海报风格选项等),供前端下发给用户选择。
|
||
|
||
#### 4.7.1 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| cast_type | int32 | 是 | 藏品类型:1=星卡, 2=吧唧, 3=海报 |
|
||
|
||
#### 4.7.2 响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"cast_type": 2,
|
||
"options": [
|
||
{
|
||
"key": "cream_glue",
|
||
"name": "奶油胶",
|
||
"type": "decoration",
|
||
"icon_url": "https://xxx/icons/cream_glue.png"
|
||
},
|
||
{
|
||
"key": "metal_frame",
|
||
"name": "金属边框",
|
||
"type": "decoration",
|
||
"icon_url": "https://xxx/icons/metal_frame.png"
|
||
},
|
||
{
|
||
"key": "shaped",
|
||
"name": "异形裁切",
|
||
"type": "decoration",
|
||
"icon_url": "https://xxx/icons/shaped.png"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.7.3 DTO 定义
|
||
|
||
```go
|
||
// CastTypeOptionDTO 铸爱类型选项
|
||
type CastTypeOptionDTO struct {
|
||
Key string `json:"key"` // 选项标识
|
||
Name string `json:"name"` // 显示名称
|
||
Type string `json:"type"` // decoration / style
|
||
IconURL string `json:"icon_url"` // 图标
|
||
}
|
||
|
||
// CastTypeOptionsResponse 铸爱类型选项响应
|
||
type CastTypeOptionsResponse struct {
|
||
CastType int32 `json:"cast_type"`
|
||
Options []CastTypeOptionDTO `json:"options"`
|
||
}
|
||
```
|
||
|
||
#### 4.7.4 Service 层实现
|
||
|
||
```go
|
||
// GetCastTypeOptions 获取铸爱类型选项
|
||
func (s *CastService) GetCastTypeOptions(ctx context.Context, castType int32) (*CastTypeOptionsResponse, error) {
|
||
// 查询启用的选项
|
||
options, err := s.castTypeOptionRepo.FindByCastType(ctx, castType, 1)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 转换为DTO
|
||
dtos := make([]CastTypeOptionDTO, 0, len(options))
|
||
for _, opt := range options {
|
||
dtos = append(dtos, CastTypeOptionDTO{
|
||
Key: opt.OptionKey,
|
||
Name: opt.OptionName,
|
||
Type: opt.OptionType,
|
||
IconURL: opt.IconURL,
|
||
})
|
||
}
|
||
|
||
return &CastTypeOptionsResponse{
|
||
CastType: castType,
|
||
Options: dtos,
|
||
}, nil
|
||
}
|
||
```
|
||
|
||
#### 4.7.5 运维后台接口(新增)
|
||
|
||
**Endpoint:** `GET /api/v1/admin/cast/options`
|
||
|
||
| 接口 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `GET /api/v1/admin/cast/options` | GET | 获取选项列表(支持筛选cast_type) |
|
||
| `POST /api/v1/admin/cast/options` | POST | 新增选项 |
|
||
| `PUT /api/v1/admin/cast/options/:id` | PUT | 更新选项 |
|
||
| `DELETE /api/v1/admin/cast/options/:id` | DELETE | 删除选项 |
|
||
|
||
---
|
||
|
||
## 5. AI 服务对接
|
||
|
||
### 5.1 MiniMax AI 服务对接
|
||
|
||
#### 5.1.1 AI 客户端接口
|
||
|
||
```go
|
||
// AI客户端接口
|
||
type AIClient interface {
|
||
GenerateImage(ctx context.Context, req *AIGenerateRequest) (*AIGenerateResponse, error)
|
||
}
|
||
|
||
// AIGenerateRequest AI生成请求
|
||
type AIGenerateRequest struct {
|
||
Model string // 模型标识: minimax / openai / sd
|
||
InputImage string // 输入图片URL
|
||
Prompt string // 描述词
|
||
AspectRatio string // 输出尺寸比例:1:1(星卡) / landscape(海报16:9) / portrait(海报2:3) / square(海报4:3)
|
||
}
|
||
|
||
// AIGenerateResponse AI生成响应
|
||
type AIGenerateResponse struct {
|
||
ImageURL string // 生成的图片URL
|
||
RequestID string // 请求ID
|
||
}
|
||
```
|
||
|
||
#### 5.1.2 MiniMax 实现
|
||
|
||
```go
|
||
// MiniMaxAIClient MiniMax AI客户端
|
||
type MiniMaxAIClient struct {
|
||
apiKey string
|
||
apiURL string
|
||
httpClient *http.Client
|
||
}
|
||
|
||
// GenerateImage 调用MiniMax图生图API
|
||
func (c *MiniMaxAIClient) GenerateImage(ctx context.Context, req *AIGenerateRequest) (*AIGenerateResponse, error) {
|
||
// 构造请求
|
||
payload := map[string]interface{}{
|
||
"model": "minimax-vl-01",
|
||
"image_url": req.InputImage,
|
||
"prompt": req.Prompt,
|
||
"num_images": 1,
|
||
"aspect_ratio": req.AspectRatio, // 1:1(星卡) / landscape(海报16:9) / portrait(海报2:3) / square(海报4:3)
|
||
}
|
||
|
||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", c.apiURL+"/v1/image_generation", bytes.NewReader(mustMarshal(payload)))
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||
|
||
// 发送请求
|
||
resp, err := c.httpClient.Do(httpReq)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("MiniMax API error: %d - %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
// 解析响应
|
||
var result struct {
|
||
Data []struct {
|
||
ImageURL string `json:"image_url"`
|
||
} `json:"data"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(result.Data) == 0 {
|
||
return nil, errors.New("AI返回数据为空")
|
||
}
|
||
|
||
return &AIGenerateResponse{
|
||
ImageURL: result.Data[0].ImageURL,
|
||
RequestID: resp.Header.Get("X-Request-ID"),
|
||
}, nil
|
||
}
|
||
```
|
||
|
||
### 5.2 多模型切换策略(后续扩展)
|
||
|
||
```go
|
||
// MultiModelAIClient 多模型AI客户端(后续扩展)
|
||
type MultiModelAIClient struct {
|
||
clients map[string]AIClient // 模型客户端Map
|
||
defaultModel string
|
||
}
|
||
|
||
// GenerateImage 根据模型生成图片
|
||
func (c *MultiModelAIClient) GenerateImage(ctx context.Context, req *AIGenerateRequest) (*AIGenerateResponse, error) {
|
||
model := req.Model
|
||
if model == "" {
|
||
model = c.defaultModel
|
||
}
|
||
|
||
client, ok := c.clients[model]
|
||
if !ok {
|
||
return nil, fmt.Errorf("不支持的AI模型: %s", model)
|
||
}
|
||
|
||
return client.GenerateImage(ctx, req)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 服务间调用关系
|
||
|
||
### 6.1 调用链
|
||
|
||
```
|
||
API Gateway
|
||
│
|
||
├── ActivityService (gRPC)
|
||
│ └── GetBanners(position) → BannerDTO[]
|
||
│
|
||
├── AssetService (gRPC)
|
||
│ ├── GetCastItems() → CastAssetListResponse
|
||
│ ├── GetCastAssetDetail() → CastAssetDetailDTO
|
||
│ ├── CreateCastMint() → CreateCastMintResponse
|
||
│ │ │
|
||
│ │ ├── UserService.UpdateCrystalBalance() 扣水晶
|
||
│ │ └── 启动异步任务 processCastAIGeneration()
|
||
│ └── GetMintOrderStatus() → MintOrderStatusDTO
|
||
│
|
||
└── SocialService (gRPC)
|
||
├── GetUserLikedCastAssets() → assetID[]
|
||
├── IsUserLikedCastAsset() → bool
|
||
└── LikeCastAsset() → LikeCastAssetResponse
|
||
```
|
||
|
||
### 6.2 gRPC Proto 定义
|
||
|
||
```protobuf
|
||
// AssetService Proto
|
||
service AssetService {
|
||
// 获取铸爱创作列表
|
||
rpc GetCastItems(GetCastItemsRequest) returns (CastAssetListResponse);
|
||
|
||
// 获取铸爱类型选项(吧唧装饰/海报风格)
|
||
rpc GetCastTypeOptions(GetCastTypeOptionsRequest) returns (CastTypeOptionsResponse);
|
||
|
||
// 获取铸爱藏品详情
|
||
rpc GetCastAssetDetail(GetCastAssetDetailRequest) returns (CastAssetDetailDTO);
|
||
|
||
// 创建铸爱铸造订单
|
||
rpc CreateCastMint(CreateCastMintRequest) returns (CreateCastMintResponse);
|
||
|
||
// 查询铸造订单状态
|
||
rpc GetMintOrderStatus(GetMintOrderStatusRequest) returns (MintOrderStatusDTO);
|
||
}
|
||
|
||
// SocialService Proto
|
||
service SocialService {
|
||
// 铸爱藏品点赞
|
||
rpc LikeCastAsset(LikeCastAssetRequest) returns (LikeCastAssetResponse);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 前端对接说明
|
||
|
||
### 7.1 页面路由
|
||
|
||
| 页面 | 路由 | 对应接口 |
|
||
|------|------|----------|
|
||
| 铸爱首页 | `/cast` | `GetBanners`, `GetCastItems` |
|
||
| 铸造页面 | `/cast/create/:type` | `CreateCastMintOrder`, `GetCastMintStatus` |
|
||
| AI处理中 | `/cast/processing/:order_id` | `GetMintOrderStatus`(轮询) |
|
||
| 结果预览 | `/cast/result/:order_id` | `GetMintOrderStatus` |
|
||
| 创作详情 | `/cast/detail/:asset_id` | `GetCastAssetDetail`, `LikeCastAsset` |
|
||
|
||
### 7.2 前端轮询策略
|
||
|
||
```javascript
|
||
// 铸造订单状态轮询
|
||
async function pollMintStatus(orderId) {
|
||
const maxAttempts = 60; // 最多60次
|
||
const interval = 3000; // 3秒
|
||
|
||
for (let i = 0; i < maxAttempts; i++) {
|
||
const resp = await api.getCastMintStatus(orderId);
|
||
const { status, cover_url, error_message } = resp.data;
|
||
|
||
if (status === 'SUCCESS') {
|
||
// 铸造成功,跳转结果页
|
||
router.push(`/cast/result/${orderId}`);
|
||
return;
|
||
}
|
||
|
||
if (status === 'FAILED') {
|
||
// 铸造失败,显示错误
|
||
showToast(error_message || '铸造失败');
|
||
return;
|
||
}
|
||
|
||
// 继续等待
|
||
await sleep(interval);
|
||
}
|
||
|
||
// 超时
|
||
showToast('铸造超时,请稍后重试');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 待确认事项
|
||
|
||
| 问题 | 说明 | 状态 |
|
||
|------|------|------|
|
||
| 铸造费用 | 铸爱创作是否需要消耗水晶?收费标准? | 待确认 |
|
||
| AI 模型 | 初期 MiniMax,后续多模型切换 | 已确认 |
|
||
| 多模型切换 | 后续扩展时,模型切换策略(用户可选/后台配置)? | 待确认 |
|
||
| 评论功能 | 是否需要评论功能? | 待确认 |
|
||
| 分享功能 | 分享到外部平台的规则? | 待确认 |
|
||
|
||
---
|
||
|
||
## 9. 实现计划
|
||
|
||
### Phase 1:基础设施(1-2天)
|
||
- [ ] 创建数据库表 `cast_assets`、`cast_mint_orders`
|
||
- [ ] 在 AssetService 中新增 Cast 相关方法
|
||
- [ ] 在 ActivityService 中新增 Banner 轮播接口
|
||
- [ ] 扩展 asset_likes 表支持 asset_type 字段
|
||
|
||
### Phase 2:核心功能(3-5天)
|
||
- [x] 实现 `GET /api/v1/activities/banners` ← **新增**
|
||
- [x] 实现 `GET /api/v1/assets/cast/items` ← **新增**
|
||
- [ ] 实现 `GET /api/v1/assets/:asset_id` ← **复用现有接口**,扩展支持铸爱藏品查询
|
||
- [x] 实现 `POST /api/v1/assets/cast/mints` ← **新增**
|
||
- [x] 实现 `GET /api/v1/assets/cast/mints/:order_id` ← **新增**
|
||
- [x] 实现 AI 异步生成任务(MiniMax 图生图)← **新增**
|
||
|
||
### Phase 3:社交功能(1-2天)
|
||
- [ ] 实现 `POST /api/v1/social/assets/:asset_id/like` ← **复用现有接口**,扩展支持铸爱藏品点赞
|
||
|
||
### Phase 4:联调测试(2-3天)
|
||
- [ ] 前端页面联调
|
||
- [ ] 全流程测试
|
||
- [ ] 性能测试
|
||
|
||
---
|
||
|
||
**接口状态汇总:**
|
||
|
||
| 接口 | 状态 | 说明 |
|
||
|------|------|------|
|
||
| `GET /api/v1/activities/banners` | 新增 | 轮播图列表 |
|
||
| `GET /api/v1/assets/cast/items` | 新增 | 铸爱创作列表 |
|
||
| `GET /api/v1/assets/cast/options` | 新增 | 铸爱类型选项 |
|
||
| `GET /api/v1/assets/:asset_id` | 复用 | 藏品详情(需扩展) |
|
||
| `POST /api/v1/assets/cast/mints` | 新增 | 创建铸造订单 |
|
||
| `GET /api/v1/assets/cast/mints/:order_id` | 新增 | 查询订单状态 |
|
||
| `POST /api/v1/social/assets/:asset_id/like` | 复用 | 点赞(需扩展) |
|
||
|
||
---
|
||
|
||
## 10. 参考文档
|
||
|
||
| 文档 | 说明 |
|
||
|------|------|
|
||
| `docs/资产铸造AI生成流程设计文档.md` | 现有AI铸造流程设计参考 |
|
||
| `PROJECT_SUMMARY.md` | 项目整体架构说明 |
|
||
| `docs/资产服务设计文档.md` | AssetService 设计说明 |
|
||
|
||
---
|
||
|
||
> **下一步行动:** 请确认以上设计决策,特别是铸造费用和AI模型对接方案。
|