refactor(server): decouple services and improve architecture
All checks were successful
Build Backend / build (push) Successful in 4m55s
Build Backend / build-docker (push) Successful in 10m34s

- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
This commit is contained in:
2026-06-15 03:41:59 +08:00
parent 9951043034
commit d9aa4b46c3
78 changed files with 2156 additions and 1640 deletions

View File

@@ -5,6 +5,7 @@ import (
"errors"
"with_you/internal/dto"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/repository"
)
@@ -291,48 +292,36 @@ func (s *adminGroupServiceImpl) RemoveMember(ctx context.Context, groupID string
}
// TransferOwner 转让群主
// 委托 groupRepo.TransferOwner 在单个数据库事务内原子完成:
// - 更新 group.owner_id 为新群主
// - 原群主角色降级为普通成员
// - 新群主角色升级为 owner
//
// 任何一步失败都会整体回滚,避免出现「两个群主」或「零群主」的数据不一致。
func (s *adminGroupServiceImpl) TransferOwner(ctx context.Context, groupID string, newOwnerID string) (*dto.AdminGroupDetailResponse, error) {
// 先检查群组是否存在
// 校验群组存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, errors.New("群组不存在")
return nil, apperrors.ErrGroupNotFound
}
// 检查新群主是否是群成员
// 校验新群主是群成员
newOwnerMember, err := s.groupRepo.GetMember(groupID, newOwnerID)
if err != nil {
return nil, errors.New("新群主不是群组成员")
return nil, apperrors.ErrNotGroupMember
}
// 新群主已是群主则无需转让
if newOwnerMember.Role == model.GroupRoleOwner {
return nil, apperrors.ErrInvalidOperation
}
// 获取当前群主信息
currentOwnerMember, err := s.groupRepo.GetMember(groupID, group.OwnerID)
if err != nil {
return nil, errors.New("当前群主信息不存在")
// 校验当前群主信息存在
if _, err := s.groupRepo.GetMember(groupID, group.OwnerID); err != nil {
return nil, apperrors.ErrGroupNotFound
}
oldOwnerID := group.OwnerID
// 更新当前群主为普通成员
err = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleMember)
if err != nil {
return nil, err
}
// 更新新群主角色
err = s.groupRepo.SetMemberRole(groupID, newOwnerID, model.GroupRoleOwner)
if err != nil {
// 回滚
_ = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleOwner)
return nil, err
}
// 更新群组的OwnerID
group.OwnerID = newOwnerID
err = s.groupRepo.Update(group)
if err != nil {
// 回滚
_ = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleOwner)
_ = s.groupRepo.SetMemberRole(groupID, newOwnerID, newOwnerMember.Role)
// 原子转让:所有写入在 groupRepo.TransferOwner 的单个事务内完成
if err := s.groupRepo.TransferOwner(groupID, group.OwnerID, newOwnerID); err != nil {
return nil, err
}
@@ -373,7 +362,5 @@ func (s *adminGroupServiceImpl) TransferOwner(ctx context.Context, groupID strin
}
}
_ = currentOwnerMember // 避免 unused variable 警告
return response, nil
}

View File

@@ -0,0 +1,179 @@
package service
import (
"context"
"errors"
"testing"
"with_you/internal/model"
"with_you/internal/repository"
)
// stubGroupRepo 通过嵌入 repository.GroupRepository 接口(保持 nil
// 仅覆盖测试涉及的方法。未覆盖的方法被调用时会 panic——这能在测试中
// 提前暴露「Service 调用了非预期 repo 方法」的回归。
type stubGroupRepo struct {
repository.GroupRepository // nil 嵌入,未覆盖方法 panic
groupByID map[string]*model.Group
groupByIDErr error
memberByUser map[string]*model.GroupMember
memberErr error
transferCalls []transferCall
transferErr error
}
type transferCall struct {
GroupID, OldOwner, NewOwner string
}
func (s *stubGroupRepo) GetByID(id string) (*model.Group, error) {
if s.groupByIDErr != nil {
return nil, s.groupByIDErr
}
g, ok := s.groupByID[id]
if !ok {
return nil, errors.New("not found")
}
return g, nil
}
func (s *stubGroupRepo) GetMember(groupID, userID string) (*model.GroupMember, error) {
if s.memberErr != nil {
return nil, s.memberErr
}
m, ok := s.memberByUser[userID]
if !ok {
return nil, errors.New("not member")
}
return m, nil
}
func (s *stubGroupRepo) TransferOwner(groupID, oldOwnerID, newOwnerID string) error {
s.transferCalls = append(s.transferCalls, transferCall{
GroupID: groupID, OldOwner: oldOwnerID, NewOwner: newOwnerID,
})
// 模拟真实 DB 语义:事务提交后 owner_id 已变更,后续 GetByID 读到新值
if g, ok := s.groupByID[groupID]; ok {
g.OwnerID = newOwnerID
}
return s.transferErr
}
// stubUserRepo 同样仅覆盖 GetByID。
type stubUserRepo struct {
repository.UserRepository // nil 嵌入
byID map[string]*model.User
}
func (s *stubUserRepo) GetByID(id string) (*model.User, error) {
u, ok := s.byID[id]
if !ok {
return nil, errors.New("not found")
}
return u, nil
}
// TestTransferOwner_Success 正常转让TransferOwner 应被调用一次,
// 参数为 (groupID, 当前 owner, 新 owner)。
func TestTransferOwner_Success(t *testing.T) {
repo := &stubGroupRepo{
groupByID: map[string]*model.Group{
"g1": {ID: "g1", OwnerID: "old-owner", Name: "测试群"},
},
memberByUser: map[string]*model.GroupMember{
"old-owner": {UserID: "old-owner", GroupID: "g1", Role: model.GroupRoleOwner},
"new-owner": {UserID: "new-owner", GroupID: "g1", Role: model.GroupRoleMember},
},
}
userRepo := &stubUserRepo{
byID: map[string]*model.User{
"new-owner": {ID: "new-owner", Username: "new", Nickname: "新群主"},
},
}
svc := NewAdminGroupService(repo, userRepo)
resp, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
if err != nil {
t.Fatalf("TransferOwner failed: %v", err)
}
// 校验 TransferOwner 恰好被调用一次,参数正确
if len(repo.transferCalls) != 1 {
t.Fatalf("expected 1 TransferOwner call, got %d", len(repo.transferCalls))
}
tc := repo.transferCalls[0]
if tc.GroupID != "g1" || tc.OldOwner != "old-owner" || tc.NewOwner != "new-owner" {
t.Errorf("TransferOwner called with (%s,%s,%s), want (g1,old-owner,new-owner)",
tc.GroupID, tc.OldOwner, tc.NewOwner)
}
// 响应应反映新群主
if resp == nil {
t.Fatal("expected non-nil response")
}
if resp.OwnerID != "new-owner" {
t.Errorf("response OwnerID = %s, want new-owner", resp.OwnerID)
}
}
// TestTransferOwner_NewOwnerNotMember 新群主非成员时应返回错误,
// 且 TransferOwner 不应被调用。
func TestTransferOwner_NewOwnerNotMember(t *testing.T) {
repo := &stubGroupRepo{
groupByID: map[string]*model.Group{
"g1": {ID: "g1", OwnerID: "old-owner"},
},
memberByUser: map[string]*model.GroupMember{
"old-owner": {UserID: "old-owner", GroupID: "g1", Role: model.GroupRoleOwner},
// new-owner 不在成员表
},
}
svc := NewAdminGroupService(repo, &stubUserRepo{})
_, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
if err == nil {
t.Fatal("expected error when new owner is not a member")
}
if len(repo.transferCalls) != 0 {
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
}
}
// TestTransferOwner_GroupNotFound 群不存在时应返回错误。
func TestTransferOwner_GroupNotFound(t *testing.T) {
repo := &stubGroupRepo{
groupByID: map[string]*model.Group{}, // g1 不存在
}
svc := NewAdminGroupService(repo, &stubUserRepo{})
_, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
if err == nil {
t.Fatal("expected error when group does not exist")
}
if len(repo.transferCalls) != 0 {
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
}
}
// TestTransferOwner_AlreadyOwner 新群主已是群主时应拒绝(无操作幂等)。
func TestTransferOwner_AlreadyOwner(t *testing.T) {
repo := &stubGroupRepo{
groupByID: map[string]*model.Group{
"g1": {ID: "g1", OwnerID: "same-user"},
},
memberByUser: map[string]*model.GroupMember{
"same-user": {UserID: "same-user", GroupID: "g1", Role: model.GroupRoleOwner},
},
}
svc := NewAdminGroupService(repo, &stubUserRepo{})
_, err := svc.TransferOwner(context.Background(), "g1", "same-user")
if err == nil {
t.Fatal("expected error when transferring to existing owner")
}
if len(repo.transferCalls) != 0 {
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
}
}

View File

