feat(auth): add Casbin RBAC and user activity tracking
Integrate Casbin for role-based access control with admin routes for role management. Add user activity logging service to track login and refresh events. Refactor audit service to use AI-based content moderation.
This commit is contained in:
97
internal/repository/role_repo.go
Normal file
97
internal/repository/role_repo.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RoleRepository 角色仓储接口
|
||||
type RoleRepository interface {
|
||||
// GetUserRoles 获取用户的所有角色
|
||||
GetUserRoles(ctx context.Context, userID string) ([]string, error)
|
||||
|
||||
// AddRoleForUser 为用户添加角色
|
||||
AddRoleForUser(ctx context.Context, userID, role string) error
|
||||
|
||||
// DeleteRoleForUser 移除用户的角色
|
||||
DeleteRoleForUser(ctx context.Context, userID, role string) error
|
||||
|
||||
// DeleteAllRolesForUser 移除用户的所有角色
|
||||
DeleteAllRolesForUser(ctx context.Context, userID string) error
|
||||
|
||||
// HasRole 检查用户是否拥有指定角色
|
||||
HasRole(ctx context.Context, userID, role string) (bool, error)
|
||||
|
||||
// GetRole 获取角色信息
|
||||
GetRole(ctx context.Context, name string) (*model.Role, error)
|
||||
|
||||
// GetAllRoles 获取所有角色
|
||||
GetAllRoles(ctx context.Context) ([]model.Role, error)
|
||||
}
|
||||
|
||||
type roleRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRoleRepository(db *gorm.DB) RoleRepository {
|
||||
return &roleRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *roleRepository) GetUserRoles(ctx context.Context, userID string) ([]string, error) {
|
||||
var userRoles []model.UserRole
|
||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&userRoles).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles := make([]string, len(userRoles))
|
||||
for i, ur := range userRoles {
|
||||
roles[i] = ur.Role
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) AddRoleForUser(ctx context.Context, userID, role string) error {
|
||||
userRole := model.UserRole{
|
||||
UserID: userID,
|
||||
Role: role,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(&userRole).Error
|
||||
}
|
||||
|
||||
func (r *roleRepository) DeleteRoleForUser(ctx context.Context, userID, role string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("user_id = ? AND role = ?", userID, role).
|
||||
Delete(&model.UserRole{}).Error
|
||||
}
|
||||
|
||||
func (r *roleRepository) DeleteAllRolesForUser(ctx context.Context, userID string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Delete(&model.UserRole{}).Error
|
||||
}
|
||||
|
||||
func (r *roleRepository) HasRole(ctx context.Context, userID, role string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.UserRole{}).
|
||||
Where("user_id = ? AND role = ?", userID, role).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (r *roleRepository) GetRole(ctx context.Context, name string) (*model.Role, error) {
|
||||
var role model.Role
|
||||
err := r.db.WithContext(ctx).Where("name = ?", name).First(&role).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) GetAllRoles(ctx context.Context) ([]model.Role, error) {
|
||||
var roles []model.Role
|
||||
err := r.db.WithContext(ctx).Order("priority DESC").Find(&roles).Error
|
||||
return roles, err
|
||||
}
|
||||
225
internal/repository/user_activity_repo.go
Normal file
225
internal/repository/user_activity_repo.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// Redis Key 前缀常量(遵循项目命名规范)
|
||||
const (
|
||||
// DAUKeyPrefix DAU Key: stats:dau:YYYYMMDD
|
||||
DAUKeyPrefix = "stats:dau:"
|
||||
// WAUKeyPrefix WAU Key: stats:wau:YYYYWW (年-周)
|
||||
WAUKeyPrefix = "stats:wau:"
|
||||
// MAUKeyPrefix MAU Key: stats:mau:YYYYMM
|
||||
MAUKeyPrefix = "stats:mau:"
|
||||
)
|
||||
|
||||
// TTL 常量
|
||||
const (
|
||||
DAUTTL = 45 * 24 * time.Hour // DAU: 45天
|
||||
WAUTTL = 120 * 24 * time.Hour // WAU: 120天
|
||||
MAUTTL = 400 * 24 * time.Hour // MAU: 400天
|
||||
)
|
||||
|
||||
// activityCache 用户活跃缓存接口(避免循环导入)
|
||||
type activityCache interface {
|
||||
ZAdd(ctx context.Context, key string, score float64, member string) error
|
||||
ZCard(ctx context.Context, key string) (int64, error)
|
||||
ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error)
|
||||
Expire(ctx context.Context, key string, ttl time.Duration) error
|
||||
}
|
||||
|
||||
// UserActivityRepository 用户活跃数据仓储
|
||||
type UserActivityRepository struct {
|
||||
db *gorm.DB
|
||||
cache activityCache
|
||||
}
|
||||
|
||||
// NewUserActivityRepository 创建用户活跃数据仓储
|
||||
func NewUserActivityRepository(db *gorm.DB, cache activityCache) *UserActivityRepository {
|
||||
return &UserActivityRepository{db: db, cache: cache}
|
||||
}
|
||||
|
||||
// ==================== 数据库操作方法 ====================
|
||||
|
||||
// RecordActivity 记录用户活跃(写入数据库)
|
||||
func (r *UserActivityRepository) RecordActivity(ctx context.Context, userID, loginType, ip, userAgent string) error {
|
||||
log := &model.UserActiveLog{
|
||||
UserID: userID,
|
||||
ActiveDate: time.Now(),
|
||||
LoginType: loginType,
|
||||
LoginIP: ip,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(log).Error
|
||||
}
|
||||
|
||||
// GetDAU 获取指定日期的日活用户数(从数据库)
|
||||
func (r *UserActivityRepository) GetDAU(ctx context.Context, date time.Time) (int64, error) {
|
||||
var count int64
|
||||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.UserActiveLog{}).
|
||||
Where("active_date >= ? AND active_date < ?", startOfDay, endOfDay).
|
||||
Distinct("user_id").
|
||||
Count(&count).Error
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetActiveUserIDs 获取指定日期的活跃用户ID列表
|
||||
func (r *UserActivityRepository) GetActiveUserIDs(ctx context.Context, date time.Time) ([]string, error) {
|
||||
var userIDs []string
|
||||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.UserActiveLog{}).
|
||||
Select("DISTINCT user_id").
|
||||
Where("active_date >= ? AND active_date < ?", startOfDay, endOfDay).
|
||||
Pluck("user_id", &userIDs).Error
|
||||
|
||||
return userIDs, err
|
||||
}
|
||||
|
||||
// GetUserActivityCount 获取用户在指定日期范围内的活跃天数
|
||||
func (r *UserActivityRepository) GetUserActivityCount(ctx context.Context, userID string, startDate, endDate time.Time) (int64, error) {
|
||||
var count int64
|
||||
startOfDay := time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, startDate.Location())
|
||||
endOfDay := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 0, endDate.Location())
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.UserActiveLog{}).
|
||||
Where("user_id = ? AND active_date >= ? AND active_date <= ?", userID, startOfDay, endOfDay).
|
||||
Distinct("DATE(active_date)").
|
||||
Count(&count).Error
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// SaveStat 保存统计数据快照
|
||||
func (r *UserActivityRepository) SaveStat(ctx context.Context, stat *model.UserActivityStat) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "stat_type"}, {Name: "stat_date"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"active_count", "new_count", "retention_1", "retention_7", "retention_30", "updated_at"}),
|
||||
}).
|
||||
Create(stat).Error
|
||||
}
|
||||
|
||||
// GetStat 获取统计数据快照
|
||||
func (r *UserActivityRepository) GetStat(ctx context.Context, statType string, date time.Time) (*model.UserActivityStat, error) {
|
||||
var stat model.UserActivityStat
|
||||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("stat_type = ? AND stat_date = ?", statType, startOfDay).
|
||||
First(&stat).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stat, nil
|
||||
}
|
||||
|
||||
// ==================== Redis 操作方法 ====================
|
||||
|
||||
// RecordActivityToRedis 记录用户活跃到 Redis Sorted Set
|
||||
// score 为时间戳,member 为用户ID
|
||||
func (r *UserActivityRepository) RecordActivityToRedis(ctx context.Context, userID string) error {
|
||||
now := time.Now()
|
||||
score := float64(now.Unix())
|
||||
|
||||
// 记录到 DAU
|
||||
dauKey := formatDAUKey(now)
|
||||
if err := r.cache.ZAdd(ctx, dauKey, score, userID); err != nil {
|
||||
return fmt.Errorf("failed to record DAU: %w", err)
|
||||
}
|
||||
r.cache.Expire(ctx, dauKey, DAUTTL)
|
||||
|
||||
// 记录到 WAU
|
||||
year, week := now.ISOWeek()
|
||||
wauKey := formatWAUKey(year, week)
|
||||
if err := r.cache.ZAdd(ctx, wauKey, score, userID); err != nil {
|
||||
return fmt.Errorf("failed to record WAU: %w", err)
|
||||
}
|
||||
r.cache.Expire(ctx, wauKey, WAUTTL)
|
||||
|
||||
// 记录到 MAU
|
||||
mauKey := formatMAUKey(now.Year(), int(now.Month()))
|
||||
if err := r.cache.ZAdd(ctx, mauKey, score, userID); err != nil {
|
||||
return fmt.Errorf("failed to record MAU: %w", err)
|
||||
}
|
||||
r.cache.Expire(ctx, mauKey, MAUTTL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDAUFromRedis 从 Redis 获取日活用户数
|
||||
func (r *UserActivityRepository) GetDAUFromRedis(ctx context.Context, date time.Time) (int64, error) {
|
||||
key := formatDAUKey(date)
|
||||
count, err := r.cache.ZCard(ctx, key)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get DAU from redis: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetWAUFromRedis 从 Redis 获取周活用户数
|
||||
func (r *UserActivityRepository) GetWAUFromRedis(ctx context.Context, year int, week int) (int64, error) {
|
||||
key := formatWAUKey(year, week)
|
||||
count, err := r.cache.ZCard(ctx, key)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get WAU from redis: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetMAUFromRedis 从 Redis 获取月活用户数
|
||||
func (r *UserActivityRepository) GetMAUFromRedis(ctx context.Context, year int, month int) (int64, error) {
|
||||
key := formatMAUKey(year, month)
|
||||
count, err := r.cache.ZCard(ctx, key)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get MAU from redis: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetDAUUserIDsFromRedis 获取日活用户ID列表
|
||||
func (r *UserActivityRepository) GetDAUUserIDsFromRedis(ctx context.Context, date time.Time) ([]string, error) {
|
||||
key := formatDAUKey(date)
|
||||
// 获取所有成员(从最小分数到最大分数)
|
||||
members, err := r.cache.ZRangeByScore(ctx, key, "-inf", "+inf", 0, -1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DAU user IDs from redis: %w", err)
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
// formatDAUKey 格式化 DAU Key
|
||||
// 格式: stats:dau:YYYYMMDD
|
||||
func formatDAUKey(date time.Time) string {
|
||||
return fmt.Sprintf("%s%s", DAUKeyPrefix, date.Format("20060102"))
|
||||
}
|
||||
|
||||
// formatWAUKey 格式化 WAU Key
|
||||
// 格式: stats:wau:YYYYWW (年-周,周数补零到2位)
|
||||
func formatWAUKey(year int, week int) string {
|
||||
return fmt.Sprintf("%s%04d%02d", WAUKeyPrefix, year, week)
|
||||
}
|
||||
|
||||
// formatMAUKey 格式化 MAU Key
|
||||
// 格式: stats:mau:YYYYMM
|
||||
func formatMAUKey(year int, month int) string {
|
||||
return fmt.Sprintf("%s%04d%02d", MAUKeyPrefix, year, month)
|
||||
}
|
||||
Reference in New Issue
Block a user