431 lines
14 KiB
Go
431 lines
14 KiB
Go
package data
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"gorm.io/gorm"
|
|
"io/fs"
|
|
"kra/internal/biz"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type operationPO struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
|
IP, Method, Path string
|
|
Status int
|
|
LatencyMS int64 `gorm:"column:latency_ms"`
|
|
Agent string `gorm:"type:text"`
|
|
ErrorMessage string
|
|
Body string `gorm:"type:text"`
|
|
Response string `gorm:"column:resp;type:text"`
|
|
UserID uint
|
|
RequestID string `gorm:"index"`
|
|
TraceID string `gorm:"index"`
|
|
DeviceID string
|
|
}
|
|
|
|
func (operationPO) TableName() string { return "sys_operation_records" }
|
|
|
|
type loginLogPO struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
|
Username, IP string
|
|
Status bool
|
|
ErrorMessage, Agent string
|
|
UserID uint
|
|
}
|
|
|
|
func (loginLogPO) TableName() string { return "sys_login_logs" }
|
|
|
|
type dataAccessLogPO struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
|
EventType, TargetTable, Operation string
|
|
UserID, AuthorityID uint
|
|
Scope int
|
|
RequestID, Method, Path, Detail string
|
|
}
|
|
|
|
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
|
|
|
|
type errorRecordPO struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
|
Form string `gorm:"type:text"`
|
|
Info string `gorm:"type:text"`
|
|
Level string
|
|
RequestID string `gorm:"index"`
|
|
TraceID string `gorm:"index"`
|
|
Solution string `gorm:"type:text"`
|
|
Status string `gorm:"default:未处理"`
|
|
}
|
|
|
|
func (errorRecordPO) TableName() string { return "sys_error" }
|
|
|
|
type auditRepo struct{ data *Data }
|
|
|
|
func NewAuditRepo(data *Data) biz.AuditRepo { return &auditRepo{data: data} }
|
|
func (r *auditRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
|
|
return r.data.gormDB.WithContext(ctx).Create(&operationPO{IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}).Error
|
|
}
|
|
func opFromPO(v operationPO) *biz.OperationRecord {
|
|
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
|
|
}
|
|
func (r *auditRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if size < 1 {
|
|
size = 10
|
|
}
|
|
db := r.data.gormDB.WithContext(ctx).Model(&operationPO{})
|
|
if q != nil {
|
|
if q.Path != "" {
|
|
db = db.Where("path LIKE ?", "%"+q.Path+"%")
|
|
}
|
|
if q.Method != "" {
|
|
db = db.Where("method = ?", q.Method)
|
|
}
|
|
if q.Status != 0 {
|
|
db = db.Where("status = ?", q.Status)
|
|
}
|
|
if q.IP != "" {
|
|
db = db.Where("ip LIKE ?", "%"+q.IP+"%")
|
|
}
|
|
if q.UserID != 0 {
|
|
db = db.Where("user_id = ?", q.UserID)
|
|
}
|
|
}
|
|
var total int64
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var pos []operationPO
|
|
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out := make([]*biz.OperationRecord, 0, len(pos))
|
|
for _, po := range pos {
|
|
out = append(out, opFromPO(po))
|
|
}
|
|
return out, total, nil
|
|
}
|
|
func (r *auditRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) {
|
|
var po operationPO
|
|
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return opFromPO(po), nil
|
|
}
|
|
func (r *auditRepo) DeleteOperations(ctx context.Context, ids []uint) error {
|
|
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, ids).Error
|
|
}
|
|
func (r *auditRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
|
|
return r.data.gormDB.WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error
|
|
}
|
|
func loginFromPO(v loginLogPO) *biz.LoginLog {
|
|
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
|
|
}
|
|
func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if size < 1 {
|
|
size = 10
|
|
}
|
|
db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{})
|
|
if q != nil {
|
|
if q.Username != "" {
|
|
db = db.Where("username LIKE ?", "%"+q.Username+"%")
|
|
}
|
|
if q.IP != "" {
|
|
db = db.Where("ip LIKE ?", "%"+q.IP+"%")
|
|
}
|
|
}
|
|
var total int64
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var pos []loginLogPO
|
|
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out := make([]*biz.LoginLog, 0, len(pos))
|
|
for _, po := range pos {
|
|
out = append(out, loginFromPO(po))
|
|
}
|
|
return out, total, nil
|
|
}
|
|
func (r *auditRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) {
|
|
var po loginLogPO
|
|
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return loginFromPO(po), nil
|
|
}
|
|
func (r *auditRepo) DeleteLogins(ctx context.Context, ids []uint) error {
|
|
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, ids).Error
|
|
}
|
|
func (r *auditRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
|
|
return r.data.gormDB.WithContext(ctx).Create(&dataAccessLogPO{EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}).Error
|
|
}
|
|
func dataAccessFromPO(v dataAccessLogPO) *biz.DataAccessLog {
|
|
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
|
|
}
|
|
func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if size < 1 {
|
|
size = 10
|
|
}
|
|
db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{})
|
|
if q != nil {
|
|
if q.EventType != "" {
|
|
db = db.Where("event_type = ?", q.EventType)
|
|
}
|
|
if q.TargetTable != "" {
|
|
db = db.Where("target_table LIKE ?", "%"+q.TargetTable+"%")
|
|
}
|
|
if q.UserID != 0 {
|
|
db = db.Where("user_id = ?", q.UserID)
|
|
}
|
|
}
|
|
var total int64
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var pos []dataAccessLogPO
|
|
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out := make([]*biz.DataAccessLog, 0, len(pos))
|
|
for _, po := range pos {
|
|
out = append(out, dataAccessFromPO(po))
|
|
}
|
|
return out, total, nil
|
|
}
|
|
func (r *auditRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
|
|
return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, ids).Error
|
|
}
|
|
|
|
func logRoot() (string, error) {
|
|
root, err := filepath.Abs("logs")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
info, err := os.Stat(root)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return root, nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !info.IsDir() {
|
|
return "", errors.New("日志路径不是目录")
|
|
}
|
|
return root, nil
|
|
}
|
|
func (r *auditRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
|
|
if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month {
|
|
return nil, errors.New("日志月份格式不正确")
|
|
}
|
|
root, err := logRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries, err := os.ReadDir(root)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return []biz.LogDate{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := []biz.LogDate{}
|
|
for _, entry := range entries {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
|
|
continue
|
|
}
|
|
if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() {
|
|
continue
|
|
}
|
|
count := 0
|
|
_ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error {
|
|
if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") {
|
|
count++
|
|
}
|
|
return nil
|
|
})
|
|
if count > 0 {
|
|
out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count})
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date })
|
|
return out, nil
|
|
}
|
|
func (r *auditRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
|
|
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
|
|
return nil, errors.New("日志日期格式不正确")
|
|
}
|
|
root, err := logRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dateRoot := filepath.Join(root, date)
|
|
out := []biz.LogFile{}
|
|
err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
if errors.Is(walkErr, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return walkErr
|
|
}
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
if entry.Type()&os.ModeSymlink != 0 {
|
|
if entry.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".log") {
|
|
return nil
|
|
}
|
|
info, infoErr := entry.Info()
|
|
if infoErr != nil {
|
|
return infoErr
|
|
}
|
|
relative, _ := filepath.Rel(dateRoot, path)
|
|
out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()})
|
|
return nil
|
|
})
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
|
|
return out, err
|
|
}
|
|
func (r *auditRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
|
|
if _, err := time.Parse("2006-01-02", date); err != nil {
|
|
return nil, errors.New("日志日期格式不正确")
|
|
}
|
|
root, err := logRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dateRoot := filepath.Join(root, date)
|
|
target := filepath.Clean(filepath.Join(dateRoot, filepath.FromSlash(path)))
|
|
if target == dateRoot || !strings.HasPrefix(target, dateRoot+string(os.PathSeparator)) || !strings.EqualFold(filepath.Ext(target), ".log") {
|
|
return nil, errors.New("日志文件路径不合法")
|
|
}
|
|
info, err := os.Lstat(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return nil, errors.New("日志文件路径不合法")
|
|
}
|
|
end := info.Size()
|
|
if cursor != nil && *cursor >= 0 && *cursor < end {
|
|
end = *cursor
|
|
}
|
|
start := end - int64(2*1024*1024)
|
|
limited := start > 0
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
file, err := os.Open(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
data := make([]byte, end-start)
|
|
n, err := file.ReadAt(data, start)
|
|
if err != nil && n == 0 {
|
|
return nil, err
|
|
}
|
|
data = data[:n]
|
|
lines := bytes.Split(data, []byte("\n"))
|
|
if len(lines) > 501 {
|
|
drop := len(lines) - 501
|
|
offset := 0
|
|
for _, line := range lines[:drop] {
|
|
offset += len(line) + 1
|
|
}
|
|
start += int64(offset)
|
|
data = data[offset:]
|
|
limited = true
|
|
}
|
|
return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: bytes.Count(data, []byte("\n")), NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil
|
|
}
|
|
func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
|
|
return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
|
|
}
|
|
func (r *auditRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
|
|
if v.Status == "" {
|
|
v.Status = "未处理"
|
|
}
|
|
return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error
|
|
}
|
|
func (r *auditRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
|
|
return r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}).Where("id = ?", v.ID).Updates(map[string]any{"form": v.Form, "info": v.Info, "level": v.Level, "solution": v.Solution, "status": v.Status}).Error
|
|
}
|
|
func (r *auditRepo) DeleteErrors(ctx context.Context, ids []uint) error {
|
|
return r.data.gormDB.WithContext(ctx).Delete(&errorRecordPO{}, ids).Error
|
|
}
|
|
func (r *auditRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) {
|
|
var po errorRecordPO
|
|
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return errorFromPO(po), nil
|
|
}
|
|
func (r *auditRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if size < 1 {
|
|
size = 10
|
|
}
|
|
db := r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{})
|
|
if q != nil {
|
|
if q.Form != "" {
|
|
db = db.Where("form LIKE ?", "%"+q.Form+"%")
|
|
}
|
|
if q.Level != "" {
|
|
db = db.Where("level = ?", q.Level)
|
|
}
|
|
if q.Status != "" {
|
|
db = db.Where("status = ?", q.Status)
|
|
}
|
|
}
|
|
var total int64
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var pos []errorRecordPO
|
|
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out := make([]*biz.ErrorRecord, 0, len(pos))
|
|
for _, po := range pos {
|
|
out = append(out, errorFromPO(po))
|
|
}
|
|
return out, total, nil
|
|
}
|