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 = ``
-
-// 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, " 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)
}