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{"锛", "涓", "绋", "鎴", "鍙", "杈", "濂", "瀹", "熷", "€", "�"} { 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())) } } }