feat(distribution): 分销站点增加用户数量统计与用户信息展示模块
- 数据模型与迁移: 新增 000082 迁移在 users 表追加 distribution_site_id 并建索引,用户首次在分销站点注册/短信登录时自动绑定站点归属 - 分销管理接口: 新增 GET /admin/distribution/users 接口,聚合本站注册用户与下单客户,返回完整11位手机号、累计消费金额、订单数及来源标签 - 统计与大盘扩展: GET /admin/distribution/sites/:id/analytics 补全今日注册、累计用户、成交客户指标,主站分销商列表追加用户体量展示 - 分销后台前端: 新增「客户与用户」页面 (Users.tsx) 支持一键复制手机号与订单穿透,大盘 (Dashboard.tsx) 增加用户规模核心卡片 - 自动化测试与门禁: 新增 TestListDistributionUsersAndAnalytics 单元测试,通过全量 Go 后端集成测试及前后端构建检查
This commit is contained in:
@@ -1518,3 +1518,125 @@ func TestBatchOperationsAndCachingOptimizations(t *testing.T) {
|
||||
t.Fatalf("paged keyword mismatch: total=%d, items=%+v", pagedResult.Total, pagedResult.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDistributionUsersAndAnalytics(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
site := model.DistributionSite{SiteCode: "user_test_site", SiteName: "用户测试站", Domain: "users.example.com", Status: "active"}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
otherSite := model.DistributionSite{SiteCode: "other_site", SiteName: "其他站", Domain: "other.example.com", Status: "active"}
|
||||
if err := db.Create(&otherSite).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
// 用户 1: 本站注册,未下单
|
||||
u1 := model.User{Phone: "13800000001", Nickname: "用户一", DistributionSiteID: site.ID, Status: "active", CreatedAt: now}
|
||||
// 用户 2: 本站注册,且在本站下过单
|
||||
u2 := model.User{Phone: "13800000002", Nickname: "用户二", DistributionSiteID: site.ID, Status: "active", CreatedAt: now}
|
||||
// 用户 3: 在主站注册 (DistributionSiteID=0),但在本站下过单
|
||||
u3 := model.User{Phone: "13800000003", Nickname: "用户三", DistributionSiteID: 0, Status: "active", CreatedAt: now}
|
||||
// 用户 4: 在其他站注册 (DistributionSiteID=otherSite.ID),未在本站下过单
|
||||
u4 := model.User{Phone: "13800000004", Nickname: "用户四", DistributionSiteID: otherSite.ID, Status: "active", CreatedAt: now}
|
||||
|
||||
if err := db.Create(&u1).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&u2).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&u3).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&u4).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 订单 1: 用户 2 在本站下单
|
||||
o1 := model.RentalOrder{
|
||||
OrderNo: "ORD-U2", ListingID: 101, AccountID: 201, OwnerID: 99, RenterID: u2.ID,
|
||||
DistributionSiteID: &site.ID, RentAmountCent: 5000, Status: "completed", CreatedAt: now,
|
||||
}
|
||||
// 订单 2: 用户 3 在本站下单
|
||||
o2 := model.RentalOrder{
|
||||
OrderNo: "ORD-U3", ListingID: 102, AccountID: 202, OwnerID: 99, RenterID: u3.ID,
|
||||
DistributionSiteID: &site.ID, RentAmountCent: 3000, Status: "completed", CreatedAt: now,
|
||||
}
|
||||
// 订单 3: 用户 4 在其他站下单
|
||||
o3 := model.RentalOrder{
|
||||
OrderNo: "ORD-U4", ListingID: 103, AccountID: 203, OwnerID: 99, RenterID: u4.ID,
|
||||
DistributionSiteID: &otherSite.ID, RentAmountCent: 8000, Status: "completed", CreatedAt: now,
|
||||
}
|
||||
if err := db.Create(&o1).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&o2).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&o3).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 1. 查询全部用户 (应该包含 u1, u2, u3;不包含 u4)
|
||||
resAll, err := repo.ListDistributionUsers(t.Context(), 0, site.ID, ListDistributionUsersQuery{Page: 1, PageSize: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListDistributionUsers all failed: %v", err)
|
||||
}
|
||||
if resAll.Total != 3 {
|
||||
t.Fatalf("expected 3 total users, got %d", resAll.Total)
|
||||
}
|
||||
if resAll.TotalUsers != 2 { // u1, u2 本站注册
|
||||
t.Fatalf("expected 2 total registered users, got %d", resAll.TotalUsers)
|
||||
}
|
||||
if resAll.OrderUsers != 2 { // u2, u3 本站下单
|
||||
t.Fatalf("expected 2 order users, got %d", resAll.OrderUsers)
|
||||
}
|
||||
|
||||
// 验证手机号完整输出
|
||||
for _, it := range resAll.Items {
|
||||
if len(it.Phone) != 11 {
|
||||
t.Fatalf("expected full 11-digit phone, got %s", it.Phone)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 筛选 source=register
|
||||
resReg, err := repo.ListDistributionUsers(t.Context(), 0, site.ID, ListDistributionUsersQuery{Source: "register", Page: 1, PageSize: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListDistributionUsers register failed: %v", err)
|
||||
}
|
||||
if resReg.Total != 2 {
|
||||
t.Fatalf("expected 2 register users, got %d", resReg.Total)
|
||||
}
|
||||
|
||||
// 3. 筛选 source=order
|
||||
resOrd, err := repo.ListDistributionUsers(t.Context(), 0, site.ID, ListDistributionUsersQuery{Source: "order", Page: 1, PageSize: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListDistributionUsers order failed: %v", err)
|
||||
}
|
||||
if resOrd.Total != 2 {
|
||||
t.Fatalf("expected 2 order users, got %d", resOrd.Total)
|
||||
}
|
||||
|
||||
// 4. 关键字搜索手机号
|
||||
resKw, err := repo.ListDistributionUsers(t.Context(), 0, site.ID, ListDistributionUsersQuery{Keyword: "13800000001", Page: 1, PageSize: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListDistributionUsers kw failed: %v", err)
|
||||
}
|
||||
if resKw.Total != 1 || resKw.Items[0].ID != u1.ID {
|
||||
t.Fatalf("keyword search failed: %+v", resKw)
|
||||
}
|
||||
|
||||
// 5. 验证大盘用户统计
|
||||
analytics, err := repo.GetDashboardAnalytics(t.Context(), 0, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDashboardAnalytics failed: %v", err)
|
||||
}
|
||||
if analytics.TodayUserCount != 2 || analytics.TotalUserCount != 2 || analytics.TotalOrderUserCount != 2 {
|
||||
t.Fatalf("analytics user count mismatch: today=%d, total=%d, order=%d",
|
||||
analytics.TodayUserCount, analytics.TotalUserCount, analytics.TotalOrderUserCount)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user