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:
@@ -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 审核回调处理
|
||||
|
||||
225
internal/service/casbin_service.go
Normal file
225
internal/service/casbin_service.go
Normal 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
|
||||
}
|
||||
116
internal/service/role_service.go
Normal file
116
internal/service/role_service.go
Normal 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)
|
||||
}
|
||||
223
internal/service/user_activity_service.go
Normal file
223
internal/service/user_activity_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user