Browse Source

feat(admin): 添加 WAF 日志导出功能

- 新增 WafLog 导出相关结构体和接口
- 实现 WafLog 导出的数据库查询逻辑
- 添加 WafLog 导出的服务层处理逻辑
-优化 WafLog 查询性能,支持多条件筛选
fusu 5 days ago
parent
commit
c21ce371b3
4 changed files with 278 additions and 1 deletions
  1. 64 0
      GEMINI.md
  2. 31 0
      api/v1/admin/wafLog.go
  3. 92 1
      internal/repository/admin/waflog.go
  4. 91 0
      internal/service/admin/waflog.go

+ 64 - 0
GEMINI.md

@@ -0,0 +1,64 @@
+# 项目分析总结:nunu-layout-advanced
+请用中文进行回答和注释
+
+本文档是对 `nunu-layout-advanced` 项目的全面分析和总结。
+
+## 1. 项目概述
+
+`nunu-layout-advanced` 是一个基于 Go 语言和 `nunu` 框架构建的后端项目。从其结构和文档来看,这是一个功能完备、考虑了工程化实践(如部署、测试、文档)的高级项目模板或实际应用。项目同时支持 API 服务和后台任务(Task)服务,并提供了非常便捷的 Docker 化部署方案。
+
+## 2. 技术栈
+
+项目采用了现代化的 Go 技术栈,涵盖了 Web 开发的各个方面:
+
+*   **Web 框架**: `Gin` (`github.com/gin-gonic/gin`) - 高性能的 HTTP Web 框架。
+*   **数据库 ORM**: `GORM` (`gorm.io/gorm`) - 功能强大的 Go ORM,支持 MySQL, PostgreSQL, SQLite。
+*   **NoSQL 数据库**: `MongoDB` (`go.mongodb.org/mongo-driver`, `github.com/qiniu/qmgo`) - 支持 MongoDB 数据库操作。
+*   **缓存**: `Redis` (`github.com/redis/go-redis/v9`) - 用于缓存和高速数据存储。
+*   **配置管理**: `Viper` (`github.com/spf13/viper`) - 支持多种格式的配置文件和环境变量。
+*   **日志**: `Zap` (`go.uber.org/zap`) - 高性能的结构化日志库。
+*   **认证与授权**:
+    *   `JWT` (`github.com/golang-jwt/jwt/v5`) - 用于生成和验证 Token。
+    *   `Casbin` (`github.com/casbin/casbin/v2`) - 强大的访问控制库,支持 ACL, RBAC, ABAC 等模型。
+*   **定时任务**: `gocron` (`github.com/go-co-op/gocron`) - 用于处理定时和周期性任务。
+*   **消息队列**: `RabbitMQ` (`github.com/rabbitmq/amqp091-go`) - 用于服务间的异步通信和解耦。
+*   **依赖注入**: `Wire` (`github.com/google/wire`) - Google 出品的编译期依赖注入工具。
+*   **API 文档**: `Swagger` (`github.com/swaggo/gin-swagger`) - 自动生成交互式 API 文档。
+*   **数据校验**: `validator/v10` (`github.com/go-playground/validator/v10`) - 用于结构体验证。
+
+## 3. 核心功能推断
+
+根据技术栈分析,项目具备以下核心能力:
+
+*   **RESTful API 服务**: 基于 Gin 提供高性能的 API 接口。
+*   **后台异步任务**: 通过 `gocron` 和 `RabbitMQ` 处理耗时或定时的后台任务。
+*   **多数据库支持**: 同时支持 SQL (MySQL, PostgreSQL) 和 NoSQL (MongoDB) 数据库,并通过 `dbresolver` 插件可能实现了读写分离。
+*   **完善的用户认证和权限管理**: 结合 JWT 和 Casbin,可以实现复杂的用户角色和权限控制。
+*   **配置热加载**: Viper 支持在不重启服务的情况下更新配置。
+*   **自动化测试**: 集成了 `testify`, `sqlmock` 等库,便于编写单元测试和集成测试。
+*   **高可用 ID 生成**: 使用 `sonyflake` 生成分布式唯一 ID。
+
+## 4. 部署与运维
+
+项目对部署非常友好,主要特点如下:
+
+*   **Docker 化部署**: 提供了 `Dockerfile` 和详细的 `docker build/run` 命令,是推荐的部署方式。
+*   **自动化部署脚本**: `deploy/deploy.sh` 脚本可以一键部署 API 和 Task 服务,并优化了构建缓存,提高了部署效率。
+*   **宝塔面板集成**: `README.md` 详细说明了如何在宝塔面板上进行 Docker 部署和配置反向代理。
+*   **环境分离**: 支持将配置文件、日志文件等通过 Docker Volume 挂载到宿主机,方便管理和持久化。
+
+## 5. 项目结构
+
+项目遵循了清晰的、可扩展的 Go 项目布局:
+
+*   `cmd/`: 程序入口,分离 `server` (API) 和 `task` 等不同应用。
+*   `internal/`: 项目核心业务逻辑,遵循 Go 的项目布局建议,外部无法直接导入。
+*   `pkg/`: 可被外部应用复用的公共库。
+*   `api/`: `protobuf` 或 `swagger` 定义文件。
+*   `config/`: 默认配置文件。
+*   `deploy/`: 存放 Dockerfile 和部署脚本。
+*   `web/`: 可能存放前端静态资源或模板。
+
+## 总结
+
+`nunu-layout-advanced` 是一个非常优秀的 Go 项目脚手架,集成了业界主流的最佳实践。它不仅提供了一个健壮的技术基础,还通过完善的文档和脚本极大地简化了开发和部署流程。无论是用于学习 Go 的工程化实践,还是作为新项目的起点,它都是一个绝佳的选择。

