package service import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "go.uber.org/zap" ) // RequestCaptureLogRepository 定义请求捕获日志的持久化接口。 type RequestCaptureLogRepository interface { Create(ctx context.Context, params CreateRequestCaptureLogParams) (int64, error) UpdateResponseBody(ctx context.Context, id int64, responseBody string) error } // CreateRequestCaptureLogParams 创建请求捕获日志的参数。 type CreateRequestCaptureLogParams struct { APIKeyID int64 UserID int64 RequestID string Path string Method string IPAddress string RequestBody string NFSFilePath string } // RequestCaptureService 异步捕获指定 API Key 的请求体,写入数据库和 NFS。 type RequestCaptureService struct { repo RequestCaptureLogRepository nfsPath string timeout time.Duration } // nfsFileEnvelope 是写入 NFS 文件的 JSON 结构。 type nfsFileEnvelope struct { APIKeyID int64 `json:"api_key_id"` UserID int64 `json:"user_id"` RequestID string `json:"request_id"` CreatedAt time.Time `json:"created_at"` Path string `json:"path"` Method string `json:"method"` IPAddress string `json:"ip_address"` Body json.RawMessage `json:"body"` } // NewRequestCaptureService 创建 RequestCaptureService。 func NewRequestCaptureService(repo RequestCaptureLogRepository, cfg *config.Config) *RequestCaptureService { timeout := 5 * time.Second if cfg != nil && cfg.RequestCapture.WorkerTimeoutSeconds > 0 { timeout = time.Duration(cfg.RequestCapture.WorkerTimeoutSeconds) * time.Second } nfsPath := "" if cfg != nil { nfsPath = cfg.RequestCapture.NFSPath } return &RequestCaptureService{ repo: repo, nfsPath: nfsPath, timeout: timeout, } } // Capture 异步捕获请求体,立即返回 captureID(DB 行 ID),不阻塞调用方。 // 返回 0 表示捕获未启用或写入失败。 // DB 写入与 NFS 写入各自独立,互不影响。 func (s *RequestCaptureService) Capture( apiKeyID, userID int64, requestID, path, method, ipAddr string, body []byte, ) int64 { now := time.Now() // NFS 写入(独立 goroutine) nfsFilePath := "" if s.nfsPath != "" { nfsFilePath = s.buildNFSFilePath(apiKeyID, requestID, now) bodyCopy := make([]byte, len(body)) copy(bodyCopy, body) go s.writeToNFS(nfsFilePath, apiKeyID, userID, requestID, path, method, ipAddr, bodyCopy, now) } // DB 写入(同步,需要拿到 ID) ctx, cancel := context.WithTimeout(context.Background(), s.timeout) defer cancel() id, err := s.repo.Create(ctx, CreateRequestCaptureLogParams{ APIKeyID: apiKeyID, UserID: userID, RequestID: requestID, Path: path, Method: method, IPAddress: ipAddr, RequestBody: string(body), NFSFilePath: nfsFilePath, }) if err != nil { logger.L().Error("request_capture: db write failed", zap.Int64("api_key_id", apiKeyID), zap.String("request_id", requestID), zap.Error(err), ) return 0 } return id } // CaptureResponse 异步将响应体写入已有的捕获记录,不阻塞调用方。 // captureID 为 Capture 返回的 ID,为 0 时直接忽略。 func (s *RequestCaptureService) CaptureResponse(captureID int64, responseBody string) { if captureID == 0 || responseBody == "" { return } go func() { ctx, cancel := context.WithTimeout(context.Background(), s.timeout) defer cancel() if err := s.repo.UpdateResponseBody(ctx, captureID, responseBody); err != nil { logger.L().Error("request_capture: db update response failed", zap.Int64("capture_id", captureID), zap.Error(err), ) } }() } func (s *RequestCaptureService) buildNFSFilePath(apiKeyID int64, requestID string, t time.Time) string { date := t.UTC().Format("2006-01-02") filename := fmt.Sprintf("%d_%s.json", t.UnixNano(), requestID) return filepath.Join(s.nfsPath, date, fmt.Sprintf("%d", apiKeyID), filename) } func (s *RequestCaptureService) writeToNFS( filePath string, apiKeyID, userID int64, requestID, path, method, ipAddr string, body []byte, now time.Time, ) { dir := filepath.Dir(filePath) if err := os.MkdirAll(dir, 0o755); err != nil { logger.L().Error("request_capture: mkdir failed", zap.String("dir", dir), zap.Error(err), ) return } envelope := nfsFileEnvelope{ APIKeyID: apiKeyID, UserID: userID, RequestID: requestID, CreatedAt: now.UTC(), Path: path, Method: method, IPAddress: ipAddr, Body: json.RawMessage(body), } var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) if err := enc.Encode(envelope); err != nil { logger.L().Error("request_capture: json marshal failed", zap.String("request_id", requestID), zap.Error(err), ) return } if err := os.WriteFile(filePath, buf.Bytes(), 0o644); err != nil { logger.L().Error("request_capture: nfs write failed", zap.String("file", filePath), zap.Error(err), ) } }