package shunyunbao import ( "bufio" "bytes" "context" "errors" "io" "mime" "net/http" "net/url" "strconv" "strings" "cmroubao/backend-api/internal/domain" "cmroubao/backend-api/internal/usecase" ) const ( ProductImagePath = "/api/p/file" maxProductImageBytes = 20 << 20 productImageSniffLen = 512 ) func (manager *SessionManager) FetchProductImage( ctx context.Context, productThumbRef string, ) (usecase.FreightSourceImage, error) { productThumbRef = strings.TrimSpace(productThumbRef) value, err := strconv.ParseUint(productThumbRef, 10, 64) if err != nil || value == 0 || strconv.FormatUint(value, 10) != productThumbRef { return usecase.FreightSourceImage{}, domain.ErrFreightImageInvalid } manager.mu.Lock() if !manager.configuredLocked() { manager.mu.Unlock() return usecase.FreightSourceImage{}, domain.ErrFreightSourceNotConfigured } if !manager.authenticated { manager.mu.Unlock() return usecase.FreightSourceImage{}, domain.ErrFreightSourceSessionNeeded } baseURL := manager.baseURL headers := manager.headers.Clone() client := manager.http manager.mu.Unlock() query := url.Values{} query.Set("id", productThumbRef) request, err := http.NewRequestWithContext( ctx, http.MethodGet, baseURL+ProductImagePath+"?"+query.Encode(), nil, ) if err != nil { return usecase.FreightSourceImage{}, domain.ErrFreightSourceUnavailable } for name, values := range headers { request.Header[name] = append([]string(nil), values...) } request.Header.Set("Accept", "image/jpeg, image/png, image/webp") manager.logERPRequest(request) response, err := client.Do(request) if err != nil { manager.logERPTransportFailure(request) return usecase.FreightSourceImage{}, domain.ErrFreightSourceUnavailable } if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { defer response.Body.Close() manager.logERPBinaryResponse(request, response) switch response.StatusCode { case http.StatusUnauthorized, http.StatusForbidden: manager.mu.Lock() manager.clearAuthenticatedLocked() manager.mu.Unlock() return usecase.FreightSourceImage{}, domain.ErrFreightSourceSessionNeeded case http.StatusNotFound: return usecase.FreightSourceImage{}, domain.ErrFreightImageNotFound default: return usecase.FreightSourceImage{}, domain.ErrFreightSourceUnavailable } } if response.ContentLength > maxProductImageBytes { response.Body.Close() manager.logERPBinaryResponse(request, response) return usecase.FreightSourceImage{}, domain.ErrFreightImageInvalid } mediaType, content, err := productImageContent( response.Header.Get("Content-Type"), response.Body, ) if err != nil { response.Body.Close() manager.logERPBinaryResponse(request, response) return usecase.FreightSourceImage{}, domain.ErrFreightImageInvalid } manager.logERPBinaryResponse(request, response) return usecase.FreightSourceImage{ Content: content, MediaType: mediaType, }, nil } func productImageContent( contentType string, body io.ReadCloser, ) (string, io.ReadCloser, error) { contentType = strings.TrimSpace(contentType) if contentType != "" { mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { return "", nil, err } mediaType = strings.ToLower(mediaType) if supportedProductImageType(mediaType) { return mediaType, body, nil } if mediaType != "application/octet-stream" { return "", nil, domain.ErrFreightImageInvalid } } reader := bufio.NewReaderSize(body, productImageSniffLen) prefix, err := reader.Peek(productImageSniffLen) if err != nil && !errors.Is(err, io.EOF) { return "", nil, err } if len(prefix) == 0 { return "", nil, domain.ErrFreightImageInvalid } mediaType := detectProductImageType(prefix) if !supportedProductImageType(mediaType) { return "", nil, domain.ErrFreightImageInvalid } return mediaType, struct { io.Reader io.Closer }{ Reader: reader, Closer: body, }, nil } func detectProductImageType(prefix []byte) string { mediaType := strings.ToLower(http.DetectContentType(prefix)) if supportedProductImageType(mediaType) { return mediaType } if len(prefix) >= 12 && bytes.Equal(prefix[:4], []byte("RIFF")) && bytes.Equal(prefix[8:12], []byte("WEBP")) { return "image/webp" } return mediaType } func supportedProductImageType(value string) bool { switch strings.ToLower(value) { case "image/jpeg", "image/png", "image/webp": return true default: return false } } func (manager *SessionManager) logERPBinaryResponse( request *http.Request, response *http.Response, ) { if !manager.diagnosticsOn { return } byteCount := "unknown" if response.ContentLength >= 0 { byteCount = strconv.FormatInt(response.ContentLength, 10) } manager.diagnosticLog( "erp_response method=" + request.Method + " path=" + request.URL.EscapedPath() + " status=" + strconv.Itoa(response.StatusCode) + " content_type=" + diagnosticContentType(response.Header.Get("Content-Type")) + " bytes=" + byteCount + " body=omitted_binary", ) } var _ usecase.FreightImageSource = (*SessionManager)(nil)