+ 31 - 0
api/v1/admin/wafLog.go

@@ -1,5 +1,7 @@
 package admin
 
+import "time"
+
 type WafLog struct {
 	Id         int    `json:"id" form:"id" gorm:"column:id;primary_key;AUTO_INCREMENT;not null"`
 	Uid        int    `json:"uid" form:"uid" gorm:"column:uid;default:0;not null"`
@@ -33,4 +35,33 @@ type SearchWafLogParams struct {
 
 type WafLogId struct {
 	Id int `json:"id" form:"id" validate:"required,min=1"`
+}
+
+
+type ExportWafLog struct {
+	Id         int    `json:"id" form:"id" gorm:"column:id;primary_key;AUTO_INCREMENT;not null"`
+	Uid        int    `json:"uid" form:"uid" gorm:"column:uid;default:0;not null"`
+	Name       string `json:"name" form:"name" gorm:"column:name"`
+	RequestIp  string `json:"requestIp" form:"requestIp" gorm:"column:request_ip"`
+	RuleId     int    `json:"ruleId" form:"ruleId" gorm:"column:rule_id;default:0"`
+	HostIds     []int    `json:"hostIds" form:"hostIds" gorm:"column:host_id;default:0"`
+	UserAgent  string `json:"userAgent" form:"userAgent" gorm:"column:user_agent"`
+	Api        string `json:"api" form:"api" gorm:"column:api"`
+	ApiNames    []string `json:"apiNames" form:"apiNames" gorm:"column:api_name"`
+	ApiTypes    []string `json:"apiTypes" form:"apiTypes" gorm:"column:api_type"`
+	StartTime  string `json:"startTime" form:"startTime" gorm:"column:start_time"`
+	EndTime    string `json:"endTime" form:"endTime" gorm:"column:end_time"`
+}
+
+type ExportWafLogRes struct {
+	Name       string `json:"name" form:"name"`
+	RequestIp  string `json:"requestIp" form:"requestIp"`
+	HostId     int    `json:"hostId" form:"hostId"`
+	ApiName    string `json:"apiName" form:"apiName"`
+	AddrBackendList  interface{} `json:"addrBackendList" form:"addrBackendList"`
+	Domain     string `json:"domain" form:"domain"`
+	CustomHost string `json:"customHost" form:"customHost"`
+	ExposeAddr  []string `json:"exposeAddr" form:"exposeAddr"`
+	Comment    string `json:"comment" form:"comment"`
+	CreatedAt  time.Time `json:"createdAt" form:"createdAt"`
 }

+ 92 - 1
internal/repository/admin/waflog.go

@@ -1,13 +1,14 @@
 package admin
 
 import (
-    "context"
+	"context"
 	v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
 	adminApi "github.com/go-nunu/nunu-layout-advanced/api/v1/admin"
 	"github.com/go-nunu/nunu-layout-advanced/internal/model"
 	"github.com/go-nunu/nunu-layout-advanced/internal/repository"
 	"math"
 	"strings"
+	"time"
 )
 
 type WafLogRepository interface {
@@ -15,6 +16,10 @@ type WafLogRepository interface {
 	GetWafLogList(ctx context.Context, req adminApi.SearchWafLogParams) (*v1.PaginatedResponse[model.WafLog], error)
 	AddWafLog(ctx context.Context, log *model.WafLog) error
 	BatchAddWafLog(ctx context.Context, logs []*model.WafLog) error
+	// 导出日志
+	ExportWafLog(ctx context.Context, req adminApi.ExportWafLog) ([]model.WafLog, error)
+	// 获取网关组记录
+	GetWafLogGateWayIp(ctx context.Context, hostId int64, Uid int64,createdAt time.Time) (model.WafLog, error)
 }
 
 func NewWafLogRepository(
@@ -148,3 +153,89 @@ func (r *wafLogRepository) BatchAddWafLog(ctx context.Context, logs []*model.Waf
 	}
 	return r.DBWithName(ctx, "admin").CreateInBatches(logs, len(logs)).Error
 }
+
+func (r *wafLogRepository) ExportWafLog(ctx context.Context, req adminApi.ExportWafLog) ([]model.WafLog, error) {
+	var res []model.WafLog
+	query := r.DBWithName(ctx,"admin").Model(&model.WafLog{})
+	if  req.RequestIp != "" {
+		trimmedName := strings.TrimSpace(req.RequestIp)
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("request_ip = ?", trimmedName)
+	}
+
+
+	if req.Uid != 0 {
+		query = query.Where("uid = ?", req.Uid)
+	}
+
+	if req.Api != "" {
+		trimmedName := strings.TrimSpace(req.Api)
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("api = ?", trimmedName)
+	}
+
+	if req.Name != "" {
+		trimmedName := strings.TrimSpace(req.Name)
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("name = ?", trimmedName)
+	}
+
+
+	if req.RuleId != 0 {
+		query = query.Where("rule_id = ?", req.RuleId)
+	}
+
+	if len(req.HostIds) > 0 {
+		query = query.Where("host_id IN ?", req.HostIds)
+	}
+
+	if req.Api != "" {
+		trimmedName := strings.TrimSpace(req.Api)
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("api = ?", trimmedName)
+	}
+
+	if req.UserAgent != "" {
+		trimmedName := strings.TrimSpace(req.UserAgent)
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("user_agent = ?", trimmedName)
+	}
+
+	if len(req.ApiNames) > 0 {
+		trimmedNames := make([]string, len(req.ApiNames))
+		for _, apiName := range req.ApiNames {
+			trimmedNames = append(trimmedNames, strings.TrimSpace(apiName))
+		}
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("api_name IN ?", trimmedNames)
+	}
+
+	if len(req.ApiTypes) > 0 {
+		query = query.Where("api_type IN ?", req.ApiTypes)
+	}
+
+
+	if req.StartTime != "" {
+		trimmedName := strings.TrimSpace(req.StartTime)
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("create_at > ?", trimmedName)
+	}
+
+	if req.EndTime != "" {
+		trimmedName := strings.TrimSpace(req.EndTime)
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("create_at < ?", trimmedName)
+	}
+
+
+	result := query.Find(&res)
+	if result.Error != nil {
+		return nil, result.Error
+	}
+	return res, nil
+}
+
+func (r *wafLogRepository) GetWafLogGateWayIp(ctx context.Context, hostId int64, Uid int64,createdAt time.Time) (model.WafLog, error) {
+	var res model.WafLog
+	return res, r.DBWithName(ctx,"admin").Where("host_id = ? and uid = ? and api_name = ? and created_at > ? ", hostId, Uid, "分配网关组", createdAt).First(&res).Error
+}

+ 91 - 0
internal/service/admin/waflog.go

@@ -245,3 +245,94 @@ func (s *wafLogService) PublishIpWafLogTask(ctx context.Context, req adminApi.Wa
 		s.Logger.Info("已成功发布 WafLog 任务消息", zap.Int("hostId", payload.HostId), zap.Int("uid", payload.Uid), zap.Any("req", req))
 	}
 }
+
+func (s *wafLogService) ExPortWafLog(ctx context.Context, req adminApi.ExportWafLog) ([]adminApi.ExportWafLogRes, error) {
+	data, err := s.wafLogRepository.ExportWafLog(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+
+	var res []adminApi.ExportWafLogRes
+	for _, v := range data {
+		var AddrBackendList interface{}
+		var customHost string
+		var port string
+		var domain string
+		var comment string
+
+
+		var mapData map[string]interface{}
+		err := json.Unmarshal(v.ExtraData, &mapData)
+		if err != nil {
+			return nil, err
+		}
+
+
+
+		if strings.Contains(v.ApiName, "tcp") || strings.Contains(v.ApiName, "udp") || strings.Contains(v.ApiName, "web") {
+
+			if mapData["port"] != nil {
+				port = mapData["port"].(string)
+			}
+			if mapData["domain"] != nil {
+				domain = mapData["domain"].(string)
+			}
+			if mapData["backend_list"] != nil {
+
+				if strings.Contains(v.ApiName, "web") {
+					var backendList []map[string]interface{}
+					err := json.Unmarshal([]byte(mapData["backend_list"].(string)), &backendList)
+					if err != nil {
+						return nil, err
+					}
+					for _, v := range backendList {
+						if v["addr"] != nil {
+							AddrBackendList = v["addr"]
+						}
+						if v["customHost"] != nil {
+							customHost = v["customHost"].(string)
+						}
+					}
+				}else {
+					AddrBackendList = mapData["backend_list"]
+				}
+			}
+		}
+
+		if mapData["comment"] != nil {
+			comment = mapData["comment"].(string)
+		}
+
+
+		gateWayIpModel, err := s.wafLogRepository.GetWafLogGateWayIp(ctx, int64(v.HostId), int64(v.Uid),v.CreatedAt)
+		if err != nil {
+			return nil, err
+		}
+		var gateWayIps []string
+		if err := json.Unmarshal(gateWayIpModel.ExtraData, &gateWayIps); err != nil {
+			return nil, err
+		}
+		var exposeAddr []string
+		if len(gateWayIps) > 0 {
+			for _, v := range gateWayIps {
+				exposeAddr = append(exposeAddr, v + ":" + port)
+			}
+		}
+
+		res = append(res, adminApi.ExportWafLogRes{
+			Name:       v.Name,
+			RequestIp:  v.RequestIp,
+			HostId:     v.HostId,
+			ApiName:    v.ApiName,
+			AddrBackendList:  AddrBackendList,
+			Domain:     domain,
+			Comment: 	comment,
+			CustomHost: customHost,
+			ExposeAddr: exposeAddr,
+			CreatedAt:  v.CreatedAt,
+		})
+	}
+
+	return res, nil
+
+}