1193 lines
38 KiB
Go
1193 lines
38 KiB
Go
package admin
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"bindbox-game/internal/code"
|
||
"bindbox-game/internal/pkg/core"
|
||
"bindbox-game/internal/pkg/validation"
|
||
)
|
||
|
||
type cardsRequest struct {
|
||
RangeType string `form:"rangeType"`
|
||
StartDate string `form:"start"`
|
||
EndDate string `form:"end"`
|
||
}
|
||
|
||
type cardStatResponse struct {
|
||
ItemCardSales int64 `json:"itemCardSales"`
|
||
DrawCount int64 `json:"drawCount"`
|
||
NewUsers int64 `json:"newUsers"`
|
||
TotalPoints int64 `json:"totalPoints"`
|
||
ItemCardChange string `json:"itemCardChange"`
|
||
DrawChange string `json:"drawChange"`
|
||
NewUserChange string `json:"newUserChange"`
|
||
PointsChange string `json:"pointsChange"`
|
||
}
|
||
|
||
func (h *handler) DashboardCards() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(cardsRequest)
|
||
rsp := new(cardStatResponse)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
start, end := parseRange(req.RangeType, req.StartDate, req.EndDate)
|
||
prevStart, prevEnd := previousWindow(start, end)
|
||
|
||
icCur, err := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.UserItemCards.CreatedAt.Gte(start)).
|
||
Where(h.readDB.UserItemCards.CreatedAt.Lte(end)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21001, err.Error()))
|
||
return
|
||
}
|
||
icPrev, err := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.UserItemCards.CreatedAt.Gte(prevStart)).
|
||
Where(h.readDB.UserItemCards.CreatedAt.Lte(prevEnd)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21002, err.Error()))
|
||
return
|
||
}
|
||
|
||
dlCur, err := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Gte(start)).
|
||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Lte(end)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21003, err.Error()))
|
||
return
|
||
}
|
||
dlPrev, err := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Gte(prevStart)).
|
||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Lte(prevEnd)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21004, err.Error()))
|
||
return
|
||
}
|
||
|
||
nuCur, err := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.Users.CreatedAt.Gte(start)).
|
||
Where(h.readDB.Users.CreatedAt.Lte(end)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21005, err.Error()))
|
||
return
|
||
}
|
||
nuPrev, err := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.Users.CreatedAt.Gte(prevStart)).
|
||
Where(h.readDB.Users.CreatedAt.Lte(prevEnd)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21006, err.Error()))
|
||
return
|
||
}
|
||
|
||
var tpRows []struct{ Sum int64 }
|
||
if err := h.readDB.UserPoints.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.UserPoints.ValidEnd.Gt(time.Now())).
|
||
Or(h.readDB.UserPoints.ValidEnd.Eq(time.Time{})).
|
||
Select(h.readDB.UserPoints.Points.Sum().As("sum")).
|
||
Scan(&tpRows); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21007, err.Error()))
|
||
return
|
||
}
|
||
var tpCur int64
|
||
if len(tpRows) > 0 {
|
||
tpCur = tpRows[0].Sum
|
||
}
|
||
// 使用积分流水计算净变动用于环比
|
||
// 当前窗口净变动
|
||
var curDeltaRows []struct{ Sum int64 }
|
||
if err := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.UserPointsLedger.CreatedAt.Gte(start)).
|
||
Where(h.readDB.UserPointsLedger.CreatedAt.Lte(end)).
|
||
Select(h.readDB.UserPointsLedger.Points.Sum().As("sum")).
|
||
Scan(&curDeltaRows); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21008, err.Error()))
|
||
return
|
||
}
|
||
var curDelta int64
|
||
if len(curDeltaRows) > 0 {
|
||
curDelta = curDeltaRows[0].Sum
|
||
}
|
||
// 前一窗口净变动
|
||
var prevDeltaRows []struct{ Sum int64 }
|
||
if err := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.UserPointsLedger.CreatedAt.Gte(prevStart)).
|
||
Where(h.readDB.UserPointsLedger.CreatedAt.Lte(prevEnd)).
|
||
Select(h.readDB.UserPointsLedger.Points.Sum().As("sum")).
|
||
Scan(&prevDeltaRows); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21009, err.Error()))
|
||
return
|
||
}
|
||
var prevDelta int64
|
||
if len(prevDeltaRows) > 0 {
|
||
prevDelta = prevDeltaRows[0].Sum
|
||
}
|
||
|
||
rsp.ItemCardSales = icCur
|
||
rsp.DrawCount = dlCur
|
||
rsp.NewUsers = nuCur
|
||
rsp.TotalPoints = tpCur
|
||
rsp.ItemCardChange = percentChange(icPrev, icCur)
|
||
rsp.DrawChange = percentChange(dlPrev, dlCur)
|
||
rsp.NewUserChange = percentChange(nuPrev, nuCur)
|
||
rsp.PointsChange = percentChange(prevDelta, curDelta)
|
||
ctx.Payload(rsp)
|
||
}
|
||
}
|
||
|
||
type trendRequest struct {
|
||
RangeType string `form:"rangeType"`
|
||
Granularity string `form:"granularity"`
|
||
}
|
||
|
||
type trendPoint struct {
|
||
Date string `json:"date"`
|
||
Value int64 `json:"value"`
|
||
}
|
||
|
||
type userTrendResponse struct {
|
||
Granularity string `json:"granularity"`
|
||
List []trendPoint `json:"list"`
|
||
}
|
||
|
||
func (h *handler) DashboardUserTrend() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(trendRequest)
|
||
rsp := new(userTrendResponse)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
start, end := parseRange(req.RangeType, "", "")
|
||
gran := normalizeGranularity(req.Granularity)
|
||
buckets := buildBuckets(start, end, gran)
|
||
list := make([]trendPoint, 0, len(buckets))
|
||
for _, b := range buckets {
|
||
c, err := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.Users.CreatedAt.Gte(b.Start)).
|
||
Where(h.readDB.Users.CreatedAt.Lte(b.End)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21009, err.Error()))
|
||
return
|
||
}
|
||
list = append(list, trendPoint{Date: b.Label, Value: c})
|
||
}
|
||
rsp.Granularity = gran
|
||
rsp.List = list
|
||
ctx.Payload(rsp)
|
||
}
|
||
}
|
||
|
||
type drawTrendResponse struct {
|
||
Granularity string `json:"granularity"`
|
||
List []trendPoint `json:"list"`
|
||
}
|
||
|
||
func (h *handler) DashboardDrawTrend() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(trendRequest)
|
||
rsp := new(drawTrendResponse)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
start, end := parseRange(req.RangeType, "", "")
|
||
gran := normalizeGranularity(req.Granularity)
|
||
buckets := buildBuckets(start, end, gran)
|
||
list := make([]trendPoint, 0, len(buckets))
|
||
for _, b := range buckets {
|
||
c, err := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Gte(b.Start)).
|
||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Lte(b.End)).
|
||
Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21010, err.Error()))
|
||
return
|
||
}
|
||
list = append(list, trendPoint{Date: b.Label, Value: c})
|
||
}
|
||
rsp.Granularity = gran
|
||
rsp.List = list
|
||
ctx.Payload(rsp)
|
||
}
|
||
}
|
||
|
||
type newUsersRequest struct {
|
||
Page int `form:"page"`
|
||
PageSize int `form:"page_size"`
|
||
Period string `form:"period"`
|
||
}
|
||
|
||
type newUserItem struct {
|
||
ID int64 `json:"id"`
|
||
Nickname string `json:"nickname"`
|
||
Avatar string `json:"avatar"`
|
||
CreatedAt string `json:"createdAt"`
|
||
PointsBalance int64 `json:"pointsBalance"`
|
||
InventoryCount int64 `json:"inventoryCount"`
|
||
ItemCardCount int64 `json:"itemCardCount"`
|
||
CouponCount int64 `json:"couponCount"`
|
||
TitleCount int64 `json:"titleCount"`
|
||
LastOnlineAt string `json:"lastOnlineAt"`
|
||
Titles []userTitleBrief `json:"titles"`
|
||
}
|
||
|
||
type userTitleBrief struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
}
|
||
|
||
type newUsersResponse struct {
|
||
Page int `json:"page"`
|
||
PageSize int `json:"pageSize"`
|
||
Total int64 `json:"total"`
|
||
List []newUserItem `json:"list"`
|
||
}
|
||
|
||
func (h *handler) DashboardNewUsers() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(newUsersRequest)
|
||
rsp := new(newUsersResponse)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
if req.Page <= 0 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize <= 0 {
|
||
req.PageSize = 20
|
||
}
|
||
if req.PageSize > 100 {
|
||
req.PageSize = 100
|
||
}
|
||
base := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB()
|
||
if req.Period != "" {
|
||
var s, e time.Time
|
||
now := time.Now()
|
||
switch req.Period {
|
||
case "month":
|
||
s = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||
e = s.AddDate(0, 1, 0).Add(-time.Second)
|
||
case "last_month":
|
||
lm := now.AddDate(0, -1, 0)
|
||
s = time.Date(lm.Year(), lm.Month(), 1, 0, 0, 0, 0, lm.Location())
|
||
e = s.AddDate(0, 1, 0).Add(-time.Second)
|
||
case "year":
|
||
s = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
|
||
e = time.Date(now.Year(), 12, 31, 23, 59, 59, 0, now.Location())
|
||
}
|
||
if !s.IsZero() && !e.IsZero() {
|
||
base = base.Where(h.readDB.Users.CreatedAt.Gte(s)).Where(h.readDB.Users.CreatedAt.Lte(e))
|
||
}
|
||
}
|
||
total, err := base.Count()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21011, err.Error()))
|
||
return
|
||
}
|
||
rows, err := base.Order(h.readDB.Users.ID.Desc()).Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21012, err.Error()))
|
||
return
|
||
}
|
||
// 收集用户ID做批量聚合
|
||
ids := make([]int64, len(rows))
|
||
for i, u := range rows {
|
||
ids[i] = u.ID
|
||
}
|
||
|
||
// 批量:资产数
|
||
type kvCount struct {
|
||
UserID int64
|
||
Cnt int64
|
||
}
|
||
invCounts := map[int64]int64{}
|
||
var invRows []kvCount
|
||
_ = h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.UserInventory.UserID, h.readDB.UserInventory.UserID.Count().As("cnt")).
|
||
Where(h.readDB.UserInventory.UserID.In(ids...)).
|
||
Group(h.readDB.UserInventory.UserID).
|
||
Scan(&invRows)
|
||
for _, r := range invRows {
|
||
invCounts[r.UserID] = r.Cnt
|
||
}
|
||
|
||
// 批量:道具卡(有效)
|
||
icCounts := map[int64]int64{}
|
||
var icRows []kvCount
|
||
_ = h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.UserItemCards.UserID, h.readDB.UserItemCards.UserID.Count().As("cnt")).
|
||
Where(h.readDB.UserItemCards.UserID.In(ids...)).
|
||
Where(h.readDB.UserItemCards.Status.Eq(1)).
|
||
Group(h.readDB.UserItemCards.UserID).
|
||
Scan(&icRows)
|
||
for _, r := range icRows {
|
||
icCounts[r.UserID] = r.Cnt
|
||
}
|
||
|
||
// 批量:优惠券
|
||
cpCounts := map[int64]int64{}
|
||
var cpRows []kvCount
|
||
_ = h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.UserCoupons.UserID, h.readDB.UserCoupons.UserID.Count().As("cnt")).
|
||
Where(h.readDB.UserCoupons.UserID.In(ids...)).
|
||
Group(h.readDB.UserCoupons.UserID).
|
||
Scan(&cpRows)
|
||
for _, r := range cpRows {
|
||
cpCounts[r.UserID] = r.Cnt
|
||
}
|
||
|
||
// 批量:称号列表
|
||
type titleRow struct {
|
||
UserID int64
|
||
ID int64
|
||
Name string
|
||
}
|
||
titleMap := map[int64][]userTitleBrief{}
|
||
var trows []titleRow
|
||
_ = h.readDB.UserTitles.WithContext(ctx.RequestContext()).ReadDB().
|
||
LeftJoin(h.readDB.SystemTitles, h.readDB.SystemTitles.ID.EqCol(h.readDB.UserTitles.TitleID)).
|
||
Select(h.readDB.UserTitles.UserID, h.readDB.UserTitles.TitleID.As("id"), h.readDB.SystemTitles.Name).
|
||
Where(h.readDB.UserTitles.UserID.In(ids...)).
|
||
Scan(&trows)
|
||
for _, tr := range trows {
|
||
titleMap[tr.UserID] = append(titleMap[tr.UserID], userTitleBrief{ID: tr.ID, Name: tr.Name})
|
||
}
|
||
|
||
// 批量:有效积分余额
|
||
pointsMap := map[int64]int64{}
|
||
now := time.Now()
|
||
pRows, _ := h.readDB.UserPoints.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.UserPoints.UserID.In(ids...)).
|
||
Find()
|
||
for _, r := range pRows {
|
||
if !r.ValidEnd.IsZero() && r.ValidEnd.Before(now) {
|
||
continue
|
||
}
|
||
pointsMap[r.UserID] += r.Points
|
||
}
|
||
|
||
// 批量:最后在线(各表 MAX)
|
||
type tsRow struct {
|
||
UserID int64
|
||
Ts time.Time
|
||
}
|
||
maxMap := map[int64]time.Time{}
|
||
mergeMax := func(rows []tsRow) {
|
||
for _, r := range rows {
|
||
if r.Ts.After(maxMap[r.UserID]) {
|
||
maxMap[r.UserID] = r.Ts
|
||
}
|
||
}
|
||
}
|
||
var dlMax, odMax, paidMax, plMax, icMax, ivMax []tsRow
|
||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.ActivityDrawLogs.UserID, h.readDB.ActivityDrawLogs.CreatedAt.Max().As("ts")).
|
||
Where(h.readDB.ActivityDrawLogs.UserID.In(ids...)).
|
||
Group(h.readDB.ActivityDrawLogs.UserID).
|
||
Scan(&dlMax)
|
||
mergeMax(dlMax)
|
||
_ = h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.Orders.UserID, h.readDB.Orders.UpdatedAt.Max().As("ts")).
|
||
Where(h.readDB.Orders.UserID.In(ids...)).
|
||
Group(h.readDB.Orders.UserID).
|
||
Scan(&odMax)
|
||
mergeMax(odMax)
|
||
_ = h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.Orders.UserID, h.readDB.Orders.PaidAt.Max().As("ts")).
|
||
Where(h.readDB.Orders.UserID.In(ids...)).
|
||
Group(h.readDB.Orders.UserID).
|
||
Scan(&paidMax)
|
||
mergeMax(paidMax)
|
||
_ = h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.UserPointsLedger.UserID, h.readDB.UserPointsLedger.CreatedAt.Max().As("ts")).
|
||
Where(h.readDB.UserPointsLedger.UserID.In(ids...)).
|
||
Group(h.readDB.UserPointsLedger.UserID).
|
||
Scan(&plMax)
|
||
mergeMax(plMax)
|
||
_ = h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.UserItemCards.UserID, h.readDB.UserItemCards.UpdatedAt.Max().As("ts")).
|
||
Where(h.readDB.UserItemCards.UserID.In(ids...)).
|
||
Group(h.readDB.UserItemCards.UserID).
|
||
Scan(&icMax)
|
||
mergeMax(icMax)
|
||
_ = h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.UserInventory.UserID, h.readDB.UserInventory.UpdatedAt.Max().As("ts")).
|
||
Where(h.readDB.UserInventory.UserID.In(ids...)).
|
||
Group(h.readDB.UserInventory.UserID).
|
||
Scan(&ivMax)
|
||
mergeMax(ivMax)
|
||
|
||
list := make([]newUserItem, len(rows))
|
||
for i, u := range rows {
|
||
list[i] = newUserItem{
|
||
ID: u.ID,
|
||
Nickname: u.Nickname,
|
||
Avatar: u.Avatar,
|
||
CreatedAt: u.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||
PointsBalance: pointsMap[u.ID],
|
||
InventoryCount: invCounts[u.ID],
|
||
ItemCardCount: icCounts[u.ID],
|
||
CouponCount: cpCounts[u.ID],
|
||
TitleCount: int64(len(titleMap[u.ID])),
|
||
LastOnlineAt: func() string {
|
||
t := maxMap[u.ID]
|
||
if t.IsZero() {
|
||
return ""
|
||
}
|
||
return t.Format("2006-01-02T15:04:05Z07:00")
|
||
}(),
|
||
Titles: titleMap[u.ID],
|
||
}
|
||
}
|
||
rsp.Page = req.Page
|
||
rsp.PageSize = req.PageSize
|
||
rsp.Total = total
|
||
rsp.List = list
|
||
ctx.Payload(rsp)
|
||
}
|
||
}
|
||
|
||
type drawStreamRequest struct {
|
||
SinceID *int64 `form:"since_id"`
|
||
Limit int `form:"limit"`
|
||
}
|
||
|
||
type drawStreamItem struct {
|
||
ID int64 `json:"id"`
|
||
UserID int64 `json:"userId"`
|
||
Nickname string `json:"nickname"`
|
||
IssueID int64 `json:"issueId"`
|
||
IssueName string `json:"issueName"`
|
||
ActivityName string `json:"activityName"`
|
||
IssueNumber string `json:"issueNumber"`
|
||
PrizeName string `json:"prizeName"`
|
||
IsWinner int32 `json:"isWinner"`
|
||
CreatedAt string `json:"createdAt"`
|
||
}
|
||
|
||
type drawStreamResponse struct {
|
||
List []drawStreamItem `json:"list"`
|
||
SinceID *int64 `json:"sinceId,omitempty"`
|
||
}
|
||
|
||
func (h *handler) DashboardDrawStream() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(drawStreamRequest)
|
||
rsp := new(drawStreamResponse)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
if req.Limit <= 0 {
|
||
req.Limit = 50
|
||
}
|
||
if req.Limit > 100 {
|
||
req.Limit = 100
|
||
}
|
||
q := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB()
|
||
if req.SinceID != nil {
|
||
q = q.Where(h.readDB.ActivityDrawLogs.ID.Gt(*req.SinceID))
|
||
}
|
||
type row struct {
|
||
ID int64
|
||
UserID int64
|
||
Nickname string
|
||
IssueID int64
|
||
IssueName string
|
||
ActivityName string
|
||
IssueNumber string
|
||
PrizeName string
|
||
ProductID int64
|
||
IsWinner int32
|
||
CreatedAt time.Time
|
||
}
|
||
var rows []row
|
||
err := q.
|
||
LeftJoin(h.readDB.Users, h.readDB.Users.ID.EqCol(h.readDB.ActivityDrawLogs.UserID)).
|
||
LeftJoin(h.readDB.ActivityIssues, h.readDB.ActivityIssues.ID.EqCol(h.readDB.ActivityDrawLogs.IssueID)).
|
||
LeftJoin(h.readDB.Activities, h.readDB.Activities.ID.EqCol(h.readDB.ActivityIssues.ActivityID)).
|
||
LeftJoin(h.readDB.ActivityRewardSettings, h.readDB.ActivityRewardSettings.ID.EqCol(h.readDB.ActivityDrawLogs.RewardID)).
|
||
Select(
|
||
h.readDB.ActivityDrawLogs.ID,
|
||
h.readDB.ActivityDrawLogs.UserID,
|
||
h.readDB.Users.Nickname,
|
||
h.readDB.ActivityDrawLogs.IssueID,
|
||
h.readDB.Activities.Name.As("activity_name"),
|
||
h.readDB.ActivityIssues.IssueNumber.As("issue_number"),
|
||
h.readDB.ActivityRewardSettings.ProductID.As("product_id"),
|
||
h.readDB.ActivityDrawLogs.IsWinner,
|
||
h.readDB.ActivityDrawLogs.CreatedAt,
|
||
).
|
||
Order(h.readDB.ActivityDrawLogs.ID.Desc()).
|
||
Limit(req.Limit).
|
||
Scan(&rows)
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21016, err.Error()))
|
||
return
|
||
}
|
||
// 批量获取商品名称,通过 ProductID 关联查询
|
||
productIDs := make([]int64, 0, len(rows))
|
||
for _, v := range rows {
|
||
if v.ProductID > 0 {
|
||
productIDs = append(productIDs, v.ProductID)
|
||
}
|
||
}
|
||
productNameMap := make(map[int64]string)
|
||
if len(productIDs) > 0 {
|
||
products, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.In(productIDs...)).Find()
|
||
for _, p := range products {
|
||
productNameMap[p.ID] = p.Name
|
||
}
|
||
}
|
||
list := make([]drawStreamItem, len(rows))
|
||
var maxID int64
|
||
for i, v := range rows {
|
||
iname := v.ActivityName
|
||
if iname == "" {
|
||
iname = v.IssueName
|
||
}
|
||
// 只使用商品名称
|
||
prizeName := ""
|
||
if v.ProductID > 0 {
|
||
if pn, ok := productNameMap[v.ProductID]; ok {
|
||
prizeName = pn
|
||
}
|
||
}
|
||
list[i] = drawStreamItem{
|
||
ID: v.ID,
|
||
UserID: v.UserID,
|
||
Nickname: v.Nickname,
|
||
IssueID: v.IssueID,
|
||
IssueName: func() string {
|
||
if v.ActivityName != "" && v.IssueNumber != "" {
|
||
return v.ActivityName + "-" + v.IssueNumber
|
||
}
|
||
return iname
|
||
}(),
|
||
ActivityName: v.ActivityName,
|
||
IssueNumber: v.IssueNumber,
|
||
PrizeName: prizeName,
|
||
IsWinner: v.IsWinner,
|
||
CreatedAt: v.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||
}
|
||
if v.ID > maxID {
|
||
maxID = v.ID
|
||
}
|
||
}
|
||
if len(rows) > 0 {
|
||
rsp.SinceID = &maxID
|
||
}
|
||
rsp.List = list
|
||
ctx.Payload(rsp)
|
||
}
|
||
}
|
||
|
||
type todosRequest struct {
|
||
Limit int `form:"limit"`
|
||
}
|
||
|
||
type todoItem struct {
|
||
UserID int64 `json:"userId"`
|
||
Nickname string `json:"nickname"`
|
||
Avatar string `json:"avatar"`
|
||
TaskType string `json:"taskType"`
|
||
TaskLabel string `json:"taskLabel"`
|
||
}
|
||
|
||
type todosResponse struct {
|
||
List []todoItem `json:"list"`
|
||
}
|
||
|
||
func (h *handler) DashboardTodos() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(todosRequest)
|
||
rsp := new(todosResponse)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
if req.Limit <= 0 {
|
||
req.Limit = 50
|
||
}
|
||
if req.Limit > 100 {
|
||
req.Limit = 100
|
||
}
|
||
base := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Order(h.readDB.Users.ID.Desc())
|
||
rows, err := base.Limit(req.Limit).Find()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21017, err.Error()))
|
||
return
|
||
}
|
||
out := make([]todoItem, 0, len(rows))
|
||
for _, u := range rows {
|
||
dlCount, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityDrawLogs.UserID.Eq(u.ID)).Count()
|
||
if dlCount == 0 {
|
||
out = append(out, todoItem{UserID: u.ID, Nickname: u.Nickname, Avatar: u.Avatar, TaskType: "undrawn", TaskLabel: "从未参与抽奖"})
|
||
}
|
||
}
|
||
rsp.List = out
|
||
ctx.Payload(rsp)
|
||
}
|
||
}
|
||
|
||
func parseRange(rangeType, startS, endS string) (time.Time, time.Time) {
|
||
now := time.Now()
|
||
switch rangeType {
|
||
case "today":
|
||
s := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
e := s.Add(24 * time.Hour).Add(-time.Second)
|
||
return s, e
|
||
case "24h":
|
||
e := now
|
||
s := e.Add(-24 * time.Hour)
|
||
return s, e
|
||
case "30d":
|
||
e := now
|
||
s := e.Add(-30 * 24 * time.Hour)
|
||
return s, e
|
||
case "custom":
|
||
if startS != "" && endS != "" {
|
||
if st, err := time.Parse("2006-01-02", startS); err == nil {
|
||
if et, err := time.Parse("2006-01-02", endS); err == nil {
|
||
et = et.Add(24 * time.Hour).Add(-time.Second)
|
||
if et.Sub(st) > 31*24*time.Hour {
|
||
et = st.Add(30 * 24 * time.Hour).Add(-time.Second)
|
||
}
|
||
return st, et
|
||
}
|
||
}
|
||
}
|
||
fallthrough
|
||
default:
|
||
e := now
|
||
s := e.Add(-7 * 24 * time.Hour)
|
||
return s, e
|
||
}
|
||
}
|
||
|
||
func previousWindow(start, end time.Time) (time.Time, time.Time) {
|
||
dur := end.Sub(start) + time.Second
|
||
ps := start.Add(-dur)
|
||
pe := start.Add(-time.Second)
|
||
return ps, pe
|
||
}
|
||
|
||
func percentChange(prev, cur int64) string {
|
||
if prev <= 0 {
|
||
return "+0%"
|
||
}
|
||
diff := float64(cur-prev) / float64(prev) * 100
|
||
if diff >= 0 {
|
||
return "+" + strconv.Itoa(int(diff)) + "%"
|
||
}
|
||
return "-" + strconv.Itoa(int(-diff)) + "%"
|
||
}
|
||
|
||
func daysBetween(start, end time.Time) []time.Time {
|
||
s := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
|
||
e := time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, end.Location())
|
||
var out []time.Time
|
||
for d := s; !d.After(e); d = d.Add(24 * time.Hour) {
|
||
out = append(out, d)
|
||
}
|
||
return out
|
||
}
|
||
|
||
type bucket struct {
|
||
Label string
|
||
Start time.Time
|
||
End time.Time
|
||
}
|
||
|
||
func normalizeGranularity(in string) string {
|
||
switch in {
|
||
case "week":
|
||
return "week"
|
||
case "month":
|
||
return "month"
|
||
case "hour":
|
||
return "hour"
|
||
case "minute":
|
||
return "minute"
|
||
default:
|
||
return "day"
|
||
}
|
||
}
|
||
|
||
func buildBuckets(start, end time.Time, gran string) []bucket {
|
||
if gran == "day" {
|
||
days := daysBetween(start, end)
|
||
out := make([]bucket, 0, len(days))
|
||
for _, d := range days {
|
||
ds := time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location())
|
||
de := ds.Add(24 * time.Hour).Add(-time.Second)
|
||
out = append(out, bucket{Label: ds.Format("2006-01-02"), Start: ds, End: de})
|
||
}
|
||
return out
|
||
}
|
||
if gran == "week" {
|
||
s := start
|
||
for s.Weekday() != time.Monday {
|
||
s = s.Add(-24 * time.Hour)
|
||
}
|
||
out := []bucket{}
|
||
for cur := s; cur.Before(end) || cur.Equal(end); cur = cur.Add(7 * 24 * time.Hour) {
|
||
ds := time.Date(cur.Year(), cur.Month(), cur.Day(), 0, 0, 0, 0, cur.Location())
|
||
de := ds.Add(7 * 24 * time.Hour).Add(-time.Second)
|
||
label := ds.Format("2006-01-02")
|
||
if de.After(end) {
|
||
de = end
|
||
}
|
||
out = append(out, bucket{Label: label, Start: ds, End: de})
|
||
if de.Equal(end) {
|
||
break
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
if gran == "hour" {
|
||
out := []bucket{}
|
||
// 步长 1 小时
|
||
for cur := start; !cur.After(end); cur = cur.Add(time.Hour) {
|
||
s := time.Date(cur.Year(), cur.Month(), cur.Day(), cur.Hour(), 0, 0, 0, cur.Location())
|
||
de := s.Add(time.Hour).Add(-time.Second)
|
||
out = append(out, bucket{Label: s.Format("01-02 15h"), Start: s, End: de})
|
||
}
|
||
return out
|
||
}
|
||
if gran == "minute" {
|
||
out := []bucket{}
|
||
// 步长 1 分钟
|
||
for cur := start; !cur.After(end); cur = cur.Add(time.Minute) {
|
||
s := time.Date(cur.Year(), cur.Month(), cur.Day(), cur.Hour(), cur.Minute(), 0, 0, cur.Location())
|
||
de := s.Add(time.Minute).Add(-time.Second)
|
||
out = append(out, bucket{Label: s.Format("01-02 15:04"), Start: s, End: de})
|
||
}
|
||
return out
|
||
}
|
||
s := time.Date(start.Year(), start.Month(), 1, 0, 0, 0, 0, start.Location())
|
||
out := []bucket{}
|
||
for cur := s; cur.Before(end) || cur.Equal(end); cur = cur.AddDate(0, 1, 0) {
|
||
ds := time.Date(cur.Year(), cur.Month(), 1, 0, 0, 0, 0, cur.Location())
|
||
de := ds.AddDate(0, 1, 0).Add(-time.Second)
|
||
label := ds.Format("2006-01")
|
||
if de.After(end) {
|
||
de = end
|
||
}
|
||
out = append(out, bucket{Label: label, Start: ds, End: de})
|
||
if de.Equal(end) {
|
||
break
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
type funnelItem struct {
|
||
Stage string `json:"stage"`
|
||
Count int64 `json:"count"`
|
||
Rate float64 `json:"rate"`
|
||
LostCount int64 `json:"lostCount"`
|
||
}
|
||
|
||
func (h *handler) DashboardOrderFunnel() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
s, e := parseRange(ctx.Request().URL.Query().Get("rangeType"), ctx.Request().URL.Query().Get("start"), ctx.Request().URL.Query().Get("end"))
|
||
visitors, _ := h.readDB.LogRequest.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.LogRequest.CreatedAt.Gte(s)).
|
||
Where(h.readDB.LogRequest.CreatedAt.Lte(e)).
|
||
Where(h.readDB.LogRequest.Path.Like("/api/app/%")).
|
||
Count()
|
||
orders, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.Orders.CreatedAt.Gte(s)).
|
||
Where(h.readDB.Orders.CreatedAt.Lte(e)).
|
||
Count()
|
||
payments, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.Orders.Status.Eq(2)).
|
||
Where(h.readDB.Orders.PaidAt.Gte(s)).
|
||
Where(h.readDB.Orders.PaidAt.Lte(e)).
|
||
Count()
|
||
shipped, _ := h.readDB.ShippingRecords.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.ShippingRecords.Status.In(2, 3)).
|
||
Where(h.readDB.ShippingRecords.UpdatedAt.Gte(s)).
|
||
Where(h.readDB.ShippingRecords.UpdatedAt.Lte(e)).
|
||
Count()
|
||
consumed, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.Orders.Status.Eq(2), h.readDB.Orders.IsConsumed.Eq(1)).
|
||
Where(h.readDB.Orders.UpdatedAt.Gte(s)).
|
||
Where(h.readDB.Orders.UpdatedAt.Lte(e)).
|
||
Count()
|
||
completions := shipped + consumed
|
||
stages := []struct {
|
||
name string
|
||
val int64
|
||
}{
|
||
{"访问用户", visitors}, {"下单用户", orders}, {"支付用户", payments}, {"完成订单", completions},
|
||
}
|
||
out := make([]funnelItem, 0, len(stages))
|
||
var prev int64
|
||
for i, st := range stages {
|
||
var rate float64
|
||
var lost int64
|
||
if i == 0 {
|
||
rate = 100
|
||
lost = 0
|
||
} else {
|
||
if prev > 0 {
|
||
rate = float64(st.val) / float64(prev) * 100
|
||
}
|
||
lost = prev - st.val
|
||
if lost < 0 {
|
||
lost = 0
|
||
}
|
||
}
|
||
out = append(out, funnelItem{Stage: st.name, Count: st.val, Rate: float64(int(rate*10)) / 10.0, LostCount: lost})
|
||
prev = st.val
|
||
}
|
||
ctx.Payload(out)
|
||
}
|
||
}
|
||
|
||
type activitiesItem struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Type string `json:"type"`
|
||
StartTime string `json:"startTime"`
|
||
EndTime string `json:"endTime"`
|
||
Status string `json:"status"`
|
||
TotalDraws int64 `json:"totalDraws"`
|
||
TotalParticipants int64 `json:"totalParticipants"`
|
||
}
|
||
|
||
func (h *handler) DashboardActivities() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
rows, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).ReadDB().Find()
|
||
out := make([]activitiesItem, len(rows))
|
||
for i, a := range rows {
|
||
issues, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityIssues.ActivityID.Eq(a.ID)).Find()
|
||
var drawTotal int64
|
||
var participants int64
|
||
for _, iss := range issues {
|
||
dt, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityDrawLogs.IssueID.Eq(iss.ID)).Count()
|
||
drawTotal += dt
|
||
pc, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityDrawLogs.IssueID.Eq(iss.ID)).Distinct(h.readDB.ActivityDrawLogs.UserID).Count()
|
||
participants += pc
|
||
}
|
||
status := "active"
|
||
if a.EndTime.Before(time.Now()) {
|
||
status = "ended"
|
||
}
|
||
out[i] = activitiesItem{ID: a.ID, Name: a.Name, Type: "转盘抽奖", StartTime: a.StartTime.Format("2006-01-02 15:04:05"), EndTime: a.EndTime.Format("2006-01-02 15:04:05"), Status: status, TotalDraws: drawTotal, TotalParticipants: participants}
|
||
}
|
||
ctx.Payload(out)
|
||
}
|
||
}
|
||
|
||
type activityPrize struct {
|
||
PrizeID int64 `json:"prizeId"`
|
||
PrizeName string `json:"prizeName"`
|
||
PrizeLevel int32 `json:"prizeLevel"`
|
||
PrizeType string `json:"prizeType"`
|
||
PrizeValue int64 `json:"prizeValue"`
|
||
TotalQuantity int64 `json:"totalQuantity"`
|
||
IssuedQuantity int64 `json:"issuedQuantity"`
|
||
DrawCount int64 `json:"drawCount"` // 该奖品所属期数的总抽奖次数
|
||
WinCount int64 `json:"winCount"` // 该奖品的中奖次数
|
||
WinRate float64 `json:"winRate"` // 中奖率 (WinCount / DrawCount)
|
||
Probability float64 `json:"probability"` // 配置概率 (Weight / IssueTotalWeight)
|
||
ActualProbability float64 `json:"actualProbability"` // 实际概率 (同 WinRate)
|
||
Cost int64 `json:"cost"`
|
||
}
|
||
|
||
type activityInfo struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Type string `json:"type"`
|
||
StartTime string `json:"startTime"`
|
||
EndTime string `json:"endTime"`
|
||
Status string `json:"status"`
|
||
TotalDraws int64 `json:"totalDraws"`
|
||
TotalParticipants int64 `json:"totalParticipants"`
|
||
PriceDraw int64 `json:"priceDraw"`
|
||
}
|
||
|
||
type activityPrizeAnalysis struct {
|
||
Activity activityInfo `json:"activity"`
|
||
Prizes []activityPrize `json:"prizes"`
|
||
Summary struct {
|
||
TotalCost int64 `json:"totalCost"`
|
||
AvgWinRate float64 `json:"avgWinRate"`
|
||
MaxWinRate float64 `json:"maxWinRate"`
|
||
MinWinRate float64 `json:"minWinRate"`
|
||
} `json:"summary"`
|
||
}
|
||
|
||
func (h *handler) DashboardActivityPrizeAnalysis() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
aidS := ctx.Request().URL.Query().Get("activity_id")
|
||
if aidS == "" {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "缺少活动ID"))
|
||
return
|
||
}
|
||
aid, err := strconv.ParseInt(aidS, 10, 64)
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID格式错误"))
|
||
return
|
||
}
|
||
|
||
// 1. 获取活动信息
|
||
act, err := h.readDB.Activities.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Activities.ID.Eq(aid)).First()
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21020, "活动不存在"))
|
||
return
|
||
}
|
||
|
||
// 2. 获取该活动下的所有期数 (Issues)
|
||
issues, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityIssues.ActivityID.Eq(aid)).Find()
|
||
if len(issues) == 0 {
|
||
ctx.Payload(activityPrizeAnalysis{Activity: activityInfo{ID: act.ID, Name: act.Name}})
|
||
return
|
||
}
|
||
issueIDs := make([]int64, len(issues))
|
||
for i, v := range issues {
|
||
issueIDs[i] = v.ID
|
||
}
|
||
|
||
// 3. 批量获取所有奖品配置 (Rewards)
|
||
rsAll, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityRewardSettings.IssueID.In(issueIDs...)).Find()
|
||
|
||
// 收集 Product IDs 以便批量查询价格
|
||
productIDs := make([]int64, 0)
|
||
// 计算每期的总权重 (IssueID -> TotalWeight)
|
||
issueTotalWeights := make(map[int64]int64)
|
||
for _, r := range rsAll {
|
||
if r.ProductID != 0 {
|
||
productIDs = append(productIDs, r.ProductID)
|
||
}
|
||
issueTotalWeights[r.IssueID] += int64(r.Weight)
|
||
}
|
||
|
||
// 4. 批量查询商品价格和名称 (Product Price and Name)
|
||
priceMap := make(map[int64]int64)
|
||
productNameMap := make(map[int64]string)
|
||
if len(productIDs) > 0 {
|
||
products, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.In(productIDs...)).Find()
|
||
for _, p := range products {
|
||
priceMap[p.ID] = p.Price
|
||
productNameMap[p.ID] = p.Name
|
||
}
|
||
}
|
||
|
||
// 5. 批量聚合统计数据 (Draw Logs)
|
||
// 5.1 每期的总抽奖数 (IssueID -> Count)
|
||
type countResult struct {
|
||
Key int64
|
||
Count int64
|
||
}
|
||
drawCounts := make(map[int64]int64)
|
||
var dcRows []countResult
|
||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.ActivityDrawLogs.IssueID.As("key"), h.readDB.ActivityDrawLogs.ID.Count().As("count")).
|
||
Where(h.readDB.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||
Group(h.readDB.ActivityDrawLogs.IssueID).
|
||
Scan(&dcRows)
|
||
for _, r := range dcRows {
|
||
drawCounts[r.Key] = r.Count
|
||
}
|
||
|
||
// 5.2 每个奖品的中奖数 (RewardID -> Count)
|
||
winCounts := make(map[int64]int64)
|
||
var wcRows []countResult
|
||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||
Select(h.readDB.ActivityDrawLogs.RewardID.As("key"), h.readDB.ActivityDrawLogs.ID.Count().As("count")).
|
||
Where(h.readDB.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||
Where(h.readDB.ActivityDrawLogs.IsWinner.Eq(1)).
|
||
Group(h.readDB.ActivityDrawLogs.RewardID).
|
||
Scan(&wcRows)
|
||
for _, r := range wcRows {
|
||
winCounts[r.Key] = r.Count
|
||
}
|
||
|
||
// 5.3 活动总参与人数 (Distinct UserID)
|
||
participants, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||
Distinct(h.readDB.ActivityDrawLogs.UserID).
|
||
Count()
|
||
|
||
// 5.4 活动总抽奖次数 (Sum of Issue Draw Counts)
|
||
var totalActivityDraws int64
|
||
for _, c := range drawCounts {
|
||
totalActivityDraws += c
|
||
}
|
||
|
||
// 6. 组装数据
|
||
prizes := make([]activityPrize, len(rsAll))
|
||
var totalCost int64
|
||
var winRates []float64
|
||
|
||
for i, r := range rsAll {
|
||
dc := drawCounts[r.IssueID] // 该期的总抽奖数
|
||
wc := winCounts[r.ID] // 该奖品的中奖数
|
||
price := priceMap[r.ProductID]
|
||
|
||
issued := r.OriginalQty - r.Quantity
|
||
|
||
// 计算配置概率:该奖品权重 / 该期总权重
|
||
var prob float64
|
||
if w := issueTotalWeights[r.IssueID]; w > 0 {
|
||
prob = float64(r.Weight) / float64(w) * 100
|
||
}
|
||
|
||
// 计算实际中奖率:中奖数 / 该期总抽奖数
|
||
var actual float64
|
||
if dc > 0 {
|
||
actual = float64(wc) / float64(dc) * 100
|
||
}
|
||
|
||
prizes[i] = activityPrize{
|
||
PrizeID: r.ID,
|
||
PrizeName: productNameMap[r.ProductID],
|
||
PrizeLevel: r.Level,
|
||
PrizeType: func() string {
|
||
if r.ProductID != 0 {
|
||
return "实物奖品"
|
||
}
|
||
return "虚拟/道具"
|
||
}(),
|
||
PrizeValue: price,
|
||
TotalQuantity: r.OriginalQty,
|
||
IssuedQuantity: issued,
|
||
DrawCount: dc,
|
||
WinCount: wc,
|
||
WinRate: actual,
|
||
Probability: prob,
|
||
ActualProbability: actual,
|
||
Cost: price,
|
||
}
|
||
|
||
totalCost += price * wc
|
||
winRates = append(winRates, actual)
|
||
}
|
||
|
||
// 7. 计算汇总指标
|
||
status := "active"
|
||
if act.EndTime.Before(time.Now()) {
|
||
status = "ended"
|
||
}
|
||
|
||
var avgW, maxW, minW float64
|
||
if len(winRates) > 0 {
|
||
var sum float64
|
||
maxW = winRates[0]
|
||
minW = winRates[0]
|
||
for _, w := range winRates {
|
||
sum += w
|
||
if w > maxW {
|
||
maxW = w
|
||
}
|
||
if w < minW {
|
||
minW = w
|
||
}
|
||
}
|
||
avgW = sum / float64(len(winRates))
|
||
}
|
||
|
||
out := activityPrizeAnalysis{}
|
||
out.Activity = activityInfo{
|
||
ID: act.ID,
|
||
Name: act.Name,
|
||
Type: "转盘抽奖",
|
||
StartTime: act.StartTime.Format("2006-01-02 15:04:05"),
|
||
EndTime: act.EndTime.Format("2006-01-02 15:04:05"),
|
||
Status: status,
|
||
TotalDraws: totalActivityDraws,
|
||
TotalParticipants: participants,
|
||
PriceDraw: act.PriceDraw,
|
||
}
|
||
out.Prizes = prizes
|
||
out.Summary.TotalCost = totalCost
|
||
out.Summary.AvgWinRate = avgW
|
||
out.Summary.MaxWinRate = maxW
|
||
out.Summary.MinWinRate = minW
|
||
|
||
ctx.Payload(out)
|
||
}
|
||
}
|
||
|
||
type userOverviewResponse struct {
|
||
Chart []struct {
|
||
Date string `json:"date"`
|
||
Value int64 `json:"value"`
|
||
} `json:"chart"`
|
||
Metrics struct {
|
||
TotalUsers int64 `json:"totalUsers"`
|
||
TotalVisits int64 `json:"totalVisits"`
|
||
DailyVisits int64 `json:"dailyVisits"`
|
||
WeeklyGrowth string `json:"weeklyGrowth"`
|
||
} `json:"metrics"`
|
||
}
|
||
|
||
func (h *handler) DashboardUserOverview() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
totalUsers, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Count()
|
||
todayS, todayE := parseRange("today", "", "")
|
||
dailyVisits, _ := h.readDB.LogRequest.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.LogRequest.CreatedAt.Gte(todayS)).Where(h.readDB.LogRequest.CreatedAt.Lte(todayE)).Where(h.readDB.LogRequest.Path.Like("/api/app/%")).Count()
|
||
s7, e7 := parseRange("7d", "", "")
|
||
lastS := s7.Add(-7 * 24 * time.Hour)
|
||
lastE := s7.Add(-time.Second)
|
||
curVisits, _ := h.readDB.LogRequest.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.LogRequest.CreatedAt.Gte(s7)).Where(h.readDB.LogRequest.CreatedAt.Lte(e7)).Where(h.readDB.LogRequest.Path.Like("/api/app/%")).Count()
|
||
prevVisits, _ := h.readDB.LogRequest.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.LogRequest.CreatedAt.Gte(lastS)).Where(h.readDB.LogRequest.CreatedAt.Lte(lastE)).Where(h.readDB.LogRequest.Path.Like("/api/app/%")).Count()
|
||
var wg string
|
||
if prevVisits > 0 {
|
||
diff := float64(curVisits-prevVisits) / float64(prevVisits) * 100
|
||
if diff >= 0 {
|
||
wg = "+" + strconv.Itoa(int(diff)) + "%"
|
||
} else {
|
||
wg = "-" + strconv.Itoa(int(-diff)) + "%"
|
||
}
|
||
} else {
|
||
wg = "+0%"
|
||
}
|
||
totalVisits, _ := h.readDB.LogRequest.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.LogRequest.Path.Like("/api/app/%")).Count()
|
||
type chartPoint struct {
|
||
Date string `json:"date"`
|
||
Value int64 `json:"value"`
|
||
}
|
||
chart := []chartPoint{}
|
||
now := time.Now()
|
||
for i := 8; i >= 0; i-- {
|
||
dt := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, now.Location())
|
||
dn := dt.AddDate(0, 1, 0).Add(-time.Second)
|
||
c, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.CreatedAt.Gte(dt)).Where(h.readDB.Users.CreatedAt.Lte(dn)).Count()
|
||
chart = append(chart, chartPoint{Date: dt.Format("2006-01"), Value: c})
|
||
}
|
||
out := userOverviewResponse{}
|
||
out.Chart = make([]struct {
|
||
Date string `json:"date"`
|
||
Value int64 `json:"value"`
|
||
}, len(chart))
|
||
for i := range chart {
|
||
out.Chart[i] = struct {
|
||
Date string `json:"date"`
|
||
Value int64 `json:"value"`
|
||
}{Date: chart[i].Date, Value: chart[i].Value}
|
||
}
|
||
out.Metrics.TotalUsers = totalUsers
|
||
out.Metrics.TotalVisits = totalVisits
|
||
out.Metrics.DailyVisits = dailyVisits
|
||
out.Metrics.WeeklyGrowth = wg
|
||
ctx.Payload(out)
|
||
}
|
||
}
|