feat(notification): M6 示例模块 + 单元测试
Complete 4-layer notification module as reference template: domain → infrastructure → application → interfaces. 10 unit tests covering all service methods with mock repository.
This commit is contained in:
parent
1f4e2ca0d0
commit
cb7ad4d35d
@ -17,6 +17,7 @@ import (
|
|||||||
orginterfaces "mengstack/internal/modules/org/interfaces"
|
orginterfaces "mengstack/internal/modules/org/interfaces"
|
||||||
auditinterfaces "mengstack/internal/modules/audit/interfaces"
|
auditinterfaces "mengstack/internal/modules/audit/interfaces"
|
||||||
settingsinterfaces "mengstack/internal/modules/settings/interfaces"
|
settingsinterfaces "mengstack/internal/modules/settings/interfaces"
|
||||||
|
notificationinterfaces "mengstack/internal/modules/notification/interfaces"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@ -45,6 +46,7 @@ func newEngine(
|
|||||||
orgHandler *orginterfaces.Handler,
|
orgHandler *orginterfaces.Handler,
|
||||||
auditHandler *auditinterfaces.Handler,
|
auditHandler *auditinterfaces.Handler,
|
||||||
settingsHandler *settingsinterfaces.Handler,
|
settingsHandler *settingsinterfaces.Handler,
|
||||||
|
notificationHandler *notificationinterfaces.Handler,
|
||||||
) *gin.Engine {
|
) *gin.Engine {
|
||||||
ginMode := "release"
|
ginMode := "release"
|
||||||
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
||||||
@ -68,6 +70,7 @@ func newEngine(
|
|||||||
orginterfaces.SetupRoutes(r, orgHandler, authMW)
|
orginterfaces.SetupRoutes(r, orgHandler, authMW)
|
||||||
auditinterfaces.SetupRoutes(r, auditHandler, authMW)
|
auditinterfaces.SetupRoutes(r, auditHandler, authMW)
|
||||||
settingsinterfaces.SetupRoutes(r, settingsHandler, authMW)
|
settingsinterfaces.SetupRoutes(r, settingsHandler, authMW)
|
||||||
|
notificationinterfaces.SetupRoutes(r, notificationHandler, authMW)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@ -113,6 +116,7 @@ func NewApp() *fx.App {
|
|||||||
orginterfaces.Module,
|
orginterfaces.Module,
|
||||||
auditinterfaces.Module,
|
auditinterfaces.Module,
|
||||||
settingsinterfaces.Module,
|
settingsinterfaces.Module,
|
||||||
|
notificationinterfaces.Module,
|
||||||
Module,
|
Module,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
82
internal/modules/notification/application/service.go
Normal file
82
internal/modules/notification/application/service.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/notification/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo domain.NotificationRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.NotificationRepository) *Service {
|
||||||
|
return &Service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, tenantID string, req *domain.CreateNotificationRequest) (*domain.NotificationDTO, error) {
|
||||||
|
n := &domain.Notification{
|
||||||
|
TenantID: tenantID,
|
||||||
|
UserID: req.UserID,
|
||||||
|
Title: req.Title,
|
||||||
|
Content: req.Content,
|
||||||
|
Type: req.Type,
|
||||||
|
}
|
||||||
|
if n.Type == "" {
|
||||||
|
n.Type = "info"
|
||||||
|
}
|
||||||
|
if err := s.repo.Create(ctx, n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto := domain.ToNotificationDTO(n)
|
||||||
|
return &dto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(ctx context.Context, tenantID string, id uint) (*domain.NotificationDTO, error) {
|
||||||
|
n, err := s.repo.FindByID(ctx, tenantID, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto := domain.ToNotificationDTO(n)
|
||||||
|
return &dto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListByUser(ctx context.Context, tenantID string, userID uint, page, pageSize int) ([]domain.NotificationDTO, int64, error) {
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
notifications, total, err := s.repo.FindByUser(ctx, tenantID, userID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
result := make([]domain.NotificationDTO, len(notifications))
|
||||||
|
for i, n := range notifications {
|
||||||
|
result[i] = domain.ToNotificationDTO(&n)
|
||||||
|
}
|
||||||
|
return result, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) MarkAsRead(ctx context.Context, tenantID string, id uint) error {
|
||||||
|
return s.repo.MarkAsRead(ctx, tenantID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) MarkAllAsRead(ctx context.Context, tenantID string, userID uint) error {
|
||||||
|
return s.repo.MarkAllAsRead(ctx, tenantID, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CountUnread(ctx context.Context, tenantID string, userID uint) (int64, error) {
|
||||||
|
return s.repo.CountUnread(ctx, tenantID, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, tenantID string, id uint) error {
|
||||||
|
return s.repo.Delete(ctx, tenantID, id)
|
||||||
|
}
|
||||||
279
internal/modules/notification/application/service_test.go
Normal file
279
internal/modules/notification/application/service_test.go
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/notification/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockRepo struct {
|
||||||
|
notifications []domain.Notification
|
||||||
|
nextID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockRepo() *mockRepo {
|
||||||
|
return &mockRepo{nextID: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) Create(_ context.Context, n *domain.Notification) error {
|
||||||
|
n.ID = m.nextID
|
||||||
|
m.nextID++
|
||||||
|
m.notifications = append(m.notifications, *n)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) FindByID(_ context.Context, tenantID string, id uint) (*domain.Notification, error) {
|
||||||
|
for _, n := range m.notifications {
|
||||||
|
if n.ID == id && n.TenantID == tenantID {
|
||||||
|
return &n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) FindByUser(_ context.Context, tenantID string, userID uint, page, pageSize int) ([]domain.Notification, int64, error) {
|
||||||
|
var result []domain.Notification
|
||||||
|
for _, n := range m.notifications {
|
||||||
|
if n.TenantID == tenantID && n.UserID == userID {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total := int64(len(result))
|
||||||
|
start := (page - 1) * pageSize
|
||||||
|
if start >= len(result) {
|
||||||
|
return nil, total, nil
|
||||||
|
}
|
||||||
|
end := start + pageSize
|
||||||
|
if end > len(result) {
|
||||||
|
end = len(result)
|
||||||
|
}
|
||||||
|
return result[start:end], total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) MarkAsRead(_ context.Context, tenantID string, id uint) error {
|
||||||
|
for i, n := range m.notifications {
|
||||||
|
if n.ID == id && n.TenantID == tenantID {
|
||||||
|
m.notifications[i].IsRead = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) MarkAllAsRead(_ context.Context, tenantID string, userID uint) error {
|
||||||
|
for i, n := range m.notifications {
|
||||||
|
if n.TenantID == tenantID && n.UserID == userID {
|
||||||
|
m.notifications[i].IsRead = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) CountUnread(_ context.Context, tenantID string, userID uint) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
for _, n := range m.notifications {
|
||||||
|
if n.TenantID == tenantID && n.UserID == userID && !n.IsRead {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) Delete(_ context.Context, tenantID string, id uint) error {
|
||||||
|
for i, n := range m.notifications {
|
||||||
|
if n.ID == id && n.TenantID == tenantID {
|
||||||
|
m.notifications = append(m.notifications[:i], m.notifications[i+1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
req := &domain.CreateNotificationRequest{
|
||||||
|
UserID: 1,
|
||||||
|
Title: "Test Notification",
|
||||||
|
Content: "Hello",
|
||||||
|
Type: "warning",
|
||||||
|
}
|
||||||
|
dto, err := svc.Create(context.Background(), "tenant-1", req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if dto.ID != 1 {
|
||||||
|
t.Errorf("expected ID 1, got %d", dto.ID)
|
||||||
|
}
|
||||||
|
if dto.Title != "Test Notification" {
|
||||||
|
t.Errorf("expected title 'Test Notification', got %q", dto.Title)
|
||||||
|
}
|
||||||
|
if dto.Type != "warning" {
|
||||||
|
t.Errorf("expected type 'warning', got %q", dto.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateDefaultType(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
req := &domain.CreateNotificationRequest{
|
||||||
|
UserID: 1,
|
||||||
|
Title: "Test",
|
||||||
|
}
|
||||||
|
dto, err := svc.Create(context.Background(), "tenant-1", req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if dto.Type != "info" {
|
||||||
|
t.Errorf("expected default type 'info', got %q", dto.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
req := &domain.CreateNotificationRequest{UserID: 1, Title: "Test"}
|
||||||
|
svc.Create(context.Background(), "tenant-1", req)
|
||||||
|
|
||||||
|
dto, err := svc.Get(context.Background(), "tenant-1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if dto == nil {
|
||||||
|
t.Fatal("expected notification, got nil")
|
||||||
|
}
|
||||||
|
if dto.Title != "Test" {
|
||||||
|
t.Errorf("expected title 'Test', got %q", dto.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNotFound(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
dto, err := svc.Get(context.Background(), "tenant-1", 999)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if dto != nil {
|
||||||
|
t.Error("expected nil for non-existent notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListByUser(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
svc.Create(context.Background(), "tenant-1", &domain.CreateNotificationRequest{UserID: 1, Title: "Test"})
|
||||||
|
}
|
||||||
|
svc.Create(context.Background(), "tenant-1", &domain.CreateNotificationRequest{UserID: 2, Title: "Other user"})
|
||||||
|
|
||||||
|
items, total, err := svc.ListByUser(context.Background(), "tenant-1", 1, 1, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if total != 5 {
|
||||||
|
t.Errorf("expected total 5, got %d", total)
|
||||||
|
}
|
||||||
|
if len(items) != 5 {
|
||||||
|
t.Errorf("expected 5 items, got %d", len(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListByUserPagination(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
svc.Create(context.Background(), "tenant-1", &domain.CreateNotificationRequest{UserID: 1, Title: "Test"})
|
||||||
|
}
|
||||||
|
|
||||||
|
items, total, err := svc.ListByUser(context.Background(), "tenant-1", 1, 1, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if total != 5 {
|
||||||
|
t.Errorf("expected total 5, got %d", total)
|
||||||
|
}
|
||||||
|
if len(items) != 2 {
|
||||||
|
t.Errorf("expected 2 items on page 1, got %d", len(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkAsRead(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
svc.Create(context.Background(), "tenant-1", &domain.CreateNotificationRequest{UserID: 1, Title: "Test"})
|
||||||
|
|
||||||
|
err := svc.MarkAsRead(context.Background(), "tenant-1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, _ := svc.Get(context.Background(), "tenant-1", 1)
|
||||||
|
if !dto.IsRead {
|
||||||
|
t.Error("expected notification to be marked as read")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkAllAsRead(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
svc.Create(context.Background(), "tenant-1", &domain.CreateNotificationRequest{UserID: 1, Title: "Test"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := svc.MarkAllAsRead(context.Background(), "tenant-1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, _ := svc.CountUnread(context.Background(), "tenant-1", 1)
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("expected 0 unread, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountUnread(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
svc.Create(context.Background(), "tenant-1", &domain.CreateNotificationRequest{UserID: 1, Title: "Test"})
|
||||||
|
}
|
||||||
|
svc.MarkAsRead(context.Background(), "tenant-1", 1)
|
||||||
|
|
||||||
|
count, err := svc.CountUnread(context.Background(), "tenant-1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if count != 2 {
|
||||||
|
t.Errorf("expected 2 unread, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
repo := newMockRepo()
|
||||||
|
svc := NewService(repo)
|
||||||
|
|
||||||
|
svc.Create(context.Background(), "tenant-1", &domain.CreateNotificationRequest{UserID: 1, Title: "Test"})
|
||||||
|
|
||||||
|
err := svc.Delete(context.Background(), "tenant-1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, _ := svc.Get(context.Background(), "tenant-1", 1)
|
||||||
|
if dto != nil {
|
||||||
|
t.Error("expected nil after delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
50
internal/modules/notification/domain/notification.go
Normal file
50
internal/modules/notification/domain/notification.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
TenantID string `json:"tenant_id" gorm:"size:36;not null;index"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||||
|
Title string `json:"title" gorm:"size:256;not null"`
|
||||||
|
Content string `json:"content" gorm:"type:text"`
|
||||||
|
Type string `json:"type" gorm:"size:32;default:info"`
|
||||||
|
IsRead bool `json:"is_read" gorm:"default:false"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Notification) TableName() string {
|
||||||
|
return "notifications"
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationDTO struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
IsRead bool `json:"is_read"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToNotificationDTO(n *Notification) NotificationDTO {
|
||||||
|
return NotificationDTO{
|
||||||
|
ID: n.ID,
|
||||||
|
TenantID: n.TenantID,
|
||||||
|
UserID: n.UserID,
|
||||||
|
Title: n.Title,
|
||||||
|
Content: n.Content,
|
||||||
|
Type: n.Type,
|
||||||
|
IsRead: n.IsRead,
|
||||||
|
CreatedAt: n.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateNotificationRequest struct {
|
||||||
|
UserID uint `json:"user_id" binding:"required"`
|
||||||
|
Title string `json:"title" binding:"required"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
13
internal/modules/notification/domain/repository.go
Normal file
13
internal/modules/notification/domain/repository.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type NotificationRepository interface {
|
||||||
|
Create(ctx context.Context, notification *Notification) error
|
||||||
|
FindByID(ctx context.Context, tenantID string, id uint) (*Notification, error)
|
||||||
|
FindByUser(ctx context.Context, tenantID string, userID uint, page, pageSize int) ([]Notification, int64, error)
|
||||||
|
MarkAsRead(ctx context.Context, tenantID string, id uint) error
|
||||||
|
MarkAllAsRead(ctx context.Context, tenantID string, userID uint) error
|
||||||
|
CountUnread(ctx context.Context, tenantID string, userID uint) (int64, error)
|
||||||
|
Delete(ctx context.Context, tenantID string, id uint) error
|
||||||
|
}
|
||||||
11
internal/modules/notification/infrastructure/migrate.go
Normal file
11
internal/modules/notification/infrastructure/migrate.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/modules/notification/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(&domain.Notification{})
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/notification/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type notificationRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationRepository(db *gorm.DB) domain.NotificationRepository {
|
||||||
|
return ¬ificationRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationRepo) Create(ctx context.Context, n *domain.Notification) error {
|
||||||
|
return r.db.WithContext(ctx).Create(n).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationRepo) FindByID(ctx context.Context, tenantID string, id uint) (*domain.Notification, error) {
|
||||||
|
var n domain.Notification
|
||||||
|
err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&n).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationRepo) FindByUser(ctx context.Context, tenantID string, userID uint, page, pageSize int) ([]domain.Notification, int64, error) {
|
||||||
|
var notifications []domain.Notification
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID)
|
||||||
|
query.Model(&domain.Notification{}).Count(&total)
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(¬ifications).Error
|
||||||
|
return notifications, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationRepo) MarkAsRead(ctx context.Context, tenantID string, id uint) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("id = ? AND tenant_id = ?", id, tenantID).
|
||||||
|
Model(&domain.Notification{}).
|
||||||
|
Update("is_read", true).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationRepo) MarkAllAsRead(ctx context.Context, tenantID string, userID uint) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("tenant_id = ? AND user_id = ? AND is_read = ?", tenantID, userID, false).
|
||||||
|
Model(&domain.Notification{}).
|
||||||
|
Update("is_read", true).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationRepo) CountUnread(ctx context.Context, tenantID string, userID uint) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("tenant_id = ? AND user_id = ? AND is_read = ?", tenantID, userID, false).
|
||||||
|
Model(&domain.Notification{}).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationRepo) Delete(ctx context.Context, tenantID string, id uint) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("id = ? AND tenant_id = ?", id, tenantID).
|
||||||
|
Delete(&domain.Notification{}).Error
|
||||||
|
}
|
||||||
124
internal/modules/notification/interfaces/handler.go
Normal file
124
internal/modules/notification/interfaces/handler.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"mengstack/internal/kernel/response"
|
||||||
|
"mengstack/internal/modules/notification/application"
|
||||||
|
"mengstack/internal/modules/notification/domain"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
svc *application.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(svc *application.Service) *Handler {
|
||||||
|
return &Handler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Create(c *gin.Context) {
|
||||||
|
var req domain.CreateNotificationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid request: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
n, err := h.svc.Create(c.Request.Context(), tenantID, &req)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Get(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
n, err := h.svc.Get(c.Request.Context(), tenantID, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n == nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("notification not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListByUser(c *gin.Context) {
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid user_id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
notifications, total, err := h.svc.ListByUser(c.Request.Context(), tenantID, uint(userID), page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"items": notifications,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) MarkAsRead(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
if err := h.svc.MarkAsRead(c.Request.Context(), tenantID, uint(id)); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) MarkAllAsRead(c *gin.Context) {
|
||||||
|
userID := c.GetUint("user_id")
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
if err := h.svc.MarkAllAsRead(c.Request.Context(), tenantID, userID); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CountUnread(c *gin.Context) {
|
||||||
|
userID := c.GetUint("user_id")
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
count, err := h.svc.CountUnread(c.Request.Context(), tenantID, userID)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, gin.H{"count": count})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Delete(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
if err := h.svc.Delete(c.Request.Context(), tenantID, uint(id)); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "deleted"})
|
||||||
|
}
|
||||||
43
internal/modules/notification/interfaces/routes.go
Normal file
43
internal/modules/notification/interfaces/routes.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/app/middleware"
|
||||||
|
"mengstack/internal/modules/notification/application"
|
||||||
|
"mengstack/internal/modules/notification/infrastructure"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(
|
||||||
|
r *gin.Engine,
|
||||||
|
h *Handler,
|
||||||
|
authMW gin.HandlerFunc,
|
||||||
|
) {
|
||||||
|
notifications := r.Group("/api/v1/notifications")
|
||||||
|
notifications.Use(authMW, middleware.MultiTenant())
|
||||||
|
{
|
||||||
|
notifications.POST("", h.Create)
|
||||||
|
notifications.GET("/:id", h.Get)
|
||||||
|
notifications.DELETE("/:id", h.Delete)
|
||||||
|
notifications.PUT("/:id/read", h.MarkAsRead)
|
||||||
|
notifications.PUT("/read-all", h.MarkAllAsRead)
|
||||||
|
notifications.GET("/unread-count", h.CountUnread)
|
||||||
|
notifications.GET("/user/:userId", h.ListByUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Module = fx.Module("notification",
|
||||||
|
fx.Provide(
|
||||||
|
infrastructure.NewNotificationRepository,
|
||||||
|
application.NewService,
|
||||||
|
NewHandler,
|
||||||
|
),
|
||||||
|
fx.Invoke(func(db *gorm.DB, log *zap.Logger) {
|
||||||
|
if err := infrastructure.Migrate(db); err != nil {
|
||||||
|
log.Fatal("notification migration failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user