diff --git a/backend/gateway/controller/social_controller.go b/backend/gateway/controller/social_controller.go index 05415de..f91903a 100644 --- a/backend/gateway/controller/social_controller.go +++ b/backend/gateway/controller/social_controller.go @@ -885,6 +885,85 @@ func (ctrl *SocialController) UnlikeAsset(c *gin.Context) { // 注意:CheckAssetLike 已被删除 // 检查点赞状态请通过 GET /api/v1/assets/:asset_id 获取,响应中的 is_liked 字段表示当前用户是否已点赞 +// GetAssetLikers 获取资产点赞用户列表 +// @Summary 获取资产点赞用户列表 +// @Description 获取指定藏品的点赞用户列表(带游标分页) +// @Tags social +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param asset_id path int true "资产ID" +// @Param page_size query int false "每页数量,默认20,最大100" +// @Param cursor query int false "游标(上一页最后一条的 created_at,首次请求传0)" +// @Success 200 {object} response.Response +// @Router /api/v1/social/assets/{asset_id}/likers [get] +func (ctrl *SocialController) GetAssetLikers(c *gin.Context) { + // 1. 解析资产ID + assetIDStr := c.Param("asset_id") + assetID, err := strconv.ParseInt(assetIDStr, 10, 64) + if err != nil { + response.Error(c, http.StatusBadRequest, "参数错误: asset_id 必须为数字") + return + } + + // 2. 解析查询参数 + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + cursorStr := c.DefaultQuery("cursor", "0") + cursor, err := strconv.ParseInt(cursorStr, 10, 64) + if err != nil { + cursor = 0 + } + + // 3. 设置上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{ + "user_id": "0", // 查询点赞列表不需要 user_id + "star_id": "0", + }) + + // 4. 调用 RPC + resp, err := ctrl.socialService.GetAssetLikers(ctx, &pbSocial.GetAssetLikersRequest{ + AssetId: assetID, + PageSize: int32(pageSize), + Cursor: cursor, + }) + + if err != nil { + logger.Logger.Error("GetAssetLikers RPC failed", + zap.Int64("asset_id", assetID), + zap.Error(err), + ) + response.Error(c, http.StatusInternalServerError, "服务调用失败") + return + } + + if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { + response.ErrorWithCode(c, int(resp.Base.Code), resp.Base.Message) + return + } + + // 5. 构建响应 + users := make([]map[string]interface{}, 0, len(resp.Users)) + for _, u := range resp.Users { + users = append(users, map[string]interface{}{ + "user_id": u.UserId, + "nickname": u.Nickname, + "avatar": u.Avatar, + "fan_level": u.FanLevel, + "liked_at": u.LikedAt, + }) + } + + response.Success(c, gin.H{ + "users": users, + "total": resp.Total, + "has_more": resp.HasMore, + "next_cursor": resp.NextCursor, + }) +} + // GetMyLikedAssets 获取我点赞的作品列表 // @Summary 获取我点赞的作品列表 // @Description 获取当前用户点赞过的作品列表(只返回展出中且未过期的) diff --git a/backend/gateway/router/router.go b/backend/gateway/router/router.go index ba78a78..48a4bf1 100644 --- a/backend/gateway/router/router.go +++ b/backend/gateway/router/router.go @@ -174,6 +174,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl // 资产点赞操作(保留操作接口,查询状态已集成到资产详情) social.POST("/assets/:asset_id/like", socialCtrl.LikeAsset) // 点赞资产 social.DELETE("/assets/:asset_id/like", socialCtrl.UnlikeAsset) // 取消点赞 + social.GET("/assets/:asset_id/likers", socialCtrl.GetAssetLikers) // 获取点赞用户列表 // 注意:GET /api/v1/assets/:asset_id 会返回 is_liked 字段,无需单独查询 } diff --git a/backend/pkg/database/redis.go b/backend/pkg/database/redis.go index 11b8a2c..52a0ab4 100644 --- a/backend/pkg/database/redis.go +++ b/backend/pkg/database/redis.go @@ -13,8 +13,26 @@ import ( const ( BlacklistKeyPrefix = "blacklist:token:" InspirationFlowKeyPrefix = "inspiration_flow:" + AssetLikersKeyPrefix = "asset_likers:" ) +// AssetLikersCache 缓存数据结构 +type AssetLikersCache struct { + Users []AssetLikerWithTotal `json:"users"` // 完整用户列表(按 liked_at DESC) + Total int64 `json:"total"` // 总数 + UpdatedAt int64 `json:"updated_at"` // 缓存更新时间 +} + +// AssetLikerWithTotal 用户+点赞时间 +type AssetLikerWithTotal struct { + UserID int64 `json:"user_id"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` + FanLevel int32 `json:"fan_level"` + LikedAt int64 `json:"liked_at"` + StarID int64 `json:"star_id"` // 用于 JOIN fan_profiles,缓存时保留 +} + // RedisClient Redis 客户端单例 var RedisClient *redis.Client @@ -249,3 +267,55 @@ func GetHistoryPage(cache *InspirationFlowCache, offset, limit int) []Inspiratio } return items[start:end] } + +// AssetLikersKey 生成藏品点赞用户列表缓存 Key +func AssetLikersKey(assetID int64) string { + return fmt.Sprintf("%s%d", AssetLikersKeyPrefix, assetID) +} + +// GetAssetLikersCache 获取藏品点赞用户列表缓存 +func GetAssetLikersCache(ctx context.Context, assetID int64) (*AssetLikersCache, error) { + if RedisClient == nil { + return nil, fmt.Errorf("redis client is not initialized") + } + + key := AssetLikersKey(assetID) + data, err := RedisClient.Get(ctx, key).Result() + if err == redis.Nil { + return nil, nil // 缓存不存在 + } + if err != nil { + return nil, err + } + + var cache AssetLikersCache + if err := json.Unmarshal([]byte(data), &cache); err != nil { + return nil, err + } + return &cache, nil +} + +// SetAssetLikersCache 设置藏品点赞用户列表缓存 +func SetAssetLikersCache(ctx context.Context, assetID int64, cache *AssetLikersCache, ttl time.Duration) error { + if RedisClient == nil { + return fmt.Errorf("redis client is not initialized") + } + + key := AssetLikersKey(assetID) + data, err := json.Marshal(cache) + if err != nil { + return err + } + + return RedisClient.Set(ctx, key, data, ttl).Err() +} + +// InvalidateAssetLikersCache 删除藏品点赞用户列表缓存 +func InvalidateAssetLikersCache(ctx context.Context, assetID int64) error { + if RedisClient == nil { + return nil // Redis 未初始化时跳过 + } + + key := AssetLikersKey(assetID) + return RedisClient.Del(ctx, key).Err() +} diff --git a/backend/pkg/proto/social/social.pb.go b/backend/pkg/proto/social/social.pb.go index 6010ef3..a6daca9 100644 --- a/backend/pkg/proto/social/social.pb.go +++ b/backend/pkg/proto/social/social.pb.go @@ -2148,6 +2148,221 @@ func (x *CheckAssetLikeResponse) GetIsLiked() bool { return false } +// 获取资产点赞用户列表请求 +type GetAssetLikersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` // 每页数量(默认20,最大100) + Cursor int64 `protobuf:"varint,3,opt,name=cursor,proto3" json:"cursor,omitempty"` // 游标(上一页最后一条的 created_at,首次请求传0) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAssetLikersRequest) Reset() { + *x = GetAssetLikersRequest{} + mi := &file_social_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAssetLikersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAssetLikersRequest) ProtoMessage() {} + +func (x *GetAssetLikersRequest) ProtoReflect() protoreflect.Message { + mi := &file_social_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAssetLikersRequest.ProtoReflect.Descriptor instead. +func (*GetAssetLikersRequest) Descriptor() ([]byte, []int) { + return file_social_proto_rawDescGZIP(), []int{33} +} + +func (x *GetAssetLikersRequest) GetAssetId() int64 { + if x != nil { + return x.AssetId + } + return 0 +} + +func (x *GetAssetLikersRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *GetAssetLikersRequest) GetCursor() int64 { + if x != nil { + return x.Cursor + } + return 0 +} + +// 获取资产点赞用户列表响应 +type GetAssetLikersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Users []*AssetLiker `protobuf:"bytes,2,rep,name=users,proto3" json:"users,omitempty"` // 点赞用户列表 + Total int64 `protobuf:"varint,3,opt,name=total,proto3" json:"total,omitempty"` // 总数 + HasMore bool `protobuf:"varint,4,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` // 是否有更多 + NextCursor int64 `protobuf:"varint,5,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"` // 下一页游标 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAssetLikersResponse) Reset() { + *x = GetAssetLikersResponse{} + mi := &file_social_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAssetLikersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAssetLikersResponse) ProtoMessage() {} + +func (x *GetAssetLikersResponse) ProtoReflect() protoreflect.Message { + mi := &file_social_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAssetLikersResponse.ProtoReflect.Descriptor instead. +func (*GetAssetLikersResponse) Descriptor() ([]byte, []int) { + return file_social_proto_rawDescGZIP(), []int{34} +} + +func (x *GetAssetLikersResponse) GetBase() *common.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *GetAssetLikersResponse) GetUsers() []*AssetLiker { + if x != nil { + return x.Users + } + return nil +} + +func (x *GetAssetLikersResponse) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *GetAssetLikersResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +func (x *GetAssetLikersResponse) GetNextCursor() int64 { + if x != nil { + return x.NextCursor + } + return 0 +} + +// 点赞用户信息 +type AssetLiker struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID + Nickname string `protobuf:"bytes,2,opt,name=nickname,proto3" json:"nickname,omitempty"` // 昵称 + Avatar string `protobuf:"bytes,3,opt,name=avatar,proto3" json:"avatar,omitempty"` // 头像URL + FanLevel int32 `protobuf:"varint,4,opt,name=fan_level,json=fanLevel,proto3" json:"fan_level,omitempty"` // 粉丝等级 + LikedAt int64 `protobuf:"varint,5,opt,name=liked_at,json=likedAt,proto3" json:"liked_at,omitempty"` // 点赞时间(毫秒时间戳) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssetLiker) Reset() { + *x = AssetLiker{} + mi := &file_social_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssetLiker) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetLiker) ProtoMessage() {} + +func (x *AssetLiker) ProtoReflect() protoreflect.Message { + mi := &file_social_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetLiker.ProtoReflect.Descriptor instead. +func (*AssetLiker) Descriptor() ([]byte, []int) { + return file_social_proto_rawDescGZIP(), []int{35} +} + +func (x *AssetLiker) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *AssetLiker) GetNickname() string { + if x != nil { + return x.Nickname + } + return "" +} + +func (x *AssetLiker) GetAvatar() string { + if x != nil { + return x.Avatar + } + return "" +} + +func (x *AssetLiker) GetFanLevel() int32 { + if x != nil { + return x.FanLevel + } + return 0 +} + +func (x *AssetLiker) GetLikedAt() int64 { + if x != nil { + return x.LikedAt + } + return 0 +} + // 获取我点赞的作品列表请求 type GetMyLikedAssetsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2160,7 +2375,7 @@ type GetMyLikedAssetsRequest struct { func (x *GetMyLikedAssetsRequest) Reset() { *x = GetMyLikedAssetsRequest{} - mi := &file_social_proto_msgTypes[33] + mi := &file_social_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2172,7 +2387,7 @@ func (x *GetMyLikedAssetsRequest) String() string { func (*GetMyLikedAssetsRequest) ProtoMessage() {} func (x *GetMyLikedAssetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[33] + mi := &file_social_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2185,7 +2400,7 @@ func (x *GetMyLikedAssetsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyLikedAssetsRequest.ProtoReflect.Descriptor instead. func (*GetMyLikedAssetsRequest) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{33} + return file_social_proto_rawDescGZIP(), []int{36} } func (x *GetMyLikedAssetsRequest) GetPage() int32 { @@ -2220,7 +2435,7 @@ type GetMyLikedAssetsResponse struct { func (x *GetMyLikedAssetsResponse) Reset() { *x = GetMyLikedAssetsResponse{} - mi := &file_social_proto_msgTypes[34] + mi := &file_social_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2232,7 +2447,7 @@ func (x *GetMyLikedAssetsResponse) String() string { func (*GetMyLikedAssetsResponse) ProtoMessage() {} func (x *GetMyLikedAssetsResponse) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[34] + mi := &file_social_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2245,7 +2460,7 @@ func (x *GetMyLikedAssetsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyLikedAssetsResponse.ProtoReflect.Descriptor instead. func (*GetMyLikedAssetsResponse) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{34} + return file_social_proto_rawDescGZIP(), []int{37} } func (x *GetMyLikedAssetsResponse) GetBase() *common.BaseResponse { @@ -2276,7 +2491,7 @@ type LikedAssetsData struct { func (x *LikedAssetsData) Reset() { *x = LikedAssetsData{} - mi := &file_social_proto_msgTypes[35] + mi := &file_social_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2288,7 +2503,7 @@ func (x *LikedAssetsData) String() string { func (*LikedAssetsData) ProtoMessage() {} func (x *LikedAssetsData) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[35] + mi := &file_social_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2301,7 +2516,7 @@ func (x *LikedAssetsData) ProtoReflect() protoreflect.Message { // Deprecated: Use LikedAssetsData.ProtoReflect.Descriptor instead. func (*LikedAssetsData) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{35} + return file_social_proto_rawDescGZIP(), []int{38} } func (x *LikedAssetsData) GetItems() []*LikedAssetItem { @@ -2357,7 +2572,7 @@ type LikedAssetItem struct { func (x *LikedAssetItem) Reset() { *x = LikedAssetItem{} - mi := &file_social_proto_msgTypes[36] + mi := &file_social_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2369,7 +2584,7 @@ func (x *LikedAssetItem) String() string { func (*LikedAssetItem) ProtoMessage() {} func (x *LikedAssetItem) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[36] + mi := &file_social_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2382,7 +2597,7 @@ func (x *LikedAssetItem) ProtoReflect() protoreflect.Message { // Deprecated: Use LikedAssetItem.ProtoReflect.Descriptor instead. func (*LikedAssetItem) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{36} + return file_social_proto_rawDescGZIP(), []int{39} } func (x *LikedAssetItem) GetAssetId() int64 { @@ -2459,7 +2674,7 @@ type GetMyTodayLikedAssetsRequest struct { func (x *GetMyTodayLikedAssetsRequest) Reset() { *x = GetMyTodayLikedAssetsRequest{} - mi := &file_social_proto_msgTypes[37] + mi := &file_social_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2471,7 +2686,7 @@ func (x *GetMyTodayLikedAssetsRequest) String() string { func (*GetMyTodayLikedAssetsRequest) ProtoMessage() {} func (x *GetMyTodayLikedAssetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[37] + mi := &file_social_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2484,7 +2699,7 @@ func (x *GetMyTodayLikedAssetsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyTodayLikedAssetsRequest.ProtoReflect.Descriptor instead. func (*GetMyTodayLikedAssetsRequest) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{37} + return file_social_proto_rawDescGZIP(), []int{40} } func (x *GetMyTodayLikedAssetsRequest) GetPage() int32 { @@ -2512,7 +2727,7 @@ type GetMyTodayLikedAssetsResponse struct { func (x *GetMyTodayLikedAssetsResponse) Reset() { *x = GetMyTodayLikedAssetsResponse{} - mi := &file_social_proto_msgTypes[38] + mi := &file_social_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2524,7 +2739,7 @@ func (x *GetMyTodayLikedAssetsResponse) String() string { func (*GetMyTodayLikedAssetsResponse) ProtoMessage() {} func (x *GetMyTodayLikedAssetsResponse) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[38] + mi := &file_social_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2537,7 +2752,7 @@ func (x *GetMyTodayLikedAssetsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyTodayLikedAssetsResponse.ProtoReflect.Descriptor instead. func (*GetMyTodayLikedAssetsResponse) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{38} + return file_social_proto_rawDescGZIP(), []int{41} } func (x *GetMyTodayLikedAssetsResponse) GetBase() *common.BaseResponse { @@ -2565,7 +2780,7 @@ type GetMyWeekLikedAssetsRequest struct { func (x *GetMyWeekLikedAssetsRequest) Reset() { *x = GetMyWeekLikedAssetsRequest{} - mi := &file_social_proto_msgTypes[39] + mi := &file_social_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2577,7 +2792,7 @@ func (x *GetMyWeekLikedAssetsRequest) String() string { func (*GetMyWeekLikedAssetsRequest) ProtoMessage() {} func (x *GetMyWeekLikedAssetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[39] + mi := &file_social_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2590,7 +2805,7 @@ func (x *GetMyWeekLikedAssetsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyWeekLikedAssetsRequest.ProtoReflect.Descriptor instead. func (*GetMyWeekLikedAssetsRequest) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{39} + return file_social_proto_rawDescGZIP(), []int{42} } func (x *GetMyWeekLikedAssetsRequest) GetPage() int32 { @@ -2618,7 +2833,7 @@ type GetMyWeekLikedAssetsResponse struct { func (x *GetMyWeekLikedAssetsResponse) Reset() { *x = GetMyWeekLikedAssetsResponse{} - mi := &file_social_proto_msgTypes[40] + mi := &file_social_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2630,7 +2845,7 @@ func (x *GetMyWeekLikedAssetsResponse) String() string { func (*GetMyWeekLikedAssetsResponse) ProtoMessage() {} func (x *GetMyWeekLikedAssetsResponse) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[40] + mi := &file_social_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2643,7 +2858,7 @@ func (x *GetMyWeekLikedAssetsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyWeekLikedAssetsResponse.ProtoReflect.Descriptor instead. func (*GetMyWeekLikedAssetsResponse) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{40} + return file_social_proto_rawDescGZIP(), []int{43} } func (x *GetMyWeekLikedAssetsResponse) GetBase() *common.BaseResponse { @@ -2672,7 +2887,7 @@ type GetUserLikedAssetsRequest struct { func (x *GetUserLikedAssetsRequest) Reset() { *x = GetUserLikedAssetsRequest{} - mi := &file_social_proto_msgTypes[41] + mi := &file_social_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2684,7 +2899,7 @@ func (x *GetUserLikedAssetsRequest) String() string { func (*GetUserLikedAssetsRequest) ProtoMessage() {} func (x *GetUserLikedAssetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[41] + mi := &file_social_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2697,7 +2912,7 @@ func (x *GetUserLikedAssetsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserLikedAssetsRequest.ProtoReflect.Descriptor instead. func (*GetUserLikedAssetsRequest) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{41} + return file_social_proto_rawDescGZIP(), []int{44} } func (x *GetUserLikedAssetsRequest) GetUserId() int64 { @@ -2732,7 +2947,7 @@ type GetUserLikedAssetsResponse struct { func (x *GetUserLikedAssetsResponse) Reset() { *x = GetUserLikedAssetsResponse{} - mi := &file_social_proto_msgTypes[42] + mi := &file_social_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2744,7 +2959,7 @@ func (x *GetUserLikedAssetsResponse) String() string { func (*GetUserLikedAssetsResponse) ProtoMessage() {} func (x *GetUserLikedAssetsResponse) ProtoReflect() protoreflect.Message { - mi := &file_social_proto_msgTypes[42] + mi := &file_social_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2757,7 +2972,7 @@ func (x *GetUserLikedAssetsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserLikedAssetsResponse.ProtoReflect.Descriptor instead. func (*GetUserLikedAssetsResponse) Descriptor() ([]byte, []int) { - return file_social_proto_rawDescGZIP(), []int{42} + return file_social_proto_rawDescGZIP(), []int{45} } func (x *GetUserLikedAssetsResponse) GetBase() *common.BaseResponse { @@ -2939,7 +3154,25 @@ const file_social_proto_rawDesc = "" + "\basset_id\x18\x01 \x01(\x03R\aassetId\"e\n" + "\x16CheckAssetLikeResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x19\n" + - "\bis_liked\x18\x02 \x01(\bR\aisLiked\"e\n" + + "\bis_liked\x18\x02 \x01(\bR\aisLiked\"g\n" + + "\x15GetAssetLikersRequest\x12\x19\n" + + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x16\n" + + "\x06cursor\x18\x03 \x01(\x03R\x06cursor\"\xce\x01\n" + + "\x16GetAssetLikersResponse\x120\n" + + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x120\n" + + "\x05users\x18\x02 \x03(\v2\x1a.topfans.social.AssetLikerR\x05users\x12\x14\n" + + "\x05total\x18\x03 \x01(\x03R\x05total\x12\x19\n" + + "\bhas_more\x18\x04 \x01(\bR\ahasMore\x12\x1f\n" + + "\vnext_cursor\x18\x05 \x01(\x03R\n" + + "nextCursor\"\x91\x01\n" + + "\n" + + "AssetLiker\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" + + "\bnickname\x18\x02 \x01(\tR\bnickname\x12\x16\n" + + "\x06avatar\x18\x03 \x01(\tR\x06avatar\x12\x1b\n" + + "\tfan_level\x18\x04 \x01(\x05R\bfanLevel\x12\x19\n" + + "\bliked_at\x18\x05 \x01(\x03R\alikedAt\"e\n" + "\x17GetMyLikedAssetsRequest\x12\x12\n" + "\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x19\n" + @@ -2982,7 +3215,7 @@ const file_social_proto_rawDesc = "" + "\tpage_size\x18\x03 \x01(\x05R\bpageSize\"\x83\x01\n" + "\x1aGetUserLikedAssetsResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x123\n" + - "\x04data\x18\x02 \x01(\v2\x1f.topfans.social.LikedAssetsDataR\x04data2\x94\x14\n" + + "\x04data\x18\x02 \x01(\v2\x1f.topfans.social.LikedAssetsDataR\x04data2\xa7\x15\n" + "\rSocialService\x12\x8d\x01\n" + "\x11SendFriendRequest\x12(.topfans.social.SendFriendRequestRequest\x1a).topfans.social.SendFriendRequestResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/api/v1/friends/requests\x12\x8a\x01\n" + "\x11GetFriendRequests\x12(.topfans.social.GetFriendRequestsRequest\x1a).topfans.social.GetFriendRequestsResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1/friends/requests\x12\xa7\x01\n" + @@ -2997,7 +3230,8 @@ const file_social_proto_rawDesc = "" + "\rGetUsersPaged\x12$.topfans.social.GetUsersPagedRequest\x1a%.topfans.social.GetUsersPagedResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/api/v1/social/users\x12\x7f\n" + "\tLikeAsset\x12 .topfans.social.LikeAssetRequest\x1a!.topfans.social.LikeAssetResponse\"-\x82\xd3\xe4\x93\x02'\"%/api/v1/social/assets/{asset_id}/like\x12\x85\x01\n" + "\vUnlikeAsset\x12\".topfans.social.UnlikeAssetRequest\x1a#.topfans.social.UnlikeAssetResponse\"-\x82\xd3\xe4\x93\x02'*%/api/v1/social/assets/{asset_id}/like\x12\x8e\x01\n" + - "\x0eCheckAssetLike\x12%.topfans.social.CheckAssetLikeRequest\x1a&.topfans.social.CheckAssetLikeResponse\"-\x82\xd3\xe4\x93\x02'\x12%/api/v1/social/assets/{asset_id}/like\x12\x86\x01\n" + + "\x0eCheckAssetLike\x12%.topfans.social.CheckAssetLikeRequest\x1a&.topfans.social.CheckAssetLikeResponse\"-\x82\xd3\xe4\x93\x02'\x12%/api/v1/social/assets/{asset_id}/like\x12\x90\x01\n" + + "\x0eGetAssetLikers\x12%.topfans.social.GetAssetLikersRequest\x1a&.topfans.social.GetAssetLikersResponse\"/\x82\xd3\xe4\x93\x02)\x12'/api/v1/social/assets/{asset_id}/likers\x12\x86\x01\n" + "\x10GetMyLikedAssets\x12'.topfans.social.GetMyLikedAssetsRequest\x1a(.topfans.social.GetMyLikedAssetsResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\x12\x17/api/v1/me/liked-assets\x12\x9b\x01\n" + "\x15GetMyTodayLikedAssets\x12,.topfans.social.GetMyTodayLikedAssetsRequest\x1a-.topfans.social.GetMyTodayLikedAssetsResponse\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/api/v1/me/today-liked-assets\x12\x97\x01\n" + "\x14GetMyWeekLikedAssets\x12+.topfans.social.GetMyWeekLikedAssetsRequest\x1a,.topfans.social.GetMyWeekLikedAssetsResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/api/v1/me/week-liked-assets\x12\x99\x01\n" + @@ -3015,7 +3249,7 @@ func file_social_proto_rawDescGZIP() []byte { return file_social_proto_rawDescData } -var file_social_proto_msgTypes = make([]protoimpl.MessageInfo, 43) +var file_social_proto_msgTypes = make([]protoimpl.MessageInfo, 46) var file_social_proto_goTypes = []any{ (*FriendRequest)(nil), // 0: topfans.social.FriendRequest (*Friendship)(nil), // 1: topfans.social.Friendship @@ -3050,89 +3284,96 @@ var file_social_proto_goTypes = []any{ (*UnlikeAssetResponse)(nil), // 30: topfans.social.UnlikeAssetResponse (*CheckAssetLikeRequest)(nil), // 31: topfans.social.CheckAssetLikeRequest (*CheckAssetLikeResponse)(nil), // 32: topfans.social.CheckAssetLikeResponse - (*GetMyLikedAssetsRequest)(nil), // 33: topfans.social.GetMyLikedAssetsRequest - (*GetMyLikedAssetsResponse)(nil), // 34: topfans.social.GetMyLikedAssetsResponse - (*LikedAssetsData)(nil), // 35: topfans.social.LikedAssetsData - (*LikedAssetItem)(nil), // 36: topfans.social.LikedAssetItem - (*GetMyTodayLikedAssetsRequest)(nil), // 37: topfans.social.GetMyTodayLikedAssetsRequest - (*GetMyTodayLikedAssetsResponse)(nil), // 38: topfans.social.GetMyTodayLikedAssetsResponse - (*GetMyWeekLikedAssetsRequest)(nil), // 39: topfans.social.GetMyWeekLikedAssetsRequest - (*GetMyWeekLikedAssetsResponse)(nil), // 40: topfans.social.GetMyWeekLikedAssetsResponse - (*GetUserLikedAssetsRequest)(nil), // 41: topfans.social.GetUserLikedAssetsRequest - (*GetUserLikedAssetsResponse)(nil), // 42: topfans.social.GetUserLikedAssetsResponse - (*common.BaseResponse)(nil), // 43: topfans.common.BaseResponse + (*GetAssetLikersRequest)(nil), // 33: topfans.social.GetAssetLikersRequest + (*GetAssetLikersResponse)(nil), // 34: topfans.social.GetAssetLikersResponse + (*AssetLiker)(nil), // 35: topfans.social.AssetLiker + (*GetMyLikedAssetsRequest)(nil), // 36: topfans.social.GetMyLikedAssetsRequest + (*GetMyLikedAssetsResponse)(nil), // 37: topfans.social.GetMyLikedAssetsResponse + (*LikedAssetsData)(nil), // 38: topfans.social.LikedAssetsData + (*LikedAssetItem)(nil), // 39: topfans.social.LikedAssetItem + (*GetMyTodayLikedAssetsRequest)(nil), // 40: topfans.social.GetMyTodayLikedAssetsRequest + (*GetMyTodayLikedAssetsResponse)(nil), // 41: topfans.social.GetMyTodayLikedAssetsResponse + (*GetMyWeekLikedAssetsRequest)(nil), // 42: topfans.social.GetMyWeekLikedAssetsRequest + (*GetMyWeekLikedAssetsResponse)(nil), // 43: topfans.social.GetMyWeekLikedAssetsResponse + (*GetUserLikedAssetsRequest)(nil), // 44: topfans.social.GetUserLikedAssetsRequest + (*GetUserLikedAssetsResponse)(nil), // 45: topfans.social.GetUserLikedAssetsResponse + (*common.BaseResponse)(nil), // 46: topfans.common.BaseResponse } var file_social_proto_depIdxs = []int32{ - 43, // 0: topfans.social.SendFriendRequestResponse.base:type_name -> topfans.common.BaseResponse + 46, // 0: topfans.social.SendFriendRequestResponse.base:type_name -> topfans.common.BaseResponse 20, // 1: topfans.social.SendFriendRequestResponse.matched_users:type_name -> topfans.social.FanProfileSearchResult - 43, // 2: topfans.social.GetFriendRequestsResponse.base:type_name -> topfans.common.BaseResponse + 46, // 2: topfans.social.GetFriendRequestsResponse.base:type_name -> topfans.common.BaseResponse 0, // 3: topfans.social.GetFriendRequestsResponse.items:type_name -> topfans.social.FriendRequest - 43, // 4: topfans.social.HandleFriendRequestResponse.base:type_name -> topfans.common.BaseResponse - 43, // 5: topfans.social.GetFriendListResponse.base:type_name -> topfans.common.BaseResponse + 46, // 4: topfans.social.HandleFriendRequestResponse.base:type_name -> topfans.common.BaseResponse + 46, // 5: topfans.social.GetFriendListResponse.base:type_name -> topfans.common.BaseResponse 1, // 6: topfans.social.GetFriendListResponse.items:type_name -> topfans.social.Friendship - 43, // 7: topfans.social.DeleteFriendResponse.base:type_name -> topfans.common.BaseResponse - 43, // 8: topfans.social.SetFriendRemarkResponse.base:type_name -> topfans.common.BaseResponse - 43, // 9: topfans.social.CheckFriendshipResponse.base:type_name -> topfans.common.BaseResponse - 43, // 10: topfans.social.GetFriendCountResponse.base:type_name -> topfans.common.BaseResponse - 43, // 11: topfans.social.SearchUserForFriendResponse.base:type_name -> topfans.common.BaseResponse + 46, // 7: topfans.social.DeleteFriendResponse.base:type_name -> topfans.common.BaseResponse + 46, // 8: topfans.social.SetFriendRemarkResponse.base:type_name -> topfans.common.BaseResponse + 46, // 9: topfans.social.CheckFriendshipResponse.base:type_name -> topfans.common.BaseResponse + 46, // 10: topfans.social.GetFriendCountResponse.base:type_name -> topfans.common.BaseResponse + 46, // 11: topfans.social.SearchUserForFriendResponse.base:type_name -> topfans.common.BaseResponse 20, // 12: topfans.social.SearchUserForFriendResponse.user:type_name -> topfans.social.FanProfileSearchResult - 43, // 13: topfans.social.GetRandomUsersResponse.base:type_name -> topfans.common.BaseResponse + 46, // 13: topfans.social.GetRandomUsersResponse.base:type_name -> topfans.common.BaseResponse 21, // 14: topfans.social.GetRandomUsersResponse.users:type_name -> topfans.social.RandomUser - 43, // 15: topfans.social.GetUsersPagedResponse.base:type_name -> topfans.common.BaseResponse + 46, // 15: topfans.social.GetUsersPagedResponse.base:type_name -> topfans.common.BaseResponse 24, // 16: topfans.social.GetUsersPagedResponse.users:type_name -> topfans.social.PagedUser - 43, // 17: topfans.social.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse - 43, // 18: topfans.social.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse - 43, // 19: topfans.social.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse - 43, // 20: topfans.social.GetMyLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse - 35, // 21: topfans.social.GetMyLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData - 36, // 22: topfans.social.LikedAssetsData.items:type_name -> topfans.social.LikedAssetItem - 43, // 23: topfans.social.GetMyTodayLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse - 35, // 24: topfans.social.GetMyTodayLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData - 43, // 25: topfans.social.GetMyWeekLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse - 35, // 26: topfans.social.GetMyWeekLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData - 43, // 27: topfans.social.GetUserLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse - 35, // 28: topfans.social.GetUserLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData - 2, // 29: topfans.social.SocialService.SendFriendRequest:input_type -> topfans.social.SendFriendRequestRequest - 4, // 30: topfans.social.SocialService.GetFriendRequests:input_type -> topfans.social.GetFriendRequestsRequest - 6, // 31: topfans.social.SocialService.HandleFriendRequest:input_type -> topfans.social.HandleFriendRequestRequest - 8, // 32: topfans.social.SocialService.GetFriendList:input_type -> topfans.social.GetFriendListRequest - 10, // 33: topfans.social.SocialService.DeleteFriend:input_type -> topfans.social.DeleteFriendRequest - 12, // 34: topfans.social.SocialService.SetFriendRemark:input_type -> topfans.social.SetFriendRemarkRequest - 14, // 35: topfans.social.SocialService.CheckFriendship:input_type -> topfans.social.CheckFriendshipRequest - 16, // 36: topfans.social.SocialService.GetFriendCount:input_type -> topfans.social.GetFriendCountRequest - 18, // 37: topfans.social.SocialService.SearchUserForFriend:input_type -> topfans.social.SearchUserForFriendRequest - 22, // 38: topfans.social.SocialService.GetRandomUsers:input_type -> topfans.social.GetRandomUsersRequest - 25, // 39: topfans.social.SocialService.GetUsersPaged:input_type -> topfans.social.GetUsersPagedRequest - 27, // 40: topfans.social.SocialService.LikeAsset:input_type -> topfans.social.LikeAssetRequest - 29, // 41: topfans.social.SocialService.UnlikeAsset:input_type -> topfans.social.UnlikeAssetRequest - 31, // 42: topfans.social.SocialService.CheckAssetLike:input_type -> topfans.social.CheckAssetLikeRequest - 33, // 43: topfans.social.SocialService.GetMyLikedAssets:input_type -> topfans.social.GetMyLikedAssetsRequest - 37, // 44: topfans.social.SocialService.GetMyTodayLikedAssets:input_type -> topfans.social.GetMyTodayLikedAssetsRequest - 39, // 45: topfans.social.SocialService.GetMyWeekLikedAssets:input_type -> topfans.social.GetMyWeekLikedAssetsRequest - 41, // 46: topfans.social.SocialService.GetUserLikedAssets:input_type -> topfans.social.GetUserLikedAssetsRequest - 3, // 47: topfans.social.SocialService.SendFriendRequest:output_type -> topfans.social.SendFriendRequestResponse - 5, // 48: topfans.social.SocialService.GetFriendRequests:output_type -> topfans.social.GetFriendRequestsResponse - 7, // 49: topfans.social.SocialService.HandleFriendRequest:output_type -> topfans.social.HandleFriendRequestResponse - 9, // 50: topfans.social.SocialService.GetFriendList:output_type -> topfans.social.GetFriendListResponse - 11, // 51: topfans.social.SocialService.DeleteFriend:output_type -> topfans.social.DeleteFriendResponse - 13, // 52: topfans.social.SocialService.SetFriendRemark:output_type -> topfans.social.SetFriendRemarkResponse - 15, // 53: topfans.social.SocialService.CheckFriendship:output_type -> topfans.social.CheckFriendshipResponse - 17, // 54: topfans.social.SocialService.GetFriendCount:output_type -> topfans.social.GetFriendCountResponse - 19, // 55: topfans.social.SocialService.SearchUserForFriend:output_type -> topfans.social.SearchUserForFriendResponse - 23, // 56: topfans.social.SocialService.GetRandomUsers:output_type -> topfans.social.GetRandomUsersResponse - 26, // 57: topfans.social.SocialService.GetUsersPaged:output_type -> topfans.social.GetUsersPagedResponse - 28, // 58: topfans.social.SocialService.LikeAsset:output_type -> topfans.social.LikeAssetResponse - 30, // 59: topfans.social.SocialService.UnlikeAsset:output_type -> topfans.social.UnlikeAssetResponse - 32, // 60: topfans.social.SocialService.CheckAssetLike:output_type -> topfans.social.CheckAssetLikeResponse - 34, // 61: topfans.social.SocialService.GetMyLikedAssets:output_type -> topfans.social.GetMyLikedAssetsResponse - 38, // 62: topfans.social.SocialService.GetMyTodayLikedAssets:output_type -> topfans.social.GetMyTodayLikedAssetsResponse - 40, // 63: topfans.social.SocialService.GetMyWeekLikedAssets:output_type -> topfans.social.GetMyWeekLikedAssetsResponse - 42, // 64: topfans.social.SocialService.GetUserLikedAssets:output_type -> topfans.social.GetUserLikedAssetsResponse - 47, // [47:65] is the sub-list for method output_type - 29, // [29:47] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 46, // 17: topfans.social.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse + 46, // 18: topfans.social.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse + 46, // 19: topfans.social.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse + 46, // 20: topfans.social.GetAssetLikersResponse.base:type_name -> topfans.common.BaseResponse + 35, // 21: topfans.social.GetAssetLikersResponse.users:type_name -> topfans.social.AssetLiker + 46, // 22: topfans.social.GetMyLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse + 38, // 23: topfans.social.GetMyLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData + 39, // 24: topfans.social.LikedAssetsData.items:type_name -> topfans.social.LikedAssetItem + 46, // 25: topfans.social.GetMyTodayLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse + 38, // 26: topfans.social.GetMyTodayLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData + 46, // 27: topfans.social.GetMyWeekLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse + 38, // 28: topfans.social.GetMyWeekLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData + 46, // 29: topfans.social.GetUserLikedAssetsResponse.base:type_name -> topfans.common.BaseResponse + 38, // 30: topfans.social.GetUserLikedAssetsResponse.data:type_name -> topfans.social.LikedAssetsData + 2, // 31: topfans.social.SocialService.SendFriendRequest:input_type -> topfans.social.SendFriendRequestRequest + 4, // 32: topfans.social.SocialService.GetFriendRequests:input_type -> topfans.social.GetFriendRequestsRequest + 6, // 33: topfans.social.SocialService.HandleFriendRequest:input_type -> topfans.social.HandleFriendRequestRequest + 8, // 34: topfans.social.SocialService.GetFriendList:input_type -> topfans.social.GetFriendListRequest + 10, // 35: topfans.social.SocialService.DeleteFriend:input_type -> topfans.social.DeleteFriendRequest + 12, // 36: topfans.social.SocialService.SetFriendRemark:input_type -> topfans.social.SetFriendRemarkRequest + 14, // 37: topfans.social.SocialService.CheckFriendship:input_type -> topfans.social.CheckFriendshipRequest + 16, // 38: topfans.social.SocialService.GetFriendCount:input_type -> topfans.social.GetFriendCountRequest + 18, // 39: topfans.social.SocialService.SearchUserForFriend:input_type -> topfans.social.SearchUserForFriendRequest + 22, // 40: topfans.social.SocialService.GetRandomUsers:input_type -> topfans.social.GetRandomUsersRequest + 25, // 41: topfans.social.SocialService.GetUsersPaged:input_type -> topfans.social.GetUsersPagedRequest + 27, // 42: topfans.social.SocialService.LikeAsset:input_type -> topfans.social.LikeAssetRequest + 29, // 43: topfans.social.SocialService.UnlikeAsset:input_type -> topfans.social.UnlikeAssetRequest + 31, // 44: topfans.social.SocialService.CheckAssetLike:input_type -> topfans.social.CheckAssetLikeRequest + 33, // 45: topfans.social.SocialService.GetAssetLikers:input_type -> topfans.social.GetAssetLikersRequest + 36, // 46: topfans.social.SocialService.GetMyLikedAssets:input_type -> topfans.social.GetMyLikedAssetsRequest + 40, // 47: topfans.social.SocialService.GetMyTodayLikedAssets:input_type -> topfans.social.GetMyTodayLikedAssetsRequest + 42, // 48: topfans.social.SocialService.GetMyWeekLikedAssets:input_type -> topfans.social.GetMyWeekLikedAssetsRequest + 44, // 49: topfans.social.SocialService.GetUserLikedAssets:input_type -> topfans.social.GetUserLikedAssetsRequest + 3, // 50: topfans.social.SocialService.SendFriendRequest:output_type -> topfans.social.SendFriendRequestResponse + 5, // 51: topfans.social.SocialService.GetFriendRequests:output_type -> topfans.social.GetFriendRequestsResponse + 7, // 52: topfans.social.SocialService.HandleFriendRequest:output_type -> topfans.social.HandleFriendRequestResponse + 9, // 53: topfans.social.SocialService.GetFriendList:output_type -> topfans.social.GetFriendListResponse + 11, // 54: topfans.social.SocialService.DeleteFriend:output_type -> topfans.social.DeleteFriendResponse + 13, // 55: topfans.social.SocialService.SetFriendRemark:output_type -> topfans.social.SetFriendRemarkResponse + 15, // 56: topfans.social.SocialService.CheckFriendship:output_type -> topfans.social.CheckFriendshipResponse + 17, // 57: topfans.social.SocialService.GetFriendCount:output_type -> topfans.social.GetFriendCountResponse + 19, // 58: topfans.social.SocialService.SearchUserForFriend:output_type -> topfans.social.SearchUserForFriendResponse + 23, // 59: topfans.social.SocialService.GetRandomUsers:output_type -> topfans.social.GetRandomUsersResponse + 26, // 60: topfans.social.SocialService.GetUsersPaged:output_type -> topfans.social.GetUsersPagedResponse + 28, // 61: topfans.social.SocialService.LikeAsset:output_type -> topfans.social.LikeAssetResponse + 30, // 62: topfans.social.SocialService.UnlikeAsset:output_type -> topfans.social.UnlikeAssetResponse + 32, // 63: topfans.social.SocialService.CheckAssetLike:output_type -> topfans.social.CheckAssetLikeResponse + 34, // 64: topfans.social.SocialService.GetAssetLikers:output_type -> topfans.social.GetAssetLikersResponse + 37, // 65: topfans.social.SocialService.GetMyLikedAssets:output_type -> topfans.social.GetMyLikedAssetsResponse + 41, // 66: topfans.social.SocialService.GetMyTodayLikedAssets:output_type -> topfans.social.GetMyTodayLikedAssetsResponse + 43, // 67: topfans.social.SocialService.GetMyWeekLikedAssets:output_type -> topfans.social.GetMyWeekLikedAssetsResponse + 45, // 68: topfans.social.SocialService.GetUserLikedAssets:output_type -> topfans.social.GetUserLikedAssetsResponse + 50, // [50:69] is the sub-list for method output_type + 31, // [31:50] is the sub-list for method input_type + 31, // [31:31] is the sub-list for extension type_name + 31, // [31:31] is the sub-list for extension extendee + 0, // [0:31] is the sub-list for field type_name } func init() { file_social_proto_init() } @@ -3146,7 +3387,7 @@ func file_social_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_social_proto_rawDesc), len(file_social_proto_rawDesc)), NumEnums: 0, - NumMessages: 43, + NumMessages: 46, NumExtensions: 0, NumServices: 1, }, diff --git a/backend/pkg/proto/social/social.triple.go b/backend/pkg/proto/social/social.triple.go index a65d2b7..0756656 100644 --- a/backend/pkg/proto/social/social.triple.go +++ b/backend/pkg/proto/social/social.triple.go @@ -64,6 +64,8 @@ const ( SocialServiceUnlikeAssetProcedure = "/topfans.social.SocialService/UnlikeAsset" // SocialServiceCheckAssetLikeProcedure is the fully-qualified name of the SocialService's CheckAssetLike RPC. SocialServiceCheckAssetLikeProcedure = "/topfans.social.SocialService/CheckAssetLike" + // SocialServiceGetAssetLikersProcedure is the fully-qualified name of the SocialService's GetAssetLikers RPC. + SocialServiceGetAssetLikersProcedure = "/topfans.social.SocialService/GetAssetLikers" // SocialServiceGetMyLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyLikedAssets RPC. SocialServiceGetMyLikedAssetsProcedure = "/topfans.social.SocialService/GetMyLikedAssets" // SocialServiceGetMyTodayLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyTodayLikedAssets RPC. @@ -94,6 +96,7 @@ type SocialService interface { LikeAsset(ctx context.Context, req *LikeAssetRequest, opts ...client.CallOption) (*LikeAssetResponse, error) UnlikeAsset(ctx context.Context, req *UnlikeAssetRequest, opts ...client.CallOption) (*UnlikeAssetResponse, error) CheckAssetLike(ctx context.Context, req *CheckAssetLikeRequest, opts ...client.CallOption) (*CheckAssetLikeResponse, error) + GetAssetLikers(ctx context.Context, req *GetAssetLikersRequest, opts ...client.CallOption) (*GetAssetLikersResponse, error) GetMyLikedAssets(ctx context.Context, req *GetMyLikedAssetsRequest, opts ...client.CallOption) (*GetMyLikedAssetsResponse, error) GetMyTodayLikedAssets(ctx context.Context, req *GetMyTodayLikedAssetsRequest, opts ...client.CallOption) (*GetMyTodayLikedAssetsResponse, error) GetMyWeekLikedAssets(ctx context.Context, req *GetMyWeekLikedAssetsRequest, opts ...client.CallOption) (*GetMyWeekLikedAssetsResponse, error) @@ -232,6 +235,14 @@ func (c *SocialServiceImpl) CheckAssetLike(ctx context.Context, req *CheckAssetL return resp, nil } +func (c *SocialServiceImpl) GetAssetLikers(ctx context.Context, req *GetAssetLikersRequest, opts ...client.CallOption) (*GetAssetLikersResponse, error) { + resp := new(GetAssetLikersResponse) + if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetAssetLikers", opts...); err != nil { + return nil, err + } + return resp, nil +} + func (c *SocialServiceImpl) GetMyLikedAssets(ctx context.Context, req *GetMyLikedAssetsRequest, opts ...client.CallOption) (*GetMyLikedAssetsResponse, error) { resp := new(GetMyLikedAssetsResponse) if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMyLikedAssets", opts...); err != nil { @@ -266,7 +277,7 @@ func (c *SocialServiceImpl) GetUserLikedAssets(ctx context.Context, req *GetUser var SocialService_ClientInfo = client.ClientInfo{ InterfaceName: "topfans.social.SocialService", - MethodNames: []string{"SendFriendRequest", "GetFriendRequests", "HandleFriendRequest", "GetFriendList", "DeleteFriend", "SetFriendRemark", "CheckFriendship", "GetFriendCount", "SearchUserForFriend", "GetRandomUsers", "GetUsersPaged", "LikeAsset", "UnlikeAsset", "CheckAssetLike", "GetMyLikedAssets", "GetMyTodayLikedAssets", "GetMyWeekLikedAssets", "GetUserLikedAssets"}, + MethodNames: []string{"SendFriendRequest", "GetFriendRequests", "HandleFriendRequest", "GetFriendList", "DeleteFriend", "SetFriendRemark", "CheckFriendship", "GetFriendCount", "SearchUserForFriend", "GetRandomUsers", "GetUsersPaged", "LikeAsset", "UnlikeAsset", "CheckAssetLike", "GetAssetLikers", "GetMyLikedAssets", "GetMyTodayLikedAssets", "GetMyWeekLikedAssets", "GetUserLikedAssets"}, ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) { dubboCli := dubboCliRaw.(*SocialServiceImpl) dubboCli.conn = conn @@ -289,6 +300,7 @@ type SocialServiceHandler interface { LikeAsset(context.Context, *LikeAssetRequest) (*LikeAssetResponse, error) UnlikeAsset(context.Context, *UnlikeAssetRequest) (*UnlikeAssetResponse, error) CheckAssetLike(context.Context, *CheckAssetLikeRequest) (*CheckAssetLikeResponse, error) + GetAssetLikers(context.Context, *GetAssetLikersRequest) (*GetAssetLikersResponse, error) GetMyLikedAssets(context.Context, *GetMyLikedAssetsRequest) (*GetMyLikedAssetsResponse, error) GetMyTodayLikedAssets(context.Context, *GetMyTodayLikedAssetsRequest) (*GetMyTodayLikedAssetsResponse, error) GetMyWeekLikedAssets(context.Context, *GetMyWeekLikedAssetsRequest) (*GetMyWeekLikedAssetsResponse, error) @@ -517,6 +529,21 @@ var SocialService_ServiceInfo = server.ServiceInfo{ return triple_protocol.NewResponse(res), nil }, }, + { + Name: "GetAssetLikers", + Type: constant.CallUnary, + ReqInitFunc: func() interface{} { + return new(GetAssetLikersRequest) + }, + MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) { + req := args[0].(*GetAssetLikersRequest) + res, err := handler.(SocialServiceHandler).GetAssetLikers(ctx, req) + if err != nil { + return nil, err + } + return triple_protocol.NewResponse(res), nil + }, + }, { Name: "GetMyLikedAssets", Type: constant.CallUnary, diff --git a/backend/proto/social.proto b/backend/proto/social.proto index 85f228c..5538ad6 100644 --- a/backend/proto/social.proto +++ b/backend/proto/social.proto @@ -260,6 +260,31 @@ message CheckAssetLikeResponse { bool is_liked = 2; // 是否已点赞 } +// 获取资产点赞用户列表请求 +message GetAssetLikersRequest { + int64 asset_id = 1; // 资产ID + int32 page_size = 2; // 每页数量(默认20,最大100) + int64 cursor = 3; // 游标(上一页最后一条的 created_at,首次请求传0) +} + +// 获取资产点赞用户列表响应 +message GetAssetLikersResponse { + topfans.common.BaseResponse base = 1; + repeated AssetLiker users = 2; // 点赞用户列表 + int64 total = 3; // 总数 + bool has_more = 4; // 是否有更多 + int64 next_cursor = 5; // 下一页游标 +} + +// 点赞用户信息 +message AssetLiker { + int64 user_id = 1; // 用户ID + string nickname = 2; // 昵称 + string avatar = 3; // 头像URL + int32 fan_level = 4; // 粉丝等级 + int64 liked_at = 5; // 点赞时间(毫秒时间戳) +} + // ==================== 我的作品相关消息 ==================== // 获取我点赞的作品列表请求 @@ -446,6 +471,13 @@ service SocialService { }; } + // 获取资产点赞用户列表 + rpc GetAssetLikers(GetAssetLikersRequest) returns (GetAssetLikersResponse) { + option (google.api.http) = { + get: "/api/v1/social/assets/{asset_id}/likers" + }; + } + // ========== 我的作品相关 ========== // 获取我点赞的作品列表 diff --git a/backend/scripts/init_database.sql b/backend/scripts/init_database.sql index b673f8f..b890d0e 100644 --- a/backend/scripts/init_database.sql +++ b/backend/scripts/init_database.sql @@ -159,7 +159,7 @@ CREATE TABLE IF NOT EXISTS asset_likes ( star_id BIGINT NOT NULL, created_at BIGINT NOT NULL, - CONSTRAINT uk_asset_likes_user_asset UNIQUE (user_id, asset_id) + CONSTRAINT uk_asset_likes_user_asset_exhibition UNIQUE (user_id, asset_id, exhibition_id) ); CREATE INDEX IF NOT EXISTS idx_asset_likes_asset ON asset_likes(asset_id); diff --git a/backend/scripts/migrate_asset_likes_cursor_pagination.sql b/backend/scripts/migrate_asset_likes_cursor_pagination.sql new file mode 100644 index 0000000..4e059f8 --- /dev/null +++ b/backend/scripts/migrate_asset_likes_cursor_pagination.sql @@ -0,0 +1,3 @@ +-- 为游标分页新增索引(幂等) +-- 支持查询:WHERE asset_id = ? AND created_at < ? ORDER BY created_at DESC +CREATE INDEX IF NOT EXISTS idx_asset_likes_asset_created ON asset_likes(asset_id, created_at DESC); \ No newline at end of file diff --git a/backend/scripts/migrate_asset_likes_exhibition_unique.sql b/backend/scripts/migrate_asset_likes_exhibition_unique.sql new file mode 100644 index 0000000..c743f92 --- /dev/null +++ b/backend/scripts/migrate_asset_likes_exhibition_unique.sql @@ -0,0 +1,26 @@ +-- 修改 asset_likes 表唯一约束(幂等) +-- 原约束: (user_id, asset_id) - 用户对某藏品只能点赞一次 +-- 新约束: (user_id, asset_id, exhibition_id) - 用户对某藏品在同一展出中只能点赞一次 +-- 下架后重新展出,用户可以再次点赞 + +-- 删除旧约束(如果存在) +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'uk_asset_likes_user_asset' + ) THEN + ALTER TABLE asset_likes DROP CONSTRAINT uk_asset_likes_user_asset; + END IF; +END $$; + +-- 添加新约束(如果不存在) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'uk_asset_likes_user_asset_exhibition' + ) THEN + ALTER TABLE asset_likes ADD CONSTRAINT uk_asset_likes_user_asset_exhibition UNIQUE (user_id, asset_id, exhibition_id); + END IF; +END $$; \ No newline at end of file diff --git a/backend/services/socialService/provider/social_provider.go b/backend/services/socialService/provider/social_provider.go index a875dfb..f67223b 100644 --- a/backend/services/socialService/provider/social_provider.go +++ b/backend/services/socialService/provider/social_provider.go @@ -373,6 +373,28 @@ func (p *SocialProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetL }, nil } +// GetAssetLikers 获取资产点赞用户列表 +func (p *SocialProvider) GetAssetLikers(ctx context.Context, req *pb.GetAssetLikersRequest) (*pb.GetAssetLikersResponse, error) { + // 从上下文获取用户信息(starID 可选,用于 JOIN fan_profiles) + _, starID, err := extractUserInfo(ctx) + if err != nil { + // starID 获取失败不影响查询,使用 0 让 service 层自动获取 + starID = 0 + logger.Logger.Warn("Failed to extract star_id from context, will get from asset", + zap.Error(err), + ) + } + + logger.Logger.Debug("GetAssetLikers called", + zap.Int64("asset_id", req.AssetId), + zap.Int64("star_id", starID), + zap.Int64("cursor", req.Cursor), + zap.Int32("page_size", req.PageSize), + ) + + return p.assetLikeService.GetAssetLikers(ctx, req.AssetId, starID, req.Cursor, req.PageSize) +} + // ========== 我的作品相关 ========== // GetMyLikedAssets 获取我点赞的作品列表 diff --git a/backend/services/socialService/repository/social_repository.go b/backend/services/socialService/repository/social_repository.go index 1501dfd..f53f7ba 100644 --- a/backend/services/socialService/repository/social_repository.go +++ b/backend/services/socialService/repository/social_repository.go @@ -136,6 +136,26 @@ type SocialRepository interface { // pageSize: 每页数量 // 返回: 作品列表、总数量 GetUserLikedAssets(userID, starID int64, page, pageSize int) ([]*LikedAssetInfo, int64, error) + + // ========== 藏品点赞用户相关 ========== + + // GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页) + // assetID: 资产ID + // starID: 明星ID(用于 JOIN fan_profiles) + // cursor: 游标(上一页最后一条的 created_at,首次请求传0) + // limit: 每页数量 + // 返回: 点赞用户列表 + GetByAssetWithUsers(assetID, starID int64, cursor int64, limit int) ([]*AssetLikeWithUser, error) +} + +// AssetLikeWithUser 点赞记录+用户信息 +type AssetLikeWithUser struct { + UserID int64 + Nickname string `gorm:"column:nickname"` // 来自 fan_profiles.nickname + Avatar string `gorm:"column:avatar_url"` // 来自 users.avatar_url + FanLevel int32 `gorm:"column:fan_level"` // 来自 fan_profiles.level + LikedAt int64 `gorm:"column:liked_at"` + StarID int64 `gorm:"column:star_id"` // 来自 al.star_id,用于 JOIN fan_profiles } // LikedAssetInfo 我点赞的作品信息 @@ -849,3 +869,36 @@ func (r *socialRepositoryImpl) GetUserLikedAssets(userID, starID int64, page, pa return items, total, nil } + +// ========== 藏品点赞用户相关实现 ========== + +// GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页) +func (r *socialRepositoryImpl) GetByAssetWithUsers(assetID, starID int64, cursor int64, limit int) ([]*AssetLikeWithUser, error) { + var results []*AssetLikeWithUser + + query := r.db.Table("asset_likes al"). + Select(`al.user_id, + al.star_id, + COALESCE(fp.nickname, '匿名用户') as nickname, + u.avatar_url, + COALESCE(fp.level, 1) as fan_level, + al.created_at as liked_at`). + Joins("JOIN users u ON al.user_id = u.id"). + Joins("LEFT JOIN fan_profiles fp ON al.user_id = fp.user_id AND al.star_id = fp.star_id"). + Where("al.asset_id = ?", assetID) + + // 游标分页:cursor=0 时跳过游标条件,返回第一页 + if cursor > 0 { + query = query.Where("al.created_at < ?", cursor) + } + + err := query.Order("al.created_at DESC"). + Limit(limit). + Scan(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} diff --git a/backend/services/socialService/service/asset_like_service.go b/backend/services/socialService/service/asset_like_service.go index 7e68fd7..eb848de 100644 --- a/backend/services/socialService/service/asset_like_service.go +++ b/backend/services/socialService/service/asset_like_service.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/topfans/backend/pkg/database" "github.com/topfans/backend/pkg/logger" assetPb "github.com/topfans/backend/pkg/proto/asset" pbCommon "github.com/topfans/backend/pkg/proto/common" @@ -107,6 +108,9 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI zap.Int64("star_id", starID), ) + // 缓存失效 + _ = database.InvalidateAssetLikersCache(ctx, assetID) + return nil } @@ -167,6 +171,9 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta zap.Int64("star_id", starID), ) + // 缓存失效 + _ = database.InvalidateAssetLikersCache(ctx, assetID) + return nil } @@ -484,3 +491,162 @@ func (s *AssetLikeService) GetUserLikedAssets(ctx context.Context, req *pb.GetUs }, nil } +// GetAssetLikers 获取资产点赞用户列表(带缓存) +func (s *AssetLikeService) GetAssetLikers(ctx context.Context, assetID, starID int64, cursor int64, pageSize int32) (*pb.GetAssetLikersResponse, error) { + logger.Logger.Debug("AssetLikeService.GetAssetLikers called", + zap.Int64("asset_id", assetID), + zap.Int64("star_id", starID), + zap.Int64("cursor", cursor), + zap.Int32("page_size", pageSize), + ) + + // 参数校验 + if pageSize <= 0 { + pageSize = 20 + } + if pageSize > 100 { + pageSize = 100 + } + + // 0. 校验资产是否存在 + getAssetReq := &assetPb.GetAssetForRPCRequest{AssetId: assetID} + getAssetResp, err := s.assetClient.GetAssetForRPC(ctx, getAssetReq) + if err != nil { + logger.Logger.Error("Failed to get asset for RPC", + zap.Error(err), + zap.Int64("asset_id", assetID), + ) + return &pb.GetAssetLikersResponse{ + Base: &pbCommon.BaseResponse{ + Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR, + Message: "Failed to get asset", + Timestamp: time.Now().UnixMilli(), + }, + }, nil + } + if getAssetResp.Base.Code != pbCommon.StatusCode_STATUS_OK { + logger.Logger.Warn("Asset not found", + zap.Int64("asset_id", assetID), + ) + return &pb.GetAssetLikersResponse{ + Base: &pbCommon.BaseResponse{ + Code: pbCommon.StatusCode_STATUS_NOT_FOUND, + Message: "Asset not found", + Timestamp: time.Now().UnixMilli(), + }, + }, nil + } + + // 1. 先查缓存 + cache, err := database.GetAssetLikersCache(ctx, assetID) + if err != nil { + logger.Logger.Warn("Failed to get asset likers cache", + zap.Error(err), + zap.Int64("asset_id", assetID), + ) + // 缓存错误不影响主流程,继续查 DB + } + + if cache != nil && len(cache.Users) > 0 { + // 缓存命中,从缓存中切片返回 + return sliceFromCache(cache, cursor, pageSize) + } + + // 2. 缓存未命中,查 DB + // 从已获取的资产信息中获取 starID + actualStarID := getAssetResp.StarId + if actualStarID == 0 { + actualStarID = starID // 兜底使用传入的 starID + } + + // 查询数据库,最多缓存 1000 条 + dbResults, err := s.socialRepo.GetByAssetWithUsers(assetID, actualStarID, 0, 1000) + if err != nil { + logger.Logger.Error("Failed to get asset likers from DB", + zap.Error(err), + zap.Int64("asset_id", assetID), + ) + return &pb.GetAssetLikersResponse{ + Base: &pbCommon.BaseResponse{ + Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR, + Message: "Failed to get asset likers", + Timestamp: time.Now().UnixMilli(), + }, + }, nil + } + + // 3. 写入缓存 + total := int64(len(dbResults)) + cacheUsers := make([]database.AssetLikerWithTotal, 0, len(dbResults)) + for _, r := range dbResults { + cacheUsers = append(cacheUsers, database.AssetLikerWithTotal{ + UserID: r.UserID, + Nickname: r.Nickname, + Avatar: r.Avatar, + FanLevel: r.FanLevel, + LikedAt: r.LikedAt, + StarID: r.StarID, + }) + } + cache = &database.AssetLikersCache{ + Users: cacheUsers, + Total: total, + UpdatedAt: time.Now().UnixMilli(), + } + _ = database.SetAssetLikersCache(ctx, assetID, cache, 60*time.Second) + + // 4. 返回数据 + return sliceFromCache(cache, cursor, pageSize) +} + +// sliceFromCache 从缓存中按游标切片 +func sliceFromCache(cache *database.AssetLikersCache, cursor int64, pageSize int32) (*pb.GetAssetLikersResponse, error) { + users := cache.Users + total := cache.Total + + // 找到起始位置 + start := 0 + if cursor > 0 { + for i, u := range users { + if u.LikedAt < cursor { + start = i + break + } + } + } + + // 找到结束位置 + end := start + int(pageSize) + hasMore := end < len(users) + if end > len(users) { + end = len(users) + } + + // 构建返回结果 + pbUsers := make([]*pb.AssetLiker, 0, end-start) + var nextCursor int64 + for i := start; i < end; i++ { + u := users[i] + pbUsers = append(pbUsers, &pb.AssetLiker{ + UserId: u.UserID, + Nickname: u.Nickname, + Avatar: u.Avatar, + FanLevel: u.FanLevel, + LikedAt: u.LikedAt, + }) + nextCursor = u.LikedAt + } + + return &pb.GetAssetLikersResponse{ + Base: &pbCommon.BaseResponse{ + Code: pbCommon.StatusCode_STATUS_OK, + Message: "success", + Timestamp: time.Now().UnixMilli(), + }, + Users: pbUsers, + Total: total, + HasMore: hasMore, + NextCursor: nextCursor, + }, nil +} + diff --git a/docker/init-db.sql b/docker/init-db.sql index bae6e74..9fb6713 100644 --- a/docker/init-db.sql +++ b/docker/init-db.sql @@ -1129,11 +1129,11 @@ ALTER TABLE ONLY public.activity_user_stats -- --- Name: asset_likes uk_asset_likes_user_asset; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: asset_likes uk_asset_likes_user_asset_exhibition; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.asset_likes - ADD CONSTRAINT uk_asset_likes_user_asset UNIQUE (user_id, asset_id); + ADD CONSTRAINT uk_asset_likes_user_asset_exhibition UNIQUE (user_id, asset_id, exhibition_id); -- @@ -1250,6 +1250,12 @@ CREATE INDEX idx_asset_likes_asset ON public.asset_likes USING btree (asset_id); CREATE INDEX idx_asset_likes_user_star ON public.asset_likes USING btree (user_id, star_id); +-- +-- Name: idx_asset_likes_asset_created; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX IF NOT EXISTS idx_asset_likes_asset_created ON public.asset_likes USING btree (asset_id, created_at DESC); + -- -- Name: idx_registry_owner_star; Type: INDEX; Schema: public; Owner: - diff --git a/frontend/pages/asset-detail/asset-detail.vue b/frontend/pages/asset-detail/asset-detail.vue index 5a8132e..ff59834 100644 --- a/frontend/pages/asset-detail/asset-detail.vue +++ b/frontend/pages/asset-detail/asset-detail.vue @@ -196,17 +196,16 @@ - +