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:
2026-03-14 02:09:38 +08:00
parent c561a0bb0c
commit ae5b997924
27 changed files with 2197 additions and 691 deletions

22
internal/config/casbin.go Normal file
View File

@@ -0,0 +1,22 @@
package config
// CasbinConfig Casbin 配置
type CasbinConfig struct {
// ModelPath Casbin 模型文件路径
ModelPath string `mapstructure:"model_path"`
// AutoSave 是否自动保存策略到数据库
AutoSave bool `mapstructure:"auto_save"`
// AutoLoad 是否自动从数据库加载策略
AutoLoad bool `mapstructure:"auto_load"`
// EnableCache 是否启用 Redis 缓存
EnableCache bool `mapstructure:"enable_cache"`
// CacheTTL 缓存过期时间 (秒)
CacheTTL int `mapstructure:"cache_ttl"`
// SkipRoutes 跳过权限检查的路由
SkipRoutes []string `mapstructure:"skip_routes"`
}

View File

@@ -26,6 +26,7 @@ type Config struct {
Email EmailConfig `mapstructure:"email"`
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
GRPC GRPCConfig `mapstructure:"grpc"`
Casbin CasbinConfig `mapstructure:"casbin"`
}
// Load 加载配置文件
@@ -134,6 +135,13 @@ func Load(configPath string) (*Config, error) {
viper.SetDefault("grpc.tls_enabled", false)
viper.SetDefault("grpc.tls_cert_file", "")
viper.SetDefault("grpc.tls_key_file", "")
// Casbin 默认值
viper.SetDefault("casbin.model_path", "./configs/casbin/model.conf")
viper.SetDefault("casbin.auto_save", true)
viper.SetDefault("casbin.auto_load", true)
viper.SetDefault("casbin.enable_cache", true)
viper.SetDefault("casbin.cache_ttl", 300)
viper.SetDefault("casbin.skip_routes", []string{"/health", "/api/v1/auth/login", "/api/v1/auth/register"})
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
@@ -215,6 +223,12 @@ func Load(configPath string) (*Config, error) {
cfg.GRPC.TLSEnabled, _ = strconv.ParseBool(getEnvOrDefault("APP_GRPC_TLS_ENABLED", fmt.Sprintf("%t", cfg.GRPC.TLSEnabled)))
cfg.GRPC.TLSCertFile = getEnvOrDefault("APP_GRPC_TLS_CERT_FILE", cfg.GRPC.TLSCertFile)
cfg.GRPC.TLSKeyFile = getEnvOrDefault("APP_GRPC_TLS_KEY_FILE", cfg.GRPC.TLSKeyFile)
// Casbin 环境变量覆盖
cfg.Casbin.ModelPath = getEnvOrDefault("APP_CASBIN_MODEL_PATH", cfg.Casbin.ModelPath)
cfg.Casbin.AutoSave, _ = strconv.ParseBool(getEnvOrDefault("APP_CASBIN_AUTO_SAVE", fmt.Sprintf("%t", cfg.Casbin.AutoSave)))
cfg.Casbin.AutoLoad, _ = strconv.ParseBool(getEnvOrDefault("APP_CASBIN_AUTO_LOAD", fmt.Sprintf("%t", cfg.Casbin.AutoLoad)))
cfg.Casbin.EnableCache, _ = strconv.ParseBool(getEnvOrDefault("APP_CASBIN_ENABLE_CACHE", fmt.Sprintf("%t", cfg.Casbin.EnableCache)))
cfg.Casbin.CacheTTL, _ = strconv.Atoi(getEnvOrDefault("APP_CASBIN_CACHE_TTL", fmt.Sprintf("%d", cfg.Casbin.CacheTTL)))
return &cfg, nil
}

View File

@@ -90,6 +90,15 @@ var (
// 通知相关错误
ErrUnauthorizedNotification = &AppError{Code: "UNAUTHORIZED_NOTIFICATION", Message: "无权删除此通知"}
// Casbin 相关错误
ErrPermissionDenied = &AppError{Code: "PERMISSION_DENIED", Message: "权限不足"}
ErrRoleNotFound = &AppError{Code: "ROLE_NOT_FOUND", Message: "角色不存在"}
ErrRoleAlreadyAssigned = &AppError{Code: "ROLE_ALREADY_ASSIGNED", Message: "用户已拥有该角色"}
ErrRoleNotAssigned = &AppError{Code: "ROLE_NOT_ASSIGNED", Message: "用户未拥有该角色"}
ErrCannotModifyOwnRole = &AppError{Code: "CANNOT_MODIFY_OWN_ROLE", Message: "不能修改自己的角色"}
ErrCannotModifySuperAdmin = &AppError{Code: "CANNOT_MODIFY_SUPER_ADMIN", Message: "不能修改超级管理员角色"}
ErrCasbinInternal = &AppError{Code: "CASBIN_INTERNAL_ERROR", Message: "权限系统内部错误"}
)
// Wrap 包装错误

View File

@@ -0,0 +1,204 @@
package handler
import (
"errors"
"net/http"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
// RoleHandler 角色管理处理器
type RoleHandler struct {
roleService service.RoleService
}
// NewRoleHandler 创建角色处理器
func NewRoleHandler(roleService service.RoleService) *RoleHandler {
return &RoleHandler{
roleService: roleService,
}
}
// GetAllRoles 获取所有角色
// GET /api/v1/admin/roles
func (h *RoleHandler) GetAllRoles(c *gin.Context) {
roles, err := h.roleService.GetAllRoles(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": "获取角色列表失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": h.toRoleListResponse(roles),
})
}
// GetUserRoles 获取用户的角色
// GET /api/v1/admin/users/:id/roles
func (h *RoleHandler) GetUserRoles(c *gin.Context) {
userID := c.Param("id")
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "缺少用户ID",
})
return
}
roles, err := h.roleService.GetUserRoles(c.Request.Context(), userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": "获取用户角色失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": h.toRoleListResponse(roles),
})
}
// AssignRoleRequest 分配角色请求
type AssignRoleRequest struct {
Role string `json:"role" binding:"required"`
}
// AssignRole 为用户分配角色
// POST /api/v1/admin/users/:id/roles
func (h *RoleHandler) AssignRole(c *gin.Context) {
targetUserID := c.Param("id")
if targetUserID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "缺少用户ID",
})
return
}
var req AssignRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "请求参数错误",
})
return
}
// 获取操作者ID
operatorID, _ := c.Get("user_id")
err := h.roleService.AssignRole(c.Request.Context(), operatorID.(string), targetUserID, req.Role)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "角色分配成功",
})
}
// RemoveRole 移除用户角色
// DELETE /api/v1/admin/users/:id/roles/:role
func (h *RoleHandler) RemoveRole(c *gin.Context) {
targetUserID := c.Param("id")
role := c.Param("role")
if targetUserID == "" || role == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "缺少用户ID或角色名",
})
return
}
// 获取操作者ID
operatorID, _ := c.Get("user_id")
err := h.roleService.RemoveRole(c.Request.Context(), operatorID.(string), targetUserID, role)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "角色移除成功",
})
}
// GetMyRoles 获取当前用户的角色
// GET /api/v1/users/me/roles
func (h *RoleHandler) GetMyRoles(c *gin.Context) {
userID, _ := c.Get("user_id")
roles, err := h.roleService.GetUserRoles(c.Request.Context(), userID.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": "获取用户角色失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": h.toRoleListResponse(roles),
})
}
// toRoleListResponse 转换为响应格式
func (h *RoleHandler) toRoleListResponse(roles []model.Role) []gin.H {
result := make([]gin.H, len(roles))
for i, role := range roles {
result[i] = gin.H{
"name": role.Name,
"display_name": role.DisplayName,
"description": role.Description,
"priority": role.Priority,
}
}
return result
}
// handleError 处理错误
func (h *RoleHandler) handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, apperrors.ErrCannotModifyOwnRole):
c.JSON(http.StatusForbidden, gin.H{
"code": "CANNOT_MODIFY_OWN_ROLE",
"message": "不能修改自己的角色",
})
case errors.Is(err, apperrors.ErrCannotModifySuperAdmin):
c.JSON(http.StatusForbidden, gin.H{
"code": "CANNOT_MODIFY_SUPER_ADMIN",
"message": "不能修改超级管理员角色",
})
case errors.Is(err, apperrors.ErrRoleAlreadyAssigned):
c.JSON(http.StatusConflict, gin.H{
"code": "ROLE_ALREADY_ASSIGNED",
"message": "用户已拥有该角色",
})
case errors.Is(err, apperrors.ErrRoleNotAssigned):
c.JSON(http.StatusConflict, gin.H{
"code": "ROLE_NOT_ASSIGNED",
"message": "用户未拥有该角色",
})
case errors.Is(err, apperrors.ErrRoleNotFound):
c.JSON(http.StatusNotFound, gin.H{
"code": "ROLE_NOT_FOUND",
"message": "角色不存在",
})
default:
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": "操作失败",
})
}
}

