公告中心(前台公告列表/详情)此前完全没有站点概念,分站访客只能看到主站公告。 本次按「默认继承主站、站长可切仅本站」的口径支持分站自定义。 数据与口径: - 新增 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 与跨站防护、公告来源开关校验与非法值容错
177 lines
5.7 KiB
Go
177 lines
5.7 KiB
Go
package announcement
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"hfb_sys/backend/internal/database"
|
||
"hfb_sys/backend/internal/model"
|
||
)
|
||
|
||
// TestPublicScopeAndSiteIsolation 覆盖公告中心的分站可见范围:
|
||
// 主站只看主站、分站继承=主站+本站、分站仅本站=只有本站,详情也不能跨站读取。
|
||
func TestPublicScopeAndSiteIsolation(t *testing.T) {
|
||
db := database.NewTestDB()
|
||
repo := NewRepository(db)
|
||
ctx := context.Background()
|
||
|
||
mainAnnouncement := model.Announcement{Title: "主站公告", Content: "主站内容", Category: "notice", Status: "published"}
|
||
if err := db.Create(&mainAnnouncement).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
inheritSite := model.DistributionSite{
|
||
SiteCode: "ann_inherit", SiteName: "继承站", Domain: "ann-inherit.example.com",
|
||
Status: "active", AnnouncementMode: model.AnnouncementModeInherit,
|
||
}
|
||
ownOnlySite := model.DistributionSite{
|
||
SiteCode: "ann_own_only", SiteName: "独立站", Domain: "ann-own.example.com",
|
||
Status: "active", AnnouncementMode: model.AnnouncementModeOwnOnly,
|
||
}
|
||
if err := db.Create(&inheritSite).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := db.Create(&ownOnlySite).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
inheritOwn := model.Announcement{
|
||
DistributionSiteID: inheritSite.ID, Title: "继承站公告", Content: "本站内容",
|
||
Category: "notice", Status: "published",
|
||
}
|
||
ownOnlyOwn := model.Announcement{
|
||
DistributionSiteID: ownOnlySite.ID, Title: "独立站公告", Content: "本站内容",
|
||
Category: "notice", Status: "published",
|
||
}
|
||
if err := db.Create(&inheritOwn).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := db.Create(&ownOnlyOwn).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 主站:仅 [0]
|
||
mainScope, err := repo.ResolvePublicSiteScope(ctx, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(mainScope) != 1 || mainScope[0] != 0 {
|
||
t.Fatalf("main scope = %v, want [0]", mainScope)
|
||
}
|
||
mainList, err := repo.List(ctx, AnnouncementListQuery{AllowedSiteIDs: mainScope, Page: 1, PageSize: 20})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if mainList.Total != 1 {
|
||
t.Fatalf("主站应只看到 1 条主站公告, got %d", mainList.Total)
|
||
}
|
||
|
||
// 分站 inherit:[0, siteID] → 主站公告 + 本站公告
|
||
inheritScope, err := repo.ResolvePublicSiteScope(ctx, inheritSite.SiteCode)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(inheritScope) != 2 {
|
||
t.Fatalf("inherit scope = %v, want [0, siteID]", inheritScope)
|
||
}
|
||
inheritList, err := repo.List(ctx, AnnouncementListQuery{AllowedSiteIDs: inheritScope, Page: 1, PageSize: 20})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if inheritList.Total != 2 {
|
||
t.Fatalf("继承站应看到主站+本站共 2 条, got %d", inheritList.Total)
|
||
}
|
||
|
||
// 分站 own_only:只有本站公告(不含主站公告)
|
||
ownScope, err := repo.ResolvePublicSiteScope(ctx, ownOnlySite.SiteCode)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(ownScope) != 1 || ownScope[0] != ownOnlySite.ID {
|
||
t.Fatalf("own_only scope = %v, want [siteID]", ownScope)
|
||
}
|
||
ownList, err := repo.List(ctx, AnnouncementListQuery{AllowedSiteIDs: ownScope, Page: 1, PageSize: 20})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if ownList.Total != 1 {
|
||
t.Fatalf("仅本站站点应只看到本站 1 条, got %d", ownList.Total)
|
||
}
|
||
|
||
// 无效站点编码回落主站,避免把主站公告错误地当成某站的
|
||
fallbackScope, err := repo.ResolvePublicSiteScope(ctx, "not_exists_site")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(fallbackScope) != 1 || fallbackScope[0] != 0 {
|
||
t.Fatalf("未知站点应回落主站, got %v", fallbackScope)
|
||
}
|
||
|
||
// 详情跨站隔离:主站访客不能按 ID 读分站公告
|
||
if _, err := repo.GetByID(ctx, inheritOwn.ID, []uint64{0}); err == nil {
|
||
t.Fatalf("主站不应能读取分站公告详情")
|
||
}
|
||
if _, err := repo.GetByID(ctx, inheritOwn.ID, inheritScope); err != nil {
|
||
t.Fatalf("继承站应能读取本站公告详情: %v", err)
|
||
}
|
||
if _, err := repo.GetByID(ctx, ownOnlyOwn.ID, inheritScope); err == nil {
|
||
t.Fatalf("继承站不应能读取其它分站公告详情")
|
||
}
|
||
}
|
||
|
||
// TestAdminListFiltersBySite 管理端可按站点筛选,并带出站点名。
|
||
func TestAdminListFiltersBySite(t *testing.T) {
|
||
db := database.NewTestDB()
|
||
repo := NewRepository(db)
|
||
ctx := context.Background()
|
||
|
||
site := model.DistributionSite{
|
||
SiteCode: "ann_admin", SiteName: "筛选站", Domain: "ann-admin.example.com",
|
||
Status: "active", AnnouncementMode: model.AnnouncementModeInherit,
|
||
}
|
||
if err := db.Create(&site).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := db.Create(&model.Announcement{Title: "主站公告", Content: "x", Category: "notice", Status: "draft"}).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := db.Create(&model.Announcement{
|
||
DistributionSiteID: site.ID, Title: "分站公告", Content: "y", Category: "notice", Status: "draft",
|
||
}).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
all, err := repo.AdminList(ctx, AnnouncementListQuery{Page: 1, PageSize: 20})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if all.Total != 2 {
|
||
t.Fatalf("不传站点应返回全部 2 条, got %d", all.Total)
|
||
}
|
||
|
||
mainOnly := uint64(0)
|
||
mainList, err := repo.AdminList(ctx, AnnouncementListQuery{AdminSiteID: &mainOnly, Page: 1, PageSize: 20})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if mainList.Total != 1 {
|
||
t.Fatalf("site_id=0 应只返回主站公告, got %d", mainList.Total)
|
||
}
|
||
|
||
siteID := site.ID
|
||
siteList, err := repo.AdminList(ctx, AnnouncementListQuery{AdminSiteID: &siteID, Page: 1, PageSize: 20})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if siteList.Total != 1 {
|
||
t.Fatalf("按站点筛选应只返回该站公告, got %d", siteList.Total)
|
||
}
|
||
items, ok := siteList.Items.([]AnnouncementDTO)
|
||
if !ok || len(items) != 1 {
|
||
t.Fatalf("unexpected items: %#v", siteList.Items)
|
||
}
|
||
if items[0].DistributionSiteID != site.ID || items[0].DistributionSiteName != "筛选站" {
|
||
t.Fatalf("站点信息未透出: %+v", items[0])
|
||
}
|
||
}
|