feat(distribution): 独立分销后台账号体系、多租户隔离与主站后台分销管理GUI
- 独立账号鉴权:分销后台支持独立登录页面、Bearer+Cookie 双模鉴权与租户资料接口 - 多租户隔离:分销商成员强制锁定自身站点数据范围,防止横向越权;平台超管享有全局视界 - 一键开通分销商:平台端支持一键录入站点、管理员账号、分配角色并原子化初始化佣金规则 - 主后台分销分组:在主站后台增加「分销管理」导航分组,提供站点管理、规则配置、订单监控与结算管理 GUI - 门禁与联调:补充完整单测,全量代码门禁 check.sh 100% 通过,dev.sh 支持前后端与分销后台一键同启
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/auth"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestOnboardSiteAndLoginFlow(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
jwtMgr := auth.NewJWTManager("test_secret_32_bytes_long_keys_123")
|
||||
svc := NewService(repo, jwtMgr)
|
||||
ctx := context.Background()
|
||||
|
||||
// 1. Onboard new site
|
||||
req := OnboardSiteRequest{
|
||||
SiteCode: "gz_dist",
|
||||
SiteName: "广州分销站",
|
||||
Domain: "gz.distribution.com",
|
||||
AdminUsername: "gz_operator",
|
||||
AdminPassword: "Password123!",
|
||||
AdminNickname: "广州运营小李",
|
||||
MemberType: "operator",
|
||||
DistributorReceiveRatio: 3.8,
|
||||
PlatformSellRatio: 4.2,
|
||||
ServiceFeeValue: 2.0,
|
||||
}
|
||||
|
||||
resp, err := svc.OnboardSite(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to onboard site: %v", err)
|
||||
}
|
||||
|
||||
if resp.Site.SiteCode != "gz_dist" {
|
||||
t.Fatalf("expected SiteCode gz_dist, got %s", resp.Site.SiteCode)
|
||||
}
|
||||
if resp.Member.Username != "gz_operator" {
|
||||
t.Fatalf("expected Username gz_operator, got %s", resp.Member.Username)
|
||||
}
|
||||
if resp.Member.IsSuperadmin {
|
||||
t.Fatalf("expected IsSuperadmin=false for site member")
|
||||
}
|
||||
if resp.Member.SiteID == nil || *resp.Member.SiteID != resp.Site.ID {
|
||||
t.Fatalf("expected SiteID=%d, got %v", resp.Site.ID, resp.Member.SiteID)
|
||||
}
|
||||
if resp.Rule.DistributorReceiveRatio != 3.8 {
|
||||
t.Fatalf("expected rule ratio 3.8, got %f", resp.Rule.DistributorReceiveRatio)
|
||||
}
|
||||
|
||||
// 2. Duplicate site code or domain check
|
||||
dupReq := req
|
||||
dupReq.AdminUsername = "gz_operator_2"
|
||||
if _, err := svc.OnboardSite(ctx, dupReq); err != ErrDuplicateSiteCodeOrDomain {
|
||||
t.Fatalf("expected ErrDuplicateSiteCodeOrDomain, got %v", err)
|
||||
}
|
||||
|
||||
// Duplicate username check
|
||||
dupUserReq := req
|
||||
dupUserReq.SiteCode = "gz_dist_2"
|
||||
dupUserReq.Domain = "gz2.distribution.com"
|
||||
if _, err := svc.OnboardSite(ctx, dupUserReq); err != ErrDuplicateUsername {
|
||||
t.Fatalf("expected ErrDuplicateUsername, got %v", err)
|
||||
}
|
||||
|
||||
// 3. Login with onboarded credentials
|
||||
loginResp, err := svc.Login(ctx, DistributionLoginRequest{
|
||||
Username: "gz_operator",
|
||||
Password: "Password123!",
|
||||
}, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to login: %v", err)
|
||||
}
|
||||
|
||||
if loginResp.Token == "" {
|
||||
t.Fatalf("expected token, got empty")
|
||||
}
|
||||
if loginResp.Member.SiteCode != "gz_dist" {
|
||||
t.Fatalf("expected site code gz_dist, got %s", loginResp.Member.SiteCode)
|
||||
}
|
||||
|
||||
// 4. Login with wrong password
|
||||
_, err = svc.Login(ctx, DistributionLoginRequest{
|
||||
Username: "gz_operator",
|
||||
Password: "wrong_password",
|
||||
}, "127.0.0.1")
|
||||
if err != ErrInvalidCredential {
|
||||
t.Fatalf("expected ErrInvalidCredential, got %v", err)
|
||||
}
|
||||
|
||||
// 5. Login with non-existent user
|
||||
_, err = svc.Login(ctx, DistributionLoginRequest{
|
||||
Username: "non_existent",
|
||||
Password: "Password123!",
|
||||
}, "127.0.0.1")
|
||||
if err != ErrInvalidCredential {
|
||||
t.Fatalf("expected ErrInvalidCredential, got %v", err)
|
||||
}
|
||||
|
||||
// 6. Test disabled user
|
||||
if err := db.Model(&model.AdminUser{}).Where("username = ?", "gz_operator").Update("status", "disabled").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = svc.Login(ctx, DistributionLoginRequest{
|
||||
Username: "gz_operator",
|
||||
Password: "Password123!",
|
||||
}, "127.0.0.1")
|
||||
if err != ErrAdminDisabled {
|
||||
t.Fatalf("expected ErrAdminDisabled, got %v", err)
|
||||
}
|
||||
|
||||
// 7. Test superadmin login (user without site member binding)
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte("SuperAdmin123!"), bcrypt.DefaultCost)
|
||||
superAdmin := model.AdminUser{
|
||||
Username: "platform_admin",
|
||||
PasswordHash: string(hash),
|
||||
Nickname: "平台超级管理员",
|
||||
Status: "active",
|
||||
TokenVersion: 1,
|
||||
}
|
||||
if err := db.Create(&superAdmin).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
superLoginResp, err := svc.Login(ctx, DistributionLoginRequest{
|
||||
Username: "platform_admin",
|
||||
Password: "SuperAdmin123!",
|
||||
}, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("superadmin login failed: %v", err)
|
||||
}
|
||||
if !superLoginResp.Member.IsSuperadmin {
|
||||
t.Fatalf("expected IsSuperadmin=true for platform superadmin")
|
||||
}
|
||||
if superLoginResp.Member.SiteID != nil {
|
||||
t.Fatalf("expected nil SiteID for superadmin, got %v", superLoginResp.Member.SiteID)
|
||||
}
|
||||
|
||||
// 8. Test Me profile
|
||||
profile, err := svc.Me(ctx, superAdmin.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get profile: %v", err)
|
||||
}
|
||||
if profile.Username != "platform_admin" || !profile.IsSuperadmin {
|
||||
t.Fatalf("unexpected profile: %#v", profile)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user