refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion. - Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects. - Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management. - Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering. - Improve database initialization by moving it from `internal/model` to `internal/database`. - Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool. - Enhance error handling and security by implementing fail-fast checks for encryption key length during startup. - Clean up unused code, including the `avatar` package and several unused DTOs.
This commit is contained in:
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
7
internal/cache/keys.go
vendored
7
internal/cache/keys.go
vendored
@@ -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)
|
||||||
|
|||||||
11
internal/cache/seq_buffer.go
vendored
11
internal/cache/seq_buffer.go
vendored
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
222
internal/database/database_test.go
Normal file
222
internal/database/database_test.go
Normal 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_created(data_change_log / login_log / operation_log)而冲突。
|
||||||
|
// 这是 model 包的既有约束(PostgreSQL 经 CREATE INDEX IF NOT EXISTS 容忍),
|
||||||
|
// 与本次分层拆分无关。因此种子逻辑测试在隔离的 SQLite 库上验证:
|
||||||
|
// 仅迁移被种子函数读写的表,避开跨表索引冲突,专注验证种子正确性与幂等性。
|
||||||
|
|
||||||
|
// newSeedTestDB 创建一个仅迁移种子相关表的 SQLite 测试库。
|
||||||
|
func newSeedTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "seed_test.db")
|
||||||
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅迁移种子函数涉及的表(无 idx_created 冲突)
|
||||||
|
if err := db.AutoMigrate(
|
||||||
|
&model.Role{},
|
||||||
|
&model.CasbinRule{},
|
||||||
|
&model.Channel{},
|
||||||
|
&model.MaterialSubject{},
|
||||||
|
&model.Post{}, // dropPostsHotScoreColumnIfExists 依赖 posts 表存在
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("autoMigrate seed tables failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if sqlDB, err := db.DB(); err == nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeedRoles 验证角色种子写入 5 个预定义角色。
|
||||||
|
func TestSeedRoles(t *testing.T) {
|
||||||
|
db := newSeedTestDB(t)
|
||||||
|
|
||||||
|
if err := seedRoles(db); err != nil {
|
||||||
|
t.Fatalf("seedRoles failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&model.Role{}).Count(&count)
|
||||||
|
if count != 5 {
|
||||||
|
t.Errorf("expected 5 seeded roles, got %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
model.RoleSuperAdmin, model.RoleAdmin, model.RoleModerator,
|
||||||
|
model.RoleUser, model.RoleBanned,
|
||||||
|
}
|
||||||
|
for _, name := range expected {
|
||||||
|
var role model.Role
|
||||||
|
if err := db.Where("name = ?", name).First(&role).Error; err != nil {
|
||||||
|
t.Errorf("expected role %q: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeedRoles_Idempotent 验证角色种子幂等:表非空时跳过。
|
||||||
|
func TestSeedRoles_Idempotent(t *testing.T) {
|
||||||
|
db := newSeedTestDB(t)
|
||||||
|
|
||||||
|
if err := seedRoles(db); err != nil {
|
||||||
|
t.Fatalf("first seedRoles: %v", err)
|
||||||
|
}
|
||||||
|
// 第二次调用应跳过(表已有数据)
|
||||||
|
if err := seedRoles(db); err != nil {
|
||||||
|
t.Fatalf("second seedRoles: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&model.Role{}).Count(&count)
|
||||||
|
if count != 5 {
|
||||||
|
t.Errorf("idempotent seed failed: expected 5 roles, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeedPermissions 验证权限策略种子写入,含超级管理员通配规则。
|
||||||
|
func TestSeedPermissions(t *testing.T) {
|
||||||
|
db := newSeedTestDB(t)
|
||||||
|
|
||||||
|
if err := seedPermissions(db); err != nil {
|
||||||
|
t.Fatalf("seedPermissions failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count)
|
||||||
|
if count == 0 {
|
||||||
|
t.Fatal("expected seeded permissions, got 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超级管理员 /* 通配
|
||||||
|
var rule model.CasbinRule
|
||||||
|
if err := db.Where("ptype=? AND v0=? AND v1=?", "p", model.RoleSuperAdmin, "/*").First(&rule).Error; err != nil {
|
||||||
|
t.Errorf("expected superadmin wildcard permission: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeedPermissions_Idempotent 验证权限种子幂等。
|
||||||
|
func TestSeedPermissions_Idempotent(t *testing.T) {
|
||||||
|
db := newSeedTestDB(t)
|
||||||
|
|
||||||
|
if err := seedPermissions(db); err != nil {
|
||||||
|
t.Fatalf("first seedPermissions: %v", err)
|
||||||
|
}
|
||||||
|
var count1 int64
|
||||||
|
db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count1)
|
||||||
|
|
||||||
|
if err := seedPermissions(db); err != nil {
|
||||||
|
t.Fatalf("second seedPermissions: %v", err)
|
||||||
|
}
|
||||||
|
var count2 int64
|
||||||
|
db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count2)
|
||||||
|
|
||||||
|
if count2 != count1 {
|
||||||
|
t.Errorf("seed not idempotent: first=%d, second=%d", count1, count2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeedChannels 验证频道种子写入 5 个默认频道。
|
||||||
|
func TestSeedChannels(t *testing.T) {
|
||||||
|
db := newSeedTestDB(t)
|
||||||
|
|
||||||
|
if err := seedChannels(db); err != nil {
|
||||||
|
t.Fatalf("seedChannels failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&model.Channel{}).Count(&count)
|
||||||
|
if count != 5 {
|
||||||
|
t.Errorf("expected 5 channels, got %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽查一个频道
|
||||||
|
var ch model.Channel
|
||||||
|
if err := db.Where("slug = ?", "market").First(&ch).Error; err != nil {
|
||||||
|
t.Errorf("expected market channel: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeedMaterialSubjects 验证学科种子写入 8 个默认学科。
|
||||||
|
func TestSeedMaterialSubjects(t *testing.T) {
|
||||||
|
db := newSeedTestDB(t)
|
||||||
|
|
||||||
|
if err := seedMaterialSubjects(db); err != nil {
|
||||||
|
t.Fatalf("seedMaterialSubjects failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&model.MaterialSubject{}).Count(&count)
|
||||||
|
if count != 8 {
|
||||||
|
t.Errorf("expected 8 material subjects, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseGormLogLevel 验证日志级别解析映射。
|
||||||
|
func TestParseGormLogLevel(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in string
|
||||||
|
want logger.LogLevel
|
||||||
|
}{
|
||||||
|
{"silent", logger.Silent},
|
||||||
|
{"error", logger.Error},
|
||||||
|
{"warn", logger.Warn},
|
||||||
|
{"info", logger.Info},
|
||||||
|
{"", logger.Warn}, // 默认
|
||||||
|
{"bogus", logger.Warn}, // 未知值降级为 warn
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := parseGormLogLevel(c.in); got != c.want {
|
||||||
|
t.Errorf("parseGormLogLevel(%q) = %v, want %v", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDropPostsHotScoreColumnIfExists_NoTable 验证当 posts 表不存在时安全跳过。
|
||||||
|
func TestDropPostsHotScoreColumnIfExists_NoTable(t *testing.T) {
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "empty.db")
|
||||||
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if sqlDB, err := db.DB(); err == nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 未迁移 posts 表 → 应安全返回 nil
|
||||||
|
if err := dropPostsHotScoreColumnIfExists(db); err != nil {
|
||||||
|
t.Errorf("expected nil when posts table absent, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译期断言:确保 NewDB 签名符合 (*config.DatabaseConfig) -> (*gorm.DB, error)
|
||||||
|
var _ = func() (*gorm.DB, error) {
|
||||||
|
return NewDB(&config.DatabaseConfig{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = os.RemoveAll // 保持 os 引用可用(供未来临时文件场景扩展)
|
||||||
@@ -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响应结构体
|
||||||
|
|||||||
@@ -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
119
internal/dto/group_dto.go
Normal 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
86
internal/dto/post_dto.go
Normal 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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 == "" {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
110
internal/middleware/verification_test.go
Normal file
110
internal/middleware/verification_test.go
Normal 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 已认证用户 → 放行(200,next 被调用)。
|
||||||
|
func TestRequireVerified_Approved(t *testing.T) {
|
||||||
|
svc := &stubUserService{
|
||||||
|
user: &model.User{ID: "u1", VerificationStatus: model.VerificationStatusApproved},
|
||||||
|
}
|
||||||
|
code, next := runMiddleware(t, svc, true, "u1")
|
||||||
|
if code != http.StatusOK {
|
||||||
|
t.Errorf("status = %d, want %d", code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if !next {
|
||||||
|
t.Error("next handler should be called for verified user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireVerified_NotApproved 未通过认证用户 → 403 VERIFICATION_REQUIRED。
|
||||||
|
func TestRequireVerified_NotApproved(t *testing.T) {
|
||||||
|
svc := &stubUserService{
|
||||||
|
user: &model.User{ID: "u1", VerificationStatus: model.VerificationStatusPending},
|
||||||
|
}
|
||||||
|
code, next := runMiddleware(t, svc, true, "u1")
|
||||||
|
if code != http.StatusForbidden {
|
||||||
|
t.Errorf("status = %d, want %d", code, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
if next {
|
||||||
|
t.Error("next handler should NOT be called for unverified user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireVerified_UserLookupError 查询用户失败 → 500。
|
||||||
|
func TestRequireVerified_UserLookupError(t *testing.T) {
|
||||||
|
svc := &stubUserService{err: errors.New("db down")}
|
||||||
|
code, next := runMiddleware(t, svc, true, "u1")
|
||||||
|
if code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("status = %d, want %d", code, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
if next {
|
||||||
|
t.Error("next handler should NOT be called on lookup error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireVerified_NoUserID 未登录(无 user_id)→ 401。
|
||||||
|
func TestRequireVerified_NoUserID(t *testing.T) {
|
||||||
|
svc := &stubUserService{
|
||||||
|
user: &model.User{ID: "u1", VerificationStatus: model.VerificationStatusApproved},
|
||||||
|
}
|
||||||
|
code, next := runMiddleware(t, svc, false, nil)
|
||||||
|
if code != http.StatusUnauthorized {
|
||||||
|
t.Errorf("status = %d, want %d", code, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
if next {
|
||||||
|
t.Error("next handler should NOT be called when user_id missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.BatchDecrypt(worker 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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
140
internal/model/message_crypto_test.go
Normal file
140
internal/model/message_crypto_test.go
Normal 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
|
||||||
@@ -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)]
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
68
internal/query/filters.go
Normal 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"`
|
||||||
|
}
|
||||||
82
internal/query/filters_test.go
Normal file
82
internal/query/filters_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 删除旧的数据变更日志(用于定时清理)
|
||||||
|
|||||||
@@ -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的失败登录记录(用于风控)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 删除旧的操作日志(用于定时清理)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
179
internal/service/admin_group_service_test.go
Normal file
179
internal/service/admin_group_service_test.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"with_you/internal/model"
|
||||||
|
"with_you/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stubGroupRepo 通过嵌入 repository.GroupRepository 接口(保持 nil),
|
||||||
|
// 仅覆盖测试涉及的方法。未覆盖的方法被调用时会 panic——这能在测试中
|
||||||
|
// 提前暴露「Service 调用了非预期 repo 方法」的回归。
|
||||||
|
type stubGroupRepo struct {
|
||||||
|
repository.GroupRepository // nil 嵌入,未覆盖方法 panic
|
||||||
|
|
||||||
|
groupByID map[string]*model.Group
|
||||||
|
groupByIDErr error
|
||||||
|
memberByUser map[string]*model.GroupMember
|
||||||
|
memberErr error
|
||||||
|
transferCalls []transferCall
|
||||||
|
transferErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
type transferCall struct {
|
||||||
|
GroupID, OldOwner, NewOwner string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubGroupRepo) GetByID(id string) (*model.Group, error) {
|
||||||
|
if s.groupByIDErr != nil {
|
||||||
|
return nil, s.groupByIDErr
|
||||||
|
}
|
||||||
|
g, ok := s.groupByID[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubGroupRepo) GetMember(groupID, userID string) (*model.GroupMember, error) {
|
||||||
|
if s.memberErr != nil {
|
||||||
|
return nil, s.memberErr
|
||||||
|
}
|
||||||
|
m, ok := s.memberByUser[userID]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("not member")
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubGroupRepo) TransferOwner(groupID, oldOwnerID, newOwnerID string) error {
|
||||||
|
s.transferCalls = append(s.transferCalls, transferCall{
|
||||||
|
GroupID: groupID, OldOwner: oldOwnerID, NewOwner: newOwnerID,
|
||||||
|
})
|
||||||
|
// 模拟真实 DB 语义:事务提交后 owner_id 已变更,后续 GetByID 读到新值
|
||||||
|
if g, ok := s.groupByID[groupID]; ok {
|
||||||
|
g.OwnerID = newOwnerID
|
||||||
|
}
|
||||||
|
return s.transferErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// stubUserRepo 同样仅覆盖 GetByID。
|
||||||
|
type stubUserRepo struct {
|
||||||
|
repository.UserRepository // nil 嵌入
|
||||||
|
|
||||||
|
byID map[string]*model.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubUserRepo) GetByID(id string) (*model.User, error) {
|
||||||
|
u, ok := s.byID[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTransferOwner_Success 正常转让:TransferOwner 应被调用一次,
|
||||||
|
// 参数为 (groupID, 当前 owner, 新 owner)。
|
||||||
|
func TestTransferOwner_Success(t *testing.T) {
|
||||||
|
repo := &stubGroupRepo{
|
||||||
|
groupByID: map[string]*model.Group{
|
||||||
|
"g1": {ID: "g1", OwnerID: "old-owner", Name: "测试群"},
|
||||||
|
},
|
||||||
|
memberByUser: map[string]*model.GroupMember{
|
||||||
|
"old-owner": {UserID: "old-owner", GroupID: "g1", Role: model.GroupRoleOwner},
|
||||||
|
"new-owner": {UserID: "new-owner", GroupID: "g1", Role: model.GroupRoleMember},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
userRepo := &stubUserRepo{
|
||||||
|
byID: map[string]*model.User{
|
||||||
|
"new-owner": {ID: "new-owner", Username: "new", Nickname: "新群主"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := NewAdminGroupService(repo, userRepo)
|
||||||
|
|
||||||
|
resp, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TransferOwner failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 TransferOwner 恰好被调用一次,参数正确
|
||||||
|
if len(repo.transferCalls) != 1 {
|
||||||
|
t.Fatalf("expected 1 TransferOwner call, got %d", len(repo.transferCalls))
|
||||||
|
}
|
||||||
|
tc := repo.transferCalls[0]
|
||||||
|
if tc.GroupID != "g1" || tc.OldOwner != "old-owner" || tc.NewOwner != "new-owner" {
|
||||||
|
t.Errorf("TransferOwner called with (%s,%s,%s), want (g1,old-owner,new-owner)",
|
||||||
|
tc.GroupID, tc.OldOwner, tc.NewOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应应反映新群主
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
if resp.OwnerID != "new-owner" {
|
||||||
|
t.Errorf("response OwnerID = %s, want new-owner", resp.OwnerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTransferOwner_NewOwnerNotMember 新群主非成员时应返回错误,
|
||||||
|
// 且 TransferOwner 不应被调用。
|
||||||
|
func TestTransferOwner_NewOwnerNotMember(t *testing.T) {
|
||||||
|
repo := &stubGroupRepo{
|
||||||
|
groupByID: map[string]*model.Group{
|
||||||
|
"g1": {ID: "g1", OwnerID: "old-owner"},
|
||||||
|
},
|
||||||
|
memberByUser: map[string]*model.GroupMember{
|
||||||
|
"old-owner": {UserID: "old-owner", GroupID: "g1", Role: model.GroupRoleOwner},
|
||||||
|
// new-owner 不在成员表
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := NewAdminGroupService(repo, &stubUserRepo{})
|
||||||
|
|
||||||
|
_, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when new owner is not a member")
|
||||||
|
}
|
||||||
|
if len(repo.transferCalls) != 0 {
|
||||||
|
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTransferOwner_GroupNotFound 群不存在时应返回错误。
|
||||||
|
func TestTransferOwner_GroupNotFound(t *testing.T) {
|
||||||
|
repo := &stubGroupRepo{
|
||||||
|
groupByID: map[string]*model.Group{}, // g1 不存在
|
||||||
|
}
|
||||||
|
svc := NewAdminGroupService(repo, &stubUserRepo{})
|
||||||
|
|
||||||
|
_, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when group does not exist")
|
||||||
|
}
|
||||||
|
if len(repo.transferCalls) != 0 {
|
||||||
|
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTransferOwner_AlreadyOwner 新群主已是群主时应拒绝(无操作幂等)。
|
||||||
|
func TestTransferOwner_AlreadyOwner(t *testing.T) {
|
||||||
|
repo := &stubGroupRepo{
|
||||||
|
groupByID: map[string]*model.Group{
|
||||||
|
"g1": {ID: "g1", OwnerID: "same-user"},
|
||||||
|
},
|
||||||
|
memberByUser: map[string]*model.GroupMember{
|
||||||
|
"same-user": {UserID: "same-user", GroupID: "g1", Role: model.GroupRoleOwner},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := NewAdminGroupService(repo, &stubUserRepo{})
|
||||||
|
|
||||||
|
_, err := svc.TransferOwner(context.Background(), "g1", "same-user")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when transferring to existing owner")
|
||||||
|
}
|
||||||
|
if len(repo.transferCalls) != 0 {
|
||||||
|
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"with_you/internal/dto"
|
"with_you/internal/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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -196,4 +196,3 @@ func NewLogService(
|
|||||||
DataChangeLog: dataChangeLogService,
|
DataChangeLog: dataChangeLogService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ¬ificationService{
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,4 +498,3 @@ func (s *sensitiveServiceImpl) loadFromRedis(ctx context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
// 计算目标尺寸
|
// 计算目标尺寸
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
91
internal/wire/infrastructure_test.go
Normal file
91
internal/wire/infrastructure_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 提供用户活跃数据仓储
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user