feat: 邀请关系列表增加邀请人累计统计

This commit is contained in:
yml2213
2026-10-19 17:52:54 +08:00
parent 27a6442c18
commit 51855a0e17
4 changed files with 214 additions and 13 deletions
@@ -74,6 +74,110 @@ func TestListAdminInvitationsFiltersPhoneAndInviteCode(t *testing.T) {
}
}
func TestListAdminInvitationsIncludesInviterAggregateStats(t *testing.T) {
db := setupTestDB(t)
repo := NewRepository(db)
ctx := context.Background()
now := time.Now()
inviter := model.User{
Phone: "13800000001",
Nickname: "邀请人A",
Status: "active",
RealnameStatus: "verified",
}
if err := db.Create(&inviter).Error; err != nil {
t.Fatalf("创建邀请人失败: %v", err)
}
specs := []struct {
phone string
status string
siteID uint64
verified bool
}{
{phone: "13900000001", status: StatusActive, siteID: 1, verified: true},
{phone: "13900000002", status: StatusActive, siteID: 1, verified: true},
{phone: "13900000003", status: StatusPending, siteID: 2, verified: false},
{phone: "13900000004", status: StatusPending, siteID: 0, verified: false},
{phone: "13900000005", status: StatusInvalid, siteID: 1, verified: false},
}
for _, spec := range specs {
invitee := model.User{
Phone: spec.phone,
Nickname: "被邀请人",
Status: "active",
RealnameStatus: "verified",
}
if err := db.Create(&invitee).Error; err != nil {
t.Fatalf("创建被邀请人失败: %v", err)
}
record := model.UserInvitation{
InviterID: inviter.ID,
InviteeID: invitee.ID,
DistributionSiteID: spec.siteID,
InviteCode: "aggcode",
Status: spec.status,
CreatedAt: now,
}
if spec.verified {
record.VerifiedAt = &now
}
if err := db.Create(&record).Error; err != nil {
t.Fatalf("创建邀请关系失败: %v", err)
}
}
assertStats := func(t *testing.T, items []map[string]any, wantTotal, wantActive, wantPending, wantInvalid int64) {
t.Helper()
if len(items) == 0 {
t.Fatal("查询结果为空")
}
for _, item := range items {
if got := item["inviter_total_invited"]; got != wantTotal {
t.Fatalf("累计邀请人数 = %v, want %d", got, wantTotal)
}
if got := item["inviter_active_invited"]; got != wantActive {
t.Fatalf("已实名人数 = %v, want %d", got, wantActive)
}
if got := item["inviter_pending_invited"]; got != wantPending {
t.Fatalf("待实名人数 = %v, want %d", got, wantPending)
}
if got := item["inviter_invalid_invited"]; got != wantInvalid {
t.Fatalf("无效邀请人数 = %v, want %d", got, wantInvalid)
}
}
}
items, total, err := repo.ListAdminInvitations(ctx, AdminInvitationQuery{
Page: 1,
PageSize: 20,
InviterID: inviter.ID,
})
if err != nil {
t.Fatalf("查询邀请关系失败: %v", err)
}
if total != 5 {
t.Fatalf("查询结果数量 = %d, want 5", total)
}
assertStats(t, items, 5, 2, 2, 1)
siteID := uint64(1)
items, total, err = repo.ListAdminInvitations(ctx, AdminInvitationQuery{
Page: 1,
PageSize: 20,
InviterID: inviter.ID,
DistributionSiteID: &siteID,
})
if err != nil {
t.Fatalf("按站点查询邀请关系失败: %v", err)
}
if total != 3 {
t.Fatalf("站点查询结果数量 = %d, want 3", total)
}
assertStats(t, items, 3, 2, 0, 1)
}
func TestListAdminRebateLedgersFiltersPartialPhoneAndOrderNo(t *testing.T) {
db := setupTestDB(t)
repo := NewRepository(db)
@@ -922,21 +922,69 @@ func (r *Repository) ListAdminInvitations(ctx context.Context, query AdminInvita
return nil, 0, err
}
// 汇总本页邀请人名下的累计邀请人数(一次 IN 查询,避免逐行统计)。
inviterIDs := make([]uint64, 0, len(rows))
seenInviter := make(map[uint64]struct{}, len(rows))
for _, row := range rows {
if row.InviterID == 0 {
continue
}
if _, ok := seenInviter[row.InviterID]; ok {
continue
}
seenInviter[row.InviterID] = struct{}{}
inviterIDs = append(inviterIDs, row.InviterID)
}
type inviterStatRow struct {
InviterID uint64 `gorm:"column:inviter_id"`
TotalInvited int64 `gorm:"column:total_invited"`
ActiveInvited int64 `gorm:"column:active_invited"`
PendingInvited int64 `gorm:"column:pending_invited"`
InvalidInvited int64 `gorm:"column:invalid_invited"`
}
inviterStats := make(map[uint64]inviterStatRow, len(inviterIDs))
if len(inviterIDs) > 0 {
var statRows []inviterStatRow
statDB := r.db.WithContext(ctx).Model(&model.UserInvitation{}).
Select("inviter_id, COUNT(*) AS total_invited, "+
"SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_invited, "+
"SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_invited, "+
"SUM(CASE WHEN status = 'invalid' THEN 1 ELSE 0 END) AS invalid_invited").
Where("inviter_id IN ?", inviterIDs).
Group("inviter_id")
// 分站后台只能看到本站范围内的累计数据,避免跨站泄露。
if query.DistributionSiteID != nil {
statDB = statDB.Where("distribution_site_id = ?", *query.DistributionSiteID)
}
if err := statDB.Scan(&statRows).Error; err != nil {
return nil, 0, err
}
for _, stat := range statRows {
inviterStats[stat.InviterID] = stat
}
}
list := make([]map[string]any, 0, len(rows))
for _, row := range rows {
stat := inviterStats[row.InviterID]
list = append(list, map[string]any{
"id": row.ID,
"inviter_id": row.InviterID,
"inviter_phone": row.InviterPhone,
"inviter_nickname": row.InviterNickname,
"invitee_id": row.InviteeID,
"invitee_phone": row.InviteePhone,
"invitee_nickname": row.InviteeNickname,
"distribution_site_id": row.DistributionSiteID,
"invite_code": row.InviteCode,
"status": row.Status,
"verified_at": row.VerifiedAt,
"created_at": row.CreatedAt,
"id": row.ID,
"inviter_id": row.InviterID,
"inviter_phone": row.InviterPhone,
"inviter_nickname": row.InviterNickname,
"inviter_total_invited": stat.TotalInvited,
"inviter_active_invited": stat.ActiveInvited,
"inviter_pending_invited": stat.PendingInvited,
"inviter_invalid_invited": stat.InvalidInvited,
"invitee_id": row.InviteeID,
"invitee_phone": row.InviteePhone,
"invitee_nickname": row.InviteeNickname,
"distribution_site_id": row.DistributionSiteID,
"invite_code": row.InviteCode,
"status": row.Status,
"verified_at": row.VerifiedAt,
"created_at": row.CreatedAt,
})
}
return list, total, nil
@@ -8,6 +8,14 @@ export interface AdminInvitationRow {
inviter_id: number
inviter_phone: string
inviter_nickname: string
/** 邀请人名下累计邀请人数(受当前站点范围限制) */
inviter_total_invited: number
/** 邀请人名下已实名生效人数 */
inviter_active_invited: number
/** 邀请人名下待实名人数 */
inviter_pending_invited: number
/** 邀请人名下无效邀请人数 */
inviter_invalid_invited: number
invitee_id: number
invitee_phone: string
invitee_nickname: string
@@ -196,7 +196,7 @@ function cycleLabel(cycle: string) {
<div class="page-header">
<h2>邀请返利与分成管理</h2>
<p class="page-desc">
配置主站及各分销站点的返利阶梯比例(首月、次月、往后永久),监控全网邀请关系与返利流水。
配置主站及各分销站点的返利阶梯比例(首月、次月、往后永久),监控全网邀请关系与返利流水,快速查看每位邀请人的累计邀请、已实名与待实名人数。
</p>
</div>
@@ -320,6 +320,9 @@ function cycleLabel(cycle: string) {
<el-button type="primary" :icon="Search" @click="searchInvitations">查询</el-button>
<el-button :icon="Refresh" @click="searchInvitations">刷新</el-button>
</div>
<div class="table-hint">
「邀请人累计」统计该邀请人名下全部邀请人数;分站后台仅统计本站范围。
</div>
<el-table
v-loading="loadingInvitations"
@@ -334,6 +337,23 @@ function cycleLabel(cycle: string) {
<code style="font-size: 12px; color: #64748b">{{ row.inviter_phone }}</code>
</template>
</el-table-column>
<el-table-column label="邀请人累计" align="center">
<el-table-column label="总邀请" width="86" align="center">
<template #default="{ row }">
<span class="invite-stat-total">{{ row.inviter_total_invited ?? 0 }}</span>
</template>
</el-table-column>
<el-table-column label="已实名" width="86" align="center">
<template #default="{ row }">
<span class="invite-stat-active">{{ row.inviter_active_invited ?? 0 }}</span>
</template>
</el-table-column>
<el-table-column label="待实名" width="86" align="center">
<template #default="{ row }">
<span class="invite-stat-pending">{{ row.inviter_pending_invited ?? 0 }}</span>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="被邀请人" min-width="150">
<template #default="{ row }">
<div>{{ row.invitee_nickname || '-' }}</div>
@@ -575,6 +595,27 @@ function cycleLabel(cycle: string) {
flex-wrap: wrap;
}
.table-hint {
margin-top: 10px;
font-size: 12px;
color: #94a3b8;
}
.invite-stat-total {
font-weight: 700;
color: #1e293b;
}
.invite-stat-active {
font-weight: 700;
color: #16a34a;
}
.invite-stat-pending {
font-weight: 700;
color: #d97706;
}
.pagination-box {
margin-top: 16px;
display: flex;