48 KiB
铸爱创作模块 技术设计文档
文档版本: 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
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
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)
扩展现有表,新增字段:
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生成。
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 响应格式
{
"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 定义
// 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 层实现
// 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 层实现
// 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 响应格式
{
"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 定义
// 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 层实现
// 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 层实现
// 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 响应格式
{
"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 定义
// 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 层实现
// 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 请求格式
{
"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 响应格式
{
"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 定义
// 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 层实现(核心流程)
// 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生成任务(核心代码)
// 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 身份证编号生成
// 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 响应格式
{
"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 定义
// 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 层实现
// 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 请求格式
{
"asset_id": 12345
}
4.6.2 响应格式
{
"code": 200,
"message": "success",
"data": {
"asset_id": 12345,
"is_liked": true,
"like_count": 1000
}
}
4.6.3 DTO 定义
// 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 层实现
// 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 响应格式
{
"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 定义
// 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 层实现
// 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 客户端接口
// 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 实现
// 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 多模型切换策略(后续扩展)
// 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 定义
// 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 前端轮询策略
// 铸造订单状态轮询
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天)
- 实现
GET /api/v1/activities/banners← 新增 - 实现
GET /api/v1/assets/cast/items← 新增 - 实现
GET /api/v1/assets/:asset_id← 复用现有接口,扩展支持铸爱藏品查询 - 实现
POST /api/v1/assets/cast/mints← 新增 - 实现
GET /api/v1/assets/cast/mints/:order_id← 新增 - 实现 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模型对接方案。