Skip to content

Commit f29dfdb

Browse files
committed
feat: add tenant analytics API
1 parent a9f69c9 commit f29dfdb

5 files changed

Lines changed: 214 additions & 5 deletions

File tree

internal/api/sys_user.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ func (*UserApi) HandleUserListByPage(c *gin.Context) {
155155
c.Set("data", userList)
156156
}
157157

158+
// HandleTenantStatistics 获取租户数据概览
159+
// @Router /api/v1/user/tenant/statistics [get]
160+
func (*UserApi) HandleTenantStatistics(c *gin.Context) {
161+
userClaims := c.MustGet("claims").(*utils.UserClaims)
162+
163+
statistics, err := service.GroupApp.User.GetTenantStatistics(c.Request.Context(), userClaims)
164+
if err != nil {
165+
c.Error(err)
166+
return
167+
}
168+
169+
c.Set("data", statistics)
170+
}
171+
158172
// UpdateUser 修改用户信息
159173
// @Router /api/v1/user [put]
160174
func (*UserApi) UpdateUser(c *gin.Context) {

internal/dal/users.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,24 @@ func GetUserListByPageWithAddress(userListReq *model.UserListReq, claims *utils.
347347
if userListReq.Status != nil && *userListReq.Status != "" {
348348
queryBuilder = queryBuilder.Where(q.Status.Eq(*userListReq.Status))
349349
}
350+
if userListReq.ActivityScope != nil && *userListReq.ActivityScope != "" && claims.Authority == SYS_ADMIN {
351+
now := time.Now()
352+
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
353+
tomorrowStart := todayStart.AddDate(0, 0, 1)
354+
last7Start := todayStart.AddDate(0, 0, -6)
355+
last30Start := todayStart.AddDate(0, 0, -29)
356+
357+
switch *userListReq.ActivityScope {
358+
case model.TenantActivityScopeToday:
359+
queryBuilder = queryBuilder.Where(q.LastVisitTime.Gte(todayStart), q.LastVisitTime.Lt(tomorrowStart))
360+
case model.TenantActivityScopeLast7Days:
361+
queryBuilder = queryBuilder.Where(q.LastVisitTime.Gte(last7Start), q.LastVisitTime.Lt(tomorrowStart))
362+
case model.TenantActivityScopeLast30Days:
363+
queryBuilder = queryBuilder.Where(q.LastVisitTime.Gte(last30Start), q.LastVisitTime.Lt(tomorrowStart))
364+
case model.TenantActivityScopeInactiveOver30Days:
365+
queryBuilder = queryBuilder.Where(q.Where(q.LastVisitTime.Lt(last30Start)).Or(q.LastVisitTime.IsNull()))
366+
}
367+
}
350368

351369
// 新增扩展字段过滤
352370
if userListReq.Organization != nil && *userListReq.Organization != "" {
@@ -400,6 +418,89 @@ func GetUserListByPageWithAddress(userListReq *model.UserListReq, claims *utils.
400418
return count, userList, nil
401419
}
402420

421+
func GetTenantStatisticsSummary(
422+
ctx context.Context,
423+
todayStart time.Time,
424+
tomorrowStart time.Time,
425+
last7Start time.Time,
426+
last30Start time.Time,
427+
) (model.TenantStatisticsSummaryRes, model.TenantRevisitStatisticsRes, error) {
428+
type summaryRow struct {
429+
Total int64 `gorm:"column:total"`
430+
ActiveToday int64 `gorm:"column:active_today"`
431+
ActiveLast7Days int64 `gorm:"column:active_last_7_days"`
432+
ActiveLast30Days int64 `gorm:"column:active_last_30_days"`
433+
InactiveOver30Days int64 `gorm:"column:inactive_over_30_days"`
434+
Revisited int64 `gorm:"column:revisited"`
435+
NotRevisited int64 `gorm:"column:not_revisited"`
436+
}
437+
438+
var row summaryRow
439+
err := global.DB.WithContext(ctx).
440+
Model(&model.User{}).
441+
Select(`
442+
COUNT(*) AS total,
443+
COALESCE(SUM(CASE WHEN last_visit_time >= ? AND last_visit_time < ? THEN 1 ELSE 0 END), 0) AS active_today,
444+
COALESCE(SUM(CASE WHEN last_visit_time >= ? AND last_visit_time < ? THEN 1 ELSE 0 END), 0) AS active_last_7_days,
445+
COALESCE(SUM(CASE WHEN last_visit_time >= ? AND last_visit_time < ? THEN 1 ELSE 0 END), 0) AS active_last_30_days,
446+
COALESCE(SUM(CASE WHEN last_visit_time < ? OR last_visit_time IS NULL THEN 1 ELSE 0 END), 0) AS inactive_over_30_days,
447+
COALESCE(SUM(CASE WHEN last_visit_time IS NOT NULL THEN 1 ELSE 0 END), 0) AS revisited,
448+
COALESCE(SUM(CASE WHEN last_visit_time IS NULL THEN 1 ELSE 0 END), 0) AS not_revisited
449+
`,
450+
todayStart, tomorrowStart,
451+
last7Start, tomorrowStart,
452+
last30Start, tomorrowStart,
453+
last30Start,
454+
).
455+
Where("authority = ?", TENANT_ADMIN).
456+
Scan(&row).Error
457+
if err != nil {
458+
return model.TenantStatisticsSummaryRes{}, model.TenantRevisitStatisticsRes{}, err
459+
}
460+
461+
return model.TenantStatisticsSummaryRes{
462+
Total: row.Total,
463+
ActiveToday: row.ActiveToday,
464+
ActiveLast7Days: row.ActiveLast7Days,
465+
ActiveLast30Days: row.ActiveLast30Days,
466+
InactiveOver30Days: row.InactiveOver30Days,
467+
}, model.TenantRevisitStatisticsRes{
468+
Revisited: row.Revisited,
469+
NotRevisited: row.NotRevisited,
470+
}, nil
471+
}
472+
473+
func GetTenantCountBefore(ctx context.Context, before time.Time) (int64, error) {
474+
return query.User.WithContext(ctx).
475+
Where(query.User.Authority.Eq(TENANT_ADMIN), query.User.CreatedAt.Lt(before)).
476+
Count()
477+
}
478+
479+
func GetTenantDailyNewCounts(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) {
480+
type dailyNewCountRow struct {
481+
Date string `gorm:"column:date"`
482+
Total int64 `gorm:"column:total"`
483+
}
484+
485+
var rows []dailyNewCountRow
486+
err := global.DB.WithContext(ctx).
487+
Model(&model.User{}).
488+
Select("TO_CHAR(created_at, 'YYYY-MM-DD') AS date, COUNT(*) AS total").
489+
Where("authority = ? AND created_at >= ? AND created_at < ?", TENANT_ADMIN, start, end).
490+
Group("TO_CHAR(created_at, 'YYYY-MM-DD')").
491+
Order("date ASC").
492+
Scan(&rows).Error
493+
if err != nil {
494+
return nil, err
495+
}
496+
497+
result := make(map[string]int64, len(rows))
498+
for _, row := range rows {
499+
result[row.Date] = row.Total
500+
}
501+
return result, nil
502+
}
503+
403504
func GetUsersCount() int64 {
404505
count, err := query.User.Count()
405506
if err != nil {

internal/model/users.http.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,50 @@ type LoginRsp struct {
3232

3333
type UserListReq struct {
3434
PageReq
35-
Email *string `json:"email" form:"email" validate:"omitempty"` // 邮箱
36-
PhoneNumber *string `json:"phone_number" form:"phone_number" validate:"omitempty,max=50"` // 手机号
37-
Name *string `json:"name" form:"name" validate:"omitempty,max=50"` // 姓名
38-
Status *string `json:"status" form:"status" validate:"omitempty,oneof=N F"` // 用户状态 F-冻结 N-正常
39-
Organization *string `json:"organization" form:"organization" validate:"omitempty,max=200"` // 组织机构名称
35+
Email *string `json:"email" form:"email" validate:"omitempty"` // 邮箱
36+
PhoneNumber *string `json:"phone_number" form:"phone_number" validate:"omitempty,max=50"` // 手机号
37+
Name *string `json:"name" form:"name" validate:"omitempty,max=50"` // 姓名
38+
Status *string `json:"status" form:"status" validate:"omitempty,oneof=N F"` // 用户状态 F-冻结 N-正常
39+
Organization *string `json:"organization" form:"organization" validate:"omitempty,max=200"` // 组织机构名称
40+
ActivityScope *string `json:"activity_scope" form:"activity_scope" validate:"omitempty,oneof=today last_7_days last_30_days inactive_over_30_days"` // 租户活跃范围
4041
// 地址相关查询字段
4142
Country *string `json:"country" form:"country" validate:"omitempty,max=50"` // 国家
4243
Province *string `json:"province" form:"province" validate:"omitempty,max=50"` // 省份
4344
City *string `json:"city" form:"city" validate:"omitempty,max=50"` // 城市
4445
}
4546

47+
const (
48+
TenantActivityScopeToday = "today"
49+
TenantActivityScopeLast7Days = "last_7_days"
50+
TenantActivityScopeLast30Days = "last_30_days"
51+
TenantActivityScopeInactiveOver30Days = "inactive_over_30_days"
52+
)
53+
54+
type TenantStatisticsRes struct {
55+
Summary TenantStatisticsSummaryRes `json:"summary"`
56+
Revisit TenantRevisitStatisticsRes `json:"revisit"`
57+
Trend []TenantDailyGrowthRes `json:"trend"`
58+
}
59+
60+
type TenantStatisticsSummaryRes struct {
61+
Total int64 `json:"total"`
62+
ActiveToday int64 `json:"active_today"`
63+
ActiveLast7Days int64 `json:"active_last_7_days"`
64+
ActiveLast30Days int64 `json:"active_last_30_days"`
65+
InactiveOver30Days int64 `json:"inactive_over_30_days"`
66+
}
67+
68+
type TenantRevisitStatisticsRes struct {
69+
Revisited int64 `json:"revisited"`
70+
NotRevisited int64 `json:"not_revisited"`
71+
}
72+
73+
type TenantDailyGrowthRes struct {
74+
Date string `json:"date"`
75+
NewTotal int64 `json:"new_total"`
76+
CumulativeTotal int64 `json:"cumulative_total"`
77+
}
78+
4679
type UserSelectorReq struct {
4780
PageReq
4881
Name *string `json:"name" form:"name" validate:"omitempty,max=50"` // 用户名称(模糊匹配)

internal/service/sys_user.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,66 @@ func (*User) GetUserListByPage(userListReq *model.UserListReq, claims *utils.Use
428428
return userListRspMap, nil
429429
}
430430

431+
func (*User) GetTenantStatistics(ctx context.Context, claims *utils.UserClaims) (*model.TenantStatisticsRes, error) {
432+
if claims.Authority != dal.SYS_ADMIN {
433+
return nil, errcode.New(errcode.CodeNoPermission)
434+
}
435+
436+
now := time.Now()
437+
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
438+
tomorrowStart := todayStart.AddDate(0, 0, 1)
439+
last7Start := todayStart.AddDate(0, 0, -6)
440+
last30Start := todayStart.AddDate(0, 0, -29)
441+
442+
summary, revisit, err := dal.GetTenantStatisticsSummary(
443+
ctx,
444+
todayStart,
445+
tomorrowStart,
446+
last7Start,
447+
last30Start,
448+
)
449+
if err != nil {
450+
return nil, errcode.WithData(errcode.CodeDBError, map[string]interface{}{
451+
"operation": "query_tenant_statistics_summary",
452+
"error": err.Error(),
453+
})
454+
}
455+
456+
cumulativeTotal, err := dal.GetTenantCountBefore(ctx, last30Start)
457+
if err != nil {
458+
return nil, errcode.WithData(errcode.CodeDBError, map[string]interface{}{
459+
"operation": "query_tenant_statistics_baseline",
460+
"error": err.Error(),
461+
})
462+
}
463+
464+
dailyCounts, err := dal.GetTenantDailyNewCounts(ctx, last30Start, tomorrowStart)
465+
if err != nil {
466+
return nil, errcode.WithData(errcode.CodeDBError, map[string]interface{}{
467+
"operation": "query_tenant_daily_growth",
468+
"error": err.Error(),
469+
})
470+
}
471+
472+
trend := make([]model.TenantDailyGrowthRes, 0, 30)
473+
for day := last30Start; day.Before(tomorrowStart); day = day.AddDate(0, 0, 1) {
474+
date := day.Format("2006-01-02")
475+
newTotal := dailyCounts[date]
476+
cumulativeTotal += newTotal
477+
trend = append(trend, model.TenantDailyGrowthRes{
478+
Date: date,
479+
NewTotal: newTotal,
480+
CumulativeTotal: cumulativeTotal,
481+
})
482+
}
483+
484+
return &model.TenantStatisticsRes{
485+
Summary: summary,
486+
Revisit: revisit,
487+
Trend: trend,
488+
}, nil
489+
}
490+
431491
// @description 修改用户信息
432492
func (*User) UpdateUser(updateUserReq *model.UpdateUserReq, claims *utils.UserClaims) error {
433493
// 检查手机号是否重复

router/apps/sys_user.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func (*User) InitUser(Router *gin.RouterGroup) {
2020

2121
// 用户管理
2222
userapi.GET("", api.Controllers.UserApi.HandleUserListByPage)
23+
userapi.GET("tenant/statistics", api.Controllers.UserApi.HandleTenantStatistics)
2324
userapi.POST("", api.Controllers.UserApi.CreateUser)
2425
userapi.PUT("", api.Controllers.UserApi.UpdateUser)
2526
userapi.DELETE(":id", api.Controllers.UserApi.DeleteUser)

0 commit comments

Comments
 (0)