diff --git a/README.md b/README.md index c1114b8..4a8893e 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # TopFans +密码:R251Y>Y8inL_BM=W \ No newline at end of file diff --git a/backend/docs/服务器部署指南.md b/backend/docs/服务器部署指南.md index 7817a75..43423c2 100644 --- a/backend/docs/服务器部署指南.md +++ b/backend/docs/服务器部署指南.md @@ -53,7 +53,7 @@ ``` ssh root@101.132.250.62 ->n73qBnCja-,#VF+Wq +R251Y>Y8inL_BM=W ``` ### 1.1 更新系统包 diff --git a/backend/gateway/controller/task_controller.go b/backend/gateway/controller/task_controller.go index 38d6e88..17d4d9b 100644 --- a/backend/gateway/controller/task_controller.go +++ b/backend/gateway/controller/task_controller.go @@ -289,7 +289,8 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) { userID, _ := c.Get("user_id") var req struct { - TaskKey string `json:"task_key"` + TaskKey string `json:"task_key"` + Stages []*pbTask.OnboardingStage `json:"stages"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "请求参数错误") @@ -303,6 +304,7 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) { logger.Logger.Info("CompleteGuide request", zap.Int64("user_id", userID.(int64)), zap.String("task_key", req.TaskKey), + zap.Int("stages_count", len(req.Stages)), ) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -315,6 +317,7 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) { resp, err := ctrl.taskMobileService.CompleteGuide(ctx, &pbTask.CompleteGuideRequest{ TaskKey: req.TaskKey, + Stages: req.Stages, }) if err != nil { @@ -476,7 +479,9 @@ func (ctrl *TaskController) ClaimOnboardingReward(c *gin.Context) { } response.Success(c, map[string]interface{}{ - "success": resp.Success, + "success": resp.Success, + "crystal_balance": resp.CrystalBalance, + "experience": resp.Experience, }) } diff --git a/backend/pkg/proto/task/task.pb.go b/backend/pkg/proto/task/task.pb.go index 30ecf69..ed997ca 100644 --- a/backend/pkg/proto/task/task.pb.go +++ b/backend/pkg/proto/task/task.pb.go @@ -596,16 +596,17 @@ func (x *ClaimAllDailyTasksResponse) GetClaimedTaskKeys() []string { } type OnboardingStage struct { - state protoimpl.MessageState `protogen:"open.v1"` - Stage int32 `protobuf:"varint,1,opt,name=stage,proto3" json:"stage,omitempty"` - 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"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Stage int32 `protobuf:"varint,1,opt,name=stage,proto3" json:"stage,omitempty"` + 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"` // 该阶段所有任务是否完成 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OnboardingStage) Reset() { @@ -687,9 +688,17 @@ func (x *OnboardingStage) GetIsCurrent() bool { return false } +func (x *OnboardingStage) GetAllTasksCompleted() bool { + if x != nil { + return x.AllTasksCompleted + } + return false +} + type CompleteGuideRequest struct { state protoimpl.MessageState `protogen:"open.v1"` TaskKey string `protobuf:"bytes,1,opt,name=task_key,json=taskKey,proto3" json:"task_key,omitempty"` + Stages []*OnboardingStage `protobuf:"bytes,2,rep,name=stages,proto3" json:"stages,omitempty"` // 前端传入的阶段配置,首次调用时存储 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -731,6 +740,13 @@ func (x *CompleteGuideRequest) GetTaskKey() string { return "" } +func (x *CompleteGuideRequest) GetStages() []*OnboardingStage { + if x != nil { + return x.Stages + } + return nil +} + type CompleteGuideResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` @@ -1076,11 +1092,13 @@ func (x *ClaimOnboardingRewardRequest) GetStage() int32 { } type ClaimOnboardingRewardResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - 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"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + 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 } func (x *ClaimOnboardingRewardResponse) Reset() { @@ -1127,6 +1145,20 @@ func (x *ClaimOnboardingRewardResponse) GetSuccess() bool { return false } +func (x *ClaimOnboardingRewardResponse) GetCrystalBalance() string { + if x != nil { + return x.CrystalBalance + } + 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"` @@ -1910,7 +1942,7 @@ const file_task_proto_rawDesc = "" + "\n" + "experience\x18\x04 \x01(\x03R\n" + "experience\x12*\n" + - "\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\xe6\x01\n" + + "\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\x96\x02\n" + "\x0fOnboardingStage\x12\x14\n" + "\x05stage\x18\x01 \x01(\x05R\x05stage\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12,\n" + @@ -1920,9 +1952,11 @@ const file_task_proto_rawDesc = "" + "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\"1\n" + + "is_current\x18\a \x01(\bR\tisCurrent\x12.\n" + + "\x13all_tasks_completed\x18\b \x01(\bR\x11allTasksCompleted\"h\n" + "\x14CompleteGuideRequest\x12\x19\n" + - "\btask_key\x18\x01 \x01(\tR\ataskKey\"\xd6\x01\n" + + "\btask_key\x18\x01 \x01(\tR\ataskKey\x125\n" + + "\x06stages\x18\x02 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"\xd6\x01\n" + "\x15CompleteGuideResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x17\n" + "\auser_id\x18\x02 \x01(\x03R\x06userId\x12#\n" + @@ -1944,10 +1978,14 @@ 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\"k\n" + + "\x05stage\x18\x01 \x01(\x05R\x05stage\"\xb4\x01\n" + "\x1dClaimOnboardingRewardResponse\x120\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\"\xe2\x02\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" + "\x15ExhibitionRevenueItem\x12\x0e\n" + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" + "\astar_id\x18\x02 \x01(\x03R\x06starId\x12#\n" + @@ -2071,50 +2109,51 @@ var file_task_proto_depIdxs = []int32{ 29, // 2: topfans.task.ReportEventResponse.base:type_name -> topfans.common.BaseResponse 29, // 3: topfans.task.ClaimDailyTaskResponse.base:type_name -> topfans.common.BaseResponse 29, // 4: topfans.task.ClaimAllDailyTasksResponse.base:type_name -> topfans.common.BaseResponse - 29, // 5: topfans.task.CompleteGuideResponse.base:type_name -> topfans.common.BaseResponse - 9, // 6: topfans.task.CompleteGuideResponse.stages:type_name -> topfans.task.OnboardingStage - 29, // 7: topfans.task.GetOnboardingStatusResponse.base:type_name -> topfans.common.BaseResponse - 9, // 8: topfans.task.GetOnboardingStatusResponse.stages:type_name -> topfans.task.OnboardingStage - 29, // 9: topfans.task.AdvanceStageResponse.base:type_name -> topfans.common.BaseResponse - 9, // 10: topfans.task.AdvanceStageResponse.stages:type_name -> topfans.task.OnboardingStage - 29, // 11: topfans.task.ClaimOnboardingRewardResponse.base:type_name -> topfans.common.BaseResponse - 29, // 12: topfans.task.GetExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse - 18, // 13: topfans.task.GetExhibitionRevenueResponse.items:type_name -> topfans.task.ExhibitionRevenueItem - 29, // 14: topfans.task.ClaimExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse - 29, // 15: topfans.task.ClaimAllExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse - 29, // 16: topfans.task.InitUserTasksResponse.base:type_name -> topfans.common.BaseResponse - 29, // 17: topfans.task.OnExhibitionCompletedResponse.base:type_name -> topfans.common.BaseResponse - 1, // 18: topfans.task.TaskMobileService.GetDailyTasks:input_type -> topfans.task.GetDailyTasksRequest - 3, // 19: topfans.task.TaskMobileService.ReportEvent:input_type -> topfans.task.ReportEventRequest - 5, // 20: topfans.task.TaskMobileService.ClaimDailyTask:input_type -> topfans.task.ClaimDailyTaskRequest - 7, // 21: topfans.task.TaskMobileService.ClaimAllDailyTasks:input_type -> topfans.task.ClaimAllDailyTasksRequest - 10, // 22: topfans.task.TaskMobileService.CompleteGuide:input_type -> topfans.task.CompleteGuideRequest - 12, // 23: topfans.task.TaskMobileService.GetOnboardingStatus:input_type -> topfans.task.GetOnboardingStatusRequest - 14, // 24: topfans.task.TaskMobileService.AdvanceStage:input_type -> topfans.task.AdvanceStageRequest - 16, // 25: topfans.task.TaskMobileService.ClaimOnboardingReward:input_type -> topfans.task.ClaimOnboardingRewardRequest - 19, // 26: topfans.task.TaskMobileService.GetExhibitionRevenue:input_type -> topfans.task.GetExhibitionRevenueRequest - 21, // 27: topfans.task.TaskMobileService.ClaimExhibitionRevenue:input_type -> topfans.task.ClaimExhibitionRevenueRequest - 23, // 28: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:input_type -> topfans.task.ClaimAllExhibitionRevenueRequest - 25, // 29: topfans.task.TaskInternalService.InitUserTasks:input_type -> topfans.task.InitUserTasksRequest - 27, // 30: topfans.task.TaskInternalService.OnExhibitionCompleted:input_type -> topfans.task.OnExhibitionCompletedRequest - 2, // 31: topfans.task.TaskMobileService.GetDailyTasks:output_type -> topfans.task.GetDailyTasksResponse - 4, // 32: topfans.task.TaskMobileService.ReportEvent:output_type -> topfans.task.ReportEventResponse - 6, // 33: topfans.task.TaskMobileService.ClaimDailyTask:output_type -> topfans.task.ClaimDailyTaskResponse - 8, // 34: topfans.task.TaskMobileService.ClaimAllDailyTasks:output_type -> topfans.task.ClaimAllDailyTasksResponse - 11, // 35: topfans.task.TaskMobileService.CompleteGuide:output_type -> topfans.task.CompleteGuideResponse - 13, // 36: topfans.task.TaskMobileService.GetOnboardingStatus:output_type -> topfans.task.GetOnboardingStatusResponse - 15, // 37: topfans.task.TaskMobileService.AdvanceStage:output_type -> topfans.task.AdvanceStageResponse - 17, // 38: topfans.task.TaskMobileService.ClaimOnboardingReward:output_type -> topfans.task.ClaimOnboardingRewardResponse - 20, // 39: topfans.task.TaskMobileService.GetExhibitionRevenue:output_type -> topfans.task.GetExhibitionRevenueResponse - 22, // 40: topfans.task.TaskMobileService.ClaimExhibitionRevenue:output_type -> topfans.task.ClaimExhibitionRevenueResponse - 24, // 41: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:output_type -> topfans.task.ClaimAllExhibitionRevenueResponse - 26, // 42: topfans.task.TaskInternalService.InitUserTasks:output_type -> topfans.task.InitUserTasksResponse - 28, // 43: topfans.task.TaskInternalService.OnExhibitionCompleted:output_type -> topfans.task.OnExhibitionCompletedResponse - 31, // [31:44] is the sub-list for method output_type - 18, // [18:31] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 9, // 5: topfans.task.CompleteGuideRequest.stages:type_name -> topfans.task.OnboardingStage + 29, // 6: topfans.task.CompleteGuideResponse.base:type_name -> topfans.common.BaseResponse + 9, // 7: topfans.task.CompleteGuideResponse.stages:type_name -> topfans.task.OnboardingStage + 29, // 8: topfans.task.GetOnboardingStatusResponse.base:type_name -> topfans.common.BaseResponse + 9, // 9: topfans.task.GetOnboardingStatusResponse.stages:type_name -> topfans.task.OnboardingStage + 29, // 10: topfans.task.AdvanceStageResponse.base:type_name -> topfans.common.BaseResponse + 9, // 11: topfans.task.AdvanceStageResponse.stages:type_name -> topfans.task.OnboardingStage + 29, // 12: topfans.task.ClaimOnboardingRewardResponse.base:type_name -> topfans.common.BaseResponse + 29, // 13: topfans.task.GetExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse + 18, // 14: topfans.task.GetExhibitionRevenueResponse.items:type_name -> topfans.task.ExhibitionRevenueItem + 29, // 15: topfans.task.ClaimExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse + 29, // 16: topfans.task.ClaimAllExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse + 29, // 17: topfans.task.InitUserTasksResponse.base:type_name -> topfans.common.BaseResponse + 29, // 18: topfans.task.OnExhibitionCompletedResponse.base:type_name -> topfans.common.BaseResponse + 1, // 19: topfans.task.TaskMobileService.GetDailyTasks:input_type -> topfans.task.GetDailyTasksRequest + 3, // 20: topfans.task.TaskMobileService.ReportEvent:input_type -> topfans.task.ReportEventRequest + 5, // 21: topfans.task.TaskMobileService.ClaimDailyTask:input_type -> topfans.task.ClaimDailyTaskRequest + 7, // 22: topfans.task.TaskMobileService.ClaimAllDailyTasks:input_type -> topfans.task.ClaimAllDailyTasksRequest + 10, // 23: topfans.task.TaskMobileService.CompleteGuide:input_type -> topfans.task.CompleteGuideRequest + 12, // 24: topfans.task.TaskMobileService.GetOnboardingStatus:input_type -> topfans.task.GetOnboardingStatusRequest + 14, // 25: topfans.task.TaskMobileService.AdvanceStage:input_type -> topfans.task.AdvanceStageRequest + 16, // 26: topfans.task.TaskMobileService.ClaimOnboardingReward:input_type -> topfans.task.ClaimOnboardingRewardRequest + 19, // 27: topfans.task.TaskMobileService.GetExhibitionRevenue:input_type -> topfans.task.GetExhibitionRevenueRequest + 21, // 28: topfans.task.TaskMobileService.ClaimExhibitionRevenue:input_type -> topfans.task.ClaimExhibitionRevenueRequest + 23, // 29: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:input_type -> topfans.task.ClaimAllExhibitionRevenueRequest + 25, // 30: topfans.task.TaskInternalService.InitUserTasks:input_type -> topfans.task.InitUserTasksRequest + 27, // 31: topfans.task.TaskInternalService.OnExhibitionCompleted:input_type -> topfans.task.OnExhibitionCompletedRequest + 2, // 32: topfans.task.TaskMobileService.GetDailyTasks:output_type -> topfans.task.GetDailyTasksResponse + 4, // 33: topfans.task.TaskMobileService.ReportEvent:output_type -> topfans.task.ReportEventResponse + 6, // 34: topfans.task.TaskMobileService.ClaimDailyTask:output_type -> topfans.task.ClaimDailyTaskResponse + 8, // 35: topfans.task.TaskMobileService.ClaimAllDailyTasks:output_type -> topfans.task.ClaimAllDailyTasksResponse + 11, // 36: topfans.task.TaskMobileService.CompleteGuide:output_type -> topfans.task.CompleteGuideResponse + 13, // 37: topfans.task.TaskMobileService.GetOnboardingStatus:output_type -> topfans.task.GetOnboardingStatusResponse + 15, // 38: topfans.task.TaskMobileService.AdvanceStage:output_type -> topfans.task.AdvanceStageResponse + 17, // 39: topfans.task.TaskMobileService.ClaimOnboardingReward:output_type -> topfans.task.ClaimOnboardingRewardResponse + 20, // 40: topfans.task.TaskMobileService.GetExhibitionRevenue:output_type -> topfans.task.GetExhibitionRevenueResponse + 22, // 41: topfans.task.TaskMobileService.ClaimExhibitionRevenue:output_type -> topfans.task.ClaimExhibitionRevenueResponse + 24, // 42: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:output_type -> topfans.task.ClaimAllExhibitionRevenueResponse + 26, // 43: topfans.task.TaskInternalService.InitUserTasks:output_type -> topfans.task.InitUserTasksResponse + 28, // 44: topfans.task.TaskInternalService.OnExhibitionCompleted:output_type -> topfans.task.OnExhibitionCompletedResponse + 32, // [32:45] is the sub-list for method output_type + 19, // [19:32] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_task_proto_init() } diff --git a/backend/proto/task.proto b/backend/proto/task.proto index 6ba5c48..79e0354 100644 --- a/backend/proto/task.proto +++ b/backend/proto/task.proto @@ -77,10 +77,12 @@ message OnboardingStage { int64 exp_reward = 5; string status = 6; // pending/completed/in_progress bool is_current = 7; + bool all_tasks_completed = 8; // 该阶段所有任务是否完成 } message CompleteGuideRequest { string task_key = 1; + repeated OnboardingStage stages = 2; // 前端传入的阶段配置,首次调用时存储 } message CompleteGuideResponse { @@ -119,6 +121,8 @@ message ClaimOnboardingRewardRequest { 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/services/assetService/repository/ranking_repository.go b/backend/services/assetService/repository/ranking_repository.go index ad08223..88b8355 100644 --- a/backend/services/assetService/repository/ranking_repository.go +++ b/backend/services/assetService/repository/ranking_repository.go @@ -118,8 +118,9 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str Where("exhibitions.expire_at > ?", now). Where("fan_profiles.star_id = ?", starID) case "month": - // 本月:统计本月内的点赞数 - db = db.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth) + // 本月:本月内开始的展览,按点赞数排序 + db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). + Where("exhibitions.start_time >= ?", startOfMonth) case "total": // 全部:直接使用 assets 表的 like_count,无需额外条件 } @@ -140,7 +141,9 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str Where("exhibitions.expire_at > ?", now). Where("host_fp.star_id = ?", starID) case "month": - countDB = countDB.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth) + // 本月:本月内开始的展览 + countDB = countDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). + Where("exhibitions.start_time >= ?", startOfMonth) } if err := countDB.Count(&total).Error; err != nil { return nil, 0, err @@ -190,8 +193,9 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str Where("exhibitions.expire_at > ?", now). Where("host_fp.star_id = ?", starID) case "month": - // 本月:关联 asset_likes 表,筛选本月有点赞的藏品 - db = db.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth) + // 本月:本月内开始的展览 + db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). + Where("exhibitions.start_time >= ?", startOfMonth) } // 获取用户在该star下点赞数最高的藏品 @@ -232,7 +236,9 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str Where("exhibitions.expire_at > ?", now). Where("host_fp.star_id = ?", starID) case "month": - rankingDB = rankingDB.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth) + // 本月:本月内开始的展览 + rankingDB = rankingDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id"). + Where("exhibitions.start_time >= ?", startOfMonth) } if err := rankingDB.Count(&rank).Error; err != nil { diff --git a/backend/services/taskService/provider/task_mobile_provider.go b/backend/services/taskService/provider/task_mobile_provider.go index 35a5639..8a38f58 100644 --- a/backend/services/taskService/provider/task_mobile_provider.go +++ b/backend/services/taskService/provider/task_mobile_provider.go @@ -159,11 +159,12 @@ func (p *TaskMobileProvider) CompleteGuide(ctx context.Context, req *pb.Complete return &pb.CompleteGuideResponse{}, nil } - logger.Logger.Debug("CompleteGuide", + logger.Logger.Info("CompleteGuide", zap.Int64("user_id", userID), - zap.String("task_key", req.TaskKey)) + zap.String("task_key", req.TaskKey), + zap.Int("stages_count", len(req.Stages))) - return p.onboardingSvc.CompleteGuide(ctx, userID, req.TaskKey) + return p.onboardingSvc.CompleteGuide(ctx, userID, req.TaskKey, req.Stages) } func (p *TaskMobileProvider) GetOnboardingStatus(ctx context.Context, req *pb.GetOnboardingStatusRequest) (*pb.GetOnboardingStatusResponse, error) { @@ -202,11 +203,6 @@ func (p *TaskMobileProvider) ClaimOnboardingReward(ctx context.Context, req *pb. return &pb.ClaimOnboardingRewardResponse{Success: false}, nil } - logger.Logger.Debug("ClaimOnboardingReward", - zap.Int64("user_id", userID), - zap.Int64("star_id", starID), - zap.Int32("stage", req.Stage)) - return p.onboardingSvc.ClaimOnboardingReward(ctx, userID, starID, req.Stage) } diff --git a/backend/services/taskService/repository/onboarding_repo.go b/backend/services/taskService/repository/onboarding_repo.go index 2893801..b6dd042 100644 --- a/backend/services/taskService/repository/onboarding_repo.go +++ b/backend/services/taskService/repository/onboarding_repo.go @@ -13,6 +13,7 @@ import ( type OnboardingRepository interface { GetOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error) GetOrCreateOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error) + GetUserOnboardingStatuses(userID int64) ([]*model.UserOnboardingStatus, error) UpdateOnboardingStatus(status *model.UserOnboardingStatus) error UpdateOnboardingProgress(progress *model.UserOnboardingProgress) error GetUserOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error) @@ -20,6 +21,8 @@ type OnboardingRepository interface { ListActiveStageConfigs() ([]*model.OnboardingStageConfig, error) ListUserOnboardingProgressByUser(userID int64) ([]*model.UserOnboardingProgress, error) GetStageConfig(stage int64) (*model.OnboardingStageConfig, error) + SaveStageConfigs(configs []*model.OnboardingStageConfig) error + CountStageConfigs() (int64, error) } type onboardingRepository struct { @@ -65,6 +68,16 @@ func (r *onboardingRepository) GetOrCreateOnboardingStatus(userID int64, starID return &status, nil } +func (r *onboardingRepository) GetUserOnboardingStatuses(userID int64) ([]*model.UserOnboardingStatus, error) { + var statuses []*model.UserOnboardingStatus + err := r.db.Where("user_id = ?", userID).Order("star_id DESC").Find(&statuses).Error + if err != nil { + logger.Logger.Error("Failed to GetUserOnboardingStatuses", zap.Int64("user_id", userID), zap.Error(err)) + return nil, err + } + return statuses, nil +} + func (r *onboardingRepository) UpdateOnboardingStatus(status *model.UserOnboardingStatus) error { status.UpdatedAt = time.Now().Unix() if err := r.db.Save(status).Error; err != nil { @@ -139,9 +152,84 @@ func (r *onboardingRepository) ListUserOnboardingProgressByUser(userID int64) ([ func (r *onboardingRepository) GetStageConfig(stage int64) (*model.OnboardingStageConfig, error) { var config model.OnboardingStageConfig + logger.Logger.Info("GetStageConfig: querying", + zap.Int64("stage", stage)) err := r.db.Where("stage = ? AND is_active = ?", stage, true).First(&config).Error if err != nil { + logger.Logger.Error("GetStageConfig: query failed", + zap.Int64("stage", stage), + zap.Error(err)) return nil, err } + logger.Logger.Info("GetStageConfig: found", + zap.Int64("stage", stage), + zap.String("name", config.Name)) return &config, nil } + +func (r *onboardingRepository) SaveStageConfigs(configs []*model.OnboardingStageConfig) error { + if len(configs) == 0 { + logger.Logger.Warn("SaveStageConfigs: configs is empty, returning") + return nil + } + now := time.Now().Unix() + + logger.Logger.Info("SaveStageConfigs: starting save", + zap.Int("count", len(configs))) + + for _, cfg := range configs { + logger.Logger.Info("SaveStageConfigs: processing config", + 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)) + + cfg.UpdatedAt = now + + // First try to update existing record + // Use Select to specify fields so GORM properly handles JSON serialization + result := r.db.Model(&model.OnboardingStageConfig{}). + Where("stage = ?", cfg.Stage). + Select("name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at"). + Updates(cfg) + + if result.Error != nil { + logger.Logger.Error("SaveStageConfigs: failed to update config", + zap.Int("stage", cfg.Stage), + zap.Error(result.Error)) + return result.Error + } + + logger.Logger.Info("SaveStageConfigs: update result", + zap.Int("stage", cfg.Stage), + zap.Int64("rows_affected", result.RowsAffected)) + + // If no rows affected, create new record + if result.RowsAffected == 0 { + cfg.CreatedAt = now + if err := r.db.Create(cfg).Error; err != nil { + logger.Logger.Error("SaveStageConfigs: failed to create config", + zap.Int("stage", cfg.Stage), + zap.Error(err)) + return err + } + logger.Logger.Info("SaveStageConfigs: created config", + zap.Int("stage", cfg.Stage), + zap.String("name", cfg.Name)) + } else { + logger.Logger.Info("SaveStageConfigs: updated config", + zap.Int("stage", cfg.Stage), + zap.String("name", cfg.Name)) + } + } + + logger.Logger.Info("SaveStageConfigs: all configs saved successfully") + return nil +} + +func (r *onboardingRepository) CountStageConfigs() (int64, error) { + var count int64 + err := r.db.Model(&model.OnboardingStageConfig{}).Where("is_active = ?", true).Count(&count).Error + return count, err +} diff --git a/frontend/components/GuideOverlay.vue b/frontend/components/GuideOverlay.vue index 4edb16b..4988094 100644 --- a/frontend/components/GuideOverlay.vue +++ b/frontend/components/GuideOverlay.vue @@ -55,13 +55,27 @@ import { useStore } from 'vuex' const store = useStore() // 状态映射 -const isActive = computed(() => store.state.guide.isActive) +const isActive = computed(() => { + const val = store.state.guide.isActive + console.log('[GuideOverlay] isActive computed:', val) + return val +}) const isComponentMode = computed(() => store.state.guide.componentMode) const targetRect = computed(() => store.state.guide.targetRect) const stepConfig = computed(() => store.getters['guide/currentStepConfig']) const isFirst = computed(() => store.getters['guide/isFirst']) const isLast = computed(() => store.getters['guide/isLast']) +// 监听引导激活状态 +watch(() => store.state.guide.isActive, (newVal) => { + console.log('[GuideOverlay] isActive changed to:', newVal) +}, { immediate: true }) + +// 组件挂载时记录当前状态 +onMounted(() => { + console.log('[GuideOverlay] mounted, isActive:', store.state.guide.isActive) +}) + // 高亮区域样式 const highlightStyle = computed(() => { const config = stepConfig.value || {} @@ -271,6 +285,7 @@ watch(() => store.state.guide.currentStep, async () => { // 组件挂载时初始化 onMounted(() => { + console.log('[GuideOverlay] mounted, isActive:', store.state.guide.isActive, 'currentGuide:', store.state.guide.currentGuide?.key, 'currentStep:', store.state.guide.currentStep) if (isActive.value) { // 延迟执行,确保 DOM 已渲染完成 setTimeout(() => { @@ -288,16 +303,20 @@ onMounted(() => { // 监听组件关闭事件,关闭后执行下一步 let closeComponentHandled = false // 防止重复处理的标志 uni.$on('guide:closeComponent', () => { + console.log('[GuideOverlay] 收到 guide:closeComponent 事件, componentMode:', store.state.guide.componentMode) // 如果已经处理过或者 componentMode 已经是 false,忽略 if (closeComponentHandled || !store.state.guide.componentMode) { + console.log('[GuideOverlay] guide:closeComponent 忽略, closeComponentHandled:', closeComponentHandled, 'componentMode:', store.state.guide.componentMode) return } closeComponentHandled = true store.commit('guide/RESET_COMPONENT_MODE') // component action 后需要手动前进到下一步 if (store.getters['guide/hasNext']) { + console.log('[GuideOverlay] 有下一步,前进') store.commit('guide/NEXT_STEP') } else { + console.log('[GuideOverlay] 没有下一步,结束引导') store.commit('guide/END_GUIDE') } // 重置标志,允许未来再次处理 @@ -356,8 +375,10 @@ function handleTooltipClick() { // 点击高亮区域 function handleHighlightClick() { const stepConfigVal = stepConfig.value + console.log('[GuideOverlay] handleHighlightClick, stepConfig:', stepConfigVal) if (stepConfigVal && stepConfigVal.action) { const { action } = stepConfigVal + console.log('[GuideOverlay] action:', action) if (action.type === 'navigate' && action.url) { // 路由跳转 @@ -482,6 +503,7 @@ defineExpose({ z-index: 10001; display: flex; align-items: center; + flex-direction: column; } .tooltip-content { diff --git a/frontend/pages.json b/frontend/pages.json index 2bd74a3..359f876 100644 --- a/frontend/pages.json +++ b/frontend/pages.json @@ -26,6 +26,15 @@ } } }, + { + "path": "pages/starbook/items", + "style": { + "navigationStyle": "custom", + "app-plus": { + "bounce": "none" + } + } + }, { "path": "pages/starcity/index", "style": { diff --git a/frontend/pages/components/Header.vue b/frontend/pages/components/Header.vue index aed36dc..167fab3 100644 --- a/frontend/pages/components/Header.vue +++ b/frontend/pages/components/Header.vue @@ -195,15 +195,22 @@ onMounted(() => { uni.$on('userInfoUpdated', handleUserInfoUpdate); uni.$on('balanceUpdated', handleBalanceUpdate); - // 上报每日首次登录事件(每天首次挂载时) - const today = new Date().toISOString().split('T')[0] + // 上报每日首次登录事件(每日5点重置后首次挂载时) + // 5点之前属于昨日周期,不触发登录事件 + const now = new Date() + const currentHour = now.getHours() + if (currentHour < 5) return // 5点之前不触发 + + const today = now.toISOString().split('T')[0] const dailyLoginKey = `daily_login_completed_${today}` // 先判断是否处于登录状态(是否有 star_id) const starId = uni.getStorageSync('star_id') if (starId && !uni.getStorageSync(dailyLoginKey)) { reportEvent('daily_login', starId).then(() => { - // 报告成功后标记今日已完成 + // 报告成功后标记今日已完成,并清除昨日的记录 uni.setStorageSync(dailyLoginKey, true) + const yesterday = new Date(now.getTime() - 86400000).toISOString().split('T')[0] + uni.removeStorageSync(`daily_login_completed_${yesterday}`) }).catch(err => { console.error('上报登录事件失败:', err) }) diff --git a/frontend/pages/components/StarbookContent.vue b/frontend/pages/components/StarbookContent.vue index d3ae1cd..a52f467 100644 --- a/frontend/pages/components/StarbookContent.vue +++ b/frontend/pages/components/StarbookContent.vue @@ -2,31 +2,185 @@ - + - - - + + - + 普通 + + + 典藏 + + + 活动 + + + + + + 加载中... + + + + + 暂无藏品 + + + + + + + + + + {{ group.category_name }} · {{ formatGrade(group.grade) }} + + + + + + + {{ item.name }} + ★{{ item.like_count }} + + + + + + + 更多> + + + + + + + + + + + + {{ group.category_name }} + + + + + + + {{ item.name }} + ★{{ item.like_count }} + + + + + + + 更多> + + + + + + + + + + + + {{ group.category_name }} + + + + + + + {{ item.name }} + ★{{ item.like_count }} + + + + + + + 更多> + + + + - @@ -34,8 +188,7 @@ import { ref, computed, onMounted, onActivated, watch } from 'vue'; import { onShow } from '@dcloudio/uni-app'; import NftCard from './NftCard.vue'; -import { getMyAssetsApi } from '@/utils/api.js'; -import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js'; +import { getStarbookHomeApi } from '@/utils/api.js'; // 屏幕宽度 const screenWidth = ref(0); @@ -43,20 +196,21 @@ const screenWidth = ref(0); // 加载状态 const loading = ref(false); -// NFT列表(初始为15个卡片,将通过API填充) -const nftList = ref([]); +// 当前选中的类型 +const currentType = ref('regular'); +// 星册首页数据 +const starbookData = ref([]); +// 上次加载时间(用于防抖) +let lastLoadedAt = 0; // 计算卡片尺寸 const cardSize = computed(() => { if (screenWidth.value === 0) return 200; - // 每行3个卡片,留出padding和间距 - // rpx转px:1rpx = screenWidth / 750 const rpxToPx = screenWidth.value / 750; - const padding = 40 * rpxToPx; // 左右各40rpx - const gap = 15 * rpxToPx; // 卡片间距15rpx(3列有2个间距) - // 可用宽度 = 屏幕宽度 - 左右padding - 2个间距 + const padding = 30 * rpxToPx; // 左右各30rpx + const gap = 15 * rpxToPx; // 卡片间距15rpx const availableWidth = screenWidth.value - (padding * 2) - (gap * 2); return Math.floor(availableWidth / 3); }); @@ -68,63 +222,57 @@ const cardCustomStyle = { left: '0' }; -// 添加NFT处理 - 跳转到铸爱页面 -const handleAddNft = () => { - // 跳转到铸爱商城页面 - uni.navigateTo({ - url: '/pages/castlove/mall' - }); +// grade 中文转换 +const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' }; +function formatGrade(grade) { + return `等级${gradeMap[grade] || grade}`; +} + +// 判断是否有数据 +const hasData = computed(() => { + return starbookData.value.length > 0; +}); + +// 根据当前类型获取分组数据 +const regularGroups = computed(() => { + const group = starbookData.value.find(g => g.type === 'regular'); + if (!group) return []; + return (group.grades || []).sort((a, b) => b.grade - a.grade); // grade大的在上 +}); + +const collectionGroups = computed(() => { + return starbookData.value.filter(g => g.type === 'collection'); +}); + +const activityGroups = computed(() => { + return starbookData.value.filter(g => g.type === 'activity'); +}); + +// 切换类型 +const switchType = (type) => { + currentType.value = type; }; -// 加载藏品列表 -const loadAssetsList = async () => { +// 加载星册数据 +const loadStarbookData = async () => { + const now = Date.now(); + if (now - lastLoadedAt < 1000) return; // 1秒内不重复加载 + lastLoadedAt = now; + loading.value = true; try { - const response = await getMyAssetsApi(1, 20); - if (response.code === 200 && response.data && response.data.items) { - // 映射后端数据并解析封面URL - const assetsPromises = response.data.items.map(async item => { - const realCoverUrl = await getAssetCoverRealUrl(item.cover_url); - return { - asset_id: item.asset_id, - name: item.name, - image: realCoverUrl, - cover_url: item.cover_url || '/static/nft/collection.png', - tx_hash: item.tx_hash, - like_count: item.like_count, - status: item.status, - locked: false - }; - }); - - const assets = await Promise.all(assetsPromises); - - // 添加一个空白卡片用于添加新藏品 - assets.push({ image: '', locked: false }); - - // 填充剩余位置为锁定卡片(总共15个卡片) - const totalCards = 15; - const remainingCards = totalCards - assets.length; - for (let i = 0; i < remainingCards; i++) { - assets.push({ image: '', locked: true }); - } - - nftList.value = assets; + const response = await getStarbookHomeApi(); + if (response.code === 200 && response.data && response.data.groups) { + starbookData.value = response.data.groups; } } catch (error) { - console.error('获取藏品列表失败:', error); + console.error('获取星册数据失败:', error); uni.showToast({ - title: error.message || '获取藏品列表失败', + title: error.message || '获取星册数据失败', icon: 'none', duration: 2000 }); - - // 失败时使用默认布局(1个空白卡片 + 14个锁定卡片) - const defaultList = [{ image: '', locked: false }]; - for (let i = 0; i < 14; i++) { - defaultList.push({ image: '', locked: true }); - } - nftList.value = defaultList; + starbookData.value = []; } finally { loading.value = false; } @@ -132,14 +280,31 @@ const loadAssetsList = async () => { // 点击卡片跳转到详情页 const handleCardClick = (item) => { - if (item.image && !item.locked && item.asset_id) { + if (item.asset_id) { uni.navigateTo({ url: `/pages/asset-detail/asset-detail?asset_id=${item.asset_id}` }); } }; -// 定义 props 用于接收父组件的显示状态 +// 点击更多跳转到查看更多页面 +const goToMore = (group) => { + const params = { + type: currentType.value, + category: group.category + }; + if (currentType.value === 'regular' && group.grade) { + params.grade = group.grade; + } + const queryString = Object.entries(params) + .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) + .join('&'); + uni.navigateTo({ + url: `/pages/starbook/items?${queryString}` + }); +}; + +// 定义 props const props = defineProps({ isActive: { type: Boolean, @@ -150,25 +315,25 @@ const props = defineProps({ onMounted(() => { const systemInfo = uni.getSystemInfoSync(); screenWidth.value = systemInfo.windowWidth; - - // 加载藏品列表 - loadAssetsList(); + loadStarbookData(); }); -// 每次组件激活时重新加载数据(keep-alive场景) +// 每次组件激活时重新加载数据 onActivated(() => { - loadAssetsList(); + loadStarbookData(); }); -// 监听页面显示事件(页面级切换场景) +// 监听页面显示事件 onShow(() => { - loadAssetsList(); + if (props.isActive) { + loadStarbookData(); + } }); -// 监听 isActive prop 变化(tab切换场景) -watch(() => props.isActive, (newValue, oldValue) => { - if (newValue && !oldValue) { - loadAssetsList(); +// 监听 isActive prop 变化 +watch(() => props.isActive, (newVal) => { + if (newVal) { + loadStarbookData(); } }); @@ -188,7 +353,7 @@ watch(() => props.isActive, (newValue, oldValue) => { background: #0d0820; } -/* 滚动条样式 - 自动隐藏 */ +/* 滚动条样式 */ .starbook-content::-webkit-scrollbar { width: 6rpx; } @@ -226,31 +391,133 @@ watch(() => props.isActive, (newValue, oldValue) => { z-index: 1; width: 100%; min-height: 100%; - padding: 250rpx 30rpx; + padding: 224rpx 30rpx 120rpx; box-sizing: border-box; } -/* NFT网格容器 */ -.nft-grid-container { - display: grid; - grid-template-columns: repeat(3, 1fr); - /* 列间距(与计算逻辑保持一致) */ - column-gap: 15rpx; - /* 行间距 */ - row-gap: 10rpx; - width: 100%; - max-width: 100%; - /* 确保网格项靠上对齐 */ - align-items: start; - justify-items: center; +/* 类型Tab */ +.type-tabs { + display: flex; + justify-content: center; + gap: 40rpx; + margin-bottom: 30rpx; + padding: 0 20rpx; } +.tab-item { + padding: 12rpx 30rpx; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.6); + border-bottom: 4rpx solid transparent; + transition: all 0.3s ease; +} + +.tab-item.active { + color: #ffffff; + border-bottom-color: #ffffff; +} + +/* 加载中 */ +.loading-container { + display: flex; + justify-content: center; + align-items: center; + padding-top: 200rpx; +} + +.loading-text { + color: rgba(255, 255, 255, 0.6); + font-size: 28rpx; +} + +/* 空状态 */ +.empty-container { + display: flex; + justify-content: center; + align-items: center; + padding-top: 200rpx; +} + +.empty-text { + color: rgba(255, 255, 255, 0.6); + font-size: 28rpx; +} + +/* 藏品列表容器 */ +.nft-list-container { + width: 100%; +} + +/* 藏品分组 */ +.nft-group { + margin-bottom: 40rpx; +} + +/* 分组标题 */ +.group-header { + margin-bottom: 20rpx; +} + +.group-title { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.8); +} + +/* 藏品行(横向滚动) */ +.nft-row { + display: flex; + flex-wrap: wrap; + gap: 15rpx; +} + +/* 藏品网格项 */ .nft-grid-item { position: relative; - width: 100%; - /* 保持3:4比例 */ - padding-top: 133.33%; - /* 确保内容靠上对齐 */ + width: 210rpx; + flex-shrink: 0; +} + +.nft-grid-item.more-item { + cursor: pointer; +} + +/* 藏品信息 */ +.nft-info { + padding: 8rpx 0; + text-align: center; +} + +.nft-name { display: block; + font-size: 22rpx; + color: rgba(255, 255, 255, 0.9); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nft-likes { + font-size: 20rpx; + color: rgba(255, 255, 255, 0.5); +} + +/* 更多覆盖层 */ +.more-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + border-radius: 16rpx; +} + +.more-text { + font-size: 26rpx; + color: #ffffff; } diff --git a/frontend/pages/exhibition/exhibition.vue b/frontend/pages/exhibition/exhibition.vue index 6f2bf56..afa1680 100644 --- a/frontend/pages/exhibition/exhibition.vue +++ b/frontend/pages/exhibition/exhibition.vue @@ -688,15 +688,16 @@ export default { // 加载展馆槽位信息 // 将 API 响应解析并填充到展馆数据中(供 loadGallerySlots 和 loadRandomGallery 共用) const parseGalleryResponse = async (data) => { - // 如果当前是查看自己的展馆,强制使用 currentUserUid - // 防止 API 返回的 gallery_owner_id 与当前用户 UID 不一致 - if (!isViewingOthers.value && currentUserUid.value) { - visitingGalleryOwnerUid.value = currentUserUid.value; - galleryOwnerNickname.value = ''; - } else { - visitingGalleryOwnerUid.value = data.gallery_owner_id; - galleryOwnerNickname.value = data.nickname || ''; - } + console.log('parseGalleryResponse called', { + isViewingOthers: isViewingOthers.value, + dataGalleryOwnerId: data.gallery_owner_id, + currentUserUid: currentUserUid.value + }); + + // 只更新展馆所有者信息,不要修改 isViewingOthers + // isViewingOthers 只在 onLoad 时设置,避免状态混乱 + visitingGalleryOwnerUid.value = data.gallery_owner_id; + galleryOwnerNickname.value = data.nickname || ''; galleryOwnerId.value = data.gallery_owner_id; uni.setStorageSync('gallery_owner_id', data.gallery_owner_id); @@ -743,10 +744,16 @@ export default { }; const loadGallerySlots = async () => { + console.log('loadGallerySlots called', { + isViewingOthers: isViewingOthers.value, + visitingGalleryOwnerUid: visitingGalleryOwnerUid.value, + currentUserUid: currentUserUid.value + }); try { let response; - // 使用 isViewingOthers 判断,因为它在 onLoad 中同步设置,比 isMyGallery 更可靠 - if (!isViewingOthers.value) { + // 根据 visitingGalleryOwnerUid 是否等于 currentUserUid 来判断是否调用自己的展馆 API + // 这样可以避免 isViewingOthers 状态错误导致的 API 调用错误 + if (visitingGalleryOwnerUid.value === currentUserUid.value) { response = await getMyGalleriesApi(); } else { response = await getUserGalleriesApi(visitingGalleryOwnerUid.value); @@ -1217,6 +1224,17 @@ export default { visitingGalleryOwnerUid.value = currentUserUid.value; isViewingOthers.value = false; } + + // 处理引导跳转参数:如果传递了 guide_key,则继续该引导 + if (options && options.guide_key) { + console.log('[Guide] exhibition 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step) + // 使用 resumeGuide 继续引导(不会检查 shouldShowGuide) + store.dispatch('guide/resumeGuide', options.guide_key).then(res => { + console.log('[Guide] exhibition resumeGuide 结果:', res) + }).catch(err => { + console.error('[Guide] exhibition resumeGuide 失败:', err) + }) + } }); // 处理点赞数实时更新 diff --git a/frontend/pages/profile/profile.vue b/frontend/pages/profile/profile.vue index a0fc279..a95ae31 100644 --- a/frontend/pages/profile/profile.vue +++ b/frontend/pages/profile/profile.vue @@ -252,13 +252,13 @@ @claim-success="handleClaimSuccess" @close="showGuideListModal = false" /> - + + + diff --git a/frontend/pages/tasks/daily-tasks.vue b/frontend/pages/tasks/daily-tasks.vue index 2f2d513..4ca1949 100644 --- a/frontend/pages/tasks/daily-tasks.vue +++ b/frontend/pages/tasks/daily-tasks.vue @@ -75,11 +75,29 @@ - - - + + + + + + + + {{ milestone }} + + + + + + + + @@ -110,6 +128,15 @@ const starId = ref(1) const hasClaimableTasks = computed(() => tasks.value.some(t => t.can_claim)) const claimableCount = computed(() => tasks.value.filter(t => t.can_claim).length) +// 进度里程碑 +const milestones = ref(['任务1', '任务2', '任务3', '任务4']) + +// 已完成的里程碑数量 +const completedCount = computed(() => { + // 计算已领取的任务数量 + return tasks.value.filter(t => t.status === 'claimed').length +}) + // 监听 visible 变化,自动加载数据 watch(() => props.visible, (newVal) => { if (newVal) { @@ -485,6 +512,67 @@ const handleCloseClick = (e) => { margin-top: 20rpx; } +.bottom-section { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.progress-section { + width: 100%; +} + +.progress-bar { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + height: 60rpx; + padding: 0 30rpx; +} + +.progress-bar::before { + content: ''; + position: absolute; + top: 50%; + left: 50rpx; + right: 50rpx; + height: 4rpx; + background: rgba(255, 255, 255, 0.2); + transform: translateY(-50%); + z-index: 0; +} + +.progress-node { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 1; +} + +.node-circle { + width: 24rpx; + height: 24rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + border: 4rpx solid rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; +} + +.progress-node.active .node-circle { + background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%); + border-color: #F08399; +} + +.node-label { + font-size: 22rpx; + color: rgba(230, 230, 230, 0.7); + margin-top: 8rpx; + font-family: 'ZaoZiGongFangJianHei-1', sans-serif; + text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3); +} + .claim-all-btn { width: 100%; padding: 24rpx 0; diff --git a/frontend/pages/tasks/guide.vue b/frontend/pages/tasks/guide.vue index 741e0bd..c8f0766 100644 --- a/frontend/pages/tasks/guide.vue +++ b/frontend/pages/tasks/guide.vue @@ -11,96 +11,103 @@ 新手引导 - - - - 加载中... - - - - - {{ errorMessage }} - - - - - - - - - 当前阶段 - 第 {{ currentStage }} 阶段 - - {{ currentStageName }} - - 💎 - {{ currentStageReward }} - | - - {{ currentStageExp }} 经验 - - - - - - - - - - {{ stage.name }} - - 进行中 + + + + + + {{ item.statusText }} + {{ item.name }} + - - - - - {{ isTaskCompleted(stage, taskKey) ? '✓' : '○' }} - - {{ taskKey }} - + + {{ item.desc }} + + + + + + + + {{ item.progress.completed }}/{{ item.progress.total }} 步骤 + + + + + + {{ item.buttonText }} + + + + {{ item.buttonText }} + + + + {{ item.buttonText }} + + + + {{ item.buttonText }} - - - - - - - 完成当前阶段所有任务后可进入下一阶段 - - - - - - - - + + + + 已完成 {{ doneCount }}/{{ totalCount }} + + + 有 {{ claimableCount }} 个奖励可领取 + @@ -223,7 +346,8 @@ onMounted(() => { .guide-container { position: relative; min-height: 100vh; - padding-bottom: 120rpx; + padding: 20rpx; + padding-bottom: 200rpx; } .background-image { @@ -264,222 +388,142 @@ onMounted(() => { padding: 20rpx 0; } -.loading-state, -.error-state { +.guide-list { position: relative; z-index: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 100rpx 0; } -.loading-spinner { - width: 60rpx; - height: 60rpx; - border: 4rpx solid #e0e0e0; - border-top-color: #6c5ce7; - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 20rpx; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.loading-text, -.error-text { - color: #999; - font-size: 28rpx; -} - -.retry-btn { - margin-top: 20rpx; - padding: 16rpx 40rpx; - background: linear-gradient(135deg, #6c5ce7, #a29bfe); - color: #fff; - border-radius: 50rpx; - font-size: 28rpx; -} - -.guide-content { - position: relative; - z-index: 1; - padding: 20rpx; -} - -.current-stage-card { - background: linear-gradient(135deg, #6c5ce7, #a29bfe); - border-radius: 20rpx; - padding: 40rpx; - margin-bottom: 30rpx; - color: #fff; -} - -.stage-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20rpx; -} - -.stage-label { - font-size: 28rpx; - opacity: 0.8; -} - -.stage-number { - font-size: 28rpx; - font-weight: bold; -} - -.stage-name { - font-size: 40rpx; - font-weight: bold; - margin-bottom: 20rpx; -} - -.stage-reward { - display: flex; - align-items: center; - gap: 8rpx; - font-size: 28rpx; -} - -.reward-icon { - font-size: 28rpx; -} - -.reward-value { - font-weight: bold; -} - -.reward-sep { - opacity: 0.5; - margin: 0 10rpx; -} - -.stages-list { - margin-bottom: 30rpx; -} - -.stage-item { +.guide-item { background: #fff; border-radius: 16rpx; - padding: 30rpx; + padding: 24rpx; margin-bottom: 20rpx; } -.stage-item.is-current { - border: 2rpx solid #6c5ce7; +.guide-item.in-progress { + border-left: 4rpx solid #4a90e2; } -.stage-item.is-completed { - opacity: 0.7; -} - -.stage-item-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20rpx; -} - -.stage-item-name { +.guide-item-header { display: flex; align-items: center; - gap: 10rpx; - font-size: 30rpx; - font-weight: bold; - color: #333; + margin-bottom: 12rpx; } -.check-icon { - color: #52c41a; - font-size: 32rpx; -} - -.current-tag { - padding: 6rpx 16rpx; - background: #6c5ce7; - color: #fff; - border-radius: 20rpx; +.guide-status-badge { + padding: 4rpx 12rpx; + border-radius: 8rpx; font-size: 22rpx; + margin-right: 12rpx; } -.task-list-mini { - display: flex; - flex-wrap: wrap; - gap: 16rpx; -} - -.task-key-item { - display: flex; - align-items: center; - gap: 8rpx; - padding: 8rpx 16rpx; - background: #f5f5f5; - border-radius: 20rpx; - font-size: 24rpx; -} - -.task-check { - color: #ddd; - font-size: 24rpx; -} - -.task-check.is-done { - color: #52c41a; -} - -.task-key-name { - color: #666; -} - -.action-bar { - padding: 20rpx 0; -} - -.advance-btn { - width: 100%; - padding: 24rpx 0; - background: linear-gradient(135deg, #6c5ce7, #a29bfe); - color: #fff; - border-radius: 50rpx; - font-size: 32rpx; - font-weight: bold; - margin-bottom: 20rpx; -} - -.advance-hint { - text-align: center; +.guide-status-badge.not_started { + background: #f0f0f0; color: #999; +} + +.guide-status-badge.in_progress { + background: #e6f0ff; + color: #4a90e2; +} + +.guide-status-badge.completed { + background: #fff3e6; + color: #ff9500; +} + +.guide-status-badge.reward_claimed { + background: #e8f5e9; + color: #4caf50; +} + +.guide-name { + font-size: 30rpx; + font-weight: 500; + color: #333; + flex: 1; +} + +.guide-desc { font-size: 26rpx; - padding: 20rpx 0; + color: #666; + margin-bottom: 16rpx; +} + +.guide-progress { + margin-bottom: 16rpx; +} + +.progress-bar { + height: 8rpx; + background: #eee; + border-radius: 4rpx; + overflow: hidden; + margin-bottom: 8rpx; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 4rpx; + transition: width 0.3s ease; +} + +.progress-text { + font-size: 22rpx; + color: #999; +} + +.guide-item-footer { + display: flex; + justify-content: flex-end; +} + +.guide-btn { + padding: 12rpx 32rpx; + color: #fff; + border-radius: 30rpx; + font-size: 26rpx; +} + +.start-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.continue-btn { + background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); } .claim-btn { - width: 100%; - padding: 24rpx 0; - background: linear-gradient(135deg, #ffd700, #ffb347); - color: #333; - border-radius: 50rpx; - font-size: 32rpx; - font-weight: bold; + background: linear-gradient(135deg, #f6d365 0%, #fda085 100%); } -.pull-to-refresh { - position: relative; - z-index: 1; - display: flex; - justify-content: center; - padding: 30rpx 0; -} - -.refresh-text { +.disabled-btn { + background: #ddd; color: #999; - font-size: 24rpx; +} + +.guide-footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 24rpx 32rpx; + background: #fff; + border-top: 1rpx solid #eee; + z-index: 10; +} + +.footer-stats { + font-size: 28rpx; + color: #333; + text-align: center; + margin-bottom: 8rpx; +} + +.footer-claim-tip { + font-size: 26rpx; + color: #ff6b6b; + text-align: center; } diff --git a/frontend/store/modules/guide.js b/frontend/store/modules/guide.js index cf454ce..994cd1e 100644 --- a/frontend/store/modules/guide.js +++ b/frontend/store/modules/guide.js @@ -39,11 +39,14 @@ const mutations = { * @param {number} [guideConfig.startFromStep] 从指定步骤开始(可选,默认从保存的进度继续) */ START_GUIDE(state, { guideConfig, startFromStep }) { + console.log('[Guide] START_GUIDE mutation called, key:', guideConfig.key, 'startFromStep:', startFromStep) state.currentGuide = guideConfig // 如果指定了起始步骤,从指定步骤开始;否则恢复之前保存的进度 state.currentStep = startFromStep !== undefined ? startFromStep : (getGuideCurrentStep(guideConfig.key) || 0) + console.log('[Guide] START_GUIDE, currentStep set to:', state.currentStep) state.isActive = true state.isNavigating = false + console.log('[Guide] START_GUIDE, isActive set to:', state.isActive) }, /** @@ -236,6 +239,7 @@ const actions = { } // 获取第一个未完成的步骤 const resumeStep = getNextIncompleteStep(guideKey) + console.log('[Guide] resumeGuide:', guideKey, 'resumeStep:', resumeStep, 'guide:', guide) commit('START_GUIDE', { guideConfig: guide, startFromStep: resumeStep }) return true }, diff --git a/frontend/utils/api.js b/frontend/utils/api.js index 6f2d23d..2e495a5 100644 --- a/frontend/utils/api.js +++ b/frontend/utils/api.js @@ -541,4 +541,26 @@ export function getActivityRankingApi(activityId, starId = null, page = 1, pageS url: url, method: 'GET' }) +} + +// ==================== 星册相关接口 ==================== + +// 获取星册首页数据 +export function getStarbookHomeApi() { + return request({ + url: '/api/v1/starbook/home', + method: 'GET' + }) +} + +// 获取星册藏品列表(分页) +export function getStarbookItemsApi(type, category, grade = null, page = 1, pageSize = 20) { + let url = `/api/v1/starbook/items?type=${type}&category=${category}&page=${page}&page_size=${pageSize}` + if (grade !== null) { + url += `&grade=${grade}` + } + return request({ + url: url, + method: 'GET' + }) } \ No newline at end of file diff --git a/frontend/utils/guideConfig.js b/frontend/utils/guideConfig.js index 7793a66..c8b68b8 100644 --- a/frontend/utils/guideConfig.js +++ b/frontend/utils/guideConfig.js @@ -52,6 +52,47 @@ * } */ +// ==================== 新手引导阶段配置 ==================== + +/** + * 新手引导阶段配置 + * 用于后端存储和双重校验 + * + * 结构: + * { + * stage: 0, // 阶段号 + * name: '初识平台', // 阶段名称 + * required_task_keys: [], // 该阶段必须完成的任务key列表 + * crystal_reward: 100, // 水晶奖励 + * exp_reward: 50 // 经验奖励 + * } + */ +export const onboardingStages = [ + { + stage: 0, + name: '初识平台', + required_task_keys: ['square_home', 'browse_exhibition'], + crystal_reward: 100, + exp_reward: 50 + }, + { + stage: 1, + name: '互动入门', + required_task_keys: ['follow_star', 'send_gift'], + crystal_reward: 200, + exp_reward: 100 + }, + { + stage: 2, + name: '社交达人', + required_task_keys: ['add_friend', 'share_content'], + crystal_reward: 300, + exp_reward: 150 + } +] + +// ==================== 引导配置文件 ==================== + export const guideConfig = { // 广场页面引导 square_home: { diff --git a/frontend/utils/task-api.js b/frontend/utils/task-api.js index d84fa3b..269d348 100644 --- a/frontend/utils/task-api.js +++ b/frontend/utils/task-api.js @@ -43,10 +43,11 @@ export const claimAllDailyTasks = (starId) => /** * 完成引导步骤 * @param {string} taskKey - 引导任务key(如 square_home, profile_edit) + * @param {Array} stages - 前端配置的阶段列表(首次调用时传,后续不传) * @returns {Promise} */ -export const completeGuide = (taskKey) => - request({ url: '/api/v1/tasks/guide/complete', method: 'POST', data: { task_key: taskKey } }) +export const completeGuide = (taskKey, stages = []) => + request({ url: '/api/v1/tasks/guide/complete', method: 'POST', data: { task_key: taskKey, stages } }) /** * 获取引导状态 diff --git a/截屏2026-04-15 18.12.27.png b/截屏2026-04-15 18.12.27.png deleted file mode 100644 index 9c92cd0..0000000 Binary files a/截屏2026-04-15 18.12.27.png and /dev/null differ diff --git a/截屏2026-04-15 18.12.51.png b/截屏2026-04-15 18.12.51.png deleted file mode 100644 index 9150de1..0000000 Binary files a/截屏2026-04-15 18.12.51.png and /dev/null differ diff --git a/截屏2026-04-15 18.13.12.png b/截屏2026-04-15 18.13.12.png deleted file mode 100644 index adb4b93..0000000 Binary files a/截屏2026-04-15 18.13.12.png and /dev/null differ