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

827 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 数字藏品功能后续实现计划
## 📋 文档说明
本文档详细说明了数字藏品功能的后续实现计划,包括具体的实现步骤、代码修改点和测试方案。
**创建日期**: 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