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

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

View File

@@ -1,4 +1,4 @@
//go:build wireinject //go:build wireinject
// +build wireinject // +build wireinject
package main package main
@@ -32,7 +32,7 @@ func ProvideRouter(
messageHandler *handler.MessageHandler, messageHandler *handler.MessageHandler,
notificationHandler *handler.NotificationHandler, notificationHandler *handler.NotificationHandler,
uploadHandler *handler.UploadHandler, uploadHandler *handler.UploadHandler,
jwtService *service.JWTService, jwtService service.JWTService,
pushHandler *handler.PushHandler, pushHandler *handler.PushHandler,
systemMessageHandler *handler.SystemMessageHandler, systemMessageHandler *handler.SystemMessageHandler,
groupHandler *handler.GroupHandler, groupHandler *handler.GroupHandler,
@@ -63,6 +63,7 @@ func ProvideRouter(
logService *service.LogService, logService *service.LogService,
activityService service.UserActivityService, activityService service.UserActivityService,
casbinService service.CasbinService, casbinService service.CasbinService,
userService service.UserService,
wsHandler *handler.WSHandler, wsHandler *handler.WSHandler,
liveKitHandler *handler.LiveKitHandler, liveKitHandler *handler.LiveKitHandler,
) *router.Router { ) *router.Router {
@@ -95,7 +96,7 @@ func ProvideRouter(
QRCodeHandler: qrcodeHandler, QRCodeHandler: qrcodeHandler,
MaterialHandler: materialHandler, MaterialHandler: materialHandler,
CallHandler: callHandler, CallHandler: callHandler,
LiveKitHandler: liveKitHandler, LiveKitHandler: liveKitHandler,
ReportHandler: reportHandler, ReportHandler: reportHandler,
AdminReportHandler: adminReportHandler, AdminReportHandler: adminReportHandler,
VerificationHandler: verificationHandler, VerificationHandler: verificationHandler,
@@ -106,6 +107,7 @@ func ProvideRouter(
LogService: logService, LogService: logService,
ActivityService: activityService, ActivityService: activityService,
CasbinService: casbinService, CasbinService: casbinService,
UserService: userService,
WSHandler: wsHandler, WSHandler: wsHandler,
}) })
} }

View File

@@ -110,13 +110,13 @@ func InitializeApp() (*App, error) {
scheduleHandler := wire.ProvideScheduleHandler(scheduleService, scheduleSyncService) scheduleHandler := wire.ProvideScheduleHandler(scheduleService, scheduleSyncService)
gradeRepository := repository.NewGradeRepository(db) gradeRepository := repository.NewGradeRepository(db)
gradeSyncService := wire.ProvideGradeSyncService(taskManager, gradeRepository, logger) gradeSyncService := wire.ProvideGradeSyncService(taskManager, gradeRepository, logger)
gradeHandler := handler.NewGradeHandler(gradeSyncService, gradeRepository) gradeHandler := handler.NewGradeHandler(gradeSyncService)
examRepository := repository.NewExamRepository(db) examRepository := repository.NewExamRepository(db)
examSyncService := wire.ProvideExamSyncService(taskManager, examRepository, logger) examSyncService := wire.ProvideExamSyncService(taskManager, examRepository, logger)
examHandler := handler.NewExamHandler(examSyncService, examRepository) examHandler := handler.NewExamHandler(examSyncService)
emptyClassroomRepository := repository.NewEmptyClassroomRepository(db) emptyClassroomRepository := repository.NewEmptyClassroomRepository(db)
emptyClassroomSyncService := wire.ProvideEmptyClassroomSyncService(taskManager, emptyClassroomRepository, logger) emptyClassroomSyncService := wire.ProvideEmptyClassroomSyncService(taskManager, emptyClassroomRepository, logger)
emptyClassroomHandler := handler.NewEmptyClassroomHandler(emptyClassroomSyncService, emptyClassroomRepository) emptyClassroomHandler := handler.NewEmptyClassroomHandler(emptyClassroomSyncService)
roleRepository := wire.ProvideRoleRepository(db) roleRepository := wire.ProvideRoleRepository(db)
enforcer := wire.ProvideCasbinEnforcer(config, db) enforcer := wire.ProvideCasbinEnforcer(config, db)
casbinService := wire.ProvideCasbinService(enforcer, cache, roleRepository, config) casbinService := wire.ProvideCasbinService(enforcer, cache, roleRepository, config)
@@ -151,17 +151,17 @@ func InitializeApp() (*App, error) {
verificationService := wire.ProvideVerificationService(verificationRepository, userRepository) verificationService := wire.ProvideVerificationService(verificationRepository, userRepository)
verificationHandler := handler.NewVerificationHandler(verificationService) verificationHandler := handler.NewVerificationHandler(verificationService)
adminVerificationService := wire.ProvideAdminVerificationService(verificationRepository, userRepository) adminVerificationService := wire.ProvideAdminVerificationService(verificationRepository, userRepository)
adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userRepository) adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userService)
adminProfileAuditHandler := handler.NewAdminProfileAuditHandler(userProfileAuditService) adminProfileAuditHandler := handler.NewAdminProfileAuditHandler(userProfileAuditService)
setupService := wire.ProvideSetupService(userRepository, roleRepository, casbinService, config) setupService := wire.ProvideSetupService(userRepository, roleRepository, casbinService, config)
setupHandler := wire.ProvideSetupHandler(setupService) setupHandler := wire.ProvideSetupHandler(setupService)
tradeRepository := repository.NewTradeRepository(db) tradeRepository := repository.NewTradeRepository(db)
tradeService := wire.ProvideTradeService(tradeRepository) tradeService := wire.ProvideTradeService(tradeRepository)
tradeHandler := handler.NewTradeHandler(tradeService) 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) liveKitService := wire.ProvideLiveKitService(config, logger)
liveKitHandler := wire.ProvideLiveKitHandler(liveKitService, callService, 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) hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, channelRepository, cache, client)
server := wire.ProvideGRPCServer(config, logger, runnerHub) server := wire.ProvideGRPCServer(config, logger, runnerHub)
runnerRegistry := wire.ProvideRunnerRegistry(config, client) runnerRegistry := wire.ProvideRunnerRegistry(config, client)
@@ -181,7 +181,7 @@ func ProvideRouter(
messageHandler *handler.MessageHandler, messageHandler *handler.MessageHandler,
notificationHandler *handler.NotificationHandler, notificationHandler *handler.NotificationHandler,
uploadHandler *handler.UploadHandler, uploadHandler *handler.UploadHandler,
jwtService *service.JWTService, jwtService service.JWTService,
pushHandler *handler.PushHandler, pushHandler *handler.PushHandler,
systemMessageHandler *handler.SystemMessageHandler, systemMessageHandler *handler.SystemMessageHandler,
groupHandler *handler.GroupHandler, groupHandler *handler.GroupHandler,
@@ -212,6 +212,7 @@ func ProvideRouter(
logService *service.LogService, logService *service.LogService,
activityService service.UserActivityService, activityService service.UserActivityService,
casbinService service.CasbinService, casbinService service.CasbinService,
userService service.UserService,
wsHandler *handler.WSHandler, wsHandler *handler.WSHandler,
liveKitHandler *handler.LiveKitHandler, liveKitHandler *handler.LiveKitHandler,
) *router.Router { ) *router.Router {
@@ -255,6 +256,7 @@ func ProvideRouter(
LogService: logService, LogService: logService,
ActivityService: activityService, ActivityService: activityService,
CasbinService: casbinService, CasbinService: casbinService,
UserService: userService,
WSHandler: wsHandler, WSHandler: wsHandler,
}) })
} }

View File

