diff --git a/cmd/server/wire.go b/cmd/server/wire.go index 480a48b..26f83fa 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -1,4 +1,4 @@ -//go:build wireinject +//go:build wireinject // +build wireinject package main @@ -32,7 +32,7 @@ func ProvideRouter( messageHandler *handler.MessageHandler, notificationHandler *handler.NotificationHandler, uploadHandler *handler.UploadHandler, - jwtService *service.JWTService, + jwtService service.JWTService, pushHandler *handler.PushHandler, systemMessageHandler *handler.SystemMessageHandler, groupHandler *handler.GroupHandler, @@ -63,6 +63,7 @@ func ProvideRouter( logService *service.LogService, activityService service.UserActivityService, casbinService service.CasbinService, + userService service.UserService, wsHandler *handler.WSHandler, liveKitHandler *handler.LiveKitHandler, ) *router.Router { @@ -95,7 +96,7 @@ func ProvideRouter( QRCodeHandler: qrcodeHandler, MaterialHandler: materialHandler, CallHandler: callHandler, - LiveKitHandler: liveKitHandler, + LiveKitHandler: liveKitHandler, ReportHandler: reportHandler, AdminReportHandler: adminReportHandler, VerificationHandler: verificationHandler, @@ -106,6 +107,7 @@ func ProvideRouter( LogService: logService, ActivityService: activityService, CasbinService: casbinService, + UserService: userService, WSHandler: wsHandler, }) } diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index c747bfd..eb81a72 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -110,13 +110,13 @@ func InitializeApp() (*App, error) { scheduleHandler := wire.ProvideScheduleHandler(scheduleService, scheduleSyncService) gradeRepository := repository.NewGradeRepository(db) gradeSyncService := wire.ProvideGradeSyncService(taskManager, gradeRepository, logger) - gradeHandler := handler.NewGradeHandler(gradeSyncService, gradeRepository) + gradeHandler := handler.NewGradeHandler(gradeSyncService) examRepository := repository.NewExamRepository(db) examSyncService := wire.ProvideExamSyncService(taskManager, examRepository, logger) - examHandler := handler.NewExamHandler(examSyncService, examRepository) + examHandler := handler.NewExamHandler(examSyncService) emptyClassroomRepository := repository.NewEmptyClassroomRepository(db) emptyClassroomSyncService := wire.ProvideEmptyClassroomSyncService(taskManager, emptyClassroomRepository, logger) - emptyClassroomHandler := handler.NewEmptyClassroomHandler(emptyClassroomSyncService, emptyClassroomRepository) + emptyClassroomHandler := handler.NewEmptyClassroomHandler(emptyClassroomSyncService) roleRepository := wire.ProvideRoleRepository(db) enforcer := wire.ProvideCasbinEnforcer(config, db) casbinService := wire.ProvideCasbinService(enforcer, cache, roleRepository, config) @@ -151,17 +151,17 @@ func InitializeApp() (*App, error) { verificationService := wire.ProvideVerificationService(verificationRepository, userRepository) verificationHandler := handler.NewVerificationHandler(verificationService) adminVerificationService := wire.ProvideAdminVerificationService(verificationRepository, userRepository) - adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userRepository) + adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userService) adminProfileAuditHandler := handler.NewAdminProfileAuditHandler(userProfileAuditService) setupService := wire.ProvideSetupService(userRepository, roleRepository, casbinService, config) setupHandler := wire.ProvideSetupHandler(setupService) tradeRepository := repository.NewTradeRepository(db) tradeService := wire.ProvideTradeService(tradeRepository) tradeHandler := handler.NewTradeHandler(tradeService) - wsHandler := wire.ProvideWSHandler(messagePublisher, chatService, groupService, jwtService, callService, userRepository) + wsHandler := wire.ProvideWSHandler(messagePublisher, chatService, groupService, jwtService, callService, userService) liveKitService := wire.ProvideLiveKitService(config, logger) liveKitHandler := wire.ProvideLiveKitHandler(liveKitService, callService, config, logger) - router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, gradeHandler, examHandler, emptyClassroomHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, setupHandler, tradeHandler, logService, userActivityService, casbinService, wsHandler, liveKitHandler) + router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, gradeHandler, examHandler, emptyClassroomHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, setupHandler, tradeHandler, logService, userActivityService, casbinService, userService, wsHandler, liveKitHandler) hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, channelRepository, cache, client) server := wire.ProvideGRPCServer(config, logger, runnerHub) runnerRegistry := wire.ProvideRunnerRegistry(config, client) @@ -181,7 +181,7 @@ func ProvideRouter( messageHandler *handler.MessageHandler, notificationHandler *handler.NotificationHandler, uploadHandler *handler.UploadHandler, - jwtService *service.JWTService, + jwtService service.JWTService, pushHandler *handler.PushHandler, systemMessageHandler *handler.SystemMessageHandler, groupHandler *handler.GroupHandler, @@ -212,6 +212,7 @@ func ProvideRouter( logService *service.LogService, activityService service.UserActivityService, casbinService service.CasbinService, + userService service.UserService, wsHandler *handler.WSHandler, liveKitHandler *handler.LiveKitHandler, ) *router.Router { @@ -255,6 +256,7 @@ func ProvideRouter( LogService: logService, ActivityService: activityService, CasbinService: casbinService, + UserService: userService, WSHandler: wsHandler, }) } diff --git a/internal/cache/keys.go b/internal/cache/keys.go index 2f9912a..996b5c4 100644 --- a/internal/cache/keys.go +++ b/internal/cache/keys.go @@ -44,7 +44,6 @@ const ( keyPrefixMsgSeq = "msg_seq" // Seq 计数器 keyPrefixMsgPage = "msg_page" // 分页缓存 keyPrefixMsgIdempotent = "msg_idem" // 消息幂等键 - keyPrefixSeqBuffer = "seq_buf" // Seq 预分配缓冲区 Hash ) // PostListKey 生成帖子列表缓存键 @@ -198,12 +197,6 @@ func MessageIdempotentKey(senderID, clientMsgID string) string { return fmt.Sprintf("%s:%s:%s", keyPrefixMsgIdempotent, senderID, clientMsgID) } -// SeqBufferKey seq 预分配缓冲区 Hash key -func SeqBufferKey(convID string) string { - return fmt.Sprintf("%s:%s", keyPrefixSeqBuffer, convID) -} - - // UserReadSeqKey 用户已读位置缓存键(OpenIM 风格 SEQ_USER_READ:{convID}:{userID}) func UserReadSeqKey(convID, userID string) string { return fmt.Sprintf("%s:%s:%s", PrefixUserReadSeq, convID, userID) diff --git a/internal/cache/seq_buffer.go b/internal/cache/seq_buffer.go index cdb47de..a600c15 100644 --- a/internal/cache/seq_buffer.go +++ b/internal/cache/seq_buffer.go @@ -94,17 +94,6 @@ func (m *SeqBufferManager) IsEnabled() bool { return m.config.Enabled && m.rdb != nil } -// NewSeqBufferManagerFromCache 从 ConversationCache 的 Cache 接口创建 SeqBufferManager -func NewSeqBufferManagerFromCache(cacheBackend Cache, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager { - redisCache, ok := cacheBackend.(*RedisCache) - if !ok { - zap.L().Warn("SeqBuffer: cache is not RedisCache, seq pre-allocation disabled") - return &SeqBufferManager{config: cfg, repo: repo, msgRepo: msgRepo} - } - rdb := redisCache.GetRedisClient().GetClient() - return NewSeqBufferManager(rdb, cfg, repo, msgRepo) -} - // NewSeqBufferManagerFromRedisPkg 从 pkg/redis.Client 创建 SeqBufferManager func NewSeqBufferManagerFromRedisPkg(redisPkgClient *redisPkg.Client, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager { if redisPkgClient == nil { diff --git a/internal/model/init.go b/internal/database/database.go similarity index 72% rename from internal/model/init.go rename to internal/database/database.go index 6ad2efb..bc51f02 100644 --- a/internal/model/init.go +++ b/internal/database/database.go @@ -1,4 +1,9 @@ -package model +// Package database 负责数据库连接初始化、自动迁移与种子数据。 +// +// 这些引导职责从 model 包拆分而来:model 包应只包含纯数据结构定义与 +// GORM 钩子,不应依赖 config 或持有启动逻辑。本包依赖 config + model, +// 方向合法(引导层 → 模型层 / 配置层)。 +package database import ( "fmt" @@ -14,6 +19,7 @@ import ( "gorm.io/plugin/dbresolver" "with_you/internal/config" + "with_you/internal/model" ) // NewDB 创建数据库连接(用于 Wire 依赖注入) @@ -128,103 +134,103 @@ func parseGormLogLevel(level string) logger.LogLevel { func autoMigrate(db *gorm.DB) error { err := db.AutoMigrate( // 用户相关 - &User{}, + &model.User{}, // 帖子相关 - &Post{}, - &PostImage{}, - &Channel{}, + &model.Post{}, + &model.PostImage{}, + &model.Channel{}, // 评论相关 - &Comment{}, - &CommentLike{}, + &model.Comment{}, + &model.CommentLike{}, // 消息相关(使用雪花算法ID和seq机制) // 已读位置存储在 ConversationParticipant.LastReadSeq 中 - &Conversation{}, - &ConversationParticipant{}, - &Message{}, + &model.Conversation{}, + &model.ConversationParticipant{}, + &model.Message{}, // 系统通知(独立表,每个用户只能看到自己的通知) - &SystemNotification{}, + &model.SystemNotification{}, // 通知 - &Notification{}, + &model.Notification{}, // 推送中心相关 - &PushRecord{}, // 推送记录 - &DeviceToken{}, // 设备Token + &model.PushRecord{}, // 推送记录 + &model.DeviceToken{}, // 设备Token // 社交 - &Follow{}, - &UserBlock{}, - &PostLike{}, - &Favorite{}, + &model.Follow{}, + &model.UserBlock{}, + &model.PostLike{}, + &model.Favorite{}, // 投票 - &VoteOption{}, - &UserVote{}, + &model.VoteOption{}, + &model.UserVote{}, // 敏感词和审核 - &SensitiveWord{}, - // &AuditLog{}, // TODO: define AuditLog model + &model.SensitiveWord{}, + // &model.AuditLog{}, // TODO: define AuditLog model // 举报 - &Report{}, + &model.Report{}, // 日志相关 - &OperationLog{}, // 操作日志 - &LoginLog{}, // 登录日志 - &DataChangeLog{}, // 数据变更日志 + &model.OperationLog{}, // 操作日志 + &model.LoginLog{}, // 登录日志 + &model.DataChangeLog{}, // 数据变更日志 // 群组相关 - &Group{}, - &GroupMember{}, - &GroupAnnouncement{}, - &GroupJoinRequest{}, + &model.Group{}, + &model.GroupMember{}, + &model.GroupAnnouncement{}, + &model.GroupJoinRequest{}, // 自定义表情 - &UserSticker{}, + &model.UserSticker{}, // 课表 - &ScheduleCourse{}, + &model.ScheduleCourse{}, // 成绩与考试 - &Grade{}, - &GpaSummary{}, - &Exam{}, - &EmptyClassroom{}, + &model.Grade{}, + &model.GpaSummary{}, + &model.Exam{}, + &model.EmptyClassroom{}, // 用户活跃相关 - &UserActiveLog{}, - &UserActivityStat{}, + &model.UserActiveLog{}, + &model.UserActivityStat{}, // 角色和权限相关 - &Role{}, - &UserRole{}, - &CasbinRule{}, + &model.Role{}, + &model.UserRole{}, + &model.CasbinRule{}, // 学习资料相关 - &MaterialSubject{}, - &MaterialFile{}, + &model.MaterialSubject{}, + &model.MaterialFile{}, // 通话相关 - &CallSession{}, - &CallParticipant{}, + &model.CallSession{}, + &model.CallParticipant{}, // 身份认证相关 - &VerificationRecord{}, + &model.VerificationRecord{}, // 用户资料审核相关 - &UserProfileAudit{}, + &model.UserProfileAudit{}, // 帖子内链引用关系 - &PostReference{}, + &model.PostReference{}, // 二手交易相关 - &TradeItem{}, - &TradeImage{}, - &TradeFavorite{}, + &model.TradeItem{}, + &model.TradeImage{}, + &model.TradeFavorite{}, ) if err != nil { return err @@ -267,7 +273,7 @@ func (postWithHotScore) TableName() string { return "posts" } func dropPostsHotScoreColumnIfExists(db *gorm.DB) error { m := db.Migrator() - if !m.HasTable(&Post{}) { + if !m.HasTable(&model.Post{}) { return nil } if !m.HasColumn(&postWithHotScore{}, "HotScore") { @@ -280,7 +286,7 @@ func dropPostsHotScoreColumnIfExists(db *gorm.DB) error { func seedRoles(db *gorm.DB) error { // 检查是否已有角色数据 var count int64 - if err := db.Model(&Role{}).Count(&count).Error; err != nil { + if err := db.Model(&model.Role{}).Count(&count).Error; err != nil { return err } @@ -290,33 +296,33 @@ func seedRoles(db *gorm.DB) error { } // 预定义角色数据 - roles := []Role{ + roles := []model.Role{ { - Name: RoleSuperAdmin, + Name: model.RoleSuperAdmin, DisplayName: "超级管理员", Description: "拥有系统最高权限,可以管理所有用户和内容", Priority: 100, }, { - Name: RoleAdmin, + Name: model.RoleAdmin, DisplayName: "管理员", Description: "系统管理员,可以管理用户和内容", Priority: 80, }, { - Name: RoleModerator, + Name: model.RoleModerator, DisplayName: "版主", Description: "内容审核员,可以审核和管理内容", Priority: 60, }, { - Name: RoleUser, + Name: model.RoleUser, DisplayName: "普通用户", Description: "注册用户,拥有基本权限", Priority: 40, }, { - Name: RoleBanned, + Name: model.RoleBanned, DisplayName: "被封禁用户", Description: "被禁止访问的用户", Priority: 0, @@ -338,7 +344,7 @@ func seedRoles(db *gorm.DB) error { func seedPermissions(db *gorm.DB) error { // 检查是否已有权限策略数据 var count int64 - if err := db.Model(&CasbinRule{}).Where("ptype = ?", "p").Count(&count).Error; err != nil { + if err := db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count).Error; err != nil { return err } @@ -349,34 +355,34 @@ func seedPermissions(db *gorm.DB) error { // 预定义权限策略数据 // p = sub, obj, act (角色, 资源, 操作) - permissions := []CasbinRule{ + permissions := []model.CasbinRule{ // 超级管理员 - 拥有所有权限 - {Ptype: "p", V0: RoleSuperAdmin, V1: "/*", V2: "*"}, + {Ptype: "p", V0: model.RoleSuperAdmin, V1: "/*", V2: "*"}, // 管理员 - 拥有管理权限 - {Ptype: "p", V0: RoleAdmin, V1: "/api/v1/admin/*", V2: "*"}, - {Ptype: "p", V0: RoleAdmin, V1: "/api/v1/users/*", V2: "GET"}, - {Ptype: "p", V0: RoleAdmin, V1: "/api/v1/posts/*", V2: "*"}, - {Ptype: "p", V0: RoleAdmin, V1: "/api/v1/comments/*", V2: "*"}, - {Ptype: "p", V0: RoleAdmin, V1: "/api/v1/groups/*", V2: "*"}, + {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/admin/*", V2: "*"}, + {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/users/*", V2: "GET"}, + {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/posts/*", V2: "*"}, + {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/comments/*", V2: "*"}, + {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/groups/*", V2: "*"}, // 版主 - 拥有内容审核权限 - {Ptype: "p", V0: RoleModerator, V1: "/api/v1/posts/*", V2: "GET"}, - {Ptype: "p", V0: RoleModerator, V1: "/api/v1/posts/*", V2: "DELETE"}, - {Ptype: "p", V0: RoleModerator, V1: "/api/v1/comments/*", V2: "GET"}, - {Ptype: "p", V0: RoleModerator, V1: "/api/v1/comments/*", V2: "DELETE"}, - {Ptype: "p", V0: RoleModerator, V1: "/api/v1/groups/*", V2: "GET"}, + {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/posts/*", V2: "GET"}, + {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/posts/*", V2: "DELETE"}, + {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/comments/*", V2: "GET"}, + {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/comments/*", V2: "DELETE"}, + {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/groups/*", V2: "GET"}, // 普通用户 - 拥有基本权限 - {Ptype: "p", V0: RoleUser, V1: "/api/v1/posts", V2: "GET"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/posts/*", V2: "GET"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/posts", V2: "POST"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/comments/*", V2: "GET"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/comments", V2: "POST"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/users/me", V2: "GET"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/users/me", V2: "PUT"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/groups", V2: "GET"}, - {Ptype: "p", V0: RoleUser, V1: "/api/v1/groups/*", V2: "GET"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts", V2: "GET"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts/*", V2: "GET"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts", V2: "POST"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/comments/*", V2: "GET"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/comments", V2: "POST"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/users/me", V2: "GET"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/users/me", V2: "PUT"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/groups", V2: "GET"}, + {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/groups/*", V2: "GET"}, // 被封禁用户 - 无权限 } @@ -396,7 +402,7 @@ func seedPermissions(db *gorm.DB) error { func seedChannels(db *gorm.DB) error { // 检查是否已有频道数据 var count int64 - if err := db.Model(&Channel{}).Count(&count).Error; err != nil { + if err := db.Model(&model.Channel{}).Count(&count).Error; err != nil { return err } if count > 0 { @@ -404,7 +410,7 @@ func seedChannels(db *gorm.DB) error { return nil } - defaultChannels := []Channel{ + defaultChannels := []model.Channel{ {Name: "跳蚤市场", Slug: "market", Description: "二手交易与闲置发布", SortOrder: 10, IsActive: true}, {Name: "课程交流", Slug: "courses", Description: "课程资料、选课与学习讨论", SortOrder: 20, IsActive: true}, {Name: "工作实习", Slug: "jobs", Description: "实习、内推与求职信息", SortOrder: 30, IsActive: true}, @@ -424,7 +430,7 @@ func seedChannels(db *gorm.DB) error { func seedMaterialSubjects(db *gorm.DB) error { // 检查是否已有数据 var count int64 - if err := db.Model(&MaterialSubject{}).Count(&count).Error; err != nil { + if err := db.Model(&model.MaterialSubject{}).Count(&count).Error; err != nil { return err } if count > 0 { @@ -432,7 +438,7 @@ func seedMaterialSubjects(db *gorm.DB) error { return nil } - defaultSubjects := []MaterialSubject{ + defaultSubjects := []model.MaterialSubject{ {Name: "数学", Icon: "calculator-variant", Color: "#FF6B6B", Description: "高等数学、线性代数、概率论等", SortOrder: 10, IsActive: true}, {Name: "英语", Icon: "translate", Color: "#4ECDC4", Description: "四六级、考研英语、托福雅思等", SortOrder: 20, IsActive: true}, {Name: "物理", Icon: "atom", Color: "#45B7D1", Description: "大学物理、电磁学、力学等", SortOrder: 30, IsActive: true}, @@ -450,4 +456,3 @@ func seedMaterialSubjects(db *gorm.DB) error { zap.L().Info("Seeded material subjects successfully", zap.Int("count", len(defaultSubjects))) return nil } - diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000..39b34fd --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,222 @@ +package database + +import ( + "os" + "path/filepath" + "testing" + + "with_you/internal/config" + "with_you/internal/model" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// 注意:完整的 NewDB→autoMigrate 在 SQLite 下会因 model 包多个表共用 +// 同名索引 idx_created(data_change_log / login_log / operation_log)而冲突。 +// 这是 model 包的既有约束(PostgreSQL 经 CREATE INDEX IF NOT EXISTS 容忍), +// 与本次分层拆分无关。因此种子逻辑测试在隔离的 SQLite 库上验证: +// 仅迁移被种子函数读写的表,避开跨表索引冲突,专注验证种子正确性与幂等性。 + +// newSeedTestDB 创建一个仅迁移种子相关表的 SQLite 测试库。 +func newSeedTestDB(t *testing.T) *gorm.DB { + t.Helper() + + dbPath := filepath.Join(t.TempDir(), "seed_test.db") + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + t.Fatalf("open sqlite failed: %v", err) + } + + // 仅迁移种子函数涉及的表(无 idx_created 冲突) + if err := db.AutoMigrate( + &model.Role{}, + &model.CasbinRule{}, + &model.Channel{}, + &model.MaterialSubject{}, + &model.Post{}, // dropPostsHotScoreColumnIfExists 依赖 posts 表存在 + ); err != nil { + t.Fatalf("autoMigrate seed tables failed: %v", err) + } + + t.Cleanup(func() { + if sqlDB, err := db.DB(); err == nil { + _ = sqlDB.Close() + } + }) + return db +} + +// TestSeedRoles 验证角色种子写入 5 个预定义角色。 +func TestSeedRoles(t *testing.T) { + db := newSeedTestDB(t) + + if err := seedRoles(db); err != nil { + t.Fatalf("seedRoles failed: %v", err) + } + + var count int64 + db.Model(&model.Role{}).Count(&count) + if count != 5 { + t.Errorf("expected 5 seeded roles, got %d", count) + } + + expected := []string{ + model.RoleSuperAdmin, model.RoleAdmin, model.RoleModerator, + model.RoleUser, model.RoleBanned, + } + for _, name := range expected { + var role model.Role + if err := db.Where("name = ?", name).First(&role).Error; err != nil { + t.Errorf("expected role %q: %v", name, err) + } + } +} + +// TestSeedRoles_Idempotent 验证角色种子幂等:表非空时跳过。 +func TestSeedRoles_Idempotent(t *testing.T) { + db := newSeedTestDB(t) + + if err := seedRoles(db); err != nil { + t.Fatalf("first seedRoles: %v", err) + } + // 第二次调用应跳过(表已有数据) + if err := seedRoles(db); err != nil { + t.Fatalf("second seedRoles: %v", err) + } + + var count int64 + db.Model(&model.Role{}).Count(&count) + if count != 5 { + t.Errorf("idempotent seed failed: expected 5 roles, got %d", count) + } +} + +// TestSeedPermissions 验证权限策略种子写入,含超级管理员通配规则。 +func TestSeedPermissions(t *testing.T) { + db := newSeedTestDB(t) + + if err := seedPermissions(db); err != nil { + t.Fatalf("seedPermissions failed: %v", err) + } + + var count int64 + db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count) + if count == 0 { + t.Fatal("expected seeded permissions, got 0") + } + + // 超级管理员 /* 通配 + var rule model.CasbinRule + if err := db.Where("ptype=? AND v0=? AND v1=?", "p", model.RoleSuperAdmin, "/*").First(&rule).Error; err != nil { + t.Errorf("expected superadmin wildcard permission: %v", err) + } +} + +// TestSeedPermissions_Idempotent 验证权限种子幂等。 +func TestSeedPermissions_Idempotent(t *testing.T) { + db := newSeedTestDB(t) + + if err := seedPermissions(db); err != nil { + t.Fatalf("first seedPermissions: %v", err) + } + var count1 int64 + db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count1) + + if err := seedPermissions(db); err != nil { + t.Fatalf("second seedPermissions: %v", err) + } + var count2 int64 + db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count2) + + if count2 != count1 { + t.Errorf("seed not idempotent: first=%d, second=%d", count1, count2) + } +} + +// TestSeedChannels 验证频道种子写入 5 个默认频道。 +func TestSeedChannels(t *testing.T) { + db := newSeedTestDB(t) + + if err := seedChannels(db); err != nil { + t.Fatalf("seedChannels failed: %v", err) + } + + var count int64 + db.Model(&model.Channel{}).Count(&count) + if count != 5 { + t.Errorf("expected 5 channels, got %d", count) + } + + // 抽查一个频道 + var ch model.Channel + if err := db.Where("slug = ?", "market").First(&ch).Error; err != nil { + t.Errorf("expected market channel: %v", err) + } +} + +// TestSeedMaterialSubjects 验证学科种子写入 8 个默认学科。 +func TestSeedMaterialSubjects(t *testing.T) { + db := newSeedTestDB(t) + + if err := seedMaterialSubjects(db); err != nil { + t.Fatalf("seedMaterialSubjects failed: %v", err) + } + + var count int64 + db.Model(&model.MaterialSubject{}).Count(&count) + if count != 8 { + t.Errorf("expected 8 material subjects, got %d", count) + } +} + +// TestParseGormLogLevel 验证日志级别解析映射。 +func TestParseGormLogLevel(t *testing.T) { + cases := []struct { + in string + want logger.LogLevel + }{ + {"silent", logger.Silent}, + {"error", logger.Error}, + {"warn", logger.Warn}, + {"info", logger.Info}, + {"", logger.Warn}, // 默认 + {"bogus", logger.Warn}, // 未知值降级为 warn + } + for _, c := range cases { + if got := parseGormLogLevel(c.in); got != c.want { + t.Errorf("parseGormLogLevel(%q) = %v, want %v", c.in, got, c.want) + } + } +} + +// TestDropPostsHotScoreColumnIfExists_NoTable 验证当 posts 表不存在时安全跳过。 +func TestDropPostsHotScoreColumnIfExists_NoTable(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "empty.db") + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + t.Cleanup(func() { + if sqlDB, err := db.DB(); err == nil { + _ = sqlDB.Close() + } + }) + + // 未迁移 posts 表 → 应安全返回 nil + if err := dropPostsHotScoreColumnIfExists(db); err != nil { + t.Errorf("expected nil when posts table absent, got: %v", err) + } +} + +// 编译期断言:确保 NewDB 签名符合 (*config.DatabaseConfig) -> (*gorm.DB, error) +var _ = func() (*gorm.DB, error) { + return NewDB(&config.DatabaseConfig{}) +} + +var _ = os.RemoveAll // 保持 os 引用可用(供未来临时文件场景扩展) diff --git a/internal/dto/dto.go b/internal/dto/dto.go index 9a56092..8cb62cc 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -131,52 +131,6 @@ type AdminUpdateUserStatusRequest struct { Status string `json:"status" binding:"required,oneof=active banned"` } -// ==================== Post DTOs ==================== - -// PostChannelBrief 帖子所属频道(列表/详情卡片展示) -type PostChannelBrief struct { - ID string `json:"id"` - Name string `json:"name"` -} - -// PostImageResponse 帖子图片响应 -type PostImageResponse struct { - ID string `json:"id"` - URL string `json:"url"` - ThumbnailURL string `json:"thumbnail_url"` - PreviewURL string `json:"preview_url"` // 列表/网格预览图 - PreviewURLLarge string `json:"preview_url_large"` // 详情页预览图 - Width int `json:"width"` - Height int `json:"height"` -} - -// PostResponse 帖子响应(列表用) -type PostResponse struct { - ID string `json:"id"` - UserID string `json:"user_id"` - ChannelID *string `json:"channel_id,omitempty"` - Title string `json:"title"` - Content string `json:"content"` - Segments model.MessageSegments `json:"segments,omitempty"` - Images []PostImageResponse `json:"images"` - Status string `json:"status,omitempty"` - LikesCount int `json:"likes_count"` - CommentsCount int `json:"comments_count"` - FavoritesCount int `json:"favorites_count"` - SharesCount int `json:"shares_count"` - ViewsCount int `json:"views_count"` - IsPinned bool `json:"is_pinned"` - IsLocked bool `json:"is_locked"` - IsVote bool `json:"is_vote"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - ContentEditedAt string `json:"content_edited_at,omitempty"` - Author *UserResponse `json:"author"` - IsLiked bool `json:"is_liked"` - IsFavorited bool `json:"is_favorited"` - Channel *PostChannelBrief `json:"channel,omitempty"` -} - // ==================== Comment DTOs ==================== // CommentImageResponse 评论图片响应 @@ -224,16 +178,16 @@ type NotificationResponse struct { type SegmentType string const ( - SegmentTypeText SegmentType = "text" - SegmentTypeImage SegmentType = "image" - SegmentTypeVoice SegmentType = "voice" - SegmentTypeVideo SegmentType = "video" - SegmentTypeFile SegmentType = "file" - SegmentTypeAt SegmentType = "at" - SegmentTypeReply SegmentType = "reply" - SegmentTypeFace SegmentType = "face" - SegmentTypeLink SegmentType = "link" - SegmentTypeVote SegmentType = "vote" + SegmentTypeText SegmentType = "text" + SegmentTypeImage SegmentType = "image" + SegmentTypeVoice SegmentType = "voice" + SegmentTypeVideo SegmentType = "video" + SegmentTypeFile SegmentType = "file" + SegmentTypeAt SegmentType = "at" + SegmentTypeReply SegmentType = "reply" + SegmentTypeFace SegmentType = "face" + SegmentTypeLink SegmentType = "link" + SegmentTypeVote SegmentType = "vote" SegmentTypePostRef SegmentType = "post_ref" ) @@ -428,8 +382,8 @@ type CreateConversationRequest struct { // SendMessageRequest 发送消息请求 type SendMessageRequest struct { Segments model.MessageSegments `json:"segments" binding:"required"` // 消息链(必须) - ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID (string类型) - ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID(幂等性去重) + ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID (string类型) + ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID(幂等性去重) } // MarkReadRequest 标记已读请求 @@ -619,122 +573,6 @@ func FormatTimePointer(t *time.Time) string { return FormatTime(*t) } -// ==================== Group DTOs ==================== - -// CreateGroupRequest 创建群组请求 -type CreateGroupRequest struct { - Name string `json:"name" binding:"required,max=50"` - Description string `json:"description" binding:"max=500"` - MemberIDs []string `json:"member_ids"` -} - -// UpdateGroupRequest 更新群组请求 -type UpdateGroupRequest struct { - Name string `json:"name" binding:"omitempty,max=50"` - Description string `json:"description" binding:"omitempty,max=500"` - Avatar string `json:"avatar" binding:"omitempty,url"` -} - -// InviteMembersRequest 邀请成员请求 -type InviteMembersRequest struct { - MemberIDs []string `json:"member_ids" binding:"required,min=1"` -} - -// TransferOwnerRequest 转让群主请求 -type TransferOwnerRequest struct { - NewOwnerID string `json:"new_owner_id" binding:"required"` -} - -// SetRoleRequest 设置角色请求 -type SetRoleRequest struct { - Role string `json:"role" binding:"required,oneof=admin member"` -} - -// SetNicknameRequest 设置昵称请求 -type SetNicknameRequest struct { - Nickname string `json:"nickname" binding:"max=50"` -} - -// MuteMemberRequest 禁言成员请求 -type MuteMemberRequest struct { - Muted bool `json:"muted"` -} - -// SetMuteAllRequest 设置全员禁言请求 -type SetMuteAllRequest struct { - MuteAll bool `json:"mute_all"` -} - -// SetJoinTypeRequest 设置加群方式请求 -type SetJoinTypeRequest struct { - JoinType int `json:"join_type" binding:"min=0,max=2"` -} - -// CreateAnnouncementRequest 创建群公告请求 -type CreateAnnouncementRequest struct { - Content string `json:"content" binding:"required,max=2000"` -} - -// GroupResponse 群组响应 -type GroupResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Avatar string `json:"avatar"` - Description string `json:"description"` - OwnerID string `json:"owner_id"` - MemberCount int `json:"member_count"` - MaxMembers int `json:"max_members"` - JoinType int `json:"join_type"` - MuteAll bool `json:"mute_all"` - CreatedAt string `json:"created_at"` -} - -// GroupMemberResponse 群成员响应 -type GroupMemberResponse struct { - ID string `json:"id"` - GroupID string `json:"group_id"` - UserID string `json:"user_id"` - Role string `json:"role"` - Nickname string `json:"nickname"` - Muted bool `json:"muted"` - JoinTime string `json:"join_time"` - User *UserResponse `json:"user,omitempty"` -} - -// GroupAnnouncementResponse 群公告响应 -type GroupAnnouncementResponse struct { - ID string `json:"id"` - GroupID string `json:"group_id"` - Content string `json:"content"` - AuthorID string `json:"author_id"` - IsPinned bool `json:"is_pinned"` - CreatedAt string `json:"created_at"` -} - -// GroupListResponse 群组列表响应 -type GroupListResponse struct { - List []*GroupResponse `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` -} - -// GroupMemberListResponse 群成员列表响应 -type GroupMemberListResponse struct { - List []*GroupMemberResponse `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` -} - -// GroupAnnouncementListResponse 群公告列表响应 -type GroupAnnouncementListResponse struct { - List []*GroupAnnouncementResponse `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` -} - // ==================== WebSocket Event DTOs ==================== // WSEventResponse WebSocket事件响应结构体 @@ -745,7 +583,7 @@ type WSEventResponse struct { Type string `json:"type"` // 事件类型 (message, notification, system等) DetailType string `json:"detail_type"` // 详细类型 (private, group, like, comment等) ConversationID string `json:"conversation_id"` // 会话ID - Seq int64 `json:"seq"` // 消息序列号 + Seq int64 `json:"seq"` // 消息序列号 Segments model.MessageSegments `json:"segments"` // 消息段数组 SenderID string `json:"sender_id"` // 发送者用户ID } @@ -754,11 +592,11 @@ type WSEventResponse struct { // SendMessageParams 发送消息参数(用于 REST API) type SendMessageParams struct { - DetailType string `json:"detail_type"` // 消息类型: private, group - ConversationID string `json:"conversation_id"` // 会话ID - Segments model.MessageSegments `json:"segments"` // 消息内容(消息段数组) - ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID - ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID(幂等性去重) + DetailType string `json:"detail_type"` // 消息类型: private, group + ConversationID string `json:"conversation_id"` // 会话ID + Segments model.MessageSegments `json:"segments"` // 消息内容(消息段数组) + ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID + ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID(幂等性去重) } // DeleteMsgParams 撤回消息参数 @@ -809,7 +647,7 @@ type SetGroupAvatarParams struct { // SetGroupDescriptionParams 设置群描述参数 type SetGroupDescriptionParams struct { - GroupID string `json:"group_id"` // 群组ID + GroupID string `json:"group_id"` // 群组ID Description string `json:"description"` // 群描述 } @@ -935,41 +773,6 @@ type GetMyMemberInfoParams struct { GroupID string `json:"group_id"` // 群组ID } -// ==================== Vote DTOs ==================== - -// CreateVotePostRequest 创建投票帖子请求 -type CreateVotePostRequest struct { - Title string `json:"title" binding:"required,max=200"` - Content string `json:"content" binding:"max=2000"` - ChannelID *string `json:"channel_id,omitempty"` - Images []string `json:"images"` - VoteOptions []string `json:"vote_options" binding:"required,min=2,max=10"` -} - -// CreatePostWithSegmentsRequest 创建帖子请求(含 segments) -type CreatePostWithSegmentsRequest struct { - Title string `json:"title" binding:"required,max=200"` - Content string `json:"content"` - Segments model.MessageSegments `json:"segments"` - Images []string `json:"images"` - ChannelID *string `json:"channel_id,omitempty"` -} - -// VoteOptionDTO 投票选项DTO -type VoteOptionDTO struct { - ID string `json:"id"` - Content string `json:"content"` - VotesCount int `json:"votes_count"` -} - -// VoteResultDTO 投票结果DTO -type VoteResultDTO struct { - Options []VoteOptionDTO `json:"options"` - TotalVotes int `json:"total_votes"` - HasVoted bool `json:"has_voted"` - VotedOptionID string `json:"voted_option_id,omitzero"` -} - // ==================== WebSocket Response DTOs ==================== // WSResponse WebSocket响应结构体 diff --git a/internal/dto/filter.go b/internal/dto/filter.go deleted file mode 100644 index c490512..0000000 --- a/internal/dto/filter.go +++ /dev/null @@ -1,51 +0,0 @@ -package dto - -import "time" - -// LogFilter 日志过滤条件 -type LogFilter struct { - UserID string - Operation string - TargetType string - TargetID string - Status string - IP string - StartTime string - EndTime string -} - -// LoginFilter 登录日志过滤条件 -type LoginFilter struct { - UserID string - Event string - Result string - FailReason string - LoginType string - IP string - StartTime time.Time - EndTime time.Time -} - -// DataChangeFilter 数据变更日志过滤条件 -type DataChangeFilter struct { - UserID string - OperatorID string - ChangeType string - TargetType string - OperatorType string - IP string - StartTime string - EndTime string -} - -// MaterialFileQueryParams 学习资料查询参数 -type MaterialFileQueryParams struct { - SubjectID string - FileType string - Status string - Keyword string - Page int - PageSize int - SortBy string - SortOrder string -} diff --git a/internal/dto/group_dto.go b/internal/dto/group_dto.go new file mode 100644 index 0000000..eac82a0 --- /dev/null +++ b/internal/dto/group_dto.go @@ -0,0 +1,119 @@ +package dto + +// ==================== Group DTOs ==================== +// 从 dto.go 拆分而来:群组相关的请求与响应 DTO。 +// 与 group_converter.go 同属群组域。 + +// CreateGroupRequest 创建群组请求 +type CreateGroupRequest struct { + Name string `json:"name" binding:"required,max=50"` + Description string `json:"description" binding:"max=500"` + MemberIDs []string `json:"member_ids"` +} + +// UpdateGroupRequest 更新群组请求 +type UpdateGroupRequest struct { + Name string `json:"name" binding:"omitempty,max=50"` + Description string `json:"description" binding:"omitempty,max=500"` + Avatar string `json:"avatar" binding:"omitempty,url"` +} + +// InviteMembersRequest 邀请成员请求 +type InviteMembersRequest struct { + MemberIDs []string `json:"member_ids" binding:"required,min=1"` +} + +// TransferOwnerRequest 转让群主请求 +type TransferOwnerRequest struct { + NewOwnerID string `json:"new_owner_id" binding:"required"` +} + +// SetRoleRequest 设置角色请求 +type SetRoleRequest struct { + Role string `json:"role" binding:"required,oneof=admin member"` +} + +// SetNicknameRequest 设置昵称请求 +type SetNicknameRequest struct { + Nickname string `json:"nickname" binding:"max=50"` +} + +// MuteMemberRequest 禁言成员请求 +type MuteMemberRequest struct { + Muted bool `json:"muted"` +} + +// SetMuteAllRequest 设置全员禁言请求 +type SetMuteAllRequest struct { + MuteAll bool `json:"mute_all"` +} + +// SetJoinTypeRequest 设置加群方式请求 +type SetJoinTypeRequest struct { + JoinType int `json:"join_type" binding:"min=0,max=2"` +} + +// CreateAnnouncementRequest 创建群公告请求 +type CreateAnnouncementRequest struct { + Content string `json:"content" binding:"required,max=2000"` +} + +// GroupResponse 群组响应 +type GroupResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Description string `json:"description"` + OwnerID string `json:"owner_id"` + MemberCount int `json:"member_count"` + MaxMembers int `json:"max_members"` + JoinType int `json:"join_type"` + MuteAll bool `json:"mute_all"` + CreatedAt string `json:"created_at"` +} + +// GroupMemberResponse 群成员响应 +type GroupMemberResponse struct { + ID string `json:"id"` + GroupID string `json:"group_id"` + UserID string `json:"user_id"` + Role string `json:"role"` + Nickname string `json:"nickname"` + Muted bool `json:"muted"` + JoinTime string `json:"join_time"` + User *UserResponse `json:"user,omitempty"` +} + +// GroupAnnouncementResponse 群公告响应 +type GroupAnnouncementResponse struct { + ID string `json:"id"` + GroupID string `json:"group_id"` + Content string `json:"content"` + AuthorID string `json:"author_id"` + IsPinned bool `json:"is_pinned"` + CreatedAt string `json:"created_at"` +} + +// GroupListResponse 群组列表响应 +type GroupListResponse struct { + List []*GroupResponse `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// GroupMemberListResponse 群成员列表响应 +type GroupMemberListResponse struct { + List []*GroupMemberResponse `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// GroupAnnouncementListResponse 群公告列表响应 +type GroupAnnouncementListResponse struct { + List []*GroupAnnouncementResponse `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} diff --git a/internal/dto/post_dto.go b/internal/dto/post_dto.go new file mode 100644 index 0000000..b659334 --- /dev/null +++ b/internal/dto/post_dto.go @@ -0,0 +1,86 @@ +package dto + +import "with_you/internal/model" + +// ==================== Post DTOs ==================== +// 从 dto.go 拆分而来:帖子与投票相关的请求/响应 DTO。 +// 与 post_converter.go 同属帖子域。 + +// PostChannelBrief 帖子所属频道(列表/详情卡片展示) +type PostChannelBrief struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// PostImageResponse 帖子图片响应 +type PostImageResponse struct { + ID string `json:"id"` + URL string `json:"url"` + ThumbnailURL string `json:"thumbnail_url"` + PreviewURL string `json:"preview_url"` // 列表/网格预览图 + PreviewURLLarge string `json:"preview_url_large"` // 详情页预览图 + Width int `json:"width"` + Height int `json:"height"` +} + +// PostResponse 帖子响应(列表用) +type PostResponse struct { + ID string `json:"id"` + UserID string `json:"user_id"` + ChannelID *string `json:"channel_id,omitempty"` + Title string `json:"title"` + Content string `json:"content"` + Segments model.MessageSegments `json:"segments,omitempty"` + Images []PostImageResponse `json:"images"` + Status string `json:"status,omitempty"` + LikesCount int `json:"likes_count"` + CommentsCount int `json:"comments_count"` + FavoritesCount int `json:"favorites_count"` + SharesCount int `json:"shares_count"` + ViewsCount int `json:"views_count"` + IsPinned bool `json:"is_pinned"` + IsLocked bool `json:"is_locked"` + IsVote bool `json:"is_vote"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ContentEditedAt string `json:"content_edited_at,omitempty"` + Author *UserResponse `json:"author"` + IsLiked bool `json:"is_liked"` + IsFavorited bool `json:"is_favorited"` + Channel *PostChannelBrief `json:"channel,omitempty"` +} + +// ==================== Vote DTOs ==================== + +// CreateVotePostRequest 创建投票帖子请求 +type CreateVotePostRequest struct { + Title string `json:"title" binding:"required,max=200"` + Content string `json:"content" binding:"max=2000"` + ChannelID *string `json:"channel_id,omitempty"` + Images []string `json:"images"` + VoteOptions []string `json:"vote_options" binding:"required,min=2,max=10"` +} + +// CreatePostWithSegmentsRequest 创建帖子请求(含 segments) +type CreatePostWithSegmentsRequest struct { + Title string `json:"title" binding:"required,max=200"` + Content string `json:"content"` + Segments model.MessageSegments `json:"segments"` + Images []string `json:"images"` + ChannelID *string `json:"channel_id,omitempty"` +} + +// VoteOptionDTO 投票选项DTO +type VoteOptionDTO struct { + ID string `json:"id"` + Content string `json:"content"` + VotesCount int `json:"votes_count"` +} + +// VoteResultDTO 投票结果DTO +type VoteResultDTO struct { + Options []VoteOptionDTO `json:"options"` + TotalVotes int `json:"total_votes"` + HasVoted bool `json:"has_voted"` + VotedOptionID string `json:"voted_option_id,omitzero"` +} diff --git a/internal/dto/report.go b/internal/dto/report.go index 9859fdf..bd6901a 100644 --- a/internal/dto/report.go +++ b/internal/dto/report.go @@ -1,4 +1,4 @@ -package dto +package dto import ( "with_you/internal/model" @@ -8,10 +8,10 @@ import ( // CreateReportRequest 创建举报请求 type CreateReportRequest struct { - TargetType string `json:"target_type" binding:"required,oneof=post comment message"` - TargetID string `json:"target_id" binding:"required"` - Reason string `json:"reason" binding:"required,oneof=spam inappropriate harassment misinformation other"` - Description string `json:"description" binding:"max=500"` + TargetType string `json:"target_type" binding:"required,oneof=post comment message"` + TargetID string `json:"target_id" binding:"required"` + Reason string `json:"reason" binding:"required,oneof=spam inappropriate harassment misinformation other"` + Description string `json:"description" binding:"max=500"` } // ReportResponse 举报响应 @@ -26,89 +26,77 @@ type ReportResponse struct { CreatedAt string `json:"created_at"` } -// AdminReportListQuery 管理端举报列表查询参数 -type AdminReportListQuery struct { - Page int `form:"page" binding:"min=1"` - PageSize int `form:"page_size" binding:"min=1,max=100"` - TargetType string `form:"target_type" binding:"omitempty,oneof=post comment message"` - Status string `form:"status" binding:"omitempty,oneof=pending processing resolved rejected"` - StartDate string `form:"start_date" binding:"omitempty"` - EndDate string `form:"end_date" binding:"omitempty"` - Keyword string `form:"keyword" binding:"omitempty"` -} - // AdminReportListResponse 管理端举报列表响应 type AdminReportListResponse struct { - ID string `json:"id"` - ReporterID string `json:"reporter_id"` + ID string `json:"id"` + ReporterID string `json:"reporter_id"` Reporter *UserResponse `json:"reporter,omitempty"` - TargetType string `json:"target_type"` - TargetID string `json:"target_id"` - Reason string `json:"reason"` - Description string `json:"description,omitempty"` - Status string `json:"status"` - HandledBy *string `json:"handled_by,omitempty"` + TargetType string `json:"target_type"` + TargetID string `json:"target_id"` + Reason string `json:"reason"` + Description string `json:"description,omitempty"` + Status string `json:"status"` + HandledBy *string `json:"handled_by,omitempty"` Handler *UserResponse `json:"handler,omitempty"` - HandledAt *string `json:"handled_at,omitempty"` - Result *string `json:"result,omitempty"` - CreatedAt string `json:"created_at"` + HandledAt *string `json:"handled_at,omitempty"` + Result *string `json:"result,omitempty"` + CreatedAt string `json:"created_at"` } // AdminReportDetailResponse 管理端举报详情响应 type AdminReportDetailResponse struct { - ID string `json:"id"` - ReporterID string `json:"reporter_id"` + ID string `json:"id"` + ReporterID string `json:"reporter_id"` Reporter *UserResponse `json:"reporter"` - TargetType string `json:"target_type"` - TargetID string `json:"target_id"` - Reason string `json:"reason"` - Description string `json:"description,omitempty"` - Status string `json:"status"` - HandledBy *string `json:"handled_by,omitempty"` + TargetType string `json:"target_type"` + TargetID string `json:"target_id"` + Reason string `json:"reason"` + Description string `json:"description,omitempty"` + Status string `json:"status"` + HandledBy *string `json:"handled_by,omitempty"` Handler *UserResponse `json:"handler,omitempty"` - HandledAt *string `json:"handled_at,omitempty"` - Result *string `json:"result,omitempty"` - CreatedAt string `json:"created_at"` - + HandledAt *string `json:"handled_at,omitempty"` + Result *string `json:"result,omitempty"` + CreatedAt string `json:"created_at"` + // 被举报内容详情 TargetContent *ReportTargetContent `json:"target_content,omitempty"` } // ReportTargetContent 被举报内容详情 type ReportTargetContent struct { - Type string `json:"type"` // post, comment, message - ID string `json:"id"` - Author *UserResponse `json:"author,omitempty"` - Content string `json:"content,omitempty"` - Title string `json:"title,omitempty"` // 帖子标题 - Images []string `json:"images,omitempty"` // 图片列表 - Status string `json:"status,omitempty"` // 内容状态 - CreatedAt string `json:"created_at,omitempty"` + Type string `json:"type"` // post, comment, message + ID string `json:"id"` + Author *UserResponse `json:"author,omitempty"` + Content string `json:"content,omitempty"` + Title string `json:"title,omitempty"` // 帖子标题 + Images []string `json:"images,omitempty"` // 图片列表 + Status string `json:"status,omitempty"` // 内容状态 + CreatedAt string `json:"created_at,omitempty"` } // HandleReportRequest 处理举报请求 type HandleReportRequest struct { Action string `json:"action" binding:"required,oneof=approve reject"` // approve: 确认违规, reject: 驳回 - Result string `json:"result" binding:"max=500"` // 处理结果说明 + Result string `json:"result" binding:"max=500"` // 处理结果说明 } // BatchHandleReportRequest 批量处理举报请求 type BatchHandleReportRequest struct { IDs []string `json:"ids" binding:"required,min=1,max=100"` Action string `json:"action" binding:"required,oneof=approve reject"` - Result string `json:"result" binding:"max=500"` + Result string `json:"result" binding:"max=500"` } // AdminBatchHandleResponse 批量处理响应 type AdminBatchHandleResponse struct { SuccessCount int `json:"success_count"` - FailedCount int `json:"failed_count"` - FailedIDs []string `json:"failed_ids,omitempty"` + FailedCount int `json:"failed_count"` + FailedIDs []string `json:"failed_ids,omitempty"` } // ==================== 转换函数 ==================== - // ConvertReportToResponse 将 Report 模型转换为响应 func ConvertReportToResponse(report *model.Report) *ReportResponse { return &ReportResponse{ @@ -180,4 +168,4 @@ func ConvertReportToAdminDetailResponse(report *model.Report, reporter *model.Us resp.HandledAt = &handledAt } return resp -} \ No newline at end of file +} diff --git a/internal/handler/admin_log_handler.go b/internal/handler/admin_log_handler.go index ba2e233..9ad13d8 100644 --- a/internal/handler/admin_log_handler.go +++ b/internal/handler/admin_log_handler.go @@ -4,8 +4,8 @@ import ( "strconv" "time" - "with_you/internal/dto" "with_you/internal/pkg/response" + "with_you/internal/query" "with_you/internal/service" "github.com/gin-gonic/gin" @@ -37,7 +37,7 @@ func (h *AdminLogHandler) GetOperationLogs(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - filters := dto.LogFilter{ + filters := query.LogFilter{ UserID: c.Query("user_id"), Operation: c.Query("operation"), TargetType: c.Query("target_type"), @@ -70,7 +70,7 @@ func (h *AdminLogHandler) GetLoginLogs(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - filters := dto.LoginFilter{ + filters := query.LoginFilter{ UserID: c.Query("user_id"), Event: c.Query("event"), Result: c.Query("result"), @@ -113,7 +113,7 @@ func (h *AdminLogHandler) GetDataChangeLogs(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - filters := dto.DataChangeFilter{ + filters := query.DataChangeFilter{ UserID: c.Query("user_id"), OperatorID: c.Query("operator_id"), ChangeType: c.Query("change_type"), @@ -190,7 +190,7 @@ func (h *AdminLogHandler) ExportLogs(c *gin.Context) { "logs": logs, }) case "login": - logs, _, err := h.loginLogService.GetLoginLogs(c.Request.Context(), dto.LoginFilter{}, 1, 10000) + logs, _, err := h.loginLogService.GetLoginLogs(c.Request.Context(), query.LoginFilter{}, 1, 10000) if err != nil { response.HandleError(c, err, "failed to export login logs") return @@ -200,7 +200,7 @@ func (h *AdminLogHandler) ExportLogs(c *gin.Context) { "log": logs, }) case "data_change": - logs, _, err := h.dataChangeLogService.GetDataChangeLogs(c.Request.Context(), dto.DataChangeFilter{}, 1, 10000) + logs, _, err := h.dataChangeLogService.GetDataChangeLogs(c.Request.Context(), query.DataChangeFilter{}, 1, 10000) if err != nil { response.HandleError(c, err, "failed to export data change logs") return diff --git a/internal/handler/admin_report_handler.go b/internal/handler/admin_report_handler.go index b418b30..12a444c 100644 --- a/internal/handler/admin_report_handler.go +++ b/internal/handler/admin_report_handler.go @@ -3,6 +3,7 @@ package handler import ( "with_you/internal/dto" "with_you/internal/pkg/response" + "with_you/internal/query" "with_you/internal/service" "github.com/gin-gonic/gin" @@ -24,7 +25,7 @@ func NewAdminReportHandler(adminReportService service.AdminReportService) *Admin // List 获取举报列表 func (h *AdminReportHandler) List(c *gin.Context) { // 解析查询参数 - var query dto.AdminReportListQuery + var query query.AdminReportListQuery if err := c.ShouldBindQuery(&query); err != nil { response.BadRequest(c, "参数错误: "+err.Error()) return @@ -136,4 +137,3 @@ func (h *AdminReportHandler) BatchHandle(c *gin.Context) { response.Success(c, result) } - diff --git a/internal/handler/admin_verification_handler.go b/internal/handler/admin_verification_handler.go index 8528b59..725b3f5 100644 --- a/internal/handler/admin_verification_handler.go +++ b/internal/handler/admin_verification_handler.go @@ -1,12 +1,12 @@ package handler import ( + "context" "strconv" "with_you/internal/dto" "with_you/internal/model" "with_you/internal/pkg/response" - "with_you/internal/repository" "with_you/internal/service" "github.com/gin-gonic/gin" @@ -14,16 +14,16 @@ import ( type AdminVerificationHandler struct { adminVerificationService service.AdminVerificationService - userRepo repository.UserRepository + userService service.UserService } func NewAdminVerificationHandler( adminVerificationService service.AdminVerificationService, - userRepo repository.UserRepository, + userService service.UserService, ) *AdminVerificationHandler { return &AdminVerificationHandler{ adminVerificationService: adminVerificationService, - userRepo: userRepo, + userService: userService, } } @@ -55,7 +55,7 @@ func (h *AdminVerificationHandler) ListVerifications(c *gin.Context) { responses := make([]*dto.AdminVerificationListResponse, len(records)) for i, record := range records { - responses[i] = h.convertToAdminListResponse(record) + responses[i] = h.convertToAdminListResponse(c.Request.Context(), record) } response.Paginated(c, responses, total, page, pageSize) @@ -74,7 +74,7 @@ func (h *AdminVerificationHandler) GetVerificationDetail(c *gin.Context) { return } - response.Success(c, h.convertToAdminDetailResponse(record)) + response.Success(c, h.convertToAdminDetailResponse(c.Request.Context(), record)) } func (h *AdminVerificationHandler) ReviewVerification(c *gin.Context) { @@ -113,10 +113,10 @@ func (h *AdminVerificationHandler) ReviewVerification(c *gin.Context) { return } - response.Success(c, h.convertToAdminDetailResponse(record)) + response.Success(c, h.convertToAdminDetailResponse(c.Request.Context(), record)) } -func (h *AdminVerificationHandler) convertToAdminListResponse(record *model.VerificationRecord) *dto.AdminVerificationListResponse { +func (h *AdminVerificationHandler) convertToAdminListResponse(ctx context.Context, record *model.VerificationRecord) *dto.AdminVerificationListResponse { resp := &dto.AdminVerificationListResponse{ ID: record.ID, UserID: record.UserID, @@ -129,7 +129,7 @@ func (h *AdminVerificationHandler) convertToAdminListResponse(record *model.Veri CreatedAt: dto.FormatTime(record.CreatedAt), } - user, err := h.userRepo.GetByID(record.UserID) + user, err := h.userService.GetUserByID(ctx, record.UserID) if err == nil && user != nil { resp.Username = user.Username resp.Nickname = user.Nickname @@ -139,7 +139,7 @@ func (h *AdminVerificationHandler) convertToAdminListResponse(record *model.Veri if record.ReviewedAt != nil { resp.ReviewedAt = dto.FormatTime(*record.ReviewedAt) if record.ReviewedBy != nil { - reviewer, err := h.userRepo.GetByID(*record.ReviewedBy) + reviewer, err := h.userService.GetUserByID(ctx, *record.ReviewedBy) if err == nil && reviewer != nil { resp.ReviewerName = reviewer.Nickname } @@ -149,7 +149,7 @@ func (h *AdminVerificationHandler) convertToAdminListResponse(record *model.Veri return resp } -func (h *AdminVerificationHandler) convertToAdminDetailResponse(record *model.VerificationRecord) *dto.AdminVerificationDetailResponse { +func (h *AdminVerificationHandler) convertToAdminDetailResponse(ctx context.Context, record *model.VerificationRecord) *dto.AdminVerificationDetailResponse { resp := &dto.AdminVerificationDetailResponse{ ID: record.ID, UserID: record.UserID, @@ -163,7 +163,7 @@ func (h *AdminVerificationHandler) convertToAdminDetailResponse(record *model.Ve CreatedAt: dto.FormatTime(record.CreatedAt), } - user, err := h.userRepo.GetByID(record.UserID) + user, err := h.userService.GetUserByID(ctx, record.UserID) if err == nil && user != nil { resp.Username = user.Username resp.Nickname = user.Nickname @@ -175,7 +175,7 @@ func (h *AdminVerificationHandler) convertToAdminDetailResponse(record *model.Ve } if record.ReviewedBy != nil { resp.ReviewedBy = *record.ReviewedBy - if reviewer, err := h.userRepo.GetByID(*record.ReviewedBy); err == nil && reviewer != nil { + if reviewer, err := h.userService.GetUserByID(ctx, *record.ReviewedBy); err == nil && reviewer != nil { resp.ReviewerName = reviewer.Nickname } } diff --git a/internal/handler/comment_handler.go b/internal/handler/comment_handler.go index e2c4087..4de47a6 100644 --- a/internal/handler/comment_handler.go +++ b/internal/handler/comment_handler.go @@ -15,11 +15,11 @@ import ( // CommentHandler 评论处理器 type CommentHandler struct { - commentService *service.CommentService + commentService service.CommentService } // NewCommentHandler 创建评论处理器 -func NewCommentHandler(commentService *service.CommentService) *CommentHandler { +func NewCommentHandler(commentService service.CommentService) *CommentHandler { return &CommentHandler{ commentService: commentService, } diff --git a/internal/handler/empty_classroom_handler.go b/internal/handler/empty_classroom_handler.go index 59adda3..fcd66be 100644 --- a/internal/handler/empty_classroom_handler.go +++ b/internal/handler/empty_classroom_handler.go @@ -4,22 +4,18 @@ import ( "github.com/gin-gonic/gin" "with_you/internal/pkg/response" - "with_you/internal/repository" "with_you/internal/service" ) type EmptyClassroomHandler struct { classroomSyncService service.EmptyClassroomSyncService - classroomRepo repository.EmptyClassroomRepository } func NewEmptyClassroomHandler( classroomSyncService service.EmptyClassroomSyncService, - classroomRepo repository.EmptyClassroomRepository, ) *EmptyClassroomHandler { return &EmptyClassroomHandler{ classroomSyncService: classroomSyncService, - classroomRepo: classroomRepo, } } @@ -77,7 +73,7 @@ func (h *EmptyClassroomHandler) ListEmptyClassrooms(c *gin.Context) { return } - classrooms, err := h.classroomRepo.ListByUserAndSemester(userID, semester) + classrooms, err := h.classroomSyncService.ListClassrooms(userID, semester) if err != nil { response.HandleError(c, err, "failed to list empty classrooms") return diff --git a/internal/handler/exam_handler.go b/internal/handler/exam_handler.go index 6bc20e3..5a057fa 100644 --- a/internal/handler/exam_handler.go +++ b/internal/handler/exam_handler.go @@ -4,19 +4,16 @@ import ( "github.com/gin-gonic/gin" "with_you/internal/pkg/response" - "with_you/internal/repository" "with_you/internal/service" ) type ExamHandler struct { examSyncService service.ExamSyncService - examRepo repository.ExamRepository } -func NewExamHandler(examSyncService service.ExamSyncService, examRepo repository.ExamRepository) *ExamHandler { +func NewExamHandler(examSyncService service.ExamSyncService) *ExamHandler { return &ExamHandler{ examSyncService: examSyncService, - examRepo: examRepo, } } @@ -68,7 +65,7 @@ func (h *ExamHandler) ListExams(c *gin.Context) { return } - exams, err := h.examRepo.ListByUserAndSemester(userID, semester) + exams, err := h.examSyncService.ListExams(userID, semester) if err != nil { response.HandleError(c, err, "failed to list exams") return diff --git a/internal/handler/grade_handler.go b/internal/handler/grade_handler.go index 8e5e442..f41178e 100644 --- a/internal/handler/grade_handler.go +++ b/internal/handler/grade_handler.go @@ -4,19 +4,16 @@ import ( "github.com/gin-gonic/gin" "with_you/internal/pkg/response" - "with_you/internal/repository" "with_you/internal/service" ) type GradeHandler struct { gradeSyncService service.GradeSyncService - gradeRepo repository.GradeRepository } -func NewGradeHandler(gradeSyncService service.GradeSyncService, gradeRepo repository.GradeRepository) *GradeHandler { +func NewGradeHandler(gradeSyncService service.GradeSyncService) *GradeHandler { return &GradeHandler{ gradeSyncService: gradeSyncService, - gradeRepo: gradeRepo, } } @@ -63,16 +60,14 @@ func (h *GradeHandler) ListGrades(c *gin.Context) { return } - grades, err := h.gradeRepo.ListByUser(userID) + summary, err := h.gradeSyncService.ListGradesWithSummary(userID) if err != nil { response.HandleError(c, err, "failed to list grades") return } - gpaSummary, _ := h.gradeRepo.GetLatestGpaSummary(userID) - response.Success(c, gin.H{ - "grades": grades, - "gpa_summary": gpaSummary, + "grades": summary.Grades, + "gpa_summary": summary.GpaSummary, }) } diff --git a/internal/handler/livekit_handler.go b/internal/handler/livekit_handler.go index fd73976..0762bfc 100644 --- a/internal/handler/livekit_handler.go +++ b/internal/handler/livekit_handler.go @@ -34,7 +34,7 @@ func NewLiveKitHandler( liveKitService: liveKitService, callService: callService, config: &cfg.LiveKit, - logger: logger, + logger: logger, } } @@ -163,4 +163,4 @@ func (h *LiveKitHandler) handleRoomFinished(ctx context.Context, event *livekit. zap.Error(err), ) } -} \ No newline at end of file +} diff --git a/internal/handler/material_handler.go b/internal/handler/material_handler.go index 052d3f2..994e77f 100644 --- a/internal/handler/material_handler.go +++ b/internal/handler/material_handler.go @@ -4,9 +4,9 @@ import ( "errors" "strconv" - "with_you/internal/dto" "with_you/internal/model" "with_you/internal/pkg/response" + "with_you/internal/query" "with_you/internal/service" "github.com/gin-gonic/gin" @@ -148,7 +148,7 @@ func (h *MaterialHandler) ListMaterials(c *gin.Context) { fileType := c.Query("file_type") keyword := c.Query("keyword") - params := dto.MaterialFileQueryParams{ + params := query.MaterialFileQueryParams{ SubjectID: subjectID, FileType: fileType, Keyword: keyword, @@ -329,7 +329,7 @@ func (h *MaterialHandler) AdminListMaterials(c *gin.Context) { status := c.Query("status") keyword := c.Query("keyword") - params := dto.MaterialFileQueryParams{ + params := query.MaterialFileQueryParams{ SubjectID: subjectID, FileType: fileType, Status: status, diff --git a/internal/handler/message_handler.go b/internal/handler/message_handler.go index a1906ad..1ca83f8 100644 --- a/internal/handler/message_handler.go +++ b/internal/handler/message_handler.go @@ -87,14 +87,14 @@ func (h *MessageHandler) enrichConversations(ctx context.Context, convs []*model // MessageHandler 消息处理器 type MessageHandler struct { chatService service.ChatService - messageService *service.MessageService + messageService service.MessageService userService service.UserService groupService service.GroupService wsPublisher ws.MessagePublisher } // NewMessageHandler 创建消息处理器 -func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService service.UserService, groupService service.GroupService, wsPublisher ws.MessagePublisher) *MessageHandler { +func NewMessageHandler(chatService service.ChatService, messageService service.MessageService, userService service.UserService, groupService service.GroupService, wsPublisher ws.MessagePublisher) *MessageHandler { return &MessageHandler{ chatService: chatService, messageService: messageService, diff --git a/internal/handler/notification_handler.go b/internal/handler/notification_handler.go index a7839dd..84e5b67 100644 --- a/internal/handler/notification_handler.go +++ b/internal/handler/notification_handler.go @@ -13,11 +13,11 @@ import ( // NotificationHandler 通知处理器 type NotificationHandler struct { - notificationService *service.NotificationService + notificationService service.NotificationService } // NewNotificationHandler 创建通知处理器 -func NewNotificationHandler(notificationService *service.NotificationService) *NotificationHandler { +func NewNotificationHandler(notificationService service.NotificationService) *NotificationHandler { return &NotificationHandler{ notificationService: notificationService, } diff --git a/internal/handler/qrcode_handler.go b/internal/handler/qrcode_handler.go index dae209d..b991c9b 100644 --- a/internal/handler/qrcode_handler.go +++ b/internal/handler/qrcode_handler.go @@ -14,11 +14,11 @@ import ( // QRCodeHandler 二维码登录处理器 type QRCodeHandler struct { - qrcodeService *service.QRCodeLoginService + qrcodeService service.QRCodeLoginService } // NewQRCodeHandler 创建二维码登录处理器 -func NewQRCodeHandler(qrcodeService *service.QRCodeLoginService) *QRCodeHandler { +func NewQRCodeHandler(qrcodeService service.QRCodeLoginService) *QRCodeHandler { return &QRCodeHandler{ qrcodeService: qrcodeService, } diff --git a/internal/handler/upload_handler.go b/internal/handler/upload_handler.go index be95b87..fdf22f4 100644 --- a/internal/handler/upload_handler.go +++ b/internal/handler/upload_handler.go @@ -9,11 +9,11 @@ import ( // UploadHandler 上传处理器 type UploadHandler struct { - uploadService *service.UploadService + uploadService service.UploadService } // NewUploadHandler 创建上传处理器 -func NewUploadHandler(uploadService *service.UploadService) *UploadHandler { +func NewUploadHandler(uploadService service.UploadService) *UploadHandler { return &UploadHandler{ uploadService: uploadService, } diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index 0568bde..2dacf27 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -36,7 +36,7 @@ type UserHandler struct { userService service.UserService activityService service.UserActivityService profileAuditService service.UserProfileAuditService - jwtService *service.JWTService + jwtService service.JWTService logService *service.LogService } @@ -53,7 +53,7 @@ func (h *UserHandler) SetProfileAuditService(profileAuditService service.UserPro } // SetJWTService 设置JWT服务 -func (h *UserHandler) SetJWTService(jwtService *service.JWTService) { +func (h *UserHandler) SetJWTService(jwtService service.JWTService) { h.jwtService = jwtService } diff --git a/internal/handler/vote_handler.go b/internal/handler/vote_handler.go index 7a31485..8488196 100644 --- a/internal/handler/vote_handler.go +++ b/internal/handler/vote_handler.go @@ -13,12 +13,12 @@ import ( // VoteHandler 投票处理器 type VoteHandler struct { - voteService *service.VoteService + voteService service.VoteService postService service.PostService } // NewVoteHandler 创建投票处理器 -func NewVoteHandler(voteService *service.VoteService, postService service.PostService) *VoteHandler { +func NewVoteHandler(voteService service.VoteService, postService service.PostService) *VoteHandler { return &VoteHandler{ voteService: voteService, postService: postService, diff --git a/internal/handler/ws_handler.go b/internal/handler/ws_handler.go index d4919a8..840df5d 100644 --- a/internal/handler/ws_handler.go +++ b/internal/handler/ws_handler.go @@ -19,7 +19,6 @@ import ( "with_you/internal/model" "with_you/internal/pkg/response" "with_you/internal/pkg/ws" - "with_you/internal/repository" "with_you/internal/service" ) @@ -69,9 +68,9 @@ type WSHandler struct { publisher ws.MessagePublisher chatService service.ChatService groupService service.GroupService - jwtService *service.JWTService + jwtService service.JWTService callService service.CallService - userRepo repository.UserRepository + userService service.UserService clientSeq uint64 } @@ -92,9 +91,9 @@ func NewWSHandler( publisher ws.MessagePublisher, chatService service.ChatService, groupService service.GroupService, - jwtService *service.JWTService, + jwtService service.JWTService, callService service.CallService, - userRepo repository.UserRepository, + userService service.UserService, ) *WSHandler { return &WSHandler{ publisher: publisher, @@ -102,7 +101,7 @@ func NewWSHandler( groupService: groupService, jwtService: jwtService, callService: callService, - userRepo: userRepo, + userService: userService, } } @@ -578,7 +577,7 @@ func (h *WSHandler) handleAck(client *ws.Client, payload json.RawMessage) { // isVerified 检查用户是否已通过身份认证 func (h *WSHandler) isVerified(ctx context.Context, client *ws.Client) bool { - user, err := h.userRepo.GetByID(client.UserID) + user, err := h.userService.GetUserByID(ctx, client.UserID) if err != nil { h.publisher.SendError(client, "internal_error", "获取用户信息失败") return false diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index bb39b0e..5fe5e1e 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -10,7 +10,7 @@ import ( ) // Auth 认证中间件 -func Auth(jwtService *service.JWTService) gin.HandlerFunc { +func Auth(jwtService service.JWTService) gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") @@ -45,7 +45,7 @@ func Auth(jwtService *service.JWTService) gin.HandlerFunc { } // OptionalAuth 可选认证中间件 -func OptionalAuth(jwtService *service.JWTService) gin.HandlerFunc { +func OptionalAuth(jwtService service.JWTService) gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { diff --git a/internal/middleware/casbin.go b/internal/middleware/casbin.go index 8aaa8e3..fc7da1b 100644 --- a/internal/middleware/casbin.go +++ b/internal/middleware/casbin.go @@ -9,63 +9,6 @@ import ( "github.com/gin-gonic/gin" ) -// CasbinAuth Casbin 权限中间件 -// 需要在 Auth 中间件之后使用 -func CasbinAuth(casbinService service.CasbinService) gin.HandlerFunc { - return func(c *gin.Context) { - // 获取请求路径和方法 - path := c.Request.URL.Path - method := c.Request.Method - - // 从上下文获取用户ID (由 Auth 中间件设置) - userID, exists := c.Get("user_id") - - // 如果用户未登录,检查是否是公开路由 - if !exists { - // 对于未登录用户,使用匿名角色检查权限 - allowed, err := casbinService.Enforce(c.Request.Context(), "anonymous", path, method) - if err != nil { - c.AbortWithStatusJSON(500, gin.H{ - "code": "INTERNAL_ERROR", - "message": "权限检查失败", - }) - return - } - - if !allowed { - c.AbortWithStatusJSON(401, gin.H{ - "code": "UNAUTHORIZED", - "message": "请先登录", - }) - return - } - - c.Next() - return - } - - // 检查用户权限 - allowed, err := casbinService.EnforceForUser(c.Request.Context(), userID.(string), path, method) - if err != nil { - c.AbortWithStatusJSON(500, gin.H{ - "code": "INTERNAL_ERROR", - "message": "权限检查失败", - }) - return - } - - if !allowed { - c.AbortWithStatusJSON(403, gin.H{ - "code": "FORBIDDEN", - "message": "权限不足", - }) - return - } - - c.Next() - } -} - // RequireRole 要求特定角色的中间件 func RequireRole(casbinService service.CasbinService, requiredRoles ...string) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/internal/middleware/verification.go b/internal/middleware/verification.go index 99f1a2f..a16ec80 100644 --- a/internal/middleware/verification.go +++ b/internal/middleware/verification.go @@ -1,16 +1,20 @@ -package middleware +package middleware import ( "net/http" "with_you/internal/model" "with_you/internal/pkg/response" - "with_you/internal/repository" + "with_you/internal/service" "github.com/gin-gonic/gin" ) -func RequireVerified(userRepo repository.UserRepository) gin.HandlerFunc { +// RequireVerified 要求用户已通过身份认证。 +// 通过 service.UserService 查询用户(而非直接访问 repository),保持分层一致: +// +// middleware → service → repository +func RequireVerified(userService service.UserService) gin.HandlerFunc { return func(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { @@ -19,7 +23,7 @@ func RequireVerified(userRepo repository.UserRepository) gin.HandlerFunc { return } - user, err := userRepo.GetByID(userID.(string)) + user, err := userService.GetUserByID(c.Request.Context(), userID.(string)) if err != nil { response.InternalServerError(c, "获取用户信息失败") c.Abort() diff --git a/internal/middleware/verification_test.go b/internal/middleware/verification_test.go new file mode 100644 index 0000000..54c00a8 --- /dev/null +++ b/internal/middleware/verification_test.go @@ -0,0 +1,110 @@ +package middleware + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "with_you/internal/model" + "with_you/internal/service" + + "github.com/gin-gonic/gin" +) + +// stubUserService 通过嵌入 service.UserService 接口(nil)仅覆盖 GetUserByID。 +// 未覆盖方法若被调用会 panic,可在测试中暴露非预期的依赖调用。 +type stubUserService struct { + service.UserService // nil 嵌入 + + user *model.User + err error +} + +func (s *stubUserService) GetUserByID(ctx context.Context, id string) (*model.User, error) { + if s.err != nil { + return nil, s.err + } + return s.user, nil +} + +// runMiddleware 构造一个 gin 引擎,挂载 RequireVerified + 一个标记 handler, +// 发起请求并返回 (status, body, nextCalled)。 +func runMiddleware(t *testing.T, svc service.UserService, setUserID bool, userID any) (int, bool) { + t.Helper() + gin.SetMode(gin.TestMode) + + nextCalled := false + r := gin.New() + r.Use(func(c *gin.Context) { + if setUserID { + c.Set("user_id", userID) + } + c.Next() + }) + r.GET("/test", RequireVerified(svc), func(c *gin.Context) { + nextCalled = true + c.JSON(http.StatusOK, gin.H{"ok": true}) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + r.ServeHTTP(w, req) + + return w.Code, nextCalled +} + +// TestRequireVerified_Approved 已认证用户 → 放行(200,next 被调用)。 +func TestRequireVerified_Approved(t *testing.T) { + svc := &stubUserService{ + user: &model.User{ID: "u1", VerificationStatus: model.VerificationStatusApproved}, + } + code, next := runMiddleware(t, svc, true, "u1") + if code != http.StatusOK { + t.Errorf("status = %d, want %d", code, http.StatusOK) + } + if !next { + t.Error("next handler should be called for verified user") + } +} + +// TestRequireVerified_NotApproved 未通过认证用户 → 403 VERIFICATION_REQUIRED。 +func TestRequireVerified_NotApproved(t *testing.T) { + svc := &stubUserService{ + user: &model.User{ID: "u1", VerificationStatus: model.VerificationStatusPending}, + } + code, next := runMiddleware(t, svc, true, "u1") + if code != http.StatusForbidden { + t.Errorf("status = %d, want %d", code, http.StatusForbidden) + } + if next { + t.Error("next handler should NOT be called for unverified user") + } +} + +// TestRequireVerified_UserLookupError 查询用户失败 → 500。 +func TestRequireVerified_UserLookupError(t *testing.T) { + svc := &stubUserService{err: errors.New("db down")} + code, next := runMiddleware(t, svc, true, "u1") + if code != http.StatusInternalServerError { + t.Errorf("status = %d, want %d", code, http.StatusInternalServerError) + } + if next { + t.Error("next handler should NOT be called on lookup error") + } +} + +// TestRequireVerified_NoUserID 未登录(无 user_id)→ 401。 +func TestRequireVerified_NoUserID(t *testing.T) { + svc := &stubUserService{ + user: &model.User{ID: "u1", VerificationStatus: model.VerificationStatusApproved}, + } + code, next := runMiddleware(t, svc, false, nil) + if code != http.StatusUnauthorized { + t.Errorf("status = %d, want %d", code, http.StatusUnauthorized) + } + if next { + t.Error("next handler should NOT be called when user_id missing") + } +} diff --git a/internal/model/message.go b/internal/model/message.go index e299bf8..865d239 100644 --- a/internal/model/message.go +++ b/internal/model/message.go @@ -1,9 +1,8 @@ -package model +package model import ( "database/sql/driver" "encoding/json" - "sync" "time" "gorm.io/gorm" @@ -303,9 +302,14 @@ func (m *Message) IsInteractionNotification() bool { } } - -// BatchDecryptMessagesParallel 使用 worker pool 并行解密 -// 这是一个更高效的版本,适用于大量消息 +// BatchDecryptMessagesParallel 批量解密消息段。 +// 并发逻辑收敛在 crypto.MessageEncryptor.BatchDecrypt(worker pool), +// 本函数仅负责「提取密文 → 回填明文到 Message.Segments」,不再自起 goroutine。 +// +// 行为保持与历史版本一致: +// - 加密器未初始化时,降级为明文 JSON 直接解析(兼容旧数据); +// - 单条解密失败时回退尝试直接 JSON 解析(兼容未加密旧数据); +// - 已解密或无加密内容的消息跳过。 func BatchDecryptMessagesParallel(messages []*Message) { if len(messages) == 0 { return @@ -313,7 +317,7 @@ func BatchDecryptMessagesParallel(messages []*Message) { encryptor := crypto.GetMessageEncryptor() if encryptor == nil { - // 加密器未初始化,串行解析 + // 加密器未初始化,串行解析为明文 JSON(兼容模式) for _, m := range messages { if !m.decrypted && m.SegmentsEncrypted != "" { _ = json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments) @@ -323,46 +327,34 @@ func BatchDecryptMessagesParallel(messages []*Message) { return } - // 确定并行度 - workerCount := 4 - if len(messages) < 20 { - workerCount = 2 - } else if len(messages) > 100 { - workerCount = 8 + // 提取待解密的密文索引与值 + indices := make([]int, 0, len(messages)) + ciphertexts := make([]string, 0, len(messages)) + for i, m := range messages { + if m.decrypted || m.SegmentsEncrypted == "" { + m.decrypted = true + continue + } + indices = append(indices, i) + ciphertexts = append(ciphertexts, m.SegmentsEncrypted) + } + if len(ciphertexts) == 0 { + return } - jobs := make(chan int, len(messages)) - var wg sync.WaitGroup + // 并发解密(worker pool 由 crypto.BatchDecrypt 内部管理) + plaintexts := encryptor.BatchDecrypt(ciphertexts, 0) - // 启动 workers - for w := 0; w < workerCount; w++ { - wg.Add(1) - go func() { - defer wg.Done() - for i := range jobs { - m := messages[i] - if m.decrypted || m.SegmentsEncrypted == "" { - m.decrypted = true - continue - } - - plaintext, err := encryptor.Decrypt(m.SegmentsEncrypted) - if err != nil { - // 尝试直接解析 - _ = json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments) - } else { - _ = json.Unmarshal(plaintext, &m.Segments) - } - m.decrypted = true - } - }() + // 回填明文;解密失败的位置回退直接 JSON 解析 + for j, idx := range indices { + m := messages[idx] + pt := plaintexts[j] + if pt == nil { + // 解密失败,兼容未加密的旧数据 + _ = json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments) + } else { + _ = json.Unmarshal(pt, &m.Segments) + } + m.decrypted = true } - - // 分发任务 - for i := range messages { - jobs <- i - } - close(jobs) - - wg.Wait() } diff --git a/internal/model/message_crypto_test.go b/internal/model/message_crypto_test.go new file mode 100644 index 0000000..5a86205 --- /dev/null +++ b/internal/model/message_crypto_test.go @@ -0,0 +1,140 @@ +package model + +import ( + "encoding/json" + "testing" + + "with_you/internal/pkg/crypto" +) + +const testKey = "12345678901234567890123456789012" + +// initEnc 初始化(幂等,因 crypto 用 sync.Once)加密器。 +func initEnc(t *testing.T) { + t.Helper() + if err := crypto.InitMessageEncryptor(testKey, 1); err != nil { + t.Fatalf("InitMessageEncryptor: %v", err) + } + if crypto.GetMessageEncryptor() == nil { + t.Fatal("encryptor not initialized") + } +} + +// TestMessage_EncryptDecryptRoundTrip 验证:BeforeCreate 加密落库 → +// 清空内存 Segments → Decrypt 还原 → 内容与原文一致。 +func TestMessage_EncryptDecryptRoundTrip(t *testing.T) { + initEnc(t) + + original := MessageSegments{ + {Type: "text", Data: MessageSegmentData{"content": "你好世界"}}, + {Type: "image", Data: MessageSegmentData{"url": "https://example.com/a.png"}}, + } + + msg := &Message{Segments: original} + + // 模拟 GORM 创建钩子:加密 + if err := msg.BeforeCreate(nil); err != nil { + t.Fatalf("BeforeCreate (encrypt) failed: %v", err) + } + + // 加密后应写入 SegmentsEncrypted,且不应是明文 JSON(除非降级) + if msg.SegmentsEncrypted == "" { + t.Fatal("SegmentsEncrypted should be populated after BeforeCreate") + } + // 校验确实加密了:明文 JSON 中应包含 "content",密文 base64 中不应直接出现该明文 + if contains(msg.SegmentsEncrypted, "你好世界") { + t.Errorf("SegmentsEncrypted appears to contain plaintext: %q", msg.SegmentsEncrypted) + } + if msg.SegmentsKeyVersion != crypto.GetMessageEncryptor().GetKeyVersion() { + t.Errorf("SegmentsKeyVersion = %d, want %d", + msg.SegmentsKeyVersion, crypto.GetMessageEncryptor().GetKeyVersion()) + } + + // 模拟从 DB 取回后清空内存段(仅 SegmentsEncrypted 落库) + msg.Segments = nil + msg.decrypted = false + + // 解密还原 + if err := msg.Decrypt(); err != nil { + t.Fatalf("Decrypt failed: %v", err) + } + + if len(msg.Segments) != len(original) { + t.Fatalf("after Decrypt, len(Segments) = %d, want %d", len(msg.Segments), len(original)) + } + for i, seg := range msg.Segments { + if seg.Type != original[i].Type { + t.Errorf("segment[%d].Type = %q, want %q", i, seg.Type, original[i].Type) + } + if seg.Data["content"] != original[i].Data["content"] { + t.Errorf("segment[%d].content mismatch: got %v, want %v", + i, seg.Data["content"], original[i].Data["content"]) + } + } +} + +// TestMessage_EncryptEmptySegments 验证:空 Segments 时加密为 no-op。 +func TestMessage_EncryptEmptySegments(t *testing.T) { + initEnc(t) + + msg := &Message{} + if err := msg.BeforeCreate(nil); err != nil { + t.Fatalf("BeforeCreate failed: %v", err) + } + if msg.SegmentsEncrypted != "" { + t.Errorf("empty Segments should not produce ciphertext, got %q", msg.SegmentsEncrypted) + } +} + +// TestBatchDecryptMessagesParallel_RoundTrip 验证批量解密路径与单条一致。 +func TestBatchDecryptMessagesParallel_RoundTrip(t *testing.T) { + initEnc(t) + + contents := []string{"消息A", "消息B", "消息C"} + msgs := make([]*Message, len(contents)) + for i, c := range contents { + m := &Message{ + Segments: MessageSegments{ + {Type: "text", Data: MessageSegmentData{"content": c}}, + }, + } + if err := m.BeforeCreate(nil); err != nil { + t.Fatalf("BeforeCreate[%d]: %v", i, err) + } + m.Segments = nil // 模拟从 DB 取回 + msgs[i] = m + } + + BatchDecryptMessagesParallel(msgs) + + for i, m := range msgs { + if !m.decrypted { + t.Errorf("msg[%d] not marked decrypted", i) + } + if len(m.Segments) != 1 { + t.Errorf("msg[%d] segments len = %d, want 1", i, len(m.Segments)) + continue + } + if got := m.Segments[0].Data["content"]; got != contents[i] { + t.Errorf("msg[%d].content = %v, want %q", i, got, contents[i]) + } + } +} + +// contains 简单子串判断(避免引入 strings 仅为此)。 +func contains(s, sub string) bool { + return len(s) >= len(sub) && (indexOf(s, sub) >= 0) +} + +func indexOf(s, sub string) int { + bs := []byte(s) + for i := 0; i+len(sub) <= len(bs); i++ { + if string(bs[i:i+len(sub)]) == sub { + return i + } + } + return -1 +} + +// 编译期保证 json 被使用(兼容路径用到)。 +var _ = json.Marshal diff --git a/internal/pkg/avatar/avatar.go b/internal/pkg/avatar/avatar.go deleted file mode 100644 index 6e0eba5..0000000 --- a/internal/pkg/avatar/avatar.go +++ /dev/null @@ -1,115 +0,0 @@ -package avatar - -import ( - "encoding/base64" - "fmt" - "unicode/utf8" -) - -// 预定义一组好看的颜色 -var colors = []string{ - "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", - "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F", - "#BB8FCE", "#85C1E9", "#F8B500", "#00CED1", - "#E74C3C", "#3498DB", "#2ECC71", "#9B59B6", - "#1ABC9C", "#F39C12", "#E67E22", "#16A085", -} - -// SVG模板 -const svgTemplate = ` - - %s -` - -// GenerateSVGAvatar 根据用户名生成SVG头像 -// username: 用户名 -// size: 头像尺寸(像素) -func GenerateSVGAvatar(username string, size int) string { - initials := getInitials(username) - color := stringToColor(username) - return fmt.Sprintf(svgTemplate, size, size, color, initials) -} - -// GenerateAvatarDataURI 生成Data URI格式的头像 -// 可以直接在HTML img标签或CSS background-image中使用 -func GenerateAvatarDataURI(username string, size int) string { - svg := GenerateSVGAvatar(username, size) - encoded := base64.StdEncoding.EncodeToString([]byte(svg)) - return fmt.Sprintf("data:image/svg+xml;base64,%s", encoded) -} - -// getInitials 获取用户名首字母 -// 中文取第一个字,英文取首字母(最多2个) -func getInitials(username string) string { - if username == "" { - return "?" - } - - // 检查是否是中文字符 - firstRune, _ := utf8.DecodeRuneInString(username) - if isChinese(firstRune) { - // 中文直接返回第一个字符 - return string(firstRune) - } - - // 英文处理:取前两个单词的首字母 - // 例如: "John Doe" -> "JD", "john" -> "J" - result := []rune{} - for i, r := range username { - if i == 0 { - result = append(result, toUpper(r)) - } else if r == ' ' || r == '_' || r == '-' { - // 找到下一个字符作为第二个首字母 - nextIdx := i + 1 - if nextIdx < len(username) { - nextRune, _ := utf8.DecodeRuneInString(username[nextIdx:]) - if nextRune != utf8.RuneError && nextRune != ' ' { - result = append(result, toUpper(nextRune)) - break - } - } - } - } - - if len(result) == 0 { - return "?" - } - - // 最多返回2个字符 - if len(result) > 2 { - result = result[:2] - } - - return string(result) -} - -// isChinese 判断是否是中文字符 -func isChinese(r rune) bool { - return r >= 0x4E00 && r <= 0x9FFF -} - -// toUpper 将字母转换为大写 -func toUpper(r rune) rune { - if r >= 'a' && r <= 'z' { - return r - 32 - } - return r -} - -// stringToColor 根据字符串生成颜色 -// 使用简单的哈希算法确保同一用户名每次生成的颜色一致 -func stringToColor(s string) string { - if s == "" { - return colors[0] - } - - hash := 0 - for _, r := range s { - hash = (hash*31 + int(r)) % len(colors) - } - if hash < 0 { - hash = -hash - } - - return colors[hash%len(colors)] -} diff --git a/internal/pkg/avatar/avatar_test.go b/internal/pkg/avatar/avatar_test.go deleted file mode 100644 index c38d934..0000000 --- a/internal/pkg/avatar/avatar_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package avatar - -import ( - "strings" - "testing" -) - -func TestGetInitials(t *testing.T) { - tests := []struct { - name string - username string - want string - }{ - {"中文用户名", "张三", "张"}, - {"英文用户名", "John", "J"}, - {"英文全名", "John Doe", "JD"}, - {"带下划线", "john_doe", "JD"}, - {"带连字符", "john-doe", "JD"}, - {"空字符串", "", "?"}, - {"小写英文", "alice", "A"}, - {"中文复合", "李小龙", "李"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getInitials(tt.username) - if got != tt.want { - t.Errorf("getInitials(%q) = %q, want %q", tt.username, got, tt.want) - } - }) - } -} - -func TestStringToColor(t *testing.T) { - // 测试同一用户名生成的颜色一致 - color1 := stringToColor("张三") - color2 := stringToColor("张三") - if color1 != color2 { - t.Errorf("stringToColor should return consistent colors for the same input") - } - - // 测试不同用户名生成不同颜色(大概率) - color3 := stringToColor("李四") - if color1 == color3 { - t.Logf("Warning: different usernames generated the same color (possible but unlikely)") - } - - // 测试空字符串 - color4 := stringToColor("") - if color4 == "" { - t.Errorf("stringToColor should return a color for empty string") - } - - // 验证颜色格式 - if !strings.HasPrefix(color4, "#") { - t.Errorf("stringToColor should return hex color format starting with #") - } -} - -func TestGenerateSVGAvatar(t *testing.T) { - svg := GenerateSVGAvatar("张三", 100) - - // 验证SVG结构 - if !strings.Contains(svg, "") { - t.Errorf("SVG should contain tag") - } - if !strings.Contains(svg, "width=\"100\"") { - t.Errorf("SVG should have width=100") - } - if !strings.Contains(svg, "height=\"100\"") { - t.Errorf("SVG should have height=100") - } - if !strings.Contains(svg, "张") { - t.Errorf("SVG should contain the initial character") - } -} - -func TestGenerateAvatarDataURI(t *testing.T) { - dataURI := GenerateAvatarDataURI("张三", 100) - - // 验证Data URI格式 - if !strings.HasPrefix(dataURI, "data:image/svg+xml;base64,") { - t.Errorf("Data URI should start with data:image/svg+xml;base64,") - } - - // 验证base64部分不为空 - parts := strings.Split(dataURI, ",") - if len(parts) != 2 { - t.Errorf("Data URI should have two parts separated by comma") - } - if parts[1] == "" { - t.Errorf("Base64 part should not be empty") - } -} - -func TestIsChinese(t *testing.T) { - tests := []struct { - r rune - want bool - }{ - {'中', true}, - {'文', true}, - {'a', false}, - {'Z', false}, - {'0', false}, - {'_', false}, - } - - for _, tt := range tests { - got := isChinese(tt.r) - if got != tt.want { - t.Errorf("isChinese(%q) = %v, want %v", tt.r, got, tt.want) - } - } -} diff --git a/internal/pkg/crypto/batch_decrypt_test.go b/internal/pkg/crypto/batch_decrypt_test.go index 65bd009..30d3a06 100644 --- a/internal/pkg/crypto/batch_decrypt_test.go +++ b/internal/pkg/crypto/batch_decrypt_test.go @@ -21,36 +21,6 @@ func BenchmarkEncryptDecryptSingle(b *testing.B) { } } -// BenchmarkBatchDecrypt 批量并行解密性能基准 -func BenchmarkBatchDecrypt(b *testing.B) { - key := "12345678901234567890123456789012" - _ = InitMessageEncryptor(key, 1) - encryptor := GetMessageEncryptor() - - // 准备测试数据 - sizes := []int{10, 50, 100, 500} - - for _, size := range sizes { - b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) { - // 生成加密数据 - ciphertexts := make([]string, size) - for i := 0; i < size; i++ { - msg := map[string]interface{}{ - "type": "text", - "data": map[string]string{"content": fmt.Sprintf("消息内容 %d", i)}, - } - plaintext, _ := json.Marshal(msg) - ciphertexts[i], _ = encryptor.Encrypt(plaintext) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = encryptor.BatchDecrypt(ciphertexts, 4) - } - }) - } -} - // BenchmarkSerialDecrypt 串行解密性能基准(对比用) func BenchmarkSerialDecrypt(b *testing.B) { key := "12345678901234567890123456789012" @@ -81,12 +51,41 @@ func BenchmarkSerialDecrypt(b *testing.B) { } } +// BenchmarkBatchDecrypt 批量并行解密性能基准 +func BenchmarkBatchDecrypt(b *testing.B) { + key := "12345678901234567890123456789012" + _ = InitMessageEncryptor(key, 1) + encryptor := GetMessageEncryptor() + + sizes := []int{10, 50, 100, 500} + + for _, size := range sizes { + b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) { + // 生成加密数据 + ciphertexts := make([]string, size) + for i := 0; i < size; i++ { + msg := map[string]interface{}{ + "type": "text", + "data": map[string]string{"content": fmt.Sprintf("消息内容 %d", i)}, + } + plaintext, _ := json.Marshal(msg) + ciphertexts[i], _ = encryptor.Encrypt(plaintext) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = encryptor.BatchDecrypt(ciphertexts, 4) + } + }) + } +} + +// TestBatchDecrypt 验证批量解密与单条解密结果一致 func TestBatchDecrypt(t *testing.T) { key := "12345678901234567890123456789012" _ = InitMessageEncryptor(key, 1) encryptor := GetMessageEncryptor() - // 准备测试数据 count := 100 ciphertexts := make([]string, count) expectedContents := make([]string, count) @@ -105,6 +104,10 @@ func TestBatchDecrypt(t *testing.T) { // 批量解密 results := encryptor.BatchDecrypt(ciphertexts, 4) + if len(results) != count { + t.Fatalf("BatchDecrypt returned %d results, want %d", len(results), count) + } + // 验证结果 for i, result := range results { if result == nil { diff --git a/internal/pkg/crypto/message_encryptor.go b/internal/pkg/crypto/message_encryptor.go index 831fc0d..4ec320c 100644 --- a/internal/pkg/crypto/message_encryptor.go +++ b/internal/pkg/crypto/message_encryptor.go @@ -139,31 +139,70 @@ func (e *MessageEncryptor) GetKeyVersion() int { return e.keyVersion } -// RotateKey 密钥轮换(用于密钥升级) -// 新密钥必须也是32字节 -func (e *MessageEncryptor) RotateKey(newKey string, newVersion int) error { - keyBytes := []byte(newKey) - if len(keyBytes) != 32 { - return ErrInvalidKey +// BatchDecrypt 批量解密多条密文,返回与输入等长的明文字节切片数组。 +// 并发度由 workers 指定(<=0 时按密文数量自适应)。 +// 解密失败或空密文的位置返回 nil,调用方可据此判断。 +// 这是 Decrypt 的无锁读优化版:AEAD 的 Open 不修改内部状态, +// 但仍走 RLock 以兼容未来可能的密钥轮换语义。 +func (e *MessageEncryptor) BatchDecrypt(ciphertexts []string, workers int) [][]byte { + results := make([][]byte, len(ciphertexts)) + if e == nil || len(ciphertexts) == 0 { + return results } - block, err := aes.NewCipher(keyBytes) - if err != nil { - return err + // 自适应并发度:与历史实现保持一致 + if workers <= 0 { + switch { + case len(ciphertexts) < 20: + workers = 2 + case len(ciphertexts) > 100: + workers = 8 + default: + workers = 4 + } + } + if workers > len(ciphertexts) { + workers = len(ciphertexts) } - gcm, err := cipher.NewGCM(block) - if err != nil { - return err + // 预取一次 AEAD,避免 worker 内重复进入 RLock + e.mu.RLock() + gcm := e.gcm + nonceSize := e.gcm.NonceSize() + e.mu.RUnlock() + + jobs := make(chan int, len(ciphertexts)) + var wg sync.WaitGroup + + for w := 0; w < workers; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := range jobs { + ct := ciphertexts[i] + if ct == "" { + continue + } + raw, err := base64.StdEncoding.DecodeString(ct) + if err != nil || len(raw) < nonceSize { + continue + } + nonce := raw[:nonceSize] + actual := raw[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, actual, nil) + if err != nil { + continue + } + results[i] = plaintext + } + }() } - e.mu.Lock() - defer e.mu.Unlock() + for i := range ciphertexts { + jobs <- i + } + close(jobs) + wg.Wait() - e.key = keyBytes - e.gcm = gcm - e.keyVersion = newVersion - - return nil + return results } - diff --git a/internal/pkg/crypto/message_encryptor_test.go b/internal/pkg/crypto/message_encryptor_test.go index b9773bf..c645ac1 100644 --- a/internal/pkg/crypto/message_encryptor_test.go +++ b/internal/pkg/crypto/message_encryptor_test.go @@ -150,16 +150,6 @@ func TestMessageEncryptor_KeyVersion(t *testing.T) { if encryptor.GetKeyVersion() != 1 { t.Errorf("GetKeyVersion() = %d, want 1", encryptor.GetKeyVersion()) } - - // 测试密钥轮换 - err = encryptor.RotateKey("abcdefghijklmnopqrstuvwxyz123456", 2) - if err != nil { - t.Fatal(err) - } - - if encryptor.GetKeyVersion() != 2 { - t.Errorf("GetKeyVersion() after rotation = %d, want 2", encryptor.GetKeyVersion()) - } } func mustMarshal(v interface{}) []byte { diff --git a/internal/query/filters.go b/internal/query/filters.go new file mode 100644 index 0000000..f7747ce --- /dev/null +++ b/internal/query/filters.go @@ -0,0 +1,68 @@ +// Package query 定义跨层共享的查询/过滤参数类型。 +// +// 这些类型被 handler、service、repository 三层共同引用, +// 属于「查询契约」而非「表现层 DTO」。放在独立 query 包可避免: +// - repository 反向依赖 dto(表现层)导致的分层倒置; +// - 将类型放入 repository 后 handler 被迫依赖 repository 的更严重违规。 +package query + +import "time" + +// LogFilter 日志过滤条件 +type LogFilter struct { + UserID string + Operation string + TargetType string + TargetID string + Status string + IP string + StartTime string + EndTime string +} + +// LoginFilter 登录日志过滤条件 +type LoginFilter struct { + UserID string + Event string + Result string + FailReason string + LoginType string + IP string + StartTime time.Time + EndTime time.Time +} + +// DataChangeFilter 数据变更日志过滤条件 +type DataChangeFilter struct { + UserID string + OperatorID string + ChangeType string + TargetType string + OperatorType string + IP string + StartTime string + EndTime string +} + +// MaterialFileQueryParams 学习资料查询参数 +type MaterialFileQueryParams struct { + SubjectID string + FileType string + Status string + Keyword string + Page int + PageSize int + SortBy string + SortOrder string +} + +// AdminReportListQuery 管理端举报列表查询参数 +type AdminReportListQuery struct { + Page int `form:"page" binding:"min=1"` + PageSize int `form:"page_size" binding:"min=1,max=100"` + TargetType string `form:"target_type" binding:"omitempty,oneof=post comment message"` + Status string `form:"status" binding:"omitempty,oneof=pending processing resolved rejected"` + StartDate string `form:"start_date" binding:"omitempty"` + EndDate string `form:"end_date" binding:"omitempty"` + Keyword string `form:"keyword" binding:"omitempty"` +} diff --git a/internal/query/filters_test.go b/internal/query/filters_test.go new file mode 100644 index 0000000..0e64569 --- /dev/null +++ b/internal/query/filters_test.go @@ -0,0 +1,82 @@ +package query + +import ( + "testing" + "time" +) + +// TestLogFilter_ZeroValue 验证 LogFilter 零值与字段可写性。 +func TestLogFilter_ZeroValue(t *testing.T) { + var f LogFilter + if f.UserID != "" || f.Status != "" { + t.Errorf("zero-value LogFilter should have empty string fields, got %+v", f) + } + f.UserID = "u1" + f.Operation = "create" + if f.UserID != "u1" || f.Operation != "create" { + t.Error("LogFilter field assignment failed") + } +} + +// TestLoginFilter_TimeFields 验证 LoginFilter 的时间字段类型正确(迁移后保持 time.Time)。 +func TestLoginFilter_TimeFields(t *testing.T) { + now := time.Now() + f := LoginFilter{UserID: "u1", StartTime: now, EndTime: now.Add(time.Hour)} + if !f.StartTime.Equal(now) { + t.Errorf("StartTime = %v, want %v", f.StartTime, now) + } + if !f.EndTime.After(f.StartTime) { + t.Error("EndTime should be after StartTime") + } +} + +// TestDataChangeFilter_Fields 验证 DataChangeFilter 全字段赋值。 +func TestDataChangeFilter_Fields(t *testing.T) { + f := DataChangeFilter{ + UserID: "u1", + OperatorID: "op1", + ChangeType: "update", + TargetType: "post", + OperatorType: "admin", + IP: "127.0.0.1", + StartTime: "2026-01-01", + EndTime: "2026-01-31", + } + if f.OperatorID != "op1" || f.ChangeType != "update" { + t.Errorf("DataChangeFilter fields not set correctly: %+v", f) + } +} + +// TestMaterialFileQueryParams_Defaults 验证分页字段零值(调用方应处理默认值)。 +func TestMaterialFileQueryParams_Defaults(t *testing.T) { + var p MaterialFileQueryParams + if p.Page != 0 || p.PageSize != 0 { + t.Errorf("zero-value MaterialFileQueryParams should have Page=0, PageSize=0, got Page=%d PageSize=%d", p.Page, p.PageSize) + } + p.Page = 1 + p.PageSize = 20 + p.SortBy = "created_at" + p.SortOrder = "desc" + if p.SortBy != "created_at" || p.SortOrder != "desc" { + t.Error("MaterialFileQueryParams field assignment failed") + } +} + +// TestAdminReportListQuery_FormBinding 验证 AdminReportListQuery 的 form tag +// 通过 gin 的 binding 反射可正确绑定(迁移后 tag 保持不变)。 +// 这里用手动构造验证字段与 tag 存在性,不引入 gin 依赖。 +func TestAdminReportListQuery_Fields(t *testing.T) { + q := AdminReportListQuery{ + Page: 1, + PageSize: 20, + TargetType: "post", + Status: "pending", + Keyword: "spam", + } + if q.Page != 1 || q.PageSize != 20 { + t.Errorf("AdminReportListQuery pagination mismatch: Page=%d PageSize=%d", q.Page, q.PageSize) + } + if q.TargetType != "post" || q.Status != "pending" { + t.Errorf("AdminReportListQuery filter fields mismatch: %+v", q) + } +} diff --git a/internal/repository/data_change_log_repo.go b/internal/repository/data_change_log_repo.go index 6170cd1..d260045 100644 --- a/internal/repository/data_change_log_repo.go +++ b/internal/repository/data_change_log_repo.go @@ -1,10 +1,10 @@ -package repository +package repository import ( "context" - "with_you/internal/dto" "with_you/internal/model" + "with_you/internal/query" "gorm.io/gorm" ) @@ -13,7 +13,7 @@ import ( type DataChangeLogRepository interface { CreateDataChangeLog(ctx context.Context, log *model.DataChangeLog) error BatchCreateDataChangeLogs(ctx context.Context, logs []*model.DataChangeLog) error - 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) DeleteOldLogs(ctx context.Context, beforeDate string) error @@ -44,7 +44,7 @@ func (r *dataChangeLogRepository) BatchCreateDataChangeLogs(ctx context.Context, } // GetDataChangeLogs 获取数据变更日志列表(分页) -func (r *dataChangeLogRepository) GetDataChangeLogs(ctx context.Context, filters dto.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error) { +func (r *dataChangeLogRepository) GetDataChangeLogs(ctx context.Context, filters query.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error) { query := r.db.WithContext(ctx).Model(&model.DataChangeLog{}) if filters.UserID != "" { @@ -88,12 +88,12 @@ func (r *dataChangeLogRepository) GetDataChangeLogs(ctx context.Context, filters // GetDataChangeLogsByUser 获取指定用户的数据变更日志 func (r *dataChangeLogRepository) GetDataChangeLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.DataChangeLog, int64, error) { - return r.GetDataChangeLogs(ctx, dto.DataChangeFilter{UserID: userID}, page, pageSize) + return r.GetDataChangeLogs(ctx, query.DataChangeFilter{UserID: userID}, page, pageSize) } // GetDataChangeLogsByOperator 获取指定操作人的日志 func (r *dataChangeLogRepository) GetDataChangeLogsByOperator(ctx context.Context, operatorID string, page, pageSize int) ([]*model.DataChangeLog, int64, error) { - return r.GetDataChangeLogs(ctx, dto.DataChangeFilter{OperatorID: operatorID}, page, pageSize) + return r.GetDataChangeLogs(ctx, query.DataChangeFilter{OperatorID: operatorID}, page, pageSize) } // DeleteOldLogs 删除旧的数据变更日志(用于定时清理) diff --git a/internal/repository/login_log_repo.go b/internal/repository/login_log_repo.go index c322e5d..886d023 100644 --- a/internal/repository/login_log_repo.go +++ b/internal/repository/login_log_repo.go @@ -1,11 +1,11 @@ -package repository +package repository import ( "context" "time" - "with_you/internal/dto" "with_you/internal/model" + "with_you/internal/query" "gorm.io/gorm" ) @@ -14,7 +14,7 @@ import ( type LoginLogRepository interface { CreateLoginLog(ctx context.Context, log *model.LoginLog) error BatchCreateLoginLogs(ctx context.Context, logs []*model.LoginLog) error - 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 time.Duration) (int64, error) GetRecentSuccessLogins(ctx context.Context, userID string, limit int) ([]*model.LoginLog, error) @@ -46,7 +46,7 @@ func (r *loginLogRepository) BatchCreateLoginLogs(ctx context.Context, logs []*m } // GetLoginLogs 获取登录日志列表(分页) -func (r *loginLogRepository) GetLoginLogs(ctx context.Context, filters dto.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error) { +func (r *loginLogRepository) GetLoginLogs(ctx context.Context, filters query.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error) { query := r.db.WithContext(ctx).Model(&model.LoginLog{}) if filters.UserID != "" { @@ -90,7 +90,7 @@ func (r *loginLogRepository) GetLoginLogs(ctx context.Context, filters dto.Login // GetLoginLogsByUser 获取指定用户的登录日志 func (r *loginLogRepository) GetLoginLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.LoginLog, int64, error) { - return r.GetLoginLogs(ctx, dto.LoginFilter{UserID: userID}, page, pageSize) + return r.GetLoginLogs(ctx, query.LoginFilter{UserID: userID}, page, pageSize) } // GetFailedLoginsByIP 获取指定IP的失败登录记录(用于风控) diff --git a/internal/repository/material_repo.go b/internal/repository/material_repo.go index a0bae09..a6186bc 100644 --- a/internal/repository/material_repo.go +++ b/internal/repository/material_repo.go @@ -1,9 +1,9 @@ -package repository +package repository import ( - "with_you/internal/dto" "with_you/internal/model" "with_you/internal/pkg/utils" + "with_you/internal/query" "gorm.io/gorm" ) @@ -80,7 +80,7 @@ func (r *materialSubjectRepository) CountMaterials(subjectID string) (int64, err // MaterialFileRepository 文件资料仓储接口 type MaterialFileRepository interface { - List(params dto.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) + List(params query.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) GetByID(id string) (*model.MaterialFile, error) GetByIDWithSubject(id string) (*model.MaterialFile, error) Create(file *model.MaterialFile) error @@ -103,7 +103,7 @@ func NewMaterialFileRepository(db *gorm.DB) MaterialFileRepository { } // List 获取资料列表(支持筛选和分页) -func (r *materialFileRepository) List(params dto.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) { +func (r *materialFileRepository) List(params query.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) { var files []*model.MaterialFile var total int64 @@ -260,4 +260,4 @@ func (r *materialFileRepository) GetLatestMaterials(limit int) ([]*model.Materia Limit(limit). Find(&files).Error return files, err -} \ No newline at end of file +} diff --git a/internal/repository/operation_log_repo.go b/internal/repository/operation_log_repo.go index 87e6bf5..3b29cdd 100644 --- a/internal/repository/operation_log_repo.go +++ b/internal/repository/operation_log_repo.go @@ -1,10 +1,10 @@ -package repository +package repository import ( "context" - "with_you/internal/dto" "with_you/internal/model" + "with_you/internal/query" "gorm.io/gorm" ) @@ -13,7 +13,7 @@ import ( type OperationLogRepository interface { CreateOperationLog(ctx context.Context, log *model.OperationLog) error BatchCreateOperationLogs(ctx context.Context, logs []*model.OperationLog) error - 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) DeleteOldLogs(ctx context.Context, beforeDate string) error @@ -44,7 +44,7 @@ func (r *operationLogRepository) BatchCreateOperationLogs(ctx context.Context, l } // GetOperationLogs 获取操作日志列表(分页) -func (r *operationLogRepository) GetOperationLogs(ctx context.Context, filters dto.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error) { +func (r *operationLogRepository) GetOperationLogs(ctx context.Context, filters query.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error) { query := r.db.WithContext(ctx).Model(&model.OperationLog{}) if filters.UserID != "" { @@ -88,12 +88,12 @@ func (r *operationLogRepository) GetOperationLogs(ctx context.Context, filters d // GetOperationLogsByUser 获取指定用户的操作日志 func (r *operationLogRepository) GetOperationLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.OperationLog, int64, error) { - return r.GetOperationLogs(ctx, dto.LogFilter{UserID: userID}, page, pageSize) + return r.GetOperationLogs(ctx, query.LogFilter{UserID: userID}, page, pageSize) } // GetOperationLogsByTimeRange 按时间范围获取操作日志 func (r *operationLogRepository) GetOperationLogsByTimeRange(ctx context.Context, startTime, endTime string, page, pageSize int) ([]*model.OperationLog, int64, error) { - return r.GetOperationLogs(ctx, dto.LogFilter{StartTime: startTime, EndTime: endTime}, page, pageSize) + return r.GetOperationLogs(ctx, query.LogFilter{StartTime: startTime, EndTime: endTime}, page, pageSize) } // DeleteOldLogs 删除旧的操作日志(用于定时清理) diff --git a/internal/repository/report_repo.go b/internal/repository/report_repo.go index dc568b5..6f0c136 100644 --- a/internal/repository/report_repo.go +++ b/internal/repository/report_repo.go @@ -1,9 +1,9 @@ -package repository +package repository import ( - "with_you/internal/dto" - "with_you/internal/model" "context" + "with_you/internal/model" + "with_you/internal/query" "gorm.io/gorm" ) @@ -19,7 +19,7 @@ type ReportRepository interface { // FindByIDWithContext 使用上下文查询举报 FindByIDWithContext(ctx context.Context, id string) (*model.Report, error) // List 查询举报列表 - List(query dto.AdminReportListQuery) ([]*model.Report, int64, error) + List(query query.AdminReportListQuery) ([]*model.Report, int64, error) // GetReportCount 获取内容的举报次数 GetReportCount(targetType model.ReportTargetType, targetID string) (int64, error) // HasUserReported 检查用户是否已举报过该内容 @@ -87,7 +87,7 @@ func (r *reportRepository) FindByIDWithContext(ctx context.Context, id string) ( } // List 查询举报列表 -func (r *reportRepository) List(query dto.AdminReportListQuery) ([]*model.Report, int64, error) { +func (r *reportRepository) List(query query.AdminReportListQuery) ([]*model.Report, int64, error) { var reports []*model.Report var total int64 diff --git a/internal/router/router.go b/internal/router/router.go index 58d1975..d2a0d37 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -1,4 +1,4 @@ -package router +package router import ( "github.com/gin-gonic/gin" @@ -28,7 +28,7 @@ type RouterDeps struct { ScheduleHandler *handler.ScheduleHandler GradeHandler *handler.GradeHandler ExamHandler *handler.ExamHandler - EmptyClassroomHandler *handler.EmptyClassroomHandler + EmptyClassroomHandler *handler.EmptyClassroomHandler RoleHandler *handler.RoleHandler AdminUserHandler *handler.AdminUserHandler AdminPostHandler *handler.AdminPostHandler @@ -48,54 +48,19 @@ type RouterDeps struct { SetupHandler *handler.SetupHandler TradeHandler *handler.TradeHandler WSHandler *handler.WSHandler - JWTService *service.JWTService + JWTService service.JWTService LogService *service.LogService ActivityService service.UserActivityService CasbinService service.CasbinService + UserService service.UserService } // Router 路由配置 +// 通过嵌入 RouterDeps 复用所有依赖字段,消除 RouterDeps 与 Router 之间的字段重复。 +// 所有 deps 字段(如 userHandler、jwtService)经嵌入提升后仍以 r.UserHandler 形式访问。 type Router struct { - engine *gin.Engine - userRepo repository.UserRepository - userHandler *handler.UserHandler - postHandler *handler.PostHandler - commentHandler *handler.CommentHandler - messageHandler *handler.MessageHandler - notificationHandler *handler.NotificationHandler - uploadHandler *handler.UploadHandler - pushHandler *handler.PushHandler - systemMessageHandler *handler.SystemMessageHandler - groupHandler *handler.GroupHandler - stickerHandler *handler.StickerHandler - voteHandler *handler.VoteHandler - channelHandler *handler.ChannelHandler - scheduleHandler *handler.ScheduleHandler - gradeHandler *handler.GradeHandler - examHandler *handler.ExamHandler - emptyClassroomHandler *handler.EmptyClassroomHandler - roleHandler *handler.RoleHandler - adminUserHandler *handler.AdminUserHandler - adminPostHandler *handler.AdminPostHandler - adminCommentHandler *handler.AdminCommentHandler - adminGroupHandler *handler.AdminGroupHandler - adminDashboardHandler *handler.AdminDashboardHandler - adminLogHandler *handler.AdminLogHandler - qrcodeHandler *handler.QRCodeHandler - materialHandler *handler.MaterialHandler - callHandler *handler.CallHandler - liveKitHandler *handler.LiveKitHandler - reportHandler *handler.ReportHandler - adminReportHandler *handler.AdminReportHandler - verificationHandler *handler.VerificationHandler - adminVerificationHandler *handler.AdminVerificationHandler - adminProfileAuditHandler *handler.AdminProfileAuditHandler - setupHandler *handler.SetupHandler - tradeHandler *handler.TradeHandler - wsHandler *handler.WSHandler - logService *service.LogService - jwtService *service.JWTService - casbinService service.CasbinService + *RouterDeps + engine *gin.Engine } // New 创建路由 @@ -104,46 +69,8 @@ func New(deps RouterDeps) *Router { deps.UserHandler.SetActivityService(deps.ActivityService) r := &Router{ - engine: gin.Default(), - userRepo: deps.UserRepo, - userHandler: deps.UserHandler, - postHandler: deps.PostHandler, - commentHandler: deps.CommentHandler, - messageHandler: deps.MessageHandler, - notificationHandler: deps.NotificationHandler, - uploadHandler: deps.UploadHandler, - pushHandler: deps.PushHandler, - systemMessageHandler: deps.SystemMessageHandler, - groupHandler: deps.GroupHandler, - stickerHandler: deps.StickerHandler, - voteHandler: deps.VoteHandler, - channelHandler: deps.ChannelHandler, - scheduleHandler: deps.ScheduleHandler, - gradeHandler: deps.GradeHandler, - examHandler: deps.ExamHandler, - emptyClassroomHandler: deps.EmptyClassroomHandler, - roleHandler: deps.RoleHandler, - adminUserHandler: deps.AdminUserHandler, - adminPostHandler: deps.AdminPostHandler, - adminCommentHandler: deps.AdminCommentHandler, - adminGroupHandler: deps.AdminGroupHandler, - adminDashboardHandler: deps.AdminDashboardHandler, - adminLogHandler: deps.AdminLogHandler, - qrcodeHandler: deps.QRCodeHandler, - materialHandler: deps.MaterialHandler, - callHandler: deps.CallHandler, - liveKitHandler: deps.LiveKitHandler, - reportHandler: deps.ReportHandler, - adminReportHandler: deps.AdminReportHandler, - verificationHandler: deps.VerificationHandler, - adminVerificationHandler: deps.AdminVerificationHandler, - adminProfileAuditHandler: deps.AdminProfileAuditHandler, - setupHandler: deps.SetupHandler, - tradeHandler: deps.TradeHandler, - logService: deps.LogService, - jwtService: deps.JWTService, - casbinService: deps.CasbinService, - wsHandler: deps.WSHandler, + RouterDeps: &deps, + engine: gin.Default(), } r.setupRoutes() @@ -169,25 +96,25 @@ func (r *Router) setupRoutes() { auth.Use(middleware.GlobalAuthRateLimit()) auth.Use(middleware.IPBanMiddleware()) { - auth.POST("/register", middleware.RegisterRateLimit(), r.userHandler.Register) - auth.POST("/register/send-code", middleware.SendCodeRateLimit(), r.userHandler.SendRegisterCode) - auth.POST("/login", middleware.LoginRateLimit(), r.userHandler.Login) - auth.POST("/password/send-code", middleware.PasswordResetRateLimit(), r.userHandler.SendPasswordResetCode) - auth.POST("/password/reset", middleware.PasswordResetRateLimit(), r.userHandler.ResetPassword) - auth.POST("/logout", r.userHandler.Logout) - auth.POST("/refresh", r.userHandler.RefreshToken) + auth.POST("/register", middleware.RegisterRateLimit(), r.UserHandler.Register) + auth.POST("/register/send-code", middleware.SendCodeRateLimit(), r.UserHandler.SendRegisterCode) + auth.POST("/login", middleware.LoginRateLimit(), r.UserHandler.Login) + auth.POST("/password/send-code", middleware.PasswordResetRateLimit(), r.UserHandler.SendPasswordResetCode) + auth.POST("/password/reset", middleware.PasswordResetRateLimit(), r.UserHandler.ResetPassword) + auth.POST("/logout", r.UserHandler.Logout) + auth.POST("/refresh", r.UserHandler.RefreshToken) } // 初始化超级管理员(公开接口,只能使用一次) - if r.setupHandler != nil { - v1.POST("/admin/setup-super-admin", r.setupHandler.SetupSuperAdmin) + if r.SetupHandler != nil { + v1.POST("/admin/setup-super-admin", r.SetupHandler.SetupSuperAdmin) } // 需要认证的路由 - authMiddleware := middleware.Auth(r.jwtService) + authMiddleware := middleware.Auth(r.JWTService) // === 访问日志中间件 === - if r.adminLogHandler != nil && r.logService != nil { + if r.AdminLogHandler != nil && r.LogService != nil { accessLogConfig := &middleware.AccessLogConfig{ Enable: true, SkipPaths: []string{"/health", "/api/v1/health"}, @@ -195,211 +122,208 @@ func (r *Router) setupRoutes() { ServerPort: 8080, } r.engine.Use(middleware.AccessLogMiddleware( - r.logService.OperationLog, + r.LogService.OperationLog, accessLogConfig, )) } - // Casbin 权限中间件(暂不全局启用,可根据需要在特定路由组上使用) - // casbinMiddleware := middleware.CasbinAuth(r.casbinService) - // 用户路由 users := v1.Group("/users") { // 当前用户 - users.GET("/me", authMiddleware, r.userHandler.GetCurrentUser) - users.PUT("/me", authMiddleware, r.userHandler.UpdateUser) - users.POST("/me/email/send-code", authMiddleware, r.userHandler.SendEmailVerifyCode) - users.POST("/me/email/verify", authMiddleware, r.userHandler.VerifyEmail) - users.POST("/me/avatar", authMiddleware, r.uploadHandler.UploadAvatar) - users.POST("/me/cover", authMiddleware, r.uploadHandler.UploadCover) - users.POST("/change-password/send-code", authMiddleware, r.userHandler.SendChangePasswordCode) - users.POST("/change-password", authMiddleware, r.userHandler.ChangePassword) + users.GET("/me", authMiddleware, r.UserHandler.GetCurrentUser) + users.PUT("/me", authMiddleware, r.UserHandler.UpdateUser) + users.POST("/me/email/send-code", authMiddleware, r.UserHandler.SendEmailVerifyCode) + users.POST("/me/email/verify", authMiddleware, r.UserHandler.VerifyEmail) + users.POST("/me/avatar", authMiddleware, r.UploadHandler.UploadAvatar) + users.POST("/me/cover", authMiddleware, r.UploadHandler.UploadCover) + users.POST("/change-password/send-code", authMiddleware, r.UserHandler.SendChangePasswordCode) + users.POST("/change-password", authMiddleware, r.UserHandler.ChangePassword) // 隐私设置 - users.GET("/me/privacy", authMiddleware, r.userHandler.GetPrivacySettings) - users.PUT("/me/privacy", authMiddleware, r.userHandler.UpdatePrivacySettings) + users.GET("/me/privacy", authMiddleware, r.UserHandler.GetPrivacySettings) + users.PUT("/me/privacy", authMiddleware, r.UserHandler.UpdatePrivacySettings) // 账号注销 - users.POST("/me/deactivate", authMiddleware, r.userHandler.RequestAccountDeletion) - users.DELETE("/me/deactivate", authMiddleware, r.userHandler.CancelAccountDeletion) - users.GET("/me/deletion-status", authMiddleware, r.userHandler.GetDeletionStatus) + users.POST("/me/deactivate", authMiddleware, r.UserHandler.RequestAccountDeletion) + users.DELETE("/me/deactivate", authMiddleware, r.UserHandler.CancelAccountDeletion) + users.GET("/me/deletion-status", authMiddleware, r.UserHandler.GetDeletionStatus) // 当前用户角色 - if r.roleHandler != nil { - users.GET("/me/roles", authMiddleware, r.roleHandler.GetMyRoles) + if r.RoleHandler != nil { + users.GET("/me/roles", authMiddleware, r.RoleHandler.GetMyRoles) } // 搜索用户 - users.GET("/search", middleware.OptionalAuth(r.jwtService), r.userHandler.Search) + users.GET("/search", middleware.OptionalAuth(r.JWTService), r.UserHandler.Search) // 其他用户 - users.GET("/:id", middleware.OptionalAuth(r.jwtService), r.userHandler.GetUserByID) - users.POST("/:id/follow", authMiddleware, r.userHandler.FollowUser) - users.DELETE("/:id/follow", authMiddleware, r.userHandler.UnfollowUser) - users.POST("/:id/block", authMiddleware, r.userHandler.BlockUser) - users.DELETE("/:id/block", authMiddleware, r.userHandler.UnblockUser) - users.GET("/:id/block-status", authMiddleware, r.userHandler.GetBlockStatus) - users.GET("/blocks", authMiddleware, r.userHandler.GetBlockedUsers) - users.GET("/:id/following", authMiddleware, r.userHandler.GetFollowingList) - users.GET("/:id/followers", authMiddleware, r.userHandler.GetFollowersList) + users.GET("/:id", middleware.OptionalAuth(r.JWTService), r.UserHandler.GetUserByID) + users.POST("/:id/follow", authMiddleware, r.UserHandler.FollowUser) + users.DELETE("/:id/follow", authMiddleware, r.UserHandler.UnfollowUser) + users.POST("/:id/block", authMiddleware, r.UserHandler.BlockUser) + users.DELETE("/:id/block", authMiddleware, r.UserHandler.UnblockUser) + users.GET("/:id/block-status", authMiddleware, r.UserHandler.GetBlockStatus) + users.GET("/blocks", authMiddleware, r.UserHandler.GetBlockedUsers) + users.GET("/:id/following", authMiddleware, r.UserHandler.GetFollowingList) + users.GET("/:id/followers", authMiddleware, r.UserHandler.GetFollowersList) // 用户帖子 - 使用 OptionalAuth 获取当前用户点赞/收藏状态 - users.GET("/:id/posts", middleware.OptionalAuth(r.jwtService), r.postHandler.GetUserPosts) - users.GET("/:id/favorites", middleware.OptionalAuth(r.jwtService), r.postHandler.GetFavorites) + users.GET("/:id/posts", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetUserPosts) + users.GET("/:id/favorites", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetFavorites) } // 身份认证路由 - if r.verificationHandler != nil { + if r.VerificationHandler != nil { verification := v1.Group("/verification") verification.Use(authMiddleware) { - verification.GET("/status", r.verificationHandler.GetVerificationStatus) - verification.POST("/submit", r.verificationHandler.SubmitVerification) - verification.GET("/records", r.verificationHandler.GetVerificationRecords) + verification.GET("/status", r.VerificationHandler.GetVerificationStatus) + verification.POST("/submit", r.VerificationHandler.SubmitVerification) + verification.GET("/records", r.VerificationHandler.GetVerificationRecords) } } // 二维码登录(公开) - if r.qrcodeHandler != nil { + if r.QRCodeHandler != nil { qrcode := v1.Group("/auth/qrcode") { - qrcode.GET("", r.qrcodeHandler.GetQRCode) - qrcode.GET("/events", r.qrcodeHandler.WSEvents) + qrcode.GET("", r.QRCodeHandler.GetQRCode) + qrcode.GET("/events", r.QRCodeHandler.WSEvents) } // 二维码操作(需认证) qrcodeAuth := v1.Group("/auth/qrcode") qrcodeAuth.Use(authMiddleware) { - qrcodeAuth.POST("/scan", r.qrcodeHandler.Scan) - qrcodeAuth.POST("/confirm", r.qrcodeHandler.Confirm) - qrcodeAuth.POST("/cancel", r.qrcodeHandler.Cancel) + qrcodeAuth.POST("/scan", r.QRCodeHandler.Scan) + qrcodeAuth.POST("/confirm", r.QRCodeHandler.Confirm) + qrcodeAuth.POST("/cancel", r.QRCodeHandler.Cancel) } } // 认证路由(公开) authPublic := v1.Group("/auth") { - authPublic.GET("/check-username", r.userHandler.CheckUsername) + authPublic.GET("/check-username", r.UserHandler.CheckUsername) } // 帖子路由 posts := v1.Group("/posts") { // 使用 OptionalAuth 中间件来获取用户登录状态 - posts.GET("", middleware.OptionalAuth(r.jwtService), r.postHandler.List) - posts.GET("/search", middleware.OptionalAuth(r.jwtService), r.postHandler.Search) - posts.GET("/suggest", middleware.OptionalAuth(r.jwtService), r.postHandler.Suggest) - posts.GET("/:id", middleware.OptionalAuth(r.jwtService), r.postHandler.GetByID) - posts.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Create) - posts.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Update) - posts.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Delete) + posts.GET("", middleware.OptionalAuth(r.JWTService), r.PostHandler.List) + posts.GET("/search", middleware.OptionalAuth(r.JWTService), r.PostHandler.Search) + posts.GET("/suggest", middleware.OptionalAuth(r.JWTService), r.PostHandler.Suggest) + posts.GET("/:id", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetByID) + posts.POST("", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Create) + posts.PUT("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Update) + posts.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Delete) // 浏览量记录(可选认证,允许游客浏览) - posts.POST("/:id/view", middleware.OptionalAuth(r.jwtService), r.postHandler.RecordView) + posts.POST("/:id/view", middleware.OptionalAuth(r.JWTService), r.PostHandler.RecordView) // 分享计数(可选认证;仅已发布帖子生效) - posts.POST("/:id/share", middleware.OptionalAuth(r.jwtService), r.postHandler.RecordShare) + posts.POST("/:id/share", middleware.OptionalAuth(r.JWTService), r.PostHandler.RecordShare) // 内链相关路由 - posts.GET("/:id/related", middleware.OptionalAuth(r.jwtService), r.postHandler.GetRelatedPosts) - posts.GET("/:id/refs", middleware.OptionalAuth(r.jwtService), r.postHandler.GetReferencedPosts) - posts.POST("/:id/ref-click", middleware.OptionalAuth(r.jwtService), r.postHandler.RecordRefClick) + posts.GET("/:id/related", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetRelatedPosts) + posts.GET("/:id/refs", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetReferencedPosts) + posts.POST("/:id/ref-click", middleware.OptionalAuth(r.JWTService), r.PostHandler.RecordRefClick) // 点赞 - posts.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Like) - posts.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Unlike) + posts.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Like) + posts.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Unlike) // 收藏 - posts.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Favorite) - posts.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Unfavorite) + posts.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Favorite) + posts.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Unfavorite) // 投票相关路由 - posts.POST("/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.CreateVotePost) // 创建投票帖子 - posts.GET("/:id/vote", middleware.OptionalAuth(r.jwtService), r.voteHandler.GetVoteResult) // 获取投票结果 - posts.POST("/:id/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.Vote) // 投票 - posts.DELETE("/:id/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.Unvote) // 取消投票 + posts.POST("/vote", authMiddleware, middleware.RequireVerified(r.UserService), r.VoteHandler.CreateVotePost) // 创建投票帖子 + posts.GET("/:id/vote", middleware.OptionalAuth(r.JWTService), r.VoteHandler.GetVoteResult) // 获取投票结果 + posts.POST("/:id/vote", authMiddleware, middleware.RequireVerified(r.UserService), r.VoteHandler.Vote) // 投票 + posts.DELETE("/:id/vote", authMiddleware, middleware.RequireVerified(r.UserService), r.VoteHandler.Unvote) // 取消投票 } // 频道路由(公开) - if r.channelHandler != nil { + if r.ChannelHandler != nil { channels := v1.Group("/channels") { - channels.GET("", r.channelHandler.ListPublic) + channels.GET("", r.ChannelHandler.ListPublic) } } // 二手交易路由 - if r.tradeHandler != nil { + if r.TradeHandler != nil { trade := v1.Group("/trade") { - trade.GET("", middleware.OptionalAuth(r.jwtService), r.tradeHandler.List) - trade.GET("/:id", middleware.OptionalAuth(r.jwtService), r.tradeHandler.GetByID) - trade.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Create) - trade.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Update) - trade.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Delete) - trade.PUT("/:id/status", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.UpdateStatus) - trade.POST("/:id/view", middleware.OptionalAuth(r.jwtService), r.tradeHandler.RecordView) - trade.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Favorite) - trade.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Unfavorite) + trade.GET("", middleware.OptionalAuth(r.JWTService), r.TradeHandler.List) + trade.GET("/:id", middleware.OptionalAuth(r.JWTService), r.TradeHandler.GetByID) + trade.POST("", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Create) + trade.PUT("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Update) + trade.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Delete) + trade.PUT("/:id/status", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.UpdateStatus) + trade.POST("/:id/view", middleware.OptionalAuth(r.JWTService), r.TradeHandler.RecordView) + trade.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Favorite) + trade.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Unfavorite) } } // 课表路由 - if r.scheduleHandler != nil { + if r.ScheduleHandler != nil { schedule := v1.Group("/schedule") schedule.Use(authMiddleware) { - schedule.GET("/courses", r.scheduleHandler.ListCourses) - schedule.POST("/courses", r.scheduleHandler.CreateCourse) - schedule.POST("/sync", r.scheduleHandler.SyncSchedule) // 同步课表 - schedule.PUT("/courses/:id", r.scheduleHandler.UpdateCourse) - schedule.DELETE("/courses/:id", r.scheduleHandler.DeleteCourse) + schedule.GET("/courses", r.ScheduleHandler.ListCourses) + schedule.POST("/courses", r.ScheduleHandler.CreateCourse) + schedule.POST("/sync", r.ScheduleHandler.SyncSchedule) // 同步课表 + schedule.PUT("/courses/:id", r.ScheduleHandler.UpdateCourse) + schedule.DELETE("/courses/:id", r.ScheduleHandler.DeleteCourse) } } // 成绩路由 - if r.gradeHandler != nil { + if r.GradeHandler != nil { grades := v1.Group("/grades") grades.Use(authMiddleware) { - grades.GET("", r.gradeHandler.ListGrades) - grades.POST("/sync", r.gradeHandler.SyncGrades) + grades.GET("", r.GradeHandler.ListGrades) + grades.POST("/sync", r.GradeHandler.SyncGrades) } } // 考试安排路由 - if r.examHandler != nil { + if r.ExamHandler != nil { exams := v1.Group("/exams") exams.Use(authMiddleware) { - exams.GET("", r.examHandler.ListExams) - exams.POST("/sync", r.examHandler.SyncExams) + exams.GET("", r.ExamHandler.ListExams) + exams.POST("/sync", r.ExamHandler.SyncExams) } } // 空教室路由 - if r.emptyClassroomHandler != nil { + if r.EmptyClassroomHandler != nil { classrooms := v1.Group("/classrooms") classrooms.Use(authMiddleware) { - classrooms.GET("", r.emptyClassroomHandler.ListEmptyClassrooms) - classrooms.POST("/sync", r.emptyClassroomHandler.SyncEmptyClassrooms) + classrooms.GET("", r.EmptyClassroomHandler.ListEmptyClassrooms) + classrooms.POST("/sync", r.EmptyClassroomHandler.SyncEmptyClassrooms) } } // 学习资料路由(公开) - if r.materialHandler != nil { + if r.MaterialHandler != nil { materials := v1.Group("/materials") { // 学科相关 - materials.GET("/subjects", r.materialHandler.ListSubjects) - materials.GET("/subjects/:id", r.materialHandler.GetSubject) + materials.GET("/subjects", r.MaterialHandler.ListSubjects) + materials.GET("/subjects/:id", r.MaterialHandler.GetSubject) // 资料相关 - materials.GET("", r.materialHandler.ListMaterials) - materials.GET("/search", r.materialHandler.SearchMaterials) - materials.GET("/:id", r.materialHandler.GetMaterial) - materials.GET("/:id/download", r.materialHandler.DownloadMaterial) + materials.GET("", r.MaterialHandler.ListMaterials) + materials.GET("/search", r.MaterialHandler.SearchMaterials) + materials.GET("/:id", r.MaterialHandler.GetMaterial) + materials.GET("/:id/download", r.MaterialHandler.DownloadMaterial) } } @@ -407,24 +331,24 @@ func (r *Router) setupRoutes() { voteOptions := v1.Group("/vote-options") voteOptions.Use(authMiddleware) { - voteOptions.PUT("/:id", r.voteHandler.UpdateVoteOption) // 更新选项 + voteOptions.PUT("/:id", r.VoteHandler.UpdateVoteOption) // 更新选项 } // 评论路由 comments := v1.Group("/comments") { - comments.GET("/post/:id", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByPostID) - comments.GET("/post/:id/cursor", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByPostIDByCursor) // 帖子评论游标分页 - comments.GET("/:id", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByID) - comments.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Create) - comments.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Update) - comments.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Delete) - comments.GET("/:id/replies", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetReplies) - comments.GET("/:id/replies/flat", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetRepliesByRootID) // 扁平化分页获取回复 - comments.GET("/:id/replies/cursor", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetRepliesByRootIDByCursor) // 回复游标分页 + comments.GET("/post/:id", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetByPostID) + comments.GET("/post/:id/cursor", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetByPostIDByCursor) // 帖子评论游标分页 + comments.GET("/:id", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetByID) + comments.POST("", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Create) + comments.PUT("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Update) + comments.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Delete) + comments.GET("/:id/replies", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetReplies) + comments.GET("/:id/replies/flat", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetRepliesByRootID) // 扁平化分页获取回复 + comments.GET("/:id/replies/cursor", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetRepliesByRootIDByCursor) // 回复游标分页 // 评论点赞 - comments.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Like) - comments.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Unlike) + comments.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Like) + comments.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Unlike) } // 会话路由 @@ -434,49 +358,49 @@ func (r *Router) setupRoutes() { // ================================================================ // 新的 RESTful 风格路由(推荐使用) // ================================================================ - conversations.GET("", r.messageHandler.HandleGetConversationList) // 列表 - conversations.GET("/cursor", r.messageHandler.GetConversationsByCursor) // 会话列表游标分页 - conversations.POST("", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleCreateConversation) // 创建 - conversations.GET("/:id", r.messageHandler.HandleGetConversation) // 详情 - conversations.GET("/:id/messages", r.messageHandler.HandleGetMessages) // 消息列表 - conversations.GET("/:id/messages/cursor", r.messageHandler.GetMessagesByCursor) // 消息列表游标分页 - conversations.POST("/:id/messages", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleSendMessage) // 发送消息 - conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读 - conversations.POST("/read-all", r.messageHandler.HandleMarkReadAll) // 批量标记已读 - conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置 - conversations.PUT("/:id/notification_muted", r.messageHandler.HandleSetConversationNotificationMuted) // 免打扰设置 - conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数 - conversations.GET("/sync-data", r.messageHandler.HandleGetSyncData) // 同步元数据 - conversations.GET("/sync", r.messageHandler.HandleGetSyncByVersion) // 增量同步(版本日志) - conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态 - conversations.DELETE("/:id/self", r.messageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己) + conversations.GET("", r.MessageHandler.HandleGetConversationList) // 列表 + conversations.GET("/cursor", r.MessageHandler.GetConversationsByCursor) // 会话列表游标分页 + conversations.POST("", middleware.RequireVerified(r.UserService), r.MessageHandler.HandleCreateConversation) // 创建 + conversations.GET("/:id", r.MessageHandler.HandleGetConversation) // 详情 + conversations.GET("/:id/messages", r.MessageHandler.HandleGetMessages) // 消息列表 + conversations.GET("/:id/messages/cursor", r.MessageHandler.GetMessagesByCursor) // 消息列表游标分页 + conversations.POST("/:id/messages", middleware.RequireVerified(r.UserService), r.MessageHandler.HandleSendMessage) // 发送消息 + conversations.POST("/:id/read", r.MessageHandler.HandleMarkRead) // 标记已读 + conversations.POST("/read-all", r.MessageHandler.HandleMarkReadAll) // 批量标记已读 + conversations.PUT("/:id/pinned", r.MessageHandler.HandleSetConversationPinned) // 置顶设置 + conversations.PUT("/:id/notification_muted", r.MessageHandler.HandleSetConversationNotificationMuted) // 免打扰设置 + conversations.GET("/unread/count", r.MessageHandler.GetUnreadCount) // 未读数 + conversations.GET("/sync-data", r.MessageHandler.HandleGetSyncData) // 同步元数据 + conversations.GET("/sync", r.MessageHandler.HandleGetSyncByVersion) // 增量同步(版本日志) + conversations.POST("/:id/typing", r.MessageHandler.HandleTyping) // 输入状态 + conversations.DELETE("/:id/self", r.MessageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己) } realtime := v1.Group("/realtime") { // WebSocket端点 - 在中间件之前处理认证(通过query参数) - realtime.GET("/ws", r.wsHandler.HandleWebSocket) + realtime.GET("/ws", r.WSHandler.HandleWebSocket) } // 通话路由 - if r.callHandler != nil { + if r.CallHandler != nil { calls := v1.Group("/calls") calls.Use(authMiddleware) { - calls.GET("/history", r.callHandler.GetCallHistory) + calls.GET("/history", r.CallHandler.GetCallHistory) } } // LiveKit 通话路由 - if r.liveKitHandler != nil { + if r.LiveKitHandler != nil { lk := v1.Group("/calls") lk.Use(authMiddleware) { - lk.GET("/token", r.liveKitHandler.GetToken) + lk.GET("/token", r.LiveKitHandler.GetToken) } // Webhook 不需要认证(LiveKit 服务端调用) - v1.POST("/calls/webhook", r.liveKitHandler.HandleWebhook) + v1.POST("/calls/webhook", r.LiveKitHandler.HandleWebhook) } // 消息操作路由 @@ -484,63 +408,63 @@ func (r *Router) setupRoutes() { messages.Use(authMiddleware) { // 撤回/删除消息(统一接口) - messages.POST("/delete_msg", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleDeleteMsg) + messages.POST("/delete_msg", middleware.RequireVerified(r.UserService), r.MessageHandler.HandleDeleteMsg) } // 举报路由 - if r.reportHandler != nil { + if r.ReportHandler != nil { reports := v1.Group("/reports") reports.Use(authMiddleware) { - reports.POST("", r.reportHandler.Create) + reports.POST("", r.ReportHandler.Create) } } // 通知路由 notifications := v1.Group("/notifications") { - notifications.GET("", authMiddleware, r.notificationHandler.GetNotifications) - notifications.GET("/cursor", authMiddleware, r.notificationHandler.GetNotificationsByCursor) // 通知游标分页 - notifications.POST("/:id/read", authMiddleware, r.notificationHandler.MarkAsRead) - notifications.POST("/read-all", authMiddleware, r.notificationHandler.MarkAllAsRead) - notifications.GET("/unread-count", authMiddleware, r.notificationHandler.GetUnreadCount) - notifications.DELETE("/:id", authMiddleware, r.notificationHandler.DeleteNotification) - notifications.DELETE("", authMiddleware, r.notificationHandler.ClearAllNotifications) + notifications.GET("", authMiddleware, r.NotificationHandler.GetNotifications) + notifications.GET("/cursor", authMiddleware, r.NotificationHandler.GetNotificationsByCursor) // 通知游标分页 + notifications.POST("/:id/read", authMiddleware, r.NotificationHandler.MarkAsRead) + notifications.POST("/read-all", authMiddleware, r.NotificationHandler.MarkAllAsRead) + notifications.GET("/unread-count", authMiddleware, r.NotificationHandler.GetUnreadCount) + notifications.DELETE("/:id", authMiddleware, r.NotificationHandler.DeleteNotification) + notifications.DELETE("", authMiddleware, r.NotificationHandler.ClearAllNotifications) } // 上传路由 uploads := v1.Group("/uploads") { - uploads.POST("/images", authMiddleware, r.uploadHandler.UploadImage) + uploads.POST("/images", authMiddleware, r.UploadHandler.UploadImage) } // 推送相关路由 - if r.pushHandler != nil { + if r.PushHandler != nil { pushGroup := v1.Group("/push") pushGroup.Use(authMiddleware) { - pushGroup.POST("/devices", r.pushHandler.RegisterDevice) - pushGroup.GET("/devices", r.pushHandler.GetMyDevices) - pushGroup.DELETE("/devices/:device_id", r.pushHandler.UnregisterDevice) - pushGroup.PUT("/devices/:device_id/token", r.pushHandler.UpdateDeviceToken) - pushGroup.GET("/records", r.pushHandler.GetPushRecords) + pushGroup.POST("/devices", r.PushHandler.RegisterDevice) + pushGroup.GET("/devices", r.PushHandler.GetMyDevices) + pushGroup.DELETE("/devices/:device_id", r.PushHandler.UnregisterDevice) + pushGroup.PUT("/devices/:device_id/token", r.PushHandler.UpdateDeviceToken) + pushGroup.GET("/records", r.PushHandler.GetPushRecords) } } // 系统消息路由 - if r.systemMessageHandler != nil { + if r.SystemMessageHandler != nil { msgGroup := v1.Group("/messages") msgGroup.Use(authMiddleware) { - msgGroup.GET("/system", r.systemMessageHandler.GetSystemMessages) - msgGroup.GET("/system/unread-count", r.systemMessageHandler.GetUnreadCount) - msgGroup.PUT("/system/:id/read", r.systemMessageHandler.MarkAsRead) - msgGroup.PUT("/system/read-all", r.systemMessageHandler.MarkAllAsRead) + msgGroup.GET("/system", r.SystemMessageHandler.GetSystemMessages) + msgGroup.GET("/system/unread-count", r.SystemMessageHandler.GetUnreadCount) + msgGroup.PUT("/system/:id/read", r.SystemMessageHandler.MarkAsRead) + msgGroup.PUT("/system/read-all", r.SystemMessageHandler.MarkAllAsRead) } } // 群组路由 - if r.groupHandler != nil { + if r.GroupHandler != nil { groups := v1.Group("/groups") groups.Use(authMiddleware) { @@ -548,179 +472,179 @@ func (r *Router) setupRoutes() { // 新的 RESTful 风格路由(推荐使用) // ================================================================ // 群组基本操作 - groups.GET("", r.groupHandler.HandleGetUserGroups) // 列表 - groups.GET("/cursor", r.groupHandler.GetGroupsByCursor) // 群组游标分页 - groups.POST("", r.groupHandler.HandleCreateGroup) // 创建 - groups.GET("/:id", r.groupHandler.HandleGetGroupInfo) // 详情 - groups.GET("/:id/me", r.groupHandler.HandleGetMyMemberInfo) // 当前用户成员信息 - groups.DELETE("/:id", r.groupHandler.HandleDissolveGroup) // 解散群组 - groups.POST("/:id/transfer", r.groupHandler.HandleTransferOwner) // 转让群主 + groups.GET("", r.GroupHandler.HandleGetUserGroups) // 列表 + groups.GET("/cursor", r.GroupHandler.GetGroupsByCursor) // 群组游标分页 + groups.POST("", r.GroupHandler.HandleCreateGroup) // 创建 + groups.GET("/:id", r.GroupHandler.HandleGetGroupInfo) // 详情 + groups.GET("/:id/me", r.GroupHandler.HandleGetMyMemberInfo) // 当前用户成员信息 + groups.DELETE("/:id", r.GroupHandler.HandleDissolveGroup) // 解散群组 + groups.POST("/:id/transfer", r.GroupHandler.HandleTransferOwner) // 转让群主 // 成员管理 - groups.POST("/:id/invitations", r.groupHandler.HandleInviteMembers) // 邀请成员 - groups.POST("/:id/join-requests", r.groupHandler.HandleJoinGroup) // 申请加群 - groups.POST("/:id/join-requests/respond", r.groupHandler.HandleRespondInvite) // 响应加群邀请/申请 - groups.POST("/:id/leave", r.groupHandler.HandleSetGroupLeave) // 退群 - groups.GET("/:id/members", r.groupHandler.HandleGetGroupMemberList) // 成员列表 - groups.GET("/:id/members/cursor", r.groupHandler.GetMembersByCursor) // 成员游标分页 - groups.POST("/:id/members/kick", r.groupHandler.HandleSetGroupKick) // 踢出成员 - groups.PUT("/:id/members/:user_id/admin", r.groupHandler.HandleSetGroupAdmin) // 设置管理员 - groups.PUT("/:id/members/me/nickname", r.groupHandler.HandleSetNickname) // 设置群昵称 - groups.POST("/:id/members/ban", r.groupHandler.HandleSetGroupBan) // 禁言成员 + groups.POST("/:id/invitations", r.GroupHandler.HandleInviteMembers) // 邀请成员 + groups.POST("/:id/join-requests", r.GroupHandler.HandleJoinGroup) // 申请加群 + groups.POST("/:id/join-requests/respond", r.GroupHandler.HandleRespondInvite) // 响应加群邀请/申请 + groups.POST("/:id/leave", r.GroupHandler.HandleSetGroupLeave) // 退群 + groups.GET("/:id/members", r.GroupHandler.HandleGetGroupMemberList) // 成员列表 + groups.GET("/:id/members/cursor", r.GroupHandler.GetMembersByCursor) // 成员游标分页 + groups.POST("/:id/members/kick", r.GroupHandler.HandleSetGroupKick) // 踢出成员 + groups.PUT("/:id/members/:user_id/admin", r.GroupHandler.HandleSetGroupAdmin) // 设置管理员 + groups.PUT("/:id/members/me/nickname", r.GroupHandler.HandleSetNickname) // 设置群昵称 + groups.POST("/:id/members/ban", r.GroupHandler.HandleSetGroupBan) // 禁言成员 // 群设置 - groups.PUT("/:id/ban", r.groupHandler.HandleSetGroupWholeBan) // 全员禁言 - groups.PUT("/:id/join-type", r.groupHandler.HandleSetJoinType) // 加群方式 - groups.PUT("/:id/name", r.groupHandler.HandleSetGroupName) // 群名称 - groups.PUT("/:id/avatar", r.groupHandler.HandleSetGroupAvatar) // 群头像 - groups.PUT("/:id/description", r.groupHandler.HandleSetGroupDescription) // 群描述 + groups.PUT("/:id/ban", r.GroupHandler.HandleSetGroupWholeBan) // 全员禁言 + groups.PUT("/:id/join-type", r.GroupHandler.HandleSetJoinType) // 加群方式 + groups.PUT("/:id/name", r.GroupHandler.HandleSetGroupName) // 群名称 + groups.PUT("/:id/avatar", r.GroupHandler.HandleSetGroupAvatar) // 群头像 + groups.PUT("/:id/description", r.GroupHandler.HandleSetGroupDescription) // 群描述 // 群公告 - groups.POST("/:id/announcements", r.groupHandler.HandleCreateAnnouncement) // 创建公告 - groups.GET("/:id/announcements", r.groupHandler.HandleGetAnnouncements) // 公告列表 - groups.GET("/:id/announcements/cursor", r.groupHandler.GetAnnouncementsByCursor) // 公告游标分页 - groups.DELETE("/:id/announcements/:announcement_id", r.groupHandler.HandleDeleteAnnouncement) // 删除公告 + groups.POST("/:id/announcements", r.GroupHandler.HandleCreateAnnouncement) // 创建公告 + groups.GET("/:id/announcements", r.GroupHandler.HandleGetAnnouncements) // 公告列表 + groups.GET("/:id/announcements/cursor", r.GroupHandler.GetAnnouncementsByCursor) // 公告游标分页 + groups.DELETE("/:id/announcements/:announcement_id", r.GroupHandler.HandleDeleteAnnouncement) // 删除公告 // 加群请求处理 - groups.POST("/:id/join-requests/handle", r.groupHandler.HandleSetGroupAddRequest) // 处理加群请求 + groups.POST("/:id/join-requests/handle", r.GroupHandler.HandleSetGroupAddRequest) // 处理加群请求 } } // 自定义表情路由 - if r.stickerHandler != nil { + if r.StickerHandler != nil { stickers := v1.Group("/stickers") stickers.Use(authMiddleware) { - stickers.GET("", r.stickerHandler.GetStickers) - stickers.POST("", r.stickerHandler.AddSticker) - stickers.DELETE("", r.stickerHandler.DeleteSticker) - stickers.POST("/reorder", r.stickerHandler.ReorderStickers) - stickers.GET("/check", r.stickerHandler.CheckStickerExists) + stickers.GET("", r.StickerHandler.GetStickers) + stickers.POST("", r.StickerHandler.AddSticker) + stickers.DELETE("", r.StickerHandler.DeleteSticker) + stickers.POST("/reorder", r.StickerHandler.ReorderStickers) + stickers.GET("/check", r.StickerHandler.CheckStickerExists) } } // 管理路由(需要管理员权限) - if r.roleHandler != nil { + if r.RoleHandler != nil { admin := v1.Group("/admin") admin.Use(authMiddleware) - admin.Use(middleware.RequireRole(r.casbinService, model.RoleAdmin, model.RoleSuperAdmin)) + admin.Use(middleware.RequireRole(r.CasbinService, model.RoleAdmin, model.RoleSuperAdmin)) { // 角色管理 - admin.GET("/roles", r.roleHandler.GetAllRoles) - admin.GET("/roles/:name", r.roleHandler.GetRoleDetail) - admin.GET("/roles/:name/permissions", r.roleHandler.GetRolePermissions) - admin.PUT("/roles/:name/permissions", r.roleHandler.UpdateRolePermissions) - admin.GET("/permissions", r.roleHandler.GetAllPermissions) - admin.GET("/users/:id/roles", r.roleHandler.GetUserRoles) - admin.POST("/users/:id/roles", r.roleHandler.AssignRole) - admin.DELETE("/users/:id/roles/:role", r.roleHandler.RemoveRole) + admin.GET("/roles", r.RoleHandler.GetAllRoles) + admin.GET("/roles/:name", r.RoleHandler.GetRoleDetail) + admin.GET("/roles/:name/permissions", r.RoleHandler.GetRolePermissions) + admin.PUT("/roles/:name/permissions", r.RoleHandler.UpdateRolePermissions) + admin.GET("/permissions", r.RoleHandler.GetAllPermissions) + admin.GET("/users/:id/roles", r.RoleHandler.GetUserRoles) + admin.POST("/users/:id/roles", r.RoleHandler.AssignRole) + admin.DELETE("/users/:id/roles/:role", r.RoleHandler.RemoveRole) // 用户管理 - if r.adminUserHandler != nil { - admin.GET("/users", r.adminUserHandler.GetUserList) - admin.GET("/users/:id", r.adminUserHandler.GetUserDetail) - admin.PUT("/users/:id/status", r.adminUserHandler.UpdateUserStatus) + if r.AdminUserHandler != nil { + admin.GET("/users", r.AdminUserHandler.GetUserList) + admin.GET("/users/:id", r.AdminUserHandler.GetUserDetail) + admin.PUT("/users/:id/status", r.AdminUserHandler.UpdateUserStatus) } // 帖子管理 - if r.adminPostHandler != nil { - admin.GET("/posts", r.adminPostHandler.GetPostList) - admin.GET("/posts/:id", r.adminPostHandler.GetPostDetail) - admin.DELETE("/posts/:id", r.adminPostHandler.DeletePost) - admin.PUT("/posts/:id/moderate", r.adminPostHandler.ModeratePost) - admin.POST("/posts/batch-delete", r.adminPostHandler.BatchDeletePosts) - admin.POST("/posts/batch-moderate", r.adminPostHandler.BatchModeratePosts) - admin.PUT("/posts/:id/pin", r.adminPostHandler.SetPostPin) - admin.PUT("/posts/:id/feature", r.adminPostHandler.SetPostFeature) + if r.AdminPostHandler != nil { + admin.GET("/posts", r.AdminPostHandler.GetPostList) + admin.GET("/posts/:id", r.AdminPostHandler.GetPostDetail) + admin.DELETE("/posts/:id", r.AdminPostHandler.DeletePost) + admin.PUT("/posts/:id/moderate", r.AdminPostHandler.ModeratePost) + admin.POST("/posts/batch-delete", r.AdminPostHandler.BatchDeletePosts) + admin.POST("/posts/batch-moderate", r.AdminPostHandler.BatchModeratePosts) + admin.PUT("/posts/:id/pin", r.AdminPostHandler.SetPostPin) + admin.PUT("/posts/:id/feature", r.AdminPostHandler.SetPostFeature) } // 频道管理 - if r.channelHandler != nil { - admin.GET("/channels", r.channelHandler.AdminList) - admin.POST("/channels", r.channelHandler.AdminCreate) - admin.PUT("/channels/:id", r.channelHandler.AdminUpdate) - admin.DELETE("/channels/:id", r.channelHandler.AdminDelete) + if r.ChannelHandler != nil { + admin.GET("/channels", r.ChannelHandler.AdminList) + admin.POST("/channels", r.ChannelHandler.AdminCreate) + admin.PUT("/channels/:id", r.ChannelHandler.AdminUpdate) + admin.DELETE("/channels/:id", r.ChannelHandler.AdminDelete) } // 评论管理 - if r.adminCommentHandler != nil { - admin.GET("/comments", r.adminCommentHandler.GetCommentList) - admin.GET("/comments/:id", r.adminCommentHandler.GetCommentDetail) - admin.DELETE("/comments/:id", r.adminCommentHandler.DeleteComment) - admin.PUT("/comments/:id/moderate", r.adminCommentHandler.ModerateComment) - admin.POST("/comments/batch-delete", r.adminCommentHandler.BatchDeleteComments) - admin.POST("/comments/batch-moderate", r.adminCommentHandler.BatchModerateComments) - admin.GET("/comments/post/:postId", r.adminCommentHandler.GetPostComments) + if r.AdminCommentHandler != nil { + admin.GET("/comments", r.AdminCommentHandler.GetCommentList) + admin.GET("/comments/:id", r.AdminCommentHandler.GetCommentDetail) + admin.DELETE("/comments/:id", r.AdminCommentHandler.DeleteComment) + admin.PUT("/comments/:id/moderate", r.AdminCommentHandler.ModerateComment) + admin.POST("/comments/batch-delete", r.AdminCommentHandler.BatchDeleteComments) + admin.POST("/comments/batch-moderate", r.AdminCommentHandler.BatchModerateComments) + admin.GET("/comments/post/:postId", r.AdminCommentHandler.GetPostComments) } // 群组管理 - if r.adminGroupHandler != nil { - admin.GET("/groups", r.adminGroupHandler.GetGroupList) - admin.GET("/groups/:id", r.adminGroupHandler.GetGroupDetail) - admin.GET("/groups/:id/members", r.adminGroupHandler.GetGroupMembers) - admin.DELETE("/groups/:id", r.adminGroupHandler.DeleteGroup) - admin.PUT("/groups/:id/status", r.adminGroupHandler.UpdateGroupStatus) - admin.DELETE("/groups/:id/members/:userId", r.adminGroupHandler.RemoveMember) - admin.PUT("/groups/:id/transfer", r.adminGroupHandler.TransferOwner) + if r.AdminGroupHandler != nil { + admin.GET("/groups", r.AdminGroupHandler.GetGroupList) + admin.GET("/groups/:id", r.AdminGroupHandler.GetGroupDetail) + admin.GET("/groups/:id/members", r.AdminGroupHandler.GetGroupMembers) + admin.DELETE("/groups/:id", r.AdminGroupHandler.DeleteGroup) + admin.PUT("/groups/:id/status", r.AdminGroupHandler.UpdateGroupStatus) + admin.DELETE("/groups/:id/members/:userId", r.AdminGroupHandler.RemoveMember) + admin.PUT("/groups/:id/transfer", r.AdminGroupHandler.TransferOwner) } // 仪表盘 - if r.adminDashboardHandler != nil { - admin.GET("/dashboard/stats", r.adminDashboardHandler.GetStats) - admin.GET("/dashboard/user-activity", r.adminDashboardHandler.GetUserActivityTrend) - admin.GET("/dashboard/content-stats", r.adminDashboardHandler.GetContentStats) - admin.GET("/dashboard/pending-content", r.adminDashboardHandler.GetPendingContent) - admin.GET("/dashboard/online-users", r.adminDashboardHandler.GetOnlineUsers) + if r.AdminDashboardHandler != nil { + admin.GET("/dashboard/stats", r.AdminDashboardHandler.GetStats) + admin.GET("/dashboard/user-activity", r.AdminDashboardHandler.GetUserActivityTrend) + admin.GET("/dashboard/content-stats", r.AdminDashboardHandler.GetContentStats) + admin.GET("/dashboard/pending-content", r.AdminDashboardHandler.GetPendingContent) + admin.GET("/dashboard/online-users", r.AdminDashboardHandler.GetOnlineUsers) } // 日志管理 - if r.adminLogHandler != nil { + if r.AdminLogHandler != nil { logs := admin.Group("/logs") { - logs.GET("/operations", r.adminLogHandler.GetOperationLogs) - logs.GET("/logins", r.adminLogHandler.GetLoginLogs) - logs.GET("/data-changes", r.adminLogHandler.GetDataChangeLogs) - logs.GET("/statistics", r.adminLogHandler.GetLogStatistics) - logs.GET("/export", r.adminLogHandler.ExportLogs) + logs.GET("/operations", r.AdminLogHandler.GetOperationLogs) + logs.GET("/logins", r.AdminLogHandler.GetLoginLogs) + logs.GET("/data-changes", r.AdminLogHandler.GetDataChangeLogs) + logs.GET("/statistics", r.AdminLogHandler.GetLogStatistics) + logs.GET("/export", r.AdminLogHandler.ExportLogs) } } // 学习资料管理 - if r.materialHandler != nil { + if r.MaterialHandler != nil { // 学科管理 - admin.GET("/materials/subjects", r.materialHandler.AdminListSubjects) - admin.POST("/materials/subjects", r.materialHandler.AdminCreateSubject) - admin.PUT("/materials/subjects/:id", r.materialHandler.AdminUpdateSubject) - admin.DELETE("/materials/subjects/:id", r.materialHandler.AdminDeleteSubject) + admin.GET("/materials/subjects", r.MaterialHandler.AdminListSubjects) + admin.POST("/materials/subjects", r.MaterialHandler.AdminCreateSubject) + admin.PUT("/materials/subjects/:id", r.MaterialHandler.AdminUpdateSubject) + admin.DELETE("/materials/subjects/:id", r.MaterialHandler.AdminDeleteSubject) // 资料管理 - admin.GET("/materials", r.materialHandler.AdminListMaterials) - admin.GET("/materials/:id", r.materialHandler.AdminGetMaterial) - admin.POST("/materials", r.materialHandler.AdminCreateMaterial) - admin.PUT("/materials/:id", r.materialHandler.AdminUpdateMaterial) - admin.DELETE("/materials/:id", r.materialHandler.AdminDeleteMaterial) + admin.GET("/materials", r.MaterialHandler.AdminListMaterials) + admin.GET("/materials/:id", r.MaterialHandler.AdminGetMaterial) + admin.POST("/materials", r.MaterialHandler.AdminCreateMaterial) + admin.PUT("/materials/:id", r.MaterialHandler.AdminUpdateMaterial) + admin.DELETE("/materials/:id", r.MaterialHandler.AdminDeleteMaterial) } // 举报管理 - if r.adminReportHandler != nil { - admin.GET("/reports", r.adminReportHandler.List) - admin.GET("/reports/:id", r.adminReportHandler.Detail) - admin.PUT("/reports/:id/handle", r.adminReportHandler.Handle) - admin.POST("/reports/batch-handle", r.adminReportHandler.BatchHandle) + if r.AdminReportHandler != nil { + admin.GET("/reports", r.AdminReportHandler.List) + admin.GET("/reports/:id", r.AdminReportHandler.Detail) + admin.PUT("/reports/:id/handle", r.AdminReportHandler.Handle) + admin.POST("/reports/batch-handle", r.AdminReportHandler.BatchHandle) } // 身份认证管理 - if r.adminVerificationHandler != nil { - admin.GET("/verifications", r.adminVerificationHandler.ListVerifications) - admin.GET("/verifications/:id", r.adminVerificationHandler.GetVerificationDetail) - admin.PUT("/verifications/:id/review", r.adminVerificationHandler.ReviewVerification) + if r.AdminVerificationHandler != nil { + admin.GET("/verifications", r.AdminVerificationHandler.ListVerifications) + admin.GET("/verifications/:id", r.AdminVerificationHandler.GetVerificationDetail) + admin.PUT("/verifications/:id/review", r.AdminVerificationHandler.ReviewVerification) } // 用户资料审核管理 - if r.adminProfileAuditHandler != nil { - admin.GET("/profile-audits", r.adminProfileAuditHandler.GetPendingList) - admin.PUT("/profile-audits/:id/moderate", r.adminProfileAuditHandler.ModerateAudit) - admin.POST("/profile-audits/batch-moderate", r.adminProfileAuditHandler.BatchModerate) + if r.AdminProfileAuditHandler != nil { + admin.GET("/profile-audits", r.AdminProfileAuditHandler.GetPendingList) + admin.PUT("/profile-audits/:id/moderate", r.AdminProfileAuditHandler.ModerateAudit) + admin.POST("/profile-audits/batch-moderate", r.AdminProfileAuditHandler.BatchModerate) } } } diff --git a/internal/service/admin_group_service.go b/internal/service/admin_group_service.go index d46e8dc..778b99b 100644 --- a/internal/service/admin_group_service.go +++ b/internal/service/admin_group_service.go @@ -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 } diff --git a/internal/service/admin_group_service_test.go b/internal/service/admin_group_service_test.go new file mode 100644 index 0000000..c68fe7e --- /dev/null +++ b/internal/service/admin_group_service_test.go @@ -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)) + } +} diff --git a/internal/service/admin_report_service.go b/internal/service/admin_report_service.go index 1f8cf90..b2e452c 100644 --- a/internal/service/admin_report_service.go +++ b/internal/service/admin_report_service.go @@ -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 diff --git a/internal/service/call_service.go b/internal/service/call_service.go index 7788d74..e037244 100644 --- a/internal/service/call_service.go +++ b/internal/service/call_service.go @@ -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() diff --git a/internal/service/chat_service.go b/internal/service/chat_service.go index 899701c..1c91928 100644 --- a/internal/service/chat_service.go +++ b/internal/service/chat_service.go @@ -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) diff --git a/internal/service/comment_service.go b/internal/service/comment_service.go index 1789664..a71f67f 100644 --- a/internal/service/comment_service.go +++ b/internal/service/comment_service.go @@ -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) } diff --git a/internal/service/empty_classroom_sync_service.go b/internal/service/empty_classroom_sync_service.go index e45446b..330fd01 100644 --- a/internal/service/empty_classroom_sync_service.go +++ b/internal/service/empty_classroom_sync_service.go @@ -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) +} diff --git a/internal/service/exam_sync_service.go b/internal/service/exam_sync_service.go index b60c882..613956b 100644 --- a/internal/service/exam_sync_service.go +++ b/internal/service/exam_sync_service.go @@ -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 -} \ No newline at end of file +} + +// ListExams 查询用户某学期的考试安排。 +func (s *examSyncService) ListExams(userID, semester string) ([]*model.Exam, error) { + return s.examRepo.ListByUserAndSemester(userID, semester) +} diff --git a/internal/service/grade_sync_service.go b/internal/service/grade_sync_service.go index a3082cb..22eb45c 100644 --- a/internal/service/grade_sync_service.go +++ b/internal/service/grade_sync_service.go @@ -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 -} \ No newline at end of file +} + +// 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 +} diff --git a/internal/service/group_service.go b/internal/service/group_service.go index a2d2aff..4c4ef84 100644 --- a/internal/service/group_service.go +++ b/internal/service/group_service.go @@ -47,7 +47,6 @@ var ( ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled ErrNotRequestTarget = apperrors.ErrNotRequestTarget ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee - ErrNotMutualFollow = apperrors.ErrNotMutualFollow ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted ) diff --git a/internal/service/jwt_service.go b/internal/service/jwt_service.go index bf4addb..25d7ef2 100644 --- a/internal/service/jwt_service.go +++ b/internal/service/jwt_service.go @@ -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) } diff --git a/internal/service/livekit_service.go b/internal/service/livekit_service.go index a86bf9d..91e1b7a 100644 --- a/internal/service/livekit_service.go +++ b/internal/service/livekit_service.go @@ -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 -} diff --git a/internal/service/log_cleanup_service.go b/internal/service/log_cleanup_service.go index 77292d1..26494ec 100644 --- a/internal/service/log_cleanup_service.go +++ b/internal/service/log_cleanup_service.go @@ -196,4 +196,3 @@ func NewLogService( DataChangeLog: dataChangeLogService, } } - diff --git a/internal/service/log_service.go b/internal/service/log_service.go index 2de6afd..4bf6c9a 100644 --- a/internal/service/log_service.go +++ b/internal/service/log_service.go @@ -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) } diff --git a/internal/service/material_service.go b/internal/service/material_service.go index ce6db09..22c49db 100644 --- a/internal/service/material_service.go +++ b/internal/service/material_service.go @@ -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) } diff --git a/internal/service/message_segment_image.go b/internal/service/message_segment_image.go index 0923edf..b717d2c 100644 --- a/internal/service/message_segment_image.go +++ b/internal/service/message_segment_image.go @@ -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 } diff --git a/internal/service/message_service.go b/internal/service/message_service.go index 1bc7d3c..868f995 100644 --- a/internal/service/message_service.go +++ b/internal/service/message_service.go @@ -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) } diff --git a/internal/service/notification_service.go b/internal/service/notification_service.go index 72da762..eb826bb 100644 --- a/internal/service/notification_service.go +++ b/internal/service/notification_service.go @@ -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) } diff --git a/internal/service/post_ai_service.go b/internal/service/post_ai_service.go index cfec88b..f007c3f 100644 --- a/internal/service/post_ai_service.go +++ b/internal/service/post_ai_service.go @@ -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, diff --git a/internal/service/post_service.go b/internal/service/post_service.go index 2c8cf7b..600d4c9 100644 --- a/internal/service/post_service.go +++ b/internal/service/post_service.go @@ -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, diff --git a/internal/service/push_worker.go b/internal/service/push_worker.go index a1c0fa2..cacf682 100644 --- a/internal/service/push_worker.go +++ b/internal/service/push_worker.go @@ -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() -} \ No newline at end of file +} diff --git a/internal/service/qrcode_login_service.go b/internal/service/qrcode_login_service.go index e3eb189..cbeadad 100644 --- a/internal/service/qrcode_login_service.go +++ b/internal/service/qrcode_login_service.go @@ -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 } diff --git a/internal/service/sensitive_service.go b/internal/service/sensitive_service.go index d3c5ffb..29be3d2 100644 --- a/internal/service/sensitive_service.go +++ b/internal/service/sensitive_service.go @@ -498,4 +498,3 @@ func (s *sensitiveServiceImpl) loadFromRedis(ctx context.Context) error { return nil } - diff --git a/internal/service/trade_service.go b/internal/service/trade_service.go index fea0f2f..b41a72b 100644 --- a/internal/service/trade_service.go +++ b/internal/service/trade_service.go @@ -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" diff --git a/internal/service/upload_service.go b/internal/service/upload_service.go index 4523182..edd99af 100644 --- a/internal/service/upload_service.go +++ b/internal/service/upload_service.go @@ -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 // 计算目标尺寸 diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go index c8265e1..41e7dc7 100644 --- a/internal/service/vote_service.go +++ b/internal/service/vote_service.go @@ -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 } diff --git a/internal/wire/grpc.go b/internal/wire/grpc.go index 647a23c..528fef7 100644 --- a/internal/wire/grpc.go +++ b/internal/wire/grpc.go @@ -1,4 +1,4 @@ -package wire +package wire import ( "time" @@ -7,8 +7,8 @@ import ( "with_you/internal/grpc/runner" "with_you/internal/pkg/redis" - "github.com/google/wire" "github.com/google/uuid" + "github.com/google/wire" "go.uber.org/zap" ) diff --git a/internal/wire/handler.go b/internal/wire/handler.go index 7bbd28c..1bd1ea4 100644 --- a/internal/wire/handler.go +++ b/internal/wire/handler.go @@ -1,10 +1,9 @@ -package wire +package wire import ( "with_you/internal/config" "with_you/internal/handler" "with_you/internal/pkg/ws" - "with_you/internal/repository" "with_you/internal/service" "github.com/google/wire" @@ -66,7 +65,7 @@ func ProvideUserHandler( func ProvideMessageHandler( chatService service.ChatService, - messageService *service.MessageService, + messageService service.MessageService, userService service.UserService, groupService service.GroupService, publisher ws.MessagePublisher, @@ -102,11 +101,11 @@ func ProvideWSHandler( publisher ws.MessagePublisher, chatService service.ChatService, groupService service.GroupService, - jwtService *service.JWTService, + jwtService service.JWTService, callService service.CallService, - userRepo repository.UserRepository, + userService service.UserService, ) *handler.WSHandler { - return handler.NewWSHandler(publisher, chatService, groupService, jwtService, callService, userRepo) + return handler.NewWSHandler(publisher, chatService, groupService, jwtService, callService, userService) } // ProvideLiveKitHandler 提供 LiveKit 处理器 @@ -122,9 +121,9 @@ func ProvideLiveKitHandler( // ProvideAdminVerificationHandler 提供管理端身份认证处理器 func ProvideAdminVerificationHandler( adminVerificationService service.AdminVerificationService, - userRepo repository.UserRepository, + userService service.UserService, ) *handler.AdminVerificationHandler { - return handler.NewAdminVerificationHandler(adminVerificationService, userRepo) + return handler.NewAdminVerificationHandler(adminVerificationService, userService) } func ProvideSetupHandler( diff --git a/internal/wire/infrastructure.go b/internal/wire/infrastructure.go index 38fbe84..d07d217 100644 --- a/internal/wire/infrastructure.go +++ b/internal/wire/infrastructure.go @@ -1,11 +1,12 @@ -package wire +package wire import ( + "fmt" "time" "with_you/internal/cache" "with_you/internal/config" - "with_you/internal/model" + "with_you/internal/database" "with_you/internal/pkg/crypto" "with_you/internal/pkg/email" "with_you/internal/pkg/hook" @@ -18,8 +19,8 @@ import ( "with_you/internal/repository" "with_you/internal/service" - redisPkg "github.com/redis/go-redis/v9" "github.com/google/wire" + redisPkg "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" ) @@ -74,7 +75,7 @@ func ProvideConfig() (*config.Config, error) { // ProvideDB 提供数据库连接 func ProvideDB(cfg *config.Config) (*gorm.DB, error) { - return model.NewDB(&cfg.Database) + return database.NewDB(&cfg.Database) } // ProvideRedisClient 提供 Redis 客户端 @@ -145,7 +146,7 @@ func ProvideBuiltinHooks(manager *hook.Manager) *hook.BuiltinHooks { // ProvideModerationHooks 注册审核钩子 func ProvideModerationHooks( manager *hook.Manager, - postAIService *service.PostAIService, + postAIService service.PostAIService, postRepo repository.PostRepository, commentRepo repository.CommentRepository, userRepo repository.UserRepository, @@ -248,6 +249,7 @@ func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager { } // ProvideMessageEncryptor 提供消息加密器 +// 加密启用时密钥必须是 32 字节(AES-256),否则视为致命配置错误,启动期 fail-fast。 func ProvideMessageEncryptor(cfg *config.Config) error { if !cfg.Encryption.Enabled { zap.L().Info("Message encryption is disabled") @@ -255,15 +257,12 @@ func ProvideMessageEncryptor(cfg *config.Config) error { } if len(cfg.Encryption.Key) != 32 { - zap.L().Error("Encryption key must be 32 bytes for AES-256", - zap.Int("actual_length", len(cfg.Encryption.Key)), - ) - return nil + // 致命安全配置:不允许降级为「加密已启用但实际不加密」 + return fmt.Errorf("encryption is enabled but key must be 32 bytes for AES-256, got %d", len(cfg.Encryption.Key)) } if err := crypto.InitMessageEncryptor(cfg.Encryption.Key, cfg.Encryption.KeyVersion); err != nil { - zap.L().Error("Failed to initialize message encryptor", zap.Error(err)) - return err + return fmt.Errorf("failed to initialize message encryptor: %w", err) } zap.L().Info("Message encryption initialized successfully", diff --git a/internal/wire/infrastructure_test.go b/internal/wire/infrastructure_test.go new file mode 100644 index 0000000..c9148ee --- /dev/null +++ b/internal/wire/infrastructure_test.go @@ -0,0 +1,91 @@ +package wire + +import ( + "strings" + "testing" + + "with_you/internal/config" + "with_you/internal/pkg/crypto" +) + +// validTestKey 是用于测试的 32 字节 AES-256 密钥(仅测试用,不含敏感信息)。 +const validTestKey = "12345678901234567890123456789012" + +// TestProvideMessageEncryptor_Disabled 校验:加密关闭时应返回 nil 且不初始化加密器。 +func TestProvideMessageEncryptor_Disabled(t *testing.T) { + cfg := &config.Config{ + Encryption: config.EncryptionConfig{ + Enabled: false, + Key: "", + }, + } + + err := ProvideMessageEncryptor(cfg) + if err != nil { + t.Fatalf("disabled encryption should return nil error, got: %v", err) + } +} + +// TestProvideMessageEncryptor_EnabledBadKey 校验:加密启用但密钥非 32 字节时, +// 必须 fail-fast 返回 error,且 error 信息提示长度问题。 +func TestProvideMessageEncryptor_EnabledBadKey(t *testing.T) { + cases := []struct { + name string + key string + }{ + {"empty", ""}, + {"too_short", "shortkey"}, + {"too_long", strings.Repeat("a", 64)}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := &config.Config{ + Encryption: config.EncryptionConfig{ + Enabled: true, + Key: tc.key, + KeyVersion: 1, + }, + } + + err := ProvideMessageEncryptor(cfg) + if err == nil { + t.Fatalf("enabled encryption with bad key (len=%d) must fail-fast, got nil", len(tc.key)) + } + // 错误信息应包含 "32 bytes" 提示 + if !strings.Contains(err.Error(), "32 bytes") { + t.Errorf("error should mention 32 bytes requirement, got: %v", err) + } + }) + } +} + +// TestProvideMessageEncryptor_EnabledValidKey 校验:加密启用且密钥合法时返回 nil, +// 且加密器被正确初始化(GetKeyVersion 与配置一致)。 +// 注意:crypto 用 sync.Once 初始化,同一进程只生效一次。 +// 该用例必须最先在 fail-fast 用例之前运行以保证 Once 在合法状态下被消费; +// 为避免测试顺序耦合,本用例独立 t.Run 顺序中放最后,并在合法 key 下幂等。 +func TestProvideMessageEncryptor_EnabledValidKey(t *testing.T) { + // 确保加密器在合法状态:直接以合法 key 初始化一次 + if err := crypto.InitMessageEncryptor(validTestKey, 1); err != nil { + t.Fatalf("pre-init encryptor failed: %v", err) + } + + cfg := &config.Config{ + Encryption: config.EncryptionConfig{ + Enabled: true, + Key: validTestKey, + KeyVersion: 1, + }, + } + + err := ProvideMessageEncryptor(cfg) + if err != nil { + t.Fatalf("valid encryption config should succeed, got: %v", err) + } + + enc := crypto.GetMessageEncryptor() + if enc == nil { + t.Fatal("encryptor should be initialized after valid config") + } +} diff --git a/internal/wire/repository.go b/internal/wire/repository.go index e1884fb..11e77d4 100644 --- a/internal/wire/repository.go +++ b/internal/wire/repository.go @@ -1,4 +1,4 @@ -package wire +package wire import ( "with_you/internal/cache" @@ -24,9 +24,9 @@ var RepositorySet = wire.NewSet( repository.NewVoteRepository, repository.NewChannelRepository, repository.NewScheduleRepository, - repository.NewGradeRepository, - repository.NewExamRepository, - repository.NewEmptyClassroomRepository, + repository.NewGradeRepository, + repository.NewExamRepository, + repository.NewEmptyClassroomRepository, repository.NewMaterialSubjectRepository, repository.NewMaterialFileRepository, repository.NewCallRepository, @@ -53,8 +53,8 @@ var RepositorySet = wire.NewSet( // 二手交易相关 repository.NewTradeRepository, - // 版本日志同步 - repository.NewConversationVersionLogRepository, + // 版本日志同步 + repository.NewConversationVersionLogRepository, ) // ProvideUserActivityRepository 提供用户活跃数据仓储 diff --git a/internal/wire/service.go b/internal/wire/service.go index df80b20..32a3013 100644 --- a/internal/wire/service.go +++ b/internal/wire/service.go @@ -1,4 +1,4 @@ -package wire +package wire import ( "time" @@ -46,9 +46,9 @@ var ServiceSet = wire.NewSet( ProvideChatService, ProvideScheduleService, ProvideScheduleSyncService, - ProvideGradeSyncService, - ProvideExamSyncService, - ProvideEmptyClassroomSyncService, + ProvideGradeSyncService, + ProvideExamSyncService, + ProvideEmptyClassroomSyncService, ProvideGroupService, ProvideUploadService, ProvideUserActivityService, @@ -87,7 +87,7 @@ var ServiceSet = wire.NewSet( ) // ProvideJWTService 提供 JWT 服务 -func ProvideJWTService(cfg *config.Config) *service.JWTService { +func ProvideJWTService(cfg *config.Config) service.JWTService { return service.NewJWTService( cfg.JWT.Secret, int64(cfg.JWT.AccessTokenExpire.Seconds()), @@ -101,7 +101,7 @@ func ProvideEmailService(client email.Client) service.EmailService { } // ProvidePostAIService 提供 AI 服务 -func ProvidePostAIService(openAIClient openai.Client, tencentClient tencent.Client, cfg *config.Config) *service.PostAIService { +func ProvidePostAIService(openAIClient openai.Client, tencentClient tencent.Client, cfg *config.Config) service.PostAIService { return service.NewPostAIService(openAIClient, tencentClient, cfg.OpenAI.StrictModeration) } @@ -130,7 +130,7 @@ func ProvideSystemMessageService( func ProvidePostService( postRepo repository.PostRepository, systemMessageService service.SystemMessageService, - postAIService *service.PostAIService, + postAIService service.PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager, hookManager *hook.Manager, @@ -145,11 +145,11 @@ func ProvideCommentService( commentRepo repository.CommentRepository, postRepo repository.PostRepository, systemMessageService service.SystemMessageService, - postAIService *service.PostAIService, + postAIService service.PostAIService, cacheBackend cache.Cache, hookManager *hook.Manager, logService *service.LogService, -) *service.CommentService { +) service.CommentService { return service.NewCommentService(commentRepo, postRepo, systemMessageService, postAIService, cacheBackend, hookManager, logService) } @@ -157,8 +157,8 @@ func ProvideCommentService( func ProvideMessageService( messageRepo repository.MessageRepository, cacheBackend cache.Cache, - uploadService *service.UploadService, -) *service.MessageService { + uploadService service.UploadService, +) service.MessageService { return service.NewMessageService(messageRepo, cacheBackend, uploadService) } @@ -166,7 +166,7 @@ func ProvideMessageService( func ProvideNotificationService( notificationRepo repository.NotificationRepository, cacheBackend cache.Cache, -) *service.NotificationService { +) service.NotificationService { return service.NewNotificationService(notificationRepo, cacheBackend) } @@ -193,11 +193,11 @@ func ProvideVoteService( voteRepo repository.VoteRepository, postRepo repository.PostRepository, cache cache.Cache, - postAIService *service.PostAIService, + postAIService service.PostAIService, systemMessageService service.SystemMessageService, hookManager *hook.Manager, logService *service.LogService, -) *service.VoteService { +) service.VoteService { return service.NewVoteService(voteRepo, postRepo, cache, postAIService, systemMessageService, hookManager, logService) } @@ -212,7 +212,7 @@ func ProvideChatService( userRepo repository.UserRepository, publisher ws.MessagePublisher, cacheBackend cache.Cache, - uploadService *service.UploadService, + uploadService service.UploadService, pushService service.PushService, seqBufferMgr *cache.SeqBufferManager, pushWorker *service.PushWorker, @@ -291,7 +291,7 @@ func ProvideGroupService( func ProvideUploadService( s3Client *s3.Client, userService service.UserService, -) *service.UploadService { +) service.UploadService { return service.NewUploadService(s3Client, userService) } @@ -432,11 +432,11 @@ func ProvideHotRankWorker(cfg *config.Config, postRepo repository.PostRepository func ProvideQRCodeLoginService( redisClient *redis.Client, publisher ws.MessagePublisher, - jwtService *service.JWTService, + jwtService service.JWTService, userService service.UserService, activityService service.UserActivityService, logService *service.LogService, -) *service.QRCodeLoginService { +) service.QRCodeLoginService { return service.NewQRCodeLoginService(redisClient, publisher, jwtService, userService, activityService, logService) }