View File

@@ -1,25 +1,29 @@
package handler
import (
"context"
"strconv"
"github.com/gin-gonic/gin"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
)
// UserHandler 用户处理器
type UserHandler struct {
userService service.UserService
jwtService *service.JWTService
userService service.UserService
activityService service.UserActivityService // 用户活跃统计服务
jwtService *service.JWTService
}
// NewUserHandler 创建用户处理器
func NewUserHandler(userService service.UserService) *UserHandler {
func NewUserHandler(userService service.UserService, activityService service.UserActivityService) *UserHandler {
return &UserHandler{
userService: userService,
userService: userService,
activityService: activityService,
}
}
@@ -90,6 +94,17 @@ func (h *UserHandler) Login(c *gin.Context) {
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
// 记录用户活跃
if h.activityService != nil {
go func() {
ctx := context.Background()
loginType := model.LoginTypeLogin
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
}()
}
response.Success(c, gin.H{
"user": dto.ConvertUserToResponse(user),
"token": accessToken,
@@ -353,6 +368,17 @@ func (h *UserHandler) RefreshToken(c *gin.Context) {
accessToken, _ := h.jwtService.GenerateAccessToken(claims.UserID, claims.Username)
refreshToken, _ := h.jwtService.GenerateRefreshToken(claims.UserID, claims.Username)
// 记录用户活跃
if h.activityService != nil {
go func() {
ctx := context.Background()
loginType := model.LoginTypeRefresh
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
h.activityService.RecordUserActive(ctx, claims.UserID, loginType, ip, userAgent)
}()
}
response.Success(c, gin.H{
"token": accessToken,
"refresh_token": refreshToken,
@@ -364,6 +390,11 @@ func (h *UserHandler) SetJWTService(jwtSvc *service.JWTService) {
h.jwtService = jwtSvc
}
// SetActivityService 设置活跃统计服务
func (h *UserHandler) SetActivityService(activityService service.UserActivityService) {
h.activityService = activityService
}
// FollowUser 关注用户
func (h *UserHandler) FollowUser(c *gin.Context) {
userID := c.Param("id")

View File

@@ -0,0 +1,130 @@
package middleware
import (
"strings"
"carrot_bbs/internal/service"
"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()
}
}
// OptionalCasbinAuth 可选的 Casbin 权限检查
// 不阻止请求,但会设置权限标志
func OptionalCasbinAuth(casbinService service.CasbinService) gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if exists {
path := c.Request.URL.Path
method := c.Request.Method
allowed, err := casbinService.EnforceForUser(c.Request.Context(), userID.(string), path, method)
if err == nil {
c.Set("permission_granted", allowed)
}
}
c.Next()
}
}
// RequireRole 要求特定角色的中间件
func RequireRole(casbinService service.CasbinService, requiredRoles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.AbortWithStatusJSON(401, gin.H{
"code": "UNAUTHORIZED",
"message": "请先登录",
})
return
}
userRoles, err := casbinService.GetRolesForUser(c.Request.Context(), userID.(string))
if err != nil {
c.AbortWithStatusJSON(500, gin.H{
"code": "INTERNAL_ERROR",
"message": "获取用户角色失败",
})
return
}
// 检查用户是否拥有任一所需角色
roleMap := make(map[string]bool)
for _, role := range userRoles {
roleMap[role] = true
}
hasRole := false
for _, required := range requiredRoles {
if roleMap[required] {
hasRole = true
break
}
}
if !hasRole {
c.AbortWithStatusJSON(403, gin.H{
"code": "FORBIDDEN",
"message": "需要以下角色之一: " + strings.Join(requiredRoles, ", "),
})
return
}
c.Next()
}
}

View File

@@ -141,6 +141,15 @@ func autoMigrate(db *gorm.DB) error {
// 课表
&ScheduleCourse{},
// 用户活跃相关
&UserActiveLog{},
&UserActivityStat{},
// 角色和权限相关
&Role{},
&UserRole{},
&CasbinRule{},
)
if err != nil {
return err

57
internal/model/role.go Normal file
View File

@@ -0,0 +1,57 @@
package model
import "time"
// Role 角色定义
type Role struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;size:64;not null" json:"name"`
DisplayName string `gorm:"size:128;not null" json:"display_name"`
Description string `json:"description"`
ParentRole *string `gorm:"size:64" json:"parent_role"`
Priority int `gorm:"default:0" json:"priority"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Role) TableName() string {
return "roles"
}
// UserRole 用户角色关联
type UserRole struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID string `gorm:"index;size:64;not null" json:"user_id"`
Role string `gorm:"index;size:64;not null" json:"role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (UserRole) TableName() string {
return "user_roles"
}
// CasbinRule Casbin 策略规则 (对应 casbin_rule 表)
type CasbinRule struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Ptype string `gorm:"size:255;not null;index"`
V0 string `gorm:"size:255;not null;index"`
V1 string `gorm:"size:255;not null;index"`
V2 string `gorm:"size:255;default:''"`
V3 string `gorm:"size:255;default:''"`
V4 string `gorm:"size:255;default:''"`
V5 string `gorm:"size:255;default:''"`
}
func (CasbinRule) TableName() string {
return "casbin_rule"
}
// 预定义角色常量
const (
RoleSuperAdmin = "super_admin"
RoleAdmin = "admin"
RoleModerator = "moderator"
RoleUser = "user"
RoleBanned = "banned"
)

View File

@@ -0,0 +1,49 @@
package model
import (
"time"
)
// 登录类型常量
const (
LoginTypeLogin = "login" // 登录
LoginTypeRefresh = "refresh" // 刷新Token
)
// UserActiveLog 用户活跃日志
type UserActiveLog struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
UserID string `gorm:"type:varchar(36);not null;index:idx_user_date,priority:1;index" json:"user_id"` // 用户ID (UUID)
ActiveDate time.Time `gorm:"type:date;not null;index:idx_user_date,priority:2;index" json:"active_date"` // 活跃日期
LoginType string `gorm:"type:varchar(20);not null" json:"login_type"` // 登录类型: login/refresh
LoginIP string `gorm:"type:varchar(45)" json:"login_ip"` // 登录IP
UserAgent string `gorm:"type:varchar(500)" json:"user_agent"` // 用户代理
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
// 关联
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
// TableName 指定表名
func (UserActiveLog) TableName() string {
return "user_active_logs"
}
// UserActivityStat 用户活跃统计快照
type UserActivityStat struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
StatType string `gorm:"type:varchar(10);not null;uniqueIndex:idx_type_date" json:"stat_type"` // dau/wau/mau
StatDate time.Time `gorm:"type:date;not null;uniqueIndex:idx_type_date" json:"stat_date"` // 统计日期
ActiveCount int64 `gorm:"not null" json:"active_count"` // 活跃用户数
NewCount int64 `gorm:"default:0" json:"new_count"` // 新增用户数
Retention1 float64 `gorm:"type:decimal(5,2)" json:"retention_1"` // 次日留存率
Retention7 float64 `gorm:"type:decimal(5,2)" json:"retention_7"` // 7日留存率
Retention30 float64 `gorm:"type:decimal(5,2)" json:"retention_30"` // 30日留存率
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (UserActivityStat) TableName() string {
return "user_activity_stats"
}

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

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

View File

@@ -5,6 +5,7 @@ import (
"carrot_bbs/internal/handler"
"carrot_bbs/internal/middleware"
"carrot_bbs/internal/model"
"carrot_bbs/internal/service"
)
@@ -24,7 +25,9 @@ type Router struct {
gorseHandler *handler.GorseHandler
voteHandler *handler.VoteHandler
scheduleHandler *handler.ScheduleHandler
roleHandler *handler.RoleHandler
jwtService *service.JWTService
casbinService service.CasbinService
}
// New 创建路由
@@ -43,9 +46,14 @@ func New(
gorseHandler *handler.GorseHandler,
voteHandler *handler.VoteHandler,
scheduleHandler *handler.ScheduleHandler,
roleHandler *handler.RoleHandler,
activityService service.UserActivityService,
casbinService service.CasbinService,
) *Router {
// 设置JWT服务
userHandler.SetJWTService(jwtService)
// 设置活跃统计服务
userHandler.SetActivityService(activityService)
r := &Router{
engine: gin.Default(),
@@ -62,7 +70,9 @@ func New(
gorseHandler: gorseHandler,
voteHandler: voteHandler,
scheduleHandler: scheduleHandler,
roleHandler: roleHandler,
jwtService: jwtService,
casbinService: casbinService,
}
r.setupRoutes()
@@ -95,6 +105,8 @@ func (r *Router) setupRoutes() {
// 需要认证的路由
authMiddleware := middleware.Auth(r.jwtService)
// Casbin 权限中间件(暂不全局启用,可根据需要在特定路由组上使用)
// casbinMiddleware := middleware.CasbinAuth(r.casbinService)
// 用户路由
users := v1.Group("/users")
@@ -109,6 +121,11 @@ func (r *Router) setupRoutes() {
users.POST("/change-password/send-code", authMiddleware, r.userHandler.SendChangePasswordCode)
users.POST("/change-password", authMiddleware, r.userHandler.ChangePassword)
// 当前用户角色
if r.roleHandler != nil {
users.GET("/me/roles", authMiddleware, r.roleHandler.GetMyRoles)
}
// 搜索用户
users.GET("/search", middleware.OptionalAuth(r.jwtService), r.userHandler.Search)
@@ -339,6 +356,20 @@ func (r *Router) setupRoutes() {
gorseGroup.POST("/import", r.gorseHandler.ImportData)
}
}
// 管理路由(需要管理员权限)
if r.roleHandler != nil {
admin := v1.Group("/admin")
admin.Use(authMiddleware)
admin.Use(middleware.RequireRole(r.casbinService, model.RoleAdmin, model.RoleSuperAdmin))
{
// 角色管理
admin.GET("/roles", r.roleHandler.GetAllRoles)
admin.GET("/users/:id/roles", r.roleHandler.GetUserRoles)
admin.POST("/users/:id/roles", r.roleHandler.AssignRole)
admin.DELETE("/users/:id/roles/:role", r.roleHandler.RemoveRole)
}
}
}
}

View File

@@ -10,22 +10,13 @@ import (
"time"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/openai"
"gorm.io/gorm"
)
// ==================== 内容审核服务接口和实现 ====================
// AuditServiceProvider 内容审核服务提供商接口
type AuditServiceProvider interface {
// AuditText 审核文本
AuditText(ctx context.Context, text string, scene string) (*AuditResult, error)
// AuditImage 审核图片
AuditImage(ctx context.Context, imageURL string) (*AuditResult, error)
// GetName 获取提供商名称
GetName() string
}
// AuditResult 审核结果
type AuditResult struct {
Pass bool `json:"pass"` // 是否通过
@@ -42,169 +33,293 @@ type AuditService interface {
AuditText(ctx context.Context, text string, auditType string) (*AuditResult, error)
// AuditImage 审核图片
AuditImage(ctx context.Context, imageURL string) (*AuditResult, error)
// AuditPost 审核帖子
AuditPost(ctx context.Context, title, content string, images []string) (*AuditResult, error)
// AuditComment 审核评论
AuditComment(ctx context.Context, content string, images []string) (*AuditResult, error)
// GetAuditResult 获取审核结果
GetAuditResult(ctx context.Context, auditID string) (*AuditResult, error)
// SetProvider 设置审核服务提供商
SetProvider(provider AuditServiceProvider)
// GetProvider 获取当前审核服务提供商
GetProvider() AuditServiceProvider
// IsEnabled 检查审核服务是否启用
IsEnabled() bool
}
// auditServiceImpl 内容审核服务实现
type auditServiceImpl struct {
db *gorm.DB
provider AuditServiceProvider
config *AuditConfig
mu sync.RWMutex
db *gorm.DB
openaiClient openai.Client
config *AuditConfig
mu sync.RWMutex
}
// AuditConfig 内容审核服务配置
type AuditConfig struct {
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
// 审核服务提供商: local, aliyun, tencent, baidu
Provider string `mapstructure:"provider" yaml:"provider"`
// 阿里云配置
AliyunAccessKey string `mapstructure:"aliyun_access_key" yaml:"aliyun_access_key"`
AliyunSecretKey string `mapstructure:"aliyun_secret_key" yaml:"aliyun_secret_key"`
AliyunRegion string `mapstructure:"aliyun_region" yaml:"aliyun_region"`
// 腾讯云配置
TencentSecretID string `mapstructure:"tencent_secret_id" yaml:"tencent_secret_id"`
TencentSecretKey string `mapstructure:"tencent_secret_key" yaml:"tencent_secret_key"`
// 百度云配置
BaiduAPIKey string `mapstructure:"baidu_api_key" yaml:"baidu_api_key"`
BaiduSecretKey string `mapstructure:"baidu_secret_key" yaml:"baidu_secret_key"`
// 是否自动审核
AutoAudit bool `mapstructure:"auto_audit" yaml:"auto_audit"`
// 审核超时时间(秒)
Timeout int `mapstructure:"timeout" yaml:"timeout"`
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
StrictModeration bool `mapstructure:"strict_moderation" yaml:"strict_moderation"` // 严格模式:审核失败时拒绝;非严格模式:审核失败时放行
Timeout int `mapstructure:"timeout" yaml:"timeout"`
}
// NewAuditService 创建内容审核服务
func NewAuditService(db *gorm.DB, config *AuditConfig) AuditService {
s := &auditServiceImpl{
db: db,
config: config,
func NewAuditService(db *gorm.DB, openaiClient openai.Client, config *AuditConfig) AuditService {
return &auditServiceImpl{
db: db,
openaiClient: openaiClient,
config: config,
}
// 根据配置初始化提供商
if config.Enabled {
provider := s.initProvider(config.Provider)
s.provider = provider
}
return s
}
// initProvider 根据配置初始化审核服务提供商
func (s *auditServiceImpl) initProvider(providerType string) AuditServiceProvider {
switch strings.ToLower(providerType) {
case "aliyun":
return NewAliyunAuditProvider(s.config.AliyunAccessKey, s.config.AliyunSecretKey, s.config.AliyunRegion)
case "tencent":
return NewTencentAuditProvider(s.config.TencentSecretID, s.config.TencentSecretKey)
case "baidu":
return NewBaiduAuditProvider(s.config.BaiduAPIKey, s.config.BaiduSecretKey)
case "local":
fallthrough
default:
// 默认使用本地审核服务
return NewLocalAuditProvider()
}
// IsEnabled 检查审核服务是否启用
func (s *auditServiceImpl) IsEnabled() bool {
return s != nil && s.config != nil && s.config.Enabled && s.openaiClient != nil && s.openaiClient.IsEnabled()
}
// AuditText 审核文本
func (s *auditServiceImpl) AuditText(ctx context.Context, text string, auditType string) (*AuditResult, error) {
if !s.config.Enabled {
// 如果审核服务未启用,直接返回通过
if !s.IsEnabled() {
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Audit service disabled",
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Audit service disabled",
Provider: "ai",
}, nil
}
if text == "" {
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Empty text",
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Empty text",
Provider: "ai",
}, nil
}
var result *AuditResult
var err error
// 使用提供商审核
if s.provider != nil {
result, err = s.provider.AuditText(ctx, text, auditType)
} else {
// 如果没有设置提供商,使用本地审核
localProvider := NewLocalAuditProvider()
result, err = localProvider.AuditText(ctx, text, auditType)
// 使用 AI 审核文本
approved, reason, err := s.openaiClient.ModerateComment(ctx, text, nil)
if err != nil {
log.Printf("AI audit text error: %v", err)
if s.config.StrictModeration {
return &AuditResult{
Pass: false,
Risk: "high",
Suggest: "review",
Detail: fmt.Sprintf("Audit error: %v", err),
Provider: "ai",
}, err
}
// 非严格模式下,审核失败时放行
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err),
Provider: "ai",
}, nil
}
if err != nil {
log.Printf("Audit text error: %v", err)
return &AuditResult{
Pass: false,
Risk: "high",
Suggest: "review",
Detail: fmt.Sprintf("Audit error: %v", err),
}, err
result := &AuditResult{
Pass: approved,
Provider: "ai",
Detail: reason,
}
if approved {
result.Risk = "low"
result.Suggest = "pass"
} else {
result.Risk = "high"
result.Suggest = "block"
result.Labels = []string{"ai_rejected"}
}
// 记录审核日志
go s.saveAuditLog(ctx, "text", "", text, auditType, result)
go s.saveAuditLog(ctx, "text", text, "", auditType, result)
return result, nil
}
// AuditImage 审核图片
func (s *auditServiceImpl) AuditImage(ctx context.Context, imageURL string) (*AuditResult, error) {
if !s.config.Enabled {
if !s.IsEnabled() {
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Audit service disabled",
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Audit service disabled",
Provider: "ai",
}, nil
}
if imageURL == "" {
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Empty image URL",
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Empty image URL",
Provider: "ai",
}, nil
}
var result *AuditResult
var err error
// 使用提供商审核
if s.provider != nil {
result, err = s.provider.AuditImage(ctx, imageURL)
} else {
// 如果没有设置提供商,使用本地审核
localProvider := NewLocalAuditProvider()
result, err = localProvider.AuditImage(ctx, imageURL)
// 使用 AI 审核图片
approved, reason, err := s.openaiClient.ModerateComment(ctx, "", []string{imageURL})
if err != nil {
log.Printf("AI audit image error: %v", err)
if s.config.StrictModeration {
return &AuditResult{
Pass: false,
Risk: "high",
Suggest: "review",
Detail: fmt.Sprintf("Audit error: %v", err),
Provider: "ai",
}, err
}
// 非严格模式下,审核失败时放行
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err),
Provider: "ai",
}, nil
}
if err != nil {
log.Printf("Audit image error: %v", err)
return &AuditResult{
Pass: false,
Risk: "high",
Suggest: "review",
Detail: fmt.Sprintf("Audit error: %v", err),
}, err
result := &AuditResult{
Pass: approved,
Provider: "ai",
Detail: reason,
}
if approved {
result.Risk = "low"
result.Suggest = "pass"
} else {
result.Risk = "high"
result.Suggest = "block"
result.Labels = []string{"ai_rejected"}
}
// 记录审核日志
go s.saveAuditLog(ctx, "image", "", "", "image", result)
go s.saveAuditLog(ctx, "image", "", imageURL, "image", result)
return result, nil
}
// AuditPost 审核帖子
func (s *auditServiceImpl) AuditPost(ctx context.Context, title, content string, images []string) (*AuditResult, error) {
if !s.IsEnabled() {
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Audit service disabled",
Provider: "ai",
}, nil
}
// 使用 AI 审核帖子
approved, reason, err := s.openaiClient.ModeratePost(ctx, title, content, images)
if err != nil {
log.Printf("AI audit post error: %v", err)
if s.config.StrictModeration {
return &AuditResult{
Pass: false,
Risk: "high",
Suggest: "review",
Detail: fmt.Sprintf("Audit error: %v", err),
Provider: "ai",
}, err
}
// 非严格模式下,审核失败时放行
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err),
Provider: "ai",
}, nil
}
result := &AuditResult{
Pass: approved,
Provider: "ai",
Detail: reason,
}
if approved {
result.Risk = "low"
result.Suggest = "pass"
} else {
result.Risk = "high"
result.Suggest = "block"
result.Labels = []string{"ai_rejected"}
}
// 记录审核日志
contentSummary := fmt.Sprintf("标题: %s, 内容: %s", title, content)
if len(content) > 200 {
contentSummary = fmt.Sprintf("标题: %s, 内容: %s...", title, content[:200])
}
go s.saveAuditLog(ctx, "post", contentSummary, strings.Join(images, ","), "post", result)
return result, nil
}
// AuditComment 审核评论
func (s *auditServiceImpl) AuditComment(ctx context.Context, content string, images []string) (*AuditResult, error) {
if !s.IsEnabled() {
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: "Audit service disabled",
Provider: "ai",
}, nil
}
// 使用 AI 审核评论
approved, reason, err := s.openaiClient.ModerateComment(ctx, content, images)
if err != nil {
log.Printf("AI audit comment error: %v", err)
if s.config.StrictModeration {
return &AuditResult{
Pass: false,
Risk: "high",
Suggest: "review",
Detail: fmt.Sprintf("Audit error: %v", err),
Provider: "ai",
}, err
}
// 非严格模式下,审核失败时放行
return &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err),
Provider: "ai",
}, nil
}
result := &AuditResult{
Pass: approved,
Provider: "ai",
Detail: reason,
}
if approved {
result.Risk = "low"
result.Suggest = "pass"
} else {
result.Risk = "high"
result.Suggest = "block"
result.Labels = []string{"ai_rejected"}
}
// 记录审核日志
contentSummary := content
if len(content) > 200 {
contentSummary = content[:200] + "..."
}
go s.saveAuditLog(ctx, "comment", contentSummary, strings.Join(images, ","), "comment", result)
return result, nil
}
@@ -235,32 +350,25 @@ func (s *auditServiceImpl) GetAuditResult(ctx context.Context, auditID string) (
return result, nil
}
// SetProvider 设置审核服务提供商
func (s *auditServiceImpl) SetProvider(provider AuditServiceProvider) {
s.mu.Lock()
defer s.mu.Unlock()
s.provider = provider
}
// GetProvider 获取当前审核服务提供商
func (s *auditServiceImpl) GetProvider() AuditServiceProvider {
s.mu.RLock()
defer s.mu.RUnlock()
return s.provider
}
// saveAuditLog 保存审核日志
func (s *auditServiceImpl) saveAuditLog(ctx context.Context, contentType, content, imageURL, auditType string, result *AuditResult) {
if s.db == nil {
return
}
labelsJSON := ""
if len(result.Labels) > 0 {
if data, err := json.Marshal(result.Labels); err == nil {
labelsJSON = string(data)
}
}
auditLog := model.AuditLog{
ContentType: contentType,
Content: content,
ContentURL: imageURL,
AuditType: auditType,
Labels: strings.Join(result.Labels, ","),
Labels: labelsJSON,
Suggestion: result.Suggest,
Detail: result.Detail,
Source: model.AuditSourceAuto,
@@ -291,294 +399,6 @@ func (s *auditServiceImpl) saveAuditLog(ctx context.Context, contentType, conten
}
}
// ==================== 本地审核服务提供商 ====================
// localAuditProvider 本地审核服务提供商
type localAuditProvider struct {
// 可以注入敏感词服务进行本地审核
sensitiveService SensitiveService
}
// NewLocalAuditProvider 创建本地审核服务提供商
func NewLocalAuditProvider() AuditServiceProvider {
return &localAuditProvider{
sensitiveService: nil,
}
}
// GetName 获取提供商名称
func (p *localAuditProvider) GetName() string {
return "local"
}
// AuditText 审核文本
func (p *localAuditProvider) AuditText(ctx context.Context, text string, scene string) (*AuditResult, error) {
// 本地审核逻辑
// 1. 敏感词检查
// 2. 规则匹配
// 3. 简单的关键词检测
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "local",
}
// 如果有敏感词服务,使用它进行检测
if p.sensitiveService != nil {
hasSensitive, words := p.sensitiveService.Check(ctx, text)
if hasSensitive {
result.Pass = false
result.Risk = "high"
result.Suggest = "block"
result.Detail = fmt.Sprintf("包含敏感词: %s", strings.Join(words, ","))
result.Labels = append(result.Labels, "sensitive")
}
}
// 简单的关键词检测规则
// 实际项目中应该从数据库加载
suspiciousPatterns := []string{
"诈骗",
"钓鱼",
"木马",
"病毒",
}
for _, pattern := range suspiciousPatterns {
if strings.Contains(text, pattern) {
result.Pass = false
result.Risk = "high"
result.Suggest = "block"
result.Labels = append(result.Labels, "suspicious")
if result.Detail == "" {
result.Detail = fmt.Sprintf("包含可疑内容: %s", pattern)
} else {
result.Detail += fmt.Sprintf(", %s", pattern)
}
}
}
return result, nil
}
// AuditImage 审核图片
func (p *localAuditProvider) AuditImage(ctx context.Context, imageURL string) (*AuditResult, error) {
// 本地图片审核逻辑
// 1. 图片URL合法性检查
// 2. 图片格式检查
// 3. 可以扩展接入本地图片识别服务
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "local",
}
// 检查URL是否为空
if imageURL == "" {
result.Detail = "Empty image URL"
return result, nil
}
// 检查是否为支持的图片URL格式
validPrefixes := []string{"http://", "https://", "s3://", "oss://", "cos://"}
isValid := false
for _, prefix := range validPrefixes {
if strings.HasPrefix(strings.ToLower(imageURL), prefix) {
isValid = true
break
}
}
if !isValid {
result.Pass = false
result.Risk = "medium"
result.Suggest = "review"
result.Detail = "Invalid image URL format"
result.Labels = append(result.Labels, "invalid_url")
}
return result, nil
}
// SetSensitiveService 设置敏感词服务
func (p *localAuditProvider) SetSensitiveService(ss SensitiveService) {
p.sensitiveService = ss
}
// ==================== 阿里云审核服务提供商 ====================
// aliyunAuditProvider 阿里云审核服务提供商
type aliyunAuditProvider struct {
accessKey string
secretKey string
region string
}
// NewAliyunAuditProvider 创建阿里云审核服务提供商
func NewAliyunAuditProvider(accessKey, secretKey, region string) AuditServiceProvider {
return &aliyunAuditProvider{
accessKey: accessKey,
secretKey: secretKey,
region: region,
}
}
// GetName 获取提供商名称
func (p *aliyunAuditProvider) GetName() string {
return "aliyun"
}
// AuditText 审核文本
func (p *aliyunAuditProvider) AuditText(ctx context.Context, text string, scene string) (*AuditResult, error) {
// 阿里云内容安全API调用
// 实际项目中需要实现阿里云SDK调用
// 这里预留接口
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "aliyun",
Detail: "Aliyun audit not implemented, using pass",
}
// TODO: 实现阿里云内容安全API调用
// 具体参考: https://help.aliyun.com/document_detail/28417.html
return result, nil
}
// AuditImage 审核图片
func (p *aliyunAuditProvider) AuditImage(ctx context.Context, imageURL string) (*AuditResult, error) {
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "aliyun",
Detail: "Aliyun image audit not implemented, using pass",
}
// TODO: 实现阿里云图片审核API调用
return result, nil
}
// ==================== 腾讯云审核服务提供商 ====================
// tencentAuditProvider 腾讯云审核服务提供商
type tencentAuditProvider struct {
secretID string
secretKey string
}
// NewTencentAuditProvider 创建腾讯云审核服务提供商
func NewTencentAuditProvider(secretID, secretKey string) AuditServiceProvider {
return &tencentAuditProvider{
secretID: secretID,
secretKey: secretKey,
}
}
// GetName 获取提供商名称
func (p *tencentAuditProvider) GetName() string {
return "tencent"
}
// AuditText 审核文本
func (p *tencentAuditProvider) AuditText(ctx context.Context, text string, scene string) (*AuditResult, error) {
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "tencent",
Detail: "Tencent audit not implemented, using pass",
}
// TODO: 实现腾讯云内容审核API调用
// 具体参考: https://cloud.tencent.com/document/product/1124/64508
return result, nil
}
// AuditImage 审核图片
func (p *tencentAuditProvider) AuditImage(ctx context.Context, imageURL string) (*AuditResult, error) {
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "tencent",
Detail: "Tencent image audit not implemented, using pass",
}
// TODO: 实现腾讯云图片审核API调用
return result, nil
}
// ==================== 百度云审核服务提供商 ====================
// baiduAuditProvider 百度云审核服务提供商
type baiduAuditProvider struct {
apiKey string
secretKey string
}
// NewBaiduAuditProvider 创建百度云审核服务提供商
func NewBaiduAuditProvider(apiKey, secretKey string) AuditServiceProvider {
return &baiduAuditProvider{
apiKey: apiKey,
secretKey: secretKey,
}
}
// GetName 获取提供商名称
func (p *baiduAuditProvider) GetName() string {
return "baidu"
}
// AuditText 审核文本
func (p *baiduAuditProvider) AuditText(ctx context.Context, text string, scene string) (*AuditResult, error) {
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "baidu",
Detail: "Baidu audit not implemented, using pass",
}
// TODO: 实现百度云内容审核API调用
// 具体参考: https://cloud.baidu.com/doc/ANTISPAM/s/Jjw0r1iF6
return result, nil
}
// AuditImage 审核图片
func (p *baiduAuditProvider) AuditImage(ctx context.Context, imageURL string) (*AuditResult, error) {
result := &AuditResult{
Pass: true,
Risk: "low",
Suggest: "pass",
Labels: []string{},
Provider: "baidu",
Detail: "Baidu image audit not implemented, using pass",
}
// TODO: 实现百度云图片审核API调用
return result, nil
}
// ==================== 审核结果回调处理 ====================
// AuditCallback 审核回调处理

