Files
hfb_sys/backend/internal/modules/announcement/repository_test.go
T
yml2213 4a9e5f9d66 feat(announcement): 新增首页弹窗公告功能并完成弹窗UI视觉重构
- 数据库与后端:announcements 表增加 is_popup 字段及迁移脚本,提供前台弹窗接口与多公告优先级选取,补全选优单测
- 管理后台:总后台与分销后台公告编辑新增「弹窗公告」开关与列表标签展示
- 前台交互:前台 PC/移动端首页引入 AnnouncementPopup,支持同一用户同一天同一公告仅弹一次的关闭记录
- 视觉优化:重构弹窗结构,顶栏镶嵌品牌橙流光细条与浅暖橙通知徽标,中间内嵌浅灰舒适阅读卡片,底部搭配大锤橙实心主操作按钮
- 验证方式:后端 announcement 单测通过,前端 npm run build 编译无报错,浏览器实机截图验证
2026-10-12 18:33:44 +08:00

254 lines
8.3 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])
}
}
// 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)
}
}