topfans/backend/services/assetService/provider/asset_provider.go
zerosaturation b7022d2dc1 fix(backend): auth boundary — trusted identity from ctx (batch 2)
- 新增 pkg/authctx: 从 Dubbo attachment/gRPC metadata 提取可信 user_id/star_id,
  统一覆盖 req 同名字段, 缺身份返 Unauthenticated。
- 各 provider 接入(堵身份伪造/越权):
  * moderation SubmitReport 等 6 RPC(举报人伪造)
  * asset CheckAssetLike/GetAssetQrcode/TrackShare(点赞/分享归因伪造)
  * social CheckFriendship(修 starID=0 隐私预言机)
  * activity PurchaseItem/BatchPurchaseItem(水晶扣费伪造)等 5 RPC
  * gallery/aiChat/task/notification 迁移 authctx, 删散落 extractUserInfo*
- social 正确性: GetUserLikedAssets OR 显式分组(防御); GetRandomUsersByStar 真随机(去 rand.Seed)。
- gateway: /auth/validate 移入 AuthMiddleware 保护组(/refresh 保留,依赖注入身份)。
- 删 userService 已迁移死函数; notification 缺身份错误码统一为 Unauthenticated。
- 各 provider 单测(伪造身份被覆盖 + 缺身份拒绝)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 01:12:35 +08:00

969 lines
27 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"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
"github.com/topfans/backend/pkg/authctx"
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 接口
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
}
// 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
}