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 }