613 lines
19 KiB
Go
613 lines
19 KiB
Go
package admin
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gtime"
|
||
|
||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||
"github.com/qianfree/team-api/internal/dao"
|
||
"github.com/qianfree/team-api/internal/logic/common"
|
||
do "github.com/qianfree/team-api/internal/model/do"
|
||
"github.com/qianfree/team-api/internal/model/entity"
|
||
"github.com/qianfree/team-api/internal/utility/export"
|
||
)
|
||
|
||
// audit levels — 委托给 common 包的常量
|
||
const (
|
||
AuditLevelFull = common.AuditLevelFull
|
||
AuditLevelFullText = common.AuditLevelFullText
|
||
AuditLevelMasked = common.AuditLevelMasked
|
||
AuditLevelQuestionOnly = common.AuditLevelQuestionOnly
|
||
AuditLevelNone = common.AuditLevelNone
|
||
)
|
||
|
||
// validAuditLevels defines the set of allowed audit level values.
|
||
var validAuditLevels = map[string]bool{
|
||
AuditLevelFull: true,
|
||
AuditLevelFullText: true,
|
||
AuditLevelMasked: true,
|
||
AuditLevelQuestionOnly: true,
|
||
AuditLevelNone: true,
|
||
}
|
||
|
||
// GetGlobalAuditConfig retrieves the global audit level from sys_options.
|
||
// Package-level wrapper for use by tenant package.
|
||
func GetGlobalAuditConfig(ctx context.Context) (string, error) {
|
||
return common.GetAuditLevel(ctx), nil
|
||
}
|
||
|
||
// GetAuditConfig retrieves the global audit level from sys_options.
|
||
func (s *sAdmin) GetAuditConfig(ctx context.Context, _ *v1.AuditConfigGetReq) (*v1.AuditConfigGetRes, error) {
|
||
return &v1.AuditConfigGetRes{AuditLevel: common.GetAuditLevel(ctx)}, nil
|
||
}
|
||
|
||
// UpdateAuditConfig updates the global audit level.
|
||
func (s *sAdmin) UpdateAuditConfig(ctx context.Context, req *v1.AuditConfigUpdateReq) (*v1.AuditConfigUpdateRes, error) {
|
||
if !validAuditLevels[req.AuditLevel] {
|
||
return nil, common.NewBadRequestError("无效的审计级别,可选值:full, full_text, masked, question_only, none")
|
||
}
|
||
|
||
// Upsert into sys_options
|
||
count, err := dao.SysOptions.Ctx(ctx).
|
||
Where("key", "audit_level").Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if count > 0 {
|
||
_, err = dao.SysOptions.Ctx(ctx).
|
||
Where("key", "audit_level").
|
||
Data(do.SysOptions{
|
||
Value: req.AuditLevel,
|
||
}).
|
||
Update()
|
||
} else {
|
||
_, err = dao.SysOptions.Ctx(ctx).
|
||
Data(do.SysOptions{
|
||
Key: "audit_level",
|
||
Value: req.AuditLevel,
|
||
Description: "全局审计级别",
|
||
Category: "security",
|
||
IsPublic: false,
|
||
}).
|
||
Insert()
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Invalidate cache
|
||
common.Config().SetOption(ctx, "audit_level", req.AuditLevel)
|
||
return nil, nil
|
||
}
|
||
|
||
// queryPage 使用 GoFrame ORM 分页查询,返回 []map[string]any。
|
||
// 使用主库查询审计表(aud_request_logs 之外的审计表数据量小,无需独立库)。
|
||
func queryPage(ctx context.Context, table, fields, where, orderBy string, page, pageSize int, args ...any) ([]map[string]any, int, error) {
|
||
m := g.DB().Ctx(ctx).Model(table).Safe()
|
||
if where != "" {
|
||
m = m.Where(where, args...)
|
||
}
|
||
|
||
total, err := m.Count()
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if total == 0 {
|
||
return []map[string]any{}, 0, nil
|
||
}
|
||
|
||
if fields != "" {
|
||
m = m.Fields(fields)
|
||
}
|
||
if orderBy != "" {
|
||
m = m.Order(orderBy)
|
||
}
|
||
|
||
result, err := m.Page(page, pageSize).All()
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return toMapList(result), total, nil
|
||
}
|
||
|
||
// toMapList converts database records to JSON-safe maps.
|
||
func toMapList(result gdb.Result) []map[string]any {
|
||
if len(result) == 0 {
|
||
return []map[string]any{}
|
||
}
|
||
list := make([]map[string]any, 0, len(result))
|
||
for _, row := range result {
|
||
list = append(list, row.Map())
|
||
}
|
||
return list
|
||
}
|
||
|
||
// queryAuditPage 查询审计表分页数据,自动计数、分页、转换并批量填充关联名称。
|
||
// 用于 ListRequestAuditLogs 和 ContentFilterLogList 的公共分页逻辑。
|
||
func queryAuditPage(ctx context.Context, db gdb.DB, table, fields, where string, args []any, page, pageSize int) ([]map[string]any, int, error) {
|
||
countM := db.Ctx(ctx).Model(table).Safe()
|
||
if where != "" {
|
||
countM = countM.Where(where, args...)
|
||
}
|
||
total, err := countM.Count()
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if total == 0 {
|
||
return []map[string]any{}, 0, nil
|
||
}
|
||
|
||
dataM := db.Ctx(ctx).Model(table).Safe().Fields(fields).OrderDesc("created_at").Page(page, pageSize)
|
||
if where != "" {
|
||
dataM = dataM.Where(where, args...)
|
||
}
|
||
result, err := dataM.All()
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
items := make([]map[string]any, 0, len(result))
|
||
for _, row := range result {
|
||
m := make(map[string]any, len(row))
|
||
for k, v := range row {
|
||
m[k] = v.Val()
|
||
}
|
||
items = append(items, m)
|
||
}
|
||
|
||
common.EnrichAuditRecords(ctx, items)
|
||
return items, total, nil
|
||
}
|
||
|
||
// ListOperationLogs retrieves a paginated list of operation logs with optional filters.
|
||
func (s *sAdmin) ListOperationLogs(ctx context.Context, req *v1.OperationLogListReq) (*v1.OperationLogListRes, error) {
|
||
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||
|
||
var conditions []string
|
||
var args []any
|
||
|
||
if req.UserID > 0 {
|
||
conditions = append(conditions, "user_id = ?")
|
||
args = append(args, int64(req.UserID))
|
||
}
|
||
if req.UserType != "" {
|
||
conditions = append(conditions, "user_type = ?")
|
||
args = append(args, req.UserType)
|
||
}
|
||
if req.Action != "" {
|
||
conditions = append(conditions, "action LIKE ?")
|
||
args = append(args, "%"+req.Action+"%")
|
||
}
|
||
if req.StartDate != "" {
|
||
conditions = append(conditions, "created_at >= ?")
|
||
args = append(args, req.StartDate+" 00:00:00")
|
||
}
|
||
if req.EndDate != "" {
|
||
conditions = append(conditions, "created_at <= ?")
|
||
args = append(args, req.EndDate+" 23:59:59")
|
||
}
|
||
|
||
where := strings.Join(conditions, " AND ")
|
||
items, total, err := queryPage(ctx,
|
||
"aud_operation_logs",
|
||
"id, tenant_id, user_id, user_type, action, resource_type, resource_id, detail, changes_json, ip_address, created_at",
|
||
where, "created_at DESC", page, pageSize, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &v1.OperationLogListRes{
|
||
List: items,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// ListSensitiveAccessLogs retrieves a paginated list of sensitive data access logs.
|
||
func (s *sAdmin) ListSensitiveAccessLogs(ctx context.Context, req *v1.SensitiveLogListReq) (*v1.SensitiveLogListRes, error) {
|
||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||
|
||
var conditions []string
|
||
var args []any
|
||
|
||
if req.UserID > 0 {
|
||
conditions = append(conditions, "user_id = ?")
|
||
args = append(args, int64(req.UserID))
|
||
}
|
||
if req.ResourceType != "" {
|
||
conditions = append(conditions, "resource_type = ?")
|
||
args = append(args, req.ResourceType)
|
||
}
|
||
if req.StartDate != "" {
|
||
conditions = append(conditions, "created_at >= ?")
|
||
args = append(args, req.StartDate+" 00:00:00")
|
||
}
|
||
if req.EndDate != "" {
|
||
conditions = append(conditions, "created_at <= ?")
|
||
args = append(args, req.EndDate+" 23:59:59")
|
||
}
|
||
|
||
where := strings.Join(conditions, " AND ")
|
||
items, total, err := queryPage(ctx,
|
||
"aud_sensitive_access_logs",
|
||
"id, user_id, user_type, resource_type, resource_id, action, reason, ip_address, user_agent, created_at",
|
||
where, "created_at DESC", page, pageSize, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &v1.SensitiveLogListRes{
|
||
List: items,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// MaskSensitiveData 委托给 common.MaskSensitiveData
|
||
func MaskSensitiveData(data string) string {
|
||
return common.MaskSensitiveData(data)
|
||
}
|
||
|
||
// ListRequestAuditLogs 分页查询请求审计日志(不返回 request_body/response_body 以优化性能)
|
||
// 审计数据从审计库查询,关联信息(用户名、租户名等)从主库批量查询后在应用层合并。
|
||
func (s *sAdmin) ListRequestAuditLogs(ctx context.Context, req *v1.RequestAuditLogListReq) (*v1.RequestAuditLogListRes, error) {
|
||
if err := common.ValidateDateTimeParam(req.StartDate, "开始时间"); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := common.ValidateDateTimeParam(req.EndDate, "结束时间"); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||
|
||
var conditions []string
|
||
var args []any
|
||
|
||
if req.ID > 0 {
|
||
conditions = append(conditions, "id = ?")
|
||
args = append(args, req.ID)
|
||
}
|
||
if req.TenantID > 0 {
|
||
conditions = append(conditions, "tenant_id = ?")
|
||
args = append(args, int64(req.TenantID))
|
||
}
|
||
if req.UserID > 0 {
|
||
conditions = append(conditions, "user_id = ?")
|
||
args = append(args, req.UserID)
|
||
}
|
||
if req.ApiKeyID > 0 {
|
||
conditions = append(conditions, "api_key_id = ?")
|
||
args = append(args, int64(req.ApiKeyID))
|
||
}
|
||
// 用户名筛选:先从主库查匹配的 user_id 集合,转为 IN 条件
|
||
if req.Username != "" {
|
||
type userRow struct {
|
||
Id int64 `json:"id"`
|
||
TenantId int64 `json:"tenant_id"`
|
||
}
|
||
var users []userRow
|
||
err := g.DB().Ctx(ctx).Model("tnt_users").
|
||
Where("username LIKE ?", "%"+req.Username+"%").
|
||
Fields("id, tenant_id").
|
||
Scan(&users)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(users) == 0 {
|
||
return &v1.RequestAuditLogListRes{
|
||
List: []map[string]any{}, Total: 0, Page: page, PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
// 构建 (tenant_id, user_id) IN 条件
|
||
userConditions := make([]string, 0, len(users))
|
||
for _, u := range users {
|
||
userConditions = append(userConditions, fmt.Sprintf("(tenant_id = %d AND user_id = %d)", u.TenantId, u.Id))
|
||
}
|
||
conditions = append(conditions, "("+strings.Join(userConditions, " OR ")+")")
|
||
}
|
||
if req.RequestId != "" {
|
||
conditions = append(conditions, "request_id = ?")
|
||
args = append(args, req.RequestId)
|
||
}
|
||
if req.TaskId != "" {
|
||
conditions = append(conditions, "task_id = ?")
|
||
args = append(args, req.TaskId)
|
||
}
|
||
if req.Method != "" {
|
||
conditions = append(conditions, "method = ?")
|
||
args = append(args, req.Method)
|
||
}
|
||
if req.Path != "" {
|
||
conditions = append(conditions, "path LIKE ?")
|
||
args = append(args, "%"+req.Path+"%")
|
||
}
|
||
if req.Model != "" {
|
||
conditions = append(conditions, "model_name = ?")
|
||
args = append(args, req.Model)
|
||
}
|
||
if req.StatusCode > 0 {
|
||
conditions = append(conditions, "status_code = ?")
|
||
args = append(args, req.StatusCode)
|
||
}
|
||
if req.StartDate != "" {
|
||
conditions = append(conditions, "created_at >= ?")
|
||
args = append(args, common.StartOfRange(req.StartDate))
|
||
}
|
||
if req.EndDate != "" {
|
||
conditions = append(conditions, "created_at <= ?")
|
||
args = append(args, common.EndOfRange(req.EndDate))
|
||
}
|
||
|
||
where := strings.Join(conditions, " AND ")
|
||
// 列表白名单:表格列 + EnrichAuditRecords 回填名称所需的关联键。
|
||
// user_id / project_id 仅作关联查询键(username / project_name 据此回填),表格不展示,出参剔除;
|
||
// request_id / query_params / user_agent 等列表不展示的字段经详情接口获取。
|
||
auditFields := "id, tenant_id, user_id, api_key_id, project_id, method, path, model_name, status_code, client_ip, latency_ms, first_token_ms, audit_level, created_at"
|
||
|
||
items, total, err := queryAuditPage(ctx, common.GetAuditDB(), "aud_request_logs", auditFields, where, args, page, pageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, item := range items {
|
||
delete(item, "user_id")
|
||
delete(item, "project_id")
|
||
}
|
||
|
||
return &v1.RequestAuditLogListRes{
|
||
List: items,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// GetRequestAuditLogDetail 查询单条请求审计日志详情(含完整 request_body 和 response_body)
|
||
func (s *sAdmin) GetRequestAuditLogDetail(ctx context.Context, req *v1.RequestAuditLogDetailReq) (*v1.RequestAuditLogDetailRes, error) {
|
||
var record *entity.AudRequestLogs
|
||
err := common.AuditModelCtx(ctx, "aud_request_logs").
|
||
Where("id", req.Id).
|
||
Scan(&record)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if record == nil {
|
||
return nil, common.NewNotFoundError("审计日志")
|
||
}
|
||
// 详情含请求/响应体(full 级别下为明文对话内容),属敏感读取,落留痕
|
||
common.LogSensitiveAccess(ctx, common.GetCtxUserID(ctx), "admin", "request_log", req.Id, "view", "查看请求审计日志详情")
|
||
|
||
b, _ := json.Marshal(record)
|
||
var detail map[string]any
|
||
_ = json.Unmarshal(b, &detail)
|
||
return &v1.RequestAuditLogDetailRes{Data: detail}, nil
|
||
}
|
||
|
||
// ForwardingTraceGet 按 request_id 查询请求转发路径追踪(仅管理员,用量日志详情懒加载用)。
|
||
// 从审计库查询 aud_request_logs(可能独立于主库),仅取 forwarding_trace 与 audit_level 两列。
|
||
// 一个 request_id 仅对应一条审计记录(任务完成审计为 UPDATE 非 INSERT),OrderDesc 取最新为防御性兜底。
|
||
func (s *sAdmin) ForwardingTraceGet(ctx context.Context, req *v1.ForwardingTraceGetReq) (*v1.ForwardingTraceGetRes, error) {
|
||
var row struct {
|
||
ForwardingTrace string `orm:"forwarding_trace"`
|
||
AuditLevel string `orm:"audit_level"`
|
||
}
|
||
err := common.AuditModelCtx(ctx, "aud_request_logs").
|
||
Where("request_id", req.RequestId).
|
||
OrderDesc("created_at").
|
||
Fields("forwarding_trace, audit_level").
|
||
Scan(&row)
|
||
// Scan(&struct) 无匹配记录时返回 sql.ErrNoRows,归一化为 nil,
|
||
// 让下方「无审计记录 → found=false」分支生效,而不是把 NoRows 当错误返回。
|
||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||
return nil, err
|
||
}
|
||
// 无审计记录(两级审计级别均为 none 时不会写入)→ found=false,前端据此展示「未开启审计」
|
||
if row.ForwardingTrace == "" && row.AuditLevel == "" {
|
||
return &v1.ForwardingTraceGetRes{Found: false}, nil
|
||
}
|
||
res := &v1.ForwardingTraceGetRes{Found: true, AuditLevel: row.AuditLevel}
|
||
// forwarding_trace 为 "null" 字符串(JSONB NULL 序列化)或空时不解析
|
||
if row.ForwardingTrace != "" && row.ForwardingTrace != "null" {
|
||
var parsed map[string]any
|
||
if json.Unmarshal([]byte(row.ForwardingTrace), &parsed) == nil {
|
||
res.ForwardingTrace = parsed
|
||
}
|
||
}
|
||
return res, nil
|
||
}
|
||
|
||
// ExportOperationLogs exports operation logs to CSV or Excel.
|
||
func (s *sAdmin) ExportOperationLogs(ctx context.Context, req *v1.OperationLogExportReq) (*v1.OperationLogExportRes, error) {
|
||
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
buildOpLogWhere := func() (string, []any) {
|
||
var conditions []string
|
||
var args []any
|
||
if req.UserID > 0 {
|
||
conditions = append(conditions, "user_id = ?")
|
||
args = append(args, int64(req.UserID))
|
||
}
|
||
if req.UserType != "" {
|
||
conditions = append(conditions, "user_type = ?")
|
||
args = append(args, req.UserType)
|
||
}
|
||
if req.Action != "" {
|
||
conditions = append(conditions, "action LIKE ?")
|
||
args = append(args, "%"+req.Action+"%")
|
||
}
|
||
if req.StartDate != "" {
|
||
conditions = append(conditions, "created_at >= ?")
|
||
args = append(args, req.StartDate+" 00:00:00")
|
||
}
|
||
if req.EndDate != "" {
|
||
conditions = append(conditions, "created_at <= ?")
|
||
args = append(args, req.EndDate+" 23:59:59")
|
||
}
|
||
where := ""
|
||
if len(conditions) > 0 {
|
||
where = strings.Join(conditions, " AND ")
|
||
}
|
||
return where, args
|
||
}
|
||
|
||
selectFields := "id, user_id, user_type, action, resource_type, resource_id, ip_address, detail, created_at"
|
||
|
||
config := export.Config{
|
||
Format: req.Format,
|
||
Filename: "操作日志_" + gtime.Now().Format("Ymd_His"),
|
||
Columns: []export.Column{
|
||
{Field: "id", Header: "ID"},
|
||
{Field: "user_id", Header: "用户ID"},
|
||
{Field: "user_type", Header: "用户类型"},
|
||
{Field: "action", Header: "操作"},
|
||
{Field: "resource_type", Header: "目标类型"},
|
||
{Field: "resource_id", Header: "目标ID"},
|
||
{Field: "ip_address", Header: "IP地址"},
|
||
{Field: "detail", Header: "详情"},
|
||
{Field: "created_at", Header: "创建时间"},
|
||
},
|
||
}
|
||
|
||
return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) {
|
||
offset := 0
|
||
for {
|
||
where, args := buildOpLogWhere()
|
||
q := g.DB().Ctx(ctx).Model("aud_operation_logs").Fields(selectFields).OrderDesc("created_at").Offset(offset).Limit(1000)
|
||
if where != "" {
|
||
q = q.Where(where, args...)
|
||
}
|
||
result, err := q.All()
|
||
if err != nil {
|
||
return
|
||
}
|
||
items := make([]map[string]any, 0, len(result))
|
||
for _, row := range result {
|
||
m := make(map[string]any, len(row))
|
||
for k, v := range row {
|
||
m[k] = v.Val()
|
||
}
|
||
items = append(items, m)
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
for _, item := range items {
|
||
createdAt := ""
|
||
if v, ok := item["created_at"]; ok {
|
||
createdAt = fmt.Sprintf("%v", v)
|
||
}
|
||
if !yield(map[string]any{
|
||
"id": item["id"],
|
||
"user_id": item["user_id"],
|
||
"user_type": item["user_type"],
|
||
"action": item["action"],
|
||
"resource_type": item["resource_type"],
|
||
"resource_id": item["resource_id"],
|
||
"ip_address": item["ip_address"],
|
||
"detail": item["detail"],
|
||
"created_at": createdAt,
|
||
}) {
|
||
return
|
||
}
|
||
}
|
||
if len(items) < 1000 {
|
||
break
|
||
}
|
||
offset += 1000
|
||
}
|
||
})
|
||
}
|
||
|
||
// ContentFilterLogList returns a paginated list of content filter interception logs.
|
||
// 审计数据从审计库查询,关联信息从主库批量查询后在应用层合并。
|
||
func (s *sAdmin) ContentFilterLogList(ctx context.Context, req *v1.ContentFilterLogListReq) (*v1.ContentFilterLogListRes, error) {
|
||
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||
|
||
var conditions []string
|
||
var args []any
|
||
|
||
if req.TenantID > 0 {
|
||
conditions = append(conditions, "tenant_id = ?")
|
||
args = append(args, int64(req.TenantID))
|
||
}
|
||
if req.Mode != "" {
|
||
conditions = append(conditions, "filter_mode = ?")
|
||
args = append(args, req.Mode)
|
||
}
|
||
if req.Blocked == "true" {
|
||
conditions = append(conditions, "blocked = true")
|
||
} else if req.Blocked == "false" {
|
||
conditions = append(conditions, "blocked = false")
|
||
}
|
||
if req.StartDate != "" {
|
||
conditions = append(conditions, "created_at >= ?")
|
||
args = append(args, req.StartDate+" 00:00:00")
|
||
}
|
||
if req.EndDate != "" {
|
||
conditions = append(conditions, "created_at <= ?")
|
||
args = append(args, req.EndDate+" 23:59:59")
|
||
}
|
||
if req.Keyword != "" {
|
||
conditions = append(conditions, "(matched_words::text ILIKE ? OR path ILIKE ?)")
|
||
kw := "%" + req.Keyword + "%"
|
||
args = append(args, kw, kw)
|
||
}
|
||
|
||
where := strings.Join(conditions, " AND ")
|
||
cfFields := "id, tenant_id, user_id, api_key_id, project_id, request_id, method, path, client_ip, filter_mode, matched_words, original_snippet, blocked, created_at"
|
||
|
||
items, total, err := queryAuditPage(ctx, g.DB(), "aud_content_filter_logs", cfFields, where, args, page, pageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &v1.ContentFilterLogListRes{
|
||
List: items,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// ContentFilterLogClear 硬删除全部内容过滤拦截日志。
|
||
// 拦截日志为追加型审计流水,堆积后用 TRUNCATE 秒级清空并立即归还磁盘空间
|
||
// (DELETE 需等 VACUUM 回收)。TRUNCATE 不返回行数,故先取删除前条数作为反馈。
|
||
// 表无数据库级外键,TRUNCATE 安全。
|
||
func (s *sAdmin) ContentFilterLogClear(ctx context.Context, _ *v1.ContentFilterLogClearReq) (*v1.ContentFilterLogClearRes, error) {
|
||
count, err := dao.AudContentFilterLogs.Ctx(ctx).Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := g.DB().Ctx(ctx).Exec(ctx, "TRUNCATE TABLE aud_content_filter_logs RESTART IDENTITY"); err != nil {
|
||
return nil, err
|
||
}
|
||
return &v1.ContentFilterLogClearRes{Deleted: int64(count)}, nil
|
||
}
|