// Package http 实现通过 HTTP API 调用 Python 服务的清洗器 package http import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" "path/filepath" "time" "billai-server/adapter" ) // CleanRequest HTTP 清洗请求 type CleanRequest struct { InputPath string `json:"input_path"` OutputPath string `json:"output_path"` Year string `json:"year,omitempty"` Month string `json:"month,omitempty"` Start string `json:"start,omitempty"` End string `json:"end,omitempty"` Format string `json:"format,omitempty"` BillType string `json:"bill_type,omitempty"` } // CleanResponse HTTP 清洗响应 type CleanResponse struct { Success bool `json:"success"` BillType string `json:"bill_type"` Message string `json:"message"` OutputPath string `json:"output_path,omitempty"` } // ErrorResponse 错误响应 type ErrorResponse struct { Detail string `json:"detail"` } // Cleaner 通过 HTTP API 调用 Python 服务的清洗器实现 type Cleaner struct { baseURL string // Python 服务基础 URL httpClient *http.Client // HTTP 客户端 } // NewCleaner 创建 HTTP 清洗器 func NewCleaner(baseURL string) *Cleaner { return &Cleaner{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 60 * time.Second, // 清洗可能需要较长时间 }, } } // NewCleanerWithClient 使用自定义 HTTP 客户端创建清洗器 func NewCleanerWithClient(baseURL string, client *http.Client) *Cleaner { return &Cleaner{ baseURL: baseURL, httpClient: client, } } // Clean 执行账单清洗(使用文件上传方式) func (c *Cleaner) Clean(inputPath, outputPath string, opts *adapter.CleanOptions) (*adapter.CleanResult, error) { // 打开输入文件 file, err := os.Open(inputPath) if err != nil { return nil, fmt.Errorf("打开文件失败: %w", err) } defer file.Close() // 创建 multipart form var body bytes.Buffer writer := multipart.NewWriter(&body) // 添加文件 part, err := writer.CreateFormFile("file", filepath.Base(inputPath)) if err != nil { return nil, fmt.Errorf("创建表单文件失败: %w", err) } if _, err := io.Copy(part, file); err != nil { return nil, fmt.Errorf("复制文件内容失败: %w", err) } // 添加其他参数 if opts != nil { if opts.Year != "" { writer.WriteField("year", opts.Year) } if opts.Month != "" { writer.WriteField("month", opts.Month) } if opts.Start != "" { writer.WriteField("start", opts.Start) } if opts.End != "" { writer.WriteField("end", opts.End) } if opts.Format != "" { writer.WriteField("format", opts.Format) } } writer.WriteField("bill_type", "auto") writer.Close() // 发送上传请求 fmt.Printf("🌐 调用清洗服务: %s/clean/upload\n", c.baseURL) req, err := http.NewRequest("POST", c.baseURL+"/clean/upload", &body) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("HTTP 请求失败: %w", err) } defer resp.Body.Close() // 读取响应 respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } // 处理错误响应 if resp.StatusCode != http.StatusOK { var errResp ErrorResponse if err := json.Unmarshal(respBody, &errResp); err == nil { return nil, fmt.Errorf("清洗失败: %s", errResp.Detail) } return nil, fmt.Errorf("清洗失败: HTTP %d - %s", resp.StatusCode, string(respBody)) } // 解析成功响应 var cleanResp CleanResponse if err := json.Unmarshal(respBody, &cleanResp); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 下载清洗后的文件 if cleanResp.OutputPath != "" { if err := c.downloadFile(cleanResp.OutputPath, outputPath); err != nil { return nil, fmt.Errorf("下载清洗结果失败: %w", err) } } return &adapter.CleanResult{ BillType: cleanResp.BillType, Output: cleanResp.Message, }, nil } // downloadFile 下载清洗后的文件 func (c *Cleaner) downloadFile(remotePath, localPath string) error { // 构建下载 URL downloadURL := fmt.Sprintf("%s/clean/download/%s", c.baseURL, remotePath) resp, err := c.httpClient.Get(downloadURL) if err != nil { return fmt.Errorf("下载请求失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("下载失败: HTTP %d", resp.StatusCode) } // 创建本地文件 out, err := os.Create(localPath) if err != nil { return fmt.Errorf("创建文件失败: %w", err) } defer out.Close() // 写入文件内容 if _, err := io.Copy(out, resp.Body); err != nil { return fmt.Errorf("写入文件失败: %w", err) } return nil } // HealthCheck 检查 Python 服务健康状态 func (c *Cleaner) HealthCheck() error { resp, err := c.httpClient.Get(c.baseURL + "/health") if err != nil { return fmt.Errorf("健康检查失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("服务不健康: HTTP %d", resp.StatusCode) } return nil } // 确保 Cleaner 实现了 adapter.Cleaner 接口 var _ adapter.Cleaner = (*Cleaner)(nil)