Files
qiweimanager-master/operation_record.go

325 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"unicode/utf8"
"golang.org/x/text/encoding/simplifiedchinese"
"qiweimanager/logger"
)
var operationLogFileMu sync.Mutex
func SaveLogEntry(logDir string, entry logger.LogEntry) error {
operationLogFileMu.Lock()
defer operationLogFileMu.Unlock()
entry = normalizeOperationLogEntry(entry)
operationLogDir := filepath.Join(logDir, "operations")
if err := os.MkdirAll(operationLogDir, 0755); err != nil {
return fmt.Errorf("无法创建操作日志目录: %v", err)
}
logFilePath := filepath.Join(operationLogDir, fmt.Sprintf("%s_operations.json", time.Now().Format("2006-01-02")))
logEntries := []logger.LogEntry{}
if info, err := os.Stat(logFilePath); err == nil && info.Size() > 0 {
byteValue, err := ioutil.ReadFile(logFilePath)
if err != nil {
return fmt.Errorf("读取日志文件失败: %v", err)
}
entries, repaired, err := parseOperationLogData(byteValue)
if err != nil {
if renameErr := quarantineCorruptOperationLog(logFilePath); renameErr != nil {
return fmt.Errorf("解析日志文件失败: %v备份损坏文件失败: %v", err, renameErr)
}
logEntries = []logger.LogEntry{}
} else {
logEntries = entries
if repaired {
_ = backupOperationLog(logFilePath, "repaired")
}
}
}
logEntries = append(logEntries, entry)
if len(logEntries) > 1000 {
logEntries = logEntries[len(logEntries)-1000:]
}
jsonData, err := json.MarshalIndent(logEntries, "", " ")
if err != nil {
return fmt.Errorf("序列化日志失败: %v", err)
}
if err := writeFileAtomically(logFilePath, jsonData, 0644); err != nil {
return fmt.Errorf("写入日志文件失败: %v", err)
}
go manageLogFiles(operationLogDir)
return nil
}
func LoadLogEntries(logDir string, page, pageSize int, logType string) ([]logger.LogEntry, int, error) {
operationLogFileMu.Lock()
defer operationLogFileMu.Unlock()
operationLogDir := filepath.Join(logDir, "operations")
if _, err := os.Stat(operationLogDir); os.IsNotExist(err) {
return []logger.LogEntry{}, 0, nil
}
files, err := ioutil.ReadDir(operationLogDir)
if err != nil {
return nil, 0, fmt.Errorf("读取日志目录失败: %v", err)
}
sort.Slice(files, func(i, j int) bool {
return files[i].ModTime().After(files[j].ModTime())
})
var allEntries []logger.LogEntry
for _, file := range files {
if !strings.HasSuffix(file.Name(), ".json") || strings.Contains(file.Name(), ".corrupt.") {
continue
}
filePath := filepath.Join(operationLogDir, file.Name())
byteValue, err := ioutil.ReadFile(filePath)
if err != nil {
continue
}
entries, repaired, err := parseOperationLogData(byteValue)
if err != nil {
_ = quarantineCorruptOperationLog(filePath)
continue
}
if repaired {
_ = backupOperationLog(filePath, "repaired")
if data, marshalErr := json.MarshalIndent(entries, "", " "); marshalErr == nil {
_ = writeFileAtomically(filePath, data, 0644)
}
}
allEntries = append(allEntries, entries...)
}
sort.Slice(allEntries, func(i, j int) bool {
return allEntries[i].ID > allEntries[j].ID
})
if logType != "all" {
filteredEntries := make([]logger.LogEntry, 0, len(allEntries))
for _, entry := range allEntries {
if entry.Type == logType {
filteredEntries = append(filteredEntries, entry)
}
}
allEntries = filteredEntries
}
totalCount := len(allEntries)
start := (page - 1) * pageSize
end := start + pageSize
if start >= totalCount {
return []logger.LogEntry{}, totalCount, nil
}
if end > totalCount {
end = totalCount
}
return allEntries[start:end], totalCount, nil
}
func loadOperationLogEntriesFromFile(filePath string) ([]logger.LogEntry, error) {
operationLogFileMu.Lock()
defer operationLogFileMu.Unlock()
data, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
entries, repaired, err := parseOperationLogData(data)
if err != nil {
_ = quarantineCorruptOperationLog(filePath)
return nil, err
}
if repaired {
_ = backupOperationLog(filePath, "repaired")
if clean, marshalErr := json.MarshalIndent(entries, "", " "); marshalErr == nil {
_ = writeFileAtomically(filePath, clean, 0644)
}
}
return entries, nil
}
func parseOperationLogData(data []byte) ([]logger.LogEntry, bool, error) {
var entries []logger.LogEntry
if err := json.Unmarshal(data, &entries); err == nil {
return normalizeOperationLogEntries(entries), operationLogEntriesNeedRepair(entries), nil
}
trimmed := bytes.TrimSpace(data)
lastArrayEnd := bytes.LastIndexByte(trimmed, ']')
if lastArrayEnd < 0 {
return nil, false, fmt.Errorf("日志文件不是有效JSON数组")
}
recovered := trimmed[:lastArrayEnd+1]
if err := json.Unmarshal(recovered, &entries); err != nil {
return nil, false, err
}
return normalizeOperationLogEntries(entries), true, nil
}
func normalizeOperationLogEntries(entries []logger.LogEntry) []logger.LogEntry {
out := make([]logger.LogEntry, len(entries))
for i, entry := range entries {
out[i] = normalizeOperationLogEntry(entry)
}
return out
}
func normalizeOperationLogEntry(entry logger.LogEntry) logger.LogEntry {
entry.Source = repairMojibakeText(entry.Source)
entry.Type = repairMojibakeText(entry.Type)
entry.Content = repairMojibakeText(entry.Content)
return entry
}
func operationLogEntriesNeedRepair(entries []logger.LogEntry) bool {
for _, entry := range entries {
if looksLikeMojibake(entry.Source) || looksLikeMojibake(entry.Type) || looksLikeMojibake(entry.Content) {
return true
}
}
return false
}
func repairMojibakeText(text string) string {
if !looksLikeMojibake(text) {
return text
}
replaced := replaceKnownOperationMojibake(text)
if mojibakeScore(replaced) < mojibakeScore(text) {
text = replaced
}
encoded, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(text))
if err != nil || !utf8.Valid(encoded) {
return text
}
repaired := string(encoded)
if mojibakeScore(repaired) < mojibakeScore(text) {
return repaired
}
return text
}
func replaceKnownOperationMojibake(text string) string {
replacements := map[string]string{
"绋嬪簭鍒濆鎴愬姛": "程序初始成功",
"HTTP瀹㈡埛绔垵濮嬪寲瀹屾垚锛岀鍙?": "HTTP客户端初始化完成端口:",
"鍙戦€佽姹傚埌杈呭姪绋嬪簭锛屾秷鎭被鍨?": "发送请求到辅助程序,消息类型:",
"璋冪敤杈呭姪绋嬪簭鎴愬姛锛屾秷鎭被鍨?": "调用辅助程序成功,消息类型:",
"璋冪敤杈呭姪绋嬪簭杩斿洖澶辫触锛屾秷鎭被鍨?": "调用辅助程序返回失败,消息类型:",
"璋冪敤杈呭姪绋嬪簭澶辫触": "调用辅助程序失败",
"閲嶆柊璋冪敤杈呭姪绋嬪簭澶辫触": "重新调用辅助程序失败",
"鎿嶄綔鏃ュ織璋冭瘯璇锋眰瀹屾垚": "操作日志调试请求完成",
"鎴愬姛鑾峰彇": "成功获取",
"涓紒寰处鍙?": "个企微账号",
"浼佷笟寰俊鏈嶅姟杩炴帴鎴愬姛": "企业微信服务连接成功",
"鍐呭瓨浣跨敤鐜囪秴杩?0%": "内存使用率超过80%",
"鎴愬姛鍒犻櫎璐﹀彿": "成功删除账号",
}
for old, replacement := range replacements {
text = strings.ReplaceAll(text, old, replacement)
}
return text
}
func looksLikeMojibake(text string) bool {
return mojibakeScore(text) >= 2
}
func mojibakeScore(text string) int {
score := 0
for _, marker := range []string{"锛", "涓", "绋", "鎴", "鍙", "杈", "濂", "瀹", "熷", "€", "<22>"} {
score += strings.Count(text, marker)
}
return score
}
func writeFileAtomically(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
tmp, err := ioutil.TempFile(dir, filepath.Base(path)+".tmp-*")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Chmod(tmpPath, perm); err != nil {
return err
}
_ = os.Remove(path)
return os.Rename(tmpPath, path)
}
func quarantineCorruptOperationLog(path string) error {
if strings.TrimSpace(path) == "" {
return nil
}
if _, err := os.Stat(path); err != nil {
return err
}
ext := filepath.Ext(path)
base := strings.TrimSuffix(path, ext)
if ext == "" {
ext = ".json"
}
target := fmt.Sprintf("%s.corrupt.%s%s", base, time.Now().Format("20060102_150405_000000000"), ext)
return os.Rename(path, target)
}
func backupOperationLog(path string, label string) error {
if strings.TrimSpace(path) == "" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
ext := filepath.Ext(path)
base := strings.TrimSuffix(path, ext)
if ext == "" {
ext = ".json"
}
target := fmt.Sprintf("%s.%s.%s%s", base, label, time.Now().Format("20060102_150405_000000000"), ext)
return os.WriteFile(target, data, 0644)
}
func manageLogFiles(operationLogDir string) {
files, err := ioutil.ReadDir(operationLogDir)
if err != nil {
return
}
sevenDaysAgo := time.Now().AddDate(0, 0, -7)
for _, file := range files {
if file.ModTime().Before(sevenDaysAgo) {
_ = os.Remove(filepath.Join(operationLogDir, file.Name()))
}
}
}