topfans/backend/docs/数字藏品功能后续实现计划.md
2026-04-07 22:29:48 +08:00

24 KiB
Raw Permalink Blame History

数字藏品功能后续实现计划

📋 文档说明

本文档详细说明了数字藏品功能的后续实现计划,包括具体的实现步骤、代码修改点和测试方案。

创建日期: 2026-01-12
维护者: AI Assistant
状态: 🟡 待实现


📊 实现任务总览

任务 优先级 预计工作量 状态
1. 修改获取我的藏品列表路径 🔴 P0 0.5小时 待实现
2. 添加点赞数字段到列表响应 🔴 P0 1小时 待实现
3. 实现图片上传接口URL方式 🔴 P0 1-2小时 待实现
4. 实现取消铸造功能 🔴 P0 2-3小时 待实现

总预计工作量: 4.5-6.5小时


🎯 任务1: 修改获取我的藏品列表路径

需求说明

  • 当前路径: GET /api/v1/assets/me
  • 目标路径: GET /api/v1/assets/me/items
  • 说明: 仅修改路径,功能保持不变

实现步骤

1.1 修改路由配置

文件: gateway/router/router.go

修改内容:

// 资产相关路由(需要认证)
assets := v1.Group("/assets")
assets.Use(middleware.AuthMiddleware())
{
    assets.POST("/mints", assetCtrl.CreateMintOrder)           // 创建铸造订单
    assets.GET("/me/items", assetCtrl.GetMyAssets)             // 获取我的藏品列表(修改路径)
    assets.GET("/:asset_id", assetCtrl.GetAsset)               // 获取资产详情
    assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus)  // 查询上链状态
}

注意: 路径 /me/items 必须在 /:asset_id 之前定义,避免路由冲突。


🎯 任务2: 添加点赞数字段到列表响应

需求说明

  • GetMyAssets 接口的响应中,每个列表项需要包含 like_count 字段
  • 点赞数已经在 Asset 模型中存在,需要确保在列表响应中返回

实现步骤

2.1 检查Proto定义

文件: proto/asset.proto

当前定义:

// 资产列表项(简化版,用于列表展示)
message AssetListItem {
  int64 asset_id = 1;             // 资产ID
  string name = 2;                // 藏品名称
  string cover_url = 3;           // 封面图URL
  string status = 4;              // 状态pending, minting, minted, failed
  string tx_hash = 5;             // 交易哈希(可选)
  int64 created_at = 6;           // 创建时间(毫秒时间戳)
  int64 minted_at = 7;            // 上链成功时间(毫秒时间戳,可选)
}

需要添加:

message AssetListItem {
  int64 asset_id = 1;             // 资产ID
  string name = 2;                // 藏品名称
  string cover_url = 3;           // 封面图URL
  string status = 4;              // 状态pending, minting, minted, failed
  string tx_hash = 5;             // 交易哈希(可选)
  int64 created_at = 6;           // 创建时间(毫秒时间戳)
  int64 minted_at = 7;            // 上链成功时间(毫秒时间戳,可选)
  int32 like_count = 8;           // 点赞数(新增)
}

2.2 重新编译Proto

cd /Users/haihuizhu/infinite_matrix/TopFans/backend
./scripts/compile-proto.sh

2.3 修改Repository层

文件: services/assetService/repository/asset_repository.go

检查 GetMyAssets 方法是否已经查询了 like_count 字段。如果使用 Select 查询,需要确保包含 like_count

示例:

// 确保查询包含 like_count 字段
var assets []models.Asset
err := r.db.
    Select("id", "name", "cover_url", "status", "tx_hash", "created_at", "minted_at", "like_count").
    Where("owner_uid = ? AND star_id = ?", userID, starID).
    // ... 其他查询条件
    Find(&assets).Error

2.4 修改Service层

文件: services/assetService/service/asset_service.go

检查 GetMyAssets 方法,确保在构建 AssetListItem 时包含 like_count

2.5 修改Provider层

文件: services/assetService/provider/asset_provider.go

检查 GetMyAssets 方法,确保在转换响应时包含 like_count

