- 新增 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>
969 lines
27 KiB
Go
969 lines
27 KiB
Go
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
|
||
}
|