827 lines
24 KiB
Markdown
827 lines
24 KiB
Markdown
# 数字藏品功能后续实现计划
|
||
|
||
## 📋 文档说明
|
||
|
||
本文档详细说明了数字藏品功能的后续实现计划,包括具体的实现步骤、代码修改点和测试方案。
|
||
|
||
**创建日期**: 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`
|
||
|
||
**修改内容**:
|
||
```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`
|
||
|
||
**当前定义**:
|
||
```protobuf
|
||
// 资产列表项(简化版,用于列表展示)
|
||
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; // 上链成功时间(毫秒时间戳,可选)
|
||
}
|
||
```
|
||
|
||
**需要添加**:
|
||
```protobuf
|
||
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
|
||
```bash
|
||
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`。
|
||
|
||
**示例**:
|
||
```go
|
||
// 确保查询包含 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`。
|
||
|
||
**示例**:
|
||
```go
|
||
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`
|
||
|
||
**修改**:
|
||
```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`
|
||
|
||
**修改**:
|
||
```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`
|
||
|
||
**添加**:
|
||
```protobuf
|
||
// 上传图片请求
|
||
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`
|
||
|
||
**添加方法**:
|
||
```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])
|
||
}
|
||
```
|
||
|
||
**需要添加的导入**:
|
||
```go
|
||
import (
|
||
"net/url"
|
||
"time"
|
||
"github.com/google/uuid"
|
||
)
|
||
```
|
||
|
||
#### 3.3 添加DTO定义
|
||
**文件**: `gateway/dto/asset_dto.go`
|
||
|
||
**添加**:
|
||
```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`
|
||
|
||
**修改**:
|
||
```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`
|
||
- **功能**: 取消指定的铸造订单
|
||
- **业务规则**:
|
||
- 只能取消状态为 `PENDING` 或 `PROCESSING` 的订单
|
||
- 不能取消已成功(`SUCCESS`)或已失败(`FAILED`)的订单
|
||
- **不需要退回水晶**(因为只有在创建mint订单时才扣除)
|
||
- 取消后订单状态变为 `CANCELLED`
|
||
|
||
### 实现步骤
|
||
|
||
#### 4.1 添加Proto定义
|
||
**文件**: `proto/asset.proto`
|
||
|
||
**添加**:
|
||
```protobuf
|
||
// 取消铸造订单请求
|
||
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`
|
||
|
||
**检查状态常量**:
|
||
```go
|
||
// 铸造订单状态常量
|
||
const (
|
||
MintOrderStatusPending = "PENDING" // 待处理
|
||
MintOrderStatusProcessing = "PROCESSING" // 处理中(上链中,目前模拟)
|
||
MintOrderStatusSuccess = "SUCCESS" // 成功
|
||
MintOrderStatusFailed = "FAILED" // 失败
|
||
MintOrderStatusCancelled = "CANCELLED" // 已取消(新增)
|
||
)
|
||
```
|
||
|
||
#### 4.3 实现Repository层
|
||
**文件**: `services/assetService/repository/mint_order_repository.go`
|
||
|
||
**添加方法**:
|
||
```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
|
||
}
|
||
```
|
||
|
||
**更新接口定义**:
|
||
```go
|
||
// MintOrderRepository 铸造订单Repository接口
|
||
type MintOrderRepository interface {
|
||
// ... 现有方法
|
||
|
||
// CancelOrder 取消订单
|
||
CancelOrder(orderID string) error
|
||
}
|
||
```
|
||
|
||
#### 4.4 实现Service层
|
||
**文件**: `services/assetService/service/mint_service.go`
|
||
|
||
**添加方法**:
|
||
```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`
|
||
|
||
**添加方法**:
|
||
```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`
|
||
|
||
**添加方法**:
|
||
```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`
|
||
|
||
**添加**:
|
||
```go
|
||
// CancelMintOrderResponseDTO 取消铸造订单响应
|
||
type CancelMintOrderResponseDTO struct {
|
||
OrderID string `json:"order_id"` // 订单ID
|
||
Status string `json:"status"` // 订单状态(CANCELLED)
|
||
}
|
||
```
|
||
|
||
**文件**: `gateway/dto/asset_converter.go`
|
||
|
||
**添加**:
|
||
```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`
|
||
|
||
**修改**:
|
||
```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
|
||
```bash
|
||
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: 修改路径测试
|
||
```bash
|
||
# 测试新路径
|
||
curl -X GET "http://localhost:8080/api/v1/assets/me/items?page=1&page_size=20" \
|
||
-H "Authorization: Bearer YOUR_TOKEN"
|
||
```
|
||
|
||
### 测试2: 点赞数字段测试
|
||
```bash
|
||
# 验证响应中包含 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: 图片上传测试
|
||
```bash
|
||
# 测试上传接口
|
||
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"
|
||
}'
|
||
```
|
||
|
||
**预期响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "ok",
|
||
"data": {
|
||
"pic_url": "https://example.com/image.jpg",
|
||
"upload_id": "upload_1736234567_abc12345"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 测试4: 取消铸造测试
|
||
```bash
|
||
# 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"
|
||
```
|
||
|
||
**预期响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "ok",
|
||
"data": {
|
||
"order_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"status": "CANCELLED"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 测试5: 取消铸造边界测试
|
||
```bash
|
||
# 测试取消已成功的订单(应该失败)
|
||
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. **状态管理**:
|
||
- 取消订单时不需要退回水晶
|
||
- 只能取消 `PENDING` 或 `PROCESSING` 状态的订单
|
||
|
||
4. **图片上传**:
|
||
- 目前只是接收URL并返回,不进行实际处理
|
||
- 后续可以扩展审核和AI生成功能
|
||
|
||
5. **点赞数**:
|
||
- 确保数据库查询包含 `like_count` 字段
|
||
- 确保所有层都正确传递和转换
|
||
|
||
---
|
||
|
||
## 📝 后续扩展(可选)
|
||
|
||
### 图片上传功能扩展
|
||
- [ ] 实际文件上传(multipart/form-data)
|
||
- [ ] 图片存储到OSS/S3
|
||
- [ ] 图片审核功能
|
||
- [ ] AI生成封面功能
|
||
|
||
### 取消铸造功能扩展
|
||
- [ ] 取消时删除关联资产(可选)
|
||
- [ ] 取消原因记录
|
||
- [ ] 取消通知功能
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**最后更新**: 2026-01-12
|
||
**维护者**: AI Assistant
|