@@ -7,6 +7,7 @@ import (
"with_you/internal/dto"
"with_you/internal/model"
"with_you/internal/query"
"with_you/internal/repository"
"go.uber.org/zap"
@@ -15,7 +16,7 @@ import (
// AdminReportService 管理端举报服务接口
type AdminReportService interface {
// GetReportList 获取举报列表
GetReportList(ctx context.Context, query dto.AdminReportListQuery) ([]dto.AdminReportListResponse, int64, error)
GetReportList(ctx context.Context, query query.AdminReportListQuery) ([]dto.AdminReportListResponse, int64, error)
// GetReportDetail 获取举报详情
GetReportDetail(ctx context.Context, id string) (*dto.AdminReportDetailResponse, error)
// HandleReport 处理举报
@@ -63,7 +64,7 @@ func NewAdminReportService(
}
// GetReportList 获取举报列表
func (s *adminReportServiceImpl) GetReportList(ctx context.Context, query dto.AdminReportListQuery) ([]dto.AdminReportListResponse, int64, error) {
func (s *adminReportServiceImpl) GetReportList(ctx context.Context, query query.AdminReportListQuery) ([]dto.AdminReportListResponse, int64, error) {
// 设置默认分页参数
if query.Page <= 0 {
query.Page = 1

View File

@@ -26,11 +26,11 @@ const (
CallLifetimeMs = 60000 // 通话邀请有效期 60秒 (参考 Matrix)
CallTimeoutMs = 120000 // 通话超时(无人接听) 120秒
redisCallKeyPrefix = "call:"
redisCallByUserPrefix = "call_by_user:"
redisCallAcceptPrefix = "call:accept:"
redisCallTTL = 120 * time.Second
redisCallAcceptLockTTL = 10 * time.Second
redisCallKeyPrefix = "call:"
redisCallByUserPrefix = "call_by_user:"
redisCallAcceptPrefix = "call:accept:"
redisCallTTL = 120 * time.Second
redisCallAcceptLockTTL = 10 * time.Second
)
// activeCallRedisData Redis 中存储的通话数据(精简字段)
@@ -431,7 +431,7 @@ func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversati
callerID: {UserID: callerID, Status: model.ParticipantStatusJoined, JoinedAt: &now},
calleeID: {UserID: calleeID, Status: model.ParticipantStatusInvited},
},
}
}
// 存储到内存
s.storeActiveCall(call)
@@ -451,7 +451,7 @@ func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversati
"media_type": mediaType,
"created_at": now.UnixMilli(),
"lifetime": CallLifetimeMs,
}
}
online := s.hub.PublishToUserOnline(calleeID, "call_incoming", payload)
@@ -510,9 +510,9 @@ func (s *callService) Accept(ctx context.Context, callID, userID string) (*Activ
// 通知拨打方
s.hub.PublishToUserOnline(call.CallerID, "call_accepted", map[string]any{
"call_id": callID,
"started_at": now.UnixMilli(),
})
"call_id": callID,
"started_at": now.UnixMilli(),
})
// 通知被叫方其他设备
s.hub.PublishToUserOnline(userID, "call_answered_elsewhere", map[string]any{
@@ -896,7 +896,6 @@ func (s *callService) GetCallHistory(ctx context.Context, userID string, page, p
return s.callRepo.GetCallHistory(userID, page, pageSize)
}
// saveCallHistory 保存通话历史到数据库
func (s *callService) saveCallHistory(call *ActiveCall, status model.CallStatus, duration int64, endedBy string) {
ctx := context.Background()

View File

@@ -42,7 +42,7 @@ type ChatService interface {
// 已读管理
MarkAsRead(ctx context.Context, conversationID string, userID string, seq int64) error
MarkAsReadBatch(ctx context.Context, userID string, items []dto.BatchMarkReadItem) (int, error)
MarkAsReadBatch(ctx context.Context, userID string, items []dto.BatchMarkReadItem) (int, error)
GetUnreadCount(ctx context.Context, conversationID string, userID string) (int64, error)
GetAllUnreadCount(ctx context.Context, userID string) (int64, error)
@@ -69,21 +69,21 @@ type ChatService interface {
GetUnreadCountBatch(ctx context.Context, userID string, convIDs []string) (map[string]int64, error)
GetLastMessagesBatch(ctx context.Context, convIDs []string) (map[string]*model.Message, error)
// 已读位置OpenIM 风格缓存)
GetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error)
// 已读位置OpenIM 风格缓存)
GetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error)
}
// chatServiceImpl 聊天服务实现
type chatServiceImpl struct {
repo repository.MessageRepository
userRepo repository.UserRepository
sensitive SensitiveService
wsHub ws.MessagePublisher
pushSvc PushService
cache cache.Cache
repo repository.MessageRepository
userRepo repository.UserRepository
sensitive SensitiveService
wsHub ws.MessagePublisher
pushSvc PushService
cache cache.Cache
conversationCache *cache.ConversationCache
uploadService *UploadService
uploadService UploadService
versionLogRepo repository.ConversationVersionLogRepository
versionLogEnabled bool
versionLogMaxGap int64
@@ -98,7 +98,7 @@ func NewChatService(
sensitive SensitiveService,
publisher ws.MessagePublisher,
cacheBackend cache.Cache,
uploadService *UploadService,
uploadService UploadService,
pushSvc PushService,
seqBufferMgr *cache.SeqBufferManager,
pushWorker *PushWorker,
@@ -479,7 +479,7 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
Status: model.MessageStatusNormal,
}
// seq 由 CreateMessageWithSeq 在事务内原子分配
// seq 由 CreateMessageWithSeq 在事务内原子分配
// 使用事务创建消息并更新seq
if err := s.repo.CreateMessageWithSeq(message); err != nil {
@@ -1071,7 +1071,7 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
Status: model.MessageStatusNormal,
}
// seq 由 CreateMessageWithSeq 在事务内原子分配
// seq 由 CreateMessageWithSeq 在事务内原子分配
if err := s.repo.CreateMessageWithSeq(message); err != nil {
return nil, fmt.Errorf("failed to save message: %w", err)

View File

@@ -6,9 +6,9 @@ import (
"log"
"strings"
apperrors "with_you/internal/errors"
"with_you/internal/cache"
"with_you/internal/dto"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/pkg/hook"
@@ -17,18 +17,34 @@ import (
"go.uber.org/zap"
)
type CommentService struct {
// CommentService 评论服务接口
type CommentService interface {
Create(ctx context.Context, postID, userID, content string, segments model.MessageSegments, parentID *string, images string, imageURLs []string) (*model.Comment, error)
GetByID(ctx context.Context, id string) (*model.Comment, error)
GetByPostID(ctx context.Context, postID string, page, pageSize int) ([]*model.Comment, int64, error)
GetRepliesByRootID(ctx context.Context, rootID string, page, pageSize int) ([]*model.Comment, int64, error)
GetReplies(ctx context.Context, parentID string) ([]*model.Comment, error)
Update(ctx context.Context, userID string, comment *model.Comment) error
Delete(ctx context.Context, userID string, id string) error
Like(ctx context.Context, commentID, userID string) error
Unlike(ctx context.Context, commentID, userID string) error
IsLiked(ctx context.Context, commentID, userID string) bool
GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error)
GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error)
}
type commentService struct {
commentRepo repository.CommentRepository
postRepo repository.PostRepository
systemMessageService SystemMessageService
cache cache.Cache
postAIService *PostAIService
postAIService PostAIService
logService *LogService
hookManager *hook.Manager
}
func NewCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository, systemMessageService SystemMessageService, postAIService *PostAIService, cacheBackend cache.Cache, hookManager *hook.Manager, logService *LogService) *CommentService {
return &CommentService{
func NewCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository, systemMessageService SystemMessageService, postAIService PostAIService, cacheBackend cache.Cache, hookManager *hook.Manager, logService *LogService) CommentService {
return &commentService{
commentRepo: commentRepo,
postRepo: postRepo,
systemMessageService: systemMessageService,
@@ -40,7 +56,7 @@ func NewCommentService(commentRepo repository.CommentRepository, postRepo reposi
}
// Create 创建评论
func (s *CommentService) Create(ctx context.Context, postID, userID, content string, segments model.MessageSegments, parentID *string, images string, imageURLs []string) (*model.Comment, error) {
func (s *commentService) Create(ctx context.Context, postID, userID, content string, segments model.MessageSegments, parentID *string, images string, imageURLs []string) (*model.Comment, error) {
if s.postAIService != nil {
// 采用异步审核,前端先立即返回
}
@@ -110,7 +126,7 @@ func (s *CommentService) Create(ctx context.Context, postID, userID, content str
}
// processCommentMentionNotifications 处理评论中 @提及的通知
func (s *CommentService) processCommentMentionNotifications(ctx context.Context, authorID, postID, postTitle string, segments model.MessageSegments) {
func (s *commentService) processCommentMentionNotifications(ctx context.Context, authorID, postID, postTitle string, segments model.MessageSegments) {
if s.systemMessageService == nil {
return
}
@@ -130,7 +146,7 @@ func (s *CommentService) processCommentMentionNotifications(ctx context.Context,
}
}
func (s *CommentService) reviewCommentAsync(
func (s *commentService) reviewCommentAsync(
commentID, userID, postID, content string,
imageURLs []string,
parentID *string,
@@ -211,7 +227,7 @@ func (s *CommentService) reviewCommentAsync(
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
}
func (s *CommentService) applyCommentPublishedStats(commentID string) error {
func (s *commentService) applyCommentPublishedStats(commentID string) error {
comment, err := s.commentRepo.GetByID(commentID)
if err != nil {
return err
@@ -219,12 +235,12 @@ func (s *CommentService) applyCommentPublishedStats(commentID string) error {
return s.commentRepo.ApplyPublishedStats(comment)
}
func (s *CommentService) invalidatePostCaches(postID string) {
func (s *commentService) invalidatePostCaches(postID string) {
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
}
func (s *CommentService) afterCommentPublished(userID, postID, commentID string, parentID *string, parentUserID, postOwnerID string) {
func (s *commentService) afterCommentPublished(userID, postID, commentID string, parentID *string, parentUserID, postOwnerID string) {
// 发送系统消息通知
if s.systemMessageService != nil {
go func() {
@@ -254,7 +270,7 @@ func (s *CommentService) afterCommentPublished(userID, postID, commentID string,
}
func (s *CommentService) notifyCommentModerationRejected(userID, reason string) {
func (s *commentService) notifyCommentModerationRejected(userID, reason string) {
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
return
}
@@ -279,28 +295,28 @@ func (s *CommentService) notifyCommentModerationRejected(userID, reason string)
}
// GetByID 根据ID获取评论
func (s *CommentService) GetByID(ctx context.Context, id string) (*model.Comment, error) {
func (s *commentService) GetByID(ctx context.Context, id string) (*model.Comment, error) {
return s.commentRepo.GetByID(id)
}
// GetByPostID 获取帖子评论
func (s *CommentService) GetByPostID(ctx context.Context, postID string, page, pageSize int) ([]*model.Comment, int64, error) {
func (s *commentService) GetByPostID(ctx context.Context, postID string, page, pageSize int) ([]*model.Comment, int64, error) {
// 使用带回复的查询默认加载前3条回复
return s.commentRepo.GetByPostIDWithReplies(postID, page, pageSize, 3)
}
// GetRepliesByRootID 根据根评论ID分页获取回复
func (s *CommentService) GetRepliesByRootID(ctx context.Context, rootID string, page, pageSize int) ([]*model.Comment, int64, error) {
func (s *commentService) GetRepliesByRootID(ctx context.Context, rootID string, page, pageSize int) ([]*model.Comment, int64, error) {
return s.commentRepo.GetRepliesByRootID(rootID, page, pageSize)
}
// GetReplies 获取回复
func (s *CommentService) GetReplies(ctx context.Context, parentID string) ([]*model.Comment, error) {
func (s *commentService) GetReplies(ctx context.Context, parentID string) ([]*model.Comment, error) {
return s.commentRepo.GetReplies(parentID)
}
// Update 更新评论
func (s *CommentService) Update(ctx context.Context, userID string, comment *model.Comment) error {
func (s *commentService) Update(ctx context.Context, userID string, comment *model.Comment) error {
if comment.UserID != userID {
return apperrors.ErrForbidden
}
@@ -308,7 +324,7 @@ func (s *CommentService) Update(ctx context.Context, userID string, comment *mod
}
// Delete 删除评论
func (s *CommentService) Delete(ctx context.Context, userID string, id string) error {
func (s *commentService) Delete(ctx context.Context, userID string, id string) error {
comment, err := s.commentRepo.GetByID(id)
if err != nil {
return err
@@ -324,7 +340,7 @@ func (s *CommentService) Delete(ctx context.Context, userID string, id string) e
}
// Like 点赞评论
func (s *CommentService) Like(ctx context.Context, commentID, userID string) error {
func (s *commentService) Like(ctx context.Context, commentID, userID string) error {
// 获取评论信息用于发送通知
comment, err := s.commentRepo.GetByID(commentID)
if err != nil {
@@ -371,21 +387,21 @@ func (s *CommentService) Like(ctx context.Context, commentID, userID string) err
}
// Unlike 取消点赞评论
func (s *CommentService) Unlike(ctx context.Context, commentID, userID string) error {
func (s *commentService) Unlike(ctx context.Context, commentID, userID string) error {
return s.commentRepo.Unlike(commentID, userID)
}
// IsLiked 检查是否已点赞
func (s *CommentService) IsLiked(ctx context.Context, commentID, userID string) bool {
func (s *commentService) IsLiked(ctx context.Context, commentID, userID string) bool {
return s.commentRepo.IsLiked(commentID, userID)
}
// GetCommentsByCursor 游标分页获取帖子评论
func (s *CommentService) GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
func (s *commentService) GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
return s.commentRepo.GetCommentsByCursor(ctx, postID, req)
}
// GetRepliesByCursor 游标分页获取根评论的回复
func (s *CommentService) GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
func (s *commentService) GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
return s.commentRepo.GetRepliesByCursor(ctx, rootID, req)
}

View File

@@ -15,12 +15,12 @@ import (
)
type SyncEmptyClassroomsRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Semester string `json:"semester"`
WeekStart string `json:"week_start"`
WeekEnd string `json:"week_end"`
CampusCode string `json:"campus_code"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Semester string `json:"semester"`
WeekStart string `json:"week_start"`
WeekEnd string `json:"week_end"`
CampusCode string `json:"campus_code"`
}
type SyncEmptyClassroomsResult struct {
@@ -32,10 +32,11 @@ type SyncEmptyClassroomsResult struct {
type EmptyClassroomSyncService interface {
SyncUserEmptyClassrooms(ctx context.Context, userID string, req *SyncEmptyClassroomsRequest) (*SyncEmptyClassroomsResult, error)
ListClassrooms(userID, semester string) ([]*model.EmptyClassroom, error)
}
type emptyClassroomSyncService struct {
taskManager *runner.TaskManager
taskManager *runner.TaskManager
classroomRepo repository.EmptyClassroomRepository
logger *zap.Logger
}
@@ -46,20 +47,20 @@ func NewEmptyClassroomSyncService(
logger *zap.Logger,
) EmptyClassroomSyncService {
return &emptyClassroomSyncService{
taskManager: taskManager,
classroomRepo: classroomRepo,
logger: logger,
taskManager: taskManager,
classroomRepo: classroomRepo,
logger: logger,
}
}
func (s *emptyClassroomSyncService) SyncUserEmptyClassrooms(ctx context.Context, userID string, req *SyncEmptyClassroomsRequest) (*SyncEmptyClassroomsResult, error) {
payload := &pb.GetEmptyClassroomPayload{
Username: req.Username,
Password: req.Password,
Semester: req.Semester,
WeekStart: req.WeekStart,
WeekEnd: req.WeekEnd,
CampusCode: req.CampusCode,
Username: req.Username,
Password: req.Password,
Semester: req.Semester,
WeekStart: req.WeekStart,
WeekEnd: req.WeekEnd,
CampusCode: req.CampusCode,
}
taskResult, err := s.taskManager.SubmitTaskSync(ctx, pb.TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM, payload)
@@ -133,3 +134,8 @@ func (s *emptyClassroomSyncService) saveClassrooms(ctx context.Context, userID,
}
return nil
}
// ListClassrooms 查询用户某学期的空教室。
func (s *emptyClassroomSyncService) ListClassrooms(userID, semester string) ([]*model.EmptyClassroom, error) {
return s.classroomRepo.ListByUserAndSemester(userID, semester)
}

View File

@@ -29,12 +29,13 @@ type SyncExamsResult struct {
type ExamSyncService interface {
SyncUserExams(ctx context.Context, userID string, req *SyncExamsRequest) (*SyncExamsResult, error)
ListExams(userID, semester string) ([]*model.Exam, error)
}
type examSyncService struct {
taskManager *runner.TaskManager
examRepo repository.ExamRepository
logger *zap.Logger
examRepo repository.ExamRepository
logger *zap.Logger
}
func NewExamSyncService(
@@ -128,4 +129,9 @@ func (s *examSyncService) saveExams(ctx context.Context, userID, semester string
return fmt.Errorf("failed to create exams: %w", err)
}
return nil
}
}
// ListExams 查询用户某学期的考试安排。
func (s *examSyncService) ListExams(userID, semester string) ([]*model.Exam, error) {
return s.examRepo.ListByUserAndSemester(userID, semester)
}

View File

@@ -27,8 +27,15 @@ type SyncGradesResult struct {
ErrorMessage string `json:"error_message,omitempty"`
}
// GradeSummary 成绩查询结果(成绩列表 + GPA 概要)
type GradeSummary struct {
Grades []*model.Grade
GpaSummary *model.GpaSummary
}
type GradeSyncService interface {
SyncUserGrades(ctx context.Context, userID string, req *SyncGradesRequest) (*SyncGradesResult, error)
ListGradesWithSummary(userID string) (*GradeSummary, error)
}
type gradeSyncService struct {
@@ -158,4 +165,15 @@ func (s *gradeSyncService) saveGrades(ctx context.Context, userID string, grades
return fmt.Errorf("failed to create grades: %w", err)
}
return nil
}
}
// ListGradesWithSummary 查询用户全部成绩及最新 GPA 概要。
// GPA 概要查询失败不影响成绩返回(保持与历史 handler 行为一致)。
func (s *gradeSyncService) ListGradesWithSummary(userID string) (*GradeSummary, error) {
grades, err := s.gradeRepo.ListByUser(userID)
if err != nil {
return nil, err
}
gpa, _ := s.gradeRepo.GetLatestGpaSummary(userID)
return &GradeSummary{Grades: grades, GpaSummary: gpa}, nil
}

View File

@@ -47,7 +47,6 @@ var (
ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled
ErrNotRequestTarget = apperrors.ErrNotRequestTarget
ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee
ErrNotMutualFollow = apperrors.ErrNotMutualFollow
ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted
)

View File

@@ -5,34 +5,42 @@ import (
"with_you/internal/pkg/jwt"
)
// JWTService JWT服务
type JWTService struct {
// JWTService JWT服务接口
type JWTService interface {
GenerateAccessToken(userID, username string) (string, error)
GenerateRefreshToken(userID, username string) (string, error)
ParseToken(tokenString string) (*jwt.Claims, error)
ValidateToken(tokenString string) error
}
// jwtServiceImpl JWT服务实现
type jwtServiceImpl struct {
jwt *jwt.JWT
}
// NewJWTService 创建JWT服务
func NewJWTService(secret string, accessExpire, refreshExpire int64) *JWTService {
return &JWTService{
func NewJWTService(secret string, accessExpire, refreshExpire int64) JWTService {
return &jwtServiceImpl{
jwt: jwt.New(secret, time.Duration(accessExpire)*time.Second, time.Duration(refreshExpire)*time.Second),
}
}
// GenerateAccessToken 生成访问令牌
func (s *JWTService) GenerateAccessToken(userID, username string) (string, error) {
func (s *jwtServiceImpl) GenerateAccessToken(userID, username string) (string, error) {
return s.jwt.GenerateAccessToken(userID, username)
}
// GenerateRefreshToken 生成刷新令牌
func (s *JWTService) GenerateRefreshToken(userID, username string) (string, error) {
func (s *jwtServiceImpl) GenerateRefreshToken(userID, username string) (string, error) {
return s.jwt.GenerateRefreshToken(userID, username)
}
// ParseToken 解析令牌
func (s *JWTService) ParseToken(tokenString string) (*jwt.Claims, error) {
func (s *jwtServiceImpl) ParseToken(tokenString string) (*jwt.Claims, error) {
return s.jwt.ParseToken(tokenString)
}
// ValidateToken 验证令牌
func (s *JWTService) ValidateToken(tokenString string) error {
func (s *jwtServiceImpl) ValidateToken(tokenString string) error {
return s.jwt.ValidateToken(tokenString)
}

View File

@@ -5,7 +5,6 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"time"
@@ -126,73 +125,3 @@ func (s *liveKitService) verifyWebhookSignature(body []byte, authHeader string)
return nil
}
// WebhookRoomFinished 处理 room_finished 事件的负载
type WebhookRoomFinished struct {
RoomName string `json:"room_name"`
Duration int64 `json:"duration"`
StartedAt int64 `json:"started_at"`
EndedAt int64 `json:"ended_at"`
}
// ParseRoomFinished 从 webhook event 中解析 room_finished 数据
func ParseRoomFinished(event *livekit.WebhookEvent) (*WebhookRoomFinished, error) {
if event.Event != "room_finished" {
return nil, fmt.Errorf("not a room_finished event: %s", event.Event)
}
room := event.Room
if room == nil {
return nil, fmt.Errorf("room is nil in webhook event")
}
duration := int64(0)
startedAt := int64(0)
endedAt := int64(0)
if room.CreationTime > 0 {
startedAt = room.CreationTime
}
endedAt = time.Now().Unix()
if startedAt > 0 {
duration = endedAt - startedAt
}
return &WebhookRoomFinished{
RoomName: room.Name,
Duration: duration,
StartedAt: startedAt,
EndedAt: endedAt,
}, nil
}
// WebhookEventJSON 用于 JSON 序列化的 webhook 事件
type WebhookEventJSON struct {
Event string `json:"event"`
Room *WebhookRoomJSON `json:"room,omitempty"`
RoomName string `json:"room_name,omitempty"`
}
// WebhookRoomJSON webhook 房间信息 JSON
type WebhookRoomJSON struct {
Sid string `json:"sid,omitempty"`
Name string `json:"name,omitempty"`
CreationTime int64 `json:"creation_time,omitempty"`
NumParticipants uint32 `json:"num_participants,omitempty"`
}
// WebhookEventToJSON 将 webhook event 转为自定义 JSON 结构(避免 protobuf 内部字段)
func WebhookEventToJSON(event *livekit.WebhookEvent) (*WebhookEventJSON, error) {
data, err := protojson.Marshal(event)
if err != nil {
return nil, err
}
var result WebhookEventJSON
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -196,4 +196,3 @@ func NewLogService(
DataChangeLog: dataChangeLogService,
}
}

View File

@@ -4,15 +4,15 @@ import (
"context"
"time"
"with_you/internal/dto"
"with_you/internal/model"
"with_you/internal/query"
"with_you/internal/repository"
)
// OperationLogService 操作日志服务接口
type OperationLogService interface {
RecordOperation(log *model.OperationLog)
GetOperationLogs(ctx context.Context, filters dto.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error)
GetOperationLogs(ctx context.Context, filters query.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error)
GetOperationLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.OperationLog, int64, error)
GetOperationLogsByTimeRange(ctx context.Context, startTime, endTime string, page, pageSize int) ([]*model.OperationLog, int64, error)
GetStatistics(ctx context.Context, startTime, endTime string) (*repository.OperationLogStatistics, error)
@@ -41,7 +41,7 @@ func (s *operationLogServiceImpl) RecordOperation(log *model.OperationLog) {
}
// GetOperationLogs 获取操作日志列表
func (s *operationLogServiceImpl) GetOperationLogs(ctx context.Context, filters dto.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error) {
func (s *operationLogServiceImpl) GetOperationLogs(ctx context.Context, filters query.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error) {
return s.repo.GetOperationLogs(ctx, filters, page, pageSize)
}
@@ -63,7 +63,7 @@ func (s *operationLogServiceImpl) GetStatistics(ctx context.Context, startTime,
// LoginLogService 登录日志服务接口
type LoginLogService interface {
RecordLogin(log *model.LoginLog)
GetLoginLogs(ctx context.Context, filters dto.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error)
GetLoginLogs(ctx context.Context, filters query.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error)
GetLoginLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.LoginLog, int64, error)
GetFailedLoginsByIP(ctx context.Context, ip string, timeWindow string) (int64, error)
GetRecentSuccessLogins(ctx context.Context, userID string, limit int) ([]*model.LoginLog, error)
@@ -93,7 +93,7 @@ func (s *loginLogServiceImpl) RecordLogin(log *model.LoginLog) {
}
// GetLoginLogs 获取登录日志列表
func (s *loginLogServiceImpl) GetLoginLogs(ctx context.Context, filters dto.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error) {
func (s *loginLogServiceImpl) GetLoginLogs(ctx context.Context, filters query.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error) {
return s.repo.GetLoginLogs(ctx, filters, page, pageSize)
}
@@ -124,7 +124,7 @@ func (s *loginLogServiceImpl) GetStatistics(ctx context.Context, startTime, endT
// DataChangeLogService 数据变更日志服务接口
type DataChangeLogService interface {
RecordDataChange(log *model.DataChangeLog)
GetDataChangeLogs(ctx context.Context, filters dto.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error)
GetDataChangeLogs(ctx context.Context, filters query.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error)
GetDataChangeLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.DataChangeLog, int64, error)
GetDataChangeLogsByOperator(ctx context.Context, operatorID string, page, pageSize int) ([]*model.DataChangeLog, int64, error)
GetStatistics(ctx context.Context, startTime, endTime string) (*repository.DataChangeLogStatistics, error)
@@ -153,7 +153,7 @@ func (s *dataChangeLogServiceImpl) RecordDataChange(log *model.DataChangeLog) {
}
// GetDataChangeLogs 获取数据变更日志列表
func (s *dataChangeLogServiceImpl) GetDataChangeLogs(ctx context.Context, filters dto.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error) {
func (s *dataChangeLogServiceImpl) GetDataChangeLogs(ctx context.Context, filters query.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error) {
return s.repo.GetDataChangeLogs(ctx, filters, page, pageSize)
}

View File

@@ -3,8 +3,8 @@ package service
import (
"errors"
"with_you/internal/dto"
"with_you/internal/model"
"with_you/internal/query"
"with_you/internal/repository"
)
@@ -23,7 +23,7 @@ type MaterialService interface {
DeleteSubject(id string) error
// 资料相关
ListMaterials(params dto.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error)
ListMaterials(params query.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error)
GetMaterialByID(id string) (*model.MaterialFile, error)
GetMaterialByIDWithSubject(id string) (*model.MaterialFile, error)
GetMaterialsBySubject(subjectID string, page, pageSize int) ([]*model.MaterialFile, int64, error)
@@ -129,7 +129,7 @@ func (s *materialServiceImpl) DeleteSubject(id string) error {
// 资料相关方法
// =====================================================
func (s *materialServiceImpl) ListMaterials(params dto.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) {
func (s *materialServiceImpl) ListMaterials(params query.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) {
return s.fileRepo.List(params)
}

View File

@@ -10,7 +10,7 @@ import (
)
// ValidateChatMessageImageSegments 校验聊天消息中的图片段:禁止 data:/base64 内联,仅允许引用本站 S3 公开前缀下的 URL。
func (s *UploadService) ValidateChatMessageImageSegments(segments model.MessageSegments) error {
func (s *uploadService) ValidateChatMessageImageSegments(segments model.MessageSegments) error {
if s == nil || s.s3Client == nil {
return nil
}

View File

@@ -22,8 +22,26 @@ const (
CacheJitterRatio = 0.1
)
// MessageService 消息服务
type MessageService struct {
// MessageService 消息服务接口
type MessageService interface {
SendMessage(ctx context.Context, senderID, receiverID string, segments model.MessageSegments) (*model.Message, error)
GetConversations(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error)
GetMessages(ctx context.Context, conversationID string, page, pageSize int) ([]*model.Message, int64, error)
GetMessagesAfterSeq(ctx context.Context, conversationID string, afterSeq int64, limit int) ([]*model.Message, error)
MarkAsRead(ctx context.Context, conversationID string, userID string, lastReadSeq int64) error
GetUnreadCount(ctx context.Context, conversationID string, userID string) (int64, error)
GetOrCreateConversation(ctx context.Context, user1ID, user2ID string) (*model.Conversation, error)
GetConversationParticipants(conversationID string) ([]*model.ConversationParticipant, error)
GetParticipantsBatch(ctx context.Context, convIDs []string) (map[string][]*model.ConversationParticipant, error)
GetMyParticipantsBatch(ctx context.Context, convIDs []string, userID string) (map[string]*model.ConversationParticipant, error)
InvalidateUserConversationCache(userID string)
InvalidateUserUnreadCache(userID, conversationID string)
GetMessagesByCursor(ctx context.Context, conversationID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Message], error)
GetConversationsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Conversation], error)
}
// messageService 消息服务实现
type messageService struct {
// 基础仓储
messageRepo repository.MessageRepository
@@ -33,11 +51,11 @@ type MessageService struct {
// 基础缓存(用于简单缓存操作)
baseCache cache.Cache
uploadService *UploadService
uploadService UploadService
}
// NewMessageService 创建消息服务
func NewMessageService(messageRepo repository.MessageRepository, cacheBackend cache.Cache, uploadService *UploadService) *MessageService {
func NewMessageService(messageRepo repository.MessageRepository, cacheBackend cache.Cache, uploadService UploadService) MessageService {
// 创建适配器
convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo)
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
@@ -51,7 +69,7 @@ func NewMessageService(messageRepo repository.MessageRepository, cacheBackend ca
nil,
)
return &MessageService{
return &messageService{
messageRepo: messageRepo,
conversationCache: conversationCache,
baseCache: cacheBackend,
@@ -67,7 +85,7 @@ type ConversationListResult struct {
// SendMessage 发送消息(使用 segments
// senderID 和 receiverID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID string, segments model.MessageSegments) (*model.Message, error) {
func (s *messageService) SendMessage(ctx context.Context, senderID, receiverID string, segments model.MessageSegments) (*model.Message, error) {
// 获取或创建会话
conv, err := s.messageRepo.GetOrCreatePrivateConversation(senderID, receiverID)
if err != nil {
@@ -127,7 +145,7 @@ func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID s
}
// cacheMessage 缓存消息(内部方法)
func (s *MessageService) cacheMessage(ctx context.Context, convID string, msg *model.Message) error {
func (s *messageService) cacheMessage(ctx context.Context, convID string, msg *model.Message) error {
if s.conversationCache == nil {
return nil
}
@@ -140,7 +158,7 @@ func (s *MessageService) cacheMessage(ctx context.Context, convID string, msg *m
// GetConversations 获取会话列表(带缓存)
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) GetConversations(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error) {
func (s *messageService) GetConversations(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error) {
// 优先使用 ConversationCache
if s.conversationCache != nil {
return s.conversationCache.GetConversationList(ctx, userID, page, pageSize)
@@ -190,7 +208,7 @@ func (s *MessageService) GetConversations(ctx context.Context, userID string, pa
}
// GetMessages 获取消息列表(带缓存)
func (s *MessageService) GetMessages(ctx context.Context, conversationID string, page, pageSize int) ([]*model.Message, int64, error) {
func (s *messageService) GetMessages(ctx context.Context, conversationID string, page, pageSize int) ([]*model.Message, int64, error) {
// 优先使用 ConversationCache
if s.conversationCache != nil {
return s.conversationCache.GetMessages(ctx, conversationID, page, pageSize)
@@ -201,13 +219,13 @@ func (s *MessageService) GetMessages(ctx context.Context, conversationID string,
}
// GetMessagesAfterSeq 获取指定seq之后的消息增量同步
func (s *MessageService) GetMessagesAfterSeq(ctx context.Context, conversationID string, afterSeq int64, limit int) ([]*model.Message, error) {
func (s *messageService) GetMessagesAfterSeq(ctx context.Context, conversationID string, afterSeq int64, limit int) ([]*model.Message, error) {
return s.messageRepo.GetMessagesAfterSeq(conversationID, afterSeq, limit)
}
// MarkAsRead 标记为已读(使用 Cache-Aside 模式)
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) MarkAsRead(ctx context.Context, conversationID string, userID string, lastReadSeq int64) error {
func (s *messageService) MarkAsRead(ctx context.Context, conversationID string, userID string, lastReadSeq int64) error {
// 1. 先写入DB保证数据一致性DB是唯一数据源
err := s.messageRepo.UpdateLastReadSeq(conversationID, userID, lastReadSeq)
if err != nil {
@@ -230,7 +248,7 @@ func (s *MessageService) MarkAsRead(ctx context.Context, conversationID string,
// GetUnreadCount 获取未读消息数(带缓存)
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) GetUnreadCount(ctx context.Context, conversationID string, userID string) (int64, error) {
func (s *messageService) GetUnreadCount(ctx context.Context, conversationID string, userID string) (int64, error) {
// 优先使用 ConversationCache
if s.conversationCache != nil {
return s.conversationCache.GetUnreadCount(ctx, userID, conversationID)
@@ -268,7 +286,7 @@ func (s *MessageService) GetUnreadCount(ctx context.Context, conversationID stri
// GetOrCreateConversation 获取或创建私聊会话
// user1ID 和 user2ID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) GetOrCreateConversation(ctx context.Context, user1ID, user2ID string) (*model.Conversation, error) {
func (s *messageService) GetOrCreateConversation(ctx context.Context, user1ID, user2ID string) (*model.Conversation, error) {
conv, err := s.messageRepo.GetOrCreatePrivateConversation(user1ID, user2ID)
if err != nil {
return nil, err
@@ -282,7 +300,7 @@ func (s *MessageService) GetOrCreateConversation(ctx context.Context, user1ID, u
}
// GetConversationParticipants 获取会话参与者列表
func (s *MessageService) GetConversationParticipants(conversationID string) ([]*model.ConversationParticipant, error) {
func (s *messageService) GetConversationParticipants(conversationID string) ([]*model.ConversationParticipant, error) {
// 优先使用缓存
if s.conversationCache != nil {
return s.conversationCache.GetParticipants(context.Background(), conversationID)
@@ -291,12 +309,12 @@ func (s *MessageService) GetConversationParticipants(conversationID string) ([]*
}
// GetParticipantsBatch 批量获取多个会话的参与者列表
func (s *MessageService) GetParticipantsBatch(ctx context.Context, convIDs []string) (map[string][]*model.ConversationParticipant, error) {
func (s *messageService) GetParticipantsBatch(ctx context.Context, convIDs []string) (map[string][]*model.ConversationParticipant, error) {
return s.messageRepo.GetParticipantsBatch(ctx, convIDs)
}
// GetMyParticipantsBatch 批量获取用户在多个会话中的参与者信息
func (s *MessageService) GetMyParticipantsBatch(ctx context.Context, convIDs []string, userID string) (map[string]*model.ConversationParticipant, error) {
func (s *messageService) GetMyParticipantsBatch(ctx context.Context, convIDs []string, userID string) (map[string]*model.ConversationParticipant, error) {
return s.messageRepo.GetMyParticipantsBatch(ctx, convIDs, userID)
}
@@ -306,23 +324,23 @@ func ParseConversationID(idStr string) (string, error) {
}
// InvalidateUserConversationCache 失效用户会话相关缓存(供外部调用)
func (s *MessageService) InvalidateUserConversationCache(userID string) {
func (s *messageService) InvalidateUserConversationCache(userID string) {
s.conversationCache.InvalidateConversationList(userID)
cache.InvalidateUnreadConversation(s.baseCache, userID)
}
// InvalidateUserUnreadCache 失效用户未读数缓存(供外部调用)
func (s *MessageService) InvalidateUserUnreadCache(userID, conversationID string) {
func (s *messageService) InvalidateUserUnreadCache(userID, conversationID string) {
cache.InvalidateUnreadConversation(s.baseCache, userID)
s.conversationCache.InvalidateUnreadCount(userID, conversationID)
}
// GetMessagesByCursor 游标分页获取会话消息
func (s *MessageService) GetMessagesByCursor(ctx context.Context, conversationID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Message], error) {
func (s *messageService) GetMessagesByCursor(ctx context.Context, conversationID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Message], error) {
return s.messageRepo.GetMessagesByCursor(ctx, conversationID, req)
}
// GetConversationsByCursor 游标分页获取用户会话列表
func (s *MessageService) GetConversationsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Conversation], error) {
func (s *messageService) GetConversationsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Conversation], error) {
return s.messageRepo.GetConversationsByCursor(ctx, userID, req)
}

View File

@@ -18,22 +18,34 @@ const (
NotificationCacheJitter = 0.1
)
// NotificationService 通知服务
type NotificationService struct {
// NotificationService 通知服务接口
type NotificationService interface {
Create(ctx context.Context, userID string, notificationType model.NotificationType, title, content string) (*model.Notification, error)
GetByUserID(ctx context.Context, userID string, page, pageSize int, unreadOnly bool) ([]*model.Notification, int64, error)
MarkAsReadWithUserID(ctx context.Context, id, userID string) error
MarkAllAsRead(ctx context.Context, userID string) error
GetUnreadCount(ctx context.Context, userID string) (int64, error)
DeleteNotification(ctx context.Context, id, userID string) error
ClearAllNotifications(ctx context.Context, userID string) error
GetNotificationsByCursor(ctx context.Context, userID string, unreadOnly bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Notification], error)
}
// notificationService 通知服务实现
type notificationService struct {
notificationRepo repository.NotificationRepository
cache cache.Cache
}
// NewNotificationService 创建通知服务
func NewNotificationService(notificationRepo repository.NotificationRepository, cacheBackend cache.Cache) *NotificationService {
return &NotificationService{
func NewNotificationService(notificationRepo repository.NotificationRepository, cacheBackend cache.Cache) NotificationService {
return &notificationService{
notificationRepo: notificationRepo,
cache: cacheBackend,
}
}
// Create 创建通知
func (s *NotificationService) Create(ctx context.Context, userID string, notificationType model.NotificationType, title, content string) (*model.Notification, error) {
func (s *notificationService) Create(ctx context.Context, userID string, notificationType model.NotificationType, title, content string) (*model.Notification, error) {
notification := &model.Notification{
UserID: userID,
Type: notificationType,
@@ -54,12 +66,12 @@ func (s *NotificationService) Create(ctx context.Context, userID string, notific
}
// GetByUserID 获取用户通知
func (s *NotificationService) GetByUserID(ctx context.Context, userID string, page, pageSize int, unreadOnly bool) ([]*model.Notification, int64, error) {
func (s *notificationService) GetByUserID(ctx context.Context, userID string, page, pageSize int, unreadOnly bool) ([]*model.Notification, int64, error) {
return s.notificationRepo.GetByUserID(userID, page, pageSize, unreadOnly)
}
// MarkAsReadWithUserID 标记为已读带用户ID验证所有权
func (s *NotificationService) MarkAsReadWithUserID(ctx context.Context, id, userID string) error {
func (s *notificationService) MarkAsReadWithUserID(ctx context.Context, id, userID string) error {
notification, err := s.notificationRepo.GetByID(id)
if err != nil {
return err
@@ -79,7 +91,7 @@ func (s *NotificationService) MarkAsReadWithUserID(ctx context.Context, id, user
}
// MarkAllAsRead 标记所有为已读
func (s *NotificationService) MarkAllAsRead(ctx context.Context, userID string) error {
func (s *notificationService) MarkAllAsRead(ctx context.Context, userID string) error {
err := s.notificationRepo.MarkAllAsRead(userID)
if err != nil {
return err
@@ -92,7 +104,7 @@ func (s *NotificationService) MarkAllAsRead(ctx context.Context, userID string)
}
// GetUnreadCount 获取未读数量(带缓存)
func (s *NotificationService) GetUnreadCount(ctx context.Context, userID string) (int64, error) {
func (s *notificationService) GetUnreadCount(ctx context.Context, userID string) (int64, error) {
cacheSettings := cache.GetSettings()
unreadTTL := cacheSettings.UnreadCountTTL
if unreadTTL <= 0 {
@@ -122,7 +134,7 @@ func (s *NotificationService) GetUnreadCount(ctx context.Context, userID string)
}
// DeleteNotification 删除通知(带用户验证)
func (s *NotificationService) DeleteNotification(ctx context.Context, id, userID string) error {
func (s *notificationService) DeleteNotification(ctx context.Context, id, userID string) error {
// 先检查通知是否属于该用户
notification, err := s.notificationRepo.GetByID(id)
if err != nil {
@@ -144,7 +156,7 @@ func (s *NotificationService) DeleteNotification(ctx context.Context, id, userID
}
// ClearAllNotifications 清空所有通知
func (s *NotificationService) ClearAllNotifications(ctx context.Context, userID string) error {
func (s *notificationService) ClearAllNotifications(ctx context.Context, userID string) error {
err := s.notificationRepo.DeleteAllByUserID(userID)
if err != nil {
return err
@@ -157,7 +169,7 @@ func (s *NotificationService) ClearAllNotifications(ctx context.Context, userID
}
// GetNotificationsByCursor 游标分页获取用户通知
func (s *NotificationService) GetNotificationsByCursor(ctx context.Context, userID string, unreadOnly bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Notification], error) {
func (s *notificationService) GetNotificationsByCursor(ctx context.Context, userID string, unreadOnly bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Notification], error) {
return s.notificationRepo.GetNotificationsByCursor(ctx, userID, unreadOnly, req)
}

View File

@@ -171,21 +171,31 @@ func (e *BioModerationReviewError) IsReview() bool {
return true
}
type PostAIService struct {
// PostAIService 帖子 AI 审核服务接口
type PostAIService interface {
IsEnabled() bool
ModeratePost(ctx context.Context, title, content string, images []string) error
ModerateComment(ctx context.Context, content string, images []string) error
ModerateImage(ctx context.Context, imageURL string) error
ModerateBio(ctx context.Context, bio string) error
}
// postAIService 帖子 AI 审核服务实现
type postAIService struct {
openAIClient openai.Client
tencentClient tencent.Client
strictMode bool
}
func NewPostAIService(openAIClient openai.Client, tencentClient tencent.Client, strictMode bool) *PostAIService {
return &PostAIService{
func NewPostAIService(openAIClient openai.Client, tencentClient tencent.Client, strictMode bool) PostAIService {
return &postAIService{
openAIClient: openAIClient,
tencentClient: tencentClient,
strictMode: strictMode,
}
}
func (s *PostAIService) IsEnabled() bool {
func (s *postAIService) IsEnabled() bool {
if s == nil {
return false
}
@@ -193,15 +203,15 @@ func (s *PostAIService) IsEnabled() bool {
(s.tencentClient != nil && s.tencentClient.IsEnabled())
}
func (s *PostAIService) isOpenAIEnabled() bool {
func (s *postAIService) isOpenAIEnabled() bool {
return s != nil && s.openAIClient != nil && s.openAIClient.IsEnabled()
}
func (s *PostAIService) isTencentEnabled() bool {
func (s *postAIService) isTencentEnabled() bool {
return s != nil && s.tencentClient != nil && s.tencentClient.IsEnabled()
}
func (s *PostAIService) ModeratePost(ctx context.Context, title, content string, images []string) error {
func (s *postAIService) ModeratePost(ctx context.Context, title, content string, images []string) error {
if !s.IsEnabled() {
return nil
}
@@ -234,7 +244,7 @@ func (s *PostAIService) ModeratePost(ctx context.Context, title, content string,
)
}
func (s *PostAIService) ModerateComment(ctx context.Context, content string, images []string) error {
func (s *postAIService) ModerateComment(ctx context.Context, content string, images []string) error {
if !s.IsEnabled() {
return nil
}
@@ -267,7 +277,7 @@ func (s *PostAIService) ModerateComment(ctx context.Context, content string, ima
)
}
func (s *PostAIService) ModerateImage(ctx context.Context, imageURL string) error {
func (s *postAIService) ModerateImage(ctx context.Context, imageURL string) error {
if !s.isOpenAIEnabled() {
return nil
}
@@ -293,7 +303,7 @@ func (s *PostAIService) ModerateImage(ctx context.Context, imageURL string) erro
}
}
func (s *PostAIService) ModerateBio(ctx context.Context, bio string) error {
func (s *postAIService) ModerateBio(ctx context.Context, bio string) error {
if !s.IsEnabled() {
return nil
}
@@ -326,7 +336,7 @@ func (s *PostAIService) ModerateBio(ctx context.Context, bio string) error {
)
}
func (s *PostAIService) moderateTextWithTencent(
func (s *postAIService) moderateTextWithTencent(
ctx context.Context,
text string,
rejectFactory func(string) error,

View File

@@ -8,9 +8,9 @@ import (
"strings"
"time"
apperrors "with_you/internal/errors"
"with_you/internal/cache"
"with_you/internal/dto"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/pkg/hook"
@@ -75,13 +75,13 @@ type postServiceImpl struct {
postRepo repository.PostRepository
systemMessageService SystemMessageService
cache cache.Cache
postAIService *PostAIService
postAIService PostAIService
txManager repository.TransactionManager
logService *LogService
hookManager *hook.Manager
}
func NewPostService(postRepo repository.PostRepository, systemMessageService SystemMessageService, postAIService *PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager, hookManager *hook.Manager, logService *LogService) PostService {
func NewPostService(postRepo repository.PostRepository, systemMessageService SystemMessageService, postAIService PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager, hookManager *hook.Manager, logService *LogService) PostService {
return &postServiceImpl{
postRepo: postRepo,
systemMessageService: systemMessageService,

View File

@@ -38,14 +38,14 @@ type PushStreamMsg struct {
// PushWorker 从 Redis Stream 消费消息并异步推送
type PushWorker struct {
rdb *redis.Client
publisher ws.MessagePublisher
pushSvc PushService
msgRepo repository.MessageRepository
userRepo repository.UserRepository
config PushWorkerConfig
stopCh chan struct{}
doneCh chan struct{}
rdb *redis.Client
publisher ws.MessagePublisher
pushSvc PushService
msgRepo repository.MessageRepository
userRepo repository.UserRepository
config PushWorkerConfig
stopCh chan struct{}
doneCh chan struct{}
}
// NewPushWorker 创建推送 Worker
@@ -79,14 +79,14 @@ func NewPushWorker(
config.IdleTimeoutMs = 30000
}
return &PushWorker{
rdb: rdb,
publisher: publisher,
pushSvc: pushSvc,
msgRepo: msgRepo,
userRepo: userRepo,
config: config,
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
rdb: rdb,
publisher: publisher,
pushSvc: pushSvc,
msgRepo: msgRepo,
userRepo: userRepo,
config: config,
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
}
@@ -191,12 +191,12 @@ func (w *PushWorker) claimPendingLoop() {
// 查询 pending 消息
pendingResult, err := w.rdb.XPendingExt(context.Background(), &redis.XPendingExtArgs{
Stream: w.config.Stream,
Group: w.config.Group,
Start: "-",
End: "+",
Count: int64(w.config.BatchSize),
Idle: time.Duration(w.config.IdleTimeoutMs) * time.Millisecond,
Stream: w.config.Stream,
Group: w.config.Group,
Start: "-",
End: "+",
Count: int64(w.config.BatchSize),
Idle: time.Duration(w.config.IdleTimeoutMs) * time.Millisecond,
}).Result()
if err != nil {
if err == redis.Nil {
@@ -213,7 +213,7 @@ func (w *PushWorker) claimPendingLoop() {
Group: w.config.Group,
Consumer: consumerName,
MinIdle: time.Duration(w.config.IdleTimeoutMs) * time.Millisecond,
Messages: []string{pending.ID},
Messages: []string{pending.ID},
}).Result()
if err != nil || len(claimed) == 0 {
continue
@@ -378,4 +378,4 @@ func PublishToPush(ctx context.Context, rdb *redis.Client, stream string, msg *P
"data": string(data),
},
}).Err()
}
}

View File

@@ -51,19 +51,30 @@ type QRCodeSession struct {
ExpiresAt int64 `json:"expires_at"`
}
// QRCodeLoginService 二维码登录服务
type QRCodeLoginService struct {
// QRCodeLoginService 二维码登录服务接口
type QRCodeLoginService interface {
CreateSession(ctx context.Context) (*QRCodeSession, error)
GetSession(ctx context.Context, sessionID string) (*QRCodeSession, error)
Scan(ctx context.Context, sessionID, userID string) error
Confirm(ctx context.Context, sessionID, userID string, ip, userAgent string) (*LoginResponse, error)
Cancel(ctx context.Context, sessionID, userID string) error
GetWSHub() *ws.Hub
GetUserService() UserService
}
// qrcodeLoginService 二维码登录服务实现
type qrcodeLoginService struct {
redis *redis.Client
wsHub ws.MessagePublisher
jwtService *JWTService
jwtService JWTService
userService UserService
activityService UserActivityService
logService *LogService
}
// NewQRCodeLoginService 创建二维码登录服务
func NewQRCodeLoginService(redis *redis.Client, publisher ws.MessagePublisher, jwtService *JWTService, userService UserService, activityService UserActivityService, logService *LogService) *QRCodeLoginService {
return &QRCodeLoginService{
func NewQRCodeLoginService(redis *redis.Client, publisher ws.MessagePublisher, jwtService JWTService, userService UserService, activityService UserActivityService, logService *LogService) QRCodeLoginService {
return &qrcodeLoginService{
redis: redis,
wsHub: publisher,
jwtService: jwtService,
@@ -74,7 +85,7 @@ func NewQRCodeLoginService(redis *redis.Client, publisher ws.MessagePublisher, j
}
// CreateSession 创建二维码会话
func (s *QRCodeLoginService) CreateSession(ctx context.Context) (*QRCodeSession, error) {
func (s *qrcodeLoginService) CreateSession(ctx context.Context) (*QRCodeSession, error) {
sessionID := uuid.New().String()
now := time.Now()
expiresAt := now.Add(qrcodeSessionTTL)
@@ -106,7 +117,7 @@ func (s *QRCodeLoginService) CreateSession(ctx context.Context) (*QRCodeSession,
}
// GetSession 获取会话
func (s *QRCodeLoginService) GetSession(ctx context.Context, sessionID string) (*QRCodeSession, error) {
func (s *qrcodeLoginService) GetSession(ctx context.Context, sessionID string) (*QRCodeSession, error) {
key := qrcodeSessionPrefix + sessionID
data, err := s.redis.HGetAll(ctx, key)
if err != nil {
@@ -140,7 +151,7 @@ func (s *QRCodeLoginService) GetSession(ctx context.Context, sessionID string) (
}
// Scan 扫描二维码
func (s *QRCodeLoginService) Scan(ctx context.Context, sessionID, userID string) error {
func (s *qrcodeLoginService) Scan(ctx context.Context, sessionID, userID string) error {
key := qrcodeSessionPrefix + sessionID
// 使用 Lua 脚本实现原子性检查和更新,避免竞态条件
@@ -176,7 +187,7 @@ func (s *QRCodeLoginService) Scan(ctx context.Context, sessionID, userID string)
}
// Confirm 确认登录
func (s *QRCodeLoginService) Confirm(ctx context.Context, sessionID, userID string, ip, userAgent string) (*LoginResponse, error) {
func (s *qrcodeLoginService) Confirm(ctx context.Context, sessionID, userID string, ip, userAgent string) (*LoginResponse, error) {
session, err := s.GetSession(ctx, sessionID)
if err != nil {
return nil, err
@@ -255,7 +266,7 @@ type LoginResponse struct {
}
// Cancel 取消登录
func (s *QRCodeLoginService) Cancel(ctx context.Context, sessionID, userID string) error {
func (s *qrcodeLoginService) Cancel(ctx context.Context, sessionID, userID string) error {
session, err := s.GetSession(ctx, sessionID)
if err != nil {
return err
@@ -281,7 +292,7 @@ func (s *QRCodeLoginService) Cancel(ctx context.Context, sessionID, userID strin
}
// GetWSHub 获取底层 WS Hub用于 Subscribe 等本地操作)
func (s *QRCodeLoginService) GetWSHub() *ws.Hub {
func (s *qrcodeLoginService) GetWSHub() *ws.Hub {
switch p := s.wsHub.(type) {
case *ws.Bus:
return p.Hub()
@@ -293,6 +304,6 @@ func (s *QRCodeLoginService) GetWSHub() *ws.Hub {
}
// GetUserService 获取用户服务
func (s *QRCodeLoginService) GetUserService() UserService {
func (s *qrcodeLoginService) GetUserService() UserService {
return s.userService
}

View File

@@ -498,4 +498,3 @@ func (s *sensitiveServiceImpl) loadFromRedis(ctx context.Context) error {
return nil
}

View File

@@ -6,8 +6,8 @@ import (
"fmt"
"time"
apperrors "with_you/internal/errors"
"with_you/internal/dto"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/repository"

View File

@@ -16,6 +16,7 @@ import (
"path/filepath"
"strings"
"with_you/internal/model"
"with_you/internal/pkg/s3"
"github.com/disintegration/imaging"
@@ -25,8 +26,20 @@ import (
_ "golang.org/x/image/tiff"
)
// UploadService 上传服务
type UploadService struct {
// UploadService 上传服务接口
type UploadService interface {
UploadImage(ctx context.Context, file *multipart.FileHeader) (string, string, string, error)
UploadImageBytes(ctx context.Context, raw []byte, filename string) (url, previewURL, previewURLLarge string, err error)
UploadAvatar(ctx context.Context, userID string, file *multipart.FileHeader) (string, error)
UploadCover(ctx context.Context, userID string, file *multipart.FileHeader) (string, error)
GetURL(ctx context.Context, objectName string) (string, error)
Delete(ctx context.Context, objectName string) error
GeneratePreviewImages(ctx context.Context, originalData []byte, hash string) (previewURL, previewURLLarge string, err error)
ValidateChatMessageImageSegments(segments model.MessageSegments) error
}
// uploadService 上传服务实现
type uploadService struct {
s3Client *s3.Client
userService UserService
}
@@ -39,15 +52,15 @@ const (
)
// NewUploadService 创建上传服务
func NewUploadService(s3Client *s3.Client, userService UserService) *UploadService {
return &UploadService{
func NewUploadService(s3Client *s3.Client, userService UserService) UploadService {
return &uploadService{
s3Client: s3Client,
userService: userService,
}
}
// UploadImage 上传图片返回原图URL和预览图URL
func (s *UploadService) UploadImage(ctx context.Context, file *multipart.FileHeader) (string, string, string, error) {
func (s *uploadService) UploadImage(ctx context.Context, file *multipart.FileHeader) (string, string, string, error) {
processedData, contentType, ext, err := prepareImageForUpload(file)
if err != nil {
return "", "", "", err
@@ -78,7 +91,7 @@ func (s *UploadService) UploadImage(ctx context.Context, file *multipart.FileHea
const maxUploadImageBytesFromBuffer = 15 << 20 // 与聊天内联解码上限一致,略大于 upload.max_file_size 时可接受
// UploadImageBytes 从内存字节上传图片(与 multipart 上传走同一套压缩与存储逻辑)。
func (s *UploadService) UploadImageBytes(ctx context.Context, raw []byte, filename string) (url, previewURL, previewURLLarge string, err error) {
func (s *uploadService) UploadImageBytes(ctx context.Context, raw []byte, filename string) (url, previewURL, previewURLLarge string, err error) {
if len(raw) == 0 {
return "", "", "", fmt.Errorf("empty image data")
}
@@ -136,7 +149,7 @@ func getExtFromContentType(contentType string) string {
}
// UploadAvatar 上传头像
func (s *UploadService) UploadAvatar(ctx context.Context, userID string, file *multipart.FileHeader) (string, error) {
func (s *uploadService) UploadAvatar(ctx context.Context, userID string, file *multipart.FileHeader) (string, error) {
processedData, contentType, ext, err := prepareImageForUpload(file)
if err != nil {
return "", err
@@ -170,7 +183,7 @@ func (s *UploadService) UploadAvatar(ctx context.Context, userID string, file *m
}
// UploadCover 上传头图(个人主页封面)
func (s *UploadService) UploadCover(ctx context.Context, userID string, file *multipart.FileHeader) (string, error) {
func (s *uploadService) UploadCover(ctx context.Context, userID string, file *multipart.FileHeader) (string, error) {
processedData, contentType, ext, err := prepareImageForUpload(file)
if err != nil {
return "", err
@@ -204,12 +217,12 @@ func (s *UploadService) UploadCover(ctx context.Context, userID string, file *mu
}
// GetURL 获取文件URL
func (s *UploadService) GetURL(ctx context.Context, objectName string) (string, error) {
func (s *uploadService) GetURL(ctx context.Context, objectName string) (string, error) {
return s.s3Client.GetURL(ctx, objectName)
}
// Delete 删除文件
func (s *UploadService) Delete(ctx context.Context, objectName string) error {
func (s *uploadService) Delete(ctx context.Context, objectName string) error {
return s.s3Client.Delete(ctx, objectName)
}
@@ -313,7 +326,7 @@ func normalizeImageContentType(contentType string) string {
}
// GeneratePreviewImages 生成预览图
func (s *UploadService) GeneratePreviewImages(ctx context.Context, originalData []byte, hash string) (previewURL, previewURLLarge string, err error) {
func (s *uploadService) GeneratePreviewImages(ctx context.Context, originalData []byte, hash string) (previewURL, previewURLLarge string, err error) {
img, _, err := image.Decode(bytes.NewReader(originalData))
if err != nil {
return "", "", fmt.Errorf("failed to decode image: %w", err)
@@ -343,7 +356,7 @@ func (s *UploadService) GeneratePreviewImages(ctx context.Context, originalData
}
// generatePreview 生成单个预览图
func (s *UploadService) generatePreview(ctx context.Context, img image.Image, originalWidth, originalHeight int, mode string, maxDim int, hash string) (string, error) {
func (s *uploadService) generatePreview(ctx context.Context, img image.Image, originalWidth, originalHeight int, mode string, maxDim int, hash string) (string, error) {
var targetWidth, targetHeight int
// 计算目标尺寸

View File

@@ -8,20 +8,30 @@ import (
"strings"
"time"
apperrors "with_you/internal/errors"
"with_you/internal/cache"
"with_you/internal/dto"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/pkg/hook"
"with_you/internal/repository"
)
// VoteService 投票服务
type VoteService struct {
// VoteService 投票服务接口
type VoteService interface {
CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error)
GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error)
GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error)
Vote(ctx context.Context, postID, userID, optionID string) error
Unvote(ctx context.Context, postID, userID string) error
UpdateVoteOption(ctx context.Context, postID, optionID, userID, content string) error
}
// voteService 投票服务实现
type voteService struct {
voteRepo repository.VoteRepository
postRepo repository.PostRepository
cache cache.Cache
postAIService *PostAIService
postAIService PostAIService
systemMessageService SystemMessageService
hookManager *hook.Manager
logService *LogService
@@ -31,12 +41,12 @@ func NewVoteService(
voteRepo repository.VoteRepository,
postRepo repository.PostRepository,
cache cache.Cache,
postAIService *PostAIService,
postAIService PostAIService,
systemMessageService SystemMessageService,
hookManager *hook.Manager,
logService *LogService,
) *VoteService {
return &VoteService{
) VoteService {
return &voteService{
voteRepo: voteRepo,
postRepo: postRepo,
cache: cache,
@@ -48,7 +58,7 @@ func NewVoteService(
}
// CreateVotePost 创建投票帖子
func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error) {
func (s *voteService) CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error) {
if len(req.VoteOptions) < 2 {
return nil, errors.New("投票选项至少需要2个")
}
@@ -108,7 +118,7 @@ func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dt
return s.convertToPostResponse(createdPost, userID), nil
}
func (s *VoteService) reviewVotePostAsync(postID, userID, title, content string, images []string) {
func (s *voteService) reviewVotePostAsync(postID, userID, title, content string, images []string) {
defer func() {
if r := recover(); r != nil {
log.Printf("[ERROR] Panic in vote post moderation async flow, fallback publish post=%s panic=%v", postID, r)
@@ -183,7 +193,7 @@ func (s *VoteService) reviewVotePostAsync(postID, userID, title, content string,
cache.InvalidatePostList(s.cache)
}
func (s *VoteService) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
func (s *voteService) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
const maxAttempts = 3
const retryDelay = 200 * time.Millisecond
@@ -203,7 +213,7 @@ func (s *VoteService) updateModerationStatusWithRetry(postID string, status mode
return lastErr
}
func (s *VoteService) notifyModerationRejected(userID, reason string) {
func (s *voteService) notifyModerationRejected(userID, reason string) {
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
return
}
@@ -224,7 +234,7 @@ func (s *VoteService) notifyModerationRejected(userID, reason string) {
}
// resolveVoteOptions 解析投票选项(优先从 segments 读取,兼容旧数据从 vote_options 表读取)
func (s *VoteService) resolveVoteOptions(postID string, segments model.MessageSegments) ([]dto.VoteOptionDTO, error) {
func (s *voteService) resolveVoteOptions(postID string, segments model.MessageSegments) ([]dto.VoteOptionDTO, error) {
if len(segments) > 0 {
voteData := dto.ExtractVoteSegmentData(segments)
if voteData != nil {
@@ -262,7 +272,7 @@ func (s *VoteService) resolveVoteOptions(postID string, segments model.MessageSe
}
// GetVoteOptions 获取投票选项(优先从 segments 读取,兼容旧数据从 vote_options 表读取)
func (s *VoteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error) {
func (s *voteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error) {
post, err := s.postRepo.GetByID(postID)
if err != nil {
return nil, err
@@ -272,7 +282,7 @@ func (s *VoteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error)
}
// GetVoteResult 获取投票结果(包含用户投票状态)
func (s *VoteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error) {
func (s *voteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error) {
post, err := s.postRepo.GetByID(postID)
if err != nil {
return nil, err
@@ -306,7 +316,7 @@ func (s *VoteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO,
}
// Vote 投票
func (s *VoteService) Vote(ctx context.Context, postID, userID, optionID string) error {
func (s *voteService) Vote(ctx context.Context, postID, userID, optionID string) error {
// 调用voteRepo.Vote
err := s.voteRepo.Vote(postID, userID, optionID)
if err != nil {
@@ -320,7 +330,7 @@ func (s *VoteService) Vote(ctx context.Context, postID, userID, optionID string)
}
// Unvote 取消投票
func (s *VoteService) Unvote(ctx context.Context, postID, userID string) error {
func (s *voteService) Unvote(ctx context.Context, postID, userID string) error {
// 调用voteRepo.Unvote
err := s.voteRepo.Unvote(postID, userID)
if err != nil {
@@ -334,7 +344,7 @@ func (s *VoteService) Unvote(ctx context.Context, postID, userID string) error {
}
// UpdateVoteOption 更新投票选项(作者权限)
func (s *VoteService) UpdateVoteOption(ctx context.Context, postID, optionID, userID, content string) error {
func (s *voteService) UpdateVoteOption(ctx context.Context, postID, optionID, userID, content string) error {
// 获取帖子信息
post, err := s.postRepo.GetByID(postID)
if err != nil {
@@ -351,7 +361,7 @@ func (s *VoteService) UpdateVoteOption(ctx context.Context, postID, optionID, us
}
// convertToPostResponse 将Post模型转换为PostResponse DTO
func (s *VoteService) convertToPostResponse(post *model.Post, currentUserID string) *dto.PostResponse {
func (s *voteService) convertToPostResponse(post *model.Post, currentUserID string) *dto.PostResponse {
if post == nil {
return nil
}