公告中心(前台公告列表/详情)此前完全没有站点概念,分站访客只能看到主站公告。 本次按「默认继承主站、站长可切仅本站」的口径支持分站自定义。 数据与口径: - 新增 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 与跨站防护、公告来源开关校验与非法值容错
159 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|