示例:

items := make([]*pbAsset.AssetListItem, 0, len(assets))
for _, asset := range assets {
    items = append(items, &pbAsset.AssetListItem{
        AssetId:   asset.ID,
        Name:      asset.Name,
        CoverUrl:  asset.CoverURL,
        Status:    convertAssetStatus(asset.Status),
        TxHash:    getStringValue(asset.TxHash),
        CreatedAt: asset.CreatedAt,
        MintedAt:  getInt64Value(asset.MintedAt),
        LikeCount: asset.LikeCount,  // 新增
    })
}

2.6 修改DTO转换

文件: gateway/dto/asset_dto.go

修改:

// AssetListItemDTO 资产列表项(简化版,用于列表展示)
type AssetListItemDTO struct {
    AssetID   int64  `json:"asset_id"`            // 资产ID
    Name      string `json:"name"`                // 藏品名称
    CoverURL  string `json:"cover_url"`           // 封面图URL
    Status    string `json:"status"`              // 状态pending, minting, minted, failed
    TxHash    string `json:"tx_hash,omitempty"`   // 交易哈希(可选)
    CreatedAt int64  `json:"created_at"`          // 创建时间(毫秒时间戳)
    MintedAt  int64  `json:"minted_at,omitempty"` // 上链成功时间(毫秒时间戳,可选)
    LikeCount int32  `json:"like_count"`          // 点赞数(新增)
}

文件: gateway/dto/asset_converter.go

修改:

// ConvertAssetListItem 转换资产列表项
func ConvertAssetListItem(pbItem *pbAsset.AssetListItem) AssetListItemDTO {
    dto := AssetListItemDTO{
        AssetID:   pbItem.AssetId,
        Name:      pbItem.Name,
        CoverURL:  pbItem.CoverUrl,
        Status:    pbItem.Status,
        CreatedAt: pbItem.CreatedAt,
        LikeCount: pbItem.LikeCount,  // 新增
    }

    // 可选字段
    if pbItem.TxHash != "" {
        dto.TxHash = pbItem.TxHash
    }
    if pbItem.MintedAt > 0 {
        dto.MintedAt = pbItem.MintedAt
    }

    return dto
}

🎯 任务3: 实现图片上传接口URL方式

需求说明

  • 接口路径: POST /api/v1/assets/upload
  • 功能: 接收图片URL返回处理后的URL目前暂定为直接返回后续可扩展审核和AI生成
  • 说明: 目前不需要实际上传文件只需要接收URL并返回

实现步骤

3.1 添加Proto定义可选

文件: proto/asset.proto

添加:

// 上传图片请求
message UploadImageRequest {
  string image_url = 1;  // 图片URL必填
  string type = 2;       // 上传类型cover, material可选默认cover
}

// 上传图片响应
message UploadImageResponse {
  topfans.common.BaseResponse base = 1;
  string pic_url = 2;     // 处理后的图片URL
  string upload_id = 3;   // 上传ID可选用于后续取消
}

// 在 AssetService 中添加
service AssetService {
  // ... 现有方法
  
  // 上传图片
  rpc UploadImage(UploadImageRequest) returns (UploadImageResponse) {
    option (google.api.http) = {
      post: "/api/v1/assets/upload"
      body: "*"
    };
  }
}

注意: 如果不想修改Proto可以直接在Gateway层实现不调用AssetService。

3.2 实现Gateway层接口推荐方案

文件: gateway/controller/asset_controller.go

添加方法:

