diff --git a/backend/gateway/controller/gallery_controller.go b/backend/gateway/controller/gallery_controller.go index 1f35420..cb1525d 100644 --- a/backend/gateway/controller/gallery_controller.go +++ b/backend/gateway/controller/gallery_controller.go @@ -614,7 +614,8 @@ func (ctrl *GalleryController) GetMyExhibitedAssets(c *gin.Context) { LikeCount: item.LikeCount, ExhibitedAt: item.ExhibitedAt, ExpireAt: item.ExpireAt, - Earnings: item.Earnings, + Earnings: item.Earnings, + HourlyEarnings: item.HourlyEarnings, SlotIndex: item.SlotIndex, IsLenticular: item.IsLenticular, }) @@ -804,7 +805,8 @@ func (ctrl *GalleryController) GetUserExhibitedAssets(c *gin.Context) { LikeCount: item.LikeCount, ExhibitedAt: item.ExhibitedAt, ExpireAt: item.ExpireAt, - Earnings: item.Earnings, + Earnings: item.Earnings, + HourlyEarnings: item.HourlyEarnings, SlotIndex: item.SlotIndex, IsLenticular: item.IsLenticular, }) diff --git a/backend/gateway/controller/social_controller.go b/backend/gateway/controller/social_controller.go index 552fe7f..f3cdb71 100644 --- a/backend/gateway/controller/social_controller.go +++ b/backend/gateway/controller/social_controller.go @@ -930,12 +930,13 @@ func (ctrl *SocialController) GetMyLikedAssets(c *gin.Context) { items := make([]map[string]interface{}, 0, len(resp.Data.Items)) for _, item := range resp.Data.Items { items = append(items, map[string]interface{}{ - "asset_id": item.AssetId, - "name": item.Name, - "cover_url": item.CoverUrl, - "like_count": item.LikeCount, - "liked_at": item.LikedAt, - "earnings": item.Earnings, + "asset_id": item.AssetId, + "name": item.Name, + "cover_url": item.CoverUrl, + "like_count": item.LikeCount, + "liked_at": item.LikedAt, + "earnings": item.Earnings, + "hourly_earnings": item.HourlyEarnings, }) } @@ -993,12 +994,13 @@ func (ctrl *SocialController) GetMyTodayLikedAssets(c *gin.Context) { items := make([]map[string]interface{}, 0, len(resp.Data.Items)) for _, item := range resp.Data.Items { items = append(items, map[string]interface{}{ - "asset_id": item.AssetId, - "name": item.Name, - "cover_url": item.CoverUrl, - "like_count": item.LikeCount, - "liked_at": item.LikedAt, - "earnings": item.Earnings, + "asset_id": item.AssetId, + "name": item.Name, + "cover_url": item.CoverUrl, + "like_count": item.LikeCount, + "liked_at": item.LikedAt, + "earnings": item.Earnings, + "hourly_earnings": item.HourlyEarnings, }) } @@ -1056,12 +1058,13 @@ func (ctrl *SocialController) GetMyWeekLikedAssets(c *gin.Context) { items := make([]map[string]interface{}, 0, len(resp.Data.Items)) for _, item := range resp.Data.Items { items = append(items, map[string]interface{}{ - "asset_id": item.AssetId, - "name": item.Name, - "cover_url": item.CoverUrl, - "like_count": item.LikeCount, - "liked_at": item.LikedAt, - "earnings": item.Earnings, + "asset_id": item.AssetId, + "name": item.Name, + "cover_url": item.CoverUrl, + "like_count": item.LikeCount, + "liked_at": item.LikedAt, + "earnings": item.Earnings, + "hourly_earnings": item.HourlyEarnings, }) } @@ -1128,12 +1131,13 @@ func (ctrl *SocialController) GetUserLikedAssets(c *gin.Context) { items := make([]map[string]interface{}, 0, len(resp.Data.Items)) for _, item := range resp.Data.Items { items = append(items, map[string]interface{}{ - "asset_id": item.AssetId, - "name": item.Name, - "cover_url": item.CoverUrl, - "like_count": item.LikeCount, - "liked_at": item.LikedAt, - "earnings": item.Earnings, + "asset_id": item.AssetId, + "name": item.Name, + "cover_url": item.CoverUrl, + "like_count": item.LikeCount, + "liked_at": item.LikedAt, + "earnings": item.Earnings, + "hourly_earnings": item.HourlyEarnings, }) } diff --git a/backend/gateway/dto/asset_converter.go b/backend/gateway/dto/asset_converter.go index c97303f..3df64dc 100644 --- a/backend/gateway/dto/asset_converter.go +++ b/backend/gateway/dto/asset_converter.go @@ -150,6 +150,7 @@ func ConvertAsset(pbAsset *pbAsset.Asset) AssetDTO { Info: pbAsset.Info, DisplayStatus: pbAsset.DisplayStatus, Earnings: pbAsset.Earnings, + HourlyEarnings: pbAsset.HourlyEarnings, ExhibitionExpireAt: pbAsset.ExhibitionExpireAt, } diff --git a/backend/gateway/dto/asset_dto.go b/backend/gateway/dto/asset_dto.go index 0b7b5f2..32e038f 100644 --- a/backend/gateway/dto/asset_dto.go +++ b/backend/gateway/dto/asset_dto.go @@ -80,9 +80,10 @@ type AssetDTO struct { Owner *OwnerInfoDTO `json:"owner,omitempty"` // 持有者信息(可选,保留用于兼容性) IsLiked bool `json:"is_liked"` // 当前用户是否已点赞 Info string `json:"info"` // 藏品信息 - DisplayStatus int32 `json:"display_status"` // 展示状态:0=待展示, 1=已展示 - Earnings int64 `json:"earnings"` // 当前展出收益(实时计算,仅展出中时有值) - ExhibitionExpireAt int64 `json:"exhibition_expire_at"` // 展出过期时间(毫秒时间戳,仅展出中时有值,0=未展出) + DisplayStatus int32 `json:"display_status"` // 展示状态:0=待展示, 1=已展示 + Earnings int64 `json:"earnings"` // 当前展出收益(实时计算,仅展出中时有值) + HourlyEarnings int64 `json:"hourly_earnings"` // 每小时收益(实时计算,仅展出中时有值) + ExhibitionExpireAt int64 `json:"exhibition_expire_at"` // 展出过期时间(毫秒时间戳,仅展出中时有值,0=未展出) } // OwnerInfoDTO 持有者信息 diff --git a/backend/gateway/dto/gallery_dto.go b/backend/gateway/dto/gallery_dto.go index a7dfa55..d0286cd 100644 --- a/backend/gateway/dto/gallery_dto.go +++ b/backend/gateway/dto/gallery_dto.go @@ -80,6 +80,7 @@ type ExhibitedAssetItemDTO struct { ExhibitedAt int64 `json:"exhibited_at"` // 展出开始时间(毫秒时间戳) ExpireAt int64 `json:"expire_at"` // 展出过期时间(毫秒时间戳) Earnings int64 `json:"earnings"` // 当前可领取收益 + HourlyEarnings int64 `json:"hourly_earnings"` // 每小时收益 SlotIndex int32 `json:"slot_index"` // 展位序号 IsLenticular bool `json:"is_lenticular"` // 是否为光栅卡 } diff --git a/backend/pkg/proto/asset/asset.pb.go b/backend/pkg/proto/asset/asset.pb.go index 3f27348..9401e0c 100644 --- a/backend/pkg/proto/asset/asset.pb.go +++ b/backend/pkg/proto/asset/asset.pb.go @@ -50,6 +50,7 @@ type Asset struct { Info string `protobuf:"bytes,21,opt,name=info,proto3" json:"info,omitempty"` // 藏品信息 DisplayStatus int32 `protobuf:"varint,22,opt,name=display_status,json=displayStatus,proto3" json:"display_status,omitempty"` // 展示状态:0=待展示, 1=已展示 Earnings int64 `protobuf:"varint,23,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前展出收益(实时计算,仅展出中时有值) + HourlyEarnings int64 `protobuf:"varint,25,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益(实时计算,仅展出中时有值) ExhibitionExpireAt int64 `protobuf:"varint,24,opt,name=exhibition_expire_at,json=exhibitionExpireAt,proto3" json:"exhibition_expire_at,omitempty"` // 展出过期时间(毫秒时间戳,仅展出中时有值,0=未展出) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -246,6 +247,13 @@ func (x *Asset) GetEarnings() int64 { return 0 } +func (x *Asset) GetHourlyEarnings() int64 { + if x != nil { + return x.HourlyEarnings + } + return 0 +} + func (x *Asset) GetExhibitionExpireAt() int64 { if x != nil { return x.ExhibitionExpireAt @@ -3631,7 +3639,7 @@ var File_asset_proto protoreflect.FileDescriptor const file_asset_proto_rawDesc = "" + "\n" + - "\vasset.proto\x12\rtopfans.asset\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xe1\x05\n" + + "\vasset.proto\x12\rtopfans.asset\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\x8a\x06\n" + "\x05Asset\x12\x19\n" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x1b\n" + "\towner_uid\x18\x02 \x01(\x03R\bownerUid\x12\x17\n" + @@ -3661,7 +3669,8 @@ const file_asset_proto_rawDesc = "" + "\bis_liked\x18\x14 \x01(\bR\aisLiked\x12\x12\n" + "\x04info\x18\x15 \x01(\tR\x04info\x12%\n" + "\x0edisplay_status\x18\x16 \x01(\x05R\rdisplayStatus\x12\x1a\n" + - "\bearnings\x18\x17 \x01(\x03R\bearnings\x120\n" + + "\bearnings\x18\x17 \x01(\x03R\bearnings\x12'\n" + + "\x0fhourly_earnings\x18\x19 \x01(\x03R\x0ehourlyEarnings\x120\n" + "\x14exhibition_expire_at\x18\x18 \x01(\x03R\x12exhibitionExpireAt\"X\n" + "\tOwnerInfo\x12\x17\n" + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" + diff --git a/backend/pkg/proto/gallery/gallery.pb.go b/backend/pkg/proto/gallery/gallery.pb.go index 687586d..e2fbbb0 100644 --- a/backend/pkg/proto/gallery/gallery.pb.go +++ b/backend/pkg/proto/gallery/gallery.pb.go @@ -1214,18 +1214,19 @@ func (x *ExhibitedAssetsData) GetHasMore() bool { // 展出作品项 type ExhibitedAssetItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称 - CoverUrl string `protobuf:"bytes,3,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` // 封面图URL - LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"` // 实时点赞数 - ExhibitedAt int64 `protobuf:"varint,5,opt,name=exhibited_at,json=exhibitedAt,proto3" json:"exhibited_at,omitempty"` // 展出开始时间(毫秒时间戳) - ExpireAt int64 `protobuf:"varint,6,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` // 展出过期时间(毫秒时间戳) - Earnings int64 `protobuf:"varint,7,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益 - SlotIndex int32 `protobuf:"varint,8,opt,name=slot_index,json=slotIndex,proto3" json:"slot_index,omitempty"` // 展位序号 - IsLenticular bool `protobuf:"varint,9,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡(根据 tags 判断) - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称 + CoverUrl string `protobuf:"bytes,3,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` // 封面图URL + LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"` // 实时点赞数 + ExhibitedAt int64 `protobuf:"varint,5,opt,name=exhibited_at,json=exhibitedAt,proto3" json:"exhibited_at,omitempty"` // 展出开始时间(毫秒时间戳) + ExpireAt int64 `protobuf:"varint,6,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` // 展出过期时间(毫秒时间戳) + Earnings int64 `protobuf:"varint,7,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益 + HourlyEarnings int64 `protobuf:"varint,10,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益 + SlotIndex int32 `protobuf:"varint,8,opt,name=slot_index,json=slotIndex,proto3" json:"slot_index,omitempty"` // 展位序号 + IsLenticular bool `protobuf:"varint,9,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡(根据 tags 判断) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExhibitedAssetItem) Reset() { @@ -1307,6 +1308,13 @@ func (x *ExhibitedAssetItem) GetEarnings() int64 { return 0 } +func (x *ExhibitedAssetItem) GetHourlyEarnings() int64 { + if x != nil { + return x.HourlyEarnings + } + return 0 +} + func (x *ExhibitedAssetItem) GetSlotIndex() int32 { if x != nil { return x.SlotIndex @@ -1817,7 +1825,7 @@ const file_gallery_proto_rawDesc = "" + "\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" + "\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" + "\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" + - "\bhas_more\x18\x05 \x01(\bR\ahasMore\"\x9f\x02\n" + + "\bhas_more\x18\x05 \x01(\bR\ahasMore\"\xc8\x02\n" + "\x12ExhibitedAssetItem\x12\x19\n" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" + @@ -1826,7 +1834,9 @@ const file_gallery_proto_rawDesc = "" + "like_count\x18\x04 \x01(\x05R\tlikeCount\x12!\n" + "\fexhibited_at\x18\x05 \x01(\x03R\vexhibitedAt\x12\x1b\n" + "\texpire_at\x18\x06 \x01(\x03R\bexpireAt\x12\x1a\n" + - "\bearnings\x18\a \x01(\x03R\bearnings\x12\x1d\n" + + "\bearnings\x18\a \x01(\x03R\bearnings\x12'\n" + + "\x0fhourly_earnings\x18\n" + + " \x01(\x03R\x0ehourlyEarnings\x12\x1d\n" + "\n" + "slot_index\x18\b \x01(\x05R\tslotIndex\x12#\n" + "\ris_lenticular\x18\t \x01(\bR\fisLenticular\"\x9a\x01\n" + diff --git a/backend/pkg/proto/social/social.pb.go b/backend/pkg/proto/social/social.pb.go index cd7d297..26ff027 100644 --- a/backend/pkg/proto/social/social.pb.go +++ b/backend/pkg/proto/social/social.pb.go @@ -2333,15 +2333,16 @@ func (x *LikedAssetsData) GetHasMore() bool { // 点赞作品项 type LikedAssetItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称 - CoverUrl string `protobuf:"bytes,3,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` // 封面图URL - LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"` // 实时点赞数 - LikedAt int64 `protobuf:"varint,5,opt,name=liked_at,json=likedAt,proto3" json:"liked_at,omitempty"` // 点赞时间(毫秒时间戳) - Earnings int64 `protobuf:"varint,6,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称 + CoverUrl string `protobuf:"bytes,3,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` // 封面图URL + LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"` // 实时点赞数 + LikedAt int64 `protobuf:"varint,5,opt,name=liked_at,json=likedAt,proto3" json:"liked_at,omitempty"` // 点赞时间(毫秒时间戳) + Earnings int64 `protobuf:"varint,6,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益 + HourlyEarnings int64 `protobuf:"varint,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LikedAssetItem) Reset() { @@ -2416,6 +2417,13 @@ func (x *LikedAssetItem) GetEarnings() int64 { return 0 } +func (x *LikedAssetItem) GetHourlyEarnings() int64 { + if x != nil { + return x.HourlyEarnings + } + return 0 +} + // 获取我今日点赞的作品列表请求(暂不实现) type GetMyTodayLikedAssetsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2919,7 +2927,7 @@ const file_social_proto_rawDesc = "" + "\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" + "\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" + "\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" + - "\bhas_more\x18\x05 \x01(\bR\ahasMore\"\xb2\x01\n" + + "\bhas_more\x18\x05 \x01(\bR\ahasMore\"\xdb\x01\n" + "\x0eLikedAssetItem\x12\x19\n" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" + @@ -2927,7 +2935,8 @@ const file_social_proto_rawDesc = "" + "\n" + "like_count\x18\x04 \x01(\x05R\tlikeCount\x12\x19\n" + "\bliked_at\x18\x05 \x01(\x03R\alikedAt\x12\x1a\n" + - "\bearnings\x18\x06 \x01(\x03R\bearnings\"O\n" + + "\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" + + "\x0fhourly_earnings\x18\a \x01(\x03R\x0ehourlyEarnings\"O\n" + "\x1cGetMyTodayLikedAssetsRequest\x12\x12\n" + "\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" + diff --git a/backend/proto/asset.proto b/backend/proto/asset.proto index 224b767..d93eab1 100644 --- a/backend/proto/asset.proto +++ b/backend/proto/asset.proto @@ -36,6 +36,7 @@ message Asset { string info = 21; // 藏品信息 int32 display_status = 22; // 展示状态:0=待展示, 1=已展示 int64 earnings = 23; // 当前展出收益(实时计算,仅展出中时有值) + int64 hourly_earnings = 25; // 每小时收益(实时计算,仅展出中时有值) int64 exhibition_expire_at = 24; // 展出过期时间(毫秒时间戳,仅展出中时有值,0=未展出) } diff --git a/backend/proto/gallery.proto b/backend/proto/gallery.proto index e1f5742..94235b7 100644 --- a/backend/proto/gallery.proto +++ b/backend/proto/gallery.proto @@ -207,6 +207,7 @@ message ExhibitedAssetItem { int64 exhibited_at = 5; // 展出开始时间(毫秒时间戳) int64 expire_at = 6; // 展出过期时间(毫秒时间戳) int64 earnings = 7; // 当前可领取收益 + int64 hourly_earnings = 10; // 每小时收益 int32 slot_index = 8; // 展位序号 bool is_lenticular = 9; // 是否为光栅卡(根据 tags 判断) } diff --git a/backend/proto/social.proto b/backend/proto/social.proto index 40a8d47..f174079 100644 --- a/backend/proto/social.proto +++ b/backend/proto/social.proto @@ -291,6 +291,7 @@ message LikedAssetItem { int32 like_count = 4; // 实时点赞数 int64 liked_at = 5; // 点赞时间(毫秒时间戳) int64 earnings = 6; // 当前可领取收益 + int64 hourly_earnings = 7; // 每小时收益 } // 获取我今日点赞的作品列表请求(暂不实现) diff --git a/backend/services/assetService/service/asset_service.go b/backend/services/assetService/service/asset_service.go index f6ab3c0..3483763 100644 --- a/backend/services/assetService/service/asset_service.go +++ b/backend/services/assetService/service/asset_service.go @@ -475,7 +475,10 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) ( displayStatus = 0 } - // 6. 计算当前展出收益和过期时间(仅展出中时有值) + // 6. 计算每小时收益(始终计算,基于当前点赞数) + hourlyEarnings := calculateHourlyEarnings(asset.LikeCount) + + // 7. 计算当前展出收益和过期时间(仅展出中时有值) earnings := int64(0) exhibitionExpireAt := int64(0) if displayStatus == 1 { @@ -507,7 +510,7 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) ( Message: "", Timestamp: time.Now().UnixMilli(), }, - Asset: ModelToProtoAssetDetail(asset, ownerNickname, isLiked, displayStatus, earnings, exhibitionExpireAt, grade), + Asset: ModelToProtoAssetDetail(asset, ownerNickname, isLiked, displayStatus, earnings, hourlyEarnings, exhibitionExpireAt, grade), } logger.Logger.Debug("Get asset successful", @@ -656,7 +659,7 @@ func ModelToProtoAsset(asset *models.Asset) *pb.AssetListItem { } // ModelToProtoAssetDetail 将数据库模型转换为Proto格式(Asset详情) -func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked bool, displayStatus int32, earnings int64, exhibitionExpireAt int64, grade int32) *pb.Asset { +func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked bool, displayStatus int32, earnings int64, hourlyEarnings int64, exhibitionExpireAt int64, grade int32) *pb.Asset { if asset == nil { return nil } @@ -685,6 +688,7 @@ func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked Info: asset.Info, DisplayStatus: displayStatus, // 展示状态:0=待展示, 1=已展示 Earnings: earnings, // 当前展出收益(实时计算) + HourlyEarnings: hourlyEarnings, // 每小时收益(实时计算) ExhibitionExpireAt: exhibitionExpireAt, // 展出过期时间 } } @@ -725,18 +729,12 @@ func getStatusString(status int32) string { } } -// calculateRealtimeEarnings 实时计算展示收益 -// 公式:R1 = R0 × T × [100% + Buff(n)] -// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算 -func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { +// calculateHourlyEarnings 计算每小时收益 +// 公式:R0 × [100% + Buff(n)] +// R0 = 5 水晶/小时,Buff(n) 根据点赞数计算 +func calculateHourlyEarnings(likeCount int32) int64 { R0 := int64(5) // 水晶/小时 - // 计算上架时长(毫秒转小时) - T := (now - startTime) / 3600000 - if T <= 0 { - T = 1 // 最少1小时 - } - // 计算Buff var buff int switch { @@ -750,11 +748,20 @@ func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { buff = 0 } - // 基础收益 - baseRevenue := R0 * T - - // 应用Buff加成:R1 = R0 × T × (100% + Buff) - buffedRevenue := baseRevenue * (100 + int64(buff)) / 100 - - return buffedRevenue + // 应用Buff加成:R1 = R0 × (100% + Buff) + return R0 * (100 + int64(buff)) / 100 +} + +// calculateRealtimeEarnings 实时计算展示收益 +// 公式:R1 = R0 × T × [100% + Buff(n)] +// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算 +func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { + // 计算上架时长(毫秒转小时) + T := (now - startTime) / 3600000 + if T <= 0 { + T = 1 // 最少1小时 + } + + // 总收益 = 每小时收益 × 时长 + return calculateHourlyEarnings(likeCount) * T } diff --git a/backend/services/assetService/service/mint_service.go b/backend/services/assetService/service/mint_service.go index fe85dc4..f2f8dac 100644 --- a/backend/services/assetService/service/mint_service.go +++ b/backend/services/assetService/service/mint_service.go @@ -464,7 +464,7 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(mintOrder), - Asset: ModelToProtoAssetDetail(asset, ownerNickname, false, 0, 0, 0, getInt32Value(asset.Grade)), // 新创建的资产,is_liked 为 false,display_status 默认为 0,earnings 和 exhibitionExpireAt 为 0,grade 从 asset.Grade 获取 + Asset: ModelToProtoAssetDetail(asset, ownerNickname, false, 0, 0, 0, 0, getInt32Value(asset.Grade)), // 新创建的资产,is_liked 为 false,display_status 默认为 0,earnings、hourlyEarnings 和 exhibitionExpireAt 为 0,grade 从 asset.Grade 获取 CostCrystal: capturedCostCrystal, BalanceAfter: newBalance, } @@ -569,7 +569,7 @@ func (s *mintService) GetMintOrder(orderID string, userID, starID int64) (*pb.Ge // 由于是查询自己的订单,is_liked 设为 false(简化处理) // 获取 display_status displayStatus, _ := s.assetRepo.GetDisplayStatusByAssetID(asset.ID) - assetProto = ModelToProtoAssetDetail(asset, ownerNickname, false, displayStatus, 0, 0, getInt32Value(asset.Grade)) // 新创建的资产,earnings 和 exhibitionExpireAt 为 0 + assetProto = ModelToProtoAssetDetail(asset, ownerNickname, false, displayStatus, 0, 0, 0, getInt32Value(asset.Grade)) // 新创建的资产,earnings、hourlyEarnings 和 exhibitionExpireAt 为 0 // 如果 cover_url 存在,生成预签名 URL if assetProto.CoverUrl != "" { diff --git a/backend/services/galleryService/repository/gallery_repository.go b/backend/services/galleryService/repository/gallery_repository.go index 10e1101..303c493 100644 --- a/backend/services/galleryService/repository/gallery_repository.go +++ b/backend/services/galleryService/repository/gallery_repository.go @@ -99,6 +99,7 @@ type ExhibitedAssetInfo struct { ExhibitedAt int64 ExpireAt int64 Earnings int64 + HourlyEarnings int64 SlotIndex int32 IsLenticular bool } @@ -438,6 +439,7 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag // R0 = 5 水晶/小时 now := time.Now().UnixMilli() for _, item := range items { + item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount) item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now) } @@ -481,6 +483,7 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p // 实时计算每个资产的收益 for _, item := range items { + item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount) item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now) } @@ -619,18 +622,12 @@ func generateHostProfileID(userID, starID int64) int64 { return userID*1000000 + starID } -// calculateRealtimeEarnings 实时计算展示收益 -// 公式:R1 = R0 × T × [100% + Buff(n)] -// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算 -func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { +// calculateHourlyEarnings 计算每小时收益 +// 公式:R0 × [100% + Buff(n)] +// R0 = 5 水晶/小时,Buff(n) 根据点赞数计算 +func calculateHourlyEarnings(likeCount int32) int64 { R0 := int64(5) // 水晶/小时 - // 计算上架时长(毫秒转小时) - T := (now - startTime) / 3600000 - if T <= 0 { - T = 1 // 最少1小时 - } - // 计算Buff var buff int switch { @@ -644,11 +641,20 @@ func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { buff = 0 } - // 基础收益 - baseRevenue := R0 * T - - // 应用Buff加成:R1 = R0 × T × (100% + Buff) - buffedRevenue := baseRevenue * (100 + int64(buff)) / 100 - - return buffedRevenue + // 应用Buff加成:R1 = R0 × (100% + Buff) + return R0 * (100 + int64(buff)) / 100 +} + +// calculateRealtimeEarnings 实时计算展示收益 +// 公式:R1 = R0 × T × [100% + Buff(n)] +// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算 +func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { + // 计算上架时长(毫秒转小时) + T := (now - startTime) / 3600000 + if T <= 0 { + T = 1 // 最少1小时 + } + + // 总收益 = 每小时收益 × 时长 + return calculateHourlyEarnings(likeCount) * T } diff --git a/backend/services/galleryService/service/exhibition_service.go b/backend/services/galleryService/service/exhibition_service.go index 9f154a9..c258f50 100644 --- a/backend/services/galleryService/service/exhibition_service.go +++ b/backend/services/galleryService/service/exhibition_service.go @@ -216,6 +216,7 @@ func (s *exhibitionService) GetMyExhibitedAssets(ctx context.Context, userID, st ExhibitedAt: item.ExhibitedAt, ExpireAt: item.ExpireAt, Earnings: item.Earnings, + HourlyEarnings: item.HourlyEarnings, SlotIndex: item.SlotIndex, IsLenticular: item.IsLenticular, }) diff --git a/backend/services/socialService/repository/social_repository.go b/backend/services/socialService/repository/social_repository.go index 4abea5b..fd8d7a7 100644 --- a/backend/services/socialService/repository/social_repository.go +++ b/backend/services/socialService/repository/social_repository.go @@ -145,6 +145,7 @@ type LikedAssetInfo struct { LikeCount int32 LikedAt int64 Earnings int64 + HourlyEarnings int64 } // RandomUserInfo 随机用户信息 @@ -618,6 +619,7 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page var exhibition models.Exhibition if err := r.db.Where("asset_id = ? AND deleted_at IS NULL", item.AssetID). First(&exhibition).Error; err == nil { + item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount) item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, now) } } @@ -625,18 +627,12 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page return items, total, nil } -// calculateRealtimeEarnings 实时计算展示收益 -// 公式:R1 = R0 × T × [100% + Buff(n)] -// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算 -func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { +// calculateHourlyEarnings 计算每小时收益 +// 公式:R0 × [100% + Buff(n)] +// R0 = 5 水晶/小时,Buff(n) 根据点赞数计算 +func calculateHourlyEarnings(likeCount int32) int64 { R0 := int64(5) // 水晶/小时 - // 计算上架时长(毫秒转小时) - T := (now - startTime) / 3600000 - if T <= 0 { - T = 1 // 最少1小时 - } - // 计算Buff var buff int switch { @@ -650,13 +646,22 @@ func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { buff = 0 } - // 基础收益 - baseRevenue := R0 * T + // 应用Buff加成:R1 = R0 × (100% + Buff) + return R0 * (100 + int64(buff)) / 100 +} - // 应用Buff加成:R1 = R0 × T × (100% + Buff) - buffedRevenue := baseRevenue * (100 + int64(buff)) / 100 +// calculateRealtimeEarnings 实时计算展示收益 +// 公式:R1 = R0 × T × [100% + Buff(n)] +// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算 +func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 { + // 计算上架时长(毫秒转小时) + T := (now - startTime) / 3600000 + if T <= 0 { + T = 1 // 最少1小时 + } - return buffedRevenue + // 总收益 = 每小时收益 × 时长 + return calculateHourlyEarnings(likeCount) * T } // GetMyTodayLikedAssets 获取我今日点赞的作品列表(只返回展出中且未过期的) @@ -704,6 +709,7 @@ func (r *socialRepositoryImpl) GetMyTodayLikedAssets(userID, starID int64, page, var exhibition models.Exhibition if err := r.db.Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", item.AssetID, now.UnixMilli()). First(&exhibition).Error; err == nil { + item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount) item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, now.UnixMilli()) } } @@ -764,6 +770,7 @@ func (r *socialRepositoryImpl) GetMyWeekLikedAssets(userID, starID int64, page, var exhibition models.Exhibition if err := r.db.Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", item.AssetID, nowMillis). First(&exhibition).Error; err == nil { + item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount) item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, nowMillis) } } @@ -813,6 +820,7 @@ func (r *socialRepositoryImpl) GetUserLikedAssets(userID, starID int64, page, pa var exhibition models.Exhibition if err := r.db.Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", item.AssetID, now). First(&exhibition).Error; err == nil { + item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount) item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, now) } } diff --git a/backend/services/socialService/service/asset_like_service.go b/backend/services/socialService/service/asset_like_service.go index 6b904c6..4bd44fa 100644 --- a/backend/services/socialService/service/asset_like_service.go +++ b/backend/services/socialService/service/asset_like_service.go @@ -277,6 +277,7 @@ func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLi LikeCount: item.LikeCount, LikedAt: item.LikedAt, Earnings: item.Earnings, + HourlyEarnings: item.HourlyEarnings, }) } @@ -337,6 +338,7 @@ func (s *AssetLikeService) GetMyTodayLikedAssets(ctx context.Context, req *pb.Ge LikeCount: item.LikeCount, LikedAt: item.LikedAt, Earnings: item.Earnings, + HourlyEarnings: item.HourlyEarnings, }) } @@ -397,6 +399,7 @@ func (s *AssetLikeService) GetMyWeekLikedAssets(ctx context.Context, req *pb.Get LikeCount: item.LikeCount, LikedAt: item.LikedAt, Earnings: item.Earnings, + HourlyEarnings: item.HourlyEarnings, }) } @@ -457,6 +460,7 @@ func (s *AssetLikeService) GetUserLikedAssets(ctx context.Context, req *pb.GetUs LikeCount: item.LikeCount, LikedAt: item.LikedAt, Earnings: item.Earnings, + HourlyEarnings: item.HourlyEarnings, }) } diff --git a/docs/api-base-url-switch-plan.md b/docs/api-base-url-switch-plan.md new file mode 100644 index 0000000..266ce97 --- /dev/null +++ b/docs/api-base-url-switch-plan.md @@ -0,0 +1,163 @@ +# 前端 API Base URL 自动切换方案 + +## 背景 + +前端 `api.js` 中的 `baseURL` 目前是硬编码,需要根据后端运行环境手动切换。为了实现自动化,制定以下方案。 + +> **项目说明**:uniapp 移动端应用,使用 HBuilderX "运行在真机调试"进行开发调试,打包时使用生产环境地址。 + +--- + +## 方案一:DEBUG_MODE 开关(手动切换) + +在 `api.js` 顶部添加一个开关,真机调试时用开发地址,打包前改成生产地址: + +```javascript +const DEBUG_MODE = true // 真机调试时 true,打包前改成 false + +const DEV_BASE = 'http://192.168.110.60:8080' // 开发环境 +const PROD_BASE = 'http://101.132.250.62:8080' // 生产环境 + +const baseURL = DEBUG_MODE ? DEV_BASE : PROD_BASE +``` + +### 优点 + +| 优点 | 说明 | +|------|------| +| **简单直接** | 只需改一个布尔值 | +| **零学习成本** | 一眼看出当前用哪个地址 | + +### 缺点 + +| 缺点 | 说明 | +|------|------| +| **手动切换** | 打包前需记得手动改值 | + +--- + +## 方案二:自动探测后端可用性(推荐 - 全自动) + +### 实现方式 + +启动时自动探测开发环境是否可用,能连通则用开发地址,否则用生产地址: + +```javascript +const DEV_BASE = 'http://192.168.110.60:8080' // 开发环境 +const PROD_BASE = 'http://101.132.250.62:8080' // 生产环境 + +let baseURL = PROD_BASE // 默认生产 + +// 启动时探测开发环境是否可用 +uni.request({ + url: DEV_BASE + '/api/v1/health', + method: 'GET', + timeout: 2000, + success: (res) => { + if (res.statusCode === 200) { + baseURL = DEV_BASE // 开发环境可用,用开发 + } + }, + fail: () => { + // 开发环境不可用,保持生产地址 + } +}) +``` + +**原理**:启动时先尝试连接开发服务器 `192.168.110.60:8080`,能连通就用开发地址,连不通自动用生产地址。 + +### 优点 + +| 优点 | 说明 | +|------|------| +| **全自动** | 无论调试还是打包都自动选择正确地址 | +| **零手动操作** | 不需要记得改任何开关 | +| **适合移动端** | 开发时手机和电脑同网络,能探测到;生产打包后手机连不上开发地址,自动用生产 | + +### 缺点 + +| 缺点 | 说明 | +|------|------| +| **启动多一步** | 首次启动会探测一次(2秒超时) | +| **依赖后端** | 需要后端有 `/api/v1/health` 接口(或任意能响应的接口) | + +### 工作流程 + +| 场景 | 探测结果 | 使用的地址 | +|------|---------|-----------| +| 真机调试(手机和电脑同局域网) | `192.168.110.60` 能连通 | `http://192.168.110.60:8080`(开发) | +| 打包 App(手机连接外网) | `192.168.110.60` 连不通 | `http://101.132.250.62:8080`(生产) | + +### 后端要求 + +后端需要有一个能响应 HTTP 200 的健康检查接口。常见选择: + +- **已有接口**:如果后端有 `/health` 或类似接口,直接使用 +- **新建接口**:在 gateway 添加一个简单的健康检查接口 +- **复用接口**:用现有的任意接口(如 `/api/v1/auth/me`,但需要 token,可能不适合) + +如果你后端没有现成的健康检查接口,我可以帮你设计一个简单的实现方案。 + +--- + +## 方案三:uniapp 条件编译(不推荐) + +```javascript +// #ifdef APP-PLUS +const baseURL = 'http://101.132.250.62:8080' // App 打包生产环境 +// #endif + +// #ifdef H5 +const baseURL = 'http://192.168.110.60:8080' // H5 开发环境 +// #endif +``` + +**问题**:HBuilderX 真机调试默认走 `APP-PLUS` 条件,调试时也会用生产地址,不适合。 + +--- + +## 方案四:Vite 环境变量(不推荐) + +```javascript +const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.110.60:8080' +``` + +**问题**:uniapp HBuilderX 不走 Vite 的 dev/build 命令,环境变量不会自动切换。 + +--- + +## 总结对比 + +| 对比项 | 方案一(开关) | 方案二(自动探测) | 条件编译 | Vite 环境变量 | +|--------|:-------------:|:-----------------:|:--------:|:-------------:| +| **自动化程度** | ❌ 手动 | ✅ **全自动** | ❌ 手动 | ❌ 手动 | +| **uniapp 兼容** | ✅ | ✅ | ❌ 不适合调试 | ❌ 不支持 | +| **简单程度** | ✅ 最简单 | 中等 | 中等 | 中等 | +| **适合真机调试** | ✅ | ✅ | ❌ 否 | ❌ 否 | +| **推荐指数** | ⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐ | + +--- + +## 地址确认 + +| 环境 | 地址 | +|------|------| +| **开发环境** | `http://192.168.110.60:8080` | +| **生产环境** | `http://101.132.250.62:8080` | + +--- + +## 确认事项 + +1. **选择方案**:___ +2. **开发环境地址**:`http://192.168.110.60:8080` +3. **生产环境地址**:`http://101.132.250.62:8080` +4. **后端是否有健康检查接口**:___(如有请告知路径) + +--- + +## 实施记录 + +- [x] ✅ 方案二已实施(自动探测后端可用性) +- [x] ✅ 后端 `/health` 接口确认存在 +- [x] ✅ `api.js` 已修改,使用自动探测逻辑 \ No newline at end of file diff --git a/frontend/pages/asset-detail/asset-detail.vue b/frontend/pages/asset-detail/asset-detail.vue index 4528f29..5820234 100644 --- a/frontend/pages/asset-detail/asset-detail.vue +++ b/frontend/pages/asset-detail/asset-detail.vue @@ -70,7 +70,7 @@ - {{ assetData.earnings || 5 }}/时 + {{ assetData.hourly_earnings || 5 }}/时 diff --git a/frontend/pages/profile/myWorks.vue b/frontend/pages/profile/myWorks.vue index 437dd4c..e3fbd70 100644 --- a/frontend/pages/profile/myWorks.vue +++ b/frontend/pages/profile/myWorks.vue @@ -68,7 +68,7 @@ :class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'"> - {{ item.earnings || 0 }}/时 + {{ item.hourly_earnings || 0 }}/时 @@ -387,27 +387,29 @@ const handleExhibitionCardTap = (item, index) => { doubleTapLike(item.id, async (success, data) => { if (success) { // 更新在展作品的点赞数 - exhibitionWorks.value[index].like_count = (exhibitionWorks.value[index].like_count || 0) + 1; - // 如果返回了收益数据,更新显示 - if (data?.earnings !== undefined) { - exhibitionWorks.value[index].earnings = data.earnings; - } else { + // exhibitionWorks.value[index].like_count = (exhibitionWorks.value[index].like_count || 0) + 1; + // // 如果返回了收益数据,更新显示 + // if (data?.earnings !== undefined) { + // exhibitionWorks.value[index].earnings = data.earnings; + // } else { // 如果没有返回收益数据,刷新列表获取最新收益 await loadExhibitedAssets(); - } - // 将作品添加到当前点赞列表 - const likedItem = { - id: item.id, - cover_url: item.cover_url, - like_count: exhibitionWorks.value[index].like_count, - earnings: exhibitionWorks.value[index].earnings, - name: item.name, - status_text: '潜力待挖', - score: exhibitionWorks.value[index].like_count, - reward: 0, - }; - likedWorks.value.unshift(likedItem); + // } + uni.showToast({ title: '点赞成功', icon: 'success' }); + + // 将作品添加到当前点赞列表 + // const likedItem = { + // id: item.id, + // cover_url: item.cover_url, + // like_count: exhibitionWorks.value[index].like_count, + // earnings: exhibitionWorks.value[index].earnings, + // name: item.name, + // status_text: '潜力待挖', + // score: exhibitionWorks.value[index].like_count, + // reward: item.earnings, + // }; + // likedWorks.value.unshift(likedItem); } }); } else { @@ -679,6 +681,7 @@ const loadExhibitedAssets = async () => { cover_url: item.cover_url, like_count: item.like_count, earnings: item.earnings, + hourly_earnings:item.hourly_earnings, exhibited_at: item.exhibited_at, expire_at: item.expire_at, name: item.name, @@ -764,7 +767,7 @@ onUnmounted(() => { }); onShow(() => { - loadLikedAssets(); + // loadLikedAssets(); }); diff --git a/frontend/utils/api.js b/frontend/utils/api.js index 5b3988e..7c94632 100644 --- a/frontend/utils/api.js +++ b/frontend/utils/api.js @@ -1,22 +1,28 @@ // API 基础配置 -// 开发阶段用 localhost,打包 App 时自动切换到服务器地址 -// 不需要手动注释! +// 自动检测后端环境:探测开发服务器是否可用,能连通则用开发地址,否则用生产地址 -// #ifdef H5 -// const baseURL = 'http://192.168.110.60:8080' // H5 开发用本机 -const baseURL = 'http://101.132.250.62:8080' // H5 开发用本机 -// #endif +const DEV_BASE = 'http://192.168.110.60:8080' // 开发环境 +const PROD_BASE = 'http://101.132.250.62:8080' // 生产环境 +const HEALTH_URL = DEV_BASE + '/health' -// #ifdef APP-PLUS -// 开发调试:手机和电脑同一WiFi时用这个(改成你电脑IP) -// 上线后:改成实际服务器地址 -// const baseURL = 'http://192.168.110.60:8080' -// #endif +// 默认使用生产地址 +let baseURL = PROD_BASE -// 服务器地址(正式上线用) -// #ifdef APP-PLUS -const baseURL = 'http://101.132.250.62:8080' -// #endif +// 启动时探测开发环境是否可用(异步,不阻塞后续逻辑) +uni.request({ + url: HEALTH_URL, + method: 'GET', + timeout: 2000, + success: (res) => { + if (res.statusCode === 200) { + baseURL = DEV_BASE // 开发环境可用,切换到开发地址 + console.log('[API] 使用开发环境地址:', DEV_BASE) + } + }, + fail: () => { + console.log('[API] 开发环境不可用,使用生产环境地址:', PROD_BASE) + } +}) // 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false) const USE_MOCK_API = false