topfans/backend/docs/铸爱创作模块技术设计文档.md
2026-04-10 16:17:45 +08:00

48 KiB
Raw Blame History

铸爱创作模块 技术设计文档

文档版本: 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 '订单IDUUID',
    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 '铸爱藏品IDasset_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_typestyle_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_assetscast_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模型对接方案。