Files
xk-hy-transit-go/internal/hyfile/upload.go
2026-05-28 17:03:08 +08:00

185 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package hyfile 处方监管 PDF 生成与上传。
//
// HtmlToPDFchromedp 将 xk-api 打印 HTML 转为 PDF。
// UploadPDFPOST PDF 二进制体Content-Type: application/pdfHeader X-Authorization 为 uploadToken
// 不使用 multipart/form-data。默认经 forward-go /mng/file/auth/upload 转发至政务云 28211。
package hyfile
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
)
var (
uploadInsecureSkipVerify bool
uploadMu sync.RWMutex
)
// SetUploadInsecureSkipVerify 政务网 HTTPS 证书校验(与 forward-go 一致,由配置注入)。
func SetUploadInsecureSkipVerify(skip bool) {
uploadMu.Lock()
uploadInsecureSkipVerify = skip
defer uploadMu.Unlock()
}
func uploadTLSConfig() *tls.Config {
uploadMu.RLock()
skip := uploadInsecureSkipVerify
uploadMu.RUnlock()
if !skip {
return nil
}
return &tls.Config{InsecureSkipVerify: true} //nolint:gosec // 政务网自签证书
}
func uploadHTTPClient() *http.Client {
transport := &http.Transport{}
if cfg := uploadTLSConfig(); cfg != nil {
transport.TLSClientConfig = cfg
}
return &http.Client{Timeout: 120 * time.Second, Transport: transport}
}
// UploadPDF 上传 PDF 至监管文件服务(通常经 forward-go成功时返回 record.fileId。
//
// uploadURLFILE_UPLOAD_VIA_FORWARD=true 时为 forward 地址;否则为云端 config 返回的政务云 URL。
// uploadTokentransit 按 FileAuth.java 本地生成scope=仅 bucket放在 X-Authorization 请求头。
// filename 仅用于本机落盘命名,不参与 HTTP 请求体。
// forwardToken非空时设置 X-Forward-Token与 forward-go FORWARD_SHARED_SECRET 对应)。
func UploadPDF(pdf []byte, filename, uploadURL, uploadToken, forwardToken string) (string, error) {
_ = filename
if len(pdf) == 0 {
return "", fmt.Errorf("empty pdf")
}
if uploadURL == "" {
return "", fmt.Errorf("file upload url empty")
}
if uploadToken == "" {
return "", fmt.Errorf("upload token empty")
}
req, err := http.NewRequest(http.MethodPost, uploadURL, bytes.NewReader(pdf))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/pdf")
req.Header.Set("X-Authorization", uploadToken)
if forwardToken != "" {
req.Header.Set("X-Forward-Token", forwardToken)
}
acquireUploadSlot()
resp, err := uploadHTTPClient().Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", formatUploadHTTPError(resp.StatusCode, raw, uploadURL)
}
var parsed struct {
Success bool `json:"success"`
Record struct {
FileID string `json:"fileId"`
} `json:"record"`
Message string `json:"message"`
}
if err := json.Unmarshal(raw, &parsed); err != nil {
return "", fmt.Errorf("parse upload response: %w", err)
}
if !parsed.Success {
msg := parsed.Message
if msg == "" {
msg = string(truncate(raw, 512))
}
return "", fmt.Errorf("upload failed: %s", msg)
}
fileID := parsed.Record.FileID
if fileID == "" {
return "", fmt.Errorf("upload response missing record.fileId: %s", truncate(raw, 512))
}
return fileID, nil
}
func formatUploadHTTPError(status int, raw []byte, uploadURL string) error {
body := strings.TrimSpace(string(truncate(raw, 512)))
msg := fmt.Sprintf("upload http %d", status)
if body != "" {
msg += ": " + body
}
if status == http.StatusForbidden {
switch {
case strings.EqualFold(body, "forbidden"):
msg += ";可能为 forward-go 拒绝:检查 ALLOW_IPS、FORWARD_SHARED_SECRET 与 X-Forward-Token"
case body == "404":
msg += ";政务云 uploadToken 验签失败(非 forward forbidden核对 FileAuth.java scope 是否为仅 bucket 名、HMAC 输入为 policy JSON"
msg += upload403Hints(body)
default:
if extra := jsonMessageField(raw); extra != "" {
msg += ";政务云: " + extra
} else {
msg += ";可能为 uploadToken 无效/过期或 fileBucket(scope) 错误"
}
msg += upload403Hints(body)
}
}
if uploadURL != "" {
msg += fmt.Sprintf(" (url=%s)", uploadURL)
}
return fmt.Errorf("%s", msg)
}
func upload403Hints(body string) string {
lower := strings.ToLower(body)
var hints []string
if looksLikeUploadRateLimit(lower) {
hints = append(hints, "政务云文件上传约 10 秒 1 次transit 已全局限频;若仍 403 请拉长 FILE_UPLOAD_MIN_INTERVAL 或稍后重试")
}
hints = append(hints,
"按 FileAuth.java 排查①NTP 时钟deadline 为上传完成校验时间)",
"②HY_APP_KEY/HY_APP_SECRET/HY_FILE_BUCKETpolicy scope 仅 bucket 名,不含文件名)",
"③token=AccessKey:encodedSign:policyB64sign=HMAC-SHA1(secret, UTF-8(policyJSON))",
"④仍失败可设 FILEAUTH_B64=raw 后重启 transit",
"⑤/config 重新拉取",
)
return "" + strings.Join(hints, "")
}
func looksLikeUploadRateLimit(lower string) bool {
for _, kw := range []string{
"频率", "频繁", "too many", "rate limit", "ratelimit", "throttle", "limit exceeded",
} {
if strings.Contains(lower, kw) {
return true
}
}
return false
}
func jsonMessageField(raw []byte) string {
var v struct {
Message string `json:"message"`
}
if err := json.Unmarshal(raw, &v); err != nil {
return ""
}
return strings.TrimSpace(v.Message)
}
func truncate(b []byte, n int) []byte {
if len(b) <= n {
return b
}
return b[:n]
}