diff --git a/backend/gateway/controller/asset_controller.go b/backend/gateway/controller/asset_controller.go index 247adca..ace8372 100644 --- a/backend/gateway/controller/asset_controller.go +++ b/backend/gateway/controller/asset_controller.go @@ -32,25 +32,43 @@ import ( "go.uber.org/zap" "github.com/topfans/backend/pkg/logger" + pbGallery "github.com/topfans/backend/pkg/proto/gallery" + pbUser "github.com/topfans/backend/pkg/proto/user" "github.com/topfans/backend/services/assetService/service" ) // AssetController 资产相关控制器 type AssetController struct { assetService pbAsset.AssetService + userService pbUser.UserSocialService + galleryService pbGallery.GalleryService minimaxService service.MinimaxService } // NewAssetController 创建资产控制器 -func NewAssetController(dubboClient *client.Client) (*AssetController, error) { +func NewAssetController(assetClient *client.Client, userClient *client.Client, galleryClient *client.Client) (*AssetController, error) { // 创建 AssetService 客户端 - assetService, err := pbAsset.NewAssetService(dubboClient) + assetService, err := pbAsset.NewAssetService(assetClient) + if err != nil { + return nil, err + } + + // 创建 UserService 客户端 + userService, err := pbUser.NewUserSocialService(userClient) + if err != nil { + return nil, err + } + + // 创建 GalleryService 客户端 + galleryService, err := pbGallery.NewGalleryService(galleryClient) if err != nil { return nil, err } return &AssetController{ assetService: assetService, + userService: userService, + galleryService: galleryService, minimaxService: service.NewMinimaxService(), }, nil } @@ -264,11 +282,11 @@ func (ctrl *AssetController) EstimateMintCost(c *gin.Context) { } data := map[string]interface{}{ - "cost_crystal": resp.CostCrystal, - "current_balance": resp.CurrentBalance, - "balance_after": resp.BalanceAfter, - "mint_count": resp.MintCount, - "next_tier_hint": resp.NextTierHint, + "cost_crystal": resp.CostCrystal, + "current_balance": resp.CurrentBalance, + "balance_after": resp.BalanceAfter, + "mint_count": resp.MintCount, + "next_tier_hint": resp.NextTierHint, } response.Success(c, data) } @@ -1585,7 +1603,8 @@ func (ctrl *AssetController) BindAssetMaterials(c *gin.Context) { uid, _ := userIDVal.(int64) starIDVal, _ := c.Get("star_id") sid, _ := starIDVal.(int64) - _ = uid; _ = sid // suppress unused + _ = uid + _ = sid // suppress unused var req dto.BindAssetMaterialsRequestDTO if err := c.ShouldBindJSON(&req); err != nil { @@ -1786,3 +1805,98 @@ func (ctrl *AssetController) UnbindAssetMaterial(c *gin.Context) { response.Success(c, gin.H{"message": "解绑成功"}) } + +// GetEarningsSummary 获取收益汇总 +// @Summary 获取收益汇总 +// @Description 获取当前用户的收益汇总(总每小时收益、总展出收益、水晶余额) +// @Tags assets +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} response.Response +// @Router /api/v1/assets/me/earnings-summary [get] +func (ctrl *AssetController) GetEarningsSummary(c *gin.Context) { + userIDVal, _ := c.Get("user_id") + starIDVal, _ := c.Get("star_id") + + userID, ok := userIDVal.(int64) + if !ok { + logger.Logger.Error("GetEarningsSummary: user_id type assertion failed", zap.Any("value", userIDVal)) + response.Error(c, http.StatusInternalServerError, "用户ID无效") + return + } + + starID, ok := starIDVal.(int64) + if !ok { + logger.Logger.Error("GetEarningsSummary: star_id type assertion failed", zap.Any("value", starIDVal)) + response.Error(c, http.StatusInternalServerError, "明星ID无效") + return + } + + logger.Logger.Info("GetEarningsSummary: start", zap.Int64("user_id", userID), zap.Int64("star_id", starID)) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{ + "user_id": strconv.FormatInt(userID, 10), + "star_id": strconv.FormatInt(starID, 10), + }) + + // 1. 获取我展出的作品列表 + var totalHourlyEarnings float64 + var totalExhibitionRevenue int64 + page := int32(1) + pageSize := int32(200) + + for { + resp, err := ctrl.galleryService.GetMyExhibitedAssets(ctx, &pbGallery.GetMyExhibitedAssetsRequest{ + Page: page, + PageSize: pageSize, + }) + if err != nil { + logger.Logger.Error("GetEarningsSummary: GetMyExhibitedAssets failed", zap.Error(err)) + response.Error(c, http.StatusInternalServerError, "获取展出作品列表失败") + return + } + + if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { + logger.Logger.Warn("GetEarningsSummary: GetMyExhibitedAssets returned non-OK", zap.Any("code", resp.Base.Code), zap.String("msg", resp.Base.Message)) + } + + // 累加每个 item 的收益(GetMyExhibitedAssets 返回的都是展出中的作品) + for _, item := range resp.Data.Items { + hourlyEarnings := item.HourlyEarnings + totalHourlyEarnings += hourlyEarnings + totalExhibitionRevenue += item.Earnings + } + + // 检查是否还有更多 + if !resp.Data.HasMore { + break + } + page++ + } + + logger.Logger.Info("GetEarningsSummary: assets fetched", zap.Float64("total_hourly", totalHourlyEarnings), zap.Int64("total_revenue", totalExhibitionRevenue)) + + // 2. 获取水晶余额 + logger.Logger.Info("GetEarningsSummary: calling GetFanProfile", zap.Int64("user_id", userID), zap.Int64("star_id", starID)) + userResp, err := ctrl.userService.GetFanProfile(ctx, &pbUser.GetFanProfileRequest{ + UserId: userID, + StarId: starID, + }) + if err != nil { + logger.Logger.Error("GetEarningsSummary: GetFanProfile failed", zap.Error(err), zap.Int64("user_id", userID), zap.Int64("star_id", starID)) + response.Error(c, http.StatusInternalServerError, "获取用户信息失败") + return + } + + logger.Logger.Info("GetEarningsSummary: success", zap.Float64("total_hourly", totalHourlyEarnings), zap.Int64("total_revenue", totalExhibitionRevenue), zap.Int64("balance", userResp.GetProfile().GetCrystalBalance())) + + response.Success(c, gin.H{ + "total_hourly_earnings": totalHourlyEarnings, + "total_exhibition_revenue": totalExhibitionRevenue, + "crystal_balance": userResp.GetProfile().GetCrystalBalance(), + }) +} diff --git a/backend/gateway/router/router.go b/backend/gateway/router/router.go index b8deaba..f50862c 100644 --- a/backend/gateway/router/router.go +++ b/backend/gateway/router/router.go @@ -46,7 +46,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl return nil, err } - assetCtrl, err := controller.NewAssetController(assetClient) + assetCtrl, err := controller.NewAssetController(assetClient, userClient, galleryClient) if err != nil { return nil, err } @@ -192,6 +192,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl assets.GET("/mints/:order_id", assetCtrl.GetMintOrder) // 查询铸造订单状态 assets.DELETE("/mints/:order_id", assetCtrl.CancelMintOrder) // 取消铸造订单 assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图 + assets.GET("/me/earnings-summary", assetCtrl.GetEarningsSummary) // 获取收益汇总 assets.GET("/me/items", assetCtrl.GetMyAssets) // 获取我的藏品列表 assets.GET("/:asset_id", assetCtrl.GetAsset) // 获取资产详情 assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus) // 查询上链状态 diff --git a/frontend/pages/components/Header.vue b/frontend/pages/components/Header.vue index 4606990..9093c86 100644 --- a/frontend/pages/components/Header.vue +++ b/frontend/pages/components/Header.vue @@ -69,12 +69,12 @@ - {{ crystalBalance }} + {{ exhibitionRevenue }} - 收益 0/H + 收益 {{ hourlyEarnings }}/时 @@ -103,6 +103,7 @@ import DailyTasks from '@/pages/tasks/daily-tasks.vue'; import GuideModal from '@/pages/tasks/GuideModal.vue'; import { useBanner } from '@/pages/square/composables/useBanner.js'; import { reportEvent } from '@/utils/task-api.js'; +import { getEarningsSummaryApi } from '@/utils/api.js'; // 获取星援活动数据(复用 square 的 useBanner) const { bannerActivities, loadBannerActivities } = useBanner(); @@ -170,10 +171,29 @@ const userAvatarUrl = computed(() => { return userInfo.value?.avatar_url || ''; }); -// 获取水晶余额 -const crystalBalance = computed(() => { - return userInfo.value?.crystal_balance ?? 9527; -}); +// // 获取水晶余额 +// const crystalBalance = computed(() => { +// return userInfo.value?.crystal_balance ?? 9527; +// }); + +// 收益相关数据 +const exhibitionRevenue = ref(0); // 当前展出收益 +const hourlyEarnings = ref(0); // 每小时收益之和 + +// 加载收益汇总 +const loadEarningsSummary = async () => { + try { + const response = await getEarningsSummaryApi(); + + if (response.code === 200) { + const data = response.data; + exhibitionRevenue.value = data.crystal_balance || 0; + hourlyEarnings.value = data.total_hourly_earnings || 0; + } + } catch (e) { + console.error('获取收益汇总失败:', e); + } +}; // 监听头像更新事件 const handleAvatarUpdate = (data) => { @@ -243,9 +263,10 @@ const starActivities = computed(() => { }); // 组件挂载时加载用户信息和星援活动数据 -onMounted(() => { - loadUserInfo(); - loadBannerActivities(); +onMounted(async() => { + await loadUserInfo(); + await loadBannerActivities(); + await loadEarningsSummary(); uni.$on('avatarUpdated', handleAvatarUpdate); uni.$on('userInfoUpdated', handleUserInfoUpdate); uni.$on('balanceUpdated', handleBalanceUpdate); diff --git a/frontend/utils/api.js b/frontend/utils/api.js index 7c94632..e0b6b23 100644 --- a/frontend/utils/api.js +++ b/frontend/utils/api.js @@ -798,4 +798,12 @@ export function getMintingActivitiesApi(starId = null, page = 1, pageSize = 10) url: url, method: 'GET' }) +} + +// 获取收益汇总接口 +export function getEarningsSummaryApi() { + return request({ + url: '/api/v1/assets/me/earnings-summary', + method: 'GET' + }) } \ No newline at end of file