Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server
This commit is contained in:
@@ -90,6 +90,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./resources/Pure_Component:/app/resources/Pure_Component
|
- ./resources/Pure_Component:/app/resources/Pure_Component
|
||||||
|
# 持久化PDF缓存目录,确保生成的PDF在容器重启后仍然存在
|
||||||
|
- ./storage/pdfg-cache:/app/storage/pdfg-cache
|
||||||
# user: "1001:1001" # 注释掉,使用root权限运行
|
# user: "1001:1001" # 注释掉,使用root权限运行
|
||||||
networks:
|
networks:
|
||||||
- tyapi-network
|
- tyapi-network
|
||||||
@@ -105,14 +107,6 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
cpus: "1.0"
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
cpus: "0.3"
|
|
||||||
|
|
||||||
# TYAPI Worker 服务
|
# TYAPI Worker 服务
|
||||||
tyapi-worker:
|
tyapi-worker:
|
||||||
@@ -147,14 +141,6 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 512M
|
|
||||||
cpus: "0.5"
|
|
||||||
reservations:
|
|
||||||
memory: 128M
|
|
||||||
cpus: "0.1"
|
|
||||||
|
|
||||||
# Asynq 任务监控 (生产环境)
|
# Asynq 任务监控 (生产环境)
|
||||||
asynq-monitor:
|
asynq-monitor:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ type PDFG01GZReq struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"` // IVYZ5A9O需要
|
|
||||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"` // 授权标识,0或1
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"` // 授权标识,0或1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package pdfg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -31,24 +33,25 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
// 获取全局logger
|
// 获取全局logger
|
||||||
zapLogger := logger.GetGlobalLogger()
|
zapLogger := logger.GetGlobalLogger()
|
||||||
|
|
||||||
// Debug:记录入口参数
|
// Debug:记录入口参数(使用 Info 级别便于线上查看)
|
||||||
zapLogger.Debug("PDFG01GZ请求开始",
|
logger.L().Info("PDFG01GZ请求开始",
|
||||||
zap.String("name", paramsDto.Name),
|
zap.String("name", paramsDto.Name),
|
||||||
zap.String("id_card", paramsDto.IDCard),
|
zap.String("id_card", paramsDto.IDCard),
|
||||||
zap.String("mobile_no", paramsDto.MobileNo),
|
zap.String("mobile_no", paramsDto.MobileNo),
|
||||||
zap.String("authorized", paramsDto.Authorized),
|
zap.String("authorized", paramsDto.Authorized),
|
||||||
zap.String("auth_authorize_file_code", paramsDto.AuthAuthorizeFileCode),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 从context获取config(如果存在)
|
// 从context获取config(如果存在)
|
||||||
var cacheTTL time.Duration = 24 * time.Hour
|
var cacheTTL time.Duration = 24 * time.Hour
|
||||||
var cacheDir string
|
var cacheDir string
|
||||||
|
var apiDomain string
|
||||||
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
||||||
cacheTTL = cfg.PDFGen.Cache.TTL
|
cacheTTL = cfg.PDFGen.Cache.TTL
|
||||||
if cacheTTL == 0 {
|
if cacheTTL == 0 {
|
||||||
cacheTTL = 24 * time.Hour
|
cacheTTL = 24 * time.Hour
|
||||||
}
|
}
|
||||||
cacheDir = cfg.PDFGen.Cache.CacheDir
|
cacheDir = cfg.PDFGen.Cache.CacheDir
|
||||||
|
apiDomain = cfg.API.Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取最大缓存大小
|
// 获取最大缓存大小
|
||||||
@@ -56,8 +59,8 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
||||||
maxSize = cfg.PDFGen.Cache.MaxSize
|
maxSize = cfg.PDFGen.Cache.MaxSize
|
||||||
|
|
||||||
// Debug:记录PDF生成服务配置
|
// Debug:记录PDF生成服务配置(使用 Info 级别便于线上查看)
|
||||||
zapLogger.Debug("PDFG01GZ加载配置",
|
logger.L().Info("PDFG01GZ加载配置",
|
||||||
zap.String("env", cfg.App.Env),
|
zap.String("env", cfg.App.Env),
|
||||||
zap.String("pdfgen_production_url", cfg.PDFGen.ProductionURL),
|
zap.String("pdfgen_production_url", cfg.PDFGen.ProductionURL),
|
||||||
zap.String("pdfgen_development_url", cfg.PDFGen.DevelopmentURL),
|
zap.String("pdfgen_development_url", cfg.PDFGen.DevelopmentURL),
|
||||||
@@ -93,31 +96,8 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
pdfGenService = pdfgen.NewPDFGenService(defaultCfg, zapLogger)
|
pdfGenService = pdfgen.NewPDFGenService(defaultCfg, zapLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查缓存
|
// 直接生成PDF,不检查缓存(每次都重新生成)
|
||||||
_, hit, createdAt, err := cacheManager.Get(paramsDto.Name, paramsDto.IDCard)
|
zapLogger.Info("开始生成PDF",
|
||||||
if err != nil {
|
|
||||||
zapLogger.Warn("检查缓存失败,继续生成PDF", zap.Error(err))
|
|
||||||
} else if hit {
|
|
||||||
// 缓存命中,模拟慢几秒
|
|
||||||
zapLogger.Info("PDF缓存命中,返回缓存文件",
|
|
||||||
zap.String("name", paramsDto.Name),
|
|
||||||
zap.String("id_card", paramsDto.IDCard),
|
|
||||||
zap.Time("created_at", createdAt),
|
|
||||||
)
|
|
||||||
// 模拟慢几秒(2-4秒)
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// 生成下载链接
|
|
||||||
downloadURL := generateDownloadURL(paramsDto.Name, paramsDto.IDCard)
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
"download_url": downloadURL,
|
|
||||||
"cached": true,
|
|
||||||
"created_at": createdAt.Format(time.RFC3339),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存未命中,需要生成PDF
|
|
||||||
zapLogger.Info("PDF缓存未命中,开始生成PDF",
|
|
||||||
zap.String("name", paramsDto.Name),
|
zap.String("name", paramsDto.Name),
|
||||||
zap.String("id_card", paramsDto.IDCard),
|
zap.String("id_card", paramsDto.IDCard),
|
||||||
)
|
)
|
||||||
@@ -130,7 +110,7 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
|
|
||||||
// 打印完整的apiData(为避免日志过大,这里直接序列化为JSON字符串)
|
// 打印完整的apiData(为避免日志过大,这里直接序列化为JSON字符串)
|
||||||
apiDataBytes, _ := json.Marshal(apiData)
|
apiDataBytes, _ := json.Marshal(apiData)
|
||||||
zapLogger.Debug("PDFG01GZ数据准备完成",
|
logger.L().Info("PDFG01GZ数据准备完成",
|
||||||
zap.Int("api_data_count", len(apiData)),
|
zap.Int("api_data_count", len(apiData)),
|
||||||
zap.Int("formatted_items", len(formattedData)),
|
zap.Int("formatted_items", len(formattedData)),
|
||||||
zap.ByteString("api_data", apiDataBytes),
|
zap.ByteString("api_data", apiDataBytes),
|
||||||
@@ -159,11 +139,10 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
|
|
||||||
// 调用PDF生成服务
|
// 调用PDF生成服务
|
||||||
// 即使部分子处理器失败,只要有APPLICANT_BASIC_INFO就可以生成PDF
|
// 即使部分子处理器失败,只要有APPLICANT_BASIC_INFO就可以生成PDF
|
||||||
zapLogger.Debug("PDFG01GZ开始调用PDF生成服务",
|
logger.L().Info("PDFG01GZ开始调用PDF生成服务",
|
||||||
zap.String("report_number", reportNumber),
|
zap.String("report_number", reportNumber),
|
||||||
zap.Int("data_items", len(formattedData)),
|
zap.Int("data_items", len(formattedData)),
|
||||||
)
|
)
|
||||||
|
|
||||||
pdfResp, err := pdfGenService.GenerateGuangzhouPDF(ctx, pdfReq)
|
pdfResp, err := pdfGenService.GenerateGuangzhouPDF(ctx, pdfReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zapLogger.Error("生成PDF失败",
|
zapLogger.Error("生成PDF失败",
|
||||||
@@ -173,26 +152,33 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("生成PDF失败: %w", err))
|
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("生成PDF失败: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到缓存
|
// 生成报告ID(每次请求都生成唯一的ID)
|
||||||
if err := cacheManager.Set(paramsDto.Name, paramsDto.IDCard, pdfResp.PDFBytes); err != nil {
|
reportID := generateReportID()
|
||||||
|
|
||||||
|
// 保存到缓存(基于报告ID,文件名包含时间戳确保唯一性)
|
||||||
|
if err := cacheManager.SetByReportID(reportID, pdfResp.PDFBytes); err != nil {
|
||||||
zapLogger.Warn("保存PDF到缓存失败", zap.Error(err))
|
zapLogger.Warn("保存PDF到缓存失败", zap.Error(err))
|
||||||
// 不影响返回结果,只记录警告
|
// 不影响返回结果,只记录警告
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成下载链接
|
// 生成下载链接(基于报告ID)
|
||||||
downloadURL := generateDownloadURL(paramsDto.Name, paramsDto.IDCard)
|
downloadURL := generateDownloadURL(apiDomain, reportID)
|
||||||
|
expiresAt := time.Now().Add(cacheTTL)
|
||||||
|
|
||||||
zapLogger.Info("PDF生成成功",
|
zapLogger.Info("PDF生成成功",
|
||||||
zap.String("name", paramsDto.Name),
|
zap.String("name", paramsDto.Name),
|
||||||
zap.String("id_card", paramsDto.IDCard),
|
zap.String("id_card", paramsDto.IDCard),
|
||||||
|
zap.String("report_id", reportID),
|
||||||
zap.String("report_number", reportNumber),
|
zap.String("report_number", reportNumber),
|
||||||
zap.String("download_url", downloadURL),
|
zap.String("download_url", downloadURL),
|
||||||
)
|
)
|
||||||
|
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
"download_url": downloadURL,
|
"download_url": downloadURL,
|
||||||
|
"report_id": reportID,
|
||||||
"report_number": reportNumber,
|
"report_number": reportNumber,
|
||||||
"cached": false,
|
"expires_at": expiresAt.Format(time.RFC3339),
|
||||||
|
"ttl_seconds": int(cacheTTL.Seconds()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,35 +196,29 @@ func collectAPIData(ctx context.Context, params dto.PDFG01GZReq, deps *processor
|
|||||||
|
|
||||||
results := make(chan processorResult, 5)
|
results := make(chan processorResult, 5)
|
||||||
|
|
||||||
// 调用IVYZ5A9O - 需要: name, id_card, auth_authorize_file_code
|
// 调用JRZQ0L85 - 需要: name, id_card, mobile_no
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
logger.Error("调用IVYZ5A9O处理器时发生panic",
|
logger.Error("调用JRZQ0L85处理器时发生panic",
|
||||||
zap.Any("panic", r),
|
zap.Any("panic", r),
|
||||||
)
|
)
|
||||||
results <- processorResult{"IVYZ5A9O", nil, fmt.Errorf("处理器panic: %v", r)}
|
results <- processorResult{"JRZQ0L85", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
// 检查必需字段
|
jrzq0l85Params := map[string]interface{}{
|
||||||
if params.AuthAuthorizeFileCode == "" {
|
"name": params.Name,
|
||||||
logger.Warn("IVYZ5A9O缺少auth_authorize_file_code字段,跳过调用")
|
"id_card": params.IDCard,
|
||||||
results <- processorResult{"IVYZ5A9O", nil, fmt.Errorf("缺少必需字段: auth_authorize_file_code")}
|
"mobile_no": params.MobileNo,
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ivyzParams := map[string]interface{}{
|
paramsBytes, err := json.Marshal(jrzq0l85Params)
|
||||||
"name": params.Name,
|
|
||||||
"id_card": params.IDCard,
|
|
||||||
"auth_authorize_file_code": params.AuthAuthorizeFileCode,
|
|
||||||
}
|
|
||||||
paramsBytes, err := json.Marshal(ivyzParams)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("序列化IVYZ5A9O参数失败", zap.Error(err))
|
logger.Warn("序列化JRZQ0L85参数失败", zap.Error(err))
|
||||||
results <- processorResult{"IVYZ5A9O", nil, err}
|
results <- processorResult{"JRZQ0L85", nil, err}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := callProcessor(ctx, "IVYZ5A9O", paramsBytes, deps)
|
data, err := callProcessor(ctx, "JRZQ0L85", paramsBytes, deps)
|
||||||
results <- processorResult{"IVYZ5A9O", data, err}
|
results <- processorResult{"JRZQ0L85", data, err}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 调用JRZQ8A2D - 需要: name, id_card, mobile_no, authorized
|
// 调用JRZQ8A2D - 需要: name, id_card, mobile_no, authorized
|
||||||
@@ -412,17 +392,17 @@ func formatDataForPDF(apiData map[string]interface{}, params dto.PDFG01GZReq, lo
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2. IVYZ5A9O - 自然人综合风险智能评估模型
|
// 2. JRZQ0L85 - 自然人综合风险智能评估模型(替代原IVYZ5A9O)
|
||||||
if data, ok := apiData["IVYZ5A9O"]; ok && data != nil {
|
if data, ok := apiData["JRZQ0L85"]; ok && data != nil {
|
||||||
result = append(result, map[string]interface{}{
|
result = append(result, map[string]interface{}{
|
||||||
"apiID": "IVYZ5A9O",
|
"apiID": "JRZQ0L85",
|
||||||
"data": data,
|
"data": data,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// 子处理器失败或无数据时,返回空对象 {}
|
// 子处理器失败或无数据时,返回空对象 {}
|
||||||
logger.Debug("IVYZ5A9O数据缺失,使用空对象")
|
logger.Debug("JRZQ0L85数据缺失,使用空对象")
|
||||||
result = append(result, map[string]interface{}{
|
result = append(result, map[string]interface{}{
|
||||||
"apiID": "IVYZ5A9O",
|
"apiID": "JRZQ0L85",
|
||||||
"data": map[string]interface{}{},
|
"data": map[string]interface{}{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -491,10 +471,29 @@ func generateReportNumber() string {
|
|||||||
return fmt.Sprintf("RPT%s", time.Now().Format("20060102150405"))
|
return fmt.Sprintf("RPT%s", time.Now().Format("20060102150405"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateDownloadURL 生成下载链接
|
// generateReportID 生成对外可见的报告ID
|
||||||
func generateDownloadURL(name, idCard string) string {
|
// 每次请求都生成唯一的ID,格式:{report_number}-{随机字符串}
|
||||||
// 这里应该生成实际的下载URL
|
// 注意:不再包含cacheKey,因为每次请求都会重新生成,不需要通过ID定位缓存文件
|
||||||
// 暂时返回一个占位符,实际应该根据服务器配置生成
|
func generateReportID() string {
|
||||||
return fmt.Sprintf("/api/v1/pdfg/download?name=%s&id_card=%s", name, idCard)
|
reportNumber := generateReportNumber()
|
||||||
|
// 生成8字节随机字符串,确保每次请求ID都不同
|
||||||
|
randomBytes := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(randomBytes); err != nil {
|
||||||
|
// 如果随机数生成失败,使用纳秒时间戳作为后备
|
||||||
|
randomBytes = []byte(fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||||
|
}
|
||||||
|
randomStr := hex.EncodeToString(randomBytes)
|
||||||
|
return fmt.Sprintf("%s-%s", reportNumber, randomStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDownloadURL 生成下载链接(基于报告ID/缓存键)
|
||||||
|
// apiDomain: 外部可访问的API域名,如 api.tianyuanapi.com
|
||||||
|
func generateDownloadURL(apiDomain, reportID string) string {
|
||||||
|
if apiDomain == "" {
|
||||||
|
// 兜底:保留相对路径,方便本地/测试环境使用
|
||||||
|
return fmt.Sprintf("/api/v1/pdfg/download?id=%s", reportID)
|
||||||
|
}
|
||||||
|
// 生成完整链接,例如:https://api.tianyuanapi.com/api/v1/pdfg/download?id=xxx
|
||||||
|
return fmt.Sprintf("https://%s/api/v1/pdfg/download?id=%s", apiDomain, reportID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,12 +93,6 @@ func (s *PDFGenService) GenerateGuangzhouPDF(ctx context.Context, req *GenerateP
|
|||||||
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug:打印请求体预览(最多1024字节),防止日志过大
|
|
||||||
bodyPreview := reqBody
|
|
||||||
if len(bodyPreview) > 1024 {
|
|
||||||
bodyPreview = bodyPreview[:1024]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建请求URL
|
// 构建请求URL
|
||||||
url := fmt.Sprintf("%s%s", s.baseURL, s.apiPath)
|
url := fmt.Sprintf("%s%s", s.baseURL, s.apiPath)
|
||||||
|
|
||||||
@@ -117,7 +111,7 @@ func (s *PDFGenService) GenerateGuangzhouPDF(ctx context.Context, req *GenerateP
|
|||||||
s.logger.Info("开始调用PDF生成服务",
|
s.logger.Info("开始调用PDF生成服务",
|
||||||
zap.String("url", url),
|
zap.String("url", url),
|
||||||
zap.Int("data_count", len(req.Data)),
|
zap.Int("data_count", len(req.Data)),
|
||||||
zap.ByteString("body_preview", bodyPreview),
|
zap.ByteString("reqBody", reqBody),
|
||||||
)
|
)
|
||||||
|
|
||||||
resp, err := s.client.Do(httpReq)
|
resp, err := s.client.Do(httpReq)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -33,22 +34,20 @@ func NewPDFGHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DownloadPDF 下载PDF文件
|
// DownloadPDF 下载PDF文件
|
||||||
// GET /api/v1/pdfg/download?name=xxx&id_card=xxx
|
// GET /api/v1/pdfg/download?id=报告ID
|
||||||
func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
||||||
name := c.Query("name")
|
reportID := c.Query("id")
|
||||||
idCard := c.Query("id_card")
|
|
||||||
|
|
||||||
if name == "" || idCard == "" {
|
if reportID == "" {
|
||||||
h.responseBuilder.BadRequest(c, "姓名和身份证号不能为空")
|
h.responseBuilder.BadRequest(c, "报告ID不能为空")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从缓存获取PDF
|
// 通过报告ID获取PDF文件
|
||||||
pdfBytes, hit, createdAt, err := h.cacheManager.Get(name, idCard)
|
pdfBytes, hit, createdAt, err := h.cacheManager.GetByReportID(reportID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("获取PDF缓存失败",
|
h.logger.Error("获取PDF缓存失败",
|
||||||
zap.String("name", name),
|
zap.String("report_id", reportID),
|
||||||
zap.String("id_card", idCard),
|
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
h.responseBuilder.InternalError(c, "获取PDF文件失败")
|
h.responseBuilder.InternalError(c, "获取PDF文件失败")
|
||||||
@@ -57,8 +56,7 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
|||||||
|
|
||||||
if !hit {
|
if !hit {
|
||||||
h.logger.Warn("PDF文件不存在或已过期",
|
h.logger.Warn("PDF文件不存在或已过期",
|
||||||
zap.String("name", name),
|
zap.String("report_id", reportID),
|
||||||
zap.String("id_card", idCard),
|
|
||||||
)
|
)
|
||||||
h.responseBuilder.NotFound(c, "PDF文件不存在或已过期,请重新生成")
|
h.responseBuilder.NotFound(c, "PDF文件不存在或已过期,请重新生成")
|
||||||
return
|
return
|
||||||
@@ -68,8 +66,7 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
|||||||
expiresAt := createdAt.Add(24 * time.Hour)
|
expiresAt := createdAt.Add(24 * time.Hour)
|
||||||
if time.Now().After(expiresAt) {
|
if time.Now().After(expiresAt) {
|
||||||
h.logger.Warn("PDF文件已过期",
|
h.logger.Warn("PDF文件已过期",
|
||||||
zap.String("name", name),
|
zap.String("report_id", reportID),
|
||||||
zap.String("id_card", idCard),
|
|
||||||
zap.Time("expires_at", expiresAt),
|
zap.Time("expires_at", expiresAt),
|
||||||
)
|
)
|
||||||
h.responseBuilder.NotFound(c, "PDF文件已过期,请重新生成")
|
h.responseBuilder.NotFound(c, "PDF文件已过期,请重新生成")
|
||||||
@@ -78,15 +75,21 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
|||||||
|
|
||||||
// 设置响应头
|
// 设置响应头
|
||||||
c.Header("Content-Type", "application/pdf")
|
c.Header("Content-Type", "application/pdf")
|
||||||
c.Header("Content-Disposition", `attachment; filename="大数据租赁风险报告.pdf"`)
|
// 使用报告ID前缀作为下载文件名的一部分
|
||||||
|
filename := "大数据租赁风险报告.pdf"
|
||||||
|
if idx := strings.LastIndex(reportID, "-"); idx > 0 {
|
||||||
|
// 使用前缀(报告编号部分)作为文件名的一部分
|
||||||
|
prefix := reportID[:idx]
|
||||||
|
filename = fmt.Sprintf("大数据租赁风险报告_%s.pdf", prefix)
|
||||||
|
}
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
c.Header("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
|
c.Header("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
|
||||||
|
|
||||||
// 发送PDF文件
|
// 发送PDF文件
|
||||||
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||||
|
|
||||||
h.logger.Info("PDF文件下载成功",
|
h.logger.Info("PDF文件下载成功",
|
||||||
zap.String("name", name),
|
zap.String("report_id", reportID),
|
||||||
zap.String("id_card", idCard),
|
|
||||||
zap.Int("file_size", len(pdfBytes)),
|
zap.Int("file_size", len(pdfBytes)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ func (m *PDFCacheManager) GetCacheKeyByProduct(productID, version string) string
|
|||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCacheKeyByReportID 生成缓存键(基于报告ID)
|
||||||
|
// 文件名格式:MD5(report_id).pdf
|
||||||
|
// report_id 本身已经包含时间戳和随机数,所以 MD5 后就是唯一的
|
||||||
|
func (m *PDFCacheManager) GetCacheKeyByReportID(reportID string) string {
|
||||||
|
hash := md5.Sum([]byte(reportID))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
// GetCachePath 获取缓存文件路径
|
// GetCachePath 获取缓存文件路径
|
||||||
func (m *PDFCacheManager) GetCachePath(cacheKey string) string {
|
func (m *PDFCacheManager) GetCachePath(cacheKey string) string {
|
||||||
return filepath.Join(m.cacheDir, fmt.Sprintf("%s.pdf", cacheKey))
|
return filepath.Join(m.cacheDir, fmt.Sprintf("%s.pdf", cacheKey))
|
||||||
@@ -98,6 +106,27 @@ func (m *PDFCacheManager) GetByProduct(productID, version string) ([]byte, bool,
|
|||||||
return pdfBytes, hit, err
|
return pdfBytes, hit, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByCacheKey 通过缓存键直接获取PDF文件
|
||||||
|
// 适用于已经持久化了缓存键(例如作为报告ID)的场景
|
||||||
|
// 返回PDF字节流、是否命中缓存、文件创建时间、错误
|
||||||
|
func (m *PDFCacheManager) GetByCacheKey(cacheKey string) ([]byte, bool, time.Time, error) {
|
||||||
|
return m.getByKey(cacheKey, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetByReportID 将PDF文件保存到缓存(基于报告ID)
|
||||||
|
func (m *PDFCacheManager) SetByReportID(reportID string, pdfBytes []byte) error {
|
||||||
|
cacheKey := m.GetCacheKeyByReportID(reportID)
|
||||||
|
return m.setByKey(cacheKey, pdfBytes, reportID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByReportID 从缓存获取PDF文件(基于报告ID)
|
||||||
|
// 直接通过 report_id 的 MD5 计算文件名,无需遍历
|
||||||
|
// 返回PDF字节流、是否命中缓存、文件创建时间、错误
|
||||||
|
func (m *PDFCacheManager) GetByReportID(reportID string) ([]byte, bool, time.Time, error) {
|
||||||
|
cacheKey := m.GetCacheKeyByReportID(reportID)
|
||||||
|
return m.getByKey(cacheKey, reportID, "")
|
||||||
|
}
|
||||||
|
|
||||||
// getByKey 内部方法:根据缓存键获取文件
|
// getByKey 内部方法:根据缓存键获取文件
|
||||||
func (m *PDFCacheManager) getByKey(cacheKey string, key1, key2 string) ([]byte, bool, time.Time, error) {
|
func (m *PDFCacheManager) getByKey(cacheKey string, key1, key2 string) ([]byte, bool, time.Time, error) {
|
||||||
cachePath := m.GetCachePath(cacheKey)
|
cachePath := m.GetCachePath(cacheKey)
|
||||||
|
|||||||
Reference in New Issue
Block a user