// UploadImage 上传图片URL方式
// POST /api/v1/assets/upload
func (ctrl *AssetController) UploadImage(c *gin.Context) {
    // 解析请求参数
    var req struct {
        ImageURL string `json:"image_url" binding:"required"`
        Type     string `json:"type"` // cover, material默认cover
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        response.Error(c, http.StatusBadRequest, "参数错误: "+err.Error())
        return
    }

    // 验证URL格式
    if !isValidURL(req.ImageURL) {
        response.Error(c, http.StatusBadRequest, "无效的图片URL")
        return
    }

    // 设置默认类型
    if req.Type == "" {
        req.Type = "cover"
    }

    // 验证类型
    if req.Type != "cover" && req.Type != "material" {
        response.Error(c, http.StatusBadRequest, "type必须是cover或material")
        return
    }

    // TODO: 后续可以添加图片审核逻辑
    // TODO: 后续可以添加AI生成逻辑
    
    // 目前直接返回原URL
    // 后续可以:
    // 1. 下载图片到本地/OSS
    // 2. 进行审核
    // 3. AI处理
    // 4. 返回处理后的URL

    // 生成上传ID可选
    uploadID := generateUploadID()

    response.Success(c, gin.H{
        "pic_url":   req.ImageURL,  // 目前直接返回原URL
        "upload_id": uploadID,      // 上传ID用于后续取消
    })
}

// isValidURL 验证URL格式
func isValidURL(url string) bool {
    u, err := url.Parse(url)
    return err == nil && u.Scheme != "" && u.Host != ""
}

// generateUploadID 生成上传ID
func generateUploadID() string {
    return fmt.Sprintf("upload_%d_%s", time.Now().Unix(), uuid.New().String()[:8])
}

需要添加的导入:

import (
    "net/url"
    "time"
    "github.com/google/uuid"
)

3.3 添加DTO定义

文件: gateway/dto/asset_dto.go

添加:

// UploadImageRequestDTO 上传图片请求
type UploadImageRequestDTO struct {
    ImageURL string `json:"image_url" binding:"required"` // 图片URL必填
    Type     string `json:"type"`                         // 上传类型cover, material可选默认cover
}

// UploadImageResponseDTO 上传图片响应
type UploadImageResponseDTO struct {
    PicURL   string `json:"pic_url"`   // 处理后的图片URL
    UploadID string `json:"upload_id"`  // 上传ID可选用于后续取消
}

3.4 添加路由

文件: gateway/router/router.go

修改:

// 资产相关路由(需要认证)
assets := v1.Group("/assets")
assets.Use(middleware.AuthMiddleware())
{
    assets.POST("/upload", assetCtrl.UploadImage)           // 上传图片(新增)
    assets.POST("/mints", assetCtrl.CreateMintOrder)       // 创建铸造订单
    assets.GET("/me/items", assetCtrl.GetMyAssets)         // 获取我的藏品列表
    assets.GET("/:asset_id", assetCtrl.GetAsset)           // 获取资产详情
    assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus)  // 查询上链状态
}

注意: /upload 路由必须在 /:asset_id 之前定义,避免路由冲突。


🎯 任务4: 实现取消铸造功能

需求说明

  • 接口路径: DELETE /api/v1/assets/mints/:order_id
  • 功能: 取消指定的铸造订单
  • 业务规则:
    • 只能取消状态为 PENDINGPROCESSING 的订单
    • 不能取消已成功(SUCCESS)或已失败(FAILED)的订单
    • 不需要退回水晶因为只有在创建mint订单时才扣除
    • 取消后订单状态变为 CANCELLED

实现步骤

4.1 添加Proto定义

文件: proto/asset.proto

添加:

// 取消铸造订单请求
message CancelMintOrderRequest {
  string order_id = 1;  // 订单ID必填
}

// 取消铸造订单响应
message CancelMintOrderResponse {
  topfans.common.BaseResponse base = 1;
  string order_id = 2;   // 订单ID
  string status = 3;     // 订单状态CANCELLED
}

// 在 AssetService 中添加
service AssetService {
  // ... 现有方法
  
  // 取消铸造订单
  rpc CancelMintOrder(CancelMintOrderRequest) returns (CancelMintOrderResponse) {
    option (google.api.http) = {
      delete: "/api/v1/assets/mints/{order_id}"
    };
  }
}

4.2 更新数据库模型(如果需要)

文件: pkg/models/asset.go

检查状态常量:

