Files
hfb_sys/backend/internal/modules/announcement/repository_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

177 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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])
}
}