View File

@@ -0,0 +1,225 @@
package service
import (
"context"
"fmt"
"time"
"carrot_bbs/internal/cache"
"carrot_bbs/internal/config"
"carrot_bbs/internal/repository"
"github.com/casbin/casbin/v2"
)
// CasbinService Casbin 权限服务接口
type CasbinService interface {
// Enforce 检查权限
Enforce(ctx context.Context, sub, obj, act string) (bool, error)
// EnforceForUser 检查用户权限 (自动获取用户角色)
EnforceForUser(ctx context.Context, userID, obj, act string) (bool, error)
// AddRoleForUser 为用户添加角色
AddRoleForUser(ctx context.Context, userID, role string) error
// DeleteRoleForUser 移除用户角色
DeleteRoleForUser(ctx context.Context, userID, role string) error
// GetRolesForUser 获取用户所有角色
GetRolesForUser(ctx context.Context, userID string) ([]string, error)
// HasRoleForUser 检查用户是否拥有指定角色
HasRoleForUser(ctx context.Context, userID, role string) (bool, error)
// AddPolicy 添加策略
AddPolicy(ctx context.Context, role, resource, action string) error
// RemovePolicy 移除策略
RemovePolicy(ctx context.Context, role, resource, action string) error
// LoadPolicy 从数据库重新加载策略
LoadPolicy(ctx context.Context) error
// ClearCache 清除权限缓存
ClearCache(ctx context.Context) error
}
type casbinService struct {
enforcer *casbin.Enforcer
cache cache.Cache
cacheTTL time.Duration
roleRepo repository.RoleRepository
skipRoutes map[string]bool
}
func NewCasbinService(
enforcer *casbin.Enforcer,
cache cache.Cache,
roleRepo repository.RoleRepository,
cfg *config.CasbinConfig,
) CasbinService {
skipRoutes := make(map[string]bool)
for _, route := range cfg.SkipRoutes {
skipRoutes[route] = true
}
return &casbinService{
enforcer: enforcer,
cache: cache,
cacheTTL: time.Duration(cfg.CacheTTL) * time.Second,
roleRepo: roleRepo,
skipRoutes: skipRoutes,
}
}
func (s *casbinService) Enforce(ctx context.Context, sub, obj, act string) (bool, error) {
// 检查是否跳过权限检查
if s.skipRoutes[obj] {
return true, nil
}
// 检查缓存
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:check:%s:%s:%s", sub, obj, act)
if cached, ok := s.cache.Get(cacheKey); ok {
if allowed, ok := cached.(bool); ok {
return allowed, nil
}
}
}
// 执行权限检查
allowed, err := s.enforcer.Enforce(sub, obj, act)
if err != nil {
return false, err
}
// 缓存结果
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:check:%s:%s:%s", sub, obj, act)
s.cache.Set(cacheKey, allowed, s.cacheTTL)
}
return allowed, nil
}
func (s *casbinService) EnforceForUser(ctx context.Context, userID, obj, act string) (bool, error) {
// 获取用户角色
roles, err := s.GetRolesForUser(ctx, userID)
if err != nil {
return false, err
}
// 如果用户没有角色,使用默认角色
if len(roles) == 0 {
roles = []string{"user"} // 默认角色
}
// 检查每个角色的权限
for _, role := range roles {
allowed, err := s.Enforce(ctx, role, obj, act)
if err != nil {
return false, err
}
if allowed {
return true, nil
}
}
return false, nil
}
func (s *casbinService) AddRoleForUser(ctx context.Context, userID, role string) error {
// 添加到数据库
if err := s.roleRepo.AddRoleForUser(ctx, userID, role); err != nil {
return err
}
// 添加到 Casbin
if _, err := s.enforcer.AddRoleForUser(userID, role); err != nil {
return err
}
// 清除缓存
return s.clearUserCache(ctx, userID)
}
func (s *casbinService) DeleteRoleForUser(ctx context.Context, userID, role string) error {
// 从数据库删除
if err := s.roleRepo.DeleteRoleForUser(ctx, userID, role); err != nil {
return err
}
// 从 Casbin 删除
if _, err := s.enforcer.DeleteRoleForUser(userID, role); err != nil {
return err
}
// 清除缓存
return s.clearUserCache(ctx, userID)
}
func (s *casbinService) GetRolesForUser(ctx context.Context, userID string) ([]string, error) {
// 检查缓存
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:user:roles:%s", userID)
if cached, ok := s.cache.Get(cacheKey); ok {
if roles, ok := cached.([]string); ok {
return roles, nil
}
}
}
// 从数据库获取
roles, err := s.roleRepo.GetUserRoles(ctx, userID)
if err != nil {
return nil, err
}
// 缓存结果
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:user:roles:%s", userID)
s.cache.Set(cacheKey, roles, s.cacheTTL)
}
return roles, nil
}
func (s *casbinService) HasRoleForUser(ctx context.Context, userID, role string) (bool, error) {
return s.roleRepo.HasRole(ctx, userID, role)
}
func (s *casbinService) AddPolicy(ctx context.Context, role, resource, action string) error {
if _, err := s.enforcer.AddPolicy(role, resource, action); err != nil {
return err
}
return s.ClearCache(ctx)
}
func (s *casbinService) RemovePolicy(ctx context.Context, role, resource, action string) error {
if _, err := s.enforcer.RemovePolicy(role, resource, action); err != nil {
return err
}
return s.ClearCache(ctx)
}
func (s *casbinService) LoadPolicy(ctx context.Context) error {
return s.enforcer.LoadPolicy()
}
func (s *casbinService) ClearCache(ctx context.Context) error {
if s.cache != nil {
// 清除所有 casbin 相关缓存
s.cache.DeleteByPrefix("casbin:")
}
return nil
}
func (s *casbinService) clearUserCache(ctx context.Context, userID string) error {
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:user:roles:%s", userID)
s.cache.Delete(cacheKey)
}
return nil
}