// 铸造订单状态常量
const (
    MintOrderStatusPending    = "PENDING"    // 待处理
    MintOrderStatusProcessing = "PROCESSING" // 处理中(上链中,目前模拟)
    MintOrderStatusSuccess    = "SUCCESS"    // 成功
    MintOrderStatusFailed     = "FAILED"     // 失败
    MintOrderStatusCancelled  = "CANCELLED"  // 已取消(新增)
)

4.3 实现Repository层

文件: services/assetService/repository/mint_order_repository.go

添加方法:

// CancelOrder 取消订单
func (r *mintOrderRepository) CancelOrder(orderID string) error {
    if orderID == "" {
        return errors.New("order_id cannot be empty")
    }

    // 更新订单状态为CANCELLED
    result := r.db.Model(&models.MintOrder{}).
        Where("order_id = ?", orderID).
        Updates(map[string]interface{}{
            "status":    models.MintOrderStatusCancelled,
            "updated_at": time.Now().UnixMilli(),
        })

    if result.Error != nil {
        return result.Error
    }

    if result.RowsAffected == 0 {
        return appErrors.NewNotFoundError("订单不存在")
    }

    return nil
}

更新接口定义:

// MintOrderRepository 铸造订单Repository接口
type MintOrderRepository interface {
    // ... 现有方法
    
    // CancelOrder 取消订单
    CancelOrder(orderID string) error
}

4.4 实现Service层

文件: services/assetService/service/mint_service.go

添加方法:

// CancelMintOrder 取消铸造订单
func (s *mintService) CancelMintOrder(orderID string, userID, starID int64) error {
    // 1. 查询订单
    order, err := s.mintOrderRepo.GetByOrderIDAndUser(orderID, userID, starID)
    if err != nil {
        return err
    }

    // 2. 验证订单状态
    if order.Status == models.MintOrderStatusSuccess {
        return appErrors.NewBadRequestError("已上链成功的订单不能取消")
    }

    if order.Status == models.MintOrderStatusFailed {
        return appErrors.NewBadRequestError("已失败的订单不能取消")
    }

    if order.Status == models.MintOrderStatusCancelled {
        return appErrors.NewBadRequestError("订单已取消")
    }

    // 3. 只能取消 PENDING 或 PROCESSING 状态的订单
    if order.Status != models.MintOrderStatusPending && 
       order.Status != models.MintOrderStatusProcessing {
        return appErrors.NewBadRequestError("订单状态不允许取消")
    }

    // 4. 取消订单(不需要退回水晶,因为创建时已扣除)
    if err := s.mintOrderRepo.CancelOrder(orderID); err != nil {
        return err
    }

    // 5. 如果订单已关联资产,可以选择删除资产(可选)
    // 根据业务需求决定是否删除资产
    // if order.AssetID != nil {
    //     // 删除资产逻辑
    // }

    return nil
}

4.5 实现Provider层

文件: services/assetService/provider/asset_provider.go

添加方法:

// CancelMintOrder 取消铸造订单
func (p *AssetProvider) CancelMintOrder(ctx context.Context, req *pb.CancelMintOrderRequest) (*pb.CancelMintOrderResponse, error) {
    logger.Logger.Info("Received CancelMintOrder request",
        zap.String("order_id", req.OrderId),
    )

    // 从 Dubbo attachments 获取用户信息
    userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
    if err != nil {
        logger.Logger.Error("Failed to extract user info from attachments",
            zap.Error(err),
        )
        return &pb.CancelMintOrderResponse{
            Base: &pbCommon.BaseResponse{
                Code:      pbCommon.StatusCode_STATUS_UNAUTHORIZED,
                Message:   "user authentication required",
                Timestamp: 0,
            },
        }, err
    }

    // 调用Service层
    err = p.mintService.CancelMintOrder(req.OrderId, userID, starID)
    if err != nil {
        logger.Logger.Error("CancelMintOrder failed",
            zap.Int64("user_id", userID),
            zap.Int64("star_id", starID),
            zap.String("order_id", req.OrderId),
            zap.Error(err),
        )

        return &pb.CancelMintOrderResponse{
            Base: &pbCommon.BaseResponse{
                Code:      appErrors.ToStatusCode(err),
                Message:   err.Error(),
                Timestamp: 0,
            },
        }, err
    }

    logger.Logger.Info("CancelMintOrder successful",
        zap.Int64("user_id", userID),
        zap.Int64("star_id", starID),
        zap.String("order_id", req.OrderId),
    )

    return &pb.CancelMintOrderResponse{
        Base: &pbCommon.BaseResponse{
            Code:      pbCommon.StatusCode_STATUS_OK,
            Message:   "订单已取消",
            Timestamp: time.Now().UnixMilli(),
        },
        OrderId: req.OrderId,
        Status:  "CANCELLED",
    }, nil
}