@@ -44,7 +44,6 @@ const (
keyPrefixMsgSeq = "msg_seq" // Seq 计数器 keyPrefixMsgSeq = "msg_seq" // Seq 计数器
keyPrefixMsgPage = "msg_page" // 分页缓存 keyPrefixMsgPage = "msg_page" // 分页缓存
keyPrefixMsgIdempotent = "msg_idem" // 消息幂等键 keyPrefixMsgIdempotent = "msg_idem" // 消息幂等键
keyPrefixSeqBuffer = "seq_buf" // Seq 预分配缓冲区 Hash
) )
// PostListKey 生成帖子列表缓存键 // PostListKey 生成帖子列表缓存键
@@ -198,12 +197,6 @@ func MessageIdempotentKey(senderID, clientMsgID string) string {
return fmt.Sprintf("%s:%s:%s", keyPrefixMsgIdempotent, senderID, clientMsgID) 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} // UserReadSeqKey 用户已读位置缓存键OpenIM 风格 SEQ_USER_READ:{convID}:{userID}
func UserReadSeqKey(convID, userID string) string { func UserReadSeqKey(convID, userID string) string {
return fmt.Sprintf("%s:%s:%s", PrefixUserReadSeq, convID, userID) return fmt.Sprintf("%s:%s:%s", PrefixUserReadSeq, convID, userID)

View File

@@ -94,17 +94,6 @@ func (m *SeqBufferManager) IsEnabled() bool {
return m.config.Enabled && m.rdb != nil 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 // NewSeqBufferManagerFromRedisPkg 从 pkg/redis.Client 创建 SeqBufferManager
func NewSeqBufferManagerFromRedisPkg(redisPkgClient *redisPkg.Client, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager { func NewSeqBufferManagerFromRedisPkg(redisPkgClient *redisPkg.Client, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
if redisPkgClient == nil { if redisPkgClient == nil {

View File

@@ -1,4 +1,9 @@
package model // Package database 负责数据库连接初始化、自动迁移与种子数据。
//
// 这些引导职责从 model 包拆分而来model 包应只包含纯数据结构定义与
// GORM 钩子,不应依赖 config 或持有启动逻辑。本包依赖 config + model
// 方向合法(引导层 → 模型层 / 配置层)。
package database
import ( import (
"fmt" "fmt"
@@ -14,6 +19,7 @@ import (
"gorm.io/plugin/dbresolver" "gorm.io/plugin/dbresolver"
"with_you/internal/config" "with_you/internal/config"
"with_you/internal/model"
) )
// NewDB 创建数据库连接(用于 Wire 依赖注入) // NewDB 创建数据库连接(用于 Wire 依赖注入)
@@ -128,103 +134,103 @@ func parseGormLogLevel(level string) logger.LogLevel {
func autoMigrate(db *gorm.DB) error { func autoMigrate(db *gorm.DB) error {
err := db.AutoMigrate( err := db.AutoMigrate(
// 用户相关 // 用户相关
&User{}, &model.User{},
// 帖子相关 // 帖子相关
&Post{}, &model.Post{},
&PostImage{}, &model.PostImage{},
&Channel{}, &model.Channel{},
// 评论相关 // 评论相关
&Comment{}, &model.Comment{},
&CommentLike{}, &model.CommentLike{},
// 消息相关使用雪花算法ID和seq机制 // 消息相关使用雪花算法ID和seq机制
// 已读位置存储在 ConversationParticipant.LastReadSeq 中 // 已读位置存储在 ConversationParticipant.LastReadSeq 中
&Conversation{}, &model.Conversation{},
&ConversationParticipant{}, &model.ConversationParticipant{},
&Message{}, &model.Message{},
// 系统通知(独立表,每个用户只能看到自己的通知) // 系统通知(独立表,每个用户只能看到自己的通知)
&SystemNotification{}, &model.SystemNotification{},
// 通知 // 通知
&Notification{}, &model.Notification{},
// 推送中心相关 // 推送中心相关
&PushRecord{}, // 推送记录 &model.PushRecord{}, // 推送记录
&DeviceToken{}, // 设备Token &model.DeviceToken{}, // 设备Token
// 社交 // 社交
&Follow{}, &model.Follow{},
&UserBlock{}, &model.UserBlock{},
&PostLike{}, &model.PostLike{},
&Favorite{}, &model.Favorite{},
// 投票 // 投票
&VoteOption{}, &model.VoteOption{},
&UserVote{}, &model.UserVote{},
// 敏感词和审核 // 敏感词和审核
&SensitiveWord{}, &model.SensitiveWord{},
// &AuditLog{}, // TODO: define AuditLog model // &model.AuditLog{}, // TODO: define AuditLog model
// 举报 // 举报
&Report{}, &model.Report{},
// 日志相关 // 日志相关
&OperationLog{}, // 操作日志 &model.OperationLog{}, // 操作日志
&LoginLog{}, // 登录日志 &model.LoginLog{}, // 登录日志
&DataChangeLog{}, // 数据变更日志 &model.DataChangeLog{}, // 数据变更日志
// 群组相关 // 群组相关
&Group{}, &model.Group{},
&GroupMember{}, &model.GroupMember{},
&GroupAnnouncement{}, &model.GroupAnnouncement{},
&GroupJoinRequest{}, &model.GroupJoinRequest{},
// 自定义表情 // 自定义表情
&UserSticker{}, &model.UserSticker{},
// 课表 // 课表
&ScheduleCourse{}, &model.ScheduleCourse{},
// 成绩与考试 // 成绩与考试
&Grade{}, &model.Grade{},
&GpaSummary{}, &model.GpaSummary{},
&Exam{}, &model.Exam{},
&EmptyClassroom{}, &model.EmptyClassroom{},
// 用户活跃相关 // 用户活跃相关
&UserActiveLog{}, &model.UserActiveLog{},
&UserActivityStat{}, &model.UserActivityStat{},
// 角色和权限相关 // 角色和权限相关
&Role{}, &model.Role{},
&UserRole{}, &model.UserRole{},
&CasbinRule{}, &model.CasbinRule{},
// 学习资料相关 // 学习资料相关
&MaterialSubject{}, &model.MaterialSubject{},
&MaterialFile{}, &model.MaterialFile{},
// 通话相关 // 通话相关
&CallSession{}, &model.CallSession{},
&CallParticipant{}, &model.CallParticipant{},
// 身份认证相关 // 身份认证相关
&VerificationRecord{}, &model.VerificationRecord{},
// 用户资料审核相关 // 用户资料审核相关
&UserProfileAudit{}, &model.UserProfileAudit{},
// 帖子内链引用关系 // 帖子内链引用关系
&PostReference{}, &model.PostReference{},
// 二手交易相关 // 二手交易相关
&TradeItem{}, &model.TradeItem{},
&TradeImage{}, &model.TradeImage{},
&TradeFavorite{}, &model.TradeFavorite{},
) )
if err != nil { if err != nil {
return err return err
@@ -267,7 +273,7 @@ func (postWithHotScore) TableName() string { return "posts" }
func dropPostsHotScoreColumnIfExists(db *gorm.DB) error { func dropPostsHotScoreColumnIfExists(db *gorm.DB) error {
m := db.Migrator() m := db.Migrator()
if !m.HasTable(&Post{}) { if !m.HasTable(&model.Post{}) {
return nil return nil
} }
if !m.HasColumn(&postWithHotScore{}, "HotScore") { if !m.HasColumn(&postWithHotScore{}, "HotScore") {
@@ -280,7 +286,7 @@ func dropPostsHotScoreColumnIfExists(db *gorm.DB) error {
func seedRoles(db *gorm.DB) error { func seedRoles(db *gorm.DB) error {
// 检查是否已有角色数据 // 检查是否已有角色数据
var count int64 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 return err
} }
@@ -290,33 +296,33 @@ func seedRoles(db *gorm.DB) error {
} }
// 预定义角色数据 // 预定义角色数据
roles := []Role{ roles := []model.Role{
{ {
Name: RoleSuperAdmin, Name: model.RoleSuperAdmin,
DisplayName: "超级管理员", DisplayName: "超级管理员",
Description: "拥有系统最高权限,可以管理所有用户和内容", Description: "拥有系统最高权限,可以管理所有用户和内容",
Priority: 100, Priority: 100,
}, },
{ {
Name: RoleAdmin, Name: model.RoleAdmin,
DisplayName: "管理员", DisplayName: "管理员",
Description: "系统管理员,可以管理用户和内容", Description: "系统管理员,可以管理用户和内容",
Priority: 80, Priority: 80,
}, },
{ {
Name: RoleModerator, Name: model.RoleModerator,
DisplayName: "版主", DisplayName: "版主",
Description: "内容审核员,可以审核和管理内容", Description: "内容审核员,可以审核和管理内容",
Priority: 60, Priority: 60,
}, },
{ {
Name: RoleUser, Name: model.RoleUser,
DisplayName: "普通用户", DisplayName: "普通用户",
Description: "注册用户,拥有基本权限", Description: "注册用户,拥有基本权限",
Priority: 40, Priority: 40,
}, },
{ {
Name: RoleBanned, Name: model.RoleBanned,
DisplayName: "被封禁用户", DisplayName: "被封禁用户",
Description: "被禁止访问的用户", Description: "被禁止访问的用户",
Priority: 0, Priority: 0,
@@ -338,7 +344,7 @@ func seedRoles(db *gorm.DB) error {
func seedPermissions(db *gorm.DB) error { func seedPermissions(db *gorm.DB) error {
// 检查是否已有权限策略数据 // 检查是否已有权限策略数据
var count int64 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 return err
} }
@@ -349,34 +355,34 @@ func seedPermissions(db *gorm.DB) error {
// 预定义权限策略数据 // 预定义权限策略数据
// p = sub, obj, act (角色, 资源, 操作) // 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: model.RoleAdmin, V1: "/api/v1/admin/*", V2: "*"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/users/*", V2: "GET"}, {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/users/*", V2: "GET"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/posts/*", V2: "*"}, {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/posts/*", V2: "*"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/comments/*", V2: "*"}, {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/comments/*", V2: "*"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/groups/*", V2: "*"}, {Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/groups/*", V2: "*"},
// 版主 - 拥有内容审核权限 // 版主 - 拥有内容审核权限
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/posts/*", V2: "GET"}, {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/posts/*", V2: "GET"},
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/posts/*", V2: "DELETE"}, {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/posts/*", V2: "DELETE"},
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/comments/*", V2: "GET"}, {Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/comments/*", V2: "GET"},
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/comments/*", V2: "DELETE"}, {Ptype: "p", V0: model.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/groups/*", V2: "GET"},
// 普通用户 - 拥有基本权限 // 普通用户 - 拥有基本权限
{Ptype: "p", V0: RoleUser, V1: "/api/v1/posts", V2: "GET"}, {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/posts/*", V2: "GET"}, {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts/*", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/posts", V2: "POST"}, {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts", V2: "POST"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/comments/*", V2: "GET"}, {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/comments/*", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/comments", V2: "POST"}, {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/comments", V2: "POST"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/users/me", V2: "GET"}, {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/users/me", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/users/me", V2: "PUT"}, {Ptype: "p", V0: model.RoleUser, V1: "/api/v1/users/me", V2: "PUT"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/groups", V2: "GET"}, {Ptype: "p", V0: model.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/groups/*", V2: "GET"},
// 被封禁用户 - 无权限 // 被封禁用户 - 无权限
} }
@@ -396,7 +402,7 @@ func seedPermissions(db *gorm.DB) error {
func seedChannels(db *gorm.DB) error { func seedChannels(db *gorm.DB) error {
// 检查是否已有频道数据 // 检查是否已有频道数据
var count int64 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 return err
} }
if count > 0 { if count > 0 {
@@ -404,7 +410,7 @@ func seedChannels(db *gorm.DB) error {
return nil return nil
} }
defaultChannels := []Channel{ defaultChannels := []model.Channel{
{Name: "跳蚤市场", Slug: "market", Description: "二手交易与闲置发布", SortOrder: 10, IsActive: true}, {Name: "跳蚤市场", Slug: "market", Description: "二手交易与闲置发布", SortOrder: 10, IsActive: true},
{Name: "课程交流", Slug: "courses", Description: "课程资料、选课与学习讨论", SortOrder: 20, IsActive: true}, {Name: "课程交流", Slug: "courses", Description: "课程资料、选课与学习讨论", SortOrder: 20, IsActive: true},
{Name: "工作实习", Slug: "jobs", Description: "实习、内推与求职信息", SortOrder: 30, 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 { func seedMaterialSubjects(db *gorm.DB) error {
// 检查是否已有数据 // 检查是否已有数据
var count int64 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 return err
} }
if count > 0 { if count > 0 {
@@ -432,7 +438,7 @@ func seedMaterialSubjects(db *gorm.DB) error {
return nil return nil
} }
defaultSubjects := []MaterialSubject{ defaultSubjects := []model.MaterialSubject{
{Name: "数学", Icon: "calculator-variant", Color: "#FF6B6B", Description: "高等数学、线性代数、概率论等", SortOrder: 10, IsActive: true}, {Name: "数学", Icon: "calculator-variant", Color: "#FF6B6B", Description: "高等数学、线性代数、概率论等", SortOrder: 10, IsActive: true},
{Name: "英语", Icon: "translate", Color: "#4ECDC4", Description: "四六级、考研英语、托福雅思等", SortOrder: 20, IsActive: true}, {Name: "英语", Icon: "translate", Color: "#4ECDC4", Description: "四六级、考研英语、托福雅思等", SortOrder: 20, IsActive: true},
{Name: "物理", Icon: "atom", Color: "#45B7D1", Description: "大学物理、电磁学、力学等", SortOrder: 30, 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))) zap.L().Info("Seeded material subjects successfully", zap.Int("count", len(defaultSubjects)))
return nil return nil
} }

View File

@@ -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_createddata_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 引用可用(供未来临时文件场景扩展)

View File

@@ -131,52 +131,6 @@ type AdminUpdateUserStatusRequest struct {
Status string `json:"status" binding:"required,oneof=active banned"` 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 ==================== // ==================== Comment DTOs ====================
// CommentImageResponse 评论图片响应 // CommentImageResponse 评论图片响应
@@ -224,16 +178,16 @@ type NotificationResponse struct {
type SegmentType string type SegmentType string
const ( const (
SegmentTypeText SegmentType = "text" SegmentTypeText SegmentType = "text"
SegmentTypeImage SegmentType = "image" SegmentTypeImage SegmentType = "image"
SegmentTypeVoice SegmentType = "voice" SegmentTypeVoice SegmentType = "voice"
SegmentTypeVideo SegmentType = "video" SegmentTypeVideo SegmentType = "video"
SegmentTypeFile SegmentType = "file" SegmentTypeFile SegmentType = "file"
SegmentTypeAt SegmentType = "at" SegmentTypeAt SegmentType = "at"
SegmentTypeReply SegmentType = "reply" SegmentTypeReply SegmentType = "reply"
SegmentTypeFace SegmentType = "face" SegmentTypeFace SegmentType = "face"
SegmentTypeLink SegmentType = "link" SegmentTypeLink SegmentType = "link"
SegmentTypeVote SegmentType = "vote" SegmentTypeVote SegmentType = "vote"
SegmentTypePostRef SegmentType = "post_ref" SegmentTypePostRef SegmentType = "post_ref"
) )
@@ -428,8 +382,8 @@ type CreateConversationRequest struct {
// SendMessageRequest 发送消息请求 // SendMessageRequest 发送消息请求
type SendMessageRequest struct { type SendMessageRequest struct {
Segments model.MessageSegments `json:"segments" binding:"required"` // 消息链(必须) Segments model.MessageSegments `json:"segments" binding:"required"` // 消息链(必须)
ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID (string类型) ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID (string类型)
ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID幂等性去重 ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID幂等性去重
} }
// MarkReadRequest 标记已读请求 // MarkReadRequest 标记已读请求
@@ -619,122 +573,6 @@ func FormatTimePointer(t *time.Time) string {
return FormatTime(*t) 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 ==================== // ==================== WebSocket Event DTOs ====================
// WSEventResponse WebSocket事件响应结构体 // WSEventResponse WebSocket事件响应结构体
@@ -745,7 +583,7 @@ type WSEventResponse struct {
Type string `json:"type"` // 事件类型 (message, notification, system等) Type string `json:"type"` // 事件类型 (message, notification, system等)
DetailType string `json:"detail_type"` // 详细类型 (private, group, like, comment等) DetailType string `json:"detail_type"` // 详细类型 (private, group, like, comment等)
ConversationID string `json:"conversation_id"` // 会话ID ConversationID string `json:"conversation_id"` // 会话ID
Seq int64 `json:"seq"` // 消息序列号 Seq int64 `json:"seq"` // 消息序列号
Segments model.MessageSegments `json:"segments"` // 消息段数组 Segments model.MessageSegments `json:"segments"` // 消息段数组
SenderID string `json:"sender_id"` // 发送者用户ID SenderID string `json:"sender_id"` // 发送者用户ID
} }
@@ -754,11 +592,11 @@ type WSEventResponse struct {
// SendMessageParams 发送消息参数(用于 REST API // SendMessageParams 发送消息参数(用于 REST API
type SendMessageParams struct { type SendMessageParams struct {
DetailType string `json:"detail_type"` // 消息类型: private, group DetailType string `json:"detail_type"` // 消息类型: private, group
ConversationID string `json:"conversation_id"` // 会话ID ConversationID string `json:"conversation_id"` // 会话ID
Segments model.MessageSegments `json:"segments"` // 消息内容(消息段数组) Segments model.MessageSegments `json:"segments"` // 消息内容(消息段数组)
ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID
ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID幂等性去重 ClientMsgID string `json:"client_msg_id,omitempty"` // 客户端消息ID幂等性去重
} }
// DeleteMsgParams 撤回消息参数 // DeleteMsgParams 撤回消息参数
@@ -809,7 +647,7 @@ type SetGroupAvatarParams struct {
// SetGroupDescriptionParams 设置群描述参数 // SetGroupDescriptionParams 设置群描述参数
type SetGroupDescriptionParams struct { type SetGroupDescriptionParams struct {
GroupID string `json:"group_id"` // 群组ID GroupID string `json:"group_id"` // 群组ID
Description string `json:"description"` // 群描述 Description string `json:"description"` // 群描述
} }
@@ -935,41 +773,6 @@ type GetMyMemberInfoParams struct {
GroupID string `json:"group_id"` // 群组ID 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 ==================== // ==================== WebSocket Response DTOs ====================
// WSResponse WebSocket响应结构体 // WSResponse WebSocket响应结构体

View File

@@ -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
}

119
internal/dto/group_dto.go Normal file
View File

@@ -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"`
}

86
internal/dto/post_dto.go Normal file
View File

@@ -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"`
}

View File

@@ -1,4 +1,4 @@
package dto package dto
import ( import (
"with_you/internal/model" "with_you/internal/model"
@@ -8,10 +8,10 @@ import (
// CreateReportRequest 创建举报请求 // CreateReportRequest 创建举报请求
type CreateReportRequest struct { type CreateReportRequest struct {
TargetType string `json:"target_type" binding:"required,oneof=post comment message"` TargetType string `json:"target_type" binding:"required,oneof=post comment message"`
TargetID string `json:"target_id" binding:"required"` TargetID string `json:"target_id" binding:"required"`
Reason string `json:"reason" binding:"required,oneof=spam inappropriate harassment misinformation other"` Reason string `json:"reason" binding:"required,oneof=spam inappropriate harassment misinformation other"`
Description string `json:"description" binding:"max=500"` Description string `json:"description" binding:"max=500"`
} }
// ReportResponse 举报响应 // ReportResponse 举报响应
@@ -26,89 +26,77 @@ type ReportResponse struct {
CreatedAt string `json:"created_at"` 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 管理端举报列表响应 // AdminReportListResponse 管理端举报列表响应
type AdminReportListResponse struct { type AdminReportListResponse struct {
ID string `json:"id"` ID string `json:"id"`
ReporterID string `json:"reporter_id"` ReporterID string `json:"reporter_id"`
Reporter *UserResponse `json:"reporter,omitempty"` Reporter *UserResponse `json:"reporter,omitempty"`
TargetType string `json:"target_type"` TargetType string `json:"target_type"`
TargetID string `json:"target_id"` TargetID string `json:"target_id"`
Reason string `json:"reason"` Reason string `json:"reason"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Status string `json:"status"` Status string `json:"status"`
HandledBy *string `json:"handled_by,omitempty"` HandledBy *string `json:"handled_by,omitempty"`
Handler *UserResponse `json:"handler,omitempty"` Handler *UserResponse `json:"handler,omitempty"`
HandledAt *string `json:"handled_at,omitempty"` HandledAt *string `json:"handled_at,omitempty"`
Result *string `json:"result,omitempty"` Result *string `json:"result,omitempty"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
} }
// AdminReportDetailResponse 管理端举报详情响应 // AdminReportDetailResponse 管理端举报详情响应
type AdminReportDetailResponse struct { type AdminReportDetailResponse struct {
ID string `json:"id"` ID string `json:"id"`
ReporterID string `json:"reporter_id"` ReporterID string `json:"reporter_id"`
Reporter *UserResponse `json:"reporter"` Reporter *UserResponse `json:"reporter"`
TargetType string `json:"target_type"` TargetType string `json:"target_type"`
TargetID string `json:"target_id"` TargetID string `json:"target_id"`
Reason string `json:"reason"` Reason string `json:"reason"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Status string `json:"status"` Status string `json:"status"`
HandledBy *string `json:"handled_by,omitempty"` HandledBy *string `json:"handled_by,omitempty"`
Handler *UserResponse `json:"handler,omitempty"` Handler *UserResponse `json:"handler,omitempty"`
HandledAt *string `json:"handled_at,omitempty"` HandledAt *string `json:"handled_at,omitempty"`
Result *string `json:"result,omitempty"` Result *string `json:"result,omitempty"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
// 被举报内容详情 // 被举报内容详情
TargetContent *ReportTargetContent `json:"target_content,omitempty"` TargetContent *ReportTargetContent `json:"target_content,omitempty"`
} }
// ReportTargetContent 被举报内容详情 // ReportTargetContent 被举报内容详情
type ReportTargetContent struct { type ReportTargetContent struct {
Type string `json:"type"` // post, comment, message Type string `json:"type"` // post, comment, message
ID string `json:"id"` ID string `json:"id"`
Author *UserResponse `json:"author,omitempty"` Author *UserResponse `json:"author,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Title string `json:"title,omitempty"` // 帖子标题 Title string `json:"title,omitempty"` // 帖子标题
Images []string `json:"images,omitempty"` // 图片列表 Images []string `json:"images,omitempty"` // 图片列表
Status string `json:"status,omitempty"` // 内容状态 Status string `json:"status,omitempty"` // 内容状态
CreatedAt string `json:"created_at,omitempty"` CreatedAt string `json:"created_at,omitempty"`
} }
// HandleReportRequest 处理举报请求 // HandleReportRequest 处理举报请求
type HandleReportRequest struct { type HandleReportRequest struct {
Action string `json:"action" binding:"required,oneof=approve reject"` // approve: 确认违规, reject: 驳回 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 批量处理举报请求 // BatchHandleReportRequest 批量处理举报请求
type BatchHandleReportRequest struct { type BatchHandleReportRequest struct {
IDs []string `json:"ids" binding:"required,min=1,max=100"` IDs []string `json:"ids" binding:"required,min=1,max=100"`
Action string `json:"action" binding:"required,oneof=approve reject"` Action string `json:"action" binding:"required,oneof=approve reject"`
Result string `json:"result" binding:"max=500"` Result string `json:"result" binding:"max=500"`
} }
// AdminBatchHandleResponse 批量处理响应 // AdminBatchHandleResponse 批量处理响应
type AdminBatchHandleResponse struct { type AdminBatchHandleResponse struct {
SuccessCount int `json:"success_count"` SuccessCount int `json:"success_count"`
FailedCount int `json:"failed_count"` FailedCount int `json:"failed_count"`
FailedIDs []string `json:"failed_ids,omitempty"` FailedIDs []string `json:"failed_ids,omitempty"`
} }
// ==================== 转换函数 ==================== // ==================== 转换函数 ====================
// ConvertReportToResponse 将 Report 模型转换为响应 // ConvertReportToResponse 将 Report 模型转换为响应
func ConvertReportToResponse(report *model.Report) *ReportResponse { func ConvertReportToResponse(report *model.Report) *ReportResponse {
return &ReportResponse{ return &ReportResponse{
@@ -180,4 +168,4 @@ func ConvertReportToAdminDetailResponse(report *model.Report, reporter *model.Us
resp.HandledAt = &handledAt resp.HandledAt = &handledAt
} }
return resp return resp
} }

View File

@@ -4,8 +4,8 @@ import (
"strconv" "strconv"
"time" "time"
"with_you/internal/dto"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/query"
"with_you/internal/service" "with_you/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -37,7 +37,7 @@ func (h *AdminLogHandler) GetOperationLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
filters := dto.LogFilter{ filters := query.LogFilter{
UserID: c.Query("user_id"), UserID: c.Query("user_id"),
Operation: c.Query("operation"), Operation: c.Query("operation"),
TargetType: c.Query("target_type"), TargetType: c.Query("target_type"),
@@ -70,7 +70,7 @@ func (h *AdminLogHandler) GetLoginLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
filters := dto.LoginFilter{ filters := query.LoginFilter{
UserID: c.Query("user_id"), UserID: c.Query("user_id"),
Event: c.Query("event"), Event: c.Query("event"),
Result: c.Query("result"), Result: c.Query("result"),
@@ -113,7 +113,7 @@ func (h *AdminLogHandler) GetDataChangeLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
filters := dto.DataChangeFilter{ filters := query.DataChangeFilter{
UserID: c.Query("user_id"), UserID: c.Query("user_id"),
OperatorID: c.Query("operator_id"), OperatorID: c.Query("operator_id"),
ChangeType: c.Query("change_type"), ChangeType: c.Query("change_type"),
@@ -190,7 +190,7 @@ func (h *AdminLogHandler) ExportLogs(c *gin.Context) {
"logs": logs, "logs": logs,
}) })
case "login": 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 { if err != nil {
response.HandleError(c, err, "failed to export login logs") response.HandleError(c, err, "failed to export login logs")
return return
@@ -200,7 +200,7 @@ func (h *AdminLogHandler) ExportLogs(c *gin.Context) {
"log": logs, "log": logs,
}) })
case "data_change": 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 { if err != nil {
response.HandleError(c, err, "failed to export data change logs") response.HandleError(c, err, "failed to export data change logs")
return return

View File

@@ -3,6 +3,7 @@ package handler
import ( import (
"with_you/internal/dto" "with_you/internal/dto"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/query"
"with_you/internal/service" "with_you/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -24,7 +25,7 @@ func NewAdminReportHandler(adminReportService service.AdminReportService) *Admin
// List 获取举报列表 // List 获取举报列表
func (h *AdminReportHandler) List(c *gin.Context) { func (h *AdminReportHandler) List(c *gin.Context) {
// 解析查询参数 // 解析查询参数
var query dto.AdminReportListQuery var query query.AdminReportListQuery
if err := c.ShouldBindQuery(&query); err != nil { if err := c.ShouldBindQuery(&query); err != nil {
response.BadRequest(c, "参数错误: "+err.Error()) response.BadRequest(c, "参数错误: "+err.Error())
return return
@@ -136,4 +137,3 @@ func (h *AdminReportHandler) BatchHandle(c *gin.Context) {
response.Success(c, result) response.Success(c, result)
} }

View File

@@ -1,12 +1,12 @@
package handler package handler
import ( import (
"context"
"strconv" "strconv"
"with_you/internal/dto" "with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/repository"
"with_you/internal/service" "with_you/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -14,16 +14,16 @@ import (
type AdminVerificationHandler struct { type AdminVerificationHandler struct {
adminVerificationService service.AdminVerificationService adminVerificationService service.AdminVerificationService
userRepo repository.UserRepository userService service.UserService
} }
func NewAdminVerificationHandler( func NewAdminVerificationHandler(
adminVerificationService service.AdminVerificationService, adminVerificationService service.AdminVerificationService,
userRepo repository.UserRepository, userService service.UserService,
) *AdminVerificationHandler { ) *AdminVerificationHandler {
return &AdminVerificationHandler{ return &AdminVerificationHandler{
adminVerificationService: adminVerificationService, adminVerificationService: adminVerificationService,
userRepo: userRepo, userService: userService,
} }
} }
@@ -55,7 +55,7 @@ func (h *AdminVerificationHandler) ListVerifications(c *gin.Context) {
responses := make([]*dto.AdminVerificationListResponse, len(records)) responses := make([]*dto.AdminVerificationListResponse, len(records))
for i, record := range 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) response.Paginated(c, responses, total, page, pageSize)
@@ -74,7 +74,7 @@ func (h *AdminVerificationHandler) GetVerificationDetail(c *gin.Context) {
return return
} }
response.Success(c, h.convertToAdminDetailResponse(record)) response.Success(c, h.convertToAdminDetailResponse(c.Request.Context(), record))
} }
func (h *AdminVerificationHandler) ReviewVerification(c *gin.Context) { func (h *AdminVerificationHandler) ReviewVerification(c *gin.Context) {
@@ -113,10 +113,10 @@ func (h *AdminVerificationHandler) ReviewVerification(c *gin.Context) {
return 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{ resp := &dto.AdminVerificationListResponse{
ID: record.ID, ID: record.ID,
UserID: record.UserID, UserID: record.UserID,
@@ -129,7 +129,7 @@ func (h *AdminVerificationHandler) convertToAdminListResponse(record *model.Veri
CreatedAt: dto.FormatTime(record.CreatedAt), 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 { if err == nil && user != nil {
resp.Username = user.Username resp.Username = user.Username
resp.Nickname = user.Nickname resp.Nickname = user.Nickname
@@ -139,7 +139,7 @@ func (h *AdminVerificationHandler) convertToAdminListResponse(record *model.Veri
if record.ReviewedAt != nil { if record.ReviewedAt != nil {
resp.ReviewedAt = dto.FormatTime(*record.ReviewedAt) resp.ReviewedAt = dto.FormatTime(*record.ReviewedAt)
if record.ReviewedBy != nil { if record.ReviewedBy != nil {
reviewer, err := h.userRepo.GetByID(*record.ReviewedBy) reviewer, err := h.userService.GetUserByID(ctx, *record.ReviewedBy)
if err == nil && reviewer != nil { if err == nil && reviewer != nil {
resp.ReviewerName = reviewer.Nickname resp.ReviewerName = reviewer.Nickname
} }
@@ -149,7 +149,7 @@ func (h *AdminVerificationHandler) convertToAdminListResponse(record *model.Veri
return resp 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{ resp := &dto.AdminVerificationDetailResponse{
ID: record.ID, ID: record.ID,
UserID: record.UserID, UserID: record.UserID,
@@ -163,7 +163,7 @@ func (h *AdminVerificationHandler) convertToAdminDetailResponse(record *model.Ve
CreatedAt: dto.FormatTime(record.CreatedAt), 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 { if err == nil && user != nil {
resp.Username = user.Username resp.Username = user.Username
resp.Nickname = user.Nickname resp.Nickname = user.Nickname
@@ -175,7 +175,7 @@ func (h *AdminVerificationHandler) convertToAdminDetailResponse(record *model.Ve
} }
if record.ReviewedBy != nil { if record.ReviewedBy != nil {
resp.ReviewedBy = *record.ReviewedBy 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 resp.ReviewerName = reviewer.Nickname
} }
} }

View File

@@ -15,11 +15,11 @@ import (
// CommentHandler 评论处理器 // CommentHandler 评论处理器
type CommentHandler struct { type CommentHandler struct {
commentService *service.CommentService commentService service.CommentService
} }
// NewCommentHandler 创建评论处理器 // NewCommentHandler 创建评论处理器
func NewCommentHandler(commentService *service.CommentService) *CommentHandler { func NewCommentHandler(commentService service.CommentService) *CommentHandler {
return &CommentHandler{ return &CommentHandler{
commentService: commentService, commentService: commentService,
} }

View File

@@ -4,22 +4,18 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/repository"
"with_you/internal/service" "with_you/internal/service"
) )
type EmptyClassroomHandler struct { type EmptyClassroomHandler struct {
classroomSyncService service.EmptyClassroomSyncService classroomSyncService service.EmptyClassroomSyncService
classroomRepo repository.EmptyClassroomRepository
} }
func NewEmptyClassroomHandler( func NewEmptyClassroomHandler(
classroomSyncService service.EmptyClassroomSyncService, classroomSyncService service.EmptyClassroomSyncService,
classroomRepo repository.EmptyClassroomRepository,
) *EmptyClassroomHandler { ) *EmptyClassroomHandler {
return &EmptyClassroomHandler{ return &EmptyClassroomHandler{
classroomSyncService: classroomSyncService, classroomSyncService: classroomSyncService,
classroomRepo: classroomRepo,
} }
} }
@@ -77,7 +73,7 @@ func (h *EmptyClassroomHandler) ListEmptyClassrooms(c *gin.Context) {
return return
} }
classrooms, err := h.classroomRepo.ListByUserAndSemester(userID, semester) classrooms, err := h.classroomSyncService.ListClassrooms(userID, semester)
if err != nil { if err != nil {
response.HandleError(c, err, "failed to list empty classrooms") response.HandleError(c, err, "failed to list empty classrooms")
return return

View File

@@ -4,19 +4,16 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/repository"
"with_you/internal/service" "with_you/internal/service"
) )
type ExamHandler struct { type ExamHandler struct {
examSyncService service.ExamSyncService examSyncService service.ExamSyncService
examRepo repository.ExamRepository
} }
func NewExamHandler(examSyncService service.ExamSyncService, examRepo repository.ExamRepository) *ExamHandler { func NewExamHandler(examSyncService service.ExamSyncService) *ExamHandler {
return &ExamHandler{ return &ExamHandler{
examSyncService: examSyncService, examSyncService: examSyncService,
examRepo: examRepo,
} }
} }
@@ -68,7 +65,7 @@ func (h *ExamHandler) ListExams(c *gin.Context) {
return return
} }
exams, err := h.examRepo.ListByUserAndSemester(userID, semester) exams, err := h.examSyncService.ListExams(userID, semester)
if err != nil { if err != nil {
response.HandleError(c, err, "failed to list exams") response.HandleError(c, err, "failed to list exams")
return return

View File

@@ -4,19 +4,16 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/repository"
"with_you/internal/service" "with_you/internal/service"
) )
type GradeHandler struct { type GradeHandler struct {
gradeSyncService service.GradeSyncService gradeSyncService service.GradeSyncService
gradeRepo repository.GradeRepository
} }
func NewGradeHandler(gradeSyncService service.GradeSyncService, gradeRepo repository.GradeRepository) *GradeHandler { func NewGradeHandler(gradeSyncService service.GradeSyncService) *GradeHandler {
return &GradeHandler{ return &GradeHandler{
gradeSyncService: gradeSyncService, gradeSyncService: gradeSyncService,
gradeRepo: gradeRepo,
} }
} }
@@ -63,16 +60,14 @@ func (h *GradeHandler) ListGrades(c *gin.Context) {
return return
} }
grades, err := h.gradeRepo.ListByUser(userID) summary, err := h.gradeSyncService.ListGradesWithSummary(userID)
if err != nil { if err != nil {
response.HandleError(c, err, "failed to list grades") response.HandleError(c, err, "failed to list grades")
return return
} }
gpaSummary, _ := h.gradeRepo.GetLatestGpaSummary(userID)
response.Success(c, gin.H{ response.Success(c, gin.H{
"grades": grades, "grades": summary.Grades,
"gpa_summary": gpaSummary, "gpa_summary": summary.GpaSummary,
}) })
} }

View File

@@ -34,7 +34,7 @@ func NewLiveKitHandler(
liveKitService: liveKitService, liveKitService: liveKitService,
callService: callService, callService: callService,
config: &cfg.LiveKit, config: &cfg.LiveKit,
logger: logger, logger: logger,
} }
} }
@@ -163,4 +163,4 @@ func (h *LiveKitHandler) handleRoomFinished(ctx context.Context, event *livekit.
zap.Error(err), zap.Error(err),
) )
} }
} }

View File

@@ -4,9 +4,9 @@ import (
"errors" "errors"
"strconv" "strconv"
"with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/query"
"with_you/internal/service" "with_you/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -148,7 +148,7 @@ func (h *MaterialHandler) ListMaterials(c *gin.Context) {
fileType := c.Query("file_type") fileType := c.Query("file_type")
keyword := c.Query("keyword") keyword := c.Query("keyword")
params := dto.MaterialFileQueryParams{ params := query.MaterialFileQueryParams{
SubjectID: subjectID, SubjectID: subjectID,
FileType: fileType, FileType: fileType,
Keyword: keyword, Keyword: keyword,
@@ -329,7 +329,7 @@ func (h *MaterialHandler) AdminListMaterials(c *gin.Context) {
status := c.Query("status") status := c.Query("status")
keyword := c.Query("keyword") keyword := c.Query("keyword")
params := dto.MaterialFileQueryParams{ params := query.MaterialFileQueryParams{
SubjectID: subjectID, SubjectID: subjectID,
FileType: fileType, FileType: fileType,
Status: status, Status: status,

View File

@@ -87,14 +87,14 @@ func (h *MessageHandler) enrichConversations(ctx context.Context, convs []*model
// MessageHandler 消息处理器 // MessageHandler 消息处理器
type MessageHandler struct { type MessageHandler struct {
chatService service.ChatService chatService service.ChatService
messageService *service.MessageService messageService service.MessageService
userService service.UserService userService service.UserService
groupService service.GroupService groupService service.GroupService
wsPublisher ws.MessagePublisher wsPublisher ws.MessagePublisher
} }
// NewMessageHandler 创建消息处理器 // 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{ return &MessageHandler{
chatService: chatService, chatService: chatService,
messageService: messageService, messageService: messageService,

View File

@@ -13,11 +13,11 @@ import (
// NotificationHandler 通知处理器 // NotificationHandler 通知处理器
type NotificationHandler struct { type NotificationHandler struct {
notificationService *service.NotificationService notificationService service.NotificationService
} }
// NewNotificationHandler 创建通知处理器 // NewNotificationHandler 创建通知处理器
func NewNotificationHandler(notificationService *service.NotificationService) *NotificationHandler { func NewNotificationHandler(notificationService service.NotificationService) *NotificationHandler {
return &NotificationHandler{ return &NotificationHandler{
notificationService: notificationService, notificationService: notificationService,
} }

View File

@@ -14,11 +14,11 @@ import (
// QRCodeHandler 二维码登录处理器 // QRCodeHandler 二维码登录处理器
type QRCodeHandler struct { type QRCodeHandler struct {
qrcodeService *service.QRCodeLoginService qrcodeService service.QRCodeLoginService
} }
// NewQRCodeHandler 创建二维码登录处理器 // NewQRCodeHandler 创建二维码登录处理器
func NewQRCodeHandler(qrcodeService *service.QRCodeLoginService) *QRCodeHandler { func NewQRCodeHandler(qrcodeService service.QRCodeLoginService) *QRCodeHandler {
return &QRCodeHandler{ return &QRCodeHandler{
qrcodeService: qrcodeService, qrcodeService: qrcodeService,
} }

View File

@@ -9,11 +9,11 @@ import (
// UploadHandler 上传处理器 // UploadHandler 上传处理器
type UploadHandler struct { type UploadHandler struct {
uploadService *service.UploadService uploadService service.UploadService
} }
// NewUploadHandler 创建上传处理器 // NewUploadHandler 创建上传处理器
func NewUploadHandler(uploadService *service.UploadService) *UploadHandler { func NewUploadHandler(uploadService service.UploadService) *UploadHandler {
return &UploadHandler{ return &UploadHandler{
uploadService: uploadService, uploadService: uploadService,
} }

View File

@@ -36,7 +36,7 @@ type UserHandler struct {
userService service.UserService userService service.UserService
activityService service.UserActivityService activityService service.UserActivityService
profileAuditService service.UserProfileAuditService profileAuditService service.UserProfileAuditService
jwtService *service.JWTService jwtService service.JWTService
logService *service.LogService logService *service.LogService
} }
@@ -53,7 +53,7 @@ func (h *UserHandler) SetProfileAuditService(profileAuditService service.UserPro
} }
// SetJWTService 设置JWT服务 // SetJWTService 设置JWT服务
func (h *UserHandler) SetJWTService(jwtService *service.JWTService) { func (h *UserHandler) SetJWTService(jwtService service.JWTService) {
h.jwtService = jwtService h.jwtService = jwtService
} }

View File

@@ -13,12 +13,12 @@ import (
// VoteHandler 投票处理器 // VoteHandler 投票处理器
type VoteHandler struct { type VoteHandler struct {
voteService *service.VoteService voteService service.VoteService
postService service.PostService postService service.PostService
} }
// NewVoteHandler 创建投票处理器 // NewVoteHandler 创建投票处理器
func NewVoteHandler(voteService *service.VoteService, postService service.PostService) *VoteHandler { func NewVoteHandler(voteService service.VoteService, postService service.PostService) *VoteHandler {
return &VoteHandler{ return &VoteHandler{
voteService: voteService, voteService: voteService,
postService: postService, postService: postService,

View File

@@ -19,7 +19,6 @@ import (
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/pkg/ws" "with_you/internal/pkg/ws"
"with_you/internal/repository"
"with_you/internal/service" "with_you/internal/service"
) )
@@ -69,9 +68,9 @@ type WSHandler struct {
publisher ws.MessagePublisher publisher ws.MessagePublisher
chatService service.ChatService chatService service.ChatService
groupService service.GroupService groupService service.GroupService
jwtService *service.JWTService jwtService service.JWTService
callService service.CallService callService service.CallService
userRepo repository.UserRepository userService service.UserService
clientSeq uint64 clientSeq uint64
} }
@@ -92,9 +91,9 @@ func NewWSHandler(
publisher ws.MessagePublisher, publisher ws.MessagePublisher,
chatService service.ChatService, chatService service.ChatService,
groupService service.GroupService, groupService service.GroupService,
jwtService *service.JWTService, jwtService service.JWTService,
callService service.CallService, callService service.CallService,
userRepo repository.UserRepository, userService service.UserService,
) *WSHandler { ) *WSHandler {
return &WSHandler{ return &WSHandler{
publisher: publisher, publisher: publisher,
@@ -102,7 +101,7 @@ func NewWSHandler(
groupService: groupService, groupService: groupService,
jwtService: jwtService, jwtService: jwtService,
callService: callService, callService: callService,
userRepo: userRepo, userService: userService,
} }
} }
@@ -578,7 +577,7 @@ func (h *WSHandler) handleAck(client *ws.Client, payload json.RawMessage) {
// isVerified 检查用户是否已通过身份认证 // isVerified 检查用户是否已通过身份认证
func (h *WSHandler) isVerified(ctx context.Context, client *ws.Client) bool { 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 { if err != nil {
h.publisher.SendError(client, "internal_error", "获取用户信息失败") h.publisher.SendError(client, "internal_error", "获取用户信息失败")
return false return false

View File

@@ -10,7 +10,7 @@ import (
) )
// Auth 认证中间件 // Auth 认证中间件
func Auth(jwtService *service.JWTService) gin.HandlerFunc { func Auth(jwtService service.JWTService) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
@@ -45,7 +45,7 @@ func Auth(jwtService *service.JWTService) gin.HandlerFunc {
} }
// OptionalAuth 可选认证中间件 // OptionalAuth 可选认证中间件
func OptionalAuth(jwtService *service.JWTService) gin.HandlerFunc { func OptionalAuth(jwtService service.JWTService) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
if authHeader == "" { if authHeader == "" {

View File

@@ -9,63 +9,6 @@ import (
"github.com/gin-gonic/gin" "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 要求特定角色的中间件 // RequireRole 要求特定角色的中间件
func RequireRole(casbinService service.CasbinService, requiredRoles ...string) gin.HandlerFunc { func RequireRole(casbinService service.CasbinService, requiredRoles ...string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {

View File

@@ -1,16 +1,20 @@
package middleware package middleware
import ( import (
"net/http" "net/http"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/repository" "with_you/internal/service"
"github.com/gin-gonic/gin" "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) { return func(c *gin.Context) {
userID, exists := c.Get("user_id") userID, exists := c.Get("user_id")
if !exists { if !exists {
@@ -19,7 +23,7 @@ func RequireVerified(userRepo repository.UserRepository) gin.HandlerFunc {
return return
} }
user, err := userRepo.GetByID(userID.(string)) user, err := userService.GetUserByID(c.Request.Context(), userID.(string))
if err != nil { if err != nil {
response.InternalServerError(c, "获取用户信息失败") response.InternalServerError(c, "获取用户信息失败")
c.Abort() c.Abort()

View File

@@ -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 已认证用户 → 放行200next 被调用)。
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")
}
}

View File

@@ -1,9 +1,8 @@
package model package model
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"sync"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -303,9 +302,14 @@ func (m *Message) IsInteractionNotification() bool {
} }
} }
// BatchDecryptMessagesParallel 批量解密消息段。
// BatchDecryptMessagesParallel 使用 worker pool 并行解密 // 并发逻辑收敛在 crypto.MessageEncryptor.BatchDecryptworker pool
// 这是一个更高效的版本,适用于大量消息 // 本函数仅负责「提取密文 → 回填明文到 Message.Segments」不再自起 goroutine。
//
// 行为保持与历史版本一致:
// - 加密器未初始化时,降级为明文 JSON 直接解析(兼容旧数据);
// - 单条解密失败时回退尝试直接 JSON 解析(兼容未加密旧数据);
// - 已解密或无加密内容的消息跳过。
func BatchDecryptMessagesParallel(messages []*Message) { func BatchDecryptMessagesParallel(messages []*Message) {
if len(messages) == 0 { if len(messages) == 0 {
return return
@@ -313,7 +317,7 @@ func BatchDecryptMessagesParallel(messages []*Message) {
encryptor := crypto.GetMessageEncryptor() encryptor := crypto.GetMessageEncryptor()
if encryptor == nil { if encryptor == nil {
// 加密器未初始化,串行解析 // 加密器未初始化,串行解析为明文 JSON兼容模式
for _, m := range messages { for _, m := range messages {
if !m.decrypted && m.SegmentsEncrypted != "" { if !m.decrypted && m.SegmentsEncrypted != "" {
_ = json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments) _ = json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments)
@@ -323,46 +327,34 @@ func BatchDecryptMessagesParallel(messages []*Message) {
return return
} }
// 确定并行度 // 提取待解密的密文索引与值
workerCount := 4 indices := make([]int, 0, len(messages))
if len(messages) < 20 { ciphertexts := make([]string, 0, len(messages))
workerCount = 2 for i, m := range messages {
} else if len(messages) > 100 { if m.decrypted || m.SegmentsEncrypted == "" {
workerCount = 8 m.decrypted = true
continue
}
indices = append(indices, i)
ciphertexts = append(ciphertexts, m.SegmentsEncrypted)
}
if len(ciphertexts) == 0 {
return
} }
jobs := make(chan int, len(messages)) // 并发解密worker pool 由 crypto.BatchDecrypt 内部管理)
var wg sync.WaitGroup plaintexts := encryptor.BatchDecrypt(ciphertexts, 0)
// 启动 workers // 回填明文;解密失败的位置回退直接 JSON 解析
for w := 0; w < workerCount; w++ { for j, idx := range indices {
wg.Add(1) m := messages[idx]
go func() { pt := plaintexts[j]
defer wg.Done() if pt == nil {
for i := range jobs { // 解密失败,兼容未加密的旧数据
m := messages[i] _ = json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments)
if m.decrypted || m.SegmentsEncrypted == "" { } else {
m.decrypted = true _ = json.Unmarshal(pt, &m.Segments)
continue }
} m.decrypted = true
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
}
}()
} }
// 分发任务
for i := range messages {
jobs <- i
}
close(jobs)
wg.Wait()
} }

View File

@@ -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

View File

@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="%d" height="%d">
<rect width="100" height="100" fill="%s"/>
<text x="50" y="50" font-family="Arial, sans-serif" font-size="40" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">%s</text>
</svg>`
// 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)]
}

View File

@@ -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, "<svg") {
t.Errorf("SVG should contain <svg tag")
}
if !strings.Contains(svg, "</svg>") {
t.Errorf("SVG should contain </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)
}
}
}

View File

@@ -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 串行解密性能基准(对比用) // BenchmarkSerialDecrypt 串行解密性能基准(对比用)
func BenchmarkSerialDecrypt(b *testing.B) { func BenchmarkSerialDecrypt(b *testing.B) {
key := "12345678901234567890123456789012" 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) { func TestBatchDecrypt(t *testing.T) {
key := "12345678901234567890123456789012" key := "12345678901234567890123456789012"
_ = InitMessageEncryptor(key, 1) _ = InitMessageEncryptor(key, 1)
encryptor := GetMessageEncryptor() encryptor := GetMessageEncryptor()
// 准备测试数据
count := 100 count := 100
ciphertexts := make([]string, count) ciphertexts := make([]string, count)
expectedContents := make([]string, count) expectedContents := make([]string, count)
@@ -105,6 +104,10 @@ func TestBatchDecrypt(t *testing.T) {
// 批量解密 // 批量解密
results := encryptor.BatchDecrypt(ciphertexts, 4) 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 { for i, result := range results {
if result == nil { if result == nil {

View File

@@ -139,31 +139,70 @@ func (e *MessageEncryptor) GetKeyVersion() int {
return e.keyVersion return e.keyVersion
} }
// RotateKey 密钥轮换(用于密钥升级) // BatchDecrypt 批量解密多条密文,返回与输入等长的明文字节切片数组。
// 新密钥必须也是32字节 // 并发度由 workers 指定(<=0 时按密文数量自适应)。
func (e *MessageEncryptor) RotateKey(newKey string, newVersion int) error { // 解密失败或空密文的位置返回 nil调用方可据此判断。
keyBytes := []byte(newKey) // 这是 Decrypt 的无锁读优化版AEAD 的 Open 不修改内部状态,
if len(keyBytes) != 32 { // 但仍走 RLock 以兼容未来可能的密钥轮换语义。
return ErrInvalidKey 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 { if workers <= 0 {
return err 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) // 预取一次 AEAD避免 worker 内重复进入 RLock
if err != nil { e.mu.RLock()
return err 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() for i := range ciphertexts {
defer e.mu.Unlock() jobs <- i
}
close(jobs)
wg.Wait()
e.key = keyBytes return results
e.gcm = gcm
e.keyVersion = newVersion
return nil
} }

View File

@@ -150,16 +150,6 @@ func TestMessageEncryptor_KeyVersion(t *testing.T) {
if encryptor.GetKeyVersion() != 1 { if encryptor.GetKeyVersion() != 1 {
t.Errorf("GetKeyVersion() = %d, want 1", encryptor.GetKeyVersion()) 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 { func mustMarshal(v interface{}) []byte {

68
internal/query/filters.go Normal file
View File

@@ -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"`
}

View File

@@ -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)
}
}

View File

@@ -1,10 +1,10 @@
package repository package repository
import ( import (
"context" "context"
"with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/query"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -13,7 +13,7 @@ import (
type DataChangeLogRepository interface { type DataChangeLogRepository interface {
CreateDataChangeLog(ctx context.Context, log *model.DataChangeLog) error CreateDataChangeLog(ctx context.Context, log *model.DataChangeLog) error
BatchCreateDataChangeLogs(ctx context.Context, logs []*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) 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) GetDataChangeLogsByOperator(ctx context.Context, operatorID string, page, pageSize int) ([]*model.DataChangeLog, int64, error)
DeleteOldLogs(ctx context.Context, beforeDate string) error DeleteOldLogs(ctx context.Context, beforeDate string) error
@@ -44,7 +44,7 @@ func (r *dataChangeLogRepository) BatchCreateDataChangeLogs(ctx context.Context,
} }
// GetDataChangeLogs 获取数据变更日志列表(分页) // 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{}) query := r.db.WithContext(ctx).Model(&model.DataChangeLog{})
if filters.UserID != "" { if filters.UserID != "" {
@@ -88,12 +88,12 @@ func (r *dataChangeLogRepository) GetDataChangeLogs(ctx context.Context, filters
// GetDataChangeLogsByUser 获取指定用户的数据变更日志 // GetDataChangeLogsByUser 获取指定用户的数据变更日志
func (r *dataChangeLogRepository) GetDataChangeLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.DataChangeLog, int64, error) { 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 获取指定操作人的日志 // GetDataChangeLogsByOperator 获取指定操作人的日志
func (r *dataChangeLogRepository) GetDataChangeLogsByOperator(ctx context.Context, operatorID string, page, pageSize int) ([]*model.DataChangeLog, int64, error) { 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 删除旧的数据变更日志(用于定时清理) // DeleteOldLogs 删除旧的数据变更日志(用于定时清理)

View File

@@ -1,11 +1,11 @@
package repository package repository
import ( import (
"context" "context"
"time" "time"
"with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/query"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -14,7 +14,7 @@ import (
type LoginLogRepository interface { type LoginLogRepository interface {
CreateLoginLog(ctx context.Context, log *model.LoginLog) error CreateLoginLog(ctx context.Context, log *model.LoginLog) error
BatchCreateLoginLogs(ctx context.Context, logs []*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) GetLoginLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.LoginLog, int64, error)
GetFailedLoginsByIP(ctx context.Context, ip string, timeWindow time.Duration) (int64, error) GetFailedLoginsByIP(ctx context.Context, ip string, timeWindow time.Duration) (int64, error)
GetRecentSuccessLogins(ctx context.Context, userID string, limit int) ([]*model.LoginLog, 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 获取登录日志列表(分页) // 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{}) query := r.db.WithContext(ctx).Model(&model.LoginLog{})
if filters.UserID != "" { if filters.UserID != "" {
@@ -90,7 +90,7 @@ func (r *loginLogRepository) GetLoginLogs(ctx context.Context, filters dto.Login
// GetLoginLogsByUser 获取指定用户的登录日志 // GetLoginLogsByUser 获取指定用户的登录日志
func (r *loginLogRepository) GetLoginLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.LoginLog, int64, error) { 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的失败登录记录用于风控 // GetFailedLoginsByIP 获取指定IP的失败登录记录用于风控

View File

@@ -1,9 +1,9 @@
package repository package repository
import ( import (
"with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/pkg/utils" "with_you/internal/pkg/utils"
"with_you/internal/query"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -80,7 +80,7 @@ func (r *materialSubjectRepository) CountMaterials(subjectID string) (int64, err
// MaterialFileRepository 文件资料仓储接口 // MaterialFileRepository 文件资料仓储接口
type MaterialFileRepository interface { 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) GetByID(id string) (*model.MaterialFile, error)
GetByIDWithSubject(id string) (*model.MaterialFile, error) GetByIDWithSubject(id string) (*model.MaterialFile, error)
Create(file *model.MaterialFile) error Create(file *model.MaterialFile) error
@@ -103,7 +103,7 @@ func NewMaterialFileRepository(db *gorm.DB) MaterialFileRepository {
} }
// List 获取资料列表(支持筛选和分页) // 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 files []*model.MaterialFile
var total int64 var total int64
@@ -260,4 +260,4 @@ func (r *materialFileRepository) GetLatestMaterials(limit int) ([]*model.Materia
Limit(limit). Limit(limit).
Find(&files).Error Find(&files).Error
return files, err return files, err
} }

View File

@@ -1,10 +1,10 @@
package repository package repository
import ( import (
"context" "context"
"with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/query"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -13,7 +13,7 @@ import (
type OperationLogRepository interface { type OperationLogRepository interface {
CreateOperationLog(ctx context.Context, log *model.OperationLog) error CreateOperationLog(ctx context.Context, log *model.OperationLog) error
BatchCreateOperationLogs(ctx context.Context, logs []*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) 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) GetOperationLogsByTimeRange(ctx context.Context, startTime, endTime string, page, pageSize int) ([]*model.OperationLog, int64, error)
DeleteOldLogs(ctx context.Context, beforeDate string) error DeleteOldLogs(ctx context.Context, beforeDate string) error
@@ -44,7 +44,7 @@ func (r *operationLogRepository) BatchCreateOperationLogs(ctx context.Context, l
} }
// GetOperationLogs 获取操作日志列表(分页) // 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{}) query := r.db.WithContext(ctx).Model(&model.OperationLog{})
if filters.UserID != "" { if filters.UserID != "" {
@@ -88,12 +88,12 @@ func (r *operationLogRepository) GetOperationLogs(ctx context.Context, filters d
// GetOperationLogsByUser 获取指定用户的操作日志 // GetOperationLogsByUser 获取指定用户的操作日志
func (r *operationLogRepository) GetOperationLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.OperationLog, int64, error) { 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 按时间范围获取操作日志 // GetOperationLogsByTimeRange 按时间范围获取操作日志
func (r *operationLogRepository) GetOperationLogsByTimeRange(ctx context.Context, startTime, endTime string, page, pageSize int) ([]*model.OperationLog, int64, error) { 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 删除旧的操作日志(用于定时清理) // DeleteOldLogs 删除旧的操作日志(用于定时清理)

View File

@@ -1,9 +1,9 @@
package repository package repository
import ( import (
"with_you/internal/dto"
"with_you/internal/model"
"context" "context"
"with_you/internal/model"
"with_you/internal/query"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -19,7 +19,7 @@ type ReportRepository interface {
// FindByIDWithContext 使用上下文查询举报 // FindByIDWithContext 使用上下文查询举报
FindByIDWithContext(ctx context.Context, id string) (*model.Report, error) FindByIDWithContext(ctx context.Context, id string) (*model.Report, error)
// List 查询举报列表 // List 查询举报列表
List(query dto.AdminReportListQuery) ([]*model.Report, int64, error) List(query query.AdminReportListQuery) ([]*model.Report, int64, error)
// GetReportCount 获取内容的举报次数 // GetReportCount 获取内容的举报次数
GetReportCount(targetType model.ReportTargetType, targetID string) (int64, error) GetReportCount(targetType model.ReportTargetType, targetID string) (int64, error)
// HasUserReported 检查用户是否已举报过该内容 // HasUserReported 检查用户是否已举报过该内容
@@ -87,7 +87,7 @@ func (r *reportRepository) FindByIDWithContext(ctx context.Context, id string) (
} }
// List 查询举报列表 // 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 reports []*model.Report
var total int64 var total int64

View File

@@ -1,4 +1,4 @@
package router package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -28,7 +28,7 @@ type RouterDeps struct {
ScheduleHandler *handler.ScheduleHandler ScheduleHandler *handler.ScheduleHandler
GradeHandler *handler.GradeHandler GradeHandler *handler.GradeHandler
ExamHandler *handler.ExamHandler ExamHandler *handler.ExamHandler
EmptyClassroomHandler *handler.EmptyClassroomHandler EmptyClassroomHandler *handler.EmptyClassroomHandler
RoleHandler *handler.RoleHandler RoleHandler *handler.RoleHandler
AdminUserHandler *handler.AdminUserHandler AdminUserHandler *handler.AdminUserHandler
AdminPostHandler *handler.AdminPostHandler AdminPostHandler *handler.AdminPostHandler
@@ -48,54 +48,19 @@ type RouterDeps struct {
SetupHandler *handler.SetupHandler SetupHandler *handler.SetupHandler
TradeHandler *handler.TradeHandler TradeHandler *handler.TradeHandler
WSHandler *handler.WSHandler WSHandler *handler.WSHandler
JWTService *service.JWTService JWTService service.JWTService
LogService *service.LogService LogService *service.LogService
ActivityService service.UserActivityService ActivityService service.UserActivityService
CasbinService service.CasbinService CasbinService service.CasbinService
UserService service.UserService
} }
// Router 路由配置 // Router 路由配置
// 通过嵌入 RouterDeps 复用所有依赖字段,消除 RouterDeps 与 Router 之间的字段重复。
// 所有 deps 字段(如 userHandler、jwtService经嵌入提升后仍以 r.UserHandler 形式访问。
type Router struct { type Router struct {
engine *gin.Engine *RouterDeps
userRepo repository.UserRepository engine *gin.Engine
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
} }
// New 创建路由 // New 创建路由
@@ -104,46 +69,8 @@ func New(deps RouterDeps) *Router {
deps.UserHandler.SetActivityService(deps.ActivityService) deps.UserHandler.SetActivityService(deps.ActivityService)
r := &Router{ r := &Router{
engine: gin.Default(), RouterDeps: &deps,
userRepo: deps.UserRepo, engine: gin.Default(),
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,
} }
r.setupRoutes() r.setupRoutes()
@@ -169,25 +96,25 @@ func (r *Router) setupRoutes() {
auth.Use(middleware.GlobalAuthRateLimit()) auth.Use(middleware.GlobalAuthRateLimit())
auth.Use(middleware.IPBanMiddleware()) auth.Use(middleware.IPBanMiddleware())
{ {
auth.POST("/register", middleware.RegisterRateLimit(), r.userHandler.Register) auth.POST("/register", middleware.RegisterRateLimit(), r.UserHandler.Register)
auth.POST("/register/send-code", middleware.SendCodeRateLimit(), r.userHandler.SendRegisterCode) auth.POST("/register/send-code", middleware.SendCodeRateLimit(), r.UserHandler.SendRegisterCode)
auth.POST("/login", middleware.LoginRateLimit(), r.userHandler.Login) auth.POST("/login", middleware.LoginRateLimit(), r.UserHandler.Login)
auth.POST("/password/send-code", middleware.PasswordResetRateLimit(), r.userHandler.SendPasswordResetCode) auth.POST("/password/send-code", middleware.PasswordResetRateLimit(), r.UserHandler.SendPasswordResetCode)
auth.POST("/password/reset", middleware.PasswordResetRateLimit(), r.userHandler.ResetPassword) auth.POST("/password/reset", middleware.PasswordResetRateLimit(), r.UserHandler.ResetPassword)
auth.POST("/logout", r.userHandler.Logout) auth.POST("/logout", r.UserHandler.Logout)
auth.POST("/refresh", r.userHandler.RefreshToken) auth.POST("/refresh", r.UserHandler.RefreshToken)
} }
// 初始化超级管理员(公开接口,只能使用一次) // 初始化超级管理员(公开接口,只能使用一次)
if r.setupHandler != nil { if r.SetupHandler != nil {
v1.POST("/admin/setup-super-admin", r.setupHandler.SetupSuperAdmin) 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{ accessLogConfig := &middleware.AccessLogConfig{
Enable: true, Enable: true,
SkipPaths: []string{"/health", "/api/v1/health"}, SkipPaths: []string{"/health", "/api/v1/health"},
@@ -195,211 +122,208 @@ func (r *Router) setupRoutes() {
ServerPort: 8080, ServerPort: 8080,
} }
r.engine.Use(middleware.AccessLogMiddleware( r.engine.Use(middleware.AccessLogMiddleware(
r.logService.OperationLog, r.LogService.OperationLog,
accessLogConfig, accessLogConfig,
)) ))
} }
// Casbin 权限中间件(暂不全局启用,可根据需要在特定路由组上使用)
// casbinMiddleware := middleware.CasbinAuth(r.casbinService)
// 用户路由 // 用户路由
users := v1.Group("/users") users := v1.Group("/users")
{ {
// 当前用户 // 当前用户
users.GET("/me", authMiddleware, r.userHandler.GetCurrentUser) users.GET("/me", authMiddleware, r.UserHandler.GetCurrentUser)
users.PUT("/me", authMiddleware, r.userHandler.UpdateUser) users.PUT("/me", authMiddleware, r.UserHandler.UpdateUser)
users.POST("/me/email/send-code", authMiddleware, r.userHandler.SendEmailVerifyCode) users.POST("/me/email/send-code", authMiddleware, r.UserHandler.SendEmailVerifyCode)
users.POST("/me/email/verify", authMiddleware, r.userHandler.VerifyEmail) users.POST("/me/email/verify", authMiddleware, r.UserHandler.VerifyEmail)
users.POST("/me/avatar", authMiddleware, r.uploadHandler.UploadAvatar) users.POST("/me/avatar", authMiddleware, r.UploadHandler.UploadAvatar)
users.POST("/me/cover", authMiddleware, r.uploadHandler.UploadCover) users.POST("/me/cover", authMiddleware, r.UploadHandler.UploadCover)
users.POST("/change-password/send-code", authMiddleware, r.userHandler.SendChangePasswordCode) users.POST("/change-password/send-code", authMiddleware, r.UserHandler.SendChangePasswordCode)
users.POST("/change-password", authMiddleware, r.userHandler.ChangePassword) users.POST("/change-password", authMiddleware, r.UserHandler.ChangePassword)
// 隐私设置 // 隐私设置
users.GET("/me/privacy", authMiddleware, r.userHandler.GetPrivacySettings) users.GET("/me/privacy", authMiddleware, r.UserHandler.GetPrivacySettings)
users.PUT("/me/privacy", authMiddleware, r.userHandler.UpdatePrivacySettings) users.PUT("/me/privacy", authMiddleware, r.UserHandler.UpdatePrivacySettings)
// 账号注销 // 账号注销
users.POST("/me/deactivate", authMiddleware, r.userHandler.RequestAccountDeletion) users.POST("/me/deactivate", authMiddleware, r.UserHandler.RequestAccountDeletion)
users.DELETE("/me/deactivate", authMiddleware, r.userHandler.CancelAccountDeletion) users.DELETE("/me/deactivate", authMiddleware, r.UserHandler.CancelAccountDeletion)
users.GET("/me/deletion-status", authMiddleware, r.userHandler.GetDeletionStatus) users.GET("/me/deletion-status", authMiddleware, r.UserHandler.GetDeletionStatus)
// 当前用户角色 // 当前用户角色
if r.roleHandler != nil { if r.RoleHandler != nil {
users.GET("/me/roles", authMiddleware, r.roleHandler.GetMyRoles) 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.GET("/:id", middleware.OptionalAuth(r.JWTService), r.UserHandler.GetUserByID)
users.POST("/:id/follow", authMiddleware, r.userHandler.FollowUser) users.POST("/:id/follow", authMiddleware, r.UserHandler.FollowUser)
users.DELETE("/:id/follow", authMiddleware, r.userHandler.UnfollowUser) users.DELETE("/:id/follow", authMiddleware, r.UserHandler.UnfollowUser)
users.POST("/:id/block", authMiddleware, r.userHandler.BlockUser) users.POST("/:id/block", authMiddleware, r.UserHandler.BlockUser)
users.DELETE("/:id/block", authMiddleware, r.userHandler.UnblockUser) users.DELETE("/:id/block", authMiddleware, r.UserHandler.UnblockUser)
users.GET("/:id/block-status", authMiddleware, r.userHandler.GetBlockStatus) users.GET("/:id/block-status", authMiddleware, r.UserHandler.GetBlockStatus)
users.GET("/blocks", authMiddleware, r.userHandler.GetBlockedUsers) users.GET("/blocks", authMiddleware, r.UserHandler.GetBlockedUsers)
users.GET("/:id/following", authMiddleware, r.userHandler.GetFollowingList) users.GET("/:id/following", authMiddleware, r.UserHandler.GetFollowingList)
users.GET("/:id/followers", authMiddleware, r.userHandler.GetFollowersList) users.GET("/:id/followers", authMiddleware, r.UserHandler.GetFollowersList)
// 用户帖子 - 使用 OptionalAuth 获取当前用户点赞/收藏状态 // 用户帖子 - 使用 OptionalAuth 获取当前用户点赞/收藏状态
users.GET("/:id/posts", middleware.OptionalAuth(r.jwtService), r.postHandler.GetUserPosts) 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/favorites", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetFavorites)
} }
// 身份认证路由 // 身份认证路由
if r.verificationHandler != nil { if r.VerificationHandler != nil {
verification := v1.Group("/verification") verification := v1.Group("/verification")
verification.Use(authMiddleware) verification.Use(authMiddleware)
{ {
verification.GET("/status", r.verificationHandler.GetVerificationStatus) verification.GET("/status", r.VerificationHandler.GetVerificationStatus)
verification.POST("/submit", r.verificationHandler.SubmitVerification) verification.POST("/submit", r.VerificationHandler.SubmitVerification)
verification.GET("/records", r.verificationHandler.GetVerificationRecords) verification.GET("/records", r.VerificationHandler.GetVerificationRecords)
} }
} }
// 二维码登录(公开) // 二维码登录(公开)
if r.qrcodeHandler != nil { if r.QRCodeHandler != nil {
qrcode := v1.Group("/auth/qrcode") qrcode := v1.Group("/auth/qrcode")
{ {
qrcode.GET("", r.qrcodeHandler.GetQRCode) qrcode.GET("", r.QRCodeHandler.GetQRCode)
qrcode.GET("/events", r.qrcodeHandler.WSEvents) qrcode.GET("/events", r.QRCodeHandler.WSEvents)
} }
// 二维码操作(需认证) // 二维码操作(需认证)
qrcodeAuth := v1.Group("/auth/qrcode") qrcodeAuth := v1.Group("/auth/qrcode")
qrcodeAuth.Use(authMiddleware) qrcodeAuth.Use(authMiddleware)
{ {
qrcodeAuth.POST("/scan", r.qrcodeHandler.Scan) qrcodeAuth.POST("/scan", r.QRCodeHandler.Scan)
qrcodeAuth.POST("/confirm", r.qrcodeHandler.Confirm) qrcodeAuth.POST("/confirm", r.QRCodeHandler.Confirm)
qrcodeAuth.POST("/cancel", r.qrcodeHandler.Cancel) qrcodeAuth.POST("/cancel", r.QRCodeHandler.Cancel)
} }
} }
// 认证路由(公开) // 认证路由(公开)
authPublic := v1.Group("/auth") authPublic := v1.Group("/auth")
{ {
authPublic.GET("/check-username", r.userHandler.CheckUsername) authPublic.GET("/check-username", r.UserHandler.CheckUsername)
} }
// 帖子路由 // 帖子路由
posts := v1.Group("/posts") posts := v1.Group("/posts")
{ {
// 使用 OptionalAuth 中间件来获取用户登录状态 // 使用 OptionalAuth 中间件来获取用户登录状态
posts.GET("", middleware.OptionalAuth(r.jwtService), r.postHandler.List) posts.GET("", middleware.OptionalAuth(r.JWTService), r.PostHandler.List)
posts.GET("/search", middleware.OptionalAuth(r.jwtService), r.postHandler.Search) posts.GET("/search", middleware.OptionalAuth(r.JWTService), r.PostHandler.Search)
posts.GET("/suggest", middleware.OptionalAuth(r.jwtService), r.postHandler.Suggest) posts.GET("/suggest", middleware.OptionalAuth(r.JWTService), r.PostHandler.Suggest)
posts.GET("/:id", middleware.OptionalAuth(r.jwtService), r.postHandler.GetByID) posts.GET("/:id", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetByID)
posts.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Create) posts.POST("", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Create)
posts.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Update) posts.PUT("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Update)
posts.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Delete) 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/related", middleware.OptionalAuth(r.JWTService), r.PostHandler.GetRelatedPosts)
posts.GET("/:id/refs", middleware.OptionalAuth(r.jwtService), r.postHandler.GetReferencedPosts) 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/ref-click", middleware.OptionalAuth(r.JWTService), r.PostHandler.RecordRefClick)
// 点赞 // 点赞
posts.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Like) posts.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Like)
posts.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Unlike) 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.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Favorite)
posts.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Unfavorite) posts.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.UserService), r.PostHandler.Unfavorite)
// 投票相关路由 // 投票相关路由
posts.POST("/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.CreateVotePost) // 创建投票帖子 posts.POST("/vote", authMiddleware, middleware.RequireVerified(r.UserService), r.VoteHandler.CreateVotePost) // 创建投票帖子
posts.GET("/:id/vote", middleware.OptionalAuth(r.jwtService), r.voteHandler.GetVoteResult) // 获取投票结果 posts.GET("/:id/vote", middleware.OptionalAuth(r.JWTService), r.VoteHandler.GetVoteResult) // 获取投票结果
posts.POST("/:id/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.Vote) // 投票 posts.POST("/:id/vote", authMiddleware, middleware.RequireVerified(r.UserService), r.VoteHandler.Vote) // 投票
posts.DELETE("/:id/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.Unvote) // 取消投票 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 := 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 := v1.Group("/trade")
{ {
trade.GET("", middleware.OptionalAuth(r.jwtService), r.tradeHandler.List) trade.GET("", middleware.OptionalAuth(r.JWTService), r.TradeHandler.List)
trade.GET("/:id", middleware.OptionalAuth(r.jwtService), r.tradeHandler.GetByID) trade.GET("/:id", middleware.OptionalAuth(r.JWTService), r.TradeHandler.GetByID)
trade.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Create) trade.POST("", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Create)
trade.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Update) trade.PUT("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Update)
trade.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Delete) trade.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Delete)
trade.PUT("/:id/status", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.UpdateStatus) 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/view", middleware.OptionalAuth(r.JWTService), r.TradeHandler.RecordView)
trade.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Favorite) trade.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.UserService), r.TradeHandler.Favorite)
trade.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Unfavorite) 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 := v1.Group("/schedule")
schedule.Use(authMiddleware) schedule.Use(authMiddleware)
{ {
schedule.GET("/courses", r.scheduleHandler.ListCourses) schedule.GET("/courses", r.ScheduleHandler.ListCourses)
schedule.POST("/courses", r.scheduleHandler.CreateCourse) schedule.POST("/courses", r.ScheduleHandler.CreateCourse)
schedule.POST("/sync", r.scheduleHandler.SyncSchedule) // 同步课表 schedule.POST("/sync", r.ScheduleHandler.SyncSchedule) // 同步课表
schedule.PUT("/courses/:id", r.scheduleHandler.UpdateCourse) schedule.PUT("/courses/:id", r.ScheduleHandler.UpdateCourse)
schedule.DELETE("/courses/:id", r.scheduleHandler.DeleteCourse) schedule.DELETE("/courses/:id", r.ScheduleHandler.DeleteCourse)
} }
} }
// 成绩路由 // 成绩路由
if r.gradeHandler != nil { if r.GradeHandler != nil {
grades := v1.Group("/grades") grades := v1.Group("/grades")
grades.Use(authMiddleware) grades.Use(authMiddleware)
{ {
grades.GET("", r.gradeHandler.ListGrades) grades.GET("", r.GradeHandler.ListGrades)
grades.POST("/sync", r.gradeHandler.SyncGrades) grades.POST("/sync", r.GradeHandler.SyncGrades)
} }
} }
// 考试安排路由 // 考试安排路由
if r.examHandler != nil { if r.ExamHandler != nil {
exams := v1.Group("/exams") exams := v1.Group("/exams")
exams.Use(authMiddleware) exams.Use(authMiddleware)
{ {
exams.GET("", r.examHandler.ListExams) exams.GET("", r.ExamHandler.ListExams)
exams.POST("/sync", r.examHandler.SyncExams) exams.POST("/sync", r.ExamHandler.SyncExams)
} }
} }
// 空教室路由 // 空教室路由
if r.emptyClassroomHandler != nil { if r.EmptyClassroomHandler != nil {
classrooms := v1.Group("/classrooms") classrooms := v1.Group("/classrooms")
classrooms.Use(authMiddleware) classrooms.Use(authMiddleware)
{ {
classrooms.GET("", r.emptyClassroomHandler.ListEmptyClassrooms) classrooms.GET("", r.EmptyClassroomHandler.ListEmptyClassrooms)
classrooms.POST("/sync", r.emptyClassroomHandler.SyncEmptyClassrooms) classrooms.POST("/sync", r.EmptyClassroomHandler.SyncEmptyClassrooms)
} }
} }
// 学习资料路由(公开) // 学习资料路由(公开)
if r.materialHandler != nil { if r.MaterialHandler != nil {
materials := v1.Group("/materials") materials := v1.Group("/materials")
{ {
// 学科相关 // 学科相关
materials.GET("/subjects", r.materialHandler.ListSubjects) materials.GET("/subjects", r.MaterialHandler.ListSubjects)
materials.GET("/subjects/:id", r.materialHandler.GetSubject) materials.GET("/subjects/:id", r.MaterialHandler.GetSubject)
// 资料相关 // 资料相关
materials.GET("", r.materialHandler.ListMaterials) materials.GET("", r.MaterialHandler.ListMaterials)
materials.GET("/search", r.materialHandler.SearchMaterials) materials.GET("/search", r.MaterialHandler.SearchMaterials)
materials.GET("/:id", r.materialHandler.GetMaterial) materials.GET("/:id", r.MaterialHandler.GetMaterial)
materials.GET("/:id/download", r.materialHandler.DownloadMaterial) materials.GET("/:id/download", r.MaterialHandler.DownloadMaterial)
} }
} }
@@ -407,24 +331,24 @@ func (r *Router) setupRoutes() {
voteOptions := v1.Group("/vote-options") voteOptions := v1.Group("/vote-options")
voteOptions.Use(authMiddleware) voteOptions.Use(authMiddleware)
{ {
voteOptions.PUT("/:id", r.voteHandler.UpdateVoteOption) // 更新选项 voteOptions.PUT("/:id", r.VoteHandler.UpdateVoteOption) // 更新选项
} }
// 评论路由 // 评论路由
comments := v1.Group("/comments") comments := v1.Group("/comments")
{ {
comments.GET("/post/:id", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByPostID) 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("/post/:id/cursor", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetByPostIDByCursor) // 帖子评论游标分页
comments.GET("/:id", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByID) comments.GET("/:id", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetByID)
comments.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Create) comments.POST("", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Create)
comments.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Update) comments.PUT("/:id", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Update)
comments.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Delete) 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", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetReplies)
comments.GET("/:id/replies/flat", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetRepliesByRootID) // 扁平化分页获取回复 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("/:id/replies/cursor", middleware.OptionalAuth(r.JWTService), r.CommentHandler.GetRepliesByRootIDByCursor) // 回复游标分页
// 评论点赞 // 评论点赞
comments.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Like) comments.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Like)
comments.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Unlike) comments.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.UserService), r.CommentHandler.Unlike)
} }
// 会话路由 // 会话路由
@@ -434,49 +358,49 @@ func (r *Router) setupRoutes() {
// ================================================================ // ================================================================
// 新的 RESTful 风格路由(推荐使用) // 新的 RESTful 风格路由(推荐使用)
// ================================================================ // ================================================================
conversations.GET("", r.messageHandler.HandleGetConversationList) // 列表 conversations.GET("", r.MessageHandler.HandleGetConversationList) // 列表
conversations.GET("/cursor", r.messageHandler.GetConversationsByCursor) // 会话列表游标分页 conversations.GET("/cursor", r.MessageHandler.GetConversationsByCursor) // 会话列表游标分页
conversations.POST("", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleCreateConversation) // 创建 conversations.POST("", middleware.RequireVerified(r.UserService), r.MessageHandler.HandleCreateConversation) // 创建
conversations.GET("/:id", r.messageHandler.HandleGetConversation) // 详情 conversations.GET("/:id", r.MessageHandler.HandleGetConversation) // 详情
conversations.GET("/:id/messages", r.messageHandler.HandleGetMessages) // 消息列表 conversations.GET("/:id/messages", r.MessageHandler.HandleGetMessages) // 消息列表
conversations.GET("/:id/messages/cursor", r.messageHandler.GetMessagesByCursor) // 消息列表游标分页 conversations.GET("/:id/messages/cursor", r.MessageHandler.GetMessagesByCursor) // 消息列表游标分页
conversations.POST("/:id/messages", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleSendMessage) // 发送消息 conversations.POST("/:id/messages", middleware.RequireVerified(r.UserService), r.MessageHandler.HandleSendMessage) // 发送消息
conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读 conversations.POST("/:id/read", r.MessageHandler.HandleMarkRead) // 标记已读
conversations.POST("/read-all", r.messageHandler.HandleMarkReadAll) // 批量标记已读 conversations.POST("/read-all", r.MessageHandler.HandleMarkReadAll) // 批量标记已读
conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置 conversations.PUT("/:id/pinned", r.MessageHandler.HandleSetConversationPinned) // 置顶设置
conversations.PUT("/:id/notification_muted", r.messageHandler.HandleSetConversationNotificationMuted) // 免打扰设置 conversations.PUT("/:id/notification_muted", r.MessageHandler.HandleSetConversationNotificationMuted) // 免打扰设置
conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数 conversations.GET("/unread/count", r.MessageHandler.GetUnreadCount) // 未读数
conversations.GET("/sync-data", r.messageHandler.HandleGetSyncData) // 同步元数据 conversations.GET("/sync-data", r.MessageHandler.HandleGetSyncData) // 同步元数据
conversations.GET("/sync", r.messageHandler.HandleGetSyncByVersion) // 增量同步(版本日志) conversations.GET("/sync", r.MessageHandler.HandleGetSyncByVersion) // 增量同步(版本日志)
conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态 conversations.POST("/:id/typing", r.MessageHandler.HandleTyping) // 输入状态
conversations.DELETE("/:id/self", r.messageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己) conversations.DELETE("/:id/self", r.MessageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己)
} }
realtime := v1.Group("/realtime") realtime := v1.Group("/realtime")
{ {
// WebSocket端点 - 在中间件之前处理认证通过query参数 // 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 := v1.Group("/calls")
calls.Use(authMiddleware) calls.Use(authMiddleware)
{ {
calls.GET("/history", r.callHandler.GetCallHistory) calls.GET("/history", r.CallHandler.GetCallHistory)
} }
} }
// LiveKit 通话路由 // LiveKit 通话路由
if r.liveKitHandler != nil { if r.LiveKitHandler != nil {
lk := v1.Group("/calls") lk := v1.Group("/calls")
lk.Use(authMiddleware) lk.Use(authMiddleware)
{ {
lk.GET("/token", r.liveKitHandler.GetToken) lk.GET("/token", r.LiveKitHandler.GetToken)
} }
// Webhook 不需要认证LiveKit 服务端调用) // 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.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 := v1.Group("/reports")
reports.Use(authMiddleware) reports.Use(authMiddleware)
{ {
reports.POST("", r.reportHandler.Create) reports.POST("", r.ReportHandler.Create)
} }
} }
// 通知路由 // 通知路由
notifications := v1.Group("/notifications") notifications := v1.Group("/notifications")
{ {
notifications.GET("", authMiddleware, r.notificationHandler.GetNotifications) notifications.GET("", authMiddleware, r.NotificationHandler.GetNotifications)
notifications.GET("/cursor", authMiddleware, r.notificationHandler.GetNotificationsByCursor) // 通知游标分页 notifications.GET("/cursor", authMiddleware, r.NotificationHandler.GetNotificationsByCursor) // 通知游标分页
notifications.POST("/:id/read", authMiddleware, r.notificationHandler.MarkAsRead) notifications.POST("/:id/read", authMiddleware, r.NotificationHandler.MarkAsRead)
notifications.POST("/read-all", authMiddleware, r.notificationHandler.MarkAllAsRead) notifications.POST("/read-all", authMiddleware, r.NotificationHandler.MarkAllAsRead)
notifications.GET("/unread-count", authMiddleware, r.notificationHandler.GetUnreadCount) notifications.GET("/unread-count", authMiddleware, r.NotificationHandler.GetUnreadCount)
notifications.DELETE("/:id", authMiddleware, r.notificationHandler.DeleteNotification) notifications.DELETE("/:id", authMiddleware, r.NotificationHandler.DeleteNotification)
notifications.DELETE("", authMiddleware, r.notificationHandler.ClearAllNotifications) notifications.DELETE("", authMiddleware, r.NotificationHandler.ClearAllNotifications)
} }
// 上传路由 // 上传路由
uploads := v1.Group("/uploads") 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 := v1.Group("/push")
pushGroup.Use(authMiddleware) pushGroup.Use(authMiddleware)
{ {
pushGroup.POST("/devices", r.pushHandler.RegisterDevice) pushGroup.POST("/devices", r.PushHandler.RegisterDevice)
pushGroup.GET("/devices", r.pushHandler.GetMyDevices) pushGroup.GET("/devices", r.PushHandler.GetMyDevices)
pushGroup.DELETE("/devices/:device_id", r.pushHandler.UnregisterDevice) pushGroup.DELETE("/devices/:device_id", r.PushHandler.UnregisterDevice)
pushGroup.PUT("/devices/:device_id/token", r.pushHandler.UpdateDeviceToken) pushGroup.PUT("/devices/:device_id/token", r.PushHandler.UpdateDeviceToken)
pushGroup.GET("/records", r.pushHandler.GetPushRecords) pushGroup.GET("/records", r.PushHandler.GetPushRecords)
} }
} }
// 系统消息路由 // 系统消息路由
if r.systemMessageHandler != nil { if r.SystemMessageHandler != nil {
msgGroup := v1.Group("/messages") msgGroup := v1.Group("/messages")
msgGroup.Use(authMiddleware) msgGroup.Use(authMiddleware)
{ {
msgGroup.GET("/system", r.systemMessageHandler.GetSystemMessages) msgGroup.GET("/system", r.SystemMessageHandler.GetSystemMessages)
msgGroup.GET("/system/unread-count", r.systemMessageHandler.GetUnreadCount) msgGroup.GET("/system/unread-count", r.SystemMessageHandler.GetUnreadCount)
msgGroup.PUT("/system/:id/read", r.systemMessageHandler.MarkAsRead) msgGroup.PUT("/system/:id/read", r.SystemMessageHandler.MarkAsRead)
msgGroup.PUT("/system/read-all", r.systemMessageHandler.MarkAllAsRead) msgGroup.PUT("/system/read-all", r.SystemMessageHandler.MarkAllAsRead)
} }
} }
// 群组路由 // 群组路由
if r.groupHandler != nil { if r.GroupHandler != nil {
groups := v1.Group("/groups") groups := v1.Group("/groups")
groups.Use(authMiddleware) groups.Use(authMiddleware)
{ {
@@ -548,179 +472,179 @@ func (r *Router) setupRoutes() {
// 新的 RESTful 风格路由(推荐使用) // 新的 RESTful 风格路由(推荐使用)
// ================================================================ // ================================================================
// 群组基本操作 // 群组基本操作
groups.GET("", r.groupHandler.HandleGetUserGroups) // 列表 groups.GET("", r.GroupHandler.HandleGetUserGroups) // 列表
groups.GET("/cursor", r.groupHandler.GetGroupsByCursor) // 群组游标分页 groups.GET("/cursor", r.GroupHandler.GetGroupsByCursor) // 群组游标分页
groups.POST("", r.groupHandler.HandleCreateGroup) // 创建 groups.POST("", r.GroupHandler.HandleCreateGroup) // 创建
groups.GET("/:id", r.groupHandler.HandleGetGroupInfo) // 详情 groups.GET("/:id", r.GroupHandler.HandleGetGroupInfo) // 详情
groups.GET("/:id/me", r.groupHandler.HandleGetMyMemberInfo) // 当前用户成员信息 groups.GET("/:id/me", r.GroupHandler.HandleGetMyMemberInfo) // 当前用户成员信息
groups.DELETE("/:id", r.groupHandler.HandleDissolveGroup) // 解散群组 groups.DELETE("/:id", r.GroupHandler.HandleDissolveGroup) // 解散群组
groups.POST("/:id/transfer", r.groupHandler.HandleTransferOwner) // 转让群主 groups.POST("/:id/transfer", r.GroupHandler.HandleTransferOwner) // 转让群主
// 成员管理 // 成员管理
groups.POST("/:id/invitations", r.groupHandler.HandleInviteMembers) // 邀请成员 groups.POST("/:id/invitations", r.GroupHandler.HandleInviteMembers) // 邀请成员
groups.POST("/:id/join-requests", r.groupHandler.HandleJoinGroup) // 申请加群 groups.POST("/:id/join-requests", r.GroupHandler.HandleJoinGroup) // 申请加群
groups.POST("/:id/join-requests/respond", r.groupHandler.HandleRespondInvite) // 响应加群邀请/申请 groups.POST("/:id/join-requests/respond", r.GroupHandler.HandleRespondInvite) // 响应加群邀请/申请
groups.POST("/:id/leave", r.groupHandler.HandleSetGroupLeave) // 退群 groups.POST("/:id/leave", r.GroupHandler.HandleSetGroupLeave) // 退群
groups.GET("/:id/members", r.groupHandler.HandleGetGroupMemberList) // 成员列表 groups.GET("/:id/members", r.GroupHandler.HandleGetGroupMemberList) // 成员列表
groups.GET("/:id/members/cursor", r.groupHandler.GetMembersByCursor) // 成员游标分页 groups.GET("/:id/members/cursor", r.GroupHandler.GetMembersByCursor) // 成员游标分页
groups.POST("/:id/members/kick", r.groupHandler.HandleSetGroupKick) // 踢出成员 groups.POST("/:id/members/kick", r.GroupHandler.HandleSetGroupKick) // 踢出成员
groups.PUT("/:id/members/:user_id/admin", r.groupHandler.HandleSetGroupAdmin) // 设置管理员 groups.PUT("/:id/members/:user_id/admin", r.GroupHandler.HandleSetGroupAdmin) // 设置管理员
groups.PUT("/:id/members/me/nickname", r.groupHandler.HandleSetNickname) // 设置群昵称 groups.PUT("/:id/members/me/nickname", r.GroupHandler.HandleSetNickname) // 设置群昵称
groups.POST("/:id/members/ban", r.groupHandler.HandleSetGroupBan) // 禁言成员 groups.POST("/:id/members/ban", r.GroupHandler.HandleSetGroupBan) // 禁言成员
// 群设置 // 群设置
groups.PUT("/:id/ban", r.groupHandler.HandleSetGroupWholeBan) // 全员禁言 groups.PUT("/:id/ban", r.GroupHandler.HandleSetGroupWholeBan) // 全员禁言
groups.PUT("/:id/join-type", r.groupHandler.HandleSetJoinType) // 加群方式 groups.PUT("/:id/join-type", r.GroupHandler.HandleSetJoinType) // 加群方式
groups.PUT("/:id/name", r.groupHandler.HandleSetGroupName) // 群名称 groups.PUT("/:id/name", r.GroupHandler.HandleSetGroupName) // 群名称
groups.PUT("/:id/avatar", r.groupHandler.HandleSetGroupAvatar) // 群头像 groups.PUT("/:id/avatar", r.GroupHandler.HandleSetGroupAvatar) // 群头像
groups.PUT("/:id/description", r.groupHandler.HandleSetGroupDescription) // 群描述 groups.PUT("/:id/description", r.GroupHandler.HandleSetGroupDescription) // 群描述
// 群公告 // 群公告
groups.POST("/:id/announcements", r.groupHandler.HandleCreateAnnouncement) // 创建公告 groups.POST("/:id/announcements", r.GroupHandler.HandleCreateAnnouncement) // 创建公告
groups.GET("/:id/announcements", r.groupHandler.HandleGetAnnouncements) // 公告列表 groups.GET("/:id/announcements", r.GroupHandler.HandleGetAnnouncements) // 公告列表
groups.GET("/:id/announcements/cursor", r.groupHandler.GetAnnouncementsByCursor) // 公告游标分页 groups.GET("/:id/announcements/cursor", r.GroupHandler.GetAnnouncementsByCursor) // 公告游标分页
groups.DELETE("/:id/announcements/:announcement_id", r.groupHandler.HandleDeleteAnnouncement) // 删除公告 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 := v1.Group("/stickers")
stickers.Use(authMiddleware) stickers.Use(authMiddleware)
{ {
stickers.GET("", r.stickerHandler.GetStickers) stickers.GET("", r.StickerHandler.GetStickers)
stickers.POST("", r.stickerHandler.AddSticker) stickers.POST("", r.StickerHandler.AddSticker)
stickers.DELETE("", r.stickerHandler.DeleteSticker) stickers.DELETE("", r.StickerHandler.DeleteSticker)
stickers.POST("/reorder", r.stickerHandler.ReorderStickers) stickers.POST("/reorder", r.StickerHandler.ReorderStickers)
stickers.GET("/check", r.stickerHandler.CheckStickerExists) stickers.GET("/check", r.StickerHandler.CheckStickerExists)
} }
} }
// 管理路由(需要管理员权限) // 管理路由(需要管理员权限)
if r.roleHandler != nil { if r.RoleHandler != nil {
admin := v1.Group("/admin") admin := v1.Group("/admin")
admin.Use(authMiddleware) 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", r.RoleHandler.GetAllRoles)
admin.GET("/roles/:name", r.roleHandler.GetRoleDetail) admin.GET("/roles/:name", r.RoleHandler.GetRoleDetail)
admin.GET("/roles/:name/permissions", r.roleHandler.GetRolePermissions) admin.GET("/roles/:name/permissions", r.RoleHandler.GetRolePermissions)
admin.PUT("/roles/:name/permissions", r.roleHandler.UpdateRolePermissions) admin.PUT("/roles/:name/permissions", r.RoleHandler.UpdateRolePermissions)
admin.GET("/permissions", r.roleHandler.GetAllPermissions) admin.GET("/permissions", r.RoleHandler.GetAllPermissions)
admin.GET("/users/:id/roles", r.roleHandler.GetUserRoles) admin.GET("/users/:id/roles", r.RoleHandler.GetUserRoles)
admin.POST("/users/:id/roles", r.roleHandler.AssignRole) admin.POST("/users/:id/roles", r.RoleHandler.AssignRole)
admin.DELETE("/users/:id/roles/:role", r.roleHandler.RemoveRole) admin.DELETE("/users/:id/roles/:role", r.RoleHandler.RemoveRole)
// 用户管理 // 用户管理
if r.adminUserHandler != nil { if r.AdminUserHandler != nil {
admin.GET("/users", r.adminUserHandler.GetUserList) admin.GET("/users", r.AdminUserHandler.GetUserList)
admin.GET("/users/:id", r.adminUserHandler.GetUserDetail) admin.GET("/users/:id", r.AdminUserHandler.GetUserDetail)
admin.PUT("/users/:id/status", r.adminUserHandler.UpdateUserStatus) admin.PUT("/users/:id/status", r.AdminUserHandler.UpdateUserStatus)
} }
// 帖子管理 // 帖子管理
if r.adminPostHandler != nil { if r.AdminPostHandler != nil {
admin.GET("/posts", r.adminPostHandler.GetPostList) admin.GET("/posts", r.AdminPostHandler.GetPostList)
admin.GET("/posts/:id", r.adminPostHandler.GetPostDetail) admin.GET("/posts/:id", r.AdminPostHandler.GetPostDetail)
admin.DELETE("/posts/:id", r.adminPostHandler.DeletePost) admin.DELETE("/posts/:id", r.AdminPostHandler.DeletePost)
admin.PUT("/posts/:id/moderate", r.adminPostHandler.ModeratePost) admin.PUT("/posts/:id/moderate", r.AdminPostHandler.ModeratePost)
admin.POST("/posts/batch-delete", r.adminPostHandler.BatchDeletePosts) admin.POST("/posts/batch-delete", r.AdminPostHandler.BatchDeletePosts)
admin.POST("/posts/batch-moderate", r.adminPostHandler.BatchModeratePosts) admin.POST("/posts/batch-moderate", r.AdminPostHandler.BatchModeratePosts)
admin.PUT("/posts/:id/pin", r.adminPostHandler.SetPostPin) admin.PUT("/posts/:id/pin", r.AdminPostHandler.SetPostPin)
admin.PUT("/posts/:id/feature", r.adminPostHandler.SetPostFeature) admin.PUT("/posts/:id/feature", r.AdminPostHandler.SetPostFeature)
} }
// 频道管理 // 频道管理
if r.channelHandler != nil { if r.ChannelHandler != nil {
admin.GET("/channels", r.channelHandler.AdminList) admin.GET("/channels", r.ChannelHandler.AdminList)
admin.POST("/channels", r.channelHandler.AdminCreate) admin.POST("/channels", r.ChannelHandler.AdminCreate)
admin.PUT("/channels/:id", r.channelHandler.AdminUpdate) admin.PUT("/channels/:id", r.ChannelHandler.AdminUpdate)
admin.DELETE("/channels/:id", r.channelHandler.AdminDelete) admin.DELETE("/channels/:id", r.ChannelHandler.AdminDelete)
} }
// 评论管理 // 评论管理
if r.adminCommentHandler != nil { if r.AdminCommentHandler != nil {
admin.GET("/comments", r.adminCommentHandler.GetCommentList) admin.GET("/comments", r.AdminCommentHandler.GetCommentList)
admin.GET("/comments/:id", r.adminCommentHandler.GetCommentDetail) admin.GET("/comments/:id", r.AdminCommentHandler.GetCommentDetail)
admin.DELETE("/comments/:id", r.adminCommentHandler.DeleteComment) admin.DELETE("/comments/:id", r.AdminCommentHandler.DeleteComment)
admin.PUT("/comments/:id/moderate", r.adminCommentHandler.ModerateComment) admin.PUT("/comments/:id/moderate", r.AdminCommentHandler.ModerateComment)
admin.POST("/comments/batch-delete", r.adminCommentHandler.BatchDeleteComments) admin.POST("/comments/batch-delete", r.AdminCommentHandler.BatchDeleteComments)
admin.POST("/comments/batch-moderate", r.adminCommentHandler.BatchModerateComments) admin.POST("/comments/batch-moderate", r.AdminCommentHandler.BatchModerateComments)
admin.GET("/comments/post/:postId", r.adminCommentHandler.GetPostComments) admin.GET("/comments/post/:postId", r.AdminCommentHandler.GetPostComments)
} }
// 群组管理 // 群组管理
if r.adminGroupHandler != nil { if r.AdminGroupHandler != nil {
admin.GET("/groups", r.adminGroupHandler.GetGroupList) admin.GET("/groups", r.AdminGroupHandler.GetGroupList)
admin.GET("/groups/:id", r.adminGroupHandler.GetGroupDetail) admin.GET("/groups/:id", r.AdminGroupHandler.GetGroupDetail)
admin.GET("/groups/:id/members", r.adminGroupHandler.GetGroupMembers) admin.GET("/groups/:id/members", r.AdminGroupHandler.GetGroupMembers)
admin.DELETE("/groups/:id", r.adminGroupHandler.DeleteGroup) admin.DELETE("/groups/:id", r.AdminGroupHandler.DeleteGroup)
admin.PUT("/groups/:id/status", r.adminGroupHandler.UpdateGroupStatus) admin.PUT("/groups/:id/status", r.AdminGroupHandler.UpdateGroupStatus)
admin.DELETE("/groups/:id/members/:userId", r.adminGroupHandler.RemoveMember) admin.DELETE("/groups/:id/members/:userId", r.AdminGroupHandler.RemoveMember)
admin.PUT("/groups/:id/transfer", r.adminGroupHandler.TransferOwner) admin.PUT("/groups/:id/transfer", r.AdminGroupHandler.TransferOwner)
} }
// 仪表盘 // 仪表盘
if r.adminDashboardHandler != nil { if r.AdminDashboardHandler != nil {
admin.GET("/dashboard/stats", r.adminDashboardHandler.GetStats) admin.GET("/dashboard/stats", r.AdminDashboardHandler.GetStats)
admin.GET("/dashboard/user-activity", r.adminDashboardHandler.GetUserActivityTrend) admin.GET("/dashboard/user-activity", r.AdminDashboardHandler.GetUserActivityTrend)
admin.GET("/dashboard/content-stats", r.adminDashboardHandler.GetContentStats) admin.GET("/dashboard/content-stats", r.AdminDashboardHandler.GetContentStats)
admin.GET("/dashboard/pending-content", r.adminDashboardHandler.GetPendingContent) admin.GET("/dashboard/pending-content", r.AdminDashboardHandler.GetPendingContent)
admin.GET("/dashboard/online-users", r.adminDashboardHandler.GetOnlineUsers) admin.GET("/dashboard/online-users", r.AdminDashboardHandler.GetOnlineUsers)
} }
// 日志管理 // 日志管理
if r.adminLogHandler != nil { if r.AdminLogHandler != nil {
logs := admin.Group("/logs") logs := admin.Group("/logs")
{ {
logs.GET("/operations", r.adminLogHandler.GetOperationLogs) logs.GET("/operations", r.AdminLogHandler.GetOperationLogs)
logs.GET("/logins", r.adminLogHandler.GetLoginLogs) logs.GET("/logins", r.AdminLogHandler.GetLoginLogs)
logs.GET("/data-changes", r.adminLogHandler.GetDataChangeLogs) logs.GET("/data-changes", r.AdminLogHandler.GetDataChangeLogs)
logs.GET("/statistics", r.adminLogHandler.GetLogStatistics) logs.GET("/statistics", r.AdminLogHandler.GetLogStatistics)
logs.GET("/export", r.adminLogHandler.ExportLogs) logs.GET("/export", r.AdminLogHandler.ExportLogs)
} }
} }
// 学习资料管理 // 学习资料管理
if r.materialHandler != nil { if r.MaterialHandler != nil {
// 学科管理 // 学科管理
admin.GET("/materials/subjects", r.materialHandler.AdminListSubjects) admin.GET("/materials/subjects", r.MaterialHandler.AdminListSubjects)
admin.POST("/materials/subjects", r.materialHandler.AdminCreateSubject) admin.POST("/materials/subjects", r.MaterialHandler.AdminCreateSubject)
admin.PUT("/materials/subjects/:id", r.materialHandler.AdminUpdateSubject) admin.PUT("/materials/subjects/:id", r.MaterialHandler.AdminUpdateSubject)
admin.DELETE("/materials/subjects/:id", r.materialHandler.AdminDeleteSubject) admin.DELETE("/materials/subjects/:id", r.MaterialHandler.AdminDeleteSubject)
// 资料管理 // 资料管理
admin.GET("/materials", r.materialHandler.AdminListMaterials) admin.GET("/materials", r.MaterialHandler.AdminListMaterials)
admin.GET("/materials/:id", r.materialHandler.AdminGetMaterial) admin.GET("/materials/:id", r.MaterialHandler.AdminGetMaterial)
admin.POST("/materials", r.materialHandler.AdminCreateMaterial) admin.POST("/materials", r.MaterialHandler.AdminCreateMaterial)
admin.PUT("/materials/:id", r.materialHandler.AdminUpdateMaterial) admin.PUT("/materials/:id", r.MaterialHandler.AdminUpdateMaterial)
admin.DELETE("/materials/:id", r.materialHandler.AdminDeleteMaterial) admin.DELETE("/materials/:id", r.MaterialHandler.AdminDeleteMaterial)
} }
// 举报管理 // 举报管理
if r.adminReportHandler != nil { if r.AdminReportHandler != nil {
admin.GET("/reports", r.adminReportHandler.List) admin.GET("/reports", r.AdminReportHandler.List)
admin.GET("/reports/:id", r.adminReportHandler.Detail) admin.GET("/reports/:id", r.AdminReportHandler.Detail)
admin.PUT("/reports/:id/handle", r.adminReportHandler.Handle) admin.PUT("/reports/:id/handle", r.AdminReportHandler.Handle)
admin.POST("/reports/batch-handle", r.adminReportHandler.BatchHandle) admin.POST("/reports/batch-handle", r.AdminReportHandler.BatchHandle)
} }
// 身份认证管理 // 身份认证管理
if r.adminVerificationHandler != nil { if r.AdminVerificationHandler != nil {
admin.GET("/verifications", r.adminVerificationHandler.ListVerifications) admin.GET("/verifications", r.AdminVerificationHandler.ListVerifications)
admin.GET("/verifications/:id", r.adminVerificationHandler.GetVerificationDetail) admin.GET("/verifications/:id", r.AdminVerificationHandler.GetVerificationDetail)
admin.PUT("/verifications/:id/review", r.adminVerificationHandler.ReviewVerification) admin.PUT("/verifications/:id/review", r.AdminVerificationHandler.ReviewVerification)
} }
// 用户资料审核管理 // 用户资料审核管理
if r.adminProfileAuditHandler != nil { if r.AdminProfileAuditHandler != nil {
admin.GET("/profile-audits", r.adminProfileAuditHandler.GetPendingList) admin.GET("/profile-audits", r.AdminProfileAuditHandler.GetPendingList)
admin.PUT("/profile-audits/:id/moderate", r.adminProfileAuditHandler.ModerateAudit) admin.PUT("/profile-audits/:id/moderate", r.AdminProfileAuditHandler.ModerateAudit)
admin.POST("/profile-audits/batch-moderate", r.adminProfileAuditHandler.BatchModerate) admin.POST("/profile-audits/batch-moderate", r.AdminProfileAuditHandler.BatchModerate)
} }
} }
} }

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import (
"with_you/internal/dto" "with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/query"
"with_you/internal/repository" "with_you/internal/repository"
"go.uber.org/zap" "go.uber.org/zap"
@@ -15,7 +16,7 @@ import (
// AdminReportService 管理端举报服务接口 // AdminReportService 管理端举报服务接口
type AdminReportService interface { type AdminReportService interface {
// GetReportList 获取举报列表 // 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 获取举报详情
GetReportDetail(ctx context.Context, id string) (*dto.AdminReportDetailResponse, error) GetReportDetail(ctx context.Context, id string) (*dto.AdminReportDetailResponse, error)
// HandleReport 处理举报 // HandleReport 处理举报
@@ -63,7 +64,7 @@ func NewAdminReportService(
} }
// GetReportList 获取举报列表 // 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 { if query.Page <= 0 {
query.Page = 1 query.Page = 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,8 @@ package service
import ( import (
"errors" "errors"
"with_you/internal/dto"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/query"
"with_you/internal/repository" "with_you/internal/repository"
) )
@@ -23,7 +23,7 @@ type MaterialService interface {
DeleteSubject(id string) error 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) GetMaterialByID(id string) (*model.MaterialFile, error)
GetMaterialByIDWithSubject(id string) (*model.MaterialFile, error) GetMaterialByIDWithSubject(id string) (*model.MaterialFile, error)
GetMaterialsBySubject(subjectID string, page, pageSize int) ([]*model.MaterialFile, int64, 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) return s.fileRepo.List(params)
} }

View File

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

View File

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

View File

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

View File

@@ -171,21 +171,31 @@ func (e *BioModerationReviewError) IsReview() bool {
return true 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 openAIClient openai.Client
tencentClient tencent.Client tencentClient tencent.Client
strictMode bool strictMode bool
} }
func NewPostAIService(openAIClient openai.Client, tencentClient tencent.Client, strictMode bool) *PostAIService { func NewPostAIService(openAIClient openai.Client, tencentClient tencent.Client, strictMode bool) PostAIService {
return &PostAIService{ return &postAIService{
openAIClient: openAIClient, openAIClient: openAIClient,
tencentClient: tencentClient, tencentClient: tencentClient,
strictMode: strictMode, strictMode: strictMode,
} }
} }
func (s *PostAIService) IsEnabled() bool { func (s *postAIService) IsEnabled() bool {
if s == nil { if s == nil {
return false return false
} }
@@ -193,15 +203,15 @@ func (s *PostAIService) IsEnabled() bool {
(s.tencentClient != nil && s.tencentClient.IsEnabled()) (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() 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() 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() { if !s.IsEnabled() {
return nil 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() { if !s.IsEnabled() {
return nil 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() { if !s.isOpenAIEnabled() {
return nil 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() { if !s.IsEnabled() {
return nil 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, ctx context.Context,
text string, text string,
rejectFactory func(string) error, rejectFactory func(string) error,

View File

@@ -8,9 +8,9 @@ import (
"strings" "strings"
"time" "time"
apperrors "with_you/internal/errors"
"with_you/internal/cache" "with_you/internal/cache"
"with_you/internal/dto" "with_you/internal/dto"
apperrors "with_you/internal/errors"
"with_you/internal/model" "with_you/internal/model"
"with_you/internal/pkg/cursor" "with_you/internal/pkg/cursor"
"with_you/internal/pkg/hook" "with_you/internal/pkg/hook"
@@ -75,13 +75,13 @@ type postServiceImpl struct {
postRepo repository.PostRepository postRepo repository.PostRepository
systemMessageService SystemMessageService systemMessageService SystemMessageService
cache cache.Cache cache cache.Cache
postAIService *PostAIService postAIService PostAIService
txManager repository.TransactionManager txManager repository.TransactionManager
logService *LogService logService *LogService
hookManager *hook.Manager 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{ return &postServiceImpl{
postRepo: postRepo, postRepo: postRepo,
systemMessageService: systemMessageService, systemMessageService: systemMessageService,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package wire package wire
import ( import (
"time" "time"
@@ -7,8 +7,8 @@ import (
"with_you/internal/grpc/runner" "with_you/internal/grpc/runner"
"with_you/internal/pkg/redis" "with_you/internal/pkg/redis"
"github.com/google/wire"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/google/wire"
"go.uber.org/zap" "go.uber.org/zap"
) )

View File

@@ -1,10 +1,9 @@
package wire package wire
import ( import (
"with_you/internal/config" "with_you/internal/config"
"with_you/internal/handler" "with_you/internal/handler"
"with_you/internal/pkg/ws" "with_you/internal/pkg/ws"
"with_you/internal/repository"
"with_you/internal/service" "with_you/internal/service"
"github.com/google/wire" "github.com/google/wire"
@@ -66,7 +65,7 @@ func ProvideUserHandler(
func ProvideMessageHandler( func ProvideMessageHandler(
chatService service.ChatService, chatService service.ChatService,
messageService *service.MessageService, messageService service.MessageService,
userService service.UserService, userService service.UserService,
groupService service.GroupService, groupService service.GroupService,
publisher ws.MessagePublisher, publisher ws.MessagePublisher,
@@ -102,11 +101,11 @@ func ProvideWSHandler(
publisher ws.MessagePublisher, publisher ws.MessagePublisher,
chatService service.ChatService, chatService service.ChatService,
groupService service.GroupService, groupService service.GroupService,
jwtService *service.JWTService, jwtService service.JWTService,
callService service.CallService, callService service.CallService,
userRepo repository.UserRepository, userService service.UserService,
) *handler.WSHandler { ) *handler.WSHandler {
return handler.NewWSHandler(publisher, chatService, groupService, jwtService, callService, userRepo) return handler.NewWSHandler(publisher, chatService, groupService, jwtService, callService, userService)
} }
// ProvideLiveKitHandler 提供 LiveKit 处理器 // ProvideLiveKitHandler 提供 LiveKit 处理器
@@ -122,9 +121,9 @@ func ProvideLiveKitHandler(
// ProvideAdminVerificationHandler 提供管理端身份认证处理器 // ProvideAdminVerificationHandler 提供管理端身份认证处理器
func ProvideAdminVerificationHandler( func ProvideAdminVerificationHandler(
adminVerificationService service.AdminVerificationService, adminVerificationService service.AdminVerificationService,
userRepo repository.UserRepository, userService service.UserService,
) *handler.AdminVerificationHandler { ) *handler.AdminVerificationHandler {
return handler.NewAdminVerificationHandler(adminVerificationService, userRepo) return handler.NewAdminVerificationHandler(adminVerificationService, userService)
} }
func ProvideSetupHandler( func ProvideSetupHandler(

View File

@@ -1,11 +1,12 @@
package wire package wire
import ( import (
"fmt"
"time" "time"
"with_you/internal/cache" "with_you/internal/cache"
"with_you/internal/config" "with_you/internal/config"
"with_you/internal/model" "with_you/internal/database"
"with_you/internal/pkg/crypto" "with_you/internal/pkg/crypto"
"with_you/internal/pkg/email" "with_you/internal/pkg/email"
"with_you/internal/pkg/hook" "with_you/internal/pkg/hook"
@@ -18,8 +19,8 @@ import (
"with_you/internal/repository" "with_you/internal/repository"
"with_you/internal/service" "with_you/internal/service"
redisPkg "github.com/redis/go-redis/v9"
"github.com/google/wire" "github.com/google/wire"
redisPkg "github.com/redis/go-redis/v9"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -74,7 +75,7 @@ func ProvideConfig() (*config.Config, error) {
// ProvideDB 提供数据库连接 // ProvideDB 提供数据库连接
func ProvideDB(cfg *config.Config) (*gorm.DB, error) { func ProvideDB(cfg *config.Config) (*gorm.DB, error) {
return model.NewDB(&cfg.Database) return database.NewDB(&cfg.Database)
} }
// ProvideRedisClient 提供 Redis 客户端 // ProvideRedisClient 提供 Redis 客户端
@@ -145,7 +146,7 @@ func ProvideBuiltinHooks(manager *hook.Manager) *hook.BuiltinHooks {
// ProvideModerationHooks 注册审核钩子 // ProvideModerationHooks 注册审核钩子
func ProvideModerationHooks( func ProvideModerationHooks(
manager *hook.Manager, manager *hook.Manager,
postAIService *service.PostAIService, postAIService service.PostAIService,
postRepo repository.PostRepository, postRepo repository.PostRepository,
commentRepo repository.CommentRepository, commentRepo repository.CommentRepository,
userRepo repository.UserRepository, userRepo repository.UserRepository,
@@ -248,6 +249,7 @@ func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager {
} }
// ProvideMessageEncryptor 提供消息加密器 // ProvideMessageEncryptor 提供消息加密器
// 加密启用时密钥必须是 32 字节AES-256否则视为致命配置错误启动期 fail-fast。
func ProvideMessageEncryptor(cfg *config.Config) error { func ProvideMessageEncryptor(cfg *config.Config) error {
if !cfg.Encryption.Enabled { if !cfg.Encryption.Enabled {
zap.L().Info("Message encryption is disabled") zap.L().Info("Message encryption is disabled")
@@ -255,15 +257,12 @@ func ProvideMessageEncryptor(cfg *config.Config) error {
} }
if len(cfg.Encryption.Key) != 32 { 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 fmt.Errorf("encryption is enabled but key must be 32 bytes for AES-256, got %d", len(cfg.Encryption.Key))
)
return nil
} }
if err := crypto.InitMessageEncryptor(cfg.Encryption.Key, cfg.Encryption.KeyVersion); err != nil { if err := crypto.InitMessageEncryptor(cfg.Encryption.Key, cfg.Encryption.KeyVersion); err != nil {
zap.L().Error("Failed to initialize message encryptor", zap.Error(err)) return fmt.Errorf("failed to initialize message encryptor: %w", err)
return err
} }
zap.L().Info("Message encryption initialized successfully", zap.L().Info("Message encryption initialized successfully",

View File

@@ -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")
}
}

View File

@@ -1,4 +1,4 @@
package wire package wire
import ( import (
"with_you/internal/cache" "with_you/internal/cache"
@@ -24,9 +24,9 @@ var RepositorySet = wire.NewSet(
repository.NewVoteRepository, repository.NewVoteRepository,
repository.NewChannelRepository, repository.NewChannelRepository,
repository.NewScheduleRepository, repository.NewScheduleRepository,
repository.NewGradeRepository, repository.NewGradeRepository,
repository.NewExamRepository, repository.NewExamRepository,
repository.NewEmptyClassroomRepository, repository.NewEmptyClassroomRepository,
repository.NewMaterialSubjectRepository, repository.NewMaterialSubjectRepository,
repository.NewMaterialFileRepository, repository.NewMaterialFileRepository,
repository.NewCallRepository, repository.NewCallRepository,
@@ -53,8 +53,8 @@ var RepositorySet = wire.NewSet(
// 二手交易相关 // 二手交易相关
repository.NewTradeRepository, repository.NewTradeRepository,
// 版本日志同步 // 版本日志同步
repository.NewConversationVersionLogRepository, repository.NewConversationVersionLogRepository,
) )
// ProvideUserActivityRepository 提供用户活跃数据仓储 // ProvideUserActivityRepository 提供用户活跃数据仓储

View File

@@ -1,4 +1,4 @@
package wire package wire
import ( import (
"time" "time"
@@ -46,9 +46,9 @@ var ServiceSet = wire.NewSet(
ProvideChatService, ProvideChatService,
ProvideScheduleService, ProvideScheduleService,
ProvideScheduleSyncService, ProvideScheduleSyncService,
ProvideGradeSyncService, ProvideGradeSyncService,
ProvideExamSyncService, ProvideExamSyncService,
ProvideEmptyClassroomSyncService, ProvideEmptyClassroomSyncService,
ProvideGroupService, ProvideGroupService,
ProvideUploadService, ProvideUploadService,
ProvideUserActivityService, ProvideUserActivityService,
@@ -87,7 +87,7 @@ var ServiceSet = wire.NewSet(
) )
// ProvideJWTService 提供 JWT 服务 // ProvideJWTService 提供 JWT 服务
func ProvideJWTService(cfg *config.Config) *service.JWTService { func ProvideJWTService(cfg *config.Config) service.JWTService {
return service.NewJWTService( return service.NewJWTService(
cfg.JWT.Secret, cfg.JWT.Secret,
int64(cfg.JWT.AccessTokenExpire.Seconds()), int64(cfg.JWT.AccessTokenExpire.Seconds()),
@@ -101,7 +101,7 @@ func ProvideEmailService(client email.Client) service.EmailService {
} }
// ProvidePostAIService 提供 AI 服务 // 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) return service.NewPostAIService(openAIClient, tencentClient, cfg.OpenAI.StrictModeration)
} }
@@ -130,7 +130,7 @@ func ProvideSystemMessageService(
func ProvidePostService( func ProvidePostService(
postRepo repository.PostRepository, postRepo repository.PostRepository,
systemMessageService service.SystemMessageService, systemMessageService service.SystemMessageService,
postAIService *service.PostAIService, postAIService service.PostAIService,
cacheBackend cache.Cache, cacheBackend cache.Cache,
txManager repository.TransactionManager, txManager repository.TransactionManager,
hookManager *hook.Manager, hookManager *hook.Manager,
@@ -145,11 +145,11 @@ func ProvideCommentService(
commentRepo repository.CommentRepository, commentRepo repository.CommentRepository,
postRepo repository.PostRepository, postRepo repository.PostRepository,
systemMessageService service.SystemMessageService, systemMessageService service.SystemMessageService,
postAIService *service.PostAIService, postAIService service.PostAIService,
cacheBackend cache.Cache, cacheBackend cache.Cache,
hookManager *hook.Manager, hookManager *hook.Manager,
logService *service.LogService, logService *service.LogService,
) *service.CommentService { ) service.CommentService {
return service.NewCommentService(commentRepo, postRepo, systemMessageService, postAIService, cacheBackend, hookManager, logService) return service.NewCommentService(commentRepo, postRepo, systemMessageService, postAIService, cacheBackend, hookManager, logService)
} }
@@ -157,8 +157,8 @@ func ProvideCommentService(
func ProvideMessageService( func ProvideMessageService(
messageRepo repository.MessageRepository, messageRepo repository.MessageRepository,
cacheBackend cache.Cache, cacheBackend cache.Cache,
uploadService *service.UploadService, uploadService service.UploadService,
) *service.MessageService { ) service.MessageService {
return service.NewMessageService(messageRepo, cacheBackend, uploadService) return service.NewMessageService(messageRepo, cacheBackend, uploadService)
} }
@@ -166,7 +166,7 @@ func ProvideMessageService(
func ProvideNotificationService( func ProvideNotificationService(
notificationRepo repository.NotificationRepository, notificationRepo repository.NotificationRepository,
cacheBackend cache.Cache, cacheBackend cache.Cache,
) *service.NotificationService { ) service.NotificationService {
return service.NewNotificationService(notificationRepo, cacheBackend) return service.NewNotificationService(notificationRepo, cacheBackend)
} }
@@ -193,11 +193,11 @@ func ProvideVoteService(
voteRepo repository.VoteRepository, voteRepo repository.VoteRepository,
postRepo repository.PostRepository, postRepo repository.PostRepository,
cache cache.Cache, cache cache.Cache,
postAIService *service.PostAIService, postAIService service.PostAIService,
systemMessageService service.SystemMessageService, systemMessageService service.SystemMessageService,
hookManager *hook.Manager, hookManager *hook.Manager,
logService *service.LogService, logService *service.LogService,
) *service.VoteService { ) service.VoteService {
return service.NewVoteService(voteRepo, postRepo, cache, postAIService, systemMessageService, hookManager, logService) return service.NewVoteService(voteRepo, postRepo, cache, postAIService, systemMessageService, hookManager, logService)
} }
@@ -212,7 +212,7 @@ func ProvideChatService(
userRepo repository.UserRepository, userRepo repository.UserRepository,
publisher ws.MessagePublisher, publisher ws.MessagePublisher,
cacheBackend cache.Cache, cacheBackend cache.Cache,
uploadService *service.UploadService, uploadService service.UploadService,
pushService service.PushService, pushService service.PushService,
seqBufferMgr *cache.SeqBufferManager, seqBufferMgr *cache.SeqBufferManager,
pushWorker *service.PushWorker, pushWorker *service.PushWorker,
@@ -291,7 +291,7 @@ func ProvideGroupService(
func ProvideUploadService( func ProvideUploadService(
s3Client *s3.Client, s3Client *s3.Client,
userService service.UserService, userService service.UserService,
) *service.UploadService { ) service.UploadService {
return service.NewUploadService(s3Client, userService) return service.NewUploadService(s3Client, userService)
} }
@@ -432,11 +432,11 @@ func ProvideHotRankWorker(cfg *config.Config, postRepo repository.PostRepository
func ProvideQRCodeLoginService( func ProvideQRCodeLoginService(
redisClient *redis.Client, redisClient *redis.Client,
publisher ws.MessagePublisher, publisher ws.MessagePublisher,
jwtService *service.JWTService, jwtService service.JWTService,
userService service.UserService, userService service.UserService,
activityService service.UserActivityService, activityService service.UserActivityService,
logService *service.LogService, logService *service.LogService,
) *service.QRCodeLoginService { ) service.QRCodeLoginService {
return service.NewQRCodeLoginService(redisClient, publisher, jwtService, userService, activityService, logService) return service.NewQRCodeLoginService(redisClient, publisher, jwtService, userService, activityService, logService)
} }