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>
172 lines
4.9 KiB
Go
172 lines
4.9 KiB
Go
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 controller(gateway → 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 透传的 ctx(Dubbo 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})
|
||
}
|