View File

@@ -0,0 +1,116 @@
package service
import (
"context"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// RoleService 角色管理服务接口
type RoleService interface {
// GetUserRoles 获取用户的所有角色
GetUserRoles(ctx context.Context, userID string) ([]model.Role, error)
// AssignRole 为用户分配角色
AssignRole(ctx context.Context, operatorID, targetUserID, role string) error
// RemoveRole 移除用户的角色
RemoveRole(ctx context.Context, operatorID, targetUserID, role string) error
// GetAllRoles 获取所有角色定义
GetAllRoles(ctx context.Context) ([]model.Role, error)
// GetRole 获取角色详情
GetRole(ctx context.Context, name string) (*model.Role, error)
}
type roleService struct {
roleRepo repository.RoleRepository
casbinSvc CasbinService
}
// NewRoleService 创建角色管理服务
func NewRoleService(
roleRepo repository.RoleRepository,
casbinSvc CasbinService,
) RoleService {
return &roleService{
roleRepo: roleRepo,
casbinSvc: casbinSvc,
}
}
func (s *roleService) GetUserRoles(ctx context.Context, userID string) ([]model.Role, error) {
roleNames, err := s.casbinSvc.GetRolesForUser(ctx, userID)
if err != nil {
return nil, err
}
var roles []model.Role
for _, name := range roleNames {
role, err := s.roleRepo.GetRole(ctx, name)
if err != nil {
continue // 忽略找不到的角色
}
roles = append(roles, *role)
}
return roles, nil
}
func (s *roleService) AssignRole(ctx context.Context, operatorID, targetUserID, role string) error {
// 不能修改自己的角色
if operatorID == targetUserID {
return apperrors.ErrCannotModifyOwnRole
}
// 检查目标用户是否已有该角色
hasRole, err := s.casbinSvc.HasRoleForUser(ctx, targetUserID, role)
if err != nil {
return err
}
if hasRole {
return apperrors.ErrRoleAlreadyAssigned
}
// 检查角色是否存在
_, err = s.roleRepo.GetRole(ctx, role)
if err != nil {
return apperrors.ErrRoleNotFound
}
// 添加角色
return s.casbinSvc.AddRoleForUser(ctx, targetUserID, role)
}
func (s *roleService) RemoveRole(ctx context.Context, operatorID, targetUserID, role string) error {
// 不能修改自己的角色
if operatorID == targetUserID {
return apperrors.ErrCannotModifyOwnRole
}
// 不能修改超级管理员角色
if role == model.RoleSuperAdmin {
return apperrors.ErrCannotModifySuperAdmin
}
// 检查用户是否有该角色
hasRole, err := s.casbinSvc.HasRoleForUser(ctx, targetUserID, role)
if err != nil {
return err
}
if !hasRole {
return apperrors.ErrRoleNotAssigned
}
return s.casbinSvc.DeleteRoleForUser(ctx, targetUserID, role)
}
func (s *roleService) GetAllRoles(ctx context.Context) ([]model.Role, error) {
return s.roleRepo.GetAllRoles(ctx)
}
func (s *roleService) GetRole(ctx context.Context, name string) (*model.Role, error) {
return s.roleRepo.GetRole(ctx, name)
}

View File

@@ -0,0 +1,223 @@
package service
import (
"context"
"fmt"
"log"
"time"
"carrot_bbs/internal/repository"
)
// UserActivityService 用户活跃统计服务接口
type UserActivityService interface {
// RecordUserActive 记录用户活跃(核心方法)
RecordUserActive(ctx context.Context, userID, loginType, ip, userAgent string) error
// GetDAU 获取日活用户数
GetDAU(ctx context.Context, date time.Time) (int64, error)
// GetWAU 获取周活用户数
GetWAU(ctx context.Context, year int, week int) (int64, error)
// GetMAU 获取月活用户数
GetMAU(ctx context.Context, year int, month int) (int64, error)
// GetRetention 计算留存率
// day1: 基准日期, dayN: N天后的日期
// 返回: day1的活跃用户在dayN仍然活跃的比例
GetRetention(ctx context.Context, day1, dayN time.Time) (float64, error)
// GetActivityStats 获取综合统计数据
GetActivityStats(ctx context.Context) (*ActivityStatsResponse, error)
}
// userActivityService 实现
type userActivityService struct {
repo *repository.UserActivityRepository
}
// NewUserActivityService 创建用户活跃统计服务
func NewUserActivityService(repo *repository.UserActivityRepository) UserActivityService {
return &userActivityService{repo: repo}
}
// ActivityStatsResponse 活跃统计响应
type ActivityStatsResponse struct {
Today DailyStats `json:"today"`
Yesterday DailyStats `json:"yesterday"`
ThisWeek PeriodStats `json:"this_week"`
ThisMonth PeriodStats `json:"this_month"`
Retention RetentionStats `json:"retention"`
}
// DailyStats 日统计
type DailyStats struct {
Date string `json:"date"`
DAU int64 `json:"dau"`
NewUsers int64 `json:"new_users"`
}
// PeriodStats 周期统计
type PeriodStats struct {
Period string `json:"period"`
ActiveUsers int64 `json:"active_users"`
}
// RetentionStats 留存统计
type RetentionStats struct {
Day1 float64 `json:"day_1"` // 次日留存
Day7 float64 `json:"day_7"` // 7日留存
Day30 float64 `json:"day_30"` // 30日留存
}
// RecordUserActive 记录用户活跃
func (s *userActivityService) RecordUserActive(ctx context.Context, userID, loginType, ip, userAgent string) error {
// 1. 记录到 Redis同步快速
if err := s.repo.RecordActivityToRedis(ctx, userID); err != nil {
// 记录日志但不阻断流程
log.Printf("failed to record activity to redis: %v", err)
}
// 2. 异步写入数据库(可以使用 goroutine 或消息队列)
go func() {
bgCtx := context.Background()
if err := s.repo.RecordActivity(bgCtx, userID, loginType, ip, userAgent); err != nil {
log.Printf("failed to record activity to db: %v", err)
}
}()
return nil
}
// GetDAU 获取日活用户数
func (s *userActivityService) GetDAU(ctx context.Context, date time.Time) (int64, error) {
return s.repo.GetDAUFromRedis(ctx, date)
}
// GetWAU 获取周活用户数
func (s *userActivityService) GetWAU(ctx context.Context, year int, week int) (int64, error) {
return s.repo.GetWAUFromRedis(ctx, year, week)
}
// GetMAU 获取月活用户数
func (s *userActivityService) GetMAU(ctx context.Context, year int, month int) (int64, error) {
return s.repo.GetMAUFromRedis(ctx, year, month)
}
// GetRetention 计算留存率
func (s *userActivityService) GetRetention(ctx context.Context, day1, dayN time.Time) (float64, error) {
// 1. 获取 day1 的活跃用户列表
day1Users, err := s.repo.GetDAUUserIDsFromRedis(ctx, day1)
if err != nil {
return 0, err
}
if len(day1Users) == 0 {
return 0, nil
}
// 2. 获取 dayN 的活跃用户列表
dayNUsers, err := s.repo.GetDAUUserIDsFromRedis(ctx, dayN)
if err != nil {
return 0, err
}
// 3. 计算交集
day1Set := make(map[string]bool)
for _, u := range day1Users {
day1Set[u] = true
}
retained := 0
for _, u := range dayNUsers {
if day1Set[u] {
retained++
}
}
// 4. 计算留存率(返回百分比形式 0-100
retention := float64(retained) / float64(len(day1Users)) * 100
return retention, nil
}
// GetActivityStats 获取综合统计数据
func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivityStatsResponse, error) {
now := time.Now()
// 获取今日和昨日的日期(零点)
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
yesterday := today.AddDate(0, 0, -1)
// 获取当前年周
year, week := now.ISOWeek()
// 并发获取各项统计数据
resp := &ActivityStatsResponse{}
// 获取今日 DAU
todayDAU, err := s.GetDAU(ctx, today)
if err != nil {
log.Printf("failed to get today DAU: %v", err)
}
resp.Today = DailyStats{
Date: today.Format("2006-01-02"),
DAU: todayDAU,
}
// 获取昨日 DAU
yesterdayDAU, err := s.GetDAU(ctx, yesterday)
if err != nil {
log.Printf("failed to get yesterday DAU: %v", err)
}
resp.Yesterday = DailyStats{
Date: yesterday.Format("2006-01-02"),
DAU: yesterdayDAU,
}
// 获取本周 WAU
wau, err := s.GetWAU(ctx, year, week)
if err != nil {
log.Printf("failed to get WAU: %v", err)
}
resp.ThisWeek = PeriodStats{
Period: fmt.Sprintf("%d-W%02d", year, week),
ActiveUsers: wau,
}
// 获取本月 MAU
mau, err := s.GetMAU(ctx, now.Year(), int(now.Month()))
if err != nil {
log.Printf("failed to get MAU: %v", err)
}
resp.ThisMonth = PeriodStats{
Period: now.Format("2006-01"),
ActiveUsers: mau,
}
// 获取留存率
// 次日留存
day1Retention, err := s.GetRetention(ctx, yesterday, today)
if err != nil {
log.Printf("failed to get day 1 retention: %v", err)
}
resp.Retention.Day1 = day1Retention
// 7日留存
day7Date := today.AddDate(0, 0, -7)
day7Retention, err := s.GetRetention(ctx, day7Date, today)
if err != nil {
log.Printf("failed to get day 7 retention: %v", err)
}
resp.Retention.Day7 = day7Retention
// 30日留存
day30Date := today.AddDate(0, 0, -30)
day30Retention, err := s.GetRetention(ctx, day30Date, today)
if err != nil {
log.Printf("failed to get day 30 retention: %v", err)
}
resp.Retention.Day30 = day30Retention
return resp, nil
}

