topfans/backend/gateway/controller/statistic_controller.go
zerosaturation 3d3f2260f3 feat(statistic): T13-T16 Gateway routes + 5-service business integration (7/7 event types)
Gateway:
- 7 routes /api/v1/dashboard/* with JWT auth (middleware.AuthMiddleware)
- statistic_controller.go: 7 methods, response format {code:200, data:resp}
- gateway/main.go: StatisticServiceClient wired
- gateway/config: StatisticServiceURL (DUBBO_STATISTIC_SERVICE_URL, default tri://127.0.0.1:20009)

pkg/statistic SDK:
- fire-and-forget TrackEvent + BatchTrackEvent
- Init(client) + Get() global singleton pattern

Business-side integration (7/7 event types):
- socialService.LikeAsset → asset.like
- galleryService.PlaceAsset → exhibition.start
- galleryService.RemoveFromSlot → exhibition.end (with duration_ms)
- taskService.OnExhibitionCompleted → exhibition.revenue
- assetService.CreateMintOrder → asset.mint
- assetService.logLevelChange → asset.level_up
- userService.UpdateCrystalBalance → crystal.change (wrapper fn added)

Cache warmup:
- main.go: 7 RPCs x 5 sample starIDs at startup (15s delay)
- prevents cold-start DB thundering herd

Existing service modifications:
- galleryService/exhibition_service.go
- taskService/revenue_service.go
- assetService/{mint_service,asset_level_service}.go
- userService/user_service.go
- socialService/asset_like_service.go

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:20:53 +08:00

172 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controller
import (
"context"
"net/http"
"strconv"
"time"
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/gin-gonic/gin"
"github.com/topfans/backend/gateway/pkg/response"
"github.com/topfans/backend/pkg/logger"
pb "github.com/topfans/backend/pkg/proto/statistic"
"go.uber.org/zap"
)
// StatisticController 数据看板 HTTP controllergateway → statisticService 7 RPC
type StatisticController struct {
statisticService pb.StatisticService
}
// NewStatisticController 构造
func NewStatisticController(dubboClient *client.Client) (*StatisticController, error) {
svc, err := pb.NewStatisticService(dubboClient)
if err != nil {
return nil, err
}
return &StatisticController{statisticService: svc}, nil
}
// parseStarID 解析 query 中的 star_id
func parseStarID(c *gin.Context) (int64, bool) {
v := c.Query("star_id")
if v == "" {
response.Error(c, http.StatusBadRequest, "star_id 是必填参数")
return 0, false
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "star_id 参数错误")
return 0, false
}
return n, true
}
// buildCtx 构造带 user_id 透传的 ctxDubbo attachment
func buildCtx(c *gin.Context) context.Context {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
_ = cancel
userID, _ := c.Get("user_id")
if userID != nil {
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(toInt64(userID), 10),
})
}
return ctx
}
func toInt64(v interface{}) int64 {
switch x := v.(type) {
case int64:
return x
case int:
return int64(x)
case string:
n, _ := strconv.ParseInt(x, 10, 64)
return n
}
return 0
}
// ===== 1. 今日概览 =====
func (ctrl *StatisticController) GetTodayOverview(c *gin.Context) {
starID, ok := parseStarID(c)
if !ok {
return
}
logger.Logger.Info("GetTodayOverview", zap.Int64("star_id", starID))
resp, err := ctrl.statisticService.GetTodayOverview(buildCtx(c), &pb.GetTodayOverviewRequest{StarId: starID})
if err != nil {
logger.Logger.Error("GetTodayOverview RPC failed", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "获取今日概览失败")
return
}
response.OK(c, gin.H{"code": 200, "data": resp})
}
// ===== 2. 七日收益曲线 =====
func (ctrl *StatisticController) Get7DayIncomeCurve(c *gin.Context) {
starID, ok := parseStarID(c)
if !ok {
return
}
resp, err := ctrl.statisticService.Get7DayIncomeCurve(buildCtx(c), &pb.Get7DayIncomeCurveRequest{StarId: starID})
if err != nil {
response.Error(c, http.StatusInternalServerError, "获取七日收益曲线失败")
return
}
response.OK(c, gin.H{"code": 200, "data": resp})
}
// ===== 3. 展出收益中心 =====
func (ctrl *StatisticController) GetExhibitionIncomeSummary(c *gin.Context) {
starID, ok := parseStarID(c)
if !ok {
return
}
resp, err := ctrl.statisticService.GetExhibitionIncomeSummary(buildCtx(c), &pb.GetExhibitionIncomeSummaryRequest{StarId: starID})
if err != nil {
response.Error(c, http.StatusInternalServerError, "获取展出收益中心失败")
return
}
response.OK(c, gin.H{"code": 200, "data": resp})
}
// ===== 4. 点赞收益按等级 =====
func (ctrl *StatisticController) GetLikeIncomeByLevel(c *gin.Context) {
starID, ok := parseStarID(c)
if !ok {
return
}
resp, err := ctrl.statisticService.GetLikeIncomeByLevel(buildCtx(c), &pb.GetLikeIncomeByLevelRequest{StarId: starID})
if err != nil {
response.Error(c, http.StatusInternalServerError, "获取点赞等级收益失败")
return
}
response.OK(c, gin.H{"code": 200, "data": resp})
}
// ===== 5. 藏品 TOP5 =====
func (ctrl *StatisticController) GetTopAssets(c *gin.Context) {
starID, ok := parseStarID(c)
if !ok {
return
}
resp, err := ctrl.statisticService.GetTopAssetsByEarning(buildCtx(c), &pb.GetTopAssetsByEarningRequest{StarId: starID})
if err != nil {
response.Error(c, http.StatusInternalServerError, "获取藏品 TOP5 失败")
return
}
response.OK(c, gin.H{"code": 200, "data": resp})
}
// ===== 6. 藏品等级分布 =====
func (ctrl *StatisticController) GetLevelDistribution(c *gin.Context) {
starID, ok := parseStarID(c)
if !ok {
return
}
resp, err := ctrl.statisticService.GetAssetLevelDistribution(buildCtx(c), &pb.GetAssetLevelDistributionRequest{StarId: starID})
if err != nil {
response.Error(c, http.StatusInternalServerError, "获取等级分布失败")
return
}
response.OK(c, gin.H{"code": 200, "data": resp})
}
// ===== 7. 升级进度 =====
func (ctrl *StatisticController) GetUpgradeProgress(c *gin.Context) {
starID, ok := parseStarID(c)
if !ok {
return
}
resp, err := ctrl.statisticService.GetAssetUpgradeProgress(buildCtx(c), &pb.GetAssetUpgradeProgressRequest{StarId: starID})
if err != nil {
response.Error(c, http.StatusInternalServerError, "获取升级进度失败")
return
}
response.OK(c, gin.H{"code": 200, "data": resp})
}