From 74182ad66285686a70729c73a715eb22a621eb2a Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Thu, 14 May 2026 15:58:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=BB=8F=E6=B5=8E?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/activity.pb.go | 1579 ----------------- backend/gateway/controller/task_controller.go | 7 - backend/gateway/dto/response_dto.go | 2 - backend/gateway/dto/user_converter.go | 2 - backend/pkg/models/level.go | 132 ++ backend/pkg/models/mint.go | 85 + backend/pkg/models/user.go | 48 +- backend/pkg/proto/asset/asset.triple.go | 2 +- backend/pkg/proto/common/common.triple.go | 17 - backend/pkg/proto/gallery/gallery.triple.go | 2 +- backend/pkg/proto/ranking/ranking.triple.go | 2 +- backend/pkg/proto/social/social.triple.go | 8 +- backend/pkg/proto/task/task.pb.go | 99 +- backend/pkg/proto/user/user.pb.go | 212 ++- backend/pkg/proto/user/user.triple.go | 24 +- backend/proto/task.proto | 19 +- backend/proto/user.proto | 38 +- backend/scripts/20260513_economic_system.sql | 278 +++ backend/scripts/create_gallery_test_users.go | 8 +- .../assetService/client/user_rpc_client.go | 7 +- backend/services/assetService/main.go | 4 +- .../repository/mint_cost_repository.go | 45 + .../repository/user_mint_count_repository.go | 115 ++ .../galleryService/client/asset_rpc_client.go | 26 + .../galleryService/client/task_rpc_client.go | 100 ++ .../galleryService/client/user_rpc_client.go | 55 + backend/services/galleryService/main.go | 22 +- .../repository/gallery_repository.go | 12 + .../taskService/client/user_rpc_client.go | 24 +- .../services/taskService/model/task_models.go | 2 - .../taskService/repository/onboarding_repo.go | 5 +- .../taskService/worker/daily_reset_worker.go | 3 +- .../userService/provider/unified_provider.go | 6 +- .../userService/provider/user_provider.go | 24 +- .../repository/fan_profile_repository.go | 266 ++- .../2026-04-15-economic-system-design.md | 31 +- 36 files changed, 1415 insertions(+), 1896 deletions(-) delete mode 100644 backend/activity.pb.go create mode 100644 backend/pkg/models/level.go create mode 100644 backend/pkg/models/mint.go delete mode 100644 backend/pkg/proto/common/common.triple.go create mode 100644 backend/scripts/20260513_economic_system.sql create mode 100644 backend/services/assetService/repository/mint_cost_repository.go create mode 100644 backend/services/assetService/repository/user_mint_count_repository.go create mode 100644 backend/services/galleryService/client/task_rpc_client.go diff --git a/backend/activity.pb.go b/backend/activity.pb.go deleted file mode 100644 index 4f7a0ed..0000000 --- a/backend/activity.pb.go +++ /dev/null @@ -1,1579 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v7.34.0 -// source: activity.proto - -package activity - -import ( - common "github.com/topfans/backend/pkg/proto/common" - _ "google.golang.org/genproto/googleapis/api/annotations" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// 活动信息 -type Activity struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - ActivityType string `protobuf:"bytes,2,opt,name=activity_type,json=activityType,proto3" json:"activity_type,omitempty"` - Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` - Theme string `protobuf:"bytes,17,opt,name=theme,proto3" json:"theme,omitempty"` // 中文主题 - Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - StarId int64 `protobuf:"varint,5,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` - StartTime int64 `protobuf:"varint,6,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` - EndTime int64 `protobuf:"varint,7,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` - TargetProgress int64 `protobuf:"varint,8,opt,name=target_progress,json=targetProgress,proto3" json:"target_progress,omitempty"` - CurrentProgress int64 `protobuf:"varint,9,opt,name=current_progress,json=currentProgress,proto3" json:"current_progress,omitempty"` - Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty"` - CurrentStage string `protobuf:"bytes,11,opt,name=current_stage,json=currentStage,proto3" json:"current_stage,omitempty"` // 当前阶段: early/mid/late/completed - Items []*ActivityItem `protobuf:"bytes,12,rep,name=items,proto3" json:"items,omitempty"` - CoverImage string `protobuf:"bytes,13,opt,name=cover_image,json=coverImage,proto3" json:"cover_image,omitempty"` // 封面图 - BannerImage string `protobuf:"bytes,14,opt,name=banner_image,json=bannerImage,proto3" json:"banner_image,omitempty"` // 横幅图 - CurrentStageBackground string `protobuf:"bytes,15,opt,name=current_stage_background,json=currentStageBackground,proto3" json:"current_stage_background,omitempty"` // 当前阶段背景图 - CurrentStageTitle string `protobuf:"bytes,16,opt,name=current_stage_title,json=currentStageTitle,proto3" json:"current_stage_title,omitempty"` // 当前阶段标题 - OverallEndTime int64 `protobuf:"varint,18,opt,name=overall_end_time,json=overallEndTime,proto3" json:"overall_end_time,omitempty"` // 整体活动结束时间 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Activity) Reset() { - *x = Activity{} - mi := &file_activity_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Activity) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Activity) ProtoMessage() {} - -func (x *Activity) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[0] - 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 Activity.ProtoReflect.Descriptor instead. -func (*Activity) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{0} -} - -func (x *Activity) GetId() int64 { - if x != nil { - return x.Id - } - return 0 -} - -func (x *Activity) GetActivityType() string { - if x != nil { - return x.ActivityType - } - return "" -} - -func (x *Activity) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *Activity) GetTheme() string { - if x != nil { - return x.Theme - } - return "" -} - -func (x *Activity) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *Activity) GetStarId() int64 { - if x != nil { - return x.StarId - } - return 0 -} - -func (x *Activity) GetStartTime() int64 { - if x != nil { - return x.StartTime - } - return 0 -} - -func (x *Activity) GetEndTime() int64 { - if x != nil { - return x.EndTime - } - return 0 -} - -func (x *Activity) GetTargetProgress() int64 { - if x != nil { - return x.TargetProgress - } - return 0 -} - -func (x *Activity) GetCurrentProgress() int64 { - if x != nil { - return x.CurrentProgress - } - return 0 -} - -func (x *Activity) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *Activity) GetCurrentStage() string { - if x != nil { - return x.CurrentStage - } - return "" -} - -func (x *Activity) GetItems() []*ActivityItem { - if x != nil { - return x.Items - } - return nil -} - -func (x *Activity) GetCoverImage() string { - if x != nil { - return x.CoverImage - } - return "" -} - -func (x *Activity) GetBannerImage() string { - if x != nil { - return x.BannerImage - } - return "" -} - -func (x *Activity) GetCurrentStageBackground() string { - if x != nil { - return x.CurrentStageBackground - } - return "" -} - -func (x *Activity) GetCurrentStageTitle() string { - if x != nil { - return x.CurrentStageTitle - } - return "" -} - -func (x *Activity) GetOverallEndTime() int64 { - if x != nil { - return x.OverallEndTime - } - return 0 -} - -// 活动道具 -type ActivityItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - ItemType string `protobuf:"bytes,2,opt,name=item_type,json=itemType,proto3" json:"item_type,omitempty"` - ItemName string `protobuf:"bytes,3,opt,name=item_name,json=itemName,proto3" json:"item_name,omitempty"` - IconUrl string `protobuf:"bytes,4,opt,name=icon_url,json=iconUrl,proto3" json:"icon_url,omitempty"` - CrystalCost int32 `protobuf:"varint,5,opt,name=crystal_cost,json=crystalCost,proto3" json:"crystal_cost,omitempty"` - ContributionPoints int32 `protobuf:"varint,6,opt,name=contribution_points,json=contributionPoints,proto3" json:"contribution_points,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ActivityItem) Reset() { - *x = ActivityItem{} - mi := &file_activity_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ActivityItem) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ActivityItem) ProtoMessage() {} - -func (x *ActivityItem) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[1] - 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 ActivityItem.ProtoReflect.Descriptor instead. -func (*ActivityItem) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{1} -} - -func (x *ActivityItem) GetId() int64 { - if x != nil { - return x.Id - } - return 0 -} - -func (x *ActivityItem) GetItemType() string { - if x != nil { - return x.ItemType - } - return "" -} - -func (x *ActivityItem) GetItemName() string { - if x != nil { - return x.ItemName - } - return "" -} - -func (x *ActivityItem) GetIconUrl() string { - if x != nil { - return x.IconUrl - } - return "" -} - -func (x *ActivityItem) GetCrystalCost() int32 { - if x != nil { - return x.CrystalCost - } - return 0 -} - -func (x *ActivityItem) GetContributionPoints() int32 { - if x != nil { - return x.ContributionPoints - } - return 0 -} - -// 活动道具列表响应 -type ActivityItemsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Items []*ActivityItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ActivityItemsResponse) Reset() { - *x = ActivityItemsResponse{} - mi := &file_activity_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ActivityItemsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ActivityItemsResponse) ProtoMessage() {} - -func (x *ActivityItemsResponse) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[2] - 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 ActivityItemsResponse.ProtoReflect.Descriptor instead. -func (*ActivityItemsResponse) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{2} -} - -func (x *ActivityItemsResponse) GetItems() []*ActivityItem { - if x != nil { - return x.Items - } - return nil -} - -// 购买道具请求 -type PurchaseItemRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ActivityId int64 `protobuf:"varint,1,opt,name=activity_id,json=activityId,proto3" json:"activity_id,omitempty"` - ItemType string `protobuf:"bytes,2,opt,name=item_type,json=itemType,proto3" json:"item_type,omitempty"` - Quantity int32 `protobuf:"varint,3,opt,name=quantity,proto3" json:"quantity,omitempty"` - StarId int64 `protobuf:"varint,4,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` - UserId int64 `protobuf:"varint,5,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 当前用户ID - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PurchaseItemRequest) Reset() { - *x = PurchaseItemRequest{} - mi := &file_activity_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PurchaseItemRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PurchaseItemRequest) ProtoMessage() {} - -func (x *PurchaseItemRequest) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[3] - 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 PurchaseItemRequest.ProtoReflect.Descriptor instead. -func (*PurchaseItemRequest) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{3} -} - -func (x *PurchaseItemRequest) GetActivityId() int64 { - if x != nil { - return x.ActivityId - } - return 0 -} - -func (x *PurchaseItemRequest) GetItemType() string { - if x != nil { - return x.ItemType - } - return "" -} - -func (x *PurchaseItemRequest) GetQuantity() int32 { - if x != nil { - return x.Quantity - } - return 0 -} - -func (x *PurchaseItemRequest) GetStarId() int64 { - if x != nil { - return x.StarId - } - return 0 -} - -func (x *PurchaseItemRequest) GetUserId() int64 { - if x != nil { - return x.UserId - } - return 0 -} - -// 购买道具响应 -type PurchaseItemResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - TotalCrystalSpent int64 `protobuf:"varint,2,opt,name=total_crystal_spent,json=totalCrystalSpent,proto3" json:"total_crystal_spent,omitempty"` // 本次消费水晶 - TotalContribution int64 `protobuf:"varint,3,opt,name=total_contribution,json=totalContribution,proto3" json:"total_contribution,omitempty"` // 本次获得贡献点 - CurrentProgress int64 `protobuf:"varint,4,opt,name=current_progress,json=currentProgress,proto3" json:"current_progress,omitempty"` // 当前活动进度 - RemainingBalance int64 `protobuf:"varint,5,opt,name=remaining_balance,json=remainingBalance,proto3" json:"remaining_balance,omitempty"` // 剩余水晶余额 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PurchaseItemResponse) Reset() { - *x = PurchaseItemResponse{} - mi := &file_activity_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PurchaseItemResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PurchaseItemResponse) ProtoMessage() {} - -func (x *PurchaseItemResponse) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[4] - 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 PurchaseItemResponse.ProtoReflect.Descriptor instead. -func (*PurchaseItemResponse) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{4} -} - -func (x *PurchaseItemResponse) GetBase() *common.BaseResponse { - if x != nil { - return x.Base - } - return nil -} - -func (x *PurchaseItemResponse) GetTotalCrystalSpent() int64 { - if x != nil { - return x.TotalCrystalSpent - } - return 0 -} - -func (x *PurchaseItemResponse) GetTotalContribution() int64 { - if x != nil { - return x.TotalContribution - } - return 0 -} - -func (x *PurchaseItemResponse) GetCurrentProgress() int64 { - if x != nil { - return x.CurrentProgress - } - return 0 -} - -func (x *PurchaseItemResponse) GetRemainingBalance() int64 { - if x != nil { - return x.RemainingBalance - } - return 0 -} - -// 贡献点排名请求 -type ContributionRankingRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ActivityId int64 `protobuf:"varint,1,opt,name=activity_id,json=activityId,proto3" json:"activity_id,omitempty"` - StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` - Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - UserId int64 `protobuf:"varint,5,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 当前用户ID,用于获取自己的排名 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ContributionRankingRequest) Reset() { - *x = ContributionRankingRequest{} - mi := &file_activity_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ContributionRankingRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ContributionRankingRequest) ProtoMessage() {} - -func (x *ContributionRankingRequest) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[5] - 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 ContributionRankingRequest.ProtoReflect.Descriptor instead. -func (*ContributionRankingRequest) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{5} -} - -func (x *ContributionRankingRequest) GetActivityId() int64 { - if x != nil { - return x.ActivityId - } - return 0 -} - -func (x *ContributionRankingRequest) GetStarId() int64 { - if x != nil { - return x.StarId - } - return 0 -} - -func (x *ContributionRankingRequest) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *ContributionRankingRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *ContributionRankingRequest) GetUserId() int64 { - if x != nil { - return x.UserId - } - return 0 -} - -// 贡献点排名项 -type ContributionRankingItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - Rank int32 `protobuf:"varint,1,opt,name=rank,proto3" json:"rank,omitempty"` - UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - Nickname string `protobuf:"bytes,3,opt,name=nickname,proto3" json:"nickname,omitempty"` - AvatarUrl string `protobuf:"bytes,4,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` - TotalContribution int64 `protobuf:"varint,5,opt,name=total_contribution,json=totalContribution,proto3" json:"total_contribution,omitempty"` - TotalCrystalSpent int64 `protobuf:"varint,6,opt,name=total_crystal_spent,json=totalCrystalSpent,proto3" json:"total_crystal_spent,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ContributionRankingItem) Reset() { - *x = ContributionRankingItem{} - mi := &file_activity_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ContributionRankingItem) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ContributionRankingItem) ProtoMessage() {} - -func (x *ContributionRankingItem) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[6] - 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 ContributionRankingItem.ProtoReflect.Descriptor instead. -func (*ContributionRankingItem) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{6} -} - -func (x *ContributionRankingItem) GetRank() int32 { - if x != nil { - return x.Rank - } - return 0 -} - -func (x *ContributionRankingItem) GetUserId() int64 { - if x != nil { - return x.UserId - } - return 0 -} - -func (x *ContributionRankingItem) GetNickname() string { - if x != nil { - return x.Nickname - } - return "" -} - -func (x *ContributionRankingItem) GetAvatarUrl() string { - if x != nil { - return x.AvatarUrl - } - return "" -} - -func (x *ContributionRankingItem) GetTotalContribution() int64 { - if x != nil { - return x.TotalContribution - } - return 0 -} - -func (x *ContributionRankingItem) GetTotalCrystalSpent() int64 { - if x != nil { - return x.TotalCrystalSpent - } - return 0 -} - -// 贡献点排名响应 -type ContributionRankingResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - Items []*ContributionRankingItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` - MyContribution *MyContribution `protobuf:"bytes,3,opt,name=my_contribution,json=myContribution,proto3" json:"my_contribution,omitempty"` - Page int32 `protobuf:"varint,4,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,5,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - Total int32 `protobuf:"varint,6,opt,name=total,proto3" json:"total,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ContributionRankingResponse) Reset() { - *x = ContributionRankingResponse{} - mi := &file_activity_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ContributionRankingResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ContributionRankingResponse) ProtoMessage() {} - -func (x *ContributionRankingResponse) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[7] - 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 ContributionRankingResponse.ProtoReflect.Descriptor instead. -func (*ContributionRankingResponse) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{7} -} - -func (x *ContributionRankingResponse) GetBase() *common.BaseResponse { - if x != nil { - return x.Base - } - return nil -} - -func (x *ContributionRankingResponse) GetItems() []*ContributionRankingItem { - if x != nil { - return x.Items - } - return nil -} - -func (x *ContributionRankingResponse) GetMyContribution() *MyContribution { - if x != nil { - return x.MyContribution - } - return nil -} - -func (x *ContributionRankingResponse) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *ContributionRankingResponse) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *ContributionRankingResponse) GetTotal() int32 { - if x != nil { - return x.Total - } - return 0 -} - -// 我的贡献信息 -type MyContribution struct { - state protoimpl.MessageState `protogen:"open.v1"` - Rank int32 `protobuf:"varint,1,opt,name=rank,proto3" json:"rank,omitempty"` - TotalContribution int64 `protobuf:"varint,2,opt,name=total_contribution,json=totalContribution,proto3" json:"total_contribution,omitempty"` - TotalCrystalSpent int64 `protobuf:"varint,3,opt,name=total_crystal_spent,json=totalCrystalSpent,proto3" json:"total_crystal_spent,omitempty"` - Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // ranked/unranked - Nickname string `protobuf:"bytes,5,opt,name=nickname,proto3" json:"nickname,omitempty"` - AvatarUrl string `protobuf:"bytes,6,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *MyContribution) Reset() { - *x = MyContribution{} - mi := &file_activity_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *MyContribution) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MyContribution) ProtoMessage() {} - -func (x *MyContribution) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[8] - 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 MyContribution.ProtoReflect.Descriptor instead. -func (*MyContribution) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{8} -} - -func (x *MyContribution) GetRank() int32 { - if x != nil { - return x.Rank - } - return 0 -} - -func (x *MyContribution) GetTotalContribution() int64 { - if x != nil { - return x.TotalContribution - } - return 0 -} - -func (x *MyContribution) GetTotalCrystalSpent() int64 { - if x != nil { - return x.TotalCrystalSpent - } - return 0 -} - -func (x *MyContribution) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *MyContribution) GetNickname() string { - if x != nil { - return x.Nickname - } - return "" -} - -func (x *MyContribution) GetAvatarUrl() string { - if x != nil { - return x.AvatarUrl - } - return "" -} - -// 活动列表请求 -type GetActivityListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - StarId int64 `protobuf:"varint,1,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` - Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` // 可选: pending/active/completed/expired - Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetActivityListRequest) Reset() { - *x = GetActivityListRequest{} - mi := &file_activity_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetActivityListRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetActivityListRequest) ProtoMessage() {} - -func (x *GetActivityListRequest) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[9] - 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 GetActivityListRequest.ProtoReflect.Descriptor instead. -func (*GetActivityListRequest) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{9} -} - -func (x *GetActivityListRequest) GetStarId() int64 { - if x != nil { - return x.StarId - } - return 0 -} - -func (x *GetActivityListRequest) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *GetActivityListRequest) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *GetActivityListRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -// 活动列表响应 -type GetActivityListResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - Activities []*Activity `protobuf:"bytes,2,rep,name=activities,proto3" json:"activities,omitempty"` - Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - Total int32 `protobuf:"varint,5,opt,name=total,proto3" json:"total,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetActivityListResponse) Reset() { - *x = GetActivityListResponse{} - mi := &file_activity_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetActivityListResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetActivityListResponse) ProtoMessage() {} - -func (x *GetActivityListResponse) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[10] - 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 GetActivityListResponse.ProtoReflect.Descriptor instead. -func (*GetActivityListResponse) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{10} -} - -func (x *GetActivityListResponse) GetBase() *common.BaseResponse { - if x != nil { - return x.Base - } - return nil -} - -func (x *GetActivityListResponse) GetActivities() []*Activity { - if x != nil { - return x.Activities - } - return nil -} - -func (x *GetActivityListResponse) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *GetActivityListResponse) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *GetActivityListResponse) GetTotal() int32 { - if x != nil { - return x.Total - } - return 0 -} - -// 进度信息请求 -type GetProgressRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ActivityId int64 `protobuf:"varint,1,opt,name=activity_id,json=activityId,proto3" json:"activity_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetProgressRequest) Reset() { - *x = GetProgressRequest{} - mi := &file_activity_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetProgressRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetProgressRequest) ProtoMessage() {} - -func (x *GetProgressRequest) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[11] - 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 GetProgressRequest.ProtoReflect.Descriptor instead. -func (*GetProgressRequest) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{11} -} - -func (x *GetProgressRequest) GetActivityId() int64 { - if x != nil { - return x.ActivityId - } - return 0 -} - -// 进度信息响应 -type GetProgressResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - ActivityId int64 `protobuf:"varint,2,opt,name=activity_id,json=activityId,proto3" json:"activity_id,omitempty"` - CurrentProgress int64 `protobuf:"varint,3,opt,name=current_progress,json=currentProgress,proto3" json:"current_progress,omitempty"` - TargetProgress int64 `protobuf:"varint,4,opt,name=target_progress,json=targetProgress,proto3" json:"target_progress,omitempty"` - CurrentStage string `protobuf:"bytes,5,opt,name=current_stage,json=currentStage,proto3" json:"current_stage,omitempty"` // early/mid/late/completed - EndTime int64 `protobuf:"varint,6,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` - Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` // pending/active/completed/expired - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetProgressResponse) Reset() { - *x = GetProgressResponse{} - mi := &file_activity_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetProgressResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetProgressResponse) ProtoMessage() {} - -func (x *GetProgressResponse) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[12] - 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 GetProgressResponse.ProtoReflect.Descriptor instead. -func (*GetProgressResponse) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{12} -} - -func (x *GetProgressResponse) GetBase() *common.BaseResponse { - if x != nil { - return x.Base - } - return nil -} - -func (x *GetProgressResponse) GetActivityId() int64 { - if x != nil { - return x.ActivityId - } - return 0 -} - -func (x *GetProgressResponse) GetCurrentProgress() int64 { - if x != nil { - return x.CurrentProgress - } - return 0 -} - -func (x *GetProgressResponse) GetTargetProgress() int64 { - if x != nil { - return x.TargetProgress - } - return 0 -} - -func (x *GetProgressResponse) GetCurrentStage() string { - if x != nil { - return x.CurrentStage - } - return "" -} - -func (x *GetProgressResponse) GetEndTime() int64 { - if x != nil { - return x.EndTime - } - return 0 -} - -func (x *GetProgressResponse) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -// 铸造活动信息(用于运营banner) -type MintingActivity struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - CoverImage string `protobuf:"bytes,4,opt,name=cover_image,json=coverImage,proto3" json:"cover_image,omitempty"` - StarId int64 `protobuf:"varint,5,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` - Route string `protobuf:"bytes,6,opt,name=route,proto3" json:"route,omitempty"` - IsActive bool `protobuf:"varint,7,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` - CreatedAt int64 `protobuf:"varint,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt int64 `protobuf:"varint,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *MintingActivity) Reset() { - *x = MintingActivity{} - mi := &file_activity_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *MintingActivity) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MintingActivity) ProtoMessage() {} - -func (x *MintingActivity) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[13] - 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 MintingActivity.ProtoReflect.Descriptor instead. -func (*MintingActivity) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{13} -} - -func (x *MintingActivity) GetId() int64 { - if x != nil { - return x.Id - } - return 0 -} - -func (x *MintingActivity) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *MintingActivity) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *MintingActivity) GetCoverImage() string { - if x != nil { - return x.CoverImage - } - return "" -} - -func (x *MintingActivity) GetStarId() int64 { - if x != nil { - return x.StarId - } - return 0 -} - -func (x *MintingActivity) GetRoute() string { - if x != nil { - return x.Route - } - return "" -} - -func (x *MintingActivity) GetIsActive() bool { - if x != nil { - return x.IsActive - } - return false -} - -func (x *MintingActivity) GetCreatedAt() int64 { - if x != nil { - return x.CreatedAt - } - return 0 -} - -func (x *MintingActivity) GetUpdatedAt() int64 { - if x != nil { - return x.UpdatedAt - } - return 0 -} - -// 获取铸造活动列表请求 -type GetMintingActivitiesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - StarId int64 `protobuf:"varint,1,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 可选,不传则返回所有 - Page int32 `protobuf:"varint,2,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetMintingActivitiesRequest) Reset() { - *x = GetMintingActivitiesRequest{} - mi := &file_activity_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetMintingActivitiesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetMintingActivitiesRequest) ProtoMessage() {} - -func (x *GetMintingActivitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[14] - 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 GetMintingActivitiesRequest.ProtoReflect.Descriptor instead. -func (*GetMintingActivitiesRequest) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{14} -} - -func (x *GetMintingActivitiesRequest) GetStarId() int64 { - if x != nil { - return x.StarId - } - return 0 -} - -func (x *GetMintingActivitiesRequest) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *GetMintingActivitiesRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -// 获取铸造活动列表响应 -type GetMintingActivitiesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - Activities []*MintingActivity `protobuf:"bytes,2,rep,name=activities,proto3" json:"activities,omitempty"` - Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - Total int32 `protobuf:"varint,5,opt,name=total,proto3" json:"total,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetMintingActivitiesResponse) Reset() { - *x = GetMintingActivitiesResponse{} - mi := &file_activity_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetMintingActivitiesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetMintingActivitiesResponse) ProtoMessage() {} - -func (x *GetMintingActivitiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_activity_proto_msgTypes[15] - 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 GetMintingActivitiesResponse.ProtoReflect.Descriptor instead. -func (*GetMintingActivitiesResponse) Descriptor() ([]byte, []int) { - return file_activity_proto_rawDescGZIP(), []int{15} -} - -func (x *GetMintingActivitiesResponse) GetBase() *common.BaseResponse { - if x != nil { - return x.Base - } - return nil -} - -func (x *GetMintingActivitiesResponse) GetActivities() []*MintingActivity { - if x != nil { - return x.Activities - } - return nil -} - -func (x *GetMintingActivitiesResponse) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *GetMintingActivitiesResponse) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *GetMintingActivitiesResponse) GetTotal() int32 { - if x != nil { - return x.Total - } - return 0 -} - -var File_activity_proto protoreflect.FileDescriptor - -const file_activity_proto_rawDesc = "" + - "\n" + - "\x0eactivity.proto\x12\x10topfans.activity\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xff\x04\n" + - "\bActivity\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x03R\x02id\x12#\n" + - "\ractivity_type\x18\x02 \x01(\tR\factivityType\x12\x14\n" + - "\x05title\x18\x03 \x01(\tR\x05title\x12\x14\n" + - "\x05theme\x18\x11 \x01(\tR\x05theme\x12 \n" + - "\vdescription\x18\x04 \x01(\tR\vdescription\x12\x17\n" + - "\astar_id\x18\x05 \x01(\x03R\x06starId\x12\x1d\n" + - "\n" + - "start_time\x18\x06 \x01(\x03R\tstartTime\x12\x19\n" + - "\bend_time\x18\a \x01(\x03R\aendTime\x12'\n" + - "\x0ftarget_progress\x18\b \x01(\x03R\x0etargetProgress\x12)\n" + - "\x10current_progress\x18\t \x01(\x03R\x0fcurrentProgress\x12\x16\n" + - "\x06status\x18\n" + - " \x01(\tR\x06status\x12#\n" + - "\rcurrent_stage\x18\v \x01(\tR\fcurrentStage\x124\n" + - "\x05items\x18\f \x03(\v2\x1e.topfans.activity.ActivityItemR\x05items\x12\x1f\n" + - "\vcover_image\x18\r \x01(\tR\n" + - "coverImage\x12!\n" + - "\fbanner_image\x18\x0e \x01(\tR\vbannerImage\x128\n" + - "\x18current_stage_background\x18\x0f \x01(\tR\x16currentStageBackground\x12.\n" + - "\x13current_stage_title\x18\x10 \x01(\tR\x11currentStageTitle\x12(\n" + - "\x10overall_end_time\x18\x12 \x01(\x03R\x0eoverallEndTime\"\xc7\x01\n" + - "\fActivityItem\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1b\n" + - "\titem_type\x18\x02 \x01(\tR\bitemType\x12\x1b\n" + - "\titem_name\x18\x03 \x01(\tR\bitemName\x12\x19\n" + - "\bicon_url\x18\x04 \x01(\tR\aiconUrl\x12!\n" + - "\fcrystal_cost\x18\x05 \x01(\x05R\vcrystalCost\x12/\n" + - "\x13contribution_points\x18\x06 \x01(\x05R\x12contributionPoints\"M\n" + - "\x15ActivityItemsResponse\x124\n" + - "\x05items\x18\x01 \x03(\v2\x1e.topfans.activity.ActivityItemR\x05items\"\xa1\x01\n" + - "\x13PurchaseItemRequest\x12\x1f\n" + - "\vactivity_id\x18\x01 \x01(\x03R\n" + - "activityId\x12\x1b\n" + - "\titem_type\x18\x02 \x01(\tR\bitemType\x12\x1a\n" + - "\bquantity\x18\x03 \x01(\x05R\bquantity\x12\x17\n" + - "\astar_id\x18\x04 \x01(\x03R\x06starId\x12\x17\n" + - "\auser_id\x18\x05 \x01(\x03R\x06userId\"\xff\x01\n" + - "\x14PurchaseItemResponse\x120\n" + - "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12.\n" + - "\x13total_crystal_spent\x18\x02 \x01(\x03R\x11totalCrystalSpent\x12-\n" + - "\x12total_contribution\x18\x03 \x01(\x03R\x11totalContribution\x12)\n" + - "\x10current_progress\x18\x04 \x01(\x03R\x0fcurrentProgress\x12+\n" + - "\x11remaining_balance\x18\x05 \x01(\x03R\x10remainingBalance\"\xa0\x01\n" + - "\x1aContributionRankingRequest\x12\x1f\n" + - "\vactivity_id\x18\x01 \x01(\x03R\n" + - "activityId\x12\x17\n" + - "\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x12\n" + - "\x04page\x18\x03 \x01(\x05R\x04page\x12\x1b\n" + - "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x17\n" + - "\auser_id\x18\x05 \x01(\x03R\x06userId\"\xe0\x01\n" + - "\x17ContributionRankingItem\x12\x12\n" + - "\x04rank\x18\x01 \x01(\x05R\x04rank\x12\x17\n" + - "\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x1a\n" + - "\bnickname\x18\x03 \x01(\tR\bnickname\x12\x1d\n" + - "\n" + - "avatar_url\x18\x04 \x01(\tR\tavatarUrl\x12-\n" + - "\x12total_contribution\x18\x05 \x01(\x03R\x11totalContribution\x12.\n" + - "\x13total_crystal_spent\x18\x06 \x01(\x03R\x11totalCrystalSpent\"\xa2\x02\n" + - "\x1bContributionRankingResponse\x120\n" + - "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12?\n" + - "\x05items\x18\x02 \x03(\v2).topfans.activity.ContributionRankingItemR\x05items\x12I\n" + - "\x0fmy_contribution\x18\x03 \x01(\v2 .topfans.activity.MyContributionR\x0emyContribution\x12\x12\n" + - "\x04page\x18\x04 \x01(\x05R\x04page\x12\x1b\n" + - "\tpage_size\x18\x05 \x01(\x05R\bpageSize\x12\x14\n" + - "\x05total\x18\x06 \x01(\x05R\x05total\"\xd6\x01\n" + - "\x0eMyContribution\x12\x12\n" + - "\x04rank\x18\x01 \x01(\x05R\x04rank\x12-\n" + - "\x12total_contribution\x18\x02 \x01(\x03R\x11totalContribution\x12.\n" + - "\x13total_crystal_spent\x18\x03 \x01(\x03R\x11totalCrystalSpent\x12\x16\n" + - "\x06status\x18\x04 \x01(\tR\x06status\x12\x1a\n" + - "\bnickname\x18\x05 \x01(\tR\bnickname\x12\x1d\n" + - "\n" + - "avatar_url\x18\x06 \x01(\tR\tavatarUrl\"z\n" + - "\x16GetActivityListRequest\x12\x17\n" + - "\astar_id\x18\x01 \x01(\x03R\x06starId\x12\x16\n" + - "\x06status\x18\x02 \x01(\tR\x06status\x12\x12\n" + - "\x04page\x18\x03 \x01(\x05R\x04page\x12\x1b\n" + - "\tpage_size\x18\x04 \x01(\x05R\bpageSize\"\xce\x01\n" + - "\x17GetActivityListResponse\x120\n" + - "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12:\n" + - "\n" + - "activities\x18\x02 \x03(\v2\x1a.topfans.activity.ActivityR\n" + - "activities\x12\x12\n" + - "\x04page\x18\x03 \x01(\x05R\x04page\x12\x1b\n" + - "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x14\n" + - "\x05total\x18\x05 \x01(\x05R\x05total\"5\n" + - "\x12GetProgressRequest\x12\x1f\n" + - "\vactivity_id\x18\x01 \x01(\x03R\n" + - "activityId\"\x94\x02\n" + - "\x13GetProgressResponse\x120\n" + - "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1f\n" + - "\vactivity_id\x18\x02 \x01(\x03R\n" + - "activityId\x12)\n" + - "\x10current_progress\x18\x03 \x01(\x03R\x0fcurrentProgress\x12'\n" + - "\x0ftarget_progress\x18\x04 \x01(\x03R\x0etargetProgress\x12#\n" + - "\rcurrent_stage\x18\x05 \x01(\tR\fcurrentStage\x12\x19\n" + - "\bend_time\x18\x06 \x01(\x03R\aendTime\x12\x16\n" + - "\x06status\x18\a \x01(\tR\x06status\"\x84\x02\n" + - "\x0fMintingActivity\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x03R\x02id\x12\x14\n" + - "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + - "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1f\n" + - "\vcover_image\x18\x04 \x01(\tR\n" + - "coverImage\x12\x17\n" + - "\astar_id\x18\x05 \x01(\x03R\x06starId\x12\x14\n" + - "\x05route\x18\x06 \x01(\tR\x05route\x12\x1b\n" + - "\tis_active\x18\a \x01(\bR\bisActive\x12\x1d\n" + - "\n" + - "created_at\x18\b \x01(\x03R\tcreatedAt\x12\x1d\n" + - "\n" + - "updated_at\x18\t \x01(\x03R\tupdatedAt\"g\n" + - "\x1bGetMintingActivitiesRequest\x12\x17\n" + - "\astar_id\x18\x01 \x01(\x03R\x06starId\x12\x12\n" + - "\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" + - "\tpage_size\x18\x03 \x01(\x05R\bpageSize\"\xda\x01\n" + - "\x1cGetMintingActivitiesResponse\x120\n" + - "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12A\n" + - "\n" + - "activities\x18\x02 \x03(\v2!.topfans.activity.MintingActivityR\n" + - "activities\x12\x12\n" + - "\x04page\x18\x03 \x01(\x05R\x04page\x12\x1b\n" + - "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x14\n" + - "\x05total\x18\x05 \x01(\x05R\x05total2\x91\b\n" + - "\x0fActivityService\x12\x82\x01\n" + - "\x0fGetActivityList\x12(.topfans.activity.GetActivityListRequest\x1a).topfans.activity.GetActivityListResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/activities\x12y\n" + - "\vGetActivity\x12$.topfans.activity.GetProgressRequest\x1a\x1a.topfans.activity.Activity\"(\x82\xd3\xe4\x93\x02\"\x12 /api/v1/activities/{activity_id}\x12\x91\x01\n" + - "\x10GetActivityItems\x12$.topfans.activity.GetProgressRequest\x1a'.topfans.activity.ActivityItemsResponse\".\x82\xd3\xe4\x93\x02(\x12&/api/v1/activities/{activity_id}/items\x12\x8d\x01\n" + - "\vGetProgress\x12$.topfans.activity.GetProgressRequest\x1a%.topfans.activity.GetProgressResponse\"1\x82\xd3\xe4\x93\x02+\x12)/api/v1/activities/{activity_id}/progress\x12\x93\x01\n" + - "\fPurchaseItem\x12%.topfans.activity.PurchaseItemRequest\x1a&.topfans.activity.PurchaseItemResponse\"4\x82\xd3\xe4\x93\x02.:\x01*\")/api/v1/activities/{activity_id}/purchase\x12\xa7\x01\n" + - "\x16GetContributionRanking\x12,.topfans.activity.ContributionRankingRequest\x1a-.topfans.activity.ContributionRankingResponse\"0\x82\xd3\xe4\x93\x02*\x12(/api/v1/activities/{activity_id}/ranking\x12\x99\x01\n" + - "\x14GetMintingActivities\x12-.topfans.activity.GetMintingActivitiesRequest\x1a..topfans.activity.GetMintingActivitiesResponse\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/api/v1/minting-activitiesB8Z6github.com/topfans/backend/pkg/proto/activity;activityb\x06proto3" - -var ( - file_activity_proto_rawDescOnce sync.Once - file_activity_proto_rawDescData []byte -) - -func file_activity_proto_rawDescGZIP() []byte { - file_activity_proto_rawDescOnce.Do(func() { - file_activity_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_activity_proto_rawDesc), len(file_activity_proto_rawDesc))) - }) - return file_activity_proto_rawDescData -} - -var file_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 16) -var file_activity_proto_goTypes = []any{ - (*Activity)(nil), // 0: topfans.activity.Activity - (*ActivityItem)(nil), // 1: topfans.activity.ActivityItem - (*ActivityItemsResponse)(nil), // 2: topfans.activity.ActivityItemsResponse - (*PurchaseItemRequest)(nil), // 3: topfans.activity.PurchaseItemRequest - (*PurchaseItemResponse)(nil), // 4: topfans.activity.PurchaseItemResponse - (*ContributionRankingRequest)(nil), // 5: topfans.activity.ContributionRankingRequest - (*ContributionRankingItem)(nil), // 6: topfans.activity.ContributionRankingItem - (*ContributionRankingResponse)(nil), // 7: topfans.activity.ContributionRankingResponse - (*MyContribution)(nil), // 8: topfans.activity.MyContribution - (*GetActivityListRequest)(nil), // 9: topfans.activity.GetActivityListRequest - (*GetActivityListResponse)(nil), // 10: topfans.activity.GetActivityListResponse - (*GetProgressRequest)(nil), // 11: topfans.activity.GetProgressRequest - (*GetProgressResponse)(nil), // 12: topfans.activity.GetProgressResponse - (*MintingActivity)(nil), // 13: topfans.activity.MintingActivity - (*GetMintingActivitiesRequest)(nil), // 14: topfans.activity.GetMintingActivitiesRequest - (*GetMintingActivitiesResponse)(nil), // 15: topfans.activity.GetMintingActivitiesResponse - (*common.BaseResponse)(nil), // 16: topfans.common.BaseResponse -} -var file_activity_proto_depIdxs = []int32{ - 1, // 0: topfans.activity.Activity.items:type_name -> topfans.activity.ActivityItem - 1, // 1: topfans.activity.ActivityItemsResponse.items:type_name -> topfans.activity.ActivityItem - 16, // 2: topfans.activity.PurchaseItemResponse.base:type_name -> topfans.common.BaseResponse - 16, // 3: topfans.activity.ContributionRankingResponse.base:type_name -> topfans.common.BaseResponse - 6, // 4: topfans.activity.ContributionRankingResponse.items:type_name -> topfans.activity.ContributionRankingItem - 8, // 5: topfans.activity.ContributionRankingResponse.my_contribution:type_name -> topfans.activity.MyContribution - 16, // 6: topfans.activity.GetActivityListResponse.base:type_name -> topfans.common.BaseResponse - 0, // 7: topfans.activity.GetActivityListResponse.activities:type_name -> topfans.activity.Activity - 16, // 8: topfans.activity.GetProgressResponse.base:type_name -> topfans.common.BaseResponse - 16, // 9: topfans.activity.GetMintingActivitiesResponse.base:type_name -> topfans.common.BaseResponse - 13, // 10: topfans.activity.GetMintingActivitiesResponse.activities:type_name -> topfans.activity.MintingActivity - 9, // 11: topfans.activity.ActivityService.GetActivityList:input_type -> topfans.activity.GetActivityListRequest - 11, // 12: topfans.activity.ActivityService.GetActivity:input_type -> topfans.activity.GetProgressRequest - 11, // 13: topfans.activity.ActivityService.GetActivityItems:input_type -> topfans.activity.GetProgressRequest - 11, // 14: topfans.activity.ActivityService.GetProgress:input_type -> topfans.activity.GetProgressRequest - 3, // 15: topfans.activity.ActivityService.PurchaseItem:input_type -> topfans.activity.PurchaseItemRequest - 5, // 16: topfans.activity.ActivityService.GetContributionRanking:input_type -> topfans.activity.ContributionRankingRequest - 14, // 17: topfans.activity.ActivityService.GetMintingActivities:input_type -> topfans.activity.GetMintingActivitiesRequest - 10, // 18: topfans.activity.ActivityService.GetActivityList:output_type -> topfans.activity.GetActivityListResponse - 0, // 19: topfans.activity.ActivityService.GetActivity:output_type -> topfans.activity.Activity - 2, // 20: topfans.activity.ActivityService.GetActivityItems:output_type -> topfans.activity.ActivityItemsResponse - 12, // 21: topfans.activity.ActivityService.GetProgress:output_type -> topfans.activity.GetProgressResponse - 4, // 22: topfans.activity.ActivityService.PurchaseItem:output_type -> topfans.activity.PurchaseItemResponse - 7, // 23: topfans.activity.ActivityService.GetContributionRanking:output_type -> topfans.activity.ContributionRankingResponse - 15, // 24: topfans.activity.ActivityService.GetMintingActivities:output_type -> topfans.activity.GetMintingActivitiesResponse - 18, // [18:25] is the sub-list for method output_type - 11, // [11:18] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name -} - -func init() { file_activity_proto_init() } -func file_activity_proto_init() { - if File_activity_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_activity_proto_rawDesc), len(file_activity_proto_rawDesc)), - NumEnums: 0, - NumMessages: 16, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_activity_proto_goTypes, - DependencyIndexes: file_activity_proto_depIdxs, - MessageInfos: file_activity_proto_msgTypes, - }.Build() - File_activity_proto = out.File - file_activity_proto_goTypes = nil - file_activity_proto_depIdxs = nil -} diff --git a/backend/gateway/controller/task_controller.go b/backend/gateway/controller/task_controller.go index 17d4d9b..4333277 100644 --- a/backend/gateway/controller/task_controller.go +++ b/backend/gateway/controller/task_controller.go @@ -211,7 +211,6 @@ func (ctrl *TaskController) ClaimDailyTask(c *gin.Context) { response.Success(c, map[string]interface{}{ "success": resp.Success, "crystal_balance": resp.CrystalBalance, - "experience": resp.Experience, }) } @@ -270,7 +269,6 @@ func (ctrl *TaskController) ClaimAllDailyTasks(c *gin.Context) { response.Success(c, map[string]interface{}{ "claimed_count": resp.ClaimedCount, "crystal_balance": resp.CrystalBalance, - "experience": resp.Experience, "claimed_task_keys": resp.ClaimedTaskKeys, }) } @@ -481,7 +479,6 @@ func (ctrl *TaskController) ClaimOnboardingReward(c *gin.Context) { response.Success(c, map[string]interface{}{ "success": resp.Success, "crystal_balance": resp.CrystalBalance, - "experience": resp.Experience, }) } @@ -677,7 +674,6 @@ func convertDailyTasksResponse(resp *pbTask.GetDailyTasksResponse) map[string]in "name": task.Name, "description": task.Description, "crystal_reward": task.CrystalReward, - "exp_reward": task.ExpReward, "status": task.Status, "can_claim": task.CanClaim, }) @@ -696,7 +692,6 @@ func convertOnboardingStatusResponse(resp *pbTask.GetOnboardingStatusResponse) m "name": stage.Name, "required_task_keys": stage.RequiredTaskKeys, "crystal_reward": stage.CrystalReward, - "exp_reward": stage.ExpReward, "status": stage.Status, "is_current": stage.IsCurrent, }) @@ -717,7 +712,6 @@ func convertAdvanceStageResponse(resp *pbTask.AdvanceStageResponse) map[string]i "name": stage.Name, "required_task_keys": stage.RequiredTaskKeys, "crystal_reward": stage.CrystalReward, - "exp_reward": stage.ExpReward, "status": stage.Status, "is_current": stage.IsCurrent, }) @@ -737,7 +731,6 @@ func convertCompleteGuideResponse(resp *pbTask.CompleteGuideResponse) map[string "name": stage.Name, "required_task_keys": stage.RequiredTaskKeys, "crystal_reward": stage.CrystalReward, - "exp_reward": stage.ExpReward, "status": stage.Status, "is_current": stage.IsCurrent, }) diff --git a/backend/gateway/dto/response_dto.go b/backend/gateway/dto/response_dto.go index d7c14e8..a0126ec 100644 --- a/backend/gateway/dto/response_dto.go +++ b/backend/gateway/dto/response_dto.go @@ -17,7 +17,6 @@ type CurrentIdentityDTO struct { IdentityName string `json:"identity_name"` // "王一博" Tag string `json:"tag"` // "小摩托" Level int32 `json:"level"` - Experience int64 `json:"experience"` CrystalBalance int64 `json:"crystal_balance"` } @@ -109,7 +108,6 @@ type MyFanIdentityItemDTO struct { StarTag string `json:"star_tag"` // 明星标签 Nickname string `json:"nickname"` // 用户昵称 Level int32 `json:"level"` // 等级 - Experience int64 `json:"experience"` // 经验值 CrystalBalance int64 `json:"crystal_balance"` // 水晶余额 IsActive bool `json:"is_active"` // 是否激活(当前使用) } diff --git a/backend/gateway/dto/user_converter.go b/backend/gateway/dto/user_converter.go index 2f8f2a6..16c39c0 100644 --- a/backend/gateway/dto/user_converter.go +++ b/backend/gateway/dto/user_converter.go @@ -24,7 +24,6 @@ func ToCurrentIdentityDTO(profile *pb.FanProfile, star *pb.Star) CurrentIdentity IdentityName: star.Name, // "王一博" Tag: star.Tag, // "小摩托" Level: profile.Level, - Experience: profile.Experience, CrystalBalance: profile.CrystalBalance, } } @@ -199,7 +198,6 @@ func ToMyFanIdentitiesResponseDTO(items []*pb.MyFanIdentityItem, currentStarID i StarTag: star.Tag, Nickname: profile.Nickname, Level: profile.Level, - Experience: profile.Experience, CrystalBalance: profile.CrystalBalance, IsActive: profile.StarId == currentStarID, }) diff --git a/backend/pkg/models/level.go b/backend/pkg/models/level.go new file mode 100644 index 0000000..4347887 --- /dev/null +++ b/backend/pkg/models/level.go @@ -0,0 +1,132 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// LevelThreshold 等级阈值配置表 +type LevelThreshold struct { + Level int32 `gorm:"primaryKey;column:level"` // 等级 1-20 + MaxExhibitionHours int64 `gorm:"not null;column:max_exhibition_hours"` // 升级到该等级需要的累计上架时长(小时) + LikeBetCount int32 `gorm:"not null;column:like_bet_count"` // 升级后解锁的点赞押注次数 + Description string `gorm:"type:varchar(100);column:description"` // 描述 +} + +// TableName 指定表名 +func (LevelThreshold) TableName() string { + return "level_thresholds" +} + +// LevelUpgradeCondition 等级升级条件配置表(21级+) +type LevelUpgradeCondition struct { + Level int32 `gorm:"primaryKey;column:level"` // 主等级(如21级) + RequireTotalHours int64 `gorm:"not null;column:require_total_hours"` // 需要的总上架时长 + RequireDaziLevel int32 `gorm:"default:0;column:require_dazi_level"` // 需要的搭子等级(0=不需要) + Description string `gorm:"type:varchar(100);column:description"` // 描述 + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (LevelUpgradeCondition) TableName() string { + return "level_upgrade_conditions" +} + +// BeforeUpdate 更新前钩子 +func (l *LevelUpgradeCondition) BeforeUpdate(tx *gorm.DB) error { + l.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// LevelCapConfig 等级上限配置表 +type LevelCapConfig struct { + ID int64 `gorm:"primaryKey;column:id"` + MaxLevel int32 `gorm:"not null;column:max_level"` // 当前等级上限 + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (LevelCapConfig) TableName() string { + return "level_cap_config" +} + +// BeforeUpdate 更新前钩子 +func (l *LevelCapConfig) BeforeUpdate(tx *gorm.DB) error { + l.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// UserExhibitionHours 用户累计上架时长表 +type UserExhibitionHours struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + UserID int64 `gorm:"uniqueIndex:uk_exhibition_user_star;not null;column:user_id"` + StarID int64 `gorm:"uniqueIndex:uk_exhibition_user_star;not null;column:star_id"` + TotalExhibitionHours int64 `gorm:"default:0;not null;column:total_exhibition_hours"` // 累计上架时长(小时) + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (UserExhibitionHours) TableName() string { + return "user_exhibition_hours" +} + +// BeforeCreate 创建前钩子 +func (u *UserExhibitionHours) BeforeCreate(tx *gorm.DB) error { + u.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// BeforeUpdate 更新前钩子 +func (u *UserExhibitionHours) BeforeUpdate(tx *gorm.DB) error { + u.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// LevelUpRewardConfig 升级奖励配置表 +type LevelUpRewardConfig struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + Level int32 `gorm:"uniqueIndex:uk_level_reward_type;not null;column:level"` // 等级 + RewardType string `gorm:"uniqueIndex:uk_level_reward_type;type:varchar(50);not null;column:reward_type"` // 奖励类型:crystal/like_bet_count + RewardValue int64 `gorm:"default:0;column:reward_value"` // 奖励值 + IsEnabled bool `gorm:"default:true;column:is_enabled"` // 功能开关 + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (LevelUpRewardConfig) TableName() string { + return "level_up_reward_config" +} + +// BeforeUpdate 更新前钩子 +func (l *LevelUpRewardConfig) BeforeUpdate(tx *gorm.DB) error { + l.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// DaziLevelThreshold 搭子等级阈值配置表(预留) +type DaziLevelThreshold struct { + Level int32 `gorm:"primaryKey;column:level"` // 搭子等级 1-N + UpgradeCondition string `gorm:"type:varchar(100);column:upgrade_condition"` // 升级条件描述 + ConditionParam int `gorm:"default:0;column:condition_param"` // 条件参数 + Description string `gorm:"type:varchar(100);column:description"` +} + +// TableName 指定表名 +func (DaziLevelThreshold) TableName() string { + return "dazi_level_thresholds" +} + +// UserDaziLevel 用户搭子等级表(预留) +type UserDaziLevel struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + UserID int64 `gorm:"uniqueIndex:uk_dazi_user_star;not null;column:user_id"` + StarID int64 `gorm:"uniqueIndex:uk_dazi_user_star;not null;column:star_id"` + DaziLevel int32 `gorm:"default:1;not null;column:dazi_level"` // 搭子等级 + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (UserDaziLevel) TableName() string { + return "user_dazi_level" +} \ No newline at end of file diff --git a/backend/pkg/models/mint.go b/backend/pkg/models/mint.go new file mode 100644 index 0000000..f0f2eac --- /dev/null +++ b/backend/pkg/models/mint.go @@ -0,0 +1,85 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// MintCostConfig 铸造消耗配置表 +type MintCostConfig struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + MintCount int32 `gorm:"uniqueIndex;not null;column:mint_count"` // 铸爱次数 1-10 + CostCrystal int64 `gorm:"not null;column:cost_crystal"` // 消耗水晶数 + Probability int64 `gorm:"default:0;column:probability"` // 保底触发概率 0-100 + RewardType *string `gorm:"type:varchar(50);column:reward_type"` // 奖励类型 + RewardValue int64 `gorm:"default:0;column:reward_value"` // 奖励值(bps) + Description *string `gorm:"type:varchar(255);column:description"` // 描述 + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (MintCostConfig) TableName() string { + return "mint_cost_config" +} + +// BeforeUpdate 更新前钩子 +func (m *MintCostConfig) BeforeUpdate(tx *gorm.DB) error { + m.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// UserMintCount 用户铸爱累计表 +type UserMintCount struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + UserID int64 `gorm:"uniqueIndex:uk_user_mint_star;not null;column:user_id"` + StarID int64 `gorm:"uniqueIndex:uk_user_mint_star;not null;column:star_id"` + MintCount int32 `gorm:"default:0;not null;column:mint_count"` // 累计铸造次数 + RevenueBoostBps int32 `gorm:"default:0;not null;column:revenue_boost_bps"` // 永久收益提升(基点),500=+5% + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (UserMintCount) TableName() string { + return "user_mint_count" +} + +// BeforeCreate 创建前钩子 +func (u *UserMintCount) BeforeCreate(tx *gorm.DB) error { + u.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// BeforeUpdate 更新前钩子 +func (u *UserMintCount) BeforeUpdate(tx *gorm.DB) error { + u.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// MintRewardConfig 铸造奖励配置表(预留) +type MintRewardConfig struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + StarID int64 `gorm:"uniqueIndex;not null;column:star_id"` // 偶像ID,0=全服默认 + BaseReward int64 `gorm:"default:0;column:base_reward"` // 每次铸造基础返还水晶数 + IsEnabled bool `gorm:"default:true;column:is_enabled"` // 功能开关 + UpdatedAt int64 `gorm:"not null;column:updated_at"` +} + +// TableName 指定表名 +func (MintRewardConfig) TableName() string { + return "mint_reward_config" +} + +// MintMilestoneConfig 铸造阶梯奖励配置表(预留) +type MintMilestoneConfig struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + StarID int64 `gorm:"uniqueIndex:uk_milestone_star_count;not null;column:star_id"` + MilestoneCount int32 `gorm:"uniqueIndex:uk_milestone_star_count;not null;column:milestone_count"` // 累计次数阈值 + BonusReward int64 `gorm:"not null;column:bonus_reward"` // 达到该阶梯时额外奖励水晶 + CreatedAt int64 `gorm:"not null;column:created_at"` +} + +// TableName 指定表名 +func (MintMilestoneConfig) TableName() string { + return "mint_milestone_config" +} \ No newline at end of file diff --git a/backend/pkg/models/user.go b/backend/pkg/models/user.go index 672fd8d..51643fe 100644 --- a/backend/pkg/models/user.go +++ b/backend/pkg/models/user.go @@ -55,17 +55,17 @@ type FanProfile struct { Level int32 `gorm:"default:1;not null;column:level"` Times int32 `gorm:"default:1;not null;column:times"` // 剩余铸造次数 Social int32 `gorm:"default:0;not null;column:social"` // 好友个数 - Experience int64 `gorm:"default:0;not null;column:experience"` CoinBalance int64 `gorm:"default:0;not null;column:coin_balance"` CrystalBalance int64 `gorm:"default:0;not null;column:crystal_balance"` Tags StringArray `gorm:"type:jsonb;column:tags"` AvatarURL *string `gorm:"type:varchar(500);column:avatar_url"` // 新增字段 - StarbookLimit int32 `gorm:"default:3;not null;column:starbook_limit"` - SlotLimit int32 `gorm:"default:3;not null;column:slot_limit"` - AssetsCount int32 `gorm:"default:0;not null;column:assets_count"` - ChainAddress *string `gorm:"type:varchar(100);column:chain_address"` + StarbookLimit int32 `gorm:"default:3;not null;column:starbook_limit"` + SlotLimit int32 `gorm:"default:3;not null;column:slot_limit"` + AssetsCount int32 `gorm:"default:0;not null;column:assets_count"` + LikeBetCount int32 `gorm:"default:0;not null;column:like_bet_count"` // 点赞押注次数 + ChainAddress *string `gorm:"type:varchar(100);column:chain_address"` IsActive bool `gorm:"default:true;not null;column:is_active"` CreatedAt int64 `gorm:"not null;column:created_at"` @@ -170,3 +170,41 @@ func (sa *StringArray) Scan(value interface{}) error { return json.Unmarshal(bytes, sa) } + +// CrystalTransactionRecord 水晶交易流水表 +type CrystalTransactionRecord struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + UserID int64 `gorm:"not null;index:ix_crystal_tx_user_star;column:user_id"` + StarID int64 `gorm:"not null;index:ix_crystal_tx_user_star;column:star_id"` + ChangeType string `gorm:"type:varchar(30);not null;index:ix_crystal_tx_change_type;column:change_type"` // task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust + Delta int64 `gorm:"not null;column:delta"` // 正数=收入,负数=消耗 + BalanceBefore int64 `gorm:"not null;column:balance_before"` // 变化前余额快照 + BalanceAfter int64 `gorm:"not null;column:balance_after"` // 变化后余额快照 + SourceID string `gorm:"type:varchar(100);column:source_id"` // 关联业务ID + Description string `gorm:"type:varchar(255);column:description"` // 可读描述 + CreatedAt int64 `gorm:"not null;index:ix_crystal_tx_created;column:created_at"` +} + +// TableName 指定表名 +func (CrystalTransactionRecord) TableName() string { + return "crystal_transaction_records" +} + +// CoinTransactionRecord 游戏币交易流水表(预留) +type CoinTransactionRecord struct { + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + UserID int64 `gorm:"not null;index:ix_coin_tx_user_star;column:user_id"` + StarID int64 `gorm:"not null;index:ix_coin_tx_user_star;column:star_id"` + ChangeType string `gorm:"type:varchar(30);not null;column:change_type"` + Delta int64 `gorm:"not null;column:delta"` + BalanceBefore int64 `gorm:"not null;column:balance_before"` + BalanceAfter int64 `gorm:"not null;column:balance_after"` + SourceID string `gorm:"type:varchar(100);column:source_id"` + Description string `gorm:"type:varchar(255);column:description"` + CreatedAt int64 `gorm:"not null;index:ix_coin_tx_created;column:created_at"` +} + +// TableName 指定表名 +func (CoinTransactionRecord) TableName() string { + return "coin_transaction_records" +} diff --git a/backend/pkg/proto/asset/asset.triple.go b/backend/pkg/proto/asset/asset.triple.go index 8de20d8..0770254 100644 --- a/backend/pkg/proto/asset/asset.triple.go +++ b/backend/pkg/proto/asset/asset.triple.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-triple. DO NOT EDIT. // -// Source: proto/asset.proto +// Source: asset.proto package asset import ( diff --git a/backend/pkg/proto/common/common.triple.go b/backend/pkg/proto/common/common.triple.go deleted file mode 100644 index 1adc355..0000000 --- a/backend/pkg/proto/common/common.triple.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by protoc-gen-triple. DO NOT EDIT. -// -// Source: proto/common.proto -package common - -import ( - "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol" -) - -// This is a compile-time assertion to ensure that this generated file and the Triple package -// are compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of Triple newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of Triple or updating the Triple -// version compiled into your binary. -const _ = triple_protocol.IsAtLeastVersion0_1_0 - -var () diff --git a/backend/pkg/proto/gallery/gallery.triple.go b/backend/pkg/proto/gallery/gallery.triple.go index 0f0358f..da9dbd5 100644 --- a/backend/pkg/proto/gallery/gallery.triple.go +++ b/backend/pkg/proto/gallery/gallery.triple.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-triple. DO NOT EDIT. // -// Source: proto/gallery.proto +// Source: gallery.proto package gallery import ( diff --git a/backend/pkg/proto/ranking/ranking.triple.go b/backend/pkg/proto/ranking/ranking.triple.go index c5037e6..7f6ed0e 100644 --- a/backend/pkg/proto/ranking/ranking.triple.go +++ b/backend/pkg/proto/ranking/ranking.triple.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-triple. DO NOT EDIT. // -// Source: proto/ranking.proto +// Source: ranking.proto package ranking import ( diff --git a/backend/pkg/proto/social/social.triple.go b/backend/pkg/proto/social/social.triple.go index d133947..a65d2b7 100644 --- a/backend/pkg/proto/social/social.triple.go +++ b/backend/pkg/proto/social/social.triple.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-triple. DO NOT EDIT. // -// Source: proto/social.proto +// Source: social.proto package social import ( @@ -66,6 +66,12 @@ const ( SocialServiceCheckAssetLikeProcedure = "/topfans.social.SocialService/CheckAssetLike" // 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. + SocialServiceGetMyTodayLikedAssetsProcedure = "/topfans.social.SocialService/GetMyTodayLikedAssets" + // SocialServiceGetMyWeekLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyWeekLikedAssets RPC. + SocialServiceGetMyWeekLikedAssetsProcedure = "/topfans.social.SocialService/GetMyWeekLikedAssets" + // SocialServiceGetUserLikedAssetsProcedure is the fully-qualified name of the SocialService's GetUserLikedAssets RPC. + SocialServiceGetUserLikedAssetsProcedure = "/topfans.social.SocialService/GetUserLikedAssets" ) var ( diff --git a/backend/pkg/proto/task/task.pb.go b/backend/pkg/proto/task/task.pb.go index 77ff955..a95424b 100644 --- a/backend/pkg/proto/task/task.pb.go +++ b/backend/pkg/proto/task/task.pb.go @@ -30,9 +30,8 @@ type DailyTaskItem struct { Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` CrystalReward int64 `protobuf:"varint,5,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"` - ExpReward int64 `protobuf:"varint,6,opt,name=exp_reward,json=expReward,proto3" json:"exp_reward,omitempty"` - Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/claimed - CanClaim bool `protobuf:"varint,8,opt,name=can_claim,json=canClaim,proto3" json:"can_claim,omitempty"` + Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/claimed + CanClaim bool `protobuf:"varint,7,opt,name=can_claim,json=canClaim,proto3" json:"can_claim,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -102,13 +101,6 @@ func (x *DailyTaskItem) GetCrystalReward() int64 { return 0 } -func (x *DailyTaskItem) GetExpReward() int64 { - if x != nil { - return x.ExpReward - } - return 0 -} - func (x *DailyTaskItem) GetStatus() string { if x != nil { return x.Status @@ -412,7 +404,6 @@ type ClaimDailyTaskResponse struct { Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` CrystalBalance int64 `protobuf:"varint,3,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` - Experience int64 `protobuf:"varint,4,opt,name=experience,proto3" json:"experience,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -468,13 +459,6 @@ func (x *ClaimDailyTaskResponse) GetCrystalBalance() int64 { return 0 } -func (x *ClaimDailyTaskResponse) GetExperience() int64 { - if x != nil { - return x.Experience - } - return 0 -} - type ClaimAllDailyTasksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` StarId int64 `protobuf:"varint,1,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` @@ -524,8 +508,7 @@ type ClaimAllDailyTasksResponse struct { Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` ClaimedCount int32 `protobuf:"varint,2,opt,name=claimed_count,json=claimedCount,proto3" json:"claimed_count,omitempty"` CrystalBalance int64 `protobuf:"varint,3,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` - Experience int64 `protobuf:"varint,4,opt,name=experience,proto3" json:"experience,omitempty"` - ClaimedTaskKeys []string `protobuf:"bytes,5,rep,name=claimed_task_keys,json=claimedTaskKeys,proto3" json:"claimed_task_keys,omitempty"` + ClaimedTaskKeys []string `protobuf:"bytes,4,rep,name=claimed_task_keys,json=claimedTaskKeys,proto3" json:"claimed_task_keys,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -581,13 +564,6 @@ func (x *ClaimAllDailyTasksResponse) GetCrystalBalance() int64 { return 0 } -func (x *ClaimAllDailyTasksResponse) GetExperience() int64 { - if x != nil { - return x.Experience - } - return 0 -} - func (x *ClaimAllDailyTasksResponse) GetClaimedTaskKeys() []string { if x != nil { return x.ClaimedTaskKeys @@ -601,11 +577,10 @@ type OnboardingStage struct { Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` RequiredTaskKeys []string `protobuf:"bytes,3,rep,name=required_task_keys,json=requiredTaskKeys,proto3" json:"required_task_keys,omitempty"` CrystalReward int64 `protobuf:"varint,4,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"` - ExpReward int64 `protobuf:"varint,5,opt,name=exp_reward,json=expReward,proto3" json:"exp_reward,omitempty"` - Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/in_progress - IsCurrent bool `protobuf:"varint,7,opt,name=is_current,json=isCurrent,proto3" json:"is_current,omitempty"` - AllTasksCompleted bool `protobuf:"varint,8,opt,name=all_tasks_completed,json=allTasksCompleted,proto3" json:"all_tasks_completed,omitempty"` // 该阶段所有任务是否完成 - IsRewardClaimed bool `protobuf:"varint,9,opt,name=is_reward_claimed,json=isRewardClaimed,proto3" json:"is_reward_claimed,omitempty"` // 该阶段奖励是否已领取 + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/in_progress + IsCurrent bool `protobuf:"varint,6,opt,name=is_current,json=isCurrent,proto3" json:"is_current,omitempty"` + AllTasksCompleted bool `protobuf:"varint,7,opt,name=all_tasks_completed,json=allTasksCompleted,proto3" json:"all_tasks_completed,omitempty"` // 该阶段所有任务是否完成 + IsRewardClaimed bool `protobuf:"varint,8,opt,name=is_reward_claimed,json=isRewardClaimed,proto3" json:"is_reward_claimed,omitempty"` // 该阶段奖励是否已领取 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -668,13 +643,6 @@ func (x *OnboardingStage) GetCrystalReward() int64 { return 0 } -func (x *OnboardingStage) GetExpReward() int64 { - if x != nil { - return x.ExpReward - } - return 0 -} - func (x *OnboardingStage) GetStatus() string { if x != nil { return x.Status @@ -1104,7 +1072,6 @@ type ClaimOnboardingRewardResponse struct { Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` CrystalBalance string `protobuf:"bytes,3,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` // 使用 string 避免 Dubbo int64 序列化 bug - Experience string `protobuf:"bytes,4,opt,name=experience,proto3" json:"experience,omitempty"` // 使用 string 避免 Dubbo int64 序列化 bug unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1160,13 +1127,6 @@ func (x *ClaimOnboardingRewardResponse) GetCrystalBalance() string { return "" } -func (x *ClaimOnboardingRewardResponse) GetExperience() string { - if x != nil { - return x.Experience - } - return "" -} - type ExhibitionRevenueItem struct { state protoimpl.MessageState `protogen:"open.v1"` Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` @@ -1904,17 +1864,15 @@ var File_task_proto protoreflect.FileDescriptor const file_task_proto_rawDesc = "" + "\n" + "\n" + - "task.proto\x12\ftopfans.task\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xf4\x01\n" + + "task.proto\x12\ftopfans.task\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xd5\x01\n" + "\rDailyTaskItem\x12\x19\n" + "\btask_key\x18\x01 \x01(\tR\ataskKey\x12\x17\n" + "\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x12\n" + "\x04name\x18\x03 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x04 \x01(\tR\vdescription\x12%\n" + - "\x0ecrystal_reward\x18\x05 \x01(\x03R\rcrystalReward\x12\x1d\n" + - "\n" + - "exp_reward\x18\x06 \x01(\x03R\texpReward\x12\x16\n" + - "\x06status\x18\a \x01(\tR\x06status\x12\x1b\n" + - "\tcan_claim\x18\b \x01(\bR\bcanClaim\"/\n" + + "\x0ecrystal_reward\x18\x05 \x01(\x03R\rcrystalReward\x12\x16\n" + + "\x06status\x18\x06 \x01(\tR\x06status\x12\x1b\n" + + "\tcan_claim\x18\a \x01(\bR\bcanClaim\"/\n" + "\x14GetDailyTasksRequest\x12\x17\n" + "\astar_id\x18\x01 \x01(\x03R\x06starId\"\x95\x01\n" + "\x15GetDailyTasksResponse\x120\n" + @@ -1933,36 +1891,28 @@ const file_task_proto_rawDesc = "" + "\amessage\x18\x05 \x01(\tR\amessage\"K\n" + "\x15ClaimDailyTaskRequest\x12\x19\n" + "\btask_key\x18\x01 \x01(\tR\ataskKey\x12\x17\n" + - "\astar_id\x18\x02 \x01(\x03R\x06starId\"\xad\x01\n" + + "\astar_id\x18\x02 \x01(\x03R\x06starId\"\x8d\x01\n" + "\x16ClaimDailyTaskResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" + "\asuccess\x18\x02 \x01(\bR\asuccess\x12'\n" + - "\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\x12\x1e\n" + - "\n" + - "experience\x18\x04 \x01(\x03R\n" + - "experience\"4\n" + + "\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\"4\n" + "\x19ClaimAllDailyTasksRequest\x12\x17\n" + - "\astar_id\x18\x01 \x01(\x03R\x06starId\"\xe8\x01\n" + + "\astar_id\x18\x01 \x01(\x03R\x06starId\"\xc8\x01\n" + "\x1aClaimAllDailyTasksResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12#\n" + "\rclaimed_count\x18\x02 \x01(\x05R\fclaimedCount\x12'\n" + - "\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\x12\x1e\n" + - "\n" + - "experience\x18\x04 \x01(\x03R\n" + - "experience\x12*\n" + - "\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\xc2\x02\n" + + "\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\x12*\n" + + "\x11claimed_task_keys\x18\x04 \x03(\tR\x0fclaimedTaskKeys\"\xa3\x02\n" + "\x0fOnboardingStage\x12\x14\n" + "\x05stage\x18\x01 \x01(\x05R\x05stage\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12,\n" + "\x12required_task_keys\x18\x03 \x03(\tR\x10requiredTaskKeys\x12%\n" + - "\x0ecrystal_reward\x18\x04 \x01(\x03R\rcrystalReward\x12\x1d\n" + + "\x0ecrystal_reward\x18\x04 \x01(\x03R\rcrystalReward\x12\x16\n" + + "\x06status\x18\x05 \x01(\tR\x06status\x12\x1d\n" + "\n" + - "exp_reward\x18\x05 \x01(\x03R\texpReward\x12\x16\n" + - "\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" + - "\n" + - "is_current\x18\a \x01(\bR\tisCurrent\x12.\n" + - "\x13all_tasks_completed\x18\b \x01(\bR\x11allTasksCompleted\x12*\n" + - "\x11is_reward_claimed\x18\t \x01(\bR\x0fisRewardClaimed\"h\n" + + "is_current\x18\x06 \x01(\bR\tisCurrent\x12.\n" + + "\x13all_tasks_completed\x18\a \x01(\bR\x11allTasksCompleted\x12*\n" + + "\x11is_reward_claimed\x18\b \x01(\bR\x0fisRewardClaimed\"h\n" + "\x14CompleteGuideRequest\x12\x19\n" + "\btask_key\x18\x01 \x01(\tR\ataskKey\x125\n" + "\x06stages\x18\x02 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"\xd6\x01\n" + @@ -1987,14 +1937,11 @@ const file_task_proto_rawDesc = "" + "\x06status\x18\x03 \x01(\tR\x06status\x125\n" + "\x06stages\x18\x04 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"4\n" + "\x1cClaimOnboardingRewardRequest\x12\x14\n" + - "\x05stage\x18\x01 \x01(\x05R\x05stage\"\xb4\x01\n" + + "\x05stage\x18\x01 \x01(\x05R\x05stage\"\x94\x01\n" + "\x1dClaimOnboardingRewardResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" + "\asuccess\x18\x02 \x01(\bR\asuccess\x12'\n" + - "\x0fcrystal_balance\x18\x03 \x01(\tR\x0ecrystalBalance\x12\x1e\n" + - "\n" + - "experience\x18\x04 \x01(\tR\n" + - "experience\"\xe2\x02\n" + + "\x0fcrystal_balance\x18\x03 \x01(\tR\x0ecrystalBalance\"\xe2\x02\n" + "\x15ExhibitionRevenueItem\x12\x0e\n" + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" + "\astar_id\x18\x02 \x01(\x03R\x06starId\x12#\n" + diff --git a/backend/pkg/proto/user/user.pb.go b/backend/pkg/proto/user/user.pb.go index ea6fabd..88b961d 100644 --- a/backend/pkg/proto/user/user.pb.go +++ b/backend/pkg/proto/user/user.pb.go @@ -113,21 +113,20 @@ type FanProfile struct { state protoimpl.MessageState `protogen:"open.v1"` Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - StarId int64 `protobuf:"varint,3,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 核心隔离键 - Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` // 星球专属昵称 - Level int32 `protobuf:"varint,5,opt,name=level,proto3" json:"level,omitempty"` // 等级 - Times int32 `protobuf:"varint,6,opt,name=times,proto3" json:"times,omitempty"` // 剩余铸造次数 - Social int32 `protobuf:"varint,7,opt,name=social,proto3" json:"social,omitempty"` // 好友个数 - Experience int64 `protobuf:"varint,8,opt,name=experience,proto3" json:"experience,omitempty"` // 经验值 - CoinBalance int64 `protobuf:"varint,9,opt,name=coin_balance,json=coinBalance,proto3" json:"coin_balance,omitempty"` // 游戏币余额 - CrystalBalance int64 `protobuf:"varint,10,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` // 顶粉水晶余额 - Tags []string `protobuf:"bytes,11,rep,name=tags,proto3" json:"tags,omitempty"` // 标签数组 - AvatarUrl string `protobuf:"bytes,17,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` // 头像URL(星球专属) - CreatedAt int64 `protobuf:"varint,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - StarbookLimit int32 `protobuf:"varint,13,opt,name=starbook_limit,json=starbookLimit,proto3" json:"starbook_limit,omitempty"` // 星书限制 - SlotLimit int32 `protobuf:"varint,14,opt,name=slot_limit,json=slotLimit,proto3" json:"slot_limit,omitempty"` // 槽位限制 - AssetsCount int32 `protobuf:"varint,15,opt,name=assets_count,json=assetsCount,proto3" json:"assets_count,omitempty"` // 资产数量 - ChainAddress string `protobuf:"bytes,16,opt,name=chain_address,json=chainAddress,proto3" json:"chain_address,omitempty"` // 链地址 + StarId int64 `protobuf:"varint,3,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 核心隔离键 + Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` // 星球专属昵称 + Level int32 `protobuf:"varint,5,opt,name=level,proto3" json:"level,omitempty"` // 等级 + Times int32 `protobuf:"varint,6,opt,name=times,proto3" json:"times,omitempty"` // 剩余铸造次数 + Social int32 `protobuf:"varint,7,opt,name=social,proto3" json:"social,omitempty"` // 好友个数 + CoinBalance int64 `protobuf:"varint,8,opt,name=coin_balance,json=coinBalance,proto3" json:"coin_balance,omitempty"` // 游戏币余额 + CrystalBalance int64 `protobuf:"varint,9,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` // 顶粉水晶余额 + Tags []string `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"` // 标签数组 + AvatarUrl string `protobuf:"bytes,17,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` // 头像URL(星球专属) + CreatedAt int64 `protobuf:"varint,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + StarbookLimit int32 `protobuf:"varint,12,opt,name=starbook_limit,json=starbookLimit,proto3" json:"starbook_limit,omitempty"` // 星书限制 + SlotLimit int32 `protobuf:"varint,13,opt,name=slot_limit,json=slotLimit,proto3" json:"slot_limit,omitempty"` // 槽位限制 + AssetsCount int32 `protobuf:"varint,14,opt,name=assets_count,json=assetsCount,proto3" json:"assets_count,omitempty"` // 资产数量 + ChainAddress string `protobuf:"bytes,15,opt,name=chain_address,json=chainAddress,proto3" json:"chain_address,omitempty"` // 链地址 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -211,13 +210,6 @@ func (x *FanProfile) GetSocial() int32 { return 0 } -func (x *FanProfile) GetExperience() int64 { - if x != nil { - return x.Experience - } - return 0 -} - func (x *FanProfile) GetCoinBalance() int64 { if x != nil { return x.CoinBalance @@ -1501,9 +1493,12 @@ func (x *UpdateFanProfileSocialResponse) GetNewSocial() int32 { // 更新水晶余额请求(内部RPC调用,用于资产服务扣除/退款水晶) type UpdateCrystalBalanceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID - StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID - Delta int64 `protobuf:"varint,3,opt,name=delta,proto3" json:"delta,omitempty"` // 变化量(正数增加,负数减少) + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID + StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID + Delta int64 `protobuf:"varint,3,opt,name=delta,proto3" json:"delta,omitempty"` // 变化量(正数增加,负数减少) + ChangeType string `protobuf:"bytes,4,opt,name=change_type,json=changeType,proto3" json:"change_type,omitempty"` // 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust + SourceId string `protobuf:"bytes,5,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` // 关联业务ID + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` // 可读描述 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1559,6 +1554,27 @@ func (x *UpdateCrystalBalanceRequest) GetDelta() int64 { return 0 } +func (x *UpdateCrystalBalanceRequest) GetChangeType() string { + if x != nil { + return x.ChangeType + } + return "" +} + +func (x *UpdateCrystalBalanceRequest) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +func (x *UpdateCrystalBalanceRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + // 更新水晶余额响应 type UpdateCrystalBalanceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1726,30 +1742,30 @@ func (x *UpdateAssetsCountResponse) GetNewCount() int32 { return 0 } -// 增加经验值请求(内部RPC调用,用于taskService增加经验) -type AddExperienceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID - StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID - Delta int64 `protobuf:"varint,3,opt,name=delta,proto3" json:"delta,omitempty"` // 变化量(正数增加,负数减少) - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// 增加累计上架时长请求(内部RPC调用,用于galleryService展品下架时累加时长) +type AddExhibitionHoursRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID + StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID + ExhibitionHours int64 `protobuf:"varint,3,opt,name=exhibition_hours,json=exhibitionHours,proto3" json:"exhibition_hours,omitempty"` // 本次展出的时长(小时) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *AddExperienceRequest) Reset() { - *x = AddExperienceRequest{} +func (x *AddExhibitionHoursRequest) Reset() { + *x = AddExhibitionHoursRequest{} mi := &file_user_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AddExperienceRequest) String() string { +func (x *AddExhibitionHoursRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AddExperienceRequest) ProtoMessage() {} +func (*AddExhibitionHoursRequest) ProtoMessage() {} -func (x *AddExperienceRequest) ProtoReflect() protoreflect.Message { +func (x *AddExhibitionHoursRequest) ProtoReflect() protoreflect.Message { mi := &file_user_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1761,55 +1777,57 @@ func (x *AddExperienceRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AddExperienceRequest.ProtoReflect.Descriptor instead. -func (*AddExperienceRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use AddExhibitionHoursRequest.ProtoReflect.Descriptor instead. +func (*AddExhibitionHoursRequest) Descriptor() ([]byte, []int) { return file_user_proto_rawDescGZIP(), []int{27} } -func (x *AddExperienceRequest) GetUserId() int64 { +func (x *AddExhibitionHoursRequest) GetUserId() int64 { if x != nil { return x.UserId } return 0 } -func (x *AddExperienceRequest) GetStarId() int64 { +func (x *AddExhibitionHoursRequest) GetStarId() int64 { if x != nil { return x.StarId } return 0 } -func (x *AddExperienceRequest) GetDelta() int64 { +func (x *AddExhibitionHoursRequest) GetExhibitionHours() int64 { if x != nil { - return x.Delta + return x.ExhibitionHours } return 0 } -// 增加经验值响应 -type AddExperienceResponse struct { +// 增加累计上架时长响应 +type AddExhibitionHoursResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - NewExperience int64 `protobuf:"varint,2,opt,name=new_experience,json=newExperience,proto3" json:"new_experience,omitempty"` // 更新后的经验值 + NewLevel int32 `protobuf:"varint,2,opt,name=new_level,json=newLevel,proto3" json:"new_level,omitempty"` // 新的等级 + LevelDelta int32 `protobuf:"varint,3,opt,name=level_delta,json=levelDelta,proto3" json:"level_delta,omitempty"` // 等级变化量(正数=升级,0=无变化) + CrystalReward int64 `protobuf:"varint,4,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"` // 升级水晶奖励(无升级时为0) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AddExperienceResponse) Reset() { - *x = AddExperienceResponse{} +func (x *AddExhibitionHoursResponse) Reset() { + *x = AddExhibitionHoursResponse{} mi := &file_user_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AddExperienceResponse) String() string { +func (x *AddExhibitionHoursResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AddExperienceResponse) ProtoMessage() {} +func (*AddExhibitionHoursResponse) ProtoMessage() {} -func (x *AddExperienceResponse) ProtoReflect() protoreflect.Message { +func (x *AddExhibitionHoursResponse) ProtoReflect() protoreflect.Message { mi := &file_user_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1821,21 +1839,35 @@ func (x *AddExperienceResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AddExperienceResponse.ProtoReflect.Descriptor instead. -func (*AddExperienceResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AddExhibitionHoursResponse.ProtoReflect.Descriptor instead. +func (*AddExhibitionHoursResponse) Descriptor() ([]byte, []int) { return file_user_proto_rawDescGZIP(), []int{28} } -func (x *AddExperienceResponse) GetBase() *common.BaseResponse { +func (x *AddExhibitionHoursResponse) GetBase() *common.BaseResponse { if x != nil { return x.Base } return nil } -func (x *AddExperienceResponse) GetNewExperience() int64 { +func (x *AddExhibitionHoursResponse) GetNewLevel() int32 { if x != nil { - return x.NewExperience + return x.NewLevel + } + return 0 +} + +func (x *AddExhibitionHoursResponse) GetLevelDelta() int32 { + if x != nil { + return x.LevelDelta + } + return 0 +} + +func (x *AddExhibitionHoursResponse) GetCrystalReward() int64 { + if x != nil { + return x.CrystalReward } return 0 } @@ -2829,7 +2861,7 @@ const file_user_proto_rawDesc = "" + "\x15global_wallet_address\x18\x04 \x01(\tR\x13globalWalletAddress\x12\x1b\n" + "\tis_active\x18\x05 \x01(\bR\bisActive\x12\x1d\n" + "\n" + - "created_at\x18\x06 \x01(\x03R\tcreatedAt\"\xfa\x03\n" + + "created_at\x18\x06 \x01(\x03R\tcreatedAt\"\xda\x03\n" + "\n" + "FanProfile\x12\x0e\n" + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" + @@ -2838,23 +2870,20 @@ const file_user_proto_rawDesc = "" + "\bnickname\x18\x04 \x01(\tR\bnickname\x12\x14\n" + "\x05level\x18\x05 \x01(\x05R\x05level\x12\x14\n" + "\x05times\x18\x06 \x01(\x05R\x05times\x12\x16\n" + - "\x06social\x18\a \x01(\x05R\x06social\x12\x1e\n" + - "\n" + - "experience\x18\b \x01(\x03R\n" + - "experience\x12!\n" + - "\fcoin_balance\x18\t \x01(\x03R\vcoinBalance\x12'\n" + - "\x0fcrystal_balance\x18\n" + - " \x01(\x03R\x0ecrystalBalance\x12\x12\n" + - "\x04tags\x18\v \x03(\tR\x04tags\x12\x1d\n" + + "\x06social\x18\a \x01(\x05R\x06social\x12!\n" + + "\fcoin_balance\x18\b \x01(\x03R\vcoinBalance\x12'\n" + + "\x0fcrystal_balance\x18\t \x01(\x03R\x0ecrystalBalance\x12\x12\n" + + "\x04tags\x18\n" + + " \x03(\tR\x04tags\x12\x1d\n" + "\n" + "avatar_url\x18\x11 \x01(\tR\tavatarUrl\x12\x1d\n" + "\n" + - "created_at\x18\f \x01(\x03R\tcreatedAt\x12%\n" + - "\x0estarbook_limit\x18\r \x01(\x05R\rstarbookLimit\x12\x1d\n" + + "created_at\x18\v \x01(\x03R\tcreatedAt\x12%\n" + + "\x0estarbook_limit\x18\f \x01(\x05R\rstarbookLimit\x12\x1d\n" + "\n" + - "slot_limit\x18\x0e \x01(\x05R\tslotLimit\x12!\n" + - "\fassets_count\x18\x0f \x01(\x05R\vassetsCount\x12#\n" + - "\rchain_address\x18\x10 \x01(\tR\fchainAddress\"\xf6\x01\n" + + "slot_limit\x18\r \x01(\x05R\tslotLimit\x12!\n" + + "\fassets_count\x18\x0e \x01(\x05R\vassetsCount\x12#\n" + + "\rchain_address\x18\x0f \x01(\tR\fchainAddress\"\xf6\x01\n" + "\x04Star\x12\x17\n" + "\astar_id\x18\x01 \x01(\x03R\x06starId\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x17\n" + @@ -2939,11 +2968,15 @@ const file_user_proto_rawDesc = "" + "\x1eUpdateFanProfileSocialResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" + "\n" + - "new_social\x18\x02 \x01(\x05R\tnewSocial\"e\n" + + "new_social\x18\x02 \x01(\x05R\tnewSocial\"\xc5\x01\n" + "\x1bUpdateCrystalBalanceRequest\x12\x17\n" + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" + "\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x14\n" + - "\x05delta\x18\x03 \x01(\x03R\x05delta\"q\n" + + "\x05delta\x18\x03 \x01(\x03R\x05delta\x12\x1f\n" + + "\vchange_type\x18\x04 \x01(\tR\n" + + "changeType\x12\x1b\n" + + "\tsource_id\x18\x05 \x01(\tR\bsourceId\x12 \n" + + "\vdescription\x18\x06 \x01(\tR\vdescription\"q\n" + "\x1cUpdateCrystalBalanceResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1f\n" + "\vnew_balance\x18\x02 \x01(\x03R\n" + @@ -2954,14 +2987,17 @@ const file_user_proto_rawDesc = "" + "\x05delta\x18\x03 \x01(\x05R\x05delta\"j\n" + "\x19UpdateAssetsCountResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" + - "\tnew_count\x18\x02 \x01(\x05R\bnewCount\"^\n" + - "\x14AddExperienceRequest\x12\x17\n" + + "\tnew_count\x18\x02 \x01(\x05R\bnewCount\"x\n" + + "\x19AddExhibitionHoursRequest\x12\x17\n" + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" + - "\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x14\n" + - "\x05delta\x18\x03 \x01(\x03R\x05delta\"p\n" + - "\x15AddExperienceResponse\x120\n" + - "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12%\n" + - "\x0enew_experience\x18\x02 \x01(\x03R\rnewExperience\"\x17\n" + + "\astar_id\x18\x02 \x01(\x03R\x06starId\x12)\n" + + "\x10exhibition_hours\x18\x03 \x01(\x03R\x0fexhibitionHours\"\xb3\x01\n" + + "\x1aAddExhibitionHoursResponse\x120\n" + + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" + + "\tnew_level\x18\x02 \x01(\x05R\bnewLevel\x12\x1f\n" + + "\vlevel_delta\x18\x03 \x01(\x05R\n" + + "levelDelta\x12%\n" + + "\x0ecrystal_reward\x18\x04 \x01(\x03R\rcrystalReward\"\x17\n" + "\x15GetCurrentUserRequest\"\xea\x01\n" + "\x16GetCurrentUserResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12&\n" + @@ -3023,7 +3059,7 @@ const file_user_proto_rawDesc = "" + "\n" + "expires_in\x18\x03 \x01(\x03R\texpiresIn\x129\n" + "\vfan_profile\x18\x04 \x01(\v2\x18.topfans.user.FanProfileR\n" + - "fanProfile2\xca\x14\n" + + "fanProfile2\xd9\x14\n" + "\x11UserSocialService\x12k\n" + "\bRegister\x12\x1d.topfans.user.RegisterRequest\x1a\x1e.topfans.user.RegisterResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/register\x12_\n" + "\x05Login\x12\x1a.topfans.user.LoginRequest\x1a\x1b.topfans.user.LoginResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/auth/login\x12v\n" + @@ -3036,8 +3072,8 @@ const file_user_proto_rawDesc = "" + "\rGetFanProfile\x12\".topfans.user.GetFanProfileRequest\x1a#.topfans.user.GetFanProfileResponse\"6\x82\xd3\xe4\x93\x020\x12./api/v1/users/{user_id}/fan-profiles/{star_id}\x12s\n" + "\x16UpdateFanProfileSocial\x12+.topfans.user.UpdateFanProfileSocialRequest\x1a,.topfans.user.UpdateFanProfileSocialResponse\x12m\n" + "\x14UpdateCrystalBalance\x12).topfans.user.UpdateCrystalBalanceRequest\x1a*.topfans.user.UpdateCrystalBalanceResponse\x12d\n" + - "\x11UpdateAssetsCount\x12&.topfans.user.UpdateAssetsCountRequest\x1a'.topfans.user.UpdateAssetsCountResponse\x12X\n" + - "\rAddExperience\x12\".topfans.user.AddExperienceRequest\x1a#.topfans.user.AddExperienceResponse\x12t\n" + + "\x11UpdateAssetsCount\x12&.topfans.user.UpdateAssetsCountRequest\x1a'.topfans.user.UpdateAssetsCountResponse\x12g\n" + + "\x12AddExhibitionHours\x12'.topfans.user.AddExhibitionHoursRequest\x1a(.topfans.user.AddExhibitionHoursResponse\x12t\n" + "\x0eGetCurrentUser\x12#.topfans.user.GetCurrentUserRequest\x1a$.topfans.user.GetCurrentUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/api/v1/auth/me\x12q\n" + "\fGetMyProfile\x12!.topfans.user.GetMyProfileRequest\x1a\".topfans.user.GetMyProfileResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/me/profile\x12z\n" + "\x0eUpdateNickname\x12#.topfans.user.UpdateNicknameRequest\x1a$.topfans.user.UpdateNicknameResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/me/profile\x12\x80\x01\n" + @@ -3089,8 +3125,8 @@ var file_user_proto_goTypes = []any{ (*UpdateCrystalBalanceResponse)(nil), // 24: topfans.user.UpdateCrystalBalanceResponse (*UpdateAssetsCountRequest)(nil), // 25: topfans.user.UpdateAssetsCountRequest (*UpdateAssetsCountResponse)(nil), // 26: topfans.user.UpdateAssetsCountResponse - (*AddExperienceRequest)(nil), // 27: topfans.user.AddExperienceRequest - (*AddExperienceResponse)(nil), // 28: topfans.user.AddExperienceResponse + (*AddExhibitionHoursRequest)(nil), // 27: topfans.user.AddExhibitionHoursRequest + (*AddExhibitionHoursResponse)(nil), // 28: topfans.user.AddExhibitionHoursResponse (*GetCurrentUserRequest)(nil), // 29: topfans.user.GetCurrentUserRequest (*GetCurrentUserResponse)(nil), // 30: topfans.user.GetCurrentUserResponse (*GetMyProfileRequest)(nil), // 31: topfans.user.GetMyProfileRequest @@ -3132,7 +3168,7 @@ var file_user_proto_depIdxs = []int32{ 48, // 16: topfans.user.UpdateFanProfileSocialResponse.base:type_name -> topfans.common.BaseResponse 48, // 17: topfans.user.UpdateCrystalBalanceResponse.base:type_name -> topfans.common.BaseResponse 48, // 18: topfans.user.UpdateAssetsCountResponse.base:type_name -> topfans.common.BaseResponse - 48, // 19: topfans.user.AddExperienceResponse.base:type_name -> topfans.common.BaseResponse + 48, // 19: topfans.user.AddExhibitionHoursResponse.base:type_name -> topfans.common.BaseResponse 48, // 20: topfans.user.GetCurrentUserResponse.base:type_name -> topfans.common.BaseResponse 0, // 21: topfans.user.GetCurrentUserResponse.user:type_name -> topfans.user.User 1, // 22: topfans.user.GetCurrentUserResponse.fan_profile:type_name -> topfans.user.FanProfile @@ -3167,7 +3203,7 @@ var file_user_proto_depIdxs = []int32{ 21, // 51: topfans.user.UserSocialService.UpdateFanProfileSocial:input_type -> topfans.user.UpdateFanProfileSocialRequest 23, // 52: topfans.user.UserSocialService.UpdateCrystalBalance:input_type -> topfans.user.UpdateCrystalBalanceRequest 25, // 53: topfans.user.UserSocialService.UpdateAssetsCount:input_type -> topfans.user.UpdateAssetsCountRequest - 27, // 54: topfans.user.UserSocialService.AddExperience:input_type -> topfans.user.AddExperienceRequest + 27, // 54: topfans.user.UserSocialService.AddExhibitionHours:input_type -> topfans.user.AddExhibitionHoursRequest 29, // 55: topfans.user.UserSocialService.GetCurrentUser:input_type -> topfans.user.GetCurrentUserRequest 31, // 56: topfans.user.UserSocialService.GetMyProfile:input_type -> topfans.user.GetMyProfileRequest 33, // 57: topfans.user.UserSocialService.UpdateNickname:input_type -> topfans.user.UpdateNicknameRequest @@ -3189,7 +3225,7 @@ var file_user_proto_depIdxs = []int32{ 22, // 73: topfans.user.UserSocialService.UpdateFanProfileSocial:output_type -> topfans.user.UpdateFanProfileSocialResponse 24, // 74: topfans.user.UserSocialService.UpdateCrystalBalance:output_type -> topfans.user.UpdateCrystalBalanceResponse 26, // 75: topfans.user.UserSocialService.UpdateAssetsCount:output_type -> topfans.user.UpdateAssetsCountResponse - 28, // 76: topfans.user.UserSocialService.AddExperience:output_type -> topfans.user.AddExperienceResponse + 28, // 76: topfans.user.UserSocialService.AddExhibitionHours:output_type -> topfans.user.AddExhibitionHoursResponse 30, // 77: topfans.user.UserSocialService.GetCurrentUser:output_type -> topfans.user.GetCurrentUserResponse 32, // 78: topfans.user.UserSocialService.GetMyProfile:output_type -> topfans.user.GetMyProfileResponse 34, // 79: topfans.user.UserSocialService.UpdateNickname:output_type -> topfans.user.UpdateNicknameResponse diff --git a/backend/pkg/proto/user/user.triple.go b/backend/pkg/proto/user/user.triple.go index 57edcab..d6b78a8 100644 --- a/backend/pkg/proto/user/user.triple.go +++ b/backend/pkg/proto/user/user.triple.go @@ -60,8 +60,8 @@ const ( UserSocialServiceUpdateCrystalBalanceProcedure = "/topfans.user.UserSocialService/UpdateCrystalBalance" // UserSocialServiceUpdateAssetsCountProcedure is the fully-qualified name of the UserSocialService's UpdateAssetsCount RPC. UserSocialServiceUpdateAssetsCountProcedure = "/topfans.user.UserSocialService/UpdateAssetsCount" - // UserSocialServiceAddExperienceProcedure is the fully-qualified name of the UserSocialService's AddExperience RPC. - UserSocialServiceAddExperienceProcedure = "/topfans.user.UserSocialService/AddExperience" + // UserSocialServiceAddExhibitionHoursProcedure is the fully-qualified name of the UserSocialService's AddExhibitionHours RPC. + UserSocialServiceAddExhibitionHoursProcedure = "/topfans.user.UserSocialService/AddExhibitionHours" // UserSocialServiceGetCurrentUserProcedure is the fully-qualified name of the UserSocialService's GetCurrentUser RPC. UserSocialServiceGetCurrentUserProcedure = "/topfans.user.UserSocialService/GetCurrentUser" // UserSocialServiceGetMyProfileProcedure is the fully-qualified name of the UserSocialService's GetMyProfile RPC. @@ -100,7 +100,7 @@ type UserSocialService interface { UpdateFanProfileSocial(ctx context.Context, req *UpdateFanProfileSocialRequest, opts ...client.CallOption) (*UpdateFanProfileSocialResponse, error) UpdateCrystalBalance(ctx context.Context, req *UpdateCrystalBalanceRequest, opts ...client.CallOption) (*UpdateCrystalBalanceResponse, error) UpdateAssetsCount(ctx context.Context, req *UpdateAssetsCountRequest, opts ...client.CallOption) (*UpdateAssetsCountResponse, error) - AddExperience(ctx context.Context, req *AddExperienceRequest, opts ...client.CallOption) (*AddExperienceResponse, error) + AddExhibitionHours(ctx context.Context, req *AddExhibitionHoursRequest, opts ...client.CallOption) (*AddExhibitionHoursResponse, error) GetCurrentUser(ctx context.Context, req *GetCurrentUserRequest, opts ...client.CallOption) (*GetCurrentUserResponse, error) GetMyProfile(ctx context.Context, req *GetMyProfileRequest, opts ...client.CallOption) (*GetMyProfileResponse, error) UpdateNickname(ctx context.Context, req *UpdateNicknameRequest, opts ...client.CallOption) (*UpdateNicknameResponse, error) @@ -228,9 +228,9 @@ func (c *UserSocialServiceImpl) UpdateAssetsCount(ctx context.Context, req *Upda return resp, nil } -func (c *UserSocialServiceImpl) AddExperience(ctx context.Context, req *AddExperienceRequest, opts ...client.CallOption) (*AddExperienceResponse, error) { - resp := new(AddExperienceResponse) - if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "AddExperience", opts...); err != nil { +func (c *UserSocialServiceImpl) AddExhibitionHours(ctx context.Context, req *AddExhibitionHoursRequest, opts ...client.CallOption) (*AddExhibitionHoursResponse, error) { + resp := new(AddExhibitionHoursResponse) + if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "AddExhibitionHours", opts...); err != nil { return nil, err } return resp, nil @@ -310,7 +310,7 @@ func (c *UserSocialServiceImpl) SwitchIdentity(ctx context.Context, req *SwitchI var UserSocialService_ClientInfo = client.ClientInfo{ InterfaceName: "topfans.user.UserSocialService", - MethodNames: []string{"Register", "Login", "RefreshToken", "ValidateToken", "Logout", "CheckNickname", "CheckMobile", "GetUser", "GetFanProfile", "UpdateFanProfileSocial", "UpdateCrystalBalance", "UpdateAssetsCount", "AddExperience", "GetCurrentUser", "GetMyProfile", "UpdateNickname", "UpdatePassword", "UpdateAvatar", "GetFanIdentities", "GetMyFanIdentities", "AddIdentity", "SwitchIdentity"}, + MethodNames: []string{"Register", "Login", "RefreshToken", "ValidateToken", "Logout", "CheckNickname", "CheckMobile", "GetUser", "GetFanProfile", "UpdateFanProfileSocial", "UpdateCrystalBalance", "UpdateAssetsCount", "AddExhibitionHours", "GetCurrentUser", "GetMyProfile", "UpdateNickname", "UpdatePassword", "UpdateAvatar", "GetFanIdentities", "GetMyFanIdentities", "AddIdentity", "SwitchIdentity"}, ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) { dubboCli := dubboCliRaw.(*UserSocialServiceImpl) dubboCli.conn = conn @@ -331,7 +331,7 @@ type UserSocialServiceHandler interface { UpdateFanProfileSocial(context.Context, *UpdateFanProfileSocialRequest) (*UpdateFanProfileSocialResponse, error) UpdateCrystalBalance(context.Context, *UpdateCrystalBalanceRequest) (*UpdateCrystalBalanceResponse, error) UpdateAssetsCount(context.Context, *UpdateAssetsCountRequest) (*UpdateAssetsCountResponse, error) - AddExperience(context.Context, *AddExperienceRequest) (*AddExperienceResponse, error) + AddExhibitionHours(context.Context, *AddExhibitionHoursRequest) (*AddExhibitionHoursResponse, error) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) GetMyProfile(context.Context, *GetMyProfileRequest) (*GetMyProfileResponse, error) UpdateNickname(context.Context, *UpdateNicknameRequest) (*UpdateNicknameResponse, error) @@ -536,14 +536,14 @@ var UserSocialService_ServiceInfo = server.ServiceInfo{ }, }, { - Name: "AddExperience", + Name: "AddExhibitionHours", Type: constant.CallUnary, ReqInitFunc: func() interface{} { - return new(AddExperienceRequest) + return new(AddExhibitionHoursRequest) }, MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) { - req := args[0].(*AddExperienceRequest) - res, err := handler.(UserSocialServiceHandler).AddExperience(ctx, req) + req := args[0].(*AddExhibitionHoursRequest) + res, err := handler.(UserSocialServiceHandler).AddExhibitionHours(ctx, req) if err != nil { return nil, err } diff --git a/backend/proto/task.proto b/backend/proto/task.proto index d7e9069..ea9aeb6 100644 --- a/backend/proto/task.proto +++ b/backend/proto/task.proto @@ -15,9 +15,8 @@ message DailyTaskItem { string name = 3; string description = 4; int64 crystal_reward = 5; - int64 exp_reward = 6; - string status = 7; // pending/completed/claimed - bool can_claim = 8; + string status = 6; // pending/completed/claimed + bool can_claim = 7; } message GetDailyTasksRequest { @@ -52,7 +51,6 @@ message ClaimDailyTaskResponse { topfans.common.BaseResponse base = 1; bool success = 2; int64 crystal_balance = 3; - int64 experience = 4; } message ClaimAllDailyTasksRequest { @@ -63,8 +61,7 @@ message ClaimAllDailyTasksResponse { topfans.common.BaseResponse base = 1; int32 claimed_count = 2; int64 crystal_balance = 3; - int64 experience = 4; - repeated string claimed_task_keys = 5; + repeated string claimed_task_keys = 4; } // ==================== 引导任务 ==================== @@ -74,11 +71,10 @@ message OnboardingStage { string name = 2; repeated string required_task_keys = 3; int64 crystal_reward = 4; - int64 exp_reward = 5; - string status = 6; // pending/completed/in_progress - bool is_current = 7; - bool all_tasks_completed = 8; // 该阶段所有任务是否完成 - bool is_reward_claimed = 9; // 该阶段奖励是否已领取 + string status = 5; // pending/completed/in_progress + bool is_current = 6; + bool all_tasks_completed = 7; // 该阶段所有任务是否完成 + bool is_reward_claimed = 8; // 该阶段奖励是否已领取 } message CompleteGuideRequest { @@ -123,7 +119,6 @@ message ClaimOnboardingRewardResponse { topfans.common.BaseResponse base = 1; bool success = 2; string crystal_balance = 3; // 使用 string 避免 Dubbo int64 序列化 bug - string experience = 4; // 使用 string 避免 Dubbo int64 序列化 bug } // ==================== 展示收益 ==================== diff --git a/backend/proto/user.proto b/backend/proto/user.proto index f4c3a73..a08ee6a 100644 --- a/backend/proto/user.proto +++ b/backend/proto/user.proto @@ -28,16 +28,15 @@ message FanProfile { int32 level = 5; // 等级 int32 times = 6; // 剩余铸造次数 int32 social = 7; // 好友个数 - int64 experience = 8; // 经验值 - int64 coin_balance = 9; // 游戏币余额 - int64 crystal_balance = 10; // 顶粉水晶余额 - repeated string tags = 11; // 标签数组 + int64 coin_balance = 8; // 游戏币余额 + int64 crystal_balance = 9; // 顶粉水晶余额 + repeated string tags = 10; // 标签数组 string avatar_url = 17; // 头像URL(星球专属) - int64 created_at = 12; - int32 starbook_limit = 13; // 星书限制 - int32 slot_limit = 14; // 槽位限制 - int32 assets_count = 15; // 资产数量 - string chain_address = 16; // 链地址 + int64 created_at = 11; + int32 starbook_limit = 12; // 星书限制 + int32 slot_limit = 13; // 槽位限制 + int32 assets_count = 14; // 资产数量 + string chain_address = 15; // 链地址 } // 明星信息 @@ -190,6 +189,9 @@ message UpdateCrystalBalanceRequest { int64 user_id = 1; // 用户ID int64 star_id = 2; // 明星ID int64 delta = 3; // 变化量(正数增加,负数减少) + string change_type = 4; // 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust + string source_id = 5; // 关联业务ID + string description = 6; // 可读描述 } // 更新水晶余额响应 @@ -211,17 +213,19 @@ message UpdateAssetsCountResponse { int32 new_count = 2; // 更新后的资产数量 } -// 增加经验值请求(内部RPC调用,用于taskService增加经验) -message AddExperienceRequest { +// 增加累计上架时长请求(内部RPC调用,用于galleryService展品下架时累加时长) +message AddExhibitionHoursRequest { int64 user_id = 1; // 用户ID int64 star_id = 2; // 明星ID - int64 delta = 3; // 变化量(正数增加,负数减少) + int64 exhibition_hours = 3; // 本次展出的时长(小时) } -// 增加经验值响应 -message AddExperienceResponse { +// 增加累计上架时长响应 +message AddExhibitionHoursResponse { topfans.common.BaseResponse base = 1; - int64 new_experience = 2; // 更新后的经验值 + int32 new_level = 2; // 新的等级 + int32 level_delta = 3; // 等级变化量(正数=升级,0=无变化) + int64 crystal_reward = 4; // 升级水晶奖励(无升级时为0) } // 获取当前登录用户信息请求 @@ -414,8 +418,8 @@ service UserSocialService { // 内部RPC:更新资产数量(仅供assetService调用) rpc UpdateAssetsCount(UpdateAssetsCountRequest) returns (UpdateAssetsCountResponse); - // 内部RPC:增加经验值(仅供taskService调用) - rpc AddExperience(AddExperienceRequest) returns (AddExperienceResponse); + // 内部RPC:增加累计上架时长(仅供galleryService调用) + rpc AddExhibitionHours(AddExhibitionHoursRequest) returns (AddExhibitionHoursResponse); rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) { option (google.api.http) = { diff --git a/backend/scripts/20260513_economic_system.sql b/backend/scripts/20260513_economic_system.sql new file mode 100644 index 0000000..a37c746 --- /dev/null +++ b/backend/scripts/20260513_economic_system.sql @@ -0,0 +1,278 @@ +-- ============================================================ +-- 经济系统建表脚本 +-- 执行方式: psql -h -U -d -f backend/scripts/20260513_economic_system.sql +-- 创建日期: 2026-05-13 +-- 说明: 本脚本用于初始化经济系统相关的表和配置 +-- - fan_profiles 表新增字段: revenue_boost_bps, like_bet_count +-- - 铸造消耗配置、用户铸爱累计 +-- - 等级阈值、升级奖励配置 +-- - 水晶/游戏币流水表 +-- ============================================================ + +-- 1. Add revenue_boost_bps column to fan_profiles (if not exists) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'fan_profiles' AND column_name = 'revenue_boost_bps') THEN + ALTER TABLE fan_profiles ADD COLUMN revenue_boost_bps INT NOT NULL DEFAULT 0; + END IF; +END $$; + +-- 1.2 Add like_bet_count column to fan_profiles (if not exists) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'fan_profiles' AND column_name = 'like_bet_count') THEN + ALTER TABLE fan_profiles ADD COLUMN like_bet_count INT NOT NULL DEFAULT 0; + END IF; +END $$; + +-- 2. 铸造消耗配置表 +CREATE TABLE IF NOT EXISTS mint_cost_config ( + id BIGSERIAL PRIMARY KEY, + mint_count INT NOT NULL UNIQUE, + cost_crystal BIGINT NOT NULL, + probability BIGINT DEFAULT 0, + reward_type VARCHAR(50) DEFAULT NULL, + reward_value BIGINT DEFAULT 0, + description VARCHAR(255), + updated_at BIGINT NOT NULL +); +COMMENT ON TABLE mint_cost_config IS '铸造消耗配置表,记录每次铸造的水晶消耗和保底概率'; + +-- 初始数据 +INSERT INTO mint_cost_config (mint_count, cost_crystal, probability, reward_type, reward_value, description, updated_at) VALUES +(1, 2, 0, NULL, 0, '第1次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(2, 4, 0, NULL, 0, '第2次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(3, 8, 0, NULL, 0, '第3次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(4, 16, 0, NULL, 0, '第4次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(5, 32, 0, NULL, 0, '第5次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(6, 64, 0, NULL, 0, '第6次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(7, 128, 0, NULL, 0, '第7次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(8, 256, 0, NULL, 0, '第8次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(9, 512, 20, '收益提升', 500, '20%概率,获得500 bps(+5%)永久收益提升(小保底)', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(10, 1024, 100, '收益提升', 500, '100%概率,获得500 bps(+5%)永久收益提升(大保底)', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)) +ON CONFLICT (mint_count) DO NOTHING; + +-- 3. 用户铸爱累计表 +CREATE TABLE IF NOT EXISTS user_mint_count ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + mint_count INT NOT NULL DEFAULT 0, + revenue_boost_bps INT NOT NULL DEFAULT 0, + updated_at BIGINT NOT NULL, + CONSTRAINT uk_user_mint_star UNIQUE (user_id, star_id) +); +COMMENT ON TABLE user_mint_count IS '用户铸爱累计表,记录用户累计铸造次数和永久收益提升基点'; + +-- 4. 等级阈值配置表 +CREATE TABLE IF NOT EXISTS level_thresholds ( + level INT PRIMARY KEY, + max_exhibition_hours BIGINT NOT NULL, + like_bet_count INT NOT NULL, + description VARCHAR(100) +); +COMMENT ON TABLE level_thresholds IS '等级阈值配置表,记录升级到每个等级需要的累计上架时长和点赞押注次数'; + +-- 初始数据(20级满级) +INSERT INTO level_thresholds (level, max_exhibition_hours, like_bet_count, description) VALUES +(1, 0, 0, '1级新手'), +(2, 6, 6, '2级粉丝'), +(3, 12, 7, '3级真爱'), +(4, 18, 8, '4级铁粉'), +(5, 24, 9, '5级钻石粉'), +(6, 30, 9, '6级钻石粉'), +(7, 36, 10, '7级钻石粉'), +(8, 42, 11, '8级钻石粉'), +(9, 48, 12, '9级钻石粉'), +(10, 54, 13, '10级钻石粉'), +(11, 60, 13, '11级钻石粉'), +(12, 66, 13, '12级钻石粉'), +(13, 72, 14, '13级钻石粉'), +(14, 78, 15, '14级钻石粉'), +(15, 84, 16, '15级钻石粉'), +(16, 90, 16, '16级钻石粉'), +(17, 96, 17, '17级钻石粉'), +(18, 102, 18, '18级钻石粉'), +(19, 108, 19, '19级钻石粉'), +(20, 114, 20, '20级终极粉') +ON CONFLICT (level) DO NOTHING; + +-- 5. 用户累计上架时长表 +CREATE TABLE IF NOT EXISTS user_exhibition_hours ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + total_exhibition_hours BIGINT NOT NULL DEFAULT 0, + updated_at BIGINT NOT NULL, + CONSTRAINT uk_exhibition_user_star UNIQUE (user_id, star_id) +); +COMMENT ON TABLE user_exhibition_hours IS '用户累计上架时长表,记录用户累计上架时长'; + +-- 6. 等级上限配置表 +CREATE TABLE IF NOT EXISTS level_cap_config ( + id BIGSERIAL PRIMARY KEY, + max_level INT NOT NULL DEFAULT 20, + updated_at BIGINT NOT NULL +); +COMMENT ON TABLE level_cap_config IS '等级上限配置表,记录最高等级'; + +INSERT INTO level_cap_config (max_level, updated_at) VALUES (20, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)) +ON CONFLICT DO NOTHING; + +-- 7. 等级升级条件配置表(21级+) +CREATE TABLE IF NOT EXISTS level_upgrade_conditions ( + level INT PRIMARY KEY, + require_total_hours BIGINT NOT NULL, + require_dazi_level INT DEFAULT 0, + description VARCHAR(100), + updated_at BIGINT NOT NULL +); +COMMENT ON TABLE level_upgrade_conditions IS '等级升级条件配置表,记录21级及以上的升级条件'; + +-- 8. 升级奖励配置表 +CREATE TABLE IF NOT EXISTS level_up_reward_config ( + id BIGSERIAL PRIMARY KEY, + level INT NOT NULL, + reward_type VARCHAR(50) NOT NULL, + reward_value BIGINT NOT NULL DEFAULT 0, + is_enabled BOOLEAN DEFAULT true, + updated_at BIGINT NOT NULL, + CONSTRAINT uk_level_reward_type UNIQUE (level, reward_type) +); +COMMENT ON TABLE level_up_reward_config IS '升级奖励配置表,记录升级时发放的奖励类型和数值'; + +-- 初始数据(21级示例,后续可扩展更多等级) +INSERT INTO level_upgrade_conditions (level, require_total_hours, require_dazi_level, description, updated_at) VALUES +(21, 120, 21, '21级:总时长120h + 搭子21级', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)) +ON CONFLICT (level) DO NOTHING; + +-- 8. 升级奖励配置表 +CREATE TABLE IF NOT EXISTS level_up_reward_config ( + id BIGSERIAL PRIMARY KEY, + level INT NOT NULL, + reward_type VARCHAR(50) NOT NULL, + reward_value BIGINT NOT NULL DEFAULT 0, + is_enabled BOOLEAN DEFAULT true, + updated_at BIGINT NOT NULL, + CONSTRAINT uk_level_reward_type UNIQUE (level, reward_type) +); + +-- 初始数据示例 +INSERT INTO level_up_reward_config (level, reward_type, reward_value, is_enabled, updated_at) VALUES +(2, 'crystal', 10, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(2, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(3, 'crystal', 20, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(3, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(4, 'crystal', 30, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(4, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(5, 'crystal', 50, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(5, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(6, 'crystal', 80, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(7, 'crystal', 120, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(8, 'crystal', 180, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(9, 'crystal', 280, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(10, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(11, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(12, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(13, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(14, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(15, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(16, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(17, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(18, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(19, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(20, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)) +ON CONFLICT (level, reward_type) DO NOTHING; + +-- 9. 搭子等级阈值配置表(预留) +CREATE TABLE IF NOT EXISTS dazi_level_thresholds ( + level INT PRIMARY KEY, + upgrade_condition VARCHAR(100), + condition_param INT DEFAULT 0, + description VARCHAR(100) +); +COMMENT ON TABLE dazi_level_thresholds IS '搭子等级阈值配置表(预留)'; + +-- 10. 用户搭子等级表(预留) +CREATE TABLE IF NOT EXISTS user_dazi_level ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + dazi_level INT NOT NULL DEFAULT 1, + updated_at BIGINT NOT NULL, + CONSTRAINT uk_dazi_user_star UNIQUE (user_id, star_id) +); +COMMENT ON TABLE user_dazi_level IS '用户搭子等级表(预留)'; + +-- 11. 水晶交易流水表(预留) +CREATE TABLE IF NOT EXISTS crystal_transaction_records ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + change_type VARCHAR(30) NOT NULL, + delta BIGINT NOT NULL, + balance_before BIGINT NOT NULL, + balance_after BIGINT NOT NULL, + source_id VARCHAR(100), + description VARCHAR(255), + created_at BIGINT NOT NULL +); +COMMENT ON TABLE crystal_transaction_records IS '水晶交易流水表,记录水晶的收支明细(复式记账)'; + +CREATE INDEX IF NOT EXISTS ix_crystal_tx_user_star ON crystal_transaction_records(user_id, star_id); +CREATE INDEX IF NOT EXISTS ix_crystal_tx_created ON crystal_transaction_records(created_at DESC); +CREATE INDEX IF NOT EXISTS ix_crystal_tx_change_type ON crystal_transaction_records(change_type); + +-- 12. 游戏币交易流水表(预留) +CREATE TABLE IF NOT EXISTS coin_transaction_records ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + change_type VARCHAR(30) NOT NULL, + delta BIGINT NOT NULL, + balance_before BIGINT NOT NULL, + balance_after BIGINT NOT NULL, + source_id VARCHAR(100), + description VARCHAR(255), + created_at BIGINT NOT NULL +); +COMMENT ON TABLE coin_transaction_records IS '游戏币交易流水表(预留)'; + +CREATE INDEX IF NOT EXISTS ix_coin_tx_user_star ON coin_transaction_records(user_id, star_id); +CREATE INDEX IF NOT EXISTS ix_coin_tx_created ON coin_transaction_records(created_at DESC); + +-- 13. 铸造奖励配置表(预留) +CREATE TABLE IF NOT EXISTS mint_reward_config ( + id BIGSERIAL PRIMARY KEY, + star_id BIGINT NOT NULL UNIQUE, + base_reward BIGINT NOT NULL DEFAULT 0, + is_enabled BOOLEAN DEFAULT true, + updated_at BIGINT NOT NULL +); +COMMENT ON TABLE mint_reward_config IS '铸造奖励配置表(预留),记录每个明星的铸造基础奖励'; + +-- 初始数据(0=全服默认) +INSERT INTO mint_reward_config (star_id, base_reward, is_enabled, updated_at) VALUES (0, 0, false, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)) +ON CONFLICT (star_id) DO NOTHING; + +-- 14. 铸造阶梯奖励配置表(预留) +CREATE TABLE IF NOT EXISTS mint_milestone_config ( + id BIGSERIAL PRIMARY KEY, + star_id BIGINT NOT NULL, + milestone_count INT NOT NULL, + bonus_reward BIGINT NOT NULL, + created_at BIGINT NOT NULL, + CONSTRAINT uk_milestone_star_count UNIQUE (star_id, milestone_count) +); +COMMENT ON TABLE mint_milestone_config IS '铸造阶梯奖励配置表(预留),记录铸造里程碑奖励'; + +-- 初始数据示例 +INSERT INTO mint_milestone_config (star_id, milestone_count, bonus_reward, created_at) VALUES +(0, 10, 5, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(0, 30, 15, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), +(0, 100, 50, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)) +ON CONFLICT (star_id, milestone_count) DO NOTHING; + +-- ============================================================ +-- 完成 +-- ============================================================ diff --git a/backend/scripts/create_gallery_test_users.go b/backend/scripts/create_gallery_test_users.go index 56be6d2..3255088 100644 --- a/backend/scripts/create_gallery_test_users.go +++ b/backend/scripts/create_gallery_test_users.go @@ -63,8 +63,8 @@ ON CONFLICT (mobile) DO UPDATE SET password_hash = EXCLUDED.password_hash; fmt.Println() // 2. 插入粉丝档案 - fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, experience, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at) -VALUES (%d, %d, '%s', 1, 1, 0, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d) + fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at) +VALUES (%d, %d, '%s', 1, 1, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d) ON CONFLICT (user_id, star_id) DO UPDATE SET nickname = EXCLUDED.nickname, crystal_balance = EXCLUDED.crystal_balance; `, user1ID, star1ID, user1Nickname, now, now) fmt.Println() @@ -114,8 +114,8 @@ ON CONFLICT (mobile) DO UPDATE SET password_hash = EXCLUDED.password_hash; fmt.Println() // 2. 插入粉丝档案 - fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, experience, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at) -VALUES (%d, %d, '%s', 1, 1, 0, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d) + fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at) +VALUES (%d, %d, '%s', 1, 1, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d) ON CONFLICT (user_id, star_id) DO UPDATE SET nickname = EXCLUDED.nickname, crystal_balance = EXCLUDED.crystal_balance; `, user2ID, star2ID, user2Nickname, now, now) fmt.Println() diff --git a/backend/services/assetService/client/user_rpc_client.go b/backend/services/assetService/client/user_rpc_client.go index 760dfa2..656728f 100644 --- a/backend/services/assetService/client/user_rpc_client.go +++ b/backend/services/assetService/client/user_rpc_client.go @@ -13,7 +13,7 @@ import ( // UserServiceClient User Service RPC 客户端接口 type UserServiceClient interface { // UpdateCrystalBalance 更新水晶余额 - UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error) + UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) // UpdateAssetsCount 更新资产数量 UpdateAssetsCount(ctx context.Context, userID, starID int64, delta int32) (int32, error) @@ -35,7 +35,7 @@ func NewUserServiceClient(client pbUser.UserSocialService) UserServiceClient { } // UpdateCrystalBalance 更新水晶余额 -func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error) { +func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) { logger.Logger.Debug("Calling UserService.UpdateCrystalBalance", zap.Int64("user_id", userID), zap.Int64("star_id", starID), @@ -46,6 +46,9 @@ func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, st UserId: userID, StarId: starID, Delta: delta, + ChangeType: changeType, + SourceId: sourceID, + Description: description, }) if err != nil { diff --git a/backend/services/assetService/main.go b/backend/services/assetService/main.go index 99fb79a..e1074d8 100644 --- a/backend/services/assetService/main.go +++ b/backend/services/assetService/main.go @@ -110,6 +110,8 @@ func main() { mintOrderRepo := repository.NewMintOrderRepository(database.GetDB()) assetLikeRepo := repository.NewAssetLikeRepository(database.GetDB()) rankingRepo := repository.NewRankingRepository(database.GetDB()) + mintCostRepo := repository.NewMintCostRepository() + userMintCountRepo := repository.NewUserMintCountRepository() logger.Logger.Info("Repository layer initialized") // 创建 Dubbo 客户端 @@ -131,7 +133,7 @@ func main() { // 创建 Service 层实例 registryRepo := starbookRepo.NewAssetRegistryRepository(database.GetDB()) assetService := service.NewAssetService(assetRepo, mintOrderRepo, assetLikeRepo, userClient, database.GetDB(), registryRepo) - mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo) + mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo, mintCostRepo, userMintCountRepo) assetLikeService := service.NewAssetLikeService(assetRepo, assetLikeRepo, database.GetDB()) rankingService := service.NewRankingService(rankingRepo, userClient) logger.Logger.Info("Service layer initialized") diff --git a/backend/services/assetService/repository/mint_cost_repository.go b/backend/services/assetService/repository/mint_cost_repository.go new file mode 100644 index 0000000..ec71df2 --- /dev/null +++ b/backend/services/assetService/repository/mint_cost_repository.go @@ -0,0 +1,45 @@ +package repository + +import ( + "github.com/topfans/backend/pkg/database" + "github.com/topfans/backend/pkg/models" + "gorm.io/gorm" +) + +// MintCostRepository 铸造消耗配置仓库 +type MintCostRepository interface { + // GetByMintCount 根据铸爱次数获取配置 + GetByMintCount(mintCount int32) (*models.MintCostConfig, error) + + // GetAll 获取所有配置 + GetAll() ([]*models.MintCostConfig, error) +} + +type mintCostRepository struct { + db *gorm.DB +} + +// NewMintCostRepository 创建铸造消耗配置仓库 +func NewMintCostRepository() MintCostRepository { + return &mintCostRepository{ + db: database.GetDB(), + } +} + +// GetByMintCount 根据铸爱次数获取配置 +func (r *mintCostRepository) GetByMintCount(mintCount int32) (*models.MintCostConfig, error) { + var config models.MintCostConfig + if err := r.db.Where("mint_count = ?", mintCount).First(&config).Error; err != nil { + return nil, err + } + return &config, nil +} + +// GetAll 获取所有配置 +func (r *mintCostRepository) GetAll() ([]*models.MintCostConfig, error) { + var configs []*models.MintCostConfig + if err := r.db.Order("mint_count ASC").Find(&configs).Error; err != nil { + return nil, err + } + return configs, nil +} diff --git a/backend/services/assetService/repository/user_mint_count_repository.go b/backend/services/assetService/repository/user_mint_count_repository.go new file mode 100644 index 0000000..0c2c75f --- /dev/null +++ b/backend/services/assetService/repository/user_mint_count_repository.go @@ -0,0 +1,115 @@ +package repository + +import ( + "errors" + "time" + + "github.com/topfans/backend/pkg/database" + "github.com/topfans/backend/pkg/models" + "gorm.io/gorm" +) + +// UserMintCountRepository 用户铸爱累计仓库 +type UserMintCountRepository interface { + // GetOrCreate 获取或创建用户铸爱累计记录 + GetOrCreate(tx *gorm.DB, userID, starID int64) (*models.UserMintCount, bool, error) + + // Get 获取用户铸爱累计记录 + Get(userID, starID int64) (*models.UserMintCount, error) + + // IncrementMintCount 累加铸爱次数 + IncrementMintCount(tx *gorm.DB, userID, starID int64, delta int32) (*models.UserMintCount, error) + + // UpdateRevenueBoost 更新永久收益提升 + UpdateRevenueBoost(tx *gorm.DB, userID, starID int64, addBps int32) (*models.UserMintCount, error) +} + +type userMintCountRepository struct { + db *gorm.DB +} + +// NewUserMintCountRepository 创建用户铸爱累计仓库 +func NewUserMintCountRepository() UserMintCountRepository { + return &userMintCountRepository{ + db: database.GetDB(), + } +} + +// GetOrCreate 获取或创建用户铸爱累计记录 +func (r *userMintCountRepository) GetOrCreate(tx *gorm.DB, userID, starID int64) (*models.UserMintCount, bool, error) { + if tx == nil { + tx = r.db + } + + var record models.UserMintCount + err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + // 创建新记录 + record = models.UserMintCount{ + UserID: userID, + StarID: starID, + MintCount: 0, + RevenueBoostBps: 0, + UpdatedAt: time.Now().UnixMilli(), + } + if err := tx.Create(&record).Error; err != nil { + return nil, false, err + } + return &record, true, nil + } + if err != nil { + return nil, false, err + } + return &record, false, nil +} + +// Get 获取用户铸爱累计记录 +func (r *userMintCountRepository) Get(userID, starID int64) (*models.UserMintCount, error) { + var record models.UserMintCount + if err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error; err != nil { + return nil, err + } + return &record, nil +} + +// IncrementMintCount 累加铸爱次数 +func (r *userMintCountRepository) IncrementMintCount(tx *gorm.DB, userID, starID int64, delta int32) (*models.UserMintCount, error) { + if tx == nil { + tx = r.db + } + + // 先查询当前记录 + var record models.UserMintCount + if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error; err != nil { + return nil, err + } + + // 更新铸爱次数 + record.MintCount += delta + record.UpdatedAt = time.Now().UnixMilli() + if err := tx.Save(&record).Error; err != nil { + return nil, err + } + return &record, nil +} + +// UpdateRevenueBoost 更新永久收益提升 +func (r *userMintCountRepository) UpdateRevenueBoost(tx *gorm.DB, userID, starID int64, addBps int32) (*models.UserMintCount, error) { + if tx == nil { + tx = r.db + } + + // 先查询当前记录 + var record models.UserMintCount + if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error; err != nil { + return nil, err + } + + // 更新收益提升 + record.RevenueBoostBps += addBps + record.UpdatedAt = time.Now().UnixMilli() + if err := tx.Save(&record).Error; err != nil { + return nil, err + } + return &record, nil +} diff --git a/backend/services/galleryService/client/asset_rpc_client.go b/backend/services/galleryService/client/asset_rpc_client.go index 0aa787c..f3a841e 100644 --- a/backend/services/galleryService/client/asset_rpc_client.go +++ b/backend/services/galleryService/client/asset_rpc_client.go @@ -31,6 +31,9 @@ type AssetRPCClient interface { // GetAssetInfo 获取资产信息(简化版,用于展示) GetAssetInfo(assetID, userID, starID int64) (*AssetInfo, error) + // GetAssetLikeCount 获取资产的点赞数 + GetAssetLikeCount(assetID int64) int + // ClearAssetLikeRecords 清除资产点赞记录(不修改 like_count),在藏品下架时调用 ClearAssetLikeRecords(assetID int64) error } @@ -220,3 +223,26 @@ func (c *assetRPCClient) ClearAssetLikeRecords(assetID int64) error { return nil } + +// GetAssetLikeCount 获取资产的点赞数(不验证用户) +func (c *assetRPCClient) GetAssetLikeCount(assetID int64) int { + ctx := context.Background() + resp, err := c.client.GetAsset(ctx, &pbAsset.GetAssetRequest{ + AssetId: assetID, + }) + + if err != nil { + logger.Logger.Error("Failed to call AssetService.GetAsset for like count", + zap.Int64("asset_id", assetID), + zap.Error(err)) + return 0 + } + + if resp == nil || resp.Base == nil || resp.Base.Code != pbCommon.StatusCode_STATUS_OK { + logger.Logger.Warn("AssetService.GetAsset returned error for like count", + zap.Int64("asset_id", assetID)) + return 0 + } + + return int(resp.Asset.LikeCount) +} diff --git a/backend/services/galleryService/client/task_rpc_client.go b/backend/services/galleryService/client/task_rpc_client.go new file mode 100644 index 0000000..0c4c20e --- /dev/null +++ b/backend/services/galleryService/client/task_rpc_client.go @@ -0,0 +1,100 @@ +package client + +import ( + "context" + "errors" + "fmt" + + "github.com/topfans/backend/pkg/logger" + pbCommon "github.com/topfans/backend/pkg/proto/common" + pbTask "github.com/topfans/backend/pkg/proto/task" + "go.uber.org/zap" +) + +// TaskRPCClient Task Service RPC客户端接口 +type TaskRPCClient interface { + // OnExhibitionCompleted 当展位到期完成时调用,创建展示收益记录 + OnExhibitionCompleted(ctx context.Context, req *OnExhibitionCompletedRequest) (*OnExhibitionCompletedResponse, error) +} + +// OnExhibitionCompletedRequest 展位完成请求 +type OnExhibitionCompletedRequest struct { + ExhibitionId int64 + AssetId int64 + SlotId int64 + OccupierUid int64 + OccupierStarId int64 + SlotOwnerUid int64 + StartTime int64 + ExpireAt int64 + CrystalAmount int64 +} + +// OnExhibitionCompletedResponse 展位完成响应 +type OnExhibitionCompletedResponse struct { + RevenueRecordId int64 +} + +// taskRPCClient Task Service RPC客户端实现 +type taskRPCClient struct { + client pbTask.TaskInternalService +} + +// NewTaskRPCClient 创建Task Service RPC客户端 +func NewTaskRPCClient(client pbTask.TaskInternalService) TaskRPCClient { + return &taskRPCClient{ + client: client, + } +} + +// OnExhibitionCompleted 当展位到期完成时调用,创建展示收益记录 +func (c *taskRPCClient) OnExhibitionCompleted(ctx context.Context, req *OnExhibitionCompletedRequest) (*OnExhibitionCompletedResponse, error) { + logger.Logger.Debug("Calling TaskService.OnExhibitionCompleted", + zap.Int64("exhibition_id", req.ExhibitionId), + zap.Int64("asset_id", req.AssetId), + zap.Int64("slot_id", req.SlotId), + zap.Int64("occupier_uid", req.OccupierUid), + zap.Int64("slot_owner_uid", req.SlotOwnerUid), + zap.Int64("crystal_amount", req.CrystalAmount)) + + if c.client == nil { + logger.Logger.Error("TaskRPCClient: client is nil") + return nil, errors.New("task client is nil") + } + + pbReq := &pbTask.OnExhibitionCompletedRequest{ + ExhibitionId: req.ExhibitionId, + AssetId: req.AssetId, + SlotId: req.SlotId, + OccupierUid: req.OccupierUid, + OccupierStarId: req.OccupierStarId, + SlotOwnerUid: req.SlotOwnerUid, + StartTime: req.StartTime, + ExpireAt: req.ExpireAt, + CrystalAmount: req.CrystalAmount, + } + + resp, err := c.client.OnExhibitionCompleted(ctx, pbReq) + if err != nil { + logger.Logger.Error("Failed to call TaskService.OnExhibitionCompleted", + zap.Int64("exhibition_id", req.ExhibitionId), + zap.Error(err)) + return nil, fmt.Errorf("call TaskService.OnExhibitionCompleted failed: %w", err) + } + + if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { + logger.Logger.Warn("TaskService.OnExhibitionCompleted returned error", + zap.Int64("exhibition_id", req.ExhibitionId), + zap.Int32("code", int32(resp.Base.Code)), + zap.String("message", resp.Base.Message)) + return nil, fmt.Errorf("TaskService.OnExhibitionCompleted error: %s", resp.Base.Message) + } + + logger.Logger.Info("TaskService.OnExhibitionCompleted successful", + zap.Int64("exhibition_id", req.ExhibitionId), + zap.Int64("revenue_record_id", resp.RevenueRecordId)) + + return &OnExhibitionCompletedResponse{ + RevenueRecordId: resp.RevenueRecordId, + }, nil +} diff --git a/backend/services/galleryService/client/user_rpc_client.go b/backend/services/galleryService/client/user_rpc_client.go index 8f95ca1..772bf34 100644 --- a/backend/services/galleryService/client/user_rpc_client.go +++ b/backend/services/galleryService/client/user_rpc_client.go @@ -28,6 +28,10 @@ type UserRPCClient interface { // UpdateCrystalBalance 更新水晶余额(返回更新后的余额) UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error) + + // AddExhibitionHours 增加用户累计上架时长 + // 返回: newLevel, levelDelta, crystalReward, error + AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) } // userRPCClient User Service RPC客户端实现 @@ -144,3 +148,54 @@ func (c *userRPCClient) UpdateCrystalBalance(userID, starID int64, delta int64) return resp.NewBalance, nil } + +// AddExhibitionHours 增加用户累计上架时长 +func (c *userRPCClient) AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) { + logger.Logger.Debug("Calling UserService.AddExhibitionHours", + zap.Int64("user_id", userID), + zap.Int64("star_id", starID), + zap.Int64("hours", hours), + ) + + ctx := context.Background() + resp, err := c.client.AddExhibitionHours(ctx, &pbUser.AddExhibitionHoursRequest{ + UserId: userID, + StarId: starID, + ExhibitionHours: hours, + }) + + if err != nil { + logger.Logger.Error("Failed to call UserService.AddExhibitionHours", + zap.Int64("user_id", userID), + zap.Int64("star_id", starID), + zap.Int64("hours", hours), + zap.Error(err), + ) + return 0, 0, 0, err + } + + if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { + errorMsg := resp.Base.Message + if errorMsg == "" { + errorMsg = fmt.Sprintf("UserService返回错误码: %d", resp.Base.Code) + } + logger.Logger.Warn("UserService.AddExhibitionHours returned error", + zap.Int64("user_id", userID), + zap.Int64("star_id", starID), + zap.Int32("code", int32(resp.Base.Code)), + zap.String("message", errorMsg), + ) + return 0, 0, 0, errors.New(errorMsg) + } + + logger.Logger.Debug("UserService.AddExhibitionHours successful", + zap.Int64("user_id", userID), + zap.Int64("star_id", starID), + zap.Int64("hours", hours), + zap.Int32("new_level", resp.NewLevel), + zap.Int32("level_delta", resp.LevelDelta), + zap.Int64("crystal_reward", resp.CrystalReward), + ) + + return resp.NewLevel, resp.LevelDelta, resp.CrystalReward, nil +} diff --git a/backend/services/galleryService/main.go b/backend/services/galleryService/main.go index 0599496..b30d461 100644 --- a/backend/services/galleryService/main.go +++ b/backend/services/galleryService/main.go @@ -19,6 +19,7 @@ import ( "github.com/topfans/backend/pkg/models" pbAsset "github.com/topfans/backend/pkg/proto/asset" pbGallery "github.com/topfans/backend/pkg/proto/gallery" + pbTask "github.com/topfans/backend/pkg/proto/task" pbUser "github.com/topfans/backend/pkg/proto/user" rpcclient "github.com/topfans/backend/services/galleryService/client" "github.com/topfans/backend/services/galleryService/provider" @@ -35,6 +36,7 @@ var ( dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "Database name") assetServiceURL = flag.String("asset-service-url", getEnv("ASSET_SERVICE_URL", "tri://localhost:20003"), "Asset service URL") userServiceURL = flag.String("user-service-url", getEnv("USER_SERVICE_URL", "tri://localhost:20000"), "User service URL") + taskServiceURL = flag.String("task-service-url", getEnv("TASK_SERVICE_URL", "tri://localhost:20002"), "Task service URL") healthHandler *health.Handler ) @@ -136,14 +138,30 @@ func main() { userRPCClient := rpcclient.NewUserRPCClient(userServiceClient) logger.Logger.Info("User Service RPC client initialized") + // 创建 Task Service Dubbo 客户端 + taskCli, err := dubboclient.NewClient( + dubboclient.WithClientURL(*taskServiceURL), + ) + if err != nil { + logger.Logger.Fatal(fmt.Sprintf("Failed to create Task Service Dubbo client: %v", err)) + } + + // 获取 Task Service RPC 客户端 + taskServiceClient, err := pbTask.NewTaskInternalService(taskCli) + if err != nil { + logger.Logger.Fatal(fmt.Sprintf("Failed to create Task Service RPC client: %v", err)) + } + taskRPCClient := rpcclient.NewTaskRPCClient(taskServiceClient) + logger.Logger.Info("Task Service RPC client initialized") + // 创建 Service 层实例 galleryService := service.NewGalleryService(galleryRepo, assetRPCClient, userRPCClient) slotService := service.NewSlotService(galleryRepo, userRPCClient) exhibitionService := service.NewExhibitionService(galleryRepo, assetRPCClient) logger.Logger.Info("Service layer initialized") - // 创建并启动清理 Worker(注入 assetRPCClient 以便下架时清除点赞记录) - cleanupWorker := service.NewCleanupWorker(galleryRepo, assetRPCClient) + // 创建并启动清理 Worker(注入 assetRPCClient, userClient 和 taskClient) + cleanupWorker := service.NewCleanupWorker(galleryRepo, assetRPCClient, userRPCClient, taskRPCClient) go cleanupWorker.Start() logger.Logger.Info("Cleanup worker started") diff --git a/backend/services/galleryService/repository/gallery_repository.go b/backend/services/galleryService/repository/gallery_repository.go index 8bbe619..f871713 100644 --- a/backend/services/galleryService/repository/gallery_repository.go +++ b/backend/services/galleryService/repository/gallery_repository.go @@ -70,6 +70,9 @@ type GalleryRepository interface { // limit: 返回数量 // offset: 偏移量(随机生成) GetRandomExhibitions(starID int64, materialType string, excludeIDs []int64, limit, offset int) ([]*InspirationFlowItem, error) + + // GetSlotOwnerUserID 获取展位所有者的用户ID + GetSlotOwnerUserID(slotID int64) (int64, error) } // InspirationFlowItem 灵感瀑布展品项 @@ -555,6 +558,15 @@ func calcSpanByLikes(likes int32) int32 { return 4 } +// GetSlotOwnerUserID 获取展位所有者的用户ID +func (r *galleryRepository) GetSlotOwnerUserID(slotID int64) (int64, error) { + var slot models.BoothSlot + if err := r.db.Where("slot_id = ?", slotID).First(&slot).Error; err != nil { + return 0, err + } + return slot.UserID, nil +} + // ==================== 辅助函数 ==================== // generateHostProfileID 生成 host_profile_id diff --git a/backend/services/taskService/client/user_rpc_client.go b/backend/services/taskService/client/user_rpc_client.go index 43f0303..389cb8d 100644 --- a/backend/services/taskService/client/user_rpc_client.go +++ b/backend/services/taskService/client/user_rpc_client.go @@ -10,8 +10,7 @@ import ( ) type UserServiceClient interface { - UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error) - AddExperience(ctx context.Context, userID, starID int64, delta int64) (int64, error) + UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) GetFanProfile(ctx context.Context, userID, starID int64) (*pbUser.FanProfile, error) } @@ -23,11 +22,11 @@ func NewUserServiceClient(client pbUser.UserSocialService) UserServiceClient { return &userServiceClient{client: client} } -func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error) { +func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) { logger.Logger.Debug("Calling UserService.UpdateCrystalBalance", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("delta", delta)) resp, err := c.client.UpdateCrystalBalance(ctx, &pbUser.UpdateCrystalBalanceRequest{ - UserId: userID, StarId: starID, Delta: delta, + UserId: userID, StarId: starID, Delta: delta, ChangeType: changeType, SourceId: sourceID, Description: description, }) if err != nil { logger.Logger.Error("UserService.UpdateCrystalBalance failed", zap.Error(err)) @@ -40,23 +39,6 @@ func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, st return resp.NewBalance, nil } -func (c *userServiceClient) AddExperience(ctx context.Context, userID, starID int64, delta int64) (int64, error) { - logger.Logger.Debug("Calling UserService.AddExperience", - zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("delta", delta)) - resp, err := c.client.AddExperience(ctx, &pbUser.AddExperienceRequest{ - UserId: userID, StarId: starID, Delta: delta, - }) - if err != nil { - logger.Logger.Error("UserService.AddExperience failed", zap.Error(err)) - return 0, err - } - if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { - logger.Logger.Warn("AddExperience non-zero code", zap.Int32("code", int32(resp.Base.Code))) - return 0, fmt.Errorf("AddExperience failed with code: %d", resp.Base.Code) - } - return resp.NewExperience, nil -} - func (c *userServiceClient) GetFanProfile(ctx context.Context, userID, starID int64) (*pbUser.FanProfile, error) { logger.Logger.Debug("Calling UserService.GetFanProfile", zap.Int64("user_id", userID), zap.Int64("star_id", starID)) diff --git a/backend/services/taskService/model/task_models.go b/backend/services/taskService/model/task_models.go index 9cb0584..ba9e81e 100644 --- a/backend/services/taskService/model/task_models.go +++ b/backend/services/taskService/model/task_models.go @@ -9,7 +9,6 @@ type TaskDefinition struct { Name string `gorm:"column:name;size:100;not null"` Description string `gorm:"column:description;type:text"` CrystalReward int64 `gorm:"column:crystal_reward;default:0"` - ExpReward int64 `gorm:"column:exp_reward;default:0"` SortOrder int `gorm:"column:sort_order;default:0"` IsActive bool `gorm:"column:is_active;default:true"` CreatedAt int64 `gorm:"column:created_at"` @@ -77,7 +76,6 @@ type OnboardingStageConfig struct { Description string `gorm:"column:description;type:text"` RequiredTaskKeys []string `gorm:"column:required_task_keys;type:text;serializer:json"` // 存储为 JSON 字符串 CrystalReward int64 `gorm:"column:crystal_reward;default:0"` - ExpReward int64 `gorm:"column:exp_reward;default:0"` SortOrder int `gorm:"column:sort_order;default:0"` IsActive bool `gorm:"column:is_active;default:true"` CreatedAt int64 `gorm:"column:created_at"` diff --git a/backend/services/taskService/repository/onboarding_repo.go b/backend/services/taskService/repository/onboarding_repo.go index 5b17bf6..d71b13a 100644 --- a/backend/services/taskService/repository/onboarding_repo.go +++ b/backend/services/taskService/repository/onboarding_repo.go @@ -183,15 +183,14 @@ func (r *onboardingRepository) SaveStageConfigs(configs []*model.OnboardingStage zap.Int("stage", cfg.Stage), zap.String("name", cfg.Name), zap.Strings("required_task_keys", cfg.RequiredTaskKeys), - zap.Int64("crystal_reward", cfg.CrystalReward), - zap.Int64("exp_reward", cfg.ExpReward)) + zap.Int64("crystal_reward", cfg.CrystalReward)) cfg.UpdatedAt = now // Use upsert via ON CONFLICT to properly handle JSON serialization upsert := clause.OnConflict{ Columns: []clause.Column{{Name: "stage"}}, DoUpdates: clause.AssignmentColumns([]string{ - "name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at", + "name", "required_task_keys", "crystal_reward", "is_active", "updated_at", }), } if err := r.db.Clauses(upsert).Create(cfg).Error; err != nil { diff --git a/backend/services/taskService/worker/daily_reset_worker.go b/backend/services/taskService/worker/daily_reset_worker.go index cfc160a..c289238 100644 --- a/backend/services/taskService/worker/daily_reset_worker.go +++ b/backend/services/taskService/worker/daily_reset_worker.go @@ -120,7 +120,8 @@ func (w *DailyResetWorker) autoClaimExhibitionRevenue() { for _, record := range records { var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { - _, err := w.userClient.UpdateCrystalBalance(context.Background(), record.UserID, record.StarID, record.CrystalAmount) + _, err := w.userClient.UpdateCrystalBalance(context.Background(), record.UserID, record.StarID, record.CrystalAmount, + "exhibition_revenue", fmt.Sprintf("%d", record.ID), fmt.Sprintf("展示收益 #%d", record.ID)) if err == nil { if err := w.revenueRepo.UpdateRevenueStatus(record.ID, "claimed"); err != nil { logger.Logger.Error("DailyResetWorker: failed to update status to claimed", diff --git a/backend/services/userService/provider/unified_provider.go b/backend/services/userService/provider/unified_provider.go index d9699c5..075a34d 100644 --- a/backend/services/userService/provider/unified_provider.go +++ b/backend/services/userService/provider/unified_provider.go @@ -145,7 +145,7 @@ func (p *UnifiedProvider) UpdateAssetsCount(ctx context.Context, req *pb.UpdateA return p.userProvider.UpdateAssetsCount(ctx, req) } -// AddExperience 增加经验值(内部RPC调用) -func (p *UnifiedProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) { - return p.userProvider.AddExperience(ctx, req) +// AddExhibitionHours 增加用户累计上架时长(内部RPC调用) +func (p *UnifiedProvider) AddExhibitionHours(ctx context.Context, req *pb.AddExhibitionHoursRequest) (*pb.AddExhibitionHoursResponse, error) { + return p.userProvider.AddExhibitionHours(ctx, req) } diff --git a/backend/services/userService/provider/user_provider.go b/backend/services/userService/provider/user_provider.go index a0e1371..b552959 100644 --- a/backend/services/userService/provider/user_provider.go +++ b/backend/services/userService/provider/user_provider.go @@ -852,27 +852,27 @@ func (p *UserProvider) UpdateAssetsCount(ctx context.Context, req *pb.UpdateAsse return resp, nil } -// AddExperience 增加经验值(内部RPC调用) -func (p *UserProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) { - logger.Logger.Info("Received AddExperience request", +// AddExhibitionHours 增加用户累计上架时长(内部RPC调用) +func (p *UserProvider) AddExhibitionHours(ctx context.Context, req *pb.AddExhibitionHoursRequest) (*pb.AddExhibitionHoursResponse, error) { + logger.Logger.Info("Received AddExhibitionHours request", zap.Int64("user_id", req.UserId), zap.Int64("star_id", req.StarId), - zap.Int64("delta", req.Delta), + zap.Int64("exhibition_hours", req.ExhibitionHours), ) // 调用Service层 - resp, err := p.userService.AddExperience(req) + resp, err := p.userService.AddExhibitionHours(req) if err != nil { - logger.Logger.Error("AddExperience failed", + logger.Logger.Error("AddExhibitionHours failed", zap.Int64("user_id", req.UserId), zap.Int64("star_id", req.StarId), - zap.Int64("delta", req.Delta), + zap.Int64("exhibition_hours", req.ExhibitionHours), zap.Error(err), ) // 如果响应为空,构建错误响应 if resp == nil { - resp = &pb.AddExperienceResponse{ + resp = &pb.AddExhibitionHoursResponse{ Base: &pbCommon.BaseResponse{ Code: appErrors.ToStatusCode(err), Message: err.Error(), @@ -884,11 +884,13 @@ func (p *UserProvider) AddExperience(ctx context.Context, req *pb.AddExperienceR return resp, err } - logger.Logger.Info("AddExperience successful", + logger.Logger.Info("AddExhibitionHours successful", zap.Int64("user_id", req.UserId), zap.Int64("star_id", req.StarId), - zap.Int64("delta", req.Delta), - zap.Int64("new_experience", resp.NewExperience), + zap.Int64("exhibition_hours", req.ExhibitionHours), + zap.Int32("new_level", resp.NewLevel), + zap.Int32("level_delta", resp.LevelDelta), + zap.Int64("crystal_reward", resp.CrystalReward), ) return resp, nil diff --git a/backend/services/userService/repository/fan_profile_repository.go b/backend/services/userService/repository/fan_profile_repository.go index b1a9a31..e450d98 100644 --- a/backend/services/userService/repository/fan_profile_repository.go +++ b/backend/services/userService/repository/fan_profile_repository.go @@ -2,13 +2,18 @@ package repository import ( "errors" + "fmt" "math" "strings" + "time" "github.com/topfans/backend/pkg/database" appErrors "github.com/topfans/backend/pkg/errors" + "github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/pkg/models" + "go.uber.org/zap" "gorm.io/gorm" + "gorm.io/gorm/clause" ) // contains 检查字符串是否包含子串(不区分大小写) @@ -79,8 +84,15 @@ type FanProfileRepository interface { // UpdateSocial 更新好友数量(social字段) UpdateSocial(userID, starID int64, delta int32) (int32, error) - // UpdateCrystalBalance 更新水晶余额 - UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error) + // UpdateCrystalBalance 更新水晶余额(支持流水记录) + // changeType: 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust + // sourceID: 关联业务ID + // description: 可读描述 + UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) + + // AddExhibitionHours 增加用户累计上架时长并同步等级(事务性) + // 返回: newLevel, levelDelta, crystalReward, error + AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) // UpdateExperience 更新经验值 UpdateExperience(userID, starID int64, delta int64) (int64, error) @@ -369,10 +381,13 @@ func (r *fanProfileRepository) UpdateSocial(userID, starID int64, delta int32) ( return newSocial, nil } -// UpdateCrystalBalance 更新水晶余额 +// UpdateCrystalBalance 更新水晶余额(支持流水记录) // delta: 变化量,正数表示增加,负数表示减少 +// changeType: 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust +// sourceID: 关联业务ID +// description: 可读描述 // 返回: 更新后的水晶余额 -func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error) { +func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) { if userID <= 0 { return 0, errors.New("user_id must be greater than 0") } @@ -384,9 +399,10 @@ func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta // 使用事务确保原子性 var newBalance int64 err := r.db.Transaction(func(tx *gorm.DB) error { - // 先查询当前的 crystal_balance 值 + // 1. SELECT FOR UPDATE 加行锁(悲观锁策略) var profile models.FanProfile - if err := tx.Where("user_id = ? AND star_id = ?", userID, starID). + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("user_id = ? AND star_id = ?", userID, starID). First(&profile).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return appErrors.ErrFanProfileNotFound @@ -394,15 +410,32 @@ func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta return err } - // 计算新值 - newBalance = profile.CrystalBalance + delta + // 2. 计算余额变化 + balanceBefore := profile.CrystalBalance + newBalance = balanceBefore + delta // 确保不会小于 0 if newBalance < 0 { newBalance = 0 } - // 更新 crystal_balance 字段 + // 3. 写入水晶流水(复式记账,包含余额快照) + crystalRecord := &models.CrystalTransactionRecord{ + UserID: userID, + StarID: starID, + ChangeType: changeType, + Delta: delta, + BalanceBefore: balanceBefore, + BalanceAfter: newBalance, + SourceID: sourceID, + Description: description, + CreatedAt: time.Now().UnixMilli(), + } + if err := tx.Create(crystalRecord).Error; err != nil { + return err + } + + // 4. 更新 crystal_balance 字段 if err := tx.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Update("crystal_balance", newBalance).Error; err != nil { @@ -416,9 +449,222 @@ func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta return 0, err } -return newBalance, nil + return newBalance, nil } +// GetOrCreateExhibitionHours 获取或创建用户累计上架时长记录 +func (r *fanProfileRepository) GetOrCreateExhibitionHours(tx *gorm.DB, userID, starID int64) (*models.UserExhibitionHours, error) { + var existing models.UserExhibitionHours + err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&existing).Error + + if err == nil { + return &existing, nil + } + + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + // 创建新记录 + now := time.Now().UnixMilli() + newRecord := &models.UserExhibitionHours{ + UserID: userID, + StarID: starID, + TotalExhibitionHours: 0, + UpdatedAt: now, + } + + if err := tx.Create(newRecord).Error; err != nil { + return nil, err + } + + return newRecord, nil +} + +// CalculateLevelFromExhibitionHours 根据累计上架时长计算等级 +func CalculateLevelFromExhibitionHours(totalHours int64) int32 { + db := database.GetDB() + if db == nil { + return 1 + } + + var threshold models.LevelThreshold + err := db.Where("max_exhibition_hours <= ?", totalHours). + Order("level DESC"). + First(&threshold).Error + + if err != nil || threshold.Level == 0 { + return 1 + } + + return threshold.Level +} + +// GetLevelCap 获取当前等级上限 +func GetLevelCap() int32 { + db := database.GetDB() + if db == nil { + return 20 + } + + var config models.LevelCapConfig + err := db.First(&config).Error + if err != nil { + return 20 // 默认20级 + } + + return config.MaxLevel +} + +// AddExhibitionHours 增加用户累计上架时长并同步等级(事务性) +// 返回: newLevel, levelDelta, crystalReward, error +func (r *fanProfileRepository) AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) { + var result struct { + OldLevel int32 + NewLevel int32 + CrystalReward int64 + } + + err := r.db.Transaction(func(tx *gorm.DB) error { + // 1. 获取或创建累计时长记录 + exhibitionHours, err := r.GetOrCreateExhibitionHours(tx, userID, starID) + if err != nil { + return err + } + + // 2. 原子性累加时长(避免竞态条件) + now := time.Now().UnixMilli() + if err := tx.Model(&models.UserExhibitionHours{}). + Where("user_id = ? AND star_id = ?", userID, starID). + Updates(map[string]interface{}{ + "total_exhibition_hours": gorm.Expr("total_exhibition_hours + ?", hours), + "updated_at": now, + }).Error; err != nil { + return err + } + + // 重新查询更新后的时长 + if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(exhibitionHours).Error; err != nil { + return err + } + + // 3. 获取当前等级上限 + maxLevel := GetLevelCap() + + // 4. 计算新等级(基于累计时长) + newLevel := CalculateLevelFromExhibitionHours(exhibitionHours.TotalExhibitionHours) + if newLevel > maxLevel { + newLevel = maxLevel + } + + // 5. SELECT FOR UPDATE 加行锁获取粉丝档案当前等级 + var profile models.FanProfile + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil { + return err + } + + result.OldLevel = profile.Level + result.NewLevel = newLevel + + // 6. 如有升级,发放奖励 + if newLevel > profile.Level { + // 查询升级奖励 + rewards, err := r.getLevelUpRewards(tx, newLevel) + if err != nil { + logger.Logger.Warn("Failed to get level up rewards", + zap.Int64("user_id", userID), + zap.Int64("star_id", starID), + zap.Int32("level", newLevel), + zap.Error(err)) + } + + // 计算水晶奖励总额 + var crystalReward int64 = 0 + var likeBetCountReward int32 = 0 + for _, reward := range rewards { + if reward.RewardType == "crystal" && reward.IsEnabled { + crystalReward += reward.RewardValue + } + if reward.RewardType == "like_bet_count" && reward.IsEnabled { + likeBetCountReward += int32(reward.RewardValue) + } + } + + result.CrystalReward = crystalReward + + // 发放升级奖励(水晶 + 点赞押注次数) + if crystalReward > 0 || likeBetCountReward > 0 { + balanceBefore := profile.CrystalBalance + balanceAfter := balanceBefore + crystalReward + + // 写入水晶流水(只有水晶有流水) + if crystalReward > 0 { + crystalRecord := &models.CrystalTransactionRecord{ + UserID: userID, + StarID: starID, + ChangeType: "level_up_bonus", + Delta: crystalReward, + BalanceBefore: balanceBefore, + BalanceAfter: balanceAfter, + SourceID: "", + Description: fmt.Sprintf("升级到%d级奖励", newLevel), + CreatedAt: time.Now().UnixMilli(), + } + if err := tx.Create(crystalRecord).Error; err != nil { + return err + } + } + + // 更新 FanProfile(等级 + 水晶余额 + 点赞押注次数) + updates := map[string]interface{}{ + "level": newLevel, + } + if crystalReward > 0 { + updates["crystal_balance"] = balanceAfter + } + if likeBetCountReward > 0 { + updates["like_bet_count"] = gorm.Expr("like_bet_count + ?", likeBetCountReward) + } + if err := tx.Model(&profile).Updates(updates).Error; err != nil { + return err + } + } else { + // 只更新等级 + if err := tx.Model(&profile).Update("level", newLevel).Error; err != nil { + return err + } + } + + logger.Logger.Info("Level up from exhibition hours", + zap.Int64("user_id", userID), + zap.Int64("star_id", starID), + zap.Int32("old_level", profile.Level), + zap.Int32("new_level", newLevel), + zap.Int64("crystal_reward", crystalReward), + zap.Int32("like_bet_count_reward", likeBetCountReward), + zap.Int64("total_hours", exhibitionHours.TotalExhibitionHours)) + } + + return nil + }) + + if err != nil { + return 0, 0, 0, err + } + + levelDelta := result.NewLevel - result.OldLevel + return result.NewLevel, levelDelta, result.CrystalReward, nil +} + +// getLevelUpRewards 获取指定等级的升级奖励 +func (r *fanProfileRepository) getLevelUpRewards(tx *gorm.DB, level int32) ([]*models.LevelUpRewardConfig, error) { + var rewards []*models.LevelUpRewardConfig + err := tx.Where("level = ? AND is_enabled = ?", level, true).Find(&rewards).Error + return rewards, err +} + +// UpdateExperience 更新经验值(同时自动更新等级) // UpdateExperience 更新经验值(同时自动更新等级) // delta: 变化量,正数表示增加,负数表示减少 // 返回: 更新后的经验值 diff --git a/docs/specs/2026-04-15-economic-system-design.md b/docs/specs/2026-04-15-economic-system-design.md index 46092bc..70319a8 100644 --- a/docs/specs/2026-04-15-economic-system-design.md +++ b/docs/specs/2026-04-15-economic-system-design.md @@ -987,13 +987,12 @@ ON CONFLICT (level) DO NOTHING; -- 升级奖励配置表 CREATE TABLE IF NOT EXISTS level_up_reward_config ( id BIGSERIAL PRIMARY KEY, - level INT NOT NULL UNIQUE, - level INT NOT NULL, - reward_type VARCHAR(50) NOT NULL, - reward_value BIGINT NOT NULL DEFAULT 0, - is_enabled BOOLEAN DEFAULT true, + level INT NOT NULL, -- 等级 2-20+ + reward_type VARCHAR(50) NOT NULL, -- 奖励类型:crystal(水晶) / like_bet_count(点赞押注数) / exhibition_count(上架藏品数) / badge(勋章) / 其他 + reward_value BIGINT NOT NULL DEFAULT 0, -- 奖励值 + is_enabled BOOLEAN DEFAULT true, -- 功能开关 updated_at BIGINT NOT NULL, - UNIQUE(level, reward_type) + UNIQUE(level, reward_type) -- 同一等级同类型奖励唯一 ); -- 插入初始数据示例(可由运营后台动态调整) @@ -1080,7 +1079,9 @@ CREATE TABLE IF NOT EXISTS user_dazi_level ( ### 13.1 铸爱次数与消耗水晶 -用户铸造藏品时,每次铸造消耗水晶数根据累计铸爱次数动态计算。**达到第10次(消耗1024水晶)后重置铸爱次数,重新从第1次开始。** +用户铸造藏品时,每次铸造消耗水晶数根据累计铸爱次数动态计算。**达到第10次(消耗1024水晶)后重置铸爱次数为0,重新从第1次开始累积。** + +> **重要:** 铸爱次数重置仅清除计数,**永久收益提升(revenue_boost_bps)不清除**,会持续累加。例如用户第9次触发小保底(+500 bps = +5%),第10次触发大保底(+500 bps = +5%),则 `revenue_boost_bps = 1000`(即 +10% 永久收益提升),铸爱次数重置为0,但收益提升保留。 | 铸爱次数 | 消耗水晶数 | 备注 | |---------|-----------|------| @@ -1092,10 +1093,10 @@ CREATE TABLE IF NOT EXISTS user_dazi_level ( | 6 | 64 | | | 7 | 128 | | | 8 | 256 | 接近1天收益 | -| 9 | 512 | 20%概率,获得5%永久收益提升(俗称小保底) | -| 10 | 1024 | 接近3天收益,建议为最高值;100%概率,获得5%永久收益提升(俗称大保底) | +| 9 | 512 | 20%概率,获得500 bps(+5%)永久收益提升(俗称小保底) | +| 10 | 1024 | 接近3天收益,建议为最高值;100%概率,获得500 bps(+5%)永久收益提升(俗称大保底) | -> **注:** 消耗水晶数上限为1024,达到第10次后重置铸爱次数。 +> **注:** 消耗水晶数上限为1024,达到第10次后铸爱次数重置为0,永久收益提升保留。 ### 13.2 数据库设计 @@ -1107,7 +1108,7 @@ CREATE TABLE IF NOT EXISTS user_dazi_level ( - `cost_crystal`:消耗水晶数(上限1024) - `probability`:保底触发概率(0-100),0=不触发 - `reward_type`:触发奖励类型(收益提升等) -- `reward_value`:奖励值(如5代表5%) +- `reward_value`:奖励值,单位为 bps(basis points),如 500 代表 +5% 永久收益提升 ```sql CREATE TABLE mint_cost_config ( @@ -1116,7 +1117,7 @@ CREATE TABLE mint_cost_config ( cost_crystal BIGINT NOT NULL, probability BIGINT DEFAULT 0, reward_type VARCHAR(50) DEFAULT NULL, - reward_value BIGINT DEFAULT 0, + reward_value BIGINT DEFAULT 0, -- 单位:bps(基点),500=+5% description VARCHAR(255), updated_at BIGINT NOT NULL ); @@ -1131,8 +1132,8 @@ INSERT INTO mint_cost_config (mint_count, cost_crystal, probability, reward_type (6, 64, 0, NULL, 0, '', UNIX_MILLIS()), (7, 128, 0, NULL, 0, '', UNIX_MILLIS()), (8, 256, 0, NULL, 0, '接近1天收益', UNIX_MILLIS()), -(9, 512, 20, '收益提升', 5, '20%概率,获得5%永久收益提升(俗称小保底)', UNIX_MILLIS()), -(10, 1024, 100, '收益提升', 5, '接近3天收益,建议为最高值;100%概率,获得5%永久收益提升(俗称大保底)', UNIX_MILLIS()); +(9, 512, 20, '收益提升', 500, '20%概率,获得500 bps(+5%)永久收益提升(俗称小保底)', UNIX_MILLIS()), +(10, 1024, 100, '收益提升', 500, '接近3天收益,建议为最高值;100%概率,获得500 bps(+5%)永久收益提升(俗称大保底)', UNIX_MILLIS()); ``` #### 13.2.2 用户累计铸爱次数表 (user_mint_count) @@ -1162,7 +1163,7 @@ func (s *MintService) CreateMintOrder(userID, starID int64, ...) (orderID string // 3. 检查是否触发保底 var boost int64 = 0 if cost.Probability > 0 && rand.Intn(100) < cost.Probability { - boost = cost.RewardValue // 如5代表5% + boost = cost.RewardValue // 单位:bps,如 500 代表 +5% } // 4. 扣水晶