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 }