package fadada import ( "fmt" "os" "path/filepath" "strings" "sync" "time" ) // debug 日志:将每次法大大 OpenAPI 调用的 cURL 命令 + 响应落盘到 logs/fadada//fadada.log // 用于排查 /sign-task/create-with-template 等接口的实际请求/响应(如免验证签不生效)。 // 敏感字段(AppSecret)不会出现在日志里;AccessToken / Sign 会以明文写入以便复现。 const ( debugLogDir = "logs/fadada" debugLogFile = "fadada.log" debugMaxBytes = 10 * 1024 * 1024 // 单文件 10MB 后截断覆盖(轮转保持简单) ) var ( debugLogMu sync.Mutex debugLogFilePtr *os.File debugLogDate string debugLogSize int64 ) // writeDebugLog 把一次 API 调用的请求与响应追加到当日日志文件。 // 调用方需保证 headers / body / respBody 不包含 AppSecret(法大大签名里只有时间戳参与,secret 本身不出现在 header/body 中)。 func writeDebugLog(method, fullURL string, headers map[string]string, body string, httpStatus int, respBody string, requestID string) { if strings.TrimSpace(fullURL) == "" { return } entry := buildDebugEntry(method, fullURL, headers, body, httpStatus, respBody, requestID) debugLogMu.Lock() defer debugLogMu.Unlock() if err := openDebugLogFileLocked(); err != nil { // 日志失败不影响业务 return } n, _ := debugLogFilePtr.WriteString(entry) debugLogSize += int64(n) if debugLogSize >= debugMaxBytes { _ = debugLogFilePtr.Close() debugLogFilePtr = nil debugLogSize = 0 } } func openDebugLogFileLocked() error { today := time.Now().Format("2006-01-02") if debugLogFilePtr != nil && debugLogDate == today { return nil } if debugLogFilePtr != nil { _ = debugLogFilePtr.Close() } dateDir := filepath.Join(debugLogDir, today) if err := os.MkdirAll(dateDir, 0o755); err != nil { return err } full := filepath.Join(dateDir, debugLogFile) f, err := os.OpenFile(full, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } if info, err := f.Stat(); err == nil { debugLogSize = info.Size() } else { debugLogSize = 0 } debugLogFilePtr = f debugLogDate = today return nil } // buildDebugEntry 生成一段可读的日志条目(cURL 命令 + 响应)。 func buildDebugEntry(method, fullURL string, headers map[string]string, body string, httpStatus int, respBody string, requestID string) string { now := time.Now().Format("2006-01-02 15:04:05.000") var sb strings.Builder sb.WriteString("\n==================== ") sb.WriteString(now) sb.WriteString(" ====================\n") // ---- cURL ---- sb.WriteString("curl -sS -X ") sb.WriteString(strings.ToUpper(strings.TrimSpace(method))) sb.WriteString(" '") sb.WriteString(fullURL) sb.WriteString("'") for _, k := range sortedHeaderKeys(headers) { sb.WriteString(" \\\n -H '") sb.WriteString(k) sb.WriteString(": ") sb.WriteString(headers[k]) sb.WriteString("'") } if strings.TrimSpace(body) != "" { sb.WriteString(" \\\n --data-urlencode 'bizContent=") sb.WriteString(body) sb.WriteString("'") } sb.WriteString("\n") // ---- RESPONSE ---- sb.WriteString("--- RESPONSE ---\n") sb.WriteString("HTTP ") sb.WriteString(fmt.Sprintf("%d", httpStatus)) if requestID != "" { sb.WriteString(" requestId=") sb.WriteString(requestID) } sb.WriteString("\n") sb.WriteString("body: ") sb.WriteString(respBody) sb.WriteString("\n") return sb.String() } func sortedHeaderKeys(headers map[string]string) []string { keys := make([]string, 0, len(headers)) for k := range headers { keys = append(keys, k) } // 简单冒泡,依赖少 for i := 0; i < len(keys); i++ { for j := i + 1; j < len(keys); j++ { if keys[i] > keys[j] { keys[i], keys[j] = keys[j], keys[i] } } } return keys }