f
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jung-kurt/gofpdf/v2"
|
||||
@@ -106,15 +107,15 @@ func GenerateQYGLReportPDF(_ context.Context, logger *zap.Logger, report map[str
|
||||
{"riskOverview", "风险情况(综合分析)"},
|
||||
{"basic", "一、主体概览(企业基础信息)"},
|
||||
{"branches", "二、分支机构"},
|
||||
{"shareholding", "三、股权与控制"},
|
||||
{"shareholding", "三、股权与控制(持股结构;认缴与实缴公示见第十六节)"},
|
||||
{"controller", "四、实际控制人"},
|
||||
{"beneficiaries", "五、最终受益人"},
|
||||
{"investments", "六、对外投资"},
|
||||
{"guarantees", "七、对外担保"},
|
||||
{"management", "八、人员与组织"},
|
||||
{"assets", "九、资产与经营(年报)"},
|
||||
{"guarantees", "七、对外担保(全量年报披露摘要;公示年报详版见第十六节)"},
|
||||
{"management", "八、人员与组织(高管与任职;年报从业与社保见第十六节)"},
|
||||
{"assets", "九、资产与经营(全量年报财务摘要;公示年报详版见第十六节)"},
|
||||
{"licenses", "十、行政许可与资质"},
|
||||
{"activities", "十一、经营活动"},
|
||||
{"activities", "十一、经营活动(招投标;网站或网店公示见第十六节)"},
|
||||
{"inspections", "十二、抽查检查"},
|
||||
{"risks", "十三、风险与合规"},
|
||||
{"timeline", "十四、发展时间线"},
|
||||
@@ -246,8 +247,8 @@ func writeKeyValue(pdf *gofpdf.Fpdf, fontName, label, value string) {
|
||||
// 使用黑色描边,左侧标签单元格填充标题蓝色背景
|
||||
pdf.SetDrawColor(0, 0, 0)
|
||||
pdf.SetFillColor(91, 155, 213)
|
||||
pdf.Rect(x, y, labelW, rowH, "FD") // 标签:填充+描边
|
||||
pdf.Rect(x+labelW, y, valueW, rowH, "D") // 值:仅描边
|
||||
pdf.Rect(x, y, labelW, rowH, "FD") // 标签:填充+描边
|
||||
pdf.Rect(x+labelW, y, valueW, rowH, "D") // 值:仅描边
|
||||
|
||||
// 写入标签单元格
|
||||
pdf.SetXY(x, y)
|
||||
@@ -891,7 +892,9 @@ func renderPDFManagement(pdf *gofpdf.Fpdf, fontName, title string, v map[string]
|
||||
// 从业与社保
|
||||
employeeCount := getString(v, "employeeCount")
|
||||
femaleEmployeeCount := getString(v, "femaleEmployeeCount")
|
||||
if employeeCount != "" || femaleEmployeeCount != "" {
|
||||
ssMap, _ := v["socialSecurity"].(map[string]interface{})
|
||||
hasSS := len(ssMap) > 0
|
||||
if employeeCount != "" || femaleEmployeeCount != "" || hasSS {
|
||||
pdf.Ln(2)
|
||||
pdf.SetFont(fontName, "B", 12)
|
||||
pdf.MultiCell(0, 6, "从业与社保", "", "L", false)
|
||||
@@ -902,6 +905,35 @@ func renderPDFManagement(pdf *gofpdf.Fpdf, fontName, title string, v map[string]
|
||||
if femaleEmployeeCount != "" {
|
||||
writeKeyValue(pdf, fontName, "女性从业人数", femaleEmployeeCount)
|
||||
}
|
||||
if hasSS {
|
||||
pdf.Ln(1)
|
||||
pdf.SetFont(fontName, "B", 11)
|
||||
pdf.MultiCell(0, 5, "社会保险参保人数", "", "L", false)
|
||||
pdf.SetFont(fontName, "", 11)
|
||||
socialLabels := map[string]string{
|
||||
"endowmentInsuranceEmployeeCnt": "城镇职工基本养老保险参保人数",
|
||||
"unemploymentInsuranceEmployeeCnt": "失业保险参保人数",
|
||||
"medicalInsuranceEmployeeCnt": "职工基本医疗保险参保人数",
|
||||
"injuryInsuranceEmployeeCnt": "工伤保险参保人数",
|
||||
"maternityInsuranceEmployeeCnt": "生育保险参保人数",
|
||||
}
|
||||
keys := make([]string, 0, len(ssMap))
|
||||
for k := range ssMap {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
val := ssMap[k]
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
lbl := socialLabels[k]
|
||||
if lbl == "" {
|
||||
lbl = k
|
||||
}
|
||||
writeKeyValue(pdf, fontName, lbl, fmt.Sprint(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 法定代表人其他任职
|
||||
@@ -1808,4 +1840,3 @@ func boolToCN(s string) string {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
177
internal/shared/pdf/qygl_report_pdf_pregen.go
Normal file
177
internal/shared/pdf/qygl_report_pdf_pregen.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// QYGLReportPDFStatus 企业报告 PDF 预生成状态(供前端轮询)
|
||||
type QYGLReportPDFStatus string
|
||||
|
||||
const (
|
||||
QYGLReportPDFStatusNone QYGLReportPDFStatus = "none"
|
||||
QYGLReportPDFStatusPending QYGLReportPDFStatus = "pending"
|
||||
QYGLReportPDFStatusGenerating QYGLReportPDFStatus = "generating"
|
||||
QYGLReportPDFStatusReady QYGLReportPDFStatus = "ready"
|
||||
QYGLReportPDFStatusFailed QYGLReportPDFStatus = "failed"
|
||||
)
|
||||
|
||||
// QYGLReportPDFPregen 异步预渲染企业报告 PDF(headless Chrome 访问公网可访问的报告 HTML)
|
||||
type QYGLReportPDFPregen struct {
|
||||
logger *zap.Logger
|
||||
cache *PDFCacheManager
|
||||
baseURL string // 已 trim,无尾斜杠;空则禁用
|
||||
|
||||
mu sync.RWMutex
|
||||
states map[string]*qyglPDFState
|
||||
}
|
||||
|
||||
type qyglPDFState struct {
|
||||
Status QYGLReportPDFStatus
|
||||
Message string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewQYGLReportPDFPregen baseURL 须为外部可访问基址(如 https://api.example.com),为空时不预生成
|
||||
func NewQYGLReportPDFPregen(logger *zap.Logger, cache *PDFCacheManager, baseURL string) *QYGLReportPDFPregen {
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
u := strings.TrimSpace(baseURL)
|
||||
u = strings.TrimRight(u, "/")
|
||||
return &QYGLReportPDFPregen{
|
||||
logger: logger,
|
||||
cache: cache,
|
||||
baseURL: u,
|
||||
states: make(map[string]*qyglPDFState),
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleQYGLReportPDF 实现 processors.QYGLReportPDFScheduler
|
||||
func (p *QYGLReportPDFPregen) ScheduleQYGLReportPDF(ctx context.Context, reportID string) {
|
||||
p.schedule(ctx, reportID)
|
||||
}
|
||||
|
||||
func (p *QYGLReportPDFPregen) schedule(_ context.Context, reportID string) {
|
||||
if p == nil || p.baseURL == "" || reportID == "" || p.cache == nil {
|
||||
return
|
||||
}
|
||||
if b, hit, _, err := p.cache.GetByReportID(reportID); hit && err == nil && len(b) > 0 {
|
||||
p.setState(reportID, QYGLReportPDFStatusReady, "")
|
||||
return
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
if st, ok := p.states[reportID]; ok {
|
||||
if st.Status == QYGLReportPDFStatusGenerating || st.Status == QYGLReportPDFStatusPending {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if st.Status == QYGLReportPDFStatusReady {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
p.states[reportID] = &qyglPDFState{
|
||||
Status: QYGLReportPDFStatusPending,
|
||||
Message: "",
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
go p.runGeneration(reportID)
|
||||
}
|
||||
|
||||
func (p *QYGLReportPDFPregen) runGeneration(reportID string) {
|
||||
p.setState(reportID, QYGLReportPDFStatusGenerating, "")
|
||||
|
||||
fullURL := p.baseURL + "/reports/qygl/" + url.PathEscape(reportID)
|
||||
gen := NewHTMLPDFGenerator(p.logger)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pdfBytes, err := gen.GenerateFromURL(ctx, fullURL)
|
||||
if err != nil {
|
||||
p.logger.Error("企业报告 PDF 预生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.String("url", fullURL),
|
||||
zap.Error(err),
|
||||
)
|
||||
p.setState(reportID, QYGLReportPDFStatusFailed, "生成失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
if len(pdfBytes) == 0 {
|
||||
p.setState(reportID, QYGLReportPDFStatusFailed, "生成的 PDF 为空")
|
||||
return
|
||||
}
|
||||
if err := p.cache.SetByReportID(reportID, pdfBytes); err != nil {
|
||||
p.logger.Error("企业报告 PDF 写入缓存失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err),
|
||||
)
|
||||
p.setState(reportID, QYGLReportPDFStatusFailed, "写入缓存失败")
|
||||
return
|
||||
}
|
||||
p.setState(reportID, QYGLReportPDFStatusReady, "")
|
||||
p.logger.Info("企业报告 PDF 预生成完成",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Int("size", len(pdfBytes)),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *QYGLReportPDFPregen) setState(reportID string, st QYGLReportPDFStatus, msg string) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.states == nil {
|
||||
p.states = make(map[string]*qyglPDFState)
|
||||
}
|
||||
p.states[reportID] = &qyglPDFState{
|
||||
Status: st,
|
||||
Message: msg,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 返回当前状态;若磁盘缓存已命中则始终为 ready
|
||||
func (p *QYGLReportPDFPregen) Status(reportID string) (QYGLReportPDFStatus, string) {
|
||||
if p == nil || reportID == "" {
|
||||
return QYGLReportPDFStatusNone, ""
|
||||
}
|
||||
if p.cache != nil {
|
||||
if b, hit, _, err := p.cache.GetByReportID(reportID); hit && err == nil && len(b) > 0 {
|
||||
return QYGLReportPDFStatusReady, ""
|
||||
}
|
||||
}
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
st := p.states[reportID]
|
||||
if st == nil {
|
||||
if p.baseURL == "" {
|
||||
return QYGLReportPDFStatusNone, "未启用预生成"
|
||||
}
|
||||
return QYGLReportPDFStatusNone, ""
|
||||
}
|
||||
return st.Status, st.Message
|
||||
}
|
||||
|
||||
// MarkReadyAfterUpload 同步生成成功后与预生成状态对齐(可选)
|
||||
func (p *QYGLReportPDFPregen) MarkReadyAfterUpload(reportID string) {
|
||||
if p == nil || reportID == "" {
|
||||
return
|
||||
}
|
||||
p.setState(reportID, QYGLReportPDFStatusReady, "")
|
||||
}
|
||||
|
||||
// Enabled 是否配置了预生成基址
|
||||
func (p *QYGLReportPDFPregen) Enabled() bool {
|
||||
return p != nil && p.baseURL != ""
|
||||
}
|
||||
Reference in New Issue
Block a user