refactor(server): decouple services and improve architecture
- 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:
@@ -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
|
||||
}
|
||||
|
||||
179
internal/service/admin_group_service_test.go
Normal file
179
internal/service/admin_group_service_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ var (
|
||||
ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled
|
||||
ErrNotRequestTarget = apperrors.ErrNotRequestTarget
|
||||
ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee
|
||||
ErrNotMutualFollow = apperrors.ErrNotMutualFollow
|
||||
ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -196,4 +196,3 @@ func NewLogService(
|
||||
DataChangeLog: dataChangeLogService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 ¬ificationService{
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -498,4 +498,3 @@ func (s *sensitiveServiceImpl) loadFromRedis(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
// 计算目标尺寸
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user