- 数据库与后端:announcements 表增加 is_popup 字段及迁移脚本,提供前台弹窗接口与多公告优先级选取,补全选优单测 - 管理后台:总后台与分销后台公告编辑新增「弹窗公告」开关与列表标签展示 - 前台交互:前台 PC/移动端首页引入 AnnouncementPopup,支持同一用户同一天同一公告仅弹一次的关闭记录 - 视觉优化:重构弹窗结构,顶栏镶嵌品牌橙流光细条与浅暖橙通知徽标,中间内嵌浅灰舒适阅读卡片,底部搭配大锤橙实心主操作按钮 - 验证方式:后端 announcement 单测通过,前端 npm run build 编译无报错,浏览器实机截图验证
254 lines
8.3 KiB
Go
254 lines
8.3 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])
|
||
}
|
||
}
|
||
|
||
// TestPublicPopupSelection 覆盖弹窗公告的选取口径:
|
||
// 只有 published + is_popup 才参与,站点范围内按优先级取最高的一条,无候选时返回 nil。
|
||
func TestPublicPopupSelection(t *testing.T) {
|
||
db := database.NewTestDB()
|
||
repo := NewRepository(db)
|
||
ctx := context.Background()
|
||
|
||
// 主站:非弹窗公告(不应参与) + 两条弹窗公告(优先级不同)
|
||
plain := model.Announcement{
|
||
Title: "普通公告", Content: "内容", Category: "notice", Status: "published",
|
||
}
|
||
lowPriority := model.Announcement{
|
||
Title: "低优先级弹窗", Content: "内容", Category: "notice",
|
||
Status: "published", IsPopup: true, Priority: 1,
|
||
}
|
||
highPriority := model.Announcement{
|
||
Title: "高优先级弹窗", Content: "内容", Category: "notice",
|
||
Status: "published", IsPopup: true, Priority: 9,
|
||
}
|
||
// 弹窗但未发布:不应参与
|
||
draftPopup := model.Announcement{
|
||
Title: "草稿弹窗", Content: "内容", Category: "notice",
|
||
Status: "draft", IsPopup: true, Priority: 99,
|
||
}
|
||
for _, row := range []*model.Announcement{&plain, &lowPriority, &highPriority, &draftPopup} {
|
||
if err := db.Create(row).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
popup, err := repo.PublicPopup(ctx, []uint64{0})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if popup == nil {
|
||
t.Fatal("主站应弹出一条弹窗公告, got nil")
|
||
}
|
||
if popup.ID != highPriority.ID || popup.Title != "高优先级弹窗" {
|
||
t.Fatalf("应取优先级最高的弹窗公告, got %+v", popup)
|
||
}
|
||
if !popup.IsPopup {
|
||
t.Fatalf("返回的公告应带 is_popup 标记: %+v", popup)
|
||
}
|
||
|
||
// 站点隔离:独立站只看到本站公告,主站弹窗不可见
|
||
site := model.DistributionSite{
|
||
SiteCode: "ann_popup_site", SiteName: "弹窗站", Domain: "ann-popup.example.com",
|
||
Status: "active", AnnouncementMode: model.AnnouncementModeOwnOnly,
|
||
}
|
||
if err := db.Create(&site).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
none, err := repo.PublicPopup(ctx, []uint64{site.ID})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if none != nil {
|
||
t.Fatalf("本站没有弹窗公告时应返回 nil, got %+v", none)
|
||
}
|
||
|
||
// 本站有自己的弹窗公告时应生效
|
||
sitePopup := model.Announcement{
|
||
DistributionSiteID: site.ID, Title: "本站弹窗", Content: "内容",
|
||
Category: "notice", Status: "published", IsPopup: true, Priority: 5,
|
||
}
|
||
if err := db.Create(&sitePopup).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := repo.PublicPopup(ctx, []uint64{site.ID})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got == nil || got.ID != sitePopup.ID {
|
||
t.Fatalf("应取本站弹窗公告, got %+v", got)
|
||
}
|
||
}
|