package main import ( "fmt" "os" "path/filepath" "strings" "sync" "time" ) // logger.go —— 简单文件日志(windowsgui 子系统无控制台,统一落盘)。 // 特性:按天轮转(跨天自动切换文件);所有写出内容经 Redact 脱敏,token 不明文落盘。 type Logger struct { mu sync.Mutex file *os.File path string day string } func NewLogger() *Logger { l := &Logger{} l.rotate(time.Now().Format("20060102")) return l } // rotate 切换到指定日期的日志文件(关闭旧句柄)。 func (l *Logger) rotate(day string) { if l.file != nil { _ = l.file.Close() l.file = nil } dir := logDir() _ = os.MkdirAll(dir, 0o755) p := filepath.Join(dir, "tray-"+day+".log") f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { f = nil } l.file = f l.path = p l.day = day } func (l *Logger) Printf(format string, args ...any) { l.mu.Lock() defer l.mu.Unlock() if day := time.Now().Format("20060102"); day != l.day { l.rotate(day) // 跨天轮转 } body := Redact(fmt.Sprintf(format, args...)) line := fmt.Sprintf("[%s] %s\n", time.Now().Format("2006-01-02 15:04:05"), body) if l.file != nil { _, _ = l.file.WriteString(line) } } func (l *Logger) Path() string { l.mu.Lock() defer l.mu.Unlock() return l.path } func (l *Logger) Close() { l.mu.Lock() defer l.mu.Unlock() if l.file != nil { _ = l.file.Close() l.file = nil } } // Redact 脱敏访问 token(token=xxxx -> token=***),避免凭据明文落盘。 func Redact(s string) string { const marker = "token=" var b strings.Builder for { i := strings.Index(s, marker) if i < 0 { b.WriteString(s) return b.String() } end := i + len(marker) b.WriteString(s[:end]) b.WriteString("***") j := end for j < len(s) && !isTokenTerminator(s[j]) { j++ } s = s[j:] } } func isTokenTerminator(c byte) bool { switch c { case ' ', '\t', '\r', '\n', '"', '\'', '&', '<', '>', '#', '?': return true } return false }