feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -152,20 +152,36 @@ func (s *PointsService) Ledger(userID uint, page, size int) ([]LedgerItem, int64
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// DeltaDay 单日积分发放与消耗
|
||||
type DeltaDay struct {
|
||||
Date string `json:"date"`
|
||||
Granted int64 `json:"granted"`
|
||||
Spent int64 `json:"spent"`
|
||||
}
|
||||
|
||||
// PointsEconomyStats 全站积分与类型帖概览(管理端)
|
||||
type PointsEconomyStats struct {
|
||||
TotalBalance int64 `json:"total_balance"` // 用户余额合计
|
||||
LedgerCount int64 `json:"ledger_count"` // 流水条数
|
||||
CheckinToday int64 `json:"checkin_today"` // 今日签到人数
|
||||
OpenBounties int64 `json:"open_bounties"` // 未结算悬赏帖
|
||||
EscrowedPoints int64 `json:"escrowed_points"` // 托管中悬赏积分(估算)
|
||||
PostsByType map[string]int64 `json:"posts_by_type"` // 各类型帖数量
|
||||
RecentLedger []LedgerItem `json:"recent_ledger"` // 最近全局流水(脱敏 note)
|
||||
TotalBalance int64 `json:"total_balance"`
|
||||
LedgerCount int64 `json:"ledger_count"`
|
||||
CheckinToday int64 `json:"checkin_today"`
|
||||
OpenBounties int64 `json:"open_bounties"`
|
||||
EscrowedPoints int64 `json:"escrowed_points"`
|
||||
PostsByType map[string]int64 `json:"posts_by_type"`
|
||||
RecentLedger []LedgerItem `json:"recent_ledger"`
|
||||
CheckinSeries []DayCount `json:"checkin_series"`
|
||||
LedgerByReason []NamedCount `json:"ledger_by_reason"`
|
||||
DeltaSeries []DeltaDay `json:"delta_series"`
|
||||
NetDelta7d int64 `json:"net_delta_7d"`
|
||||
}
|
||||
|
||||
// AdminEconomyStats 管理端经济看板
|
||||
func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
|
||||
st := &PointsEconomyStats{PostsByType: map[string]int64{}}
|
||||
st := &PointsEconomyStats{
|
||||
PostsByType: map[string]int64{},
|
||||
CheckinSeries: make([]DayCount, 0),
|
||||
LedgerByReason: make([]NamedCount, 0),
|
||||
DeltaSeries: make([]DeltaDay, 0),
|
||||
}
|
||||
if err := s.db.Model(&model.User{}).Where("deleted_at IS NULL").
|
||||
Select("COALESCE(SUM(points),0)").Scan(&st.TotalBalance).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -173,14 +189,11 @@ func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
|
||||
if err := s.db.Model(&model.PointLedger{}).Count(&st.LedgerCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
// 用日期字符串更稳妥(与 checkin 一致用 date)
|
||||
if err := s.db.Model(&model.Checkin{}).
|
||||
Where("checkin_date::date = CURRENT_DATE").
|
||||
Count(&st.CheckinToday).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = today
|
||||
|
||||
type row struct {
|
||||
PostType string
|
||||
@@ -198,7 +211,6 @@ func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
|
||||
st.PostsByType[pt] += r.Cnt
|
||||
}
|
||||
|
||||
// 未结算悬赏:type_meta 含 escrowed true 且未 accepted/refunded —— 用简易扫描估算
|
||||
var bountyPosts []model.Post
|
||||
if err := s.db.Select("id, type_meta").
|
||||
Where("post_type = ? AND deleted_at IS NULL", model.PostTypeBounty).
|
||||
@@ -227,5 +239,77 @@ func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
|
||||
RefType: r.RefType, RefID: r.RefID, Note: r.Note, CreatedAt: r.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
day0 := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
start14 := day0.AddDate(0, 0, -13)
|
||||
start7 := day0.AddDate(0, 0, -6)
|
||||
|
||||
type dayCnt struct {
|
||||
Day string
|
||||
Cnt int64
|
||||
}
|
||||
var checkinRows []dayCnt
|
||||
if err := s.db.Raw(`
|
||||
SELECT to_char(checkin_date, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM checkins
|
||||
WHERE checkin_date >= ? AND checkin_date <= ?
|
||||
GROUP BY day ORDER BY day`, start14, day0).Scan(&checkinRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkinMap := map[string]int64{}
|
||||
for _, r := range checkinRows {
|
||||
checkinMap[r.Day] = r.Cnt
|
||||
}
|
||||
st.CheckinSeries = fillDaySeries(start14, day0, checkinMap)
|
||||
|
||||
type reasonRow struct {
|
||||
Reason string
|
||||
Cnt int64
|
||||
}
|
||||
var reasons []reasonRow
|
||||
if err := s.db.Model(&model.PointLedger{}).
|
||||
Select("reason, count(*) as cnt").
|
||||
Group("reason").
|
||||
Order("cnt DESC").
|
||||
Limit(12).Scan(&reasons).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range reasons {
|
||||
st.LedgerByReason = append(st.LedgerByReason, NamedCount{
|
||||
Key: r.Reason, Label: r.Reason, Count: r.Cnt,
|
||||
})
|
||||
}
|
||||
|
||||
type deltaRow struct {
|
||||
Day string
|
||||
Granted int64
|
||||
Spent int64
|
||||
}
|
||||
var deltas []deltaRow
|
||||
if err := s.db.Raw(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day,
|
||||
COALESCE(SUM(CASE WHEN delta > 0 THEN delta ELSE 0 END), 0) AS granted,
|
||||
COALESCE(SUM(CASE WHEN delta < 0 THEN -delta ELSE 0 END), 0) AS spent
|
||||
FROM point_ledgers
|
||||
WHERE created_at >= ? AND created_at < ?
|
||||
GROUP BY day ORDER BY day`,
|
||||
start14, day0.AddDate(0, 0, 1)).Scan(&deltas).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deltaMap := map[string]DeltaDay{}
|
||||
for _, r := range deltas {
|
||||
deltaMap[r.Day] = DeltaDay{Date: r.Day, Granted: r.Granted, Spent: r.Spent}
|
||||
}
|
||||
for d := start14; !d.After(day0); d = d.AddDate(0, 0, 1) {
|
||||
k := dateKey(d)
|
||||
dd := deltaMap[k]
|
||||
dd.Date = k
|
||||
st.DeltaSeries = append(st.DeltaSeries, dd)
|
||||
if !d.Before(start7) {
|
||||
st.NetDelta7d += dd.Granted - dd.Spent
|
||||
}
|
||||
}
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user