185 lines
5.3 KiB
Go
185 lines
5.3 KiB
Go
// Package hyfile 处方监管 PDF 生成与上传。
|
||
//
|
||
// HtmlToPDF:chromedp 将 xk-api 打印 HTML 转为 PDF。
|
||
// UploadPDF:POST PDF 二进制体(Content-Type: application/pdf),Header 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。
|
||
//
|
||
// uploadURL:FILE_UPLOAD_VIA_FORWARD=true 时为 forward 地址;否则为云端 config 返回的政务云 URL。
|
||
// uploadToken:transit 按 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_BUCKET(policy scope 仅 bucket 名,不含文件名)",
|
||
"③token=AccessKey:encodedSign:policyB64;sign=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]
|
||
}
|