Files
hfb_sys/backend/internal/modules/distribution/site_crud_test.go
T
yml2213 2a76d59a30 feat(distribution): 公告中心支持分站自定义
公告中心(前台公告列表/详情)此前完全没有站点概念,分站访客只能看到主站公告。
本次按「默认继承主站、站长可切仅本站」的口径支持分站自定义。

数据与口径:
- 新增 announcements.distribution_site_id(0=主站),前台按站点隔离
- 新增 distribution_sites.announcement_mode:inherit(默认,主站+本站)/ own_only(仅本站)

后端:
- 公告模块新增 ResolvePublicSiteScope:主站 [0]、inherit [0, siteID]、own_only [siteID],
  站点编码无效/已停用回落主站;列表与详情都按该范围过滤,详情不再允许跨站按 ID 读取
- 管理端公告列表支持按站点筛选(site_id:不传=全部,0=主站,>0=分站)并带出站点名
- 分销模块新增本站公告 CRUD(distribution/announcement.go):强制 distribution_site_id=本站,
  先过 ensureSiteAccess,跨站更新/删除/上下架一律 ErrAnnouncementNotFound
- UpdateSiteBranding 支持 announcement_mode 切换(非法值报错,读侧非法历史值回落 inherit)
- 新增权限 distribution:announcement_view|manage 并授予 super_admin/ops/finance,
  站长开通时继承 ops 角色即自动获得
- 路由:站长侧 /api/distribution/announcements*,平台侧 /api/admin/distribution/announcements*

前端:
- 分销后台新增「公告中心」页:来源开关(继承主站+本站 / 仅本站)、本站公告列表与
  新建/编辑/发布/下架/归档/恢复/删除,空态按来源模式给出引导
- 平台后台公告页新增「站点」列与筛选,新建公告可选择归属站点(主站或指定分站代发)
- 前台 PC/移动无需改动:请求已带 X-Distribution-Site-Code,后端按站点隔离,页面已有空态

测试:
- 公告模块:主站/继承/仅本站三种可见范围、跨站详情拦截、无效站点回落、管理端站点筛选
- 分销模块:本站公告 CRUD 与跨站防护、公告来源开关校验与非法值容错
2026-10-01 10:02:31 +08:00

159 lines
4.6 KiB
Go

package distribution
import (
"testing"
"golang.org/x/crypto/bcrypt"
"hfb_sys/backend/internal/model"
)
func TestSiteCRUD_UpdateSiteAndDomainUniqueness(t *testing.T) {
db := distributionTestDB(t)
repo := NewRepository(db)
ctx := t.Context()
_, err := repo.CreateSite(ctx, CreateSiteRequest{
SiteCode: "site_crud_a",
SiteName: "站点A",
Domain: "a.domain.test",
})
if err != nil {
t.Fatalf("CreateSite A failed: %v", err)
}
siteB, err := repo.CreateSite(ctx, CreateSiteRequest{
SiteCode: "site_crud_b",
SiteName: "站点B",
Domain: "b.domain.test",
})
if err != nil {
t.Fatalf("CreateSite B failed: %v", err)
}
// 1. 尝试将站点B的域名更新为已存在的站点A域名 -> 应该报错
dupDomain := "a.domain.test"
_, err = repo.UpdateSite(ctx, 0, siteB.ID, UpdateSiteRequest{
Domain: &dupDomain,
})
if err == nil {
t.Fatalf("expected error when updating to duplicate domain, got nil")
}
// 2. 正常更新站点B的名称、域名、状态为 disabled、展示主站账号为 false
newName := "站点B更新后"
newDomain := "b-new.domain.test"
newStatus := "disabled"
dispFalse := false
updated, err := repo.UpdateSite(ctx, 0, siteB.ID, UpdateSiteRequest{
SiteName: &newName,
Domain: &newDomain,
Status: &newStatus,
DisplayAccountEnabled: &dispFalse,
})
if err != nil {
t.Fatalf("UpdateSite failed: %v", err)
}
if updated.SiteName != newName || updated.Domain != newDomain || updated.Status != "disabled" || updated.DisplayAccountEnabled != false {
t.Fatalf("updated site values mismatch: %+v", updated)
}
}
func TestSiteCRUD_DeleteSiteProtectionAndCascade(t *testing.T) {
db := distributionTestDB(t)
repo := NewRepository(db)
ctx := t.Context()
// 1. 使用 OnboardSite 创建一个完整的测试分销站(包含成员与管理员)
onboardResp, err := repo.OnboardSite(ctx, OnboardSiteRequest{
SiteCode: "del_test_site",
SiteName: "待删除测试分销站",
Domain: "del.domain.test",
AdminUsername: "del_test_admin",
AdminPassword: "password123",
})
if err != nil {
t.Fatalf("OnboardSite failed: %v", err)
}
siteID := onboardResp.Site.ID
adminUserID := onboardResp.Member.AdminID
// 2. 测试密码重置
newPwd := "new_secret_pwd_666"
if err := repo.ResetSiteAdminPassword(ctx, 0, siteID, newPwd); err != nil {
t.Fatalf("ResetSiteAdminPassword failed: %v", err)
}
// 校验密码是否成功重置
var adminUser model.AdminUser
if err := db.First(&adminUser, adminUserID).Error; err != nil {
t.Fatalf("failed to find admin user: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(adminUser.PasswordHash), []byte(newPwd)); err != nil {
t.Fatalf("password hash does not match new password: %v", err)
}
// 3. 验证 ListSites 与 FindSite 能够正确携带管理员信息
sites, err := repo.ListSites(ctx)
if err != nil {
t.Fatalf("ListSites failed: %v", err)
}
var foundSite *SiteDTO
for i := range sites {
if sites[i].ID == siteID {
foundSite = &sites[i]
break
}
}
if foundSite == nil {
t.Fatalf("site not found in ListSites")
}
if foundSite.AdminUsername != "del_test_admin" {
t.Fatalf("expected admin_username del_test_admin, got %s", foundSite.AdminUsername)
}
// 4. 测试有订单时的删除保护
// 创建一笔模拟关联订单
dummyOrder := model.RentalOrder{
OrderNo: "ORD_DEL_TEST_001",
RenterID: 999,
AccountID: 888,
DistributionSiteID: &siteID,
}
if err := db.Create(&dummyOrder).Error; err != nil {
t.Fatalf("failed to create dummy order: %v", err)
}
err = repo.DeleteSite(ctx, 0, siteID)
if err == nil {
t.Fatalf("expected error when deleting site with orders, got nil")
}
// 删除此测试订单,继续测试级联删除
if err := db.Delete(&dummyOrder).Error; err != nil {
t.Fatalf("failed to remove dummy order: %v", err)
}
// 5. 测试无订单时的干净级联删除
if err := repo.DeleteSite(ctx, 0, siteID); err != nil {
t.Fatalf("DeleteSite failed: %v", err)
}
// 校验站点主体已删除
var checkSite model.DistributionSite
if err := db.First(&checkSite, siteID).Error; err == nil {
t.Fatalf("site should be deleted from db")
}
// 校验专属管理员用户已同步清理
var checkAdmin model.AdminUser
if err := db.First(&checkAdmin, adminUserID).Error; err == nil {
t.Fatalf("dedicated admin user should be deleted from db")
}
// 校验规则已清理
var ruleCount int64
db.Model(&model.DistributionRuleVersion{}).Where("site_id = ?", siteID).Count(&ruleCount)
if ruleCount > 0 {
t.Fatalf("expected 0 rules for deleted site, got %d", ruleCount)
}
}