kra-new/internal/data/audit.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
}