feat: 邀请关系列表增加邀请人累计统计
This commit is contained in:
@@ -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) {
|
func TestListAdminRebateLedgersFiltersPartialPhoneAndOrderNo(t *testing.T) {
|
||||||
db := setupTestDB(t)
|
db := setupTestDB(t)
|
||||||
repo := NewRepository(db)
|
repo := NewRepository(db)
|
||||||
|
|||||||
@@ -922,21 +922,69 @@ func (r *Repository) ListAdminInvitations(ctx context.Context, query AdminInvita
|
|||||||
return nil, 0, err
|
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))
|
list := make([]map[string]any, 0, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
|
stat := inviterStats[row.InviterID]
|
||||||
list = append(list, map[string]any{
|
list = append(list, map[string]any{
|
||||||
"id": row.ID,
|
"id": row.ID,
|
||||||
"inviter_id": row.InviterID,
|
"inviter_id": row.InviterID,
|
||||||
"inviter_phone": row.InviterPhone,
|
"inviter_phone": row.InviterPhone,
|
||||||
"inviter_nickname": row.InviterNickname,
|
"inviter_nickname": row.InviterNickname,
|
||||||
"invitee_id": row.InviteeID,
|
"inviter_total_invited": stat.TotalInvited,
|
||||||
"invitee_phone": row.InviteePhone,
|
"inviter_active_invited": stat.ActiveInvited,
|
||||||
"invitee_nickname": row.InviteeNickname,
|
"inviter_pending_invited": stat.PendingInvited,
|
||||||
"distribution_site_id": row.DistributionSiteID,
|
"inviter_invalid_invited": stat.InvalidInvited,
|
||||||
"invite_code": row.InviteCode,
|
"invitee_id": row.InviteeID,
|
||||||
"status": row.Status,
|
"invitee_phone": row.InviteePhone,
|
||||||
"verified_at": row.VerifiedAt,
|
"invitee_nickname": row.InviteeNickname,
|
||||||
"created_at": row.CreatedAt,
|
"distribution_site_id": row.DistributionSiteID,
|
||||||
|
"invite_code": row.InviteCode,
|
||||||
|
"status": row.Status,
|
||||||
|
"verified_at": row.VerifiedAt,
|
||||||
|
"created_at": row.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return list, total, nil
|
return list, total, nil
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ export interface AdminInvitationRow {
|
|||||||
inviter_id: number
|
inviter_id: number
|
||||||
inviter_phone: string
|
inviter_phone: string
|
||||||
inviter_nickname: string
|
inviter_nickname: string
|
||||||
|
/** 邀请人名下累计邀请人数(受当前站点范围限制) */
|
||||||
|
inviter_total_invited: number
|
||||||
|
/** 邀请人名下已实名生效人数 */
|
||||||
|
inviter_active_invited: number
|
||||||
|
/** 邀请人名下待实名人数 */
|
||||||
|
inviter_pending_invited: number
|
||||||
|
/** 邀请人名下无效邀请人数 */
|
||||||
|
inviter_invalid_invited: number
|
||||||
invitee_id: number
|
invitee_id: number
|
||||||
invitee_phone: string
|
invitee_phone: string
|
||||||
invitee_nickname: string
|
invitee_nickname: string
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ function cycleLabel(cycle: string) {
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>邀请返利与分成管理</h2>
|
<h2>邀请返利与分成管理</h2>
|
||||||
<p class="page-desc">
|
<p class="page-desc">
|
||||||
配置主站及各分销站点的返利阶梯比例(首月、次月、往后永久),监控全网邀请关系与返利流水。
|
配置主站及各分销站点的返利阶梯比例(首月、次月、往后永久),监控全网邀请关系与返利流水,快速查看每位邀请人的累计邀请、已实名与待实名人数。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -320,6 +320,9 @@ function cycleLabel(cycle: string) {
|
|||||||
<el-button type="primary" :icon="Search" @click="searchInvitations">查询</el-button>
|
<el-button type="primary" :icon="Search" @click="searchInvitations">查询</el-button>
|
||||||
<el-button :icon="Refresh" @click="searchInvitations">刷新</el-button>
|
<el-button :icon="Refresh" @click="searchInvitations">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="table-hint">
|
||||||
|
「邀请人累计」统计该邀请人名下全部邀请人数;分站后台仅统计本站范围。
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loadingInvitations"
|
v-loading="loadingInvitations"
|
||||||
@@ -334,6 +337,23 @@ function cycleLabel(cycle: string) {
|
|||||||
<code style="font-size: 12px; color: #64748b">{{ row.inviter_phone }}</code>
|
<code style="font-size: 12px; color: #64748b">{{ row.inviter_phone }}</code>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column label="被邀请人" min-width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div>{{ row.invitee_nickname || '-' }}</div>
|
<div>{{ row.invitee_nickname || '-' }}</div>
|
||||||
@@ -575,6 +595,27 @@ function cycleLabel(cycle: string) {
|
|||||||
flex-wrap: wrap;
|
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 {
|
.pagination-box {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user