feat(distribution): 站内信与通知体系多租户独立分支隔离

- 新增 000071 数据库迁移:notifications 表支持 distribution_site_id 并自动回填历史分销订单通知
- 通知自动归属:在事务内根据订单或申诉自动关联当前分销站点 ID
- 隔离查询与已读控制:列表、未读统计与批量已读均严格按站点隔离,防止主站与分站交叉泄漏
- 前台界面联动:消息中心通知 Tab 及移动端/PC端站内信页面展示分站专属标识与精准过滤
This commit is contained in:
yml2213
2026-09-07 19:10:02 +08:00
parent e024319860
commit 4461e7929e
11 changed files with 472 additions and 86 deletions
@@ -63,3 +63,131 @@ func TestRepositoryMarkReadNotFound(t *testing.T) {
t.Fatalf("MarkRead() error = %v, want ErrNotificationNotFound", err)
}
}
func TestNotificationTenantIsolation(t *testing.T) {
db := database.NewTestDB()
if err := db.AutoMigrate(&model.Notification{}, &model.DistributionSite{}, &model.RentalOrder{}); err != nil {
t.Fatalf("AutoMigrate() error = %v", err)
}
repo := NewRepository(db)
ctx := context.Background()
site := model.DistributionSite{
ID: 101,
SiteCode: "branch_a",
SiteName: "分站A",
Status: "active",
}
if err := db.Create(&site).Error; err != nil {
t.Fatalf("create distribution site error: %v", err)
}
siteID := site.ID
// 1. 创建主站通知
mainNotif := model.Notification{
UserID: 20,
Type: "order",
Title: "主站订单通知",
Content: "这是主站的订单通知",
BizType: "order",
DistributionSiteID: nil,
}
if err := db.Create(&mainNotif).Error; err != nil {
t.Fatalf("create main notif error: %v", err)
}
// 2. 创建分站通知
branchNotif := model.Notification{
UserID: 20,
Type: "order",
Title: "分站A订单通知",
Content: "这是分站A的订单通知",
BizType: "order",
DistributionSiteID: &siteID,
}
if err := db.Create(&branchNotif).Error; err != nil {
t.Fatalf("create branch notif error: %v", err)
}
// 3. 验证未读数隔离
mainUnread, err := repo.UnreadCount(ctx, 20)
if err != nil || mainUnread != 1 {
t.Fatalf("mainUnread = %d, err = %v, want 1", mainUnread, err)
}
branchUnread, err := repo.UnreadCount(ctx, 20, "branch_a")
if err != nil || branchUnread != 1 {
t.Fatalf("branchUnread = %d, err = %v, want 1", branchUnread, err)
}
// 4. 验证列表查询隔离
mainList, err := repo.List(ctx, 20, 1, 10)
if err != nil || mainList.Total != 1 {
t.Fatalf("mainList total = %d, err = %v, want 1", mainList.Total, err)
}
items := mainList.Items.([]NotificationDTO)
if items[0].Title != "主站订单通知" {
t.Fatalf("mainList item title = %s, want 主站订单通知", items[0].Title)
}
branchList, err := repo.List(ctx, 20, 1, 10, "branch_a")
if err != nil || branchList.Total != 1 {
t.Fatalf("branchList total = %d, err = %v, want 1", branchList.Total, err)
}
branchItems := branchList.Items.([]NotificationDTO)
if branchItems[0].Title != "分站A订单通知" {
t.Fatalf("branchList item title = %s, want 分站A订单通知", branchItems[0].Title)
}
// 5. 跨站点标记单条已读阻断
err = repo.MarkRead(ctx, 20, mainNotif.ID, "branch_a")
if !errors.Is(err, ErrNotificationNotFound) {
t.Fatalf("cross-site MarkRead error = %v, want ErrNotificationNotFound", err)
}
// 6. 分站全部已读不影响主站
affected, err := repo.MarkAllRead(ctx, 20, "branch_a")
if err != nil || affected != 1 {
t.Fatalf("MarkAllRead affected = %d, err = %v, want 1", affected, err)
}
branchUnreadAfter, err := repo.UnreadCount(ctx, 20, "branch_a")
if err != nil || branchUnreadAfter != 0 {
t.Fatalf("branchUnreadAfter = %d, want 0", branchUnreadAfter)
}
mainUnreadAfter, err := repo.UnreadCount(ctx, 20)
if err != nil || mainUnreadAfter != 1 {
t.Fatalf("mainUnreadAfter = %d, want 1 (should remain unread)", mainUnreadAfter)
}
// 7. 测试 Append 自动关联订单 distribution_site_id
order := model.RentalOrder{
ID: 3001,
DistributionSiteID: &siteID,
OrderNo: "ORD3001",
}
if err := db.Create(&order).Error; err != nil {
t.Fatalf("create order error: %v", err)
}
orderID := order.ID
err = Append(db, Entry{
UserID: 20,
Type: "order",
Title: "订单自动关联分站",
Content: "自动读取订单 site ID",
BizType: "order",
BizID: &orderID,
})
if err != nil {
t.Fatalf("Append with auto lookup error: %v", err)
}
var autoNotif model.Notification
if err := db.Where("biz_id = ? AND biz_type = 'order'", orderID).First(&autoNotif).Error; err != nil {
t.Fatalf("find autoNotif error: %v", err)
}
if autoNotif.DistributionSiteID == nil || *autoNotif.DistributionSiteID != siteID {
t.Fatalf("autoNotif.DistributionSiteID = %v, want %d", autoNotif.DistributionSiteID, siteID)
}
}