54
internal/wire/casbin.go Normal file
View File

@@ -0,0 +1,54 @@
package wire
import (
"log"
"carrot_bbs/internal/cache"
"carrot_bbs/internal/config"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
"github.com/casbin/casbin/v2"
"github.com/google/wire"
"gorm.io/gorm"
)
// CasbinSet Casbin 相关 Provider Set
var CasbinSet = wire.NewSet(
ProvideCasbinEnforcer,
ProvideCasbinService,
ProvideRoleRepository,
)
// ProvideCasbinEnforcer 提供 Casbin Enforcer
func ProvideCasbinEnforcer(cfg *config.Config, db *gorm.DB) *casbin.Enforcer {
// 创建 Casbin Enforcer
// 注意:这里使用文件适配器作为示例,实际使用时需要替换为数据库适配器
enforcer, err := casbin.NewEnforcer(cfg.Casbin.ModelPath)
if err != nil {
log.Fatalf("Failed to create Casbin enforcer: %v", err)
}
// 加载策略
if err := enforcer.LoadPolicy(); err != nil {
log.Printf("[WARNING] Failed to load Casbin policy: %v", err)
}
log.Println("[Wire] Initialized Casbin enforcer")
return enforcer
}
// ProvideCasbinService 提供 Casbin 服务
func ProvideCasbinService(
enforcer *casbin.Enforcer,
cache cache.Cache,
roleRepo repository.RoleRepository,
cfg *config.Config,
) service.CasbinService {
return service.NewCasbinService(enforcer, cache, roleRepo, &cfg.Casbin)
}
// ProvideRoleRepository 提供角色仓储
func ProvideRoleRepository(db *gorm.DB) repository.RoleRepository {
return repository.NewRoleRepository(db)
}

