win f25dd04e0b
Some checks failed
CI / test (push) Failing after 1m31s
CI / golangci-lint (push) Failing after 3s
Security Scan / backend-security (push) Failing after 3s
Security Scan / frontend-security (push) Failing after 2s
feat(risk): 风控数据管道与风控中心
- DB Migration 081: 新增 account_behavior_hourly / account_risk_scores 表
- 行为采集:Gateway/OpenAI Gateway RecordUsage 注入 fire-and-forget CollectBehaviorAsync
- SQL 打分引擎:CTE 加权特征向量 → risk_score [0-1],UPSERT 保留 idle_override
- RiskSettings:Redis 缓存 → DB fallback → 默认值(observe 模式)
- REST API:/admin/risk/summary|accounts|accounts/:id|settings
- 前端:Pinia store + RiskControlView + 6 子组件(donut/radar/line 纯 SVG 图表)
- 侧边栏新增 Risk Control 入口(ShieldExclamationIcon)
- 反风控优化:移除 Antigravity 后台定时刷新,改为按需刷新避免 idle 封号
2026-03-28 03:07:17 +08:00

115 lines
2.6 KiB
Go

package admin
import (
"strconv"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
type RiskHandler struct {
service *service.RiskService
}
func NewRiskHandler(svc *service.RiskService) *RiskHandler {
return &RiskHandler{service: svc}
}
func (h *RiskHandler) GetSummary(c *gin.Context) {
summary, err := h.service.GetSummary(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, summary)
}
func (h *RiskHandler) ListAccounts(c *gin.Context) {
filter := service.RiskAccountFilter{
Level: c.Query("risk_level"),
Platform: c.Query("platform"),
}
if p := c.Query("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil {
filter.Page = v
}
}
if l := c.Query("limit"); l != "" {
if v, err := strconv.Atoi(l); err == nil {
filter.PageSize = v
}
}
list, err := h.service.ListAccounts(c.Request.Context(), filter)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, list)
}
func (h *RiskHandler) GetAccountDetail(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
response.ErrorFrom(c, service.ErrRiskAccountNotFound)
return
}
detail, err := h.service.GetAccountDetail(c.Request.Context(), id)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, detail)
}
type overrideRiskLevelRequest struct {
Level string `json:"level" binding:"required"`
Reason string `json:"reason" binding:"required"`
}
func (h *RiskHandler) OverrideRiskLevel(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
response.ErrorFrom(c, service.ErrRiskAccountNotFound)
return
}
var req overrideRiskLevelRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorFrom(c, err)
return
}
if err := h.service.OverrideRiskLevel(c.Request.Context(), id, req.Level, req.Reason); err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, nil)
}
func (h *RiskHandler) GetSettings(c *gin.Context) {
settings, err := h.service.GetSettings(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, settings)
}
func (h *RiskHandler) UpdateSettings(c *gin.Context) {
var req service.RiskSettings
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorFrom(c, err)
return
}
updated, err := h.service.UpdateSettings(c.Request.Context(), &req)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, updated)
}