增加站内信未读提醒和后台通知中心+txt 校验

This commit is contained in:
yml2213
2026-06-19 15:24:15 +08:00
parent 8d5094a8d0
commit a4cdc3e806
30 changed files with 1836 additions and 53 deletions
@@ -20,3 +20,7 @@ type PaginatedResult struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type UnreadCountDTO struct {
UnreadCount int64 `json:"unread_count"`
}
@@ -48,6 +48,34 @@ func (h *Handler) List(c *gin.Context) {
response.OK(c, result)
}
func (h *Handler) UnreadCount(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
result, err := h.service.UnreadCount(c.Request.Context(), userID)
if err != nil {
writeNotificationError(c, err)
return
}
response.OK(c, result)
}
func (h *Handler) MarkAllRead(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
count, err := h.service.MarkAllRead(c.Request.Context(), userID)
if err != nil {
writeNotificationError(c, err)
return
}
response.OK(c, gin.H{"read_count": count})
}
func (h *Handler) MarkRead(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
@@ -79,6 +107,8 @@ func writeNotificationError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接")
case errors.Is(err, ErrNotificationNotFound):
response.NotFound(c, "通知不存在")
default:
response.ServiceUnavailable(c, "通知服务暂时不可用")
}
@@ -45,9 +45,40 @@ func (r *Repository) List(ctx context.Context, userID uint64, page, pageSize int
func (r *Repository) MarkRead(ctx context.Context, userID uint64, id uint64) error {
now := time.Now()
return r.db.WithContext(ctx).Model(&model.Notification{}).
result := r.db.WithContext(ctx).Model(&model.Notification{}).
Where("id = ? AND user_id = ?", id, userID).
Update("read_at", now).Error
Update("read_at", now)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
var total int64
if err := r.db.WithContext(ctx).Model(&model.Notification{}).
Where("id = ? AND user_id = ?", id, userID).
Count(&total).Error; err != nil {
return err
}
if total == 0 {
return ErrNotificationNotFound
}
}
return nil
}
func (r *Repository) UnreadCount(ctx context.Context, userID uint64) (int64, error) {
var total int64
err := r.db.WithContext(ctx).Model(&model.Notification{}).
Where("user_id = ? AND read_at IS NULL", userID).
Count(&total).Error
return total, err
}
func (r *Repository) MarkAllRead(ctx context.Context, userID uint64) (int64, error) {
now := time.Now()
result := r.db.WithContext(ctx).Model(&model.Notification{}).
Where("user_id = ? AND read_at IS NULL", userID).
Update("read_at", now)
return result.RowsAffected, result.Error
}
func Append(tx *gorm.DB, entries ...Entry) error {
@@ -0,0 +1,65 @@
package notification
import (
"context"
"errors"
"testing"
"hfb_sys/backend/internal/database"
"hfb_sys/backend/internal/model"
)
func TestRepositoryUnreadCountAndMarkRead(t *testing.T) {
db := database.NewTestDB()
if err := db.AutoMigrate(&model.Notification{}); err != nil {
t.Fatalf("AutoMigrate() error = %v", err)
}
repo := NewRepository(db)
ctx := context.Background()
if err := db.Create(&model.Notification{
UserID: 10,
Type: "order",
Title: "订单通知",
Content: "请处理订单",
}).Error; err != nil {
t.Fatalf("Create() error = %v", err)
}
count, err := repo.UnreadCount(ctx, 10)
if err != nil {
t.Fatalf("UnreadCount() error = %v", err)
}
if count != 1 {
t.Fatalf("UnreadCount() = %d, want 1", count)
}
if err := repo.MarkRead(ctx, 10, 1); err != nil {
t.Fatalf("MarkRead() error = %v", err)
}
// 重复标记应保持幂等,不应误报不存在。
if err := repo.MarkRead(ctx, 10, 1); err != nil {
t.Fatalf("MarkRead() repeat error = %v", err)
}
count, err = repo.UnreadCount(ctx, 10)
if err != nil {
t.Fatalf("UnreadCount() after read error = %v", err)
}
if count != 0 {
t.Fatalf("UnreadCount() after read = %d, want 0", count)
}
}
func TestRepositoryMarkReadNotFound(t *testing.T) {
db := database.NewTestDB()
if err := db.AutoMigrate(&model.Notification{}); err != nil {
t.Fatalf("AutoMigrate() error = %v", err)
}
repo := NewRepository(db)
err := repo.MarkRead(context.Background(), 10, 99)
if !errors.Is(err, ErrNotificationNotFound) {
t.Fatalf("MarkRead() error = %v, want ErrNotificationNotFound", err)
}
}
@@ -6,6 +6,7 @@ import (
)
var ErrDependencyUnavailable = errors.New("dependency unavailable")
var ErrNotificationNotFound = errors.New("notification not found")
type Service struct {
repo *Repository
@@ -28,3 +29,21 @@ func (s *Service) MarkRead(ctx context.Context, userID uint64, id uint64) error
}
return s.repo.MarkRead(ctx, userID, id)
}
func (s *Service) UnreadCount(ctx context.Context, userID uint64) (*UnreadCountDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
count, err := s.repo.UnreadCount(ctx, userID)
if err != nil {
return nil, err
}
return &UnreadCountDTO{UnreadCount: count}, nil
}
func (s *Service) MarkAllRead(ctx context.Context, userID uint64) (int64, error) {
if s.repo == nil {
return 0, ErrDependencyUnavailable
}
return s.repo.MarkAllRead(ctx, userID)
}