View File

@@ -15,15 +15,16 @@ import (
// HandlerSet Handler 层 Provider Set
var HandlerSet = wire.NewSet(
// 直接使用构造函数的 Handler
handler.NewUserHandler,
handler.NewCommentHandler,
handler.NewNotificationHandler,
handler.NewUploadHandler,
handler.NewPushHandler,
handler.NewStickerHandler,
handler.NewVoteHandler,
handler.NewRoleHandler,
// 需要特殊处理的 Handler
ProvideUserHandler,
ProvidePostHandler,
ProvideMessageHandler,
ProvideSystemMessageHandler,
@@ -32,6 +33,14 @@ var HandlerSet = wire.NewSet(
ProvideScheduleHandler,
)
// ProvideUserHandler 提供用户处理器
func ProvideUserHandler(
userService service.UserService,
activityService service.UserActivityService,
) *handler.UserHandler {
return handler.NewUserHandler(userService, activityService)
}
// ProvidePostHandler 提供帖子处理器
func ProvidePostHandler(
postService service.PostService,

View File

@@ -5,6 +5,7 @@ import "github.com/google/wire"
// AllSet 包含所有 Provider Set
var AllSet = wire.NewSet(
InfrastructureSet,
CasbinSet,
RepositorySet,
ServiceSet,
HandlerSet,

View File

@@ -1,9 +1,11 @@
package wire
import (
"carrot_bbs/internal/cache"
"carrot_bbs/internal/repository"
"github.com/google/wire"
"gorm.io/gorm"
)
// RepositorySet Repository 层 Provider Set
@@ -20,8 +22,14 @@ var RepositorySet = wire.NewSet(
repository.NewStickerRepository,
repository.NewVoteRepository,
repository.NewScheduleRepository,
ProvideUserActivityRepository,
)
// ProvideUserActivityRepository 提供用户活跃数据仓储
func ProvideUserActivityRepository(db *gorm.DB, cache cache.Cache) *repository.UserActivityRepository {
return repository.NewUserActivityRepository(db, cache)
}
// Note: 以下 Repository 返回接口类型而非指针,需要单独处理
// - ScheduleRepository (repository.ScheduleRepository)
// - StickerRepository (repository.StickerRepository)

View File

@@ -41,6 +41,8 @@ var ServiceSet = wire.NewSet(
ProvideScheduleSyncService,
ProvideGroupService,
ProvideUploadService,
ProvideUserActivityService,
ProvideRoleService,
)
// ProvideJWTService 提供 JWT 服务
@@ -199,3 +201,16 @@ func ProvideUploadService(
) *service.UploadService {
return service.NewUploadService(s3Client, userService)
}
// ProvideUserActivityService 提供用户活跃统计服务
func ProvideUserActivityService(repo *repository.UserActivityRepository) service.UserActivityService {
return service.NewUserActivityService(repo)
}
// ProvideRoleService 提供角色管理服务
func ProvideRoleService(
roleRepo repository.RoleRepository,
casbinSvc service.CasbinService,
) service.RoleService {
return service.NewRoleService(roleRepo, casbinSvc)
}