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