From c1d696ac1975bae0c22ac294d2ddcdae3c97f989 Mon Sep 17 00:00:00 2001
From: coward <coward@mrx.ltd>
Date: Thu, 8 Aug 2024 17:26:50 +0800
Subject: [PATCH] =?UTF-8?q?:new:=E6=96=B0=E5=A2=9E=E6=93=8D=E4=BD=9C?=
 =?UTF-8?q?=E6=97=A5=E5=BF=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 http/api/dashboard.go      |  40 ++++++++++++++
 http/middleware/request.go | 108 +++++++++++++++++++++++++++++++++++++
 http/router/client.go      |   2 +-
 http/router/dashboard.go   |  17 ++++++
 http/router/login.go       |   3 +-
 http/router/root.go        |   1 +
 http/router/setting.go     |   3 +-
 http/router/user.go        |   2 +-
 http/vo/log.go             |  14 +++++
 model/other.go             |   4 ++
 service/log.go             |  49 +++++++++++++++++
 11 files changed, 239 insertions(+), 4 deletions(-)
 create mode 100644 http/api/dashboard.go
 create mode 100644 http/middleware/request.go
 create mode 100644 http/router/dashboard.go
 create mode 100644 http/vo/log.go
 create mode 100644 service/log.go

diff --git a/http/api/dashboard.go b/http/api/dashboard.go
new file mode 100644
index 0000000..d8e8a9f
--- /dev/null
+++ b/http/api/dashboard.go
@@ -0,0 +1,40 @@
+package api
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"wireguard-ui/http/param"
+	"wireguard-ui/http/response"
+	"wireguard-ui/http/vo"
+	"wireguard-ui/service"
+)
+
+type DashboardApi struct{}
+
+func Dashboard() DashboardApi {
+	return DashboardApi{}
+}
+
+// List
+// @description: 操作日志
+// @receiver DashboardApi
+// @param c
+func (DashboardApi) List(c *gin.Context) {
+	var p param.Page
+	if err := c.ShouldBind(&p); err != nil {
+		response.R(c).Validator(err)
+		return
+	}
+
+	var loginUser *vo.User
+	if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
+		return
+	}
+
+	data, total, err := service.Log().List(p, loginUser)
+	if err != nil {
+		response.R(c).FailedWithError(fmt.Errorf("获取操作日志失败: %v", err.Error()))
+		return
+	}
+	response.R(c).Paginate(data, total, p.Current, p.Size)
+}
diff --git a/http/middleware/request.go b/http/middleware/request.go
new file mode 100644
index 0000000..c90ab77
--- /dev/null
+++ b/http/middleware/request.go
@@ -0,0 +1,108 @@
+package middleware
+
+import (
+	"bytes"
+	"gitee.ltd/lxh/logger/log"
+	"github.com/gin-gonic/gin"
+	jsoniter "github.com/json-iterator/go"
+	"io"
+	"regexp"
+	"strings"
+	"time"
+	"wireguard-ui/http/vo"
+	"wireguard-ui/model"
+	"wireguard-ui/service"
+)
+
+// bodyWriter
+// @description: 重写ResponseBody
+type bodyWriter struct {
+	gin.ResponseWriter
+	body *bytes.Buffer
+}
+
+func RequestLog() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var userId string
+		if userInfo, ok := c.Get("user"); ok {
+			userId = userInfo.(*vo.User).Id
+		}
+
+		// 开始时间
+		start := time.Now()
+		host := c.Request.Host          // 请求域名
+		path := c.Request.URL.Path      // 接口地址
+		query := c.Request.URL.RawQuery // 参数
+		if strings.Contains(path, "/api/dashboard/request/list") {
+			c.Next()
+			return
+		}
+
+		var bodyStr string
+		if !strings.Contains(path, "/api/login") {
+			body, err := c.GetRawData() // body参数
+			if err == nil {
+				bodyStr = string(body)
+				c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
+				reg := regexp.MustCompile("\\s+")
+				bodyStr = reg.ReplaceAllString(bodyStr, "")
+			}
+		}
+
+		method := c.Request.Method // 请求方式
+		ip := c.ClientIP()         // 取出IP
+		// 处理实际客户端IP
+		if c.Request.Header.Get("X-Real-IP") != "" {
+			ip = c.Request.Header.Get("X-Real-IP") // 这个是网关Nginx自定义的Header头
+		} else if c.Request.Header.Get("X-Forwarded-For") != "" {
+			ip = c.Request.Header.Get("X-Forwarded-For") // 这个是网关Nginx自定义的Header头
+		}
+		ua := c.Request.UserAgent() // UA
+
+		// 重写客户端IP
+		c.Request.Header.Set("X-Forwarded-For", ip)
+		c.Request.Header.Set("X-Real-IP", ip)
+
+		// 拦截response
+		bw := &bodyWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
+		c.Writer = bw
+
+		header := c.Request.Header
+		headerStr, _ := jsoniter.MarshalToString(header)
+
+		// 执行下一步请求
+		c.Next()
+		// 计算耗时
+		cost := time.Since(start).Milliseconds()
+
+		// 组装实体
+		l := model.RequestLog{
+			UserId:     userId,
+			ClientIP:   ip,
+			Host:       host,
+			Method:     method,
+			Uri:        path,
+			Header:     headerStr,
+			Body:       bodyStr,
+			Form:       "",
+			Query:      query,
+			UserAgent:  ua,
+			Cost:       cost,
+			StatusCode: c.Writer.Status(),
+			Response:   "",
+		}
+
+		// 如果不是返回200,把返回值保存一下
+		if c.Writer.Status() != 200 {
+			resp := bw.body.String()
+			l.Response = resp
+		}
+
+		go func() {
+			if er := service.Log().CreateLog(l); er != nil {
+				log.Debugf("请求日志: %+v", l)
+				log.Errorf("保存请求日志失败: %v", er)
+			}
+		}()
+	}
+}
diff --git a/http/router/client.go b/http/router/client.go
index 9a278aa..2f51dc1 100644
--- a/http/router/client.go
+++ b/http/router/client.go
@@ -10,7 +10,7 @@ import (
 // @description: 登陆相关API
 // @param r
 func ClientApi(r *gin.RouterGroup) {
-	client := r.Group("client", middleware.Authorization())
+	client := r.Group("client", middleware.Authorization(), middleware.RequestLog())
 	{
 		client.POST("", api.Client().Save)                       // 新增/编辑客户端
 		client.DELETE("/:id", api.Client().Delete)               // 删除客户端
diff --git a/http/router/dashboard.go b/http/router/dashboard.go
new file mode 100644
index 0000000..f2229a6
--- /dev/null
+++ b/http/router/dashboard.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+	"github.com/gin-gonic/gin"
+	"wireguard-ui/http/api"
+	"wireguard-ui/http/middleware"
+)
+
+// DashboardApi
+// @description: 控制台相关接口
+// @param r
+func DashboardApi(r *gin.RouterGroup) {
+	dashboard := r.Group("dashboard", middleware.Authorization(), middleware.RequestLog())
+	{
+		dashboard.GET("/request/list", api.Dashboard().List) // 请求日志
+	}
+}
diff --git a/http/router/login.go b/http/router/login.go
index 70f7c1b..12a4989 100644
--- a/http/router/login.go
+++ b/http/router/login.go
@@ -3,13 +3,14 @@ package router
 import (
 	"github.com/gin-gonic/gin"
 	"wireguard-ui/http/api"
+	"wireguard-ui/http/middleware"
 )
 
 // LoginApi
 // @description: 登陆相关API
 // @param r
 func LoginApi(r *gin.RouterGroup) {
-	login := r.Group("/login")
+	login := r.Group("/login", middleware.RequestLog())
 	{
 		login.GET("/captcha", api.Login().Captcha) // 获取登陆验证码
 		login.POST("", api.Login().Login)          // 登陆
diff --git a/http/router/root.go b/http/router/root.go
index ca20716..0d2ece1 100644
--- a/http/router/root.go
+++ b/http/router/root.go
@@ -47,5 +47,6 @@ func Rooters() {
 		UserApi,
 		ClientApi,
 		SettingApi,
+		DashboardApi,
 	)
 }
diff --git a/http/router/setting.go b/http/router/setting.go
index 723b0f5..1e976da 100644
--- a/http/router/setting.go
+++ b/http/router/setting.go
@@ -3,13 +3,14 @@ package router
 import (
 	"github.com/gin-gonic/gin"
 	"wireguard-ui/http/api"
+	"wireguard-ui/http/middleware"
 )
 
 // SettingApi
 // @description: 设置相关API
 // @param r
 func SettingApi(r *gin.RouterGroup) {
-	setting := r.Group("setting")
+	setting := r.Group("setting", middleware.Authorization(), middleware.RequestLog())
 	{
 		setting.POST("", api.Setting().Set)                      // 新增/编辑设置
 		setting.DELETE("/:code", api.Setting().Delete)           // 删除配置
diff --git a/http/router/user.go b/http/router/user.go
index 108dd08..5a5687b 100644
--- a/http/router/user.go
+++ b/http/router/user.go
@@ -10,7 +10,7 @@ import (
 // @description: 用户相关API
 // @param r
 func UserApi(r *gin.RouterGroup) {
-	userApi := r.Group("user", middleware.Authorization())
+	userApi := r.Group("user", middleware.Authorization(), middleware.RequestLog())
 	{
 		userApi.GET("/info", api.User().GetLoginUser)                // 获取当前登陆用户信息
 		userApi.POST("", api.User().SaveUser)                        // 新增/编辑用户
diff --git a/http/vo/log.go b/http/vo/log.go
new file mode 100644
index 0000000..44e23f0
--- /dev/null
+++ b/http/vo/log.go
@@ -0,0 +1,14 @@
+package vo
+
+import "wireguard-ui/model"
+
+type SystemLogItem struct {
+	Id         string         `json:"id"`
+	Username   string         `json:"username"`
+	ClientIP   string         `json:"clientIP"`
+	Method     string         `json:"method"`
+	Host       string         `json:"host"`
+	Uri        string         `json:"uri"`
+	StatusCode int            `json:"statusCode"`
+	CreatedAt  model.JsonTime `json:"createdAt"`
+}
diff --git a/model/other.go b/model/other.go
index cae5fb2..b5a9aac 100644
--- a/model/other.go
+++ b/model/other.go
@@ -16,3 +16,7 @@ type RequestLog struct {
 	StatusCode int    `json:"statusCode" gorm:"type:int(10);comment:'响应状态码'"`
 	Response   string `json:"response" gorm:"type:text;comment:'返回数据'"`
 }
+
+func (RequestLog) TableName() string {
+	return "t_request_log"
+}
diff --git a/service/log.go b/service/log.go
new file mode 100644
index 0000000..fd99811
--- /dev/null
+++ b/service/log.go
@@ -0,0 +1,49 @@
+package service
+
+import (
+	"gorm.io/gorm"
+	gdb "wireguard-ui/global/client"
+	"wireguard-ui/global/constant"
+	"wireguard-ui/http/param"
+	"wireguard-ui/http/vo"
+	"wireguard-ui/model"
+)
+
+type log struct {
+	*gorm.DB
+}
+
+func Log() log {
+	return log{
+		gdb.DB,
+	}
+}
+
+// CreateLog
+// @description: 创建日志
+// @receiver l
+// @param log
+// @return error
+func (l log) CreateLog(log model.RequestLog) error {
+	return l.Create(&log).Error
+}
+
+// List
+// @description: 分页列表
+// @receiver s
+// @param p
+// @return data
+// @return total
+// @return err
+func (l log) List(p param.Page, loginUser *vo.User) (data []vo.SystemLogItem, total int64, err error) {
+	sel := l.Scopes(Paginate(p.Current, p.Size)).Table("t_request_log as tsl").
+		Joins("LEFT JOIN t_user as tu ON tu.id = tsl.user_id").
+		Select("tsl.id", "tu.nickname as username", "tsl.client_ip", "tsl.method", "tsl.status_code", "tsl.host", "tsl.uri", "tsl.created_at").
+		Order("tsl.created_at DESC")
+
+	if loginUser.IsAdmin == constant.NormalAdmin {
+		sel.Where("tsl.user_id = ?", loginUser.Id)
+	}
+	err = sel.Find(&data).Offset(-1).Limit(-1).Count(&total).Error
+	return
+}