diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 6def26b..8b4dba1 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -90,6 +90,8 @@ services: volumes: - ./logs:/app/logs - ./resources/Pure_Component:/app/resources/Pure_Component + # 持久化PDF缓存目录,确保生成的PDF在容器重启后仍然存在 + - ./storage/pdfg-cache:/app/storage/pdfg-cache # user: "1001:1001" # 注释掉,使用root权限运行 networks: - tyapi-network diff --git a/internal/domains/api/services/processors/pdfg/pdfg01gz_processor.go b/internal/domains/api/services/processors/pdfg/pdfg01gz_processor.go index efa9b97..75e2a55 100644 --- a/internal/domains/api/services/processors/pdfg/pdfg01gz_processor.go +++ b/internal/domains/api/services/processors/pdfg/pdfg01gz_processor.go @@ -94,13 +94,16 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors pdfGenService = pdfgen.NewPDFGenService(defaultCfg, zapLogger) } + // 计算缓存键(内部使用,不直接暴露给用户) + cacheKey := cacheManager.GetCacheKey(paramsDto.Name, paramsDto.IDCard) + // 检查缓存(基于姓名+身份证) _, hit, createdAt, err := cacheManager.Get(paramsDto.Name, paramsDto.IDCard) if err != nil { zapLogger.Warn("检查缓存失败,继续生成PDF", zap.Error(err)) } else if hit { - // 计算缓存键,作为报告ID(可持久化) - reportID := cacheManager.GetCacheKey(paramsDto.Name, paramsDto.IDCard) + // 生成对外可见的报告ID(包含随机前缀,防止用户直接推断缓存键) + reportID := generateReportID(cacheKey) // 缓存命中,模拟生成耗时(约4秒) zapLogger.Info("PDF缓存命中,返回缓存文件", @@ -158,9 +161,6 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors reportNumber = generateReportNumber() } - // 计算报告ID(与缓存键一致,便于通过ID直接下载) - reportID := cacheManager.GetCacheKey(paramsDto.Name, paramsDto.IDCard) - // 构建PDF生成请求 pdfReq := &pdfgen.GeneratePDFRequest{ Data: formattedData, @@ -189,6 +189,9 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors // 不影响返回结果,只记录警告 } + // 生成报告ID(与内部缓存键解耦,对外只暴露带前缀的ID) + reportID := generateReportID(cacheKey) + // 生成下载链接(基于报告ID) downloadURL := generateDownloadURL(apiDomain, reportID) expiresAt := time.Now().Add(cacheTTL) @@ -499,6 +502,13 @@ func generateReportNumber() string { return fmt.Sprintf("RPT%s", time.Now().Format("20060102150405")) } +// generateReportID 生成对外可见的报告ID +// 结构:{report_number}-{cacheKey},前缀变化以避免用户轻易看出是否命中缓存 +func generateReportID(cacheKey string) string { + reportNumber := generateReportNumber() + return fmt.Sprintf("%s-%s", reportNumber, cacheKey) +} + // generateDownloadURL 生成下载链接(基于报告ID/缓存键) // apiDomain: 外部可访问的API域名,如 api.tianyuanapi.com func generateDownloadURL(apiDomain, reportID string) string { diff --git a/internal/infrastructure/http/handlers/pdfg_handler.go b/internal/infrastructure/http/handlers/pdfg_handler.go index 2f3f1b1..0376b05 100644 --- a/internal/infrastructure/http/handlers/pdfg_handler.go +++ b/internal/infrastructure/http/handlers/pdfg_handler.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -35,18 +36,25 @@ func NewPDFGHandler( // DownloadPDF 下载PDF文件 // GET /api/v1/pdfg/download?id=报告ID func (h *PDFGHandler) DownloadPDF(c *gin.Context) { - reportID := c.Query("id") + rawReportID := c.Query("id") - if reportID == "" { + if rawReportID == "" { h.responseBuilder.BadRequest(c, "报告ID不能为空") return } + // 从对外报告ID中解析内部缓存键(使用最后一个'-'后的部分作为缓存键) + cacheKey := rawReportID + if idx := strings.LastIndex(rawReportID, "-"); idx > 0 && idx < len(rawReportID)-1 { + cacheKey = rawReportID[idx+1:] + } + // 从缓存获取PDF - pdfBytes, hit, createdAt, err := h.cacheManager.GetByCacheKey(reportID) + pdfBytes, hit, createdAt, err := h.cacheManager.GetByCacheKey(cacheKey) if err != nil { h.logger.Error("获取PDF缓存失败", - zap.String("report_id", reportID), + zap.String("report_id", rawReportID), + zap.String("cache_key", cacheKey), zap.Error(err), ) h.responseBuilder.InternalError(c, "获取PDF文件失败") @@ -55,7 +63,8 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) { if !hit { h.logger.Warn("PDF文件不存在或已过期", - zap.String("report_id", reportID), + zap.String("report_id", rawReportID), + zap.String("cache_key", cacheKey), ) h.responseBuilder.NotFound(c, "PDF文件不存在或已过期,请重新生成") return @@ -65,7 +74,8 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) { expiresAt := createdAt.Add(24 * time.Hour) if time.Now().After(expiresAt) { h.logger.Warn("PDF文件已过期", - zap.String("report_id", reportID), + zap.String("report_id", rawReportID), + zap.String("cache_key", cacheKey), zap.Time("expires_at", expiresAt), ) h.responseBuilder.NotFound(c, "PDF文件已过期,请重新生成") @@ -74,14 +84,22 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) { // 设置响应头 c.Header("Content-Type", "application/pdf") - c.Header("Content-Disposition", `attachment; filename="大数据租赁风险报告.pdf"`) + // 使用报告ID前缀作为下载文件名的一部分,避免泄露内部缓存键 + filename := "大数据租赁风险报告.pdf" + if idx := strings.LastIndex(rawReportID, "-"); idx > 0 { + // 使用前缀(报告编号部分)作为文件名的一部分 + prefix := rawReportID[: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))) // 发送PDF文件 c.Data(http.StatusOK, "application/pdf", pdfBytes) h.logger.Info("PDF文件下载成功", - zap.String("report_id", reportID), + zap.String("report_id", rawReportID), + zap.String("cache_key", cacheKey), zap.Int("file_size", len(pdfBytes)), ) }