refactor: introduce Wire dependency injection and interface-based architecture

- Add Google Wire for compile-time dependency injection
- Replace concrete service types with interfaces across handlers
- Remove global state (DB, cache) in favor of constructor injection
- Split monolithic files into focused modules:
  - config: separate files for each config domain
  - dto: converters split by domain (user, post, message, group)
  - cache: separate metrics.go and redis_cache.go
- Introduce unified apperrors package with string-based error codes
- Add transaction support with context-aware repository methods
- Upgrade golang.org/x/crypto from 0.17.0 to 0.26.0
This commit is contained in:
2026-03-13 09:38:18 +08:00
parent 1216423350
commit cf36b1350d
49 changed files with 2571 additions and 2218 deletions

View File

@@ -72,6 +72,7 @@ func NewChatService(
userRepo *repository.UserRepository,
sensitive SensitiveService,
sseHub *sse.Hub,
cacheBackend cache.Cache,
) ChatService {
// 创建适配器
convRepoAdapter := cache.NewConversationRepositoryAdapter(repo)
@@ -79,7 +80,7 @@ func NewChatService(
// 创建会话缓存
conversationCache := cache.NewConversationCache(
cache.GetCache(),
cacheBackend,
convRepoAdapter,
msgRepoAdapter,
cache.DefaultConversationCacheSettings(),

View File

@@ -24,12 +24,12 @@ type CommentService struct {
}
// NewCommentService 创建评论服务
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *CommentService {
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache) *CommentService {
return &CommentService{
commentRepo: commentRepo,
postRepo: postRepo,
systemMessageService: systemMessageService,
cache: cache.GetCache(),
cache: cacheBackend,
gorseClient: gorseClient,
postAIService: postAIService,
}

View File

@@ -43,9 +43,6 @@ type emailCodeServiceImpl struct {
}
func NewEmailCodeService(emailService EmailService, cacheBackend cache.Cache) EmailCodeService {
if cacheBackend == nil {
cacheBackend = cache.GetCache()
}
return &emailCodeServiceImpl{
emailService: emailService,
cache: cacheBackend,

View File

@@ -8,6 +8,7 @@ import (
"time"
"carrot_bbs/internal/cache"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/sse"
"carrot_bbs/internal/pkg/utils"
@@ -23,25 +24,26 @@ const (
GroupCacheJitter = 0.1
)
// 群组服务错误定义
// 群组服务错误定义 - 使用统一的 AppError
var (
ErrGroupNotFound = &ServiceError{Code: 404, Message: "群组不存在"}
ErrNotGroupMember = &ServiceError{Code: 403, Message: "不是群成员"}
ErrNotGroupAdmin = &ServiceError{Code: 403, Message: "不是群管理员"}
ErrNotGroupOwner = &ServiceError{Code: 403, Message: "不是群主"}
ErrGroupFull = &ServiceError{Code: 400, Message: "群已满"}
ErrAlreadyMember = &ServiceError{Code: 400, Message: "已经是群成员"}
ErrCannotRemoveOwner = &ServiceError{Code: 400, Message: "不能移除群主"}
ErrCannotMuteOwner = &ServiceError{Code: 400, Message: "不能禁言群主"}
ErrMuted = &ServiceError{Code: 403, Message: "你已被禁言"}
ErrMuteAllEnabled = &ServiceError{Code: 403, Message: "全员禁言中"}
ErrCannotJoin = &ServiceError{Code: 400, Message: "该群不允许加入"}
ErrJoinRequestPending = &ServiceError{Code: 400, Message: "加群申请已提交"}
ErrGroupRequestNotFound = &ServiceError{Code: 404, Message: "加群请求不存在"}
ErrGroupRequestHandled = &ServiceError{Code: 400, Message: "该加群请求已处理"}
ErrNotRequestTarget = &ServiceError{Code: 400, Message: "不是邀请目标用户"}
ErrNoEligibleInvitee = &ServiceError{Code: 400, Message: "没有可邀请的用户"}
ErrNotMutualFollow = &ServiceError{Code: 400, Message: "仅支持邀请互相关注用户"}
ErrGroupNotFound = apperrors.ErrGroupNotFound
ErrNotGroupMember = apperrors.ErrNotGroupMember
ErrNotGroupAdmin = apperrors.ErrNotGroupAdmin
ErrNotGroupOwner = apperrors.ErrNotGroupOwner
ErrGroupFull = apperrors.ErrGroupFull
ErrAlreadyMember = apperrors.ErrAlreadyGroupMember
ErrCannotRemoveOwner = apperrors.ErrCannotRemoveOwner
ErrCannotMuteOwner = apperrors.ErrCannotMuteOwner
ErrMuted = apperrors.ErrMuted
ErrMuteAllEnabled = apperrors.ErrMuteAllEnabled
ErrCannotJoin = apperrors.ErrCannotJoin
ErrJoinRequestPending = apperrors.ErrJoinRequestPending
ErrGroupRequestNotFound = apperrors.ErrGroupRequestNotFound
ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled
ErrNotRequestTarget = apperrors.ErrNotRequestTarget
ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee
ErrNotMutualFollow = apperrors.ErrNotMutualFollow
ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted
)
// GroupService 群组服务接口
@@ -104,7 +106,7 @@ type groupService struct {
}
// NewGroupService 创建群组服务
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub) GroupService {
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub, cacheBackend cache.Cache) GroupService {
return &groupService{
db: db,
groupRepo: groupRepo,
@@ -113,7 +115,7 @@ func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo
requestRepo: repository.NewGroupJoinRequestRepository(db),
notifyRepo: repository.NewSystemNotificationRepository(db),
sseHub: sseHub,
cache: cache.GetCache(),
cache: cacheBackend,
}
}
@@ -937,7 +939,7 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
if req.RequestType == model.GroupJoinRequestTypeInvite {
if req.ReviewerID == "" {
// 被邀请人还未同意,管理员不能提前审批
return &ServiceError{Code: 400, Message: "被邀请人尚未同意邀请,无法审批"}
return ErrInviteNotAccepted
}
if req.ReviewerID != req.TargetUserID {
// ReviewerID 不是被邀请人,说明已经被其他人处理过

View File

@@ -37,14 +37,14 @@ type MessageService struct {
}
// NewMessageService 创建消息服务
func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *MessageService {
func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository, cacheBackend cache.Cache) *MessageService {
// 创建适配器
convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo)
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
// 创建会话缓存
conversationCache := cache.NewConversationCache(
cache.GetCache(),
cacheBackend,
convRepoAdapter,
msgRepoAdapter,
cache.DefaultConversationCacheSettings(),
@@ -54,7 +54,7 @@ func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *
db: db,
messageRepo: messageRepo,
conversationCache: conversationCache,
baseCache: cache.GetCache(),
baseCache: cacheBackend,
}
}

View File

@@ -5,6 +5,7 @@ import (
"time"
"carrot_bbs/internal/cache"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
@@ -23,10 +24,10 @@ type NotificationService struct {
}
// NewNotificationService 创建通知服务
func NewNotificationService(notificationRepo *repository.NotificationRepository) *NotificationService {
func NewNotificationService(notificationRepo *repository.NotificationRepository, cacheBackend cache.Cache) *NotificationService {
return &NotificationService{
notificationRepo: notificationRepo,
cache: cache.GetCache(),
cache: cacheBackend,
}
}
@@ -166,4 +167,4 @@ func (s *NotificationService) ClearAllNotifications(ctx context.Context, userID
}
// 错误定义
var ErrUnauthorizedNotification = &ServiceError{Code: 403, Message: "unauthorized to delete this notification"}
var ErrUnauthorizedNotification = apperrors.ErrUnauthorizedNotification

View File

@@ -23,23 +23,63 @@ const (
anonymousViewUserID = "_anon_view"
)
// PostService 帖子服务
type PostService struct {
// PostService 帖子服务接口
type PostService interface {
// 帖子CRUD
Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error)
GetByID(ctx context.Context, id string) (*model.Post, error)
Update(ctx context.Context, post *model.Post) error
UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error
Delete(ctx context.Context, id string) error
// 帖子列表
List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error)
GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error)
GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error)
// 关注和推荐
GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error)
GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
// 交互功能
Like(ctx context.Context, postID, userID string) error
Unlike(ctx context.Context, postID, userID string) error
IsLiked(ctx context.Context, postID, userID string) bool
Favorite(ctx context.Context, postID, userID string) error
Unfavorite(ctx context.Context, postID, userID string) error
IsFavorited(ctx context.Context, postID, userID string) bool
// 交互状态查询
GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error)
GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool)
// 其他
IncrementViews(ctx context.Context, postID, userID string) error
}
// postServiceImpl 帖子服务实现
type postServiceImpl struct {
postRepo *repository.PostRepository
systemMessageService SystemMessageService
cache cache.Cache
gorseClient gorse.Client
postAIService *PostAIService
txManager repository.TransactionManager // 事务管理器
}
// NewPostService 创建帖子服务
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *PostService {
return &PostService{
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager) PostService {
return &postServiceImpl{
postRepo: postRepo,
systemMessageService: systemMessageService,
cache: cache.GetCache(),
cache: cacheBackend,
gorseClient: gorseClient,
postAIService: postAIService,
txManager: txManager,
}
}
@@ -50,7 +90,7 @@ type PostListResult struct {
}
// Create 创建帖子
func (s *PostService) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) {
func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) {
post := &model.Post{
UserID: userID,
Title: title,
@@ -73,7 +113,7 @@ func (s *PostService) Create(ctx context.Context, userID, title, content string,
return s.postRepo.GetByID(post.ID)
}
func (s *PostService) reviewPostAsync(postID, userID, title, content string, images []string) {
func (s *postServiceImpl) reviewPostAsync(postID, userID, title, content string, images []string) {
defer func() {
if r := recover(); r != nil {
log.Printf("[ERROR] Panic in post moderation async flow, fallback publish post=%s panic=%v", postID, r)
@@ -139,7 +179,7 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
}
}
func (s *PostService) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
func (s *postServiceImpl) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
const maxAttempts = 3
const retryDelay = 200 * time.Millisecond
@@ -159,12 +199,12 @@ func (s *PostService) updateModerationStatusWithRetry(postID string, status mode
return lastErr
}
func (s *PostService) invalidatePostCaches(postID string) {
func (s *postServiceImpl) invalidatePostCaches(postID string) {
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
}
func (s *PostService) notifyModerationRejected(userID, reason string) {
func (s *postServiceImpl) notifyModerationRejected(userID, reason string) {
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
return
}
@@ -187,17 +227,17 @@ func (s *PostService) notifyModerationRejected(userID, reason string) {
}
// GetByID 根据ID获取帖子
func (s *PostService) GetByID(ctx context.Context, id string) (*model.Post, error) {
func (s *postServiceImpl) GetByID(ctx context.Context, id string) (*model.Post, error) {
return s.postRepo.GetByID(id)
}
// Update 更新帖子
func (s *PostService) Update(ctx context.Context, post *model.Post) error {
func (s *postServiceImpl) Update(ctx context.Context, post *model.Post) error {
return s.UpdateWithImages(ctx, post, nil)
}
// UpdateWithImages 更新帖子并可选更新图片images=nil 表示不更新图片)
func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error {
func (s *postServiceImpl) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error {
err := s.postRepo.UpdateWithImages(post, images)
if err != nil {
return err
@@ -211,7 +251,7 @@ func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, im
}
// Delete 删除帖子
func (s *PostService) Delete(ctx context.Context, id string) error {
func (s *postServiceImpl) Delete(ctx context.Context, id string) error {
err := s.postRepo.Delete(id)
if err != nil {
return err
@@ -234,7 +274,7 @@ func (s *PostService) Delete(ctx context.Context, id string) error {
}
// List 获取帖子列表(带缓存)
func (s *PostService) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) {
func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) {
cacheSettings := cache.GetSettings()
postListTTL := cacheSettings.PostListTTL
if postListTTL <= 0 {
@@ -299,22 +339,22 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
}
// GetLatestPosts 获取最新帖子(语义化别名)
func (s *PostService) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
func (s *postServiceImpl) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
return s.List(ctx, page, pageSize, userID, false)
}
// GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核)
func (s *PostService) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
func (s *postServiceImpl) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
return s.List(ctx, page, pageSize, userID, true)
}
// GetUserPosts 获取用户帖子
func (s *PostService) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
func (s *postServiceImpl) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
return s.postRepo.GetUserPosts(userID, page, pageSize, includePending)
}
// Like 点赞
func (s *PostService) Like(ctx context.Context, postID, userID string) error {
func (s *postServiceImpl) Like(ctx context.Context, postID, userID string) error {
// 获取帖子信息用于发送通知
post, err := s.postRepo.GetByID(postID)
if err != nil {
@@ -352,7 +392,7 @@ func (s *PostService) Like(ctx context.Context, postID, userID string) error {
}
// Unlike 取消点赞
func (s *PostService) Unlike(ctx context.Context, postID, userID string) error {
func (s *postServiceImpl) Unlike(ctx context.Context, postID, userID string) error {
err := s.postRepo.Unlike(postID, userID)
if err != nil {
return err
@@ -374,12 +414,12 @@ func (s *PostService) Unlike(ctx context.Context, postID, userID string) error {
}
// IsLiked 检查是否点赞
func (s *PostService) IsLiked(ctx context.Context, postID, userID string) bool {
func (s *postServiceImpl) IsLiked(ctx context.Context, postID, userID string) bool {
return s.postRepo.IsLiked(postID, userID)
}
// Favorite 收藏
func (s *PostService) Favorite(ctx context.Context, postID, userID string) error {
func (s *postServiceImpl) Favorite(ctx context.Context, postID, userID string) error {
// 获取帖子信息用于发送通知
post, err := s.postRepo.GetByID(postID)
if err != nil {
@@ -417,7 +457,7 @@ func (s *PostService) Favorite(ctx context.Context, postID, userID string) error
}
// Unfavorite 取消收藏
func (s *PostService) Unfavorite(ctx context.Context, postID, userID string) error {
func (s *postServiceImpl) Unfavorite(ctx context.Context, postID, userID string) error {
err := s.postRepo.Unfavorite(postID, userID)
if err != nil {
return err
@@ -439,12 +479,12 @@ func (s *PostService) Unfavorite(ctx context.Context, postID, userID string) err
}
// IsFavorited 检查是否收藏
func (s *PostService) IsFavorited(ctx context.Context, postID, userID string) bool {
func (s *postServiceImpl) IsFavorited(ctx context.Context, postID, userID string) bool {
return s.postRepo.IsFavorited(postID, userID)
}
// GetPostInteractionStatus 批量获取帖子的交互状态(点赞、收藏)
func (s *PostService) GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error) {
func (s *postServiceImpl) GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error) {
isLikedMap := make(map[string]bool)
isFavoritedMap := make(map[string]bool)
@@ -461,7 +501,7 @@ func (s *PostService) GetPostInteractionStatus(ctx context.Context, postIDs []st
}
// GetPostInteractionStatusSingle 获取单个帖子的交互状态
func (s *PostService) GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool) {
func (s *postServiceImpl) GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool) {
if userID == "" {
return false, false
}
@@ -469,7 +509,7 @@ func (s *PostService) GetPostInteractionStatusSingle(ctx context.Context, postID
}
// IncrementViews 增加帖子观看量并同步到Gorse
func (s *PostService) IncrementViews(ctx context.Context, postID, userID string) error {
func (s *postServiceImpl) IncrementViews(ctx context.Context, postID, userID string) error {
if err := s.postRepo.IncrementViews(postID); err != nil {
return err
}
@@ -494,17 +534,17 @@ func (s *PostService) IncrementViews(ctx context.Context, postID, userID string)
}
// GetFavorites 获取收藏列表
func (s *PostService) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
func (s *postServiceImpl) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
return s.postRepo.GetFavorites(userID, page, pageSize)
}
// Search 搜索帖子
func (s *PostService) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) {
func (s *postServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) {
return s.postRepo.Search(keyword, page, pageSize)
}
// GetFollowingPosts 获取关注用户的帖子(带缓存)
func (s *PostService) GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
func (s *postServiceImpl) GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
cacheSettings := cache.GetSettings()
postListTTL := cacheSettings.PostListTTL
if postListTTL <= 0 {
@@ -546,7 +586,7 @@ func (s *PostService) GetFollowingPosts(ctx context.Context, userID string, page
}
// GetHotPosts 获取热门帖子使用Gorse非个性化推荐
func (s *PostService) GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
func (s *postServiceImpl) GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
// 如果Gorse启用使用自定义的非个性化推荐器
if s.gorseClient.IsEnabled() {
offset := (page - 1) * pageSize
@@ -580,7 +620,7 @@ func (s *PostService) GetHotPosts(ctx context.Context, page, pageSize int) ([]*m
}
// getHotPostsFromDB 从数据库获取热门帖子(降级路径)
func (s *PostService) getHotPostsFromDB(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
func (s *postServiceImpl) getHotPostsFromDB(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
// 直接查询数据库不再使用本地缓存Gorse失败降级时使用
posts, total, err := s.postRepo.GetHotPosts(page, pageSize)
if err != nil {
@@ -590,7 +630,7 @@ func (s *PostService) getHotPostsFromDB(ctx context.Context, page, pageSize int)
}
// GetRecommendedPosts 获取推荐帖子
func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
func (s *postServiceImpl) GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
// 如果Gorse未启用或用户未登录降级为热门帖子
if !s.gorseClient.IsEnabled() || userID == "" {
return s.GetHotPosts(ctx, page, pageSize)
@@ -638,7 +678,7 @@ func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, pa
}
// buildPostCategories 构建帖子的类别标签
func (s *PostService) buildPostCategories(post *model.Post) []string {
func (s *postServiceImpl) buildPostCategories(post *model.Post) []string {
var categories []string
// 热度标签
@@ -676,3 +716,17 @@ func (s *PostService) buildPostCategories(post *model.Post) []string {
return categories
}
// ========== 事务管理器示例方法 ==========
// DeletePostWithTransaction 使用事务管理器删除帖子(示例)
// 此方法展示如何在 Service 层使用事务管理器控制跨多个 Repository 的事务
// 当需要在一个事务中执行多个 Repository 操作时,可以使用此模式
func (s *postServiceImpl) DeletePostWithTransaction(ctx context.Context, postID string) error {
// 使用事务管理器执行事务
return s.txManager.RunInTransaction(ctx, func(ctx context.Context) error {
// 在同一个事务中执行删除操作
// Repository 方法会通过 context 获取事务 TX
return s.postRepo.DeleteWithContext(ctx, postID)
})
}

View File

@@ -8,6 +8,7 @@ import (
"strings"
"carrot_bbs/internal/dto"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
@@ -15,10 +16,10 @@ import (
)
var (
ErrInvalidSchedulePayload = &ServiceError{Code: 400, Message: "invalid schedule payload"}
ErrScheduleCourseNotFound = &ServiceError{Code: 404, Message: "schedule course not found"}
ErrScheduleForbidden = &ServiceError{Code: 403, Message: "forbidden schedule operation"}
ErrScheduleColorDuplicated = &ServiceError{Code: 400, Message: "course color already used"}
ErrInvalidSchedulePayload = apperrors.ErrInvalidSchedulePayload
ErrScheduleCourseNotFound = apperrors.ErrScheduleCourseNotFound
ErrScheduleForbidden = apperrors.ErrScheduleForbidden
ErrScheduleColorDuplicated = apperrors.ErrScheduleColorDuplicated
)
var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`)

View File

@@ -1,16 +1,18 @@
package service
import (
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"errors"
"net/url"
"strings"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
var (
ErrStickerAlreadyExists = &ServiceError{Code: 400, Message: "sticker already exists"}
ErrInvalidStickerURL = &ServiceError{Code: 400, Message: "invalid sticker url"}
ErrStickerAlreadyExists = apperrors.ErrStickerAlreadyExists
ErrInvalidStickerURL = apperrors.ErrInvalidStickerURL
)
// StickerService 自定义表情服务接口

View File

@@ -42,13 +42,14 @@ func NewSystemMessageService(
pushService PushService,
userRepo *repository.UserRepository,
postRepo *repository.PostRepository,
cacheBackend cache.Cache,
) SystemMessageService {
return &systemMessageServiceImpl{
notifyRepo: notifyRepo,
pushService: pushService,
userRepo: userRepo,
postRepo: postRepo,
cache: cache.GetCache(),
cache: cacheBackend,
}
}

View File

@@ -16,6 +16,7 @@ import (
"strings"
"carrot_bbs/internal/pkg/s3"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
)
@@ -23,11 +24,11 @@ import (
// UploadService 上传服务
type UploadService struct {
s3Client *s3.Client
userService *UserService
userService UserService
}
// NewUploadService 创建上传服务
func NewUploadService(s3Client *s3.Client, userService *UserService) *UploadService {
func NewUploadService(s3Client *s3.Client, userService UserService) *UploadService {
return &UploadService{
s3Client: s3Client,
userService: userService,

View File

@@ -6,13 +6,59 @@ import (
"strings"
"carrot_bbs/internal/cache"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/utils"
"carrot_bbs/internal/repository"
)
// UserService 用户服务
type UserService struct {
// UserService 用户服务接口
type UserService interface {
// 验证码相关
SendRegisterCode(ctx context.Context, email string) error
SendPasswordResetCode(ctx context.Context, email string) error
SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error
SendChangePasswordCode(ctx context.Context, userID string) error
// 邮箱验证
VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error
// 用户认证
Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error)
Login(ctx context.Context, account, password string) (*model.User, error)
// 用户查询
GetUserByID(ctx context.Context, id string) (*model.User, error)
GetUserPostCount(ctx context.Context, userID string) (int64, error)
GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error)
GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error)
GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error)
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error)
CheckUsernameAvailable(ctx context.Context, username string) (bool, error)
// 用户更新
UpdateUser(ctx context.Context, user *model.User) error
ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error
ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error
// 关注相关
GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
FollowUser(ctx context.Context, followerID, followeeID string) error
UnfollowUser(ctx context.Context, followerID, followeeID string) error
GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error)
// 黑名单相关
BlockUser(ctx context.Context, blockerID, blockedID string) error
UnblockUser(ctx context.Context, blockerID, blockedID string) error
GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error)
IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error)
}
// userServiceImpl 用户服务实现
type userServiceImpl struct {
userRepo *repository.UserRepository
systemMessageService SystemMessageService
emailCodeService EmailCodeService
@@ -24,8 +70,8 @@ func NewUserService(
systemMessageService SystemMessageService,
emailService EmailService,
cacheBackend cache.Cache,
) *UserService {
return &UserService{
) UserService {
return &userServiceImpl{
userRepo: userRepo,
systemMessageService: systemMessageService,
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
@@ -33,7 +79,7 @@ func NewUserService(
}
// SendRegisterCode 发送注册验证码
func (s *UserService) SendRegisterCode(ctx context.Context, email string) error {
func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email string) error {
user, err := s.userRepo.GetByEmail(email)
if err == nil && user != nil {
return ErrEmailExists
@@ -42,7 +88,7 @@ func (s *UserService) SendRegisterCode(ctx context.Context, email string) error
}
// SendPasswordResetCode 发送找回密码验证码
func (s *UserService) SendPasswordResetCode(ctx context.Context, email string) error {
func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email string) error {
user, err := s.userRepo.GetByEmail(email)
if err != nil || user == nil {
return ErrUserNotFound
@@ -51,7 +97,7 @@ func (s *UserService) SendPasswordResetCode(ctx context.Context, email string) e
}
// SendCurrentUserEmailVerifyCode 发送当前用户邮箱验证验证码
func (s *UserService) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error {
func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error {
user, err := s.userRepo.GetByID(userID)
if err != nil || user == nil {
return ErrUserNotFound
@@ -78,7 +124,7 @@ func (s *UserService) SendCurrentUserEmailVerifyCode(ctx context.Context, userID
}
// VerifyCurrentUserEmail 验证当前用户邮箱
func (s *UserService) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error {
func (s *userServiceImpl) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error {
user, err := s.userRepo.GetByID(userID)
if err != nil || user == nil {
return ErrUserNotFound
@@ -107,7 +153,7 @@ func (s *UserService) VerifyCurrentUserEmail(ctx context.Context, userID, email,
}
// SendChangePasswordCode 发送修改密码验证码
func (s *UserService) SendChangePasswordCode(ctx context.Context, userID string) error {
func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID string) error {
user, err := s.userRepo.GetByID(userID)
if err != nil || user == nil {
return ErrUserNotFound
@@ -119,7 +165,7 @@ func (s *UserService) SendChangePasswordCode(ctx context.Context, userID string)
}
// Register 用户注册
func (s *UserService) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) {
func (s *userServiceImpl) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) {
// 验证用户名
if !utils.ValidateUsername(username) {
return nil, ErrInvalidUsername
@@ -199,7 +245,7 @@ func (s *UserService) Register(ctx context.Context, username, email, password, n
}
// Login 用户登录
func (s *UserService) Login(ctx context.Context, account, password string) (*model.User, error) {
func (s *userServiceImpl) Login(ctx context.Context, account, password string) (*model.User, error) {
account = strings.TrimSpace(account)
var (
user *model.User
@@ -228,22 +274,22 @@ func (s *UserService) Login(ctx context.Context, account, password string) (*mod
}
// GetUserByID 根据ID获取用户
func (s *UserService) GetUserByID(ctx context.Context, id string) (*model.User, error) {
func (s *userServiceImpl) GetUserByID(ctx context.Context, id string) (*model.User, error) {
return s.userRepo.GetByID(id)
}
// GetUserPostCount 获取用户帖子数(实时计算)
func (s *UserService) GetUserPostCount(ctx context.Context, userID string) (int64, error) {
func (s *userServiceImpl) GetUserPostCount(ctx context.Context, userID string) (int64, error) {
return s.userRepo.GetPostsCount(userID)
}
// GetUserPostCountBatch 批量获取用户帖子数(实时计算)
func (s *UserService) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) {
func (s *userServiceImpl) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) {
return s.userRepo.GetPostsCountBatch(userIDs)
}
// GetUserByIDWithFollowingStatus 根据ID获取用户包含当前用户是否关注的状态
func (s *UserService) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) {
func (s *userServiceImpl) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) {
user, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, false, err
@@ -263,7 +309,7 @@ func (s *UserService) GetUserByIDWithFollowingStatus(ctx context.Context, userID
}
// GetUserByIDWithMutualFollowStatus 根据ID获取用户包含双向关注状态
func (s *UserService) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) {
func (s *userServiceImpl) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) {
user, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, false, false, err
@@ -290,22 +336,22 @@ func (s *UserService) GetUserByIDWithMutualFollowStatus(ctx context.Context, use
}
// UpdateUser 更新用户
func (s *UserService) UpdateUser(ctx context.Context, user *model.User) error {
func (s *userServiceImpl) UpdateUser(ctx context.Context, user *model.User) error {
return s.userRepo.Update(user)
}
// GetFollowers 获取粉丝
func (s *UserService) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
func (s *userServiceImpl) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetFollowers(userID, page, pageSize)
}
// GetFollowing 获取关注
func (s *UserService) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
func (s *userServiceImpl) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetFollowing(userID, page, pageSize)
}
// FollowUser 关注用户
func (s *UserService) FollowUser(ctx context.Context, followerID, followeeID string) error {
func (s *userServiceImpl) FollowUser(ctx context.Context, followerID, followeeID string) error {
blocked, err := s.userRepo.IsBlockedEitherDirection(followerID, followeeID)
if err != nil {
return err
@@ -357,7 +403,7 @@ func (s *UserService) FollowUser(ctx context.Context, followerID, followeeID str
}
// UnfollowUser 取消关注用户
func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID string) error {
func (s *userServiceImpl) UnfollowUser(ctx context.Context, followerID, followeeID string) error {
// 检查是否已经关注
isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID)
if err != nil {
@@ -386,7 +432,7 @@ func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID s
}
// BlockUser 拉黑用户,并自动清理双向关注/粉丝关系
func (s *UserService) BlockUser(ctx context.Context, blockerID, blockedID string) error {
func (s *userServiceImpl) BlockUser(ctx context.Context, blockerID, blockedID string) error {
if blockerID == blockedID {
return ErrInvalidOperation
}
@@ -394,7 +440,7 @@ func (s *UserService) BlockUser(ctx context.Context, blockerID, blockedID string
}
// UnblockUser 取消拉黑
func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID string) error {
func (s *userServiceImpl) UnblockUser(ctx context.Context, blockerID, blockedID string) error {
if blockerID == blockedID {
return ErrInvalidOperation
}
@@ -402,17 +448,17 @@ func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID stri
}
// GetBlockedUsers 获取黑名单列表
func (s *UserService) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) {
func (s *userServiceImpl) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetBlockedUsers(blockerID, page, pageSize)
}
// IsBlocked 检查当前用户是否已拉黑目标用户
func (s *UserService) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) {
func (s *userServiceImpl) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) {
return s.userRepo.IsBlocked(blockerID, blockedID)
}
// GetFollowingList 获取关注列表(字符串参数版本)
func (s *UserService) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
// 转换字符串参数为整数
pageInt := 1
pageSizeInt := 20
@@ -434,7 +480,7 @@ func (s *UserService) GetFollowingList(ctx context.Context, userID, page, pageSi
}
// GetFollowersList 获取粉丝列表(字符串参数版本)
func (s *UserService) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
// 转换字符串参数为整数
pageInt := 1
pageSizeInt := 20
@@ -456,12 +502,12 @@ func (s *UserService) GetFollowersList(ctx context.Context, userID, page, pageSi
}
// GetMutualFollowStatus 批量获取双向关注状态
func (s *UserService) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
func (s *userServiceImpl) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs)
}
// CheckUsernameAvailable 检查用户名是否可用
func (s *UserService) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) {
func (s *userServiceImpl) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) {
user, err := s.userRepo.GetByUsername(username)
if err != nil {
return true, nil // 用户不存在,可用
@@ -470,7 +516,7 @@ func (s *UserService) CheckUsernameAvailable(ctx context.Context, username strin
}
// ChangePassword 修改密码
func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error {
func (s *userServiceImpl) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error {
// 获取用户
user, err := s.userRepo.GetByID(userID)
if err != nil {
@@ -500,7 +546,7 @@ func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, n
}
// ResetPasswordByEmail 通过邮箱重置密码
func (s *UserService) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error {
func (s *userServiceImpl) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error {
email = strings.TrimSpace(email)
if !utils.ValidateEmail(email) {
return ErrInvalidEmail
@@ -526,40 +572,29 @@ func (s *UserService) ResetPasswordByEmail(ctx context.Context, email, verificat
}
// Search 搜索用户
func (s *UserService) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) {
func (s *userServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.Search(keyword, page, pageSize)
}
// 错误定义
// 错误定义 - 使用统一的 AppError
var (
ErrInvalidUsername = &ServiceError{Code: 400, Message: "invalid username"}
ErrInvalidEmail = &ServiceError{Code: 400, Message: "invalid email"}
ErrInvalidPhone = &ServiceError{Code: 400, Message: "invalid phone number"}
ErrWeakPassword = &ServiceError{Code: 400, Message: "password too weak"}
ErrUsernameExists = &ServiceError{Code: 400, Message: "username already exists"}
ErrEmailExists = &ServiceError{Code: 400, Message: "email already exists"}
ErrPhoneExists = &ServiceError{Code: 400, Message: "phone number already exists"}
ErrUserNotFound = &ServiceError{Code: 404, Message: "user not found"}
ErrUserBanned = &ServiceError{Code: 403, Message: "user is banned"}
ErrUserBlocked = &ServiceError{Code: 403, Message: "blocked relationship exists"}
ErrInvalidOperation = &ServiceError{Code: 400, Message: "invalid operation"}
ErrEmailServiceUnavailable = &ServiceError{Code: 503, Message: "email service unavailable"}
ErrVerificationCodeTooFrequent = &ServiceError{Code: 429, Message: "verification code sent too frequently"}
ErrVerificationCodeInvalid = &ServiceError{Code: 400, Message: "invalid verification code"}
ErrVerificationCodeExpired = &ServiceError{Code: 400, Message: "verification code expired"}
ErrVerificationCodeUnavailable = &ServiceError{Code: 500, Message: "verification code storage unavailable"}
ErrEmailAlreadyVerified = &ServiceError{Code: 400, Message: "email already verified"}
ErrEmailNotBound = &ServiceError{Code: 400, Message: "email not bound"}
ErrInvalidUsername = apperrors.ErrInvalidUsername
ErrInvalidEmail = apperrors.ErrInvalidEmail
ErrInvalidPhone = apperrors.ErrInvalidPhone
ErrWeakPassword = apperrors.ErrWeakPassword
ErrUsernameExists = apperrors.ErrUsernameExists
ErrEmailExists = apperrors.ErrEmailExists
ErrPhoneExists = apperrors.ErrPhoneExists
ErrUserNotFound = apperrors.ErrUserNotFound
ErrUserBanned = apperrors.ErrUserBanned
ErrUserBlocked = apperrors.ErrUserBlocked
ErrInvalidOperation = apperrors.ErrInvalidOperation
ErrEmailServiceUnavailable = apperrors.ErrEmailServiceUnavailable
ErrVerificationCodeTooFrequent = apperrors.ErrVerificationCodeTooFrequent
ErrVerificationCodeInvalid = apperrors.ErrVerificationCodeInvalid
ErrVerificationCodeExpired = apperrors.ErrVerificationCodeExpired
ErrVerificationCodeUnavailable = apperrors.ErrVerificationCodeUnavailable
ErrEmailAlreadyVerified = apperrors.ErrEmailAlreadyVerified
ErrEmailNotBound = apperrors.ErrEmailNotBound
ErrInvalidCredentials = apperrors.ErrInvalidCredentials
)
// ServiceError 服务错误
type ServiceError struct {
Code int
Message string
}
func (e *ServiceError) Error() string {
return e.Message
}
var ErrInvalidCredentials = &ServiceError{Code: 401, Message: "invalid username or password"}