325 lines
9.2 KiB
Go
325 lines
9.2 KiB
Go
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()))
|
||
}
|
||
}
|
||
}
|