Initial qiwei secondary development handoff
This commit is contained in:
324
operation_record.go
Normal file
324
operation_record.go
Normal file
@@ -0,0 +1,324 @@
|
||||
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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user