feat: 添加日志管理和敏感词过滤功能
- 新增日志管理模块:操作日志、登录日志、数据变更日志 - 新增敏感词过滤器 (sanitizer) - 新增日志异步写入管理器 - 新增日志定时清理服务 - 优化帖子相关服务和上传服务
This commit is contained in:
@@ -27,22 +27,32 @@ type AdminPostService interface {
|
||||
SetPostPin(ctx context.Context, postID string, isPinned bool) (*dto.AdminPostDetailResponse, error)
|
||||
// SetPostFeature 加精/取消加精帖子
|
||||
SetPostFeature(ctx context.Context, postID string, isFeatured bool) (*dto.AdminPostDetailResponse, error)
|
||||
|
||||
// 日志服务设置
|
||||
SetLogService(logService *LogService)
|
||||
}
|
||||
|
||||
// adminPostServiceImpl 管理端帖子服务实现
|
||||
type adminPostServiceImpl struct {
|
||||
postRepo *repository.PostRepository
|
||||
cache cache.Cache
|
||||
postRepo *repository.PostRepository
|
||||
cache cache.Cache
|
||||
logService *LogService
|
||||
}
|
||||
|
||||
// NewAdminPostService 创建管理端帖子服务
|
||||
func NewAdminPostService(postRepo *repository.PostRepository, cacheBackend cache.Cache) AdminPostService {
|
||||
return &adminPostServiceImpl{
|
||||
postRepo: postRepo,
|
||||
cache: cacheBackend,
|
||||
postRepo: postRepo,
|
||||
cache: cacheBackend,
|
||||
logService: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// SetLogService 设置日志服务
|
||||
func (s *adminPostServiceImpl) SetLogService(logService *LogService) {
|
||||
s.logService = logService
|
||||
}
|
||||
|
||||
// GetPostList 获取帖子列表
|
||||
func (s *adminPostServiceImpl) GetPostList(ctx context.Context, page, pageSize int, keyword, status, authorID, startDate, endDate string) ([]dto.AdminPostListResponse, int64, error) {
|
||||
query := repository.AdminPostListQuery{
|
||||
@@ -80,7 +90,12 @@ func (s *adminPostServiceImpl) GetPostDetail(ctx context.Context, postID string)
|
||||
|
||||
// DeletePost 删除帖子
|
||||
func (s *adminPostServiceImpl) DeletePost(ctx context.Context, postID string) error {
|
||||
err := s.postRepo.Delete(postID)
|
||||
post, err := s.postRepo.GetByID(postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.postRepo.Delete(postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -89,6 +104,18 @@ func (s *adminPostServiceImpl) DeletePost(ctx context.Context, postID string) er
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
// 记录操作日志
|
||||
if s.logService != nil {
|
||||
s.logService.OperationLog.RecordOperation(&model.OperationLog{
|
||||
UserID: post.UserID,
|
||||
Operation: string(model.OpAdminDeletePost),
|
||||
Module: "admin",
|
||||
TargetType: "post",
|
||||
TargetID: postID,
|
||||
Status: "success",
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
304
internal/service/async_log_manager.go
Normal file
304
internal/service/async_log_manager.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AsyncLogConfig 异步日志配置
|
||||
type AsyncLogConfig struct {
|
||||
BufferSize int
|
||||
BatchSize int
|
||||
FlushInterval time.Duration
|
||||
WorkerCount int
|
||||
EnableFallback bool
|
||||
FallbackPath string
|
||||
}
|
||||
|
||||
// AsyncLogManager 异步日志管理器
|
||||
type AsyncLogManager struct {
|
||||
operationChan chan *model.OperationLog
|
||||
loginChan chan *model.LoginLog
|
||||
dataChangeChan chan *model.DataChangeLog
|
||||
|
||||
cfg *AsyncLogConfig
|
||||
db *gorm.DB
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger *zap.Logger
|
||||
|
||||
fallbackWriter *os.File
|
||||
}
|
||||
|
||||
// NewAsyncLogManager 创建异步日志管理器
|
||||
func NewAsyncLogManager(
|
||||
db *gorm.DB,
|
||||
operationRepo *repository.OperationLogRepository,
|
||||
loginRepo *repository.LoginLogRepository,
|
||||
dataChangeRepo *repository.DataChangeLogRepository,
|
||||
logger *zap.Logger,
|
||||
cfg *AsyncLogConfig,
|
||||
) *AsyncLogManager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
m := &AsyncLogManager{
|
||||
operationChan: make(chan *model.OperationLog, cfg.BufferSize),
|
||||
loginChan: make(chan *model.LoginLog, cfg.BufferSize),
|
||||
dataChangeChan: make(chan *model.DataChangeLog, cfg.BufferSize),
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if cfg.EnableFallback {
|
||||
if err := m.initFallbackWriter(); err != nil {
|
||||
logger.Warn("failed to init fallback writer", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < cfg.WorkerCount; i++ {
|
||||
m.wg.Add(1)
|
||||
go m.worker(i, operationRepo, loginRepo, dataChangeRepo)
|
||||
}
|
||||
|
||||
go m.flushScheduler()
|
||||
|
||||
logger.Info("async log manager started",
|
||||
zap.Int("buffer_size", cfg.BufferSize),
|
||||
zap.Int("batch_size", cfg.BatchSize),
|
||||
zap.Duration("flush_interval", cfg.FlushInterval),
|
||||
zap.Int("worker_count", cfg.WorkerCount),
|
||||
)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// worker 处理协程
|
||||
func (m *AsyncLogManager) worker(
|
||||
id int,
|
||||
operationRepo *repository.OperationLogRepository,
|
||||
loginRepo *repository.LoginLogRepository,
|
||||
dataChangeRepo *repository.DataChangeLogRepository,
|
||||
) {
|
||||
defer m.wg.Done()
|
||||
|
||||
opBatch := make([]*model.OperationLog, 0, m.cfg.BatchSize)
|
||||
loginBatch := make([]*model.LoginLog, 0, m.cfg.BatchSize)
|
||||
changeBatch := make([]*model.DataChangeLog, 0, m.cfg.BatchSize)
|
||||
|
||||
ticker := time.NewTicker(m.cfg.FlushInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
m.flushBatches(opBatch, loginBatch, changeBatch, operationRepo, loginRepo, dataChangeRepo)
|
||||
return
|
||||
|
||||
case op := <-m.operationChan:
|
||||
opBatch = append(opBatch, op)
|
||||
if len(opBatch) >= m.cfg.BatchSize {
|
||||
if err := m.flushOperationLogs(opBatch, operationRepo); err != nil {
|
||||
m.handleError("operation_logs", opBatch, err)
|
||||
}
|
||||
opBatch = opBatch[:0]
|
||||
}
|
||||
|
||||
case login := <-m.loginChan:
|
||||
loginBatch = append(loginBatch, login)
|
||||
if len(loginBatch) >= m.cfg.BatchSize {
|
||||
if err := m.flushLoginLogs(loginBatch, loginRepo); err != nil {
|
||||
m.handleError("login_logs", loginBatch, err)
|
||||
}
|
||||
loginBatch = loginBatch[:0]
|
||||
}
|
||||
|
||||
case change := <-m.dataChangeChan:
|
||||
changeBatch = append(changeBatch, change)
|
||||
if len(changeBatch) >= m.cfg.BatchSize {
|
||||
if err := m.flushDataChangeLogs(changeBatch, dataChangeRepo); err != nil {
|
||||
m.handleError("data_change_logs", changeBatch, err)
|
||||
}
|
||||
changeBatch = changeBatch[:0]
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
if len(opBatch) > 0 || len(loginBatch) > 0 || len(changeBatch) > 0 {
|
||||
m.flushBatches(opBatch, loginBatch, changeBatch, operationRepo, loginRepo, dataChangeRepo)
|
||||
opBatch = opBatch[:0]
|
||||
loginBatch = loginBatch[:0]
|
||||
changeBatch = changeBatch[:0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushScheduler 定时刷新
|
||||
func (m *AsyncLogManager) flushScheduler() {
|
||||
ticker := time.NewTicker(m.cfg.FlushInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushBatches 批量写入
|
||||
func (m *AsyncLogManager) flushBatches(
|
||||
ops []*model.OperationLog,
|
||||
logins []*model.LoginLog,
|
||||
changes []*model.DataChangeLog,
|
||||
operationRepo *repository.OperationLogRepository,
|
||||
loginRepo *repository.LoginLogRepository,
|
||||
dataChangeRepo *repository.DataChangeLogRepository,
|
||||
) {
|
||||
if len(ops) == 0 && len(logins) == 0 && len(changes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if len(ops) > 0 {
|
||||
if err := operationRepo.BatchCreateOperationLogs(ctx, ops); err != nil {
|
||||
m.handleError("operation_logs", ops, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(logins) > 0 {
|
||||
if err := loginRepo.BatchCreateLoginLogs(ctx, logins); err != nil {
|
||||
m.handleError("login_logs", logins, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) > 0 {
|
||||
if err := dataChangeRepo.BatchCreateDataChangeLogs(ctx, changes); err != nil {
|
||||
m.handleError("data_change_logs", changes, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushOperationLogs 刷新操作日志
|
||||
func (m *AsyncLogManager) flushOperationLogs(logs []*model.OperationLog, repo *repository.OperationLogRepository) error {
|
||||
return repo.BatchCreateOperationLogs(context.Background(), logs)
|
||||
}
|
||||
|
||||
// flushLoginLogs 刷新登录日志
|
||||
func (m *AsyncLogManager) flushLoginLogs(logs []*model.LoginLog, repo *repository.LoginLogRepository) error {
|
||||
return repo.BatchCreateLoginLogs(context.Background(), logs)
|
||||
}
|
||||
|
||||
// flushDataChangeLogs 刷新数据变更日志
|
||||
func (m *AsyncLogManager) flushDataChangeLogs(logs []*model.DataChangeLog, repo *repository.DataChangeLogRepository) error {
|
||||
return repo.BatchCreateDataChangeLogs(context.Background(), logs)
|
||||
}
|
||||
|
||||
// handleError 处理写入错误
|
||||
func (m *AsyncLogManager) handleError(logType string, logs interface{}, err error) {
|
||||
m.logger.Error("failed to batch write logs",
|
||||
zap.String("log_type", logType),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
if m.fallbackWriter != nil {
|
||||
if err := m.writeToFallback(logType, logs); err != nil {
|
||||
m.logger.Error("failed to write to fallback", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeToFallback 写入降级文件
|
||||
func (m *AsyncLogManager) writeToFallback(logType string, logs interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initFallbackWriter 初始化降级写入器
|
||||
func (m *AsyncLogManager) initFallbackWriter() error {
|
||||
if m.cfg.FallbackPath == "" {
|
||||
m.cfg.FallbackPath = "./logs/fallback.log"
|
||||
}
|
||||
|
||||
dir := "./logs"
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(m.cfg.FallbackPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.fallbackWriter = file
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordOperation 记录操作日志(非阻塞)
|
||||
func (m *AsyncLogManager) RecordOperation(log *model.OperationLog) {
|
||||
select {
|
||||
case m.operationChan <- log:
|
||||
default:
|
||||
m.logger.Warn("operation log channel full, dropping log",
|
||||
zap.String("user_id", log.UserID),
|
||||
zap.String("operation", log.Operation),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordLogin 记录登录日志(非阻塞)
|
||||
func (m *AsyncLogManager) RecordLogin(log *model.LoginLog) {
|
||||
select {
|
||||
case m.loginChan <- log:
|
||||
default:
|
||||
m.logger.Warn("login log channel full, dropping log",
|
||||
zap.String("user_id", log.UserID),
|
||||
zap.String("event", log.Event),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordDataChange 记录数据变更日志(非阻塞)
|
||||
func (m *AsyncLogManager) RecordDataChange(log *model.DataChangeLog) {
|
||||
select {
|
||||
case m.dataChangeChan <- log:
|
||||
default:
|
||||
m.logger.Warn("data change log channel full, dropping log",
|
||||
zap.String("user_id", log.UserID),
|
||||
zap.String("change_type", log.ChangeType),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止异步日志管理器
|
||||
func (m *AsyncLogManager) Stop() {
|
||||
m.cancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
m.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
m.logger.Info("async log manager stopped gracefully")
|
||||
case <-time.After(10 * time.Second):
|
||||
m.logger.Warn("async log manager stop timeout, force closing")
|
||||
}
|
||||
|
||||
if m.fallbackWriter != nil {
|
||||
m.fallbackWriter.Close()
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type CommentService struct {
|
||||
cache cache.Cache
|
||||
gorseClient gorse.Client
|
||||
postAIService *PostAIService
|
||||
logService *LogService
|
||||
}
|
||||
|
||||
// NewCommentService 创建评论服务
|
||||
@@ -32,9 +33,15 @@ func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repo
|
||||
cache: cacheBackend,
|
||||
gorseClient: gorseClient,
|
||||
postAIService: postAIService,
|
||||
logService: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// SetLogService 设置日志服务
|
||||
func (s *CommentService) SetLogService(logService *LogService) {
|
||||
s.logService = logService
|
||||
}
|
||||
|
||||
// Create 创建评论
|
||||
func (s *CommentService) Create(ctx context.Context, postID, userID, content string, parentID *string, images string, imageURLs []string) (*model.Comment, error) {
|
||||
if s.postAIService != nil {
|
||||
@@ -75,6 +82,18 @@ func (s *CommentService) Create(ctx context.Context, postID, userID, content str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
if s.logService != nil {
|
||||
s.logService.OperationLog.RecordOperation(&model.OperationLog{
|
||||
UserID: userID,
|
||||
Operation: string(model.OpCommentCreate),
|
||||
Module: "comment",
|
||||
TargetType: "comment",
|
||||
TargetID: comment.ID,
|
||||
Status: "success",
|
||||
})
|
||||
}
|
||||
|
||||
// 重新查询以获取关联的 User
|
||||
comment, err = s.commentRepo.GetByID(comment.ID)
|
||||
if err != nil {
|
||||
|
||||
205
internal/service/log_cleanup_service.go
Normal file
205
internal/service/log_cleanup_service.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// LogCleanupConfig 日志清理配置
|
||||
type LogCleanupConfig struct {
|
||||
Enabled bool
|
||||
OperationLog int // 保留天数
|
||||
LoginLog int
|
||||
DataChangeLog int
|
||||
CleanupTime string // 清理时间,如 "02:00"
|
||||
}
|
||||
|
||||
// LogCleanupService 日志清理服务接口
|
||||
type LogCleanupService interface {
|
||||
Start()
|
||||
Stop()
|
||||
CleanupOldLogs(ctx context.Context) error
|
||||
CleanupOperationLogs(ctx context.Context, beforeDate string) error
|
||||
CleanupLoginLogs(ctx context.Context, beforeDate string) error
|
||||
CleanupDataChangeLogs(ctx context.Context, beforeDate string) error
|
||||
}
|
||||
|
||||
// logCleanupServiceImpl 日志清理服务实现
|
||||
type logCleanupServiceImpl struct {
|
||||
operationRepo *repository.OperationLogRepository
|
||||
loginRepo *repository.LoginLogRepository
|
||||
dataChangeRepo *repository.DataChangeLogRepository
|
||||
cfg *LogCleanupConfig
|
||||
logger *zap.Logger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewLogCleanupService 创建日志清理服务
|
||||
func NewLogCleanupService(
|
||||
operationRepo *repository.OperationLogRepository,
|
||||
loginRepo *repository.LoginLogRepository,
|
||||
dataChangeRepo *repository.DataChangeLogRepository,
|
||||
cfg *LogCleanupConfig,
|
||||
logger *zap.Logger,
|
||||
) LogCleanupService {
|
||||
return &logCleanupServiceImpl{
|
||||
operationRepo: operationRepo,
|
||||
loginRepo: loginRepo,
|
||||
dataChangeRepo: dataChangeRepo,
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动日志清理服务
|
||||
func (s *logCleanupServiceImpl) Start() {
|
||||
if !s.cfg.Enabled {
|
||||
s.logger.Info("log cleanup service disabled")
|
||||
return
|
||||
}
|
||||
|
||||
s.ctx, s.cancel = context.WithCancel(context.Background())
|
||||
|
||||
go s.scheduleCleanup()
|
||||
|
||||
s.logger.Info("log cleanup service started",
|
||||
zap.Int("operation_log_days", s.cfg.OperationLog),
|
||||
zap.Int("login_log_days", s.cfg.LoginLog),
|
||||
zap.Int("data_change_log_days", s.cfg.DataChangeLog),
|
||||
)
|
||||
}
|
||||
|
||||
// Stop 停止日志清理服务
|
||||
func (s *logCleanupServiceImpl) Stop() {
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
s.logger.Info("log cleanup service stopped")
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleCleanup 定时调度清理任务
|
||||
func (s *logCleanupServiceImpl) scheduleCleanup() {
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
now := time.Now()
|
||||
nextCleanup := s.getNextCleanupTime(now)
|
||||
duration := nextCleanup.Sub(now)
|
||||
|
||||
s.logger.Info("next log cleanup scheduled",
|
||||
zap.Time("time", nextCleanup),
|
||||
zap.Duration("in", duration),
|
||||
)
|
||||
|
||||
timer := time.NewTimer(duration)
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
if err := s.CleanupOldLogs(s.ctx); err != nil {
|
||||
s.logger.Error("failed to cleanup old logs", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getNextCleanupTime 计算下次清理时间
|
||||
func (s *logCleanupServiceImpl) getNextCleanupTime(now time.Time) time.Time {
|
||||
cleanupTime, err := time.Parse("15:04", s.cfg.CleanupTime)
|
||||
if err != nil {
|
||||
cleanupTime = time.Date(now.Year(), now.Month(), now.Day(), 2, 0, 0, 0, now.Location())
|
||||
}
|
||||
|
||||
nextCleanup := time.Date(now.Year(), now.Month(), now.Day(), cleanupTime.Hour(), cleanupTime.Minute(), 0, 0, now.Location())
|
||||
|
||||
if nextCleanup.Before(now) {
|
||||
nextCleanup = nextCleanup.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
return nextCleanup
|
||||
}
|
||||
|
||||
// CleanupOldLogs 清理旧日志
|
||||
func (s *logCleanupServiceImpl) CleanupOldLogs(ctx context.Context) error {
|
||||
s.logger.Info("starting log cleanup")
|
||||
|
||||
// 清理操作日志
|
||||
if err := s.CleanupOperationLogs(ctx, s.getBeforeDate(s.cfg.OperationLog)); err != nil {
|
||||
s.logger.Error("failed to cleanup operation logs", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理登录日志
|
||||
if err := s.CleanupLoginLogs(ctx, s.getBeforeDate(s.cfg.LoginLog)); err != nil {
|
||||
s.logger.Error("failed to cleanup login logs", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理数据变更日志
|
||||
if err := s.CleanupDataChangeLogs(ctx, s.getBeforeDate(s.cfg.DataChangeLog)); err != nil {
|
||||
s.logger.Error("failed to cleanup data change logs", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("log cleanup completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupOperationLogs 清理操作日志
|
||||
func (s *logCleanupServiceImpl) CleanupOperationLogs(ctx context.Context, beforeDate string) error {
|
||||
result := s.operationRepo.DeleteOldLogs(ctx, beforeDate)
|
||||
s.logger.Info("cleanup operation logs", zap.String("before_date", beforeDate))
|
||||
return result
|
||||
}
|
||||
|
||||
// CleanupLoginLogs 清理登录日志
|
||||
func (s *logCleanupServiceImpl) CleanupLoginLogs(ctx context.Context, beforeDate string) error {
|
||||
result := s.loginRepo.DeleteOldLogs(ctx, beforeDate)
|
||||
s.logger.Info("cleanup login logs", zap.String("before_date", beforeDate))
|
||||
return result
|
||||
}
|
||||
|
||||
// CleanupDataChangeLogs 清理数据变更日志
|
||||
func (s *logCleanupServiceImpl) CleanupDataChangeLogs(ctx context.Context, beforeDate string) error {
|
||||
result := s.dataChangeRepo.DeleteOldLogs(ctx, beforeDate)
|
||||
s.logger.Info("cleanup data change logs", zap.String("before_date", beforeDate))
|
||||
return result
|
||||
}
|
||||
|
||||
// getBeforeDate 计算截止日期
|
||||
func (s *logCleanupServiceImpl) getBeforeDate(days int) string {
|
||||
return time.Now().AddDate(0, 0, -days).Format("2006-01-02")
|
||||
}
|
||||
|
||||
// LogService 日志服务组合
|
||||
type LogService struct {
|
||||
OperationLog OperationLogService
|
||||
LoginLog LoginLogService
|
||||
DataChangeLog DataChangeLogService
|
||||
}
|
||||
|
||||
// NewLogService 创建日志服务组合
|
||||
func NewLogService(
|
||||
operationLogService OperationLogService,
|
||||
loginLogService LoginLogService,
|
||||
dataChangeLogService DataChangeLogService,
|
||||
) *LogService {
|
||||
return &LogService{
|
||||
OperationLog: operationLogService,
|
||||
LoginLog: loginLogService,
|
||||
DataChangeLog: dataChangeLogService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogCleanupServiceFromApp 从应用中获取日志清理服务
|
||||
func GetLogCleanupServiceFromApp(app interface{}) LogCleanupService {
|
||||
// 这里需要根据实际的app结构来获取
|
||||
// 暂时返回nil,实际使用时需要实现
|
||||
return nil
|
||||
}
|
||||
172
internal/service/log_service.go
Normal file
172
internal/service/log_service.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
)
|
||||
|
||||
// OperationLogService 操作日志服务接口
|
||||
type OperationLogService interface {
|
||||
RecordOperation(log *model.OperationLog)
|
||||
GetOperationLogs(ctx context.Context, filters repository.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error)
|
||||
GetOperationLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.OperationLog, int64, error)
|
||||
GetOperationLogsByTimeRange(ctx context.Context, startTime, endTime string, page, pageSize int) ([]*model.OperationLog, int64, error)
|
||||
GetStatistics(ctx context.Context, startTime, endTime string) (*repository.OperationLogStatistics, error)
|
||||
}
|
||||
|
||||
// operationLogServiceImpl 操作日志服务实现
|
||||
type operationLogServiceImpl struct {
|
||||
asyncManager *AsyncLogManager
|
||||
repo *repository.OperationLogRepository
|
||||
}
|
||||
|
||||
// NewOperationLogService 创建操作日志服务
|
||||
func NewOperationLogService(
|
||||
asyncManager *AsyncLogManager,
|
||||
repo *repository.OperationLogRepository,
|
||||
) OperationLogService {
|
||||
return &operationLogServiceImpl{
|
||||
asyncManager: asyncManager,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// RecordOperation 记录操作日志
|
||||
func (s *operationLogServiceImpl) RecordOperation(log *model.OperationLog) {
|
||||
s.asyncManager.RecordOperation(log)
|
||||
}
|
||||
|
||||
// GetOperationLogs 获取操作日志列表
|
||||
func (s *operationLogServiceImpl) GetOperationLogs(ctx context.Context, filters repository.LogFilter, page, pageSize int) ([]*model.OperationLog, int64, error) {
|
||||
return s.repo.GetOperationLogs(ctx, filters, page, pageSize)
|
||||
}
|
||||
|
||||
// GetOperationLogsByUser 获取指定用户的操作日志
|
||||
func (s *operationLogServiceImpl) GetOperationLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.OperationLog, int64, error) {
|
||||
return s.repo.GetOperationLogsByUser(ctx, userID, page, pageSize)
|
||||
}
|
||||
|
||||
// GetOperationLogsByTimeRange 按时间范围获取操作日志
|
||||
func (s *operationLogServiceImpl) GetOperationLogsByTimeRange(ctx context.Context, startTime, endTime string, page, pageSize int) ([]*model.OperationLog, int64, error) {
|
||||
return s.repo.GetOperationLogsByTimeRange(ctx, startTime, endTime, page, pageSize)
|
||||
}
|
||||
|
||||
// GetStatistics 获取操作日志统计
|
||||
func (s *operationLogServiceImpl) GetStatistics(ctx context.Context, startTime, endTime string) (*repository.OperationLogStatistics, error) {
|
||||
return s.repo.GetStatistics(ctx, startTime, endTime)
|
||||
}
|
||||
|
||||
// LoginLogService 登录日志服务接口
|
||||
type LoginLogService interface {
|
||||
RecordLogin(log *model.LoginLog)
|
||||
GetLoginLogs(ctx context.Context, filters repository.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error)
|
||||
GetLoginLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.LoginLog, int64, error)
|
||||
GetFailedLoginsByIP(ctx context.Context, ip string, timeWindow string) (int64, error)
|
||||
GetRecentSuccessLogins(ctx context.Context, userID string, limit int) ([]*model.LoginLog, error)
|
||||
GetStatistics(ctx context.Context, startTime, endTime string) (*repository.LoginLogStatistics, error)
|
||||
}
|
||||
|
||||
// loginLogServiceImpl 登录日志服务实现
|
||||
type loginLogServiceImpl struct {
|
||||
asyncManager *AsyncLogManager
|
||||
repo *repository.LoginLogRepository
|
||||
}
|
||||
|
||||
// NewLoginLogService 创建登录日志服务
|
||||
func NewLoginLogService(
|
||||
asyncManager *AsyncLogManager,
|
||||
repo *repository.LoginLogRepository,
|
||||
) LoginLogService {
|
||||
return &loginLogServiceImpl{
|
||||
asyncManager: asyncManager,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// RecordLogin 记录登录日志
|
||||
func (s *loginLogServiceImpl) RecordLogin(log *model.LoginLog) {
|
||||
s.asyncManager.RecordLogin(log)
|
||||
}
|
||||
|
||||
// GetLoginLogs 获取登录日志列表
|
||||
func (s *loginLogServiceImpl) GetLoginLogs(ctx context.Context, filters repository.LoginFilter, page, pageSize int) ([]*model.LoginLog, int64, error) {
|
||||
return s.repo.GetLoginLogs(ctx, filters, page, pageSize)
|
||||
}
|
||||
|
||||
// GetLoginLogsByUser 获取指定用户的登录日志
|
||||
func (s *loginLogServiceImpl) GetLoginLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.LoginLog, int64, error) {
|
||||
return s.repo.GetLoginLogsByUser(ctx, userID, page, pageSize)
|
||||
}
|
||||
|
||||
// GetFailedLoginsByIP 获取指定IP的失败登录记录
|
||||
func (s *loginLogServiceImpl) GetFailedLoginsByIP(ctx context.Context, ip string, timeWindow string) (int64, error) {
|
||||
duration, err := time.ParseDuration(timeWindow)
|
||||
if err != nil {
|
||||
duration = time.Hour // 默认1小时
|
||||
}
|
||||
return s.repo.GetFailedLoginsByIP(ctx, ip, duration)
|
||||
}
|
||||
|
||||
// GetRecentSuccessLogins 获取最近成功的登录记录
|
||||
func (s *loginLogServiceImpl) GetRecentSuccessLogins(ctx context.Context, userID string, limit int) ([]*model.LoginLog, error) {
|
||||
return s.repo.GetRecentSuccessLogins(ctx, userID, limit)
|
||||
}
|
||||
|
||||
// GetStatistics 获取登录日志统计
|
||||
func (s *loginLogServiceImpl) GetStatistics(ctx context.Context, startTime, endTime string) (*repository.LoginLogStatistics, error) {
|
||||
return s.repo.GetStatistics(ctx, startTime, endTime)
|
||||
}
|
||||
|
||||
// DataChangeLogService 数据变更日志服务接口
|
||||
type DataChangeLogService interface {
|
||||
RecordDataChange(log *model.DataChangeLog)
|
||||
GetDataChangeLogs(ctx context.Context, filters repository.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error)
|
||||
GetDataChangeLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.DataChangeLog, int64, error)
|
||||
GetDataChangeLogsByOperator(ctx context.Context, operatorID string, page, pageSize int) ([]*model.DataChangeLog, int64, error)
|
||||
GetStatistics(ctx context.Context, startTime, endTime string) (*repository.DataChangeLogStatistics, error)
|
||||
}
|
||||
|
||||
// dataChangeLogServiceImpl 数据变更日志服务实现
|
||||
type dataChangeLogServiceImpl struct {
|
||||
asyncManager *AsyncLogManager
|
||||
repo *repository.DataChangeLogRepository
|
||||
}
|
||||
|
||||
// NewDataChangeLogService 创建数据变更日志服务
|
||||
func NewDataChangeLogService(
|
||||
asyncManager *AsyncLogManager,
|
||||
repo *repository.DataChangeLogRepository,
|
||||
) DataChangeLogService {
|
||||
return &dataChangeLogServiceImpl{
|
||||
asyncManager: asyncManager,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// RecordDataChange 记录数据变更日志
|
||||
func (s *dataChangeLogServiceImpl) RecordDataChange(log *model.DataChangeLog) {
|
||||
s.asyncManager.RecordDataChange(log)
|
||||
}
|
||||
|
||||
// GetDataChangeLogs 获取数据变更日志列表
|
||||
func (s *dataChangeLogServiceImpl) GetDataChangeLogs(ctx context.Context, filters repository.DataChangeFilter, page, pageSize int) ([]*model.DataChangeLog, int64, error) {
|
||||
return s.repo.GetDataChangeLogs(ctx, filters, page, pageSize)
|
||||
}
|
||||
|
||||
// GetDataChangeLogsByUser 获取指定用户的数据变更日志
|
||||
func (s *dataChangeLogServiceImpl) GetDataChangeLogsByUser(ctx context.Context, userID string, page, pageSize int) ([]*model.DataChangeLog, int64, error) {
|
||||
return s.repo.GetDataChangeLogsByUser(ctx, userID, page, pageSize)
|
||||
}
|
||||
|
||||
// GetDataChangeLogsByOperator 获取指定操作人的日志
|
||||
func (s *dataChangeLogServiceImpl) GetDataChangeLogsByOperator(ctx context.Context, operatorID string, page, pageSize int) ([]*model.DataChangeLog, int64, error) {
|
||||
return s.repo.GetDataChangeLogsByOperator(ctx, operatorID, page, pageSize)
|
||||
}
|
||||
|
||||
// GetStatistics 获取数据变更日志统计
|
||||
func (s *dataChangeLogServiceImpl) GetStatistics(ctx context.Context, startTime, endTime string) (*repository.DataChangeLogStatistics, error) {
|
||||
return s.repo.GetStatistics(ctx, startTime, endTime)
|
||||
}
|
||||
@@ -59,6 +59,9 @@ type PostService interface {
|
||||
|
||||
// 其他
|
||||
IncrementViews(ctx context.Context, postID, userID string) error
|
||||
|
||||
// 日志服务设置
|
||||
SetLogService(logService *LogService)
|
||||
}
|
||||
|
||||
// postServiceImpl 帖子服务实现
|
||||
@@ -68,7 +71,8 @@ type postServiceImpl struct {
|
||||
cache cache.Cache
|
||||
gorseClient gorse.Client
|
||||
postAIService *PostAIService
|
||||
txManager repository.TransactionManager // 事务管理器
|
||||
txManager repository.TransactionManager
|
||||
logService *LogService
|
||||
}
|
||||
|
||||
// NewPostService 创建帖子服务
|
||||
@@ -80,9 +84,15 @@ func NewPostService(postRepo *repository.PostRepository, systemMessageService Sy
|
||||
gorseClient: gorseClient,
|
||||
postAIService: postAIService,
|
||||
txManager: txManager,
|
||||
logService: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// SetLogService 设置日志服务
|
||||
func (s *postServiceImpl) SetLogService(logService *LogService) {
|
||||
s.logService = logService
|
||||
}
|
||||
|
||||
// PostListResult 帖子列表缓存结果
|
||||
type PostListResult struct {
|
||||
Posts []*model.Post
|
||||
@@ -103,6 +113,18 @@ func (s *postServiceImpl) Create(ctx context.Context, userID, title, content str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
if s.logService != nil {
|
||||
s.logService.OperationLog.RecordOperation(&model.OperationLog{
|
||||
UserID: userID,
|
||||
Operation: string(model.OpPostCreate),
|
||||
Module: "post",
|
||||
TargetType: "post",
|
||||
TargetID: post.ID,
|
||||
Status: "success",
|
||||
})
|
||||
}
|
||||
|
||||
// 失效帖子列表缓存
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/pkg/s3"
|
||||
"github.com/disintegration/imaging"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
@@ -27,6 +29,13 @@ type UploadService struct {
|
||||
userService UserService
|
||||
}
|
||||
|
||||
// 预览图配置
|
||||
const (
|
||||
PreviewMaxWidth = 300 // 列表/网格模式最大宽度
|
||||
PreviewMaxHeight = 800 // 详情页最大高度
|
||||
PreviewQuality = 70 // WebP 质量
|
||||
)
|
||||
|
||||
// NewUploadService 创建上传服务
|
||||
func NewUploadService(s3Client *s3.Client, userService UserService) *UploadService {
|
||||
return &UploadService{
|
||||
@@ -35,25 +44,31 @@ func NewUploadService(s3Client *s3.Client, userService UserService) *UploadServi
|
||||
}
|
||||
}
|
||||
|
||||
// UploadImage 上传图片
|
||||
func (s *UploadService) UploadImage(ctx context.Context, file *multipart.FileHeader) (string, error) {
|
||||
// UploadImage 上传图片(返回原图URL和预览图URL)
|
||||
func (s *UploadService) UploadImage(ctx context.Context, file *multipart.FileHeader) (string, string, string, error) {
|
||||
processedData, contentType, ext, err := prepareImageForUpload(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
// 压缩后再计算哈希,确保同一压缩结果映射同一对象名
|
||||
hash := sha256.Sum256(processedData)
|
||||
hashStr := fmt.Sprintf("%x", hash)
|
||||
|
||||
// 上传原图
|
||||
objectName := fmt.Sprintf("images/%s%s", hashStr, ext)
|
||||
|
||||
url, err := s.s3Client.UploadData(ctx, objectName, processedData, contentType)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload to S3: %w", err)
|
||||
return "", "", "", fmt.Errorf("failed to upload to S3: %w", err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
// 生成预览图
|
||||
previewURL, previewURLLarge, err := s.GeneratePreviewImages(ctx, processedData, hashStr)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate preview images: %v", err)
|
||||
}
|
||||
|
||||
return url, previewURL, previewURLLarge, nil
|
||||
}
|
||||
|
||||
// getExtFromContentType 根据Content-Type获取文件扩展名
|
||||
@@ -272,3 +287,73 @@ func normalizeImageContentType(contentType string) string {
|
||||
return contentType
|
||||
}
|
||||
}
|
||||
|
||||
// GeneratePreviewImages 生成预览图
|
||||
func (s *UploadService) GeneratePreviewImages(ctx context.Context, originalData []byte, hash string) (previewURL, previewURLLarge string, err error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(originalData))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
originalWidth := bounds.Dx()
|
||||
originalHeight := bounds.Dy()
|
||||
|
||||
// 生成普通预览图(列表/网格模式)
|
||||
previewURL, err = s.generatePreview(ctx, img, originalWidth, originalHeight, "preview", PreviewMaxWidth, hash)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate preview: %v", err)
|
||||
}
|
||||
|
||||
// 生成大预览图(详情页)
|
||||
previewURLLarge, err = s.generatePreview(ctx, img, originalWidth, originalHeight, "large", PreviewMaxHeight, hash)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate large preview: %v", err)
|
||||
}
|
||||
|
||||
return previewURL, previewURLLarge, nil
|
||||
}
|
||||
|
||||
// generatePreview 生成单个预览图
|
||||
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
|
||||
|
||||
// 计算目标尺寸
|
||||
if originalWidth > originalHeight {
|
||||
// 宽图:按宽度缩放
|
||||
if originalWidth > maxDim {
|
||||
targetWidth = maxDim
|
||||
targetHeight = int(float64(originalHeight) * float64(maxDim) / float64(originalWidth))
|
||||
} else {
|
||||
return "", nil // 图片足够小,不需要生成预览图
|
||||
}
|
||||
} else {
|
||||
// 高图:按高度缩放
|
||||
if originalHeight > maxDim {
|
||||
targetHeight = maxDim
|
||||
targetWidth = int(float64(originalWidth) * float64(maxDim) / float64(originalHeight))
|
||||
} else {
|
||||
return "", nil // 图片足够小,不需要生成预览图
|
||||
}
|
||||
}
|
||||
|
||||
// 缩放图片(使用高质量 Lanczos 算法)
|
||||
scaledImg := imaging.Resize(img, targetWidth, targetHeight, imaging.Lanczos)
|
||||
|
||||
// 编码为 JPEG(WebP 在某些设备上可能不支持,使用 JPEG 保证兼容性)
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, scaledImg, &jpeg.Options{Quality: PreviewQuality}); err != nil {
|
||||
return "", fmt.Errorf("failed to encode jpeg: %w", err)
|
||||
}
|
||||
|
||||
// 构建对象名:thumbnails/{hash}_{mode}.jpg
|
||||
objectName := fmt.Sprintf("thumbnails/%s_%s.jpg", hash, mode)
|
||||
|
||||
// 上传到 S3
|
||||
url, err := s.s3Client.UploadData(ctx, objectName, buf.Bytes(), "image/jpeg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload preview: %w", err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ type UserService interface {
|
||||
// 关注相关
|
||||
GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
|
||||
GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
|
||||
GetFollowingList(ctx context.Context, userID string, page, pageSize string) ([]*model.User, error)
|
||||
GetFollowersList(ctx context.Context, userID string, page, pageSize string) ([]*model.User, error)
|
||||
FollowUser(ctx context.Context, followerID, followeeID string) error
|
||||
UnfollowUser(ctx context.Context, followerID, followeeID string) error
|
||||
GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error)
|
||||
@@ -55,6 +55,9 @@ type UserService interface {
|
||||
UnblockUser(ctx context.Context, blockerID, blockedID string) error
|
||||
GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error)
|
||||
|
||||
// 日志服务设置
|
||||
SetLogService(logService *LogService)
|
||||
}
|
||||
|
||||
// userServiceImpl 用户服务实现
|
||||
@@ -62,6 +65,7 @@ type userServiceImpl struct {
|
||||
userRepo *repository.UserRepository
|
||||
systemMessageService SystemMessageService
|
||||
emailCodeService EmailCodeService
|
||||
logService *LogService
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务
|
||||
@@ -75,9 +79,15 @@ func NewUserService(
|
||||
userRepo: userRepo,
|
||||
systemMessageService: systemMessageService,
|
||||
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
|
||||
logService: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// SetLogService 设置日志服务
|
||||
func (s *userServiceImpl) SetLogService(logService *LogService) {
|
||||
s.logService = logService
|
||||
}
|
||||
|
||||
// SendRegisterCode 发送注册验证码
|
||||
func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email string) error {
|
||||
user, err := s.userRepo.GetByEmail(email)
|
||||
@@ -241,12 +251,28 @@ func (s *userServiceImpl) Register(ctx context.Context, username, email, passwor
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录注册日志
|
||||
if s.logService != nil {
|
||||
s.logService.OperationLog.RecordOperation(&model.OperationLog{
|
||||
UserID: user.ID,
|
||||
UserName: user.Username,
|
||||
NickName: user.Nickname,
|
||||
Operation: string(model.OpUserRegister),
|
||||
Module: "user",
|
||||
TargetType: "user",
|
||||
TargetID: user.ID,
|
||||
IP: "",
|
||||
Status: "success",
|
||||
})
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *userServiceImpl) Login(ctx context.Context, account, password string) (*model.User, error) {
|
||||
account = strings.TrimSpace(account)
|
||||
|
||||
var (
|
||||
user *model.User
|
||||
err error
|
||||
@@ -258,18 +284,86 @@ func (s *userServiceImpl) Login(ctx context.Context, account, password string) (
|
||||
} else {
|
||||
user, err = s.userRepo.GetByUsername(account)
|
||||
}
|
||||
|
||||
// 登录失败处理
|
||||
loginResult := string(model.LoginResultSuccess)
|
||||
var failReason string
|
||||
|
||||
if err != nil || user == nil {
|
||||
loginResult = string(model.LoginResultFail)
|
||||
failReason = string(model.FailReasonUserNotFound)
|
||||
|
||||
// 记录失败登录日志
|
||||
if s.logService != nil {
|
||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||||
UserName: account,
|
||||
Event: string(model.LoginEventFail),
|
||||
IP: "",
|
||||
UserAgent: "",
|
||||
Result: loginResult,
|
||||
FailReason: failReason,
|
||||
})
|
||||
}
|
||||
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if !utils.CheckPasswordHash(password, user.PasswordHash) {
|
||||
loginResult = string(model.LoginResultFail)
|
||||
failReason = string(model.FailReasonWrongPassword)
|
||||
|
||||
// 记录失败登录日志
|
||||
if s.logService != nil {
|
||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||||
UserID: user.ID,
|
||||
UserName: user.Username,
|
||||
NickName: user.Nickname,
|
||||
Event: string(model.LoginEventFail),
|
||||
IP: "",
|
||||
UserAgent: "",
|
||||
Result: loginResult,
|
||||
FailReason: failReason,
|
||||
})
|
||||
}
|
||||
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if user.Status != model.UserStatusActive {
|
||||
loginResult = string(model.LoginResultFail)
|
||||
failReason = string(model.FailReasonAccountBanned)
|
||||
|
||||
// 记录失败登录日志
|
||||
if s.logService != nil {
|
||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||||
UserID: user.ID,
|
||||
UserName: user.Username,
|
||||
NickName: user.Nickname,
|
||||
Event: string(model.LoginEventFail),
|
||||
IP: "",
|
||||
UserAgent: "",
|
||||
Result: loginResult,
|
||||
FailReason: failReason,
|
||||
})
|
||||
}
|
||||
|
||||
return nil, ErrUserBanned
|
||||
}
|
||||
|
||||
// 记录成功登录日志
|
||||
if s.logService != nil {
|
||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||||
UserID: user.ID,
|
||||
UserName: user.Username,
|
||||
NickName: user.Nickname,
|
||||
LoginType: string(model.LoginTypePassword),
|
||||
Event: string(model.LoginEventLogin),
|
||||
IP: "",
|
||||
UserAgent: "",
|
||||
Result: string(model.LoginResultSuccess),
|
||||
})
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -337,7 +431,27 @@ func (s *userServiceImpl) GetUserByIDWithMutualFollowStatus(ctx context.Context,
|
||||
|
||||
// UpdateUser 更新用户
|
||||
func (s *userServiceImpl) UpdateUser(ctx context.Context, user *model.User) error {
|
||||
return s.userRepo.Update(user)
|
||||
err := s.userRepo.Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录数据变更日志
|
||||
if s.logService != nil {
|
||||
s.logService.DataChangeLog.RecordDataChange(&model.DataChangeLog{
|
||||
UserID: user.ID,
|
||||
ChangeType: string(model.ChangeProfile),
|
||||
TargetType: "user",
|
||||
TargetID: user.ID,
|
||||
FieldName: "profile",
|
||||
OldValue: "",
|
||||
NewValue: fmt.Sprintf("昵称:%s", user.Nickname),
|
||||
OperatorType: string(model.OperatorTypeSelf),
|
||||
Reason: "用户修改个人资料",
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFollowers 获取粉丝
|
||||
@@ -542,7 +656,26 @@ func (s *userServiceImpl) ChangePassword(ctx context.Context, userID, oldPasswor
|
||||
|
||||
// 更新密码
|
||||
user.PasswordHash = hashedPassword
|
||||
return s.userRepo.Update(user)
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录数据变更日志
|
||||
if s.logService != nil {
|
||||
s.logService.DataChangeLog.RecordDataChange(&model.DataChangeLog{
|
||||
UserID: userID,
|
||||
ChangeType: string(model.ChangePassword),
|
||||
TargetType: "user",
|
||||
TargetID: userID,
|
||||
FieldName: "password",
|
||||
OldValue: "***",
|
||||
NewValue: "***",
|
||||
OperatorType: string(model.OperatorTypeSelf),
|
||||
Reason: "用户主动修改密码",
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPasswordByEmail 通过邮箱重置密码
|
||||
|
||||
Reference in New Issue
Block a user