# 铸爱创作模块 技术设计文档 > **文档版本:** 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模型对接方案。