4.6 实现Gateway层

文件: gateway/controller/asset_controller.go

添加方法:

// CancelMintOrder 取消铸造订单
// DELETE /api/v1/assets/mints/:order_id
func (ctrl *AssetController) CancelMintOrder(c *gin.Context) {
    userID, _ := c.Get("user_id")
    starID, _ := c.Get("star_id")

    // 解析路径参数
    orderID := c.Param("order_id")
    if orderID == "" {
        response.Error(c, http.StatusBadRequest, "参数错误: order_id 不能为空")
        return
    }

    // 设置上下文
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
        "user_id": strconv.FormatInt(userID.(int64), 10),
        "star_id": strconv.FormatInt(starID.(int64), 10),
    })

    // 调用 RPC
    resp, err := ctrl.assetService.CancelMintOrder(ctx, &pbAsset.CancelMintOrderRequest{
        OrderId: orderID,
    })

    if err != nil {
        logger.Logger.Error("CancelMintOrder RPC failed",
            zap.String("order_id", orderID),
            zap.Error(err),
        )
        response.Error(c, http.StatusInternalServerError, "服务调用失败")
        return
    }

    if resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
        response.ErrorWithCode(c, int(resp.Base.Code), resp.Base.Message)
        return
    }

    // 转换响应
    data := dto.ConvertCancelMintOrderResponse(resp)
    response.Success(c, data)
}

4.7 添加DTO定义

文件: gateway/dto/asset_dto.go

添加:

// CancelMintOrderResponseDTO 取消铸造订单响应
type CancelMintOrderResponseDTO struct {
    OrderID string `json:"order_id"` // 订单ID
    Status  string `json:"status"`   // 订单状态CANCELLED
}

文件: gateway/dto/asset_converter.go

添加:

// ConvertCancelMintOrderResponse 转换取消铸造订单响应
func ConvertCancelMintOrderResponse(pbResp *pbAsset.CancelMintOrderResponse) *CancelMintOrderResponseDTO {
    if pbResp == nil {
        return nil
    }

    return &CancelMintOrderResponseDTO{
        OrderID: pbResp.OrderId,
        Status:  pbResp.Status,
    }
}

4.8 添加路由

文件: gateway/router/router.go

修改:

// 资产相关路由(需要认证)
assets := v1.Group("/assets")
assets.Use(middleware.AuthMiddleware())
{
    assets.POST("/upload", assetCtrl.UploadImage)                    // 上传图片
    assets.POST("/mints", assetCtrl.CreateMintOrder)                // 创建铸造订单
    assets.DELETE("/mints/:order_id", assetCtrl.CancelMintOrder)   // 取消铸造订单(新增)
    assets.GET("/me/items", assetCtrl.GetMyAssets)                  // 获取我的藏品列表
    assets.GET("/:asset_id", assetCtrl.GetAsset)                    // 获取资产详情
    assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus)      // 查询上链状态
}

注意: /mints/:order_id 路由必须在 /:asset_id 之前定义,避免路由冲突。

4.9 重新编译Proto

cd /Users/haihuizhu/infinite_matrix/TopFans/backend
./scripts/compile-proto.sh

📋 实现检查清单

任务1: 修改路径

  • 修改 gateway/router/router.go 中的路由路径
  • 测试新路径是否正常工作

任务2: 添加点赞数

  • 修改 proto/asset.proto 添加 like_count 字段
  • 重新编译Proto
  • 检查Repository层是否查询了 like_count
  • 检查Service层是否返回了 like_count
  • 检查Provider层是否转换了 like_count
  • 修改 gateway/dto/asset_dto.go 添加字段
  • 修改 gateway/dto/asset_converter.go 添加转换
  • 测试接口返回是否包含 like_count

任务3: 图片上传接口

  • gateway/controller/asset_controller.go 添加 UploadImage 方法
  • gateway/dto/asset_dto.go 添加DTO定义
  • gateway/router/router.go 添加路由
  • 测试上传接口

任务4: 取消铸造功能

  • 修改 proto/asset.proto 添加RPC定义
  • 更新 pkg/models/asset.go 添加 CANCELLED 状态
  • repository/mint_order_repository.go 添加 CancelOrder 方法
  • service/mint_service.go 添加 CancelMintOrder 方法
  • provider/asset_provider.go 添加 CancelMintOrder 方法
  • gateway/controller/asset_controller.go 添加 CancelMintOrder 方法
  • gateway/dto/asset_dto.go 添加DTO定义
  • gateway/dto/asset_converter.go 添加转换
  • gateway/router/router.go 添加路由
  • 重新编译Proto
  • 测试取消功能

🧪 测试方案

测试1: 修改路径测试

# 测试新路径
curl -X GET "http://localhost:8080/api/v1/assets/me/items?page=1&page_size=20" \
  -H "Authorization: Bearer YOUR_TOKEN"

测试2: 点赞数字段测试

# 验证响应中包含 like_count
curl -X GET "http://localhost:8080/api/v1/assets/me/items" \
  -H "Authorization: Bearer YOUR_TOKEN" | jq '.data.items[0].like_count'

测试3: 图片上传测试

# 测试上传接口
curl -X POST http://localhost:8080/api/v1/assets/upload \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/image.jpg",
    "type": "cover"
  }'

预期响应:

{
  "code": 200,
  "message": "ok",
  "data": {
    "pic_url": "https://example.com/image.jpg",
    "upload_id": "upload_1736234567_abc12345"
  }
}

测试4: 取消铸造测试

# 1. 先创建一个订单
ORDER_ID=$(curl -X POST http://localhost:8080/api/v1/assets/mints \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "测试藏品",
    "cover_url": "https://example.com/cover.jpg"
  }' | jq -r '.data.order.order_id')

# 2. 取消订单
curl -X DELETE "http://localhost:8080/api/v1/assets/mints/${ORDER_ID}" \
  -H "Authorization: Bearer YOUR_TOKEN"

预期响应:

{
  "code": 200,
  "message": "ok",
  "data": {
    "order_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "CANCELLED"
  }
}

测试5: 取消铸造边界测试

# 测试取消已成功的订单(应该失败)
curl -X DELETE "http://localhost:8080/api/v1/assets/mints/SUCCESS_ORDER_ID" \
  -H "Authorization: Bearer YOUR_TOKEN"

# 预期: 返回错误,提示"已上链成功的订单不能取消"

⚠️ 注意事项

  1. 路由顺序:

    • /upload 必须在 /:asset_id 之前
    • /mints/:order_id 必须在 /:asset_id 之前
    • /me/items 必须在 /:asset_id 之前
  2. Proto编译:

    • 修改Proto后必须重新编译
    • 编译命令: ./scripts/compile-proto.sh
  3. 状态管理:

    • 取消订单时不需要退回水晶
    • 只能取消 PENDINGPROCESSING 状态的订单
  4. 图片上传:

    • 目前只是接收URL并返回不进行实际处理
    • 后续可以扩展审核和AI生成功能
  5. 点赞数:

    • 确保数据库查询包含 like_count 字段
    • 确保所有层都正确传递和转换

📝 后续扩展(可选)

图片上传功能扩展

  • 实际文件上传multipart/form-data
  • 图片存储到OSS/S3
  • 图片审核功能
  • AI生成封面功能

取消铸造功能扩展

  • 取消时删除关联资产(可选)
  • 取消原因记录
  • 取消通知功能

文档版本: v1.0
最后更新: 2026-01-12
维护者: AI Assistant