302 lines
9.6 KiB
Go
302 lines
9.6 KiB
Go
package handlers
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html/template"
|
||
"net/http"
|
||
"net/url"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
|
||
"hyapi-server/internal/application/api/commands"
|
||
"hyapi-server/internal/domains/api/dto"
|
||
api_repositories "hyapi-server/internal/domains/api/repositories"
|
||
api_services "hyapi-server/internal/domains/api/services"
|
||
"hyapi-server/internal/domains/api/services/processors"
|
||
"hyapi-server/internal/domains/api/services/processors/qygl"
|
||
"hyapi-server/internal/shared/pdf"
|
||
)
|
||
|
||
// QYGLReportHandler 企业全景报告页面渲染处理器
|
||
// 使用 QYGLJ1U9 聚合接口生成企业报告数据,并通过模板引擎渲染 qiye.html
|
||
type QYGLReportHandler struct {
|
||
apiRequestService *api_services.ApiRequestService
|
||
logger *zap.Logger
|
||
|
||
reportRepo api_repositories.ReportRepository
|
||
pdfCacheManager *pdf.PDFCacheManager
|
||
qyglPDFPregen *pdf.QYGLReportPDFPregen
|
||
}
|
||
|
||
// NewQYGLReportHandler 创建企业报告页面处理器
|
||
func NewQYGLReportHandler(
|
||
apiRequestService *api_services.ApiRequestService,
|
||
logger *zap.Logger,
|
||
reportRepo api_repositories.ReportRepository,
|
||
pdfCacheManager *pdf.PDFCacheManager,
|
||
qyglPDFPregen *pdf.QYGLReportPDFPregen,
|
||
) *QYGLReportHandler {
|
||
return &QYGLReportHandler{
|
||
apiRequestService: apiRequestService,
|
||
logger: logger,
|
||
reportRepo: reportRepo,
|
||
pdfCacheManager: pdfCacheManager,
|
||
qyglPDFPregen: qyglPDFPregen,
|
||
}
|
||
}
|
||
|
||
// GetQYGLReportPage 企业全景报告页面
|
||
// GET /reports/qygl?ent_code=xxx&ent_name=yyy&ent_reg_no=zzz
|
||
func (h *QYGLReportHandler) GetQYGLReportPage(c *gin.Context) {
|
||
// 读取查询参数
|
||
entCode := c.Query("ent_code")
|
||
entName := c.Query("ent_name")
|
||
entRegNo := c.Query("ent_reg_no")
|
||
|
||
// 组装 QYGLUY3S 入参
|
||
req := dto.QYGLUY3SReq{
|
||
EntName: entName,
|
||
EntRegno: entRegNo,
|
||
EntCode: entCode,
|
||
}
|
||
|
||
params, err := json.Marshal(req)
|
||
if err != nil {
|
||
h.logger.Error("序列化企业全景报告入参失败", zap.Error(err))
|
||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||
return
|
||
}
|
||
|
||
// 通过 ApiRequestService 调用 QYGLJ1U9 聚合处理器
|
||
options := &commands.ApiCallOptions{}
|
||
callCtx := &processors.CallContext{}
|
||
|
||
ctx := c.Request.Context()
|
||
respBytes, err := h.apiRequestService.PreprocessRequestApi(ctx, "QYGLJ1U9", params, options, callCtx)
|
||
if err != nil {
|
||
h.logger.Error("调用企业全景报告处理器失败", zap.Error(err))
|
||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||
return
|
||
}
|
||
|
||
var report map[string]interface{}
|
||
if err := json.Unmarshal(respBytes, &report); err != nil {
|
||
h.logger.Error("解析企业全景报告结果失败", zap.Error(err))
|
||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||
return
|
||
}
|
||
|
||
reportJSONBytes, err := json.Marshal(report)
|
||
if err != nil {
|
||
h.logger.Error("序列化企业全景报告结果失败", zap.Error(err))
|
||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||
return
|
||
}
|
||
|
||
// 使用 template.JS 避免在脚本中被转义,直接作为 JS 对象字面量注入
|
||
reportJSON := template.JS(reportJSONBytes)
|
||
|
||
c.HTML(http.StatusOK, "qiye.html", gin.H{
|
||
"ReportJSON": reportJSON,
|
||
})
|
||
}
|
||
|
||
// GetQYGLReportPageByID 通过编号查看企业全景报告页面
|
||
// GET /reports/qygl/:id
|
||
func (h *QYGLReportHandler) GetQYGLReportPageByID(c *gin.Context) {
|
||
id := c.Param("id")
|
||
if id == "" {
|
||
c.String(http.StatusBadRequest, "报告编号不能为空")
|
||
return
|
||
}
|
||
|
||
// 优先从数据库中查询报告记录
|
||
if h.reportRepo != nil {
|
||
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
|
||
h.maybeScheduleQYGLPDFPregen(c.Request.Context(), id)
|
||
reportJSON := template.JS(entity.ReportData)
|
||
c.HTML(http.StatusOK, "qiye.html", gin.H{
|
||
"ReportJSON": reportJSON,
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 回退到进程内存缓存(兼容老的访问方式)
|
||
report, ok := qygl.GetQYGLReport(id)
|
||
if !ok {
|
||
c.String(http.StatusNotFound, "报告不存在或已过期")
|
||
return
|
||
}
|
||
|
||
reportJSONBytes, err := json.Marshal(report)
|
||
if err != nil {
|
||
h.logger.Error("序列化企业全景报告结果失败", zap.Error(err))
|
||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后再试")
|
||
return
|
||
}
|
||
|
||
reportJSON := template.JS(reportJSONBytes)
|
||
|
||
h.maybeScheduleQYGLPDFPregen(c.Request.Context(), id)
|
||
|
||
c.HTML(http.StatusOK, "qiye.html", gin.H{
|
||
"ReportJSON": reportJSON,
|
||
})
|
||
}
|
||
|
||
// qyglReportExists 报告是否仍在库或本进程内存中(用于决定是否补开预生成)
|
||
func (h *QYGLReportHandler) qyglReportExists(ctx context.Context, id string) bool {
|
||
if h.reportRepo != nil {
|
||
if e, err := h.reportRepo.FindByReportID(ctx, id); err == nil && e != nil {
|
||
return true
|
||
}
|
||
}
|
||
_, ok := qygl.GetQYGLReport(id)
|
||
return ok
|
||
}
|
||
|
||
// maybeScheduleQYGLPDFPregen 在已配置公网基址时异步预生成 PDF;Schedule 内部会去重。
|
||
// 解决:服务重启 / 多实例后内存队列为空,用户打开报告页或轮询状态时仍应能启动预生成。
|
||
func (h *QYGLReportHandler) maybeScheduleQYGLPDFPregen(ctx context.Context, id string) {
|
||
if id == "" || h.qyglPDFPregen == nil || !h.qyglPDFPregen.Enabled() {
|
||
return
|
||
}
|
||
if !h.qyglReportExists(ctx, id) {
|
||
return
|
||
}
|
||
h.qyglPDFPregen.ScheduleQYGLReportPDF(context.Background(), id)
|
||
}
|
||
|
||
// GetQYGLReportPDFStatusByID 查询企业报告 PDF 预生成状态(供前端轮询)
|
||
// GET /reports/qygl/:id/pdf/status
|
||
func (h *QYGLReportHandler) GetQYGLReportPDFStatusByID(c *gin.Context) {
|
||
id := c.Param("id")
|
||
if id == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"status": "none", "message": "报告编号不能为空"})
|
||
return
|
||
}
|
||
if h.pdfCacheManager != nil {
|
||
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
|
||
c.JSON(http.StatusOK, gin.H{"status": string(pdf.QYGLReportPDFStatusReady), "message": ""})
|
||
return
|
||
}
|
||
}
|
||
if h.qyglPDFPregen == nil || !h.qyglPDFPregen.Enabled() {
|
||
c.JSON(http.StatusOK, gin.H{"status": string(pdf.QYGLReportPDFStatusNone), "message": "未启用预生成,将在下载时现场生成"})
|
||
return
|
||
}
|
||
st, msg := h.qyglPDFPregen.Status(id)
|
||
if st == pdf.QYGLReportPDFStatusNone && h.qyglReportExists(c.Request.Context(), id) {
|
||
h.qyglPDFPregen.ScheduleQYGLReportPDF(context.Background(), id)
|
||
st, msg = h.qyglPDFPregen.Status(id)
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"status": string(st), "message": userFacingPDFStatusMessage(st, msg)})
|
||
}
|
||
|
||
func userFacingPDFStatusMessage(st pdf.QYGLReportPDFStatus, raw string) string {
|
||
switch st {
|
||
case pdf.QYGLReportPDFStatusPending:
|
||
return "排队生成中"
|
||
case pdf.QYGLReportPDFStatusGenerating:
|
||
return "正在生成 PDF"
|
||
case pdf.QYGLReportPDFStatusFailed:
|
||
if raw != "" {
|
||
return raw
|
||
}
|
||
return "预生成失败,下载时将重新生成"
|
||
case pdf.QYGLReportPDFStatusReady:
|
||
return ""
|
||
case pdf.QYGLReportPDFStatusNone:
|
||
return "尚未开始预生成"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// GetQYGLReportPDFByID 通过编号导出企业全景报告 PDF:优先读缓存;可短时等待预生成;否则 headless 现场生成并写入缓存
|
||
// GET /reports/qygl/:id/pdf
|
||
func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
|
||
id := c.Param("id")
|
||
if id == "" {
|
||
c.String(http.StatusBadRequest, "报告编号不能为空")
|
||
return
|
||
}
|
||
|
||
var fileName = "企业全景报告.pdf"
|
||
if h.reportRepo != nil {
|
||
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
|
||
if entity.EntName != "" {
|
||
fileName = fmt.Sprintf("%s_企业全景报告.pdf", entity.EntName)
|
||
}
|
||
}
|
||
}
|
||
|
||
var pdfBytes []byte
|
||
if h.pdfCacheManager != nil {
|
||
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
|
||
pdfBytes = b
|
||
}
|
||
}
|
||
|
||
// 缓存未命中时:若正在预生成,短时等待(与前端轮询互补)
|
||
if len(pdfBytes) == 0 && h.qyglPDFPregen != nil && h.qyglPDFPregen.Enabled() && h.pdfCacheManager != nil {
|
||
deadline := time.Now().Add(90 * time.Second)
|
||
for time.Now().Before(deadline) {
|
||
st, _ := h.qyglPDFPregen.Status(id)
|
||
if st == pdf.QYGLReportPDFStatusFailed {
|
||
break
|
||
}
|
||
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
|
||
pdfBytes = b
|
||
break
|
||
}
|
||
time.Sleep(400 * time.Millisecond)
|
||
}
|
||
}
|
||
|
||
if len(pdfBytes) == 0 {
|
||
scheme := "http"
|
||
if c.Request.TLS != nil {
|
||
scheme = "https"
|
||
} else if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
|
||
scheme = forwardedProto
|
||
}
|
||
reportURL := fmt.Sprintf("%s://%s/reports/qygl/%s", scheme, c.Request.Host, id)
|
||
|
||
h.logger.Info("现场生成企业全景报告 PDF(headless Chrome)",
|
||
zap.String("report_id", id),
|
||
zap.String("url", reportURL),
|
||
)
|
||
|
||
pdfGen := pdf.NewHTMLPDFGenerator(h.logger)
|
||
var err error
|
||
pdfBytes, err = pdfGen.GenerateFromURL(c.Request.Context(), reportURL)
|
||
if err != nil {
|
||
h.logger.Error("生成企业全景报告 PDF 失败", zap.String("report_id", id), zap.Error(err))
|
||
c.String(http.StatusInternalServerError, "生成企业报告 PDF 失败,请稍后重试")
|
||
return
|
||
}
|
||
if len(pdfBytes) == 0 {
|
||
h.logger.Error("生成的企业全景报告 PDF 为空", zap.String("report_id", id))
|
||
c.String(http.StatusInternalServerError, "生成的企业报告 PDF 为空,请稍后重试")
|
||
return
|
||
}
|
||
if h.pdfCacheManager != nil {
|
||
_ = h.pdfCacheManager.SetByReportID(id, pdfBytes)
|
||
}
|
||
if h.qyglPDFPregen != nil {
|
||
h.qyglPDFPregen.MarkReadyAfterUpload(id)
|
||
}
|
||
}
|
||
|
||
encodedFileName := url.QueryEscape(fileName)
|
||
c.Header("Content-Type", "application/pdf")
|
||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", encodedFileName))
|
||
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||
}
|