topfans/backend/services/assetService/provider/asset_provider.go
zerosaturation e079a6c2e2 feat(asset): restore starbook home More button by Asset.LikeCount ordering
starbook 首页每个 grade/group 按 Asset.LikeCount DESC, Asset.ID ASC 取前三张,超三显示 has_more;
GetAssetsByType 不截断返回全部匹配项。包含:
- proto/asset.proto 声明 GetAssetsByType RPC + 消息,重新生成 .pb.go/.triple.go
- gateway starbook controller 改用标准 AssetService 客户端
- assetService: 三个 build*GroupForAssets 增加 previewLimit 参数
- 新增 sortAndLimitAssetItems 共享助手
- 新增 asset_service_group_test.go DB-free 单测
- 新增 TestStarbookHomePreviewAndMoreUseAssetLikeRanking 端到端回归
- frontend pages/starbook/items.vue 适配嵌套 groups/grades/items 响应,单 grade 竖向展示

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:20:16 +08:00

1023 lines
29 KiB
Go
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.

package provider
import (
"context"
"fmt"
"time"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/topfans/backend/pkg/authctx"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/asset"
pbCommon "github.com/topfans/backend/pkg/proto/common"
"github.com/topfans/backend/services/assetService/service"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// assetLikeManager 是 provider 实际调用的 AssetLikeService 方法子集。
// 抽成接口只为可测(*service.AssetLikeService 结构性满足它),
// 让身份覆盖逻辑可以用 mock 断言传入 service 的是可信身份而非 req 伪造值。
// main.go 仍传 *service.AssetLikeService无需改动。
type assetLikeManager interface {
LikeAsset(ctx context.Context, assetID, userID, starID int64) (int32, error)
UnlikeAsset(ctx context.Context, assetID, userID, starID int64) (int32, error)
CheckAssetLike(ctx context.Context, assetID, userID, starID int64) (bool, error)
GetAssetLikes(ctx context.Context, assetID int64, page, pageSize int32) ([]*models.AssetLike, int64, error)
ClearAssetLikeRecords(ctx context.Context, assetID int64) error
}
// AssetProvider 资产服务Provider实现
// 实现 Triple 协议生成的 AssetServiceHandler 接口
type AssetProvider struct {
assetService service.AssetService
mintService service.MintService
assetLikeService assetLikeManager
materialService *service.MaterialService
}
// 确保 AssetProvider 实现了 AssetServiceHandler 接口(包含 GetAssetsByType
var _ pb.AssetServiceHandler = (*AssetProvider)(nil)
// NewAssetProvider 创建资产服务Provider实例
func NewAssetProvider(assetService service.AssetService, mintService service.MintService, assetLikeService *service.AssetLikeService, materialService *service.MaterialService) *AssetProvider {
return &AssetProvider{
assetService: assetService,
mintService: mintService,
assetLikeService: assetLikeService,
materialService: materialService,
}
}
// InitMintOrder 阶段一:初始化订单(仅落库 order_id
func (p *AssetProvider) InitMintOrder(ctx context.Context, req *pb.InitMintOrderRequest) (*pb.InitMintOrderResponse, error) {
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
return &pb.InitMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
resp, err := p.mintService.InitMintOrder(req.OrderId, userID, starID)
if err != nil {
if resp == nil {
resp = &pb.InitMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
return resp, nil
}
// PreCreateMintOrder 阶段一:预创建铸造订单(生成 order_id
func (p *AssetProvider) PreCreateMintOrder(ctx context.Context, req *pb.PreCreateMintOrderRequest) (*pb.PreCreateMintOrderResponse, error) {
logger.Logger.Info("Received PreCreateMintOrder request",
zap.String("material_url", req.MaterialUrl),
)
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
return &pb.PreCreateMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
resp, err := p.mintService.PreCreateMintOrder(req, userID, starID)
if err != nil {
logger.Logger.Error("PreCreateMintOrder failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
if resp == nil {
resp = &pb.PreCreateMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Info("PreCreateMintOrder successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("order_id", resp.Order.OrderId),
)
return resp, nil
}
// CreateMintOrder 创建铸造订单
func (p *AssetProvider) CreateMintOrder(ctx context.Context, req *pb.CreateMintOrderRequest) (*pb.CreateMintOrderResponse, error) {
// 记录请求日志
logger.Logger.Info("Received CreateMintOrder request",
zap.String("name", req.Name),
)
// 从 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.CreateMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
resp, err := p.mintService.CreateMintOrder(req, userID, starID)
if err != nil {
logger.Logger.Error("CreateMintOrder failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
// 如果响应为空,构建错误响应
if resp == nil {
resp = &pb.CreateMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Info("CreateMintOrder successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("order_id", resp.Order.OrderId),
)
return resp, nil
}
// GetMyAssets 获取我的藏品列表
func (p *AssetProvider) GetMyAssets(ctx context.Context, req *pb.GetMyAssetsRequest) (*pb.GetMyAssetsResponse, error) {
// 记录请求日志
logger.Logger.Info("Received GetMyAssets request",
zap.Int32("page", req.Page),
zap.Int32("page_size", req.PageSize),
)
// 从 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.GetMyAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
resp, err := p.assetService.GetMyAssets(req, userID, starID)
if err != nil {
logger.Logger.Error("GetMyAssets failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
// 如果响应为空,构建错误响应
if resp == nil {
resp = &pb.GetMyAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Debug("GetMyAssets successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int("count", len(resp.Data.Groups)),
)
return resp, nil
}
// GetAssetsByType 按类型/分类/等级过滤查询星册藏品(starbook 分类页) — 委托给 AssetService。
//
// 身份必须来自网关经 Dubbo attachments 注入的可信 user_id/star_id;
// req.UserId/req.StarId 是调用方可控字段,一律不读,避免越权查看他人星册。
// 与 GetMyAssets 使用同一套身份提取,保证 gateway 侧调用方式一致。
func (p *AssetProvider) GetAssetsByType(ctx context.Context, req *pb.GetAssetsByTypeRequest) (*pb.GetAssetsByTypeResponse, error) {
logger.Logger.Info("Received GetAssetsByType request",
zap.String("type", req.Type),
zap.String("category", req.Category),
zap.Int32("grade", req.Grade),
)
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
logger.Logger.Error("GetAssetsByType failed to extract user info from attachments",
zap.Error(err),
)
return &pb.GetAssetsByTypeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
resp, err := p.assetService.GetAssetsByType(req, userID, starID)
if err != nil {
logger.Logger.Error("GetAssetsByType failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
if resp == nil {
resp = &pb.GetAssetsByTypeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Debug("GetAssetsByType successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int("group_count", len(resp.Data.Groups)),
)
return resp, nil
}
// GetAsset 获取资产详情
func (p *AssetProvider) GetAsset(ctx context.Context, req *pb.GetAssetRequest) (*pb.GetAssetResponse, error) {
// 记录请求日志
logger.Logger.Info("Received GetAsset request",
zap.Int64("asset_id", req.AssetId),
)
// 从 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.GetAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
resp, err := p.assetService.GetAsset(req, userID, starID)
if err != nil {
logger.Logger.Error("GetAsset failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
// 如果响应为空,构建错误响应
if resp == nil {
resp = &pb.GetAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Debug("GetAsset successful",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
)
return resp, nil
}
// GetAssetStatus 查询上链状态
func (p *AssetProvider) GetAssetStatus(ctx context.Context, req *pb.GetAssetStatusRequest) (*pb.GetAssetStatusResponse, error) {
// 记录请求日志
logger.Logger.Info("Received GetAssetStatus request",
zap.Int64("asset_id", req.AssetId),
)
// 从 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.GetAssetStatusResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
resp, err := p.assetService.GetAssetStatus(req, userID, starID)
if err != nil {
logger.Logger.Error("GetAssetStatus failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
// 如果响应为空,构建错误响应
if resp == nil {
resp = &pb.GetAssetStatusResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Debug("GetAssetStatus successful",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
)
return resp, nil
}
// GetAssetForRPC 获取资产信息内部RPC调用
func (p *AssetProvider) GetAssetForRPC(ctx context.Context, req *pb.GetAssetForRPCRequest) (*pb.GetAssetForRPCResponse, error) {
logger.Logger.Info("Received GetAssetForRPC request",
zap.Int64("asset_id", req.AssetId),
)
// 调用Service层
resp, err := p.assetService.GetAssetForRPC(req)
if err != nil {
logger.Logger.Error("GetAssetForRPC failed",
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
// 如果响应为空,构建错误响应
if resp == nil {
resp = &pb.GetAssetForRPCResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Info("GetAssetForRPC successful",
zap.Int64("asset_id", req.AssetId),
)
return resp, nil
}
// LikeAsset 点赞资产内部RPC调用
func (p *AssetProvider) LikeAsset(ctx context.Context, req *pb.LikeAssetRequest) (*pb.LikeAssetResponse, error) {
logger.Logger.Info("Received LikeAsset request",
zap.Int64("asset_id", req.AssetId),
)
// 从 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.LikeAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
likeCount, err := p.assetLikeService.LikeAsset(ctx, req.AssetId, userID, starID)
if err != nil {
logger.Logger.Error("LikeAsset failed",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
// 返回响应但不返回 error让客户端检查 Base.Code
return &pb.LikeAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}, nil
}
logger.Logger.Info("LikeAsset successful",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
zap.Int32("like_count", likeCount),
)
return &pb.LikeAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "Asset liked successfully",
Timestamp: 0,
},
LikeCount: likeCount,
}, nil
}
// UnlikeAsset 取消点赞资产内部RPC调用
func (p *AssetProvider) UnlikeAsset(ctx context.Context, req *pb.UnlikeAssetRequest) (*pb.UnlikeAssetResponse, error) {
logger.Logger.Info("Received UnlikeAsset request",
zap.Int64("asset_id", req.AssetId),
)
// 从 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.UnlikeAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
likeCount, err := p.assetLikeService.UnlikeAsset(ctx, req.AssetId, userID, starID)
if err != nil {
logger.Logger.Error("UnlikeAsset failed",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
return &pb.UnlikeAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}, err
}
logger.Logger.Info("UnlikeAsset successful",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
zap.Int32("like_count", likeCount),
)
return &pb.UnlikeAssetResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "Asset unliked successfully",
Timestamp: 0,
},
LikeCount: likeCount,
}, nil
}
// CheckAssetLike 检查是否已点赞内部RPC调用
func (p *AssetProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetLikeRequest) (*pb.CheckAssetLikeResponse, error) {
logger.Logger.Info("Received CheckAssetLike request",
zap.Int64("asset_id", req.AssetId),
)
// 身份必须来自 ctx 里的可信值auth interceptor 从 JWT 解析后灌入),
// req.UserId / req.StarId 是调用方可控字段,一律不读——否则任何人都能
// 用 (受害者 user_id, star_id) 探测“某藏品是否被 TA 点赞”,构成隐私预言机。
// 点赞是 user+star 双维度,故用 ExtractIdentity 取双正身份。
userID, starID, err := authctx.ExtractIdentity(ctx)
if err != nil {
logger.Logger.Warn("CheckAssetLike missing trusted identity", zap.Error(err))
return &pb.CheckAssetLikeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "identity required",
Timestamp: 0,
},
}, status.Error(codes.Unauthenticated, "identity required")
}
// 调用Service层
isLiked, err := p.assetLikeService.CheckAssetLike(ctx, req.AssetId, userID, starID)
if err != nil {
logger.Logger.Error("CheckAssetLike failed",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
return &pb.CheckAssetLikeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}, err
}
logger.Logger.Info("CheckAssetLike successful",
zap.Int64("user_id", userID),
zap.Int64("asset_id", req.AssetId),
zap.Bool("is_liked", isLiked),
)
return &pb.CheckAssetLikeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "Success",
Timestamp: 0,
},
IsLiked: isLiked,
}, nil
}
// GetAssetLikes 获取资产点赞列表内部RPC调用
func (p *AssetProvider) GetAssetLikes(ctx context.Context, req *pb.GetAssetLikesRequest) (*pb.GetAssetLikesResponse, error) {
logger.Logger.Info("Received GetAssetLikes request",
zap.Int64("asset_id", req.AssetId),
zap.Int32("page", req.Page),
zap.Int32("page_size", req.PageSize),
)
// 调用Service层
likes, total, err := p.assetLikeService.GetAssetLikes(ctx, req.AssetId, req.Page, req.PageSize)
if err != nil {
logger.Logger.Error("GetAssetLikes failed",
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
return &pb.GetAssetLikesResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}, err
}
// 转换为 pb.AssetLike
pbLikes := make([]*pb.AssetLike, 0, len(likes))
for _, like := range likes {
pbLikes = append(pbLikes, &pb.AssetLike{
Id: like.ID,
AssetId: like.AssetID,
UserId: like.UserID,
StarId: like.StarID,
CreatedAt: like.CreatedAt,
})
}
// 计算分页信息
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
hasMore := total > int64(req.Page*pageSize)
logger.Logger.Info("GetAssetLikes successful",
zap.Int64("asset_id", req.AssetId),
zap.Int("count", len(pbLikes)),
zap.Int64("total", total),
)
return &pb.GetAssetLikesResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "Success",
Timestamp: 0,
},
Likes: pbLikes,
Total: total,
Page: req.Page,
PageSize: pageSize,
HasMore: hasMore,
}, nil
}
// extractUserInfoFromDubboAttachments 从 Dubbo attachments 中提取用户信息
func extractUserInfoFromDubboAttachments(ctx context.Context) (int64, int64, error) {
logger.Logger.Info("Extracting user info from Dubbo attachments")
// 使用正确的 constant.AttachmentKey 获取 Dubbo attachments
if attachments := ctx.Value(constant.AttachmentKey); attachments != nil {
logger.Logger.Info("Found attachments in context",
zap.Any("attachments", attachments),
zap.String("key", fmt.Sprintf("%v", constant.AttachmentKey)),
)
if attMap, ok := attachments.(map[string]interface{}); ok {
logger.Logger.Info("Attachments is a map", zap.Any("map", attMap))
userID, starID := extractUserInfoFromMap(attMap)
if userID > 0 && starID > 0 {
logger.Logger.Info("Successfully extracted user info",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return userID, starID, nil
} else {
logger.Logger.Warn("Extracted zero user_id or star_id",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
}
} else {
logger.Logger.Warn("Attachments is not a map[string]interface{}",
zap.String("type", fmt.Sprintf("%T", attachments)),
)
}
} else {
logger.Logger.Warn("No attachments found in context",
zap.String("key", fmt.Sprintf("%v", constant.AttachmentKey)),
)
}
logger.Logger.Error("Failed to extract user info from Dubbo attachments")
return 0, 0, fmt.Errorf("user info not found in Dubbo attachments (expected user_id and star_id from gateway)")
}
// extractUserInfoFromMap 从 map 中提取 user_id 和 star_id
func extractUserInfoFromMap(attMap map[string]interface{}) (int64, int64) {
var userID, starID int64
// 提取 user_id
if v, ok := attMap["user_id"]; ok {
userID = parseIntValue(v)
}
// 提取 star_id
if v, ok := attMap["star_id"]; ok {
starID = parseIntValue(v)
}
return userID, starID
}
// parseIntValue 解析各种类型的值为 int64
func parseIntValue(v interface{}) int64 {
switch val := v.(type) {
case int64:
return val
case int:
return int64(val)
case float64:
return int64(val)
case string:
var result int64
fmt.Sscanf(val, "%d", &result)
return result
case []string:
if len(val) > 0 {
var result int64
fmt.Sscanf(val[0], "%d", &result)
return result
}
case []interface{}:
if len(val) > 0 {
return parseIntValue(val[0])
}
}
return 0
}
// ClearAssetLikeRecords 清除资产点赞记录内部RPC调用供Gallery Service在下架时调用
func (p *AssetProvider) ClearAssetLikeRecords(ctx context.Context, req *pb.ClearAssetLikeRecordsRequest) (*pb.ClearAssetLikeRecordsResponse, error) {
logger.Logger.Info("Received ClearAssetLikeRecords request",
zap.Int64("asset_id", req.AssetId),
)
if err := p.assetLikeService.ClearAssetLikeRecords(ctx, req.AssetId); err != nil {
logger.Logger.Error("ClearAssetLikeRecords failed",
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
return &pb.ClearAssetLikeRecordsResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
},
}, err
}
logger.Logger.Info("ClearAssetLikeRecords successful",
zap.Int64("asset_id", req.AssetId),
)
return &pb.ClearAssetLikeRecordsResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "点赞记录已清除",
},
}, nil
}
// GetMintOrder 查询铸造订单状态
func (p *AssetProvider) GetMintOrder(ctx context.Context, req *pb.GetMintOrderRequest) (*pb.GetMintOrderResponse, error) {
logger.Logger.Info("Received GetMintOrder 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.GetMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
resp, err := p.mintService.GetMintOrder(req.OrderId, userID, starID)
if err != nil {
logger.Logger.Error("GetMintOrder failed",
zap.String("order_id", req.OrderId),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
// 如果响应为空,构建错误响应
if resp == nil {
resp = &pb.GetMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Info("GetMintOrder successful",
zap.String("order_id", req.OrderId),
zap.Int64("user_id", userID),
zap.String("status", resp.Order.Status),
)
return resp, nil
}
// 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: uint32(codes.Unauthenticated),
Message: "未授权",
Timestamp: time.Now().UnixMilli(),
},
}, err
}
// 调用服务层取消订单
err = p.mintService.CancelMintOrder(req.OrderId, userID, starID)
// 构建响应
resp := &pb.CancelMintOrderResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "",
Timestamp: time.Now().UnixMilli(),
},
OrderId: req.OrderId,
Status: models.MintOrderStatusCancelled,
}
if err != nil {
logger.Logger.Error("CancelMintOrder failed",
zap.String("order_id", req.OrderId),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
resp.Base.Code = uint32(appErrors.ToGRPCCode(err))
resp.Base.Message = err.Error()
return resp, err
}
logger.Logger.Debug("CancelMintOrder successful",
zap.String("order_id", req.OrderId),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return resp, nil
}
// EstimateMintCost 估算铸造费用
func (p *AssetProvider) EstimateMintCost(ctx context.Context, req *pb.EstimateMintCostRequest) (*pb.EstimateMintCostResponse, error) {
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
return &pb.EstimateMintCostResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
},
}, err
}
estimate, err := p.mintService.EstimateMintCost(userID, starID)
if err != nil {
logger.Logger.Error("EstimateMintCost failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
return &pb.EstimateMintCostResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
},
}, err
}
return &pb.EstimateMintCostResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "ok",
},
CostCrystal: estimate.CostCrystal,
CurrentBalance: estimate.CurrentBalance,
BalanceAfter: estimate.AfterBalance,
MintCount: estimate.MintCount,
NextTierCost: estimate.NextTierCost,
}, nil
}
// GetAssetQrcode 生成分享二维码(spec § 3) — 委托给 AssetService
func (p *AssetProvider) GetAssetQrcode(ctx context.Context, req *pb.GetAssetQrcodeRequest) (*pb.GetAssetQrcodeResponse, error) {
// 分享归因者 = 发起请求的登录用户,必须锚定 ctx 可信 user_id。
// req.SharerUserId 是调用方可控字段,若信任它,攻击者可把分享/裂变归因
// 嫁祸或刷到任意 user_id 名下OSS key、落地页 from= 都由此拼出)。
// 分享只需 user 维度(不绑定 star故用 ExtractUserID。
uid, err := authctx.ExtractUserID(ctx)
if err != nil {
logger.Logger.Warn("GetAssetQrcode missing trusted identity", zap.Error(err))
return &pb.GetAssetQrcodeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "identity required",
Timestamp: time.Now().UnixMilli(),
},
}, status.Error(codes.Unauthenticated, "identity required")
}
req.SharerUserId = uid
logger.Logger.Info("Received GetAssetQrcode request",
zap.Int64("asset_id", req.AssetId),
zap.Int64("sharer_user_id", req.SharerUserId),
zap.String("system_type", req.SystemType),
zap.String("share_target", req.ShareTarget),
)
resp, err := p.assetService.GetAssetQrcode(ctx, req)
if err != nil {
logger.Logger.Error("GetAssetQrcode failed",
zap.Int64("asset_id", req.AssetId),
zap.Int64("sharer_user_id", req.SharerUserId),
zap.Error(err),
)
if resp == nil {
resp = &pb.GetAssetQrcodeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}
}
return resp, err
}
return resp, nil
}
// TrackShare 记录分享归因事件(spec § 3.5) — 委托给 AssetService
func (p *AssetProvider) TrackShare(ctx context.Context, req *pb.TrackShareRequest) (*pb.TrackShareResponse, error) {
// 同 GetAssetQrcode分享埋点的归因者必须锚定 ctx 可信 user_id
// 否则 share_events.sharer_user_id 可被 req 伪造,污染归因/防刷统计。
uid, err := authctx.ExtractUserID(ctx)
if err != nil {
logger.Logger.Warn("TrackShare missing trusted identity", zap.Error(err))
return &pb.TrackShareResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "identity required",
Timestamp: time.Now().UnixMilli(),
},
}, status.Error(codes.Unauthenticated, "identity required")
}
req.SharerUserId = uid
logger.Logger.Info("Received TrackShare request",
zap.Int64("asset_id", req.AssetId),
zap.Int64("sharer_user_id", req.SharerUserId),
zap.String("system_type", req.SystemType),
zap.String("share_target", req.ShareTarget),
zap.String("result", req.Result),
)
resp, err := p.assetService.TrackShare(ctx, req)
if err != nil {
logger.Logger.Error("TrackShare failed",
zap.Int64("asset_id", req.AssetId),
zap.Int64("sharer_user_id", req.SharerUserId),
zap.Error(err),
)
if resp == nil {
resp = &pb.TrackShareResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}
}
return resp, err
}
return resp, nil
}