diff --git a/cmd/qygl_report_pdf/main.go b/cmd/qygl_report_pdf/main.go new file mode 100644 index 0000000..3b5aab3 --- /dev/null +++ b/cmd/qygl_report_pdf/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + + "go.uber.org/zap" + + "tyapi-server/internal/shared/pdf" +) + +// 一个本地调试用的小工具: +// 从 JSON 文件(企业报告.json)读取 QYGL 聚合结果,使用 gofpdf 生成企业全景报告 PDF,输出到当前目录。 +func main() { + var ( + jsonPath string + outPath string + ) + flag.StringVar(&jsonPath, "json", "企业报告.json", "企业报告 JSON 数据源文件路径") + flag.StringVar(&outPath, "out", "企业全景报告_gofpdf.pdf", "输出 PDF 文件路径") + flag.Parse() + + logger, _ := zap.NewDevelopment() + defer logger.Sync() + + absJSON, _ := filepath.Abs(jsonPath) + fmt.Printf("读取 JSON 数据源:%s\n", absJSON) + + data, err := os.ReadFile(jsonPath) + if err != nil { + fmt.Printf("读取 JSON 文件失败: %v\n", err) + os.Exit(1) + } + + var report map[string]interface{} + if err := json.Unmarshal(data, &report); err != nil { + fmt.Printf("解析 JSON 失败: %v\n", err) + os.Exit(1) + } + + fmt.Println("开始使用 gofpdf 生成企业全景报告 PDF...") + pdfBytes, err := pdf.GenerateQYGLReportPDF(context.Background(), logger, report) + if err != nil { + fmt.Printf("生成 PDF 失败: %v\n", err) + os.Exit(1) + } + + if err := os.WriteFile(outPath, pdfBytes, 0644); err != nil { + fmt.Printf("写入 PDF 文件失败: %v\n", err) + os.Exit(1) + } + + absOut, _ := filepath.Abs(outPath) + fmt.Printf("PDF 生成完成:%s\n", absOut) +} + diff --git a/internal/shared/pdf/font_manager.go b/internal/shared/pdf/font_manager.go index de4f5b5..fa85ef9 100644 --- a/internal/shared/pdf/font_manager.go +++ b/internal/shared/pdf/font_manager.go @@ -3,6 +3,7 @@ package pdf import ( "os" "path/filepath" + "runtime" "github.com/jung-kurt/gofpdf/v2" "go.uber.org/zap" @@ -131,22 +132,20 @@ func (fm *FontManager) tryAddFont(pdf *gofpdf.Fpdf, fontPath, fontName string) b // 注意:ToSlash不会改变路径的绝对/相对性质,只统一分隔符 normalizedPath := filepath.ToSlash(absFontPath) - // 在Linux下,绝对路径必须以/开头 - // 如果normalizedPath不是以/开头,说明转换有问题 - if len(normalizedPath) == 0 || normalizedPath[0] != '/' { - fm.logger.Error("字体路径转换后不是绝对路径(不以/开头)", + // 在 Linux 下,绝对路径通常以 / 开头;在 Windows 下则可能以盘符 (C:/...) 开头。 + // 这里只要保证 normalizedPath 非空即可,具体格式交给 gofpdf 处理,避免在 Windows 下误判。 + if len(normalizedPath) == 0 { + fm.logger.Error("字体路径转换后为空,无法添加到PDF", zap.String("abs_font_path", absFontPath), - zap.String("normalized_path", normalizedPath), + zap.String("font_name", fontName), ) - // 重新转换为绝对路径 - if newAbsPath, err := filepath.Abs(absFontPath); err == nil { - absFontPath = newAbsPath - normalizedPath = filepath.ToSlash(newAbsPath) - fm.logger.Info("重新转换后的路径", - zap.String("new_normalized_path", normalizedPath), - ) - } + return false } + // 额外记录当前平台,方便排查路径格式问题 + fm.logger.Debug("字体路径平台信息", + zap.String("goos", runtime.GOOS), + zap.String("normalized_path", normalizedPath), + ) fm.logger.Debug("准备添加字体到gofpdf", zap.String("original_path", fontPath), @@ -157,16 +156,6 @@ func (fm *FontManager) tryAddFont(pdf *gofpdf.Fpdf, fontPath, fontName string) b // gofpdf v2使用AddUTF8Font添加支持UTF-8的字体 // 注意:gofpdf在Output时可能会重新解析路径,必须确保路径格式正确 - // 关键:确保路径是绝对路径且以/开头,并使用filepath.ToSlash统一分隔符 - // 如果normalizedPath不是以/开头,说明路径有问题,需要重新处理 - if len(normalizedPath) == 0 || normalizedPath[0] != '/' { - fm.logger.Error("字体路径格式错误,无法添加到PDF", - zap.String("normalized_path", normalizedPath), - zap.String("font_name", fontName), - ) - return false - } - // 记录传递给gofpdf的实际路径 fm.logger.Info("添加字体到gofpdf", zap.String("font_path", normalizedPath), diff --git a/internal/shared/pdf/qygl_report_pdf.go b/internal/shared/pdf/qygl_report_pdf.go new file mode 100644 index 0000000..b64a714 --- /dev/null +++ b/internal/shared/pdf/qygl_report_pdf.go @@ -0,0 +1,1811 @@ +package pdf + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/jung-kurt/gofpdf/v2" + "go.uber.org/zap" +) + +// GenerateQYGLReportPDF 使用 gofpdf 根据 QYGLJ1U9 聚合结果生成企业全景报告 PDF, +// 尽量复刻 qiye.html 中的板块顺序和展示逻辑。 +// report 参数通常是从 JSON 反序列化得到的 map[string]interface{}。 +func GenerateQYGLReportPDF(_ context.Context, logger *zap.Logger, report map[string]interface{}) ([]byte, error) { + if logger == nil { + logger = zap.NewNop() + } + + pdf := gofpdf.New("P", "mm", "A4", "") + // 整体页边距和自动分页底部预留稍微加大,整体更「松」一点 + pdf.SetMargins(15, 25, 15) + pdf.SetAutoPageBreak(true, 22) + + // 加载中文字体 + fontManager := NewFontManager(logger) + chineseFontLoaded := fontManager.LoadChineseFont(pdf) + fontName := "ChineseFont" + if !chineseFontLoaded { + // 回退:使用内置核心字体(英文),中文可能会显示为方块 + fontName = "Arial" + } + // 整体默认正文字体稍微放大 + pdf.SetFont(fontName, "", 14) + + // 头部信息,对齐 qiye.html + entName := getString(report, "entName") + if entName == "" { + entName = "未知企业" + } + creditCode := getString(report, "creditCode") + if creditCode == "" { + creditCode = getStringFromMap(report, "basic", "creditCode") + } + basic := getMap(report, "basic") + entStatus := getString(basic, "status") + if entStatus == "" { + entStatus = getString(basic, "entStatus") + } + entType := getString(basic, "entType") + reportTime := getString(report, "reportTime") + + riskOverview := getMap(report, "riskOverview") + riskScore := getString(riskOverview, "riskScore") + riskLevel := getString(riskOverview, "riskLevel") + + // 首页头部 + pdf.AddPage() + // 预留更多顶部空白,让标题整体再下移一些 + pdf.Ln(20) + // 顶部大标题:居中、加粗、蓝色,更大字号 + pdf.SetFont(fontName, "B", 34) + pdf.SetTextColor(37, 99, 235) + pdf.CellFormat(0, 18, "企业全景报告", "", 1, "C", false, 0, "") + pdf.Ln(10) + // 恢复正文颜色 + pdf.SetTextColor(0, 0, 0) + + pdf.SetFont(fontName, "", 18) + pdf.MultiCell(0, 9, fmt.Sprintf("企业名称:%s", entName), "", "L", false) + + pdf.SetFont(fontName, "", 14) + pdf.MultiCell(0, 7, fmt.Sprintf("统一社会信用代码:%s", safePlaceholder(creditCode)), "", "L", false) + pdf.MultiCell(0, 7, fmt.Sprintf("经营状态:%s", safePlaceholder(entStatus)), "", "L", false) + pdf.MultiCell(0, 7, fmt.Sprintf("企业类型:%s", safePlaceholder(entType)), "", "L", false) + if reportTime != "" { + pdf.MultiCell(0, 7, fmt.Sprintf("报告生成时间:%s", reportTime), "", "L", false) + } + + // 综合风险评分(使用 riskOverview) + pdf.Ln(6) + pdfSubTitle(pdf, fontName, "综合风险评分") + pdf.SetFont(fontName, "", 13) + if riskScore != "" { + pdf.MultiCell(0, 7, fmt.Sprintf("风险得分:%s", riskScore), "", "L", false) + } + if riskLevel != "" { + pdf.MultiCell(0, 7, fmt.Sprintf("风险等级:%s", riskLevel), "", "L", false) + } + // 头部风险标签 + if tags, ok := riskOverview["tags"].([]interface{}); ok && len(tags) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "", 11) + pdf.MultiCell(0, 5, "风险标签:", "", "L", false) + for _, t := range tags { + pdf.MultiCell(0, 5, fmt.Sprintf("· %v", t), "", "L", false) + } + } + + // 对齐 qiye.html 的板块顺序 + sections := []struct { + key string + title string + }{ + {"riskOverview", "风险情况(综合分析)"}, + {"basic", "一、主体概览(企业基础信息)"}, + {"branches", "二、分支机构"}, + {"shareholding", "三、股权与控制"}, + {"controller", "四、实际控制人"}, + {"beneficiaries", "五、最终受益人"}, + {"investments", "六、对外投资"}, + {"guarantees", "七、对外担保"}, + {"management", "八、人员与组织"}, + {"assets", "九、资产与经营(年报)"}, + {"licenses", "十、行政许可与资质"}, + {"activities", "十一、经营活动"}, + {"inspections", "十二、抽查检查"}, + {"risks", "十三、风险与合规"}, + {"timeline", "十四、发展时间线"}, + {"listed", "十五、上市信息"}, + } + + for _, s := range sections { + switch s.key { + case "riskOverview": + renderPDFRiskOverview(pdf, fontName, s.title, riskOverview) + case "basic": + renderPDFBasic(pdf, fontName, s.title, basic, entName, creditCode) + case "branches": + renderPDFBranches(pdf, fontName, s.title, getSlice(report, "branches")) + case "shareholding": + renderPDFShareholding(pdf, fontName, s.title, getMap(report, "shareholding")) + case "controller": + renderPDFController(pdf, fontName, s.title, getMap(report, "controller")) + case "beneficiaries": + renderPDFBeneficiaries(pdf, fontName, s.title, getSlice(report, "beneficiaries")) + case "investments": + renderPDFInvestments(pdf, fontName, s.title, getMap(report, "investments")) + case "guarantees": + renderPDFGuarantees(pdf, fontName, s.title, getSlice(report, "guarantees")) + case "management": + renderPDFManagement(pdf, fontName, s.title, getMap(report, "management")) + case "assets": + renderPDFAssets(pdf, fontName, s.title, getMap(report, "assets")) + case "licenses": + renderPDFLicenses(pdf, fontName, s.title, getMap(report, "licenses")) + case "activities": + renderPDFActivities(pdf, fontName, s.title, getMap(report, "activities")) + case "inspections": + renderPDFInspections(pdf, fontName, s.title, getSlice(report, "inspections")) + case "risks": + renderPDFRisks(pdf, fontName, s.title, getMap(report, "risks")) + case "timeline": + renderPDFTimeline(pdf, fontName, s.title, getSlice(report, "timeline")) + case "listed": + renderPDFListed(pdf, fontName, s.title, getMap(report, "listed")) + } + } + + // 输出为字节 + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, fmt.Errorf("输出企业全景报告 PDF 失败: %w", err) + } + return buf.Bytes(), nil +} + +// 辅助方法 + +func getString(m map[string]interface{}, key string) string { + if m == nil { + return "" + } + if v, ok := m[key]; ok && v != nil { + if s, ok := v.(string); ok { + return s + } + return fmt.Sprint(v) + } + return "" +} + +func getStringFromMap(root map[string]interface{}, field string, key string) string { + child := getMap(root, field) + return getString(child, key) +} + +func getMap(m map[string]interface{}, key string) map[string]interface{} { + if m == nil { + return map[string]interface{}{} + } + if v, ok := m[key]; ok && v != nil { + if mm, ok := v.(map[string]interface{}); ok { + return mm + } + } + return map[string]interface{}{} +} + +func getSlice(m map[string]interface{}, key string) []interface{} { + if m == nil { + return nil + } + if v, ok := m[key]; ok && v != nil { + if arr, ok := v.([]interface{}); ok { + return arr + } + } + return nil +} + +func writeKeyValue(pdf *gofpdf.Fpdf, fontName, label, value string) { + if value == "" { + return + } + // 计算表格布局:左侧标签列 + 右侧内容列(模块内数据统一用表格框展示) + pageW, pageH := pdf.GetPageSize() + lMargin, _, rMargin, bMargin := pdf.GetMargins() + labelW := 40.0 + valueW := pageW - lMargin - rMargin - labelW + // 行高整体拉宽一点 + lineH := 10.0 + + // 当前起始坐标(统一从左边距起,栅格化对齐) + x := lMargin + y := pdf.GetY() + + // 预计算内容高度:使用 SplitLines 获取行数,避免长内容导致单元格高度不够 + lines := pdf.SplitLines([]byte(value), valueW) + rowH := float64(len(lines)) * lineH + if rowH < lineH { + rowH = lineH + } + // 轻微增加上下留白,避免文字贴边 + rowH += 2 + + // 如剩余空间不足,先换到新页再画整行,避免表格跨页导致内容重叠 + if y+rowH > pageH-bMargin { + pdf.AddPage() + y = pdf.GetY() + pdf.SetXY(lMargin, y) + } + + // 画出整行的两个单元格边框 + // 使用黑色描边,左侧标签单元格填充标题蓝色背景 + 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.SetXY(x, y) + pdf.SetFont(fontName, "B", 13) + pdf.SetTextColor(255, 255, 255) + pdf.CellFormat(labelW, rowH, fmt.Sprintf("%s:", label), "", 0, "L", false, 0, "") + + // 写入值单元格(自动换行) + pdf.SetXY(x+labelW, y) + pdf.SetFont(fontName, "", 13) + pdf.SetTextColor(0, 0, 0) + pdf.MultiCell(valueW, lineH, value, "", "L", false) + + // 将光标移动到下一行开头,恢复默认颜色 + pdf.SetDrawColor(0, 0, 0) + pdf.SetFillColor(255, 255, 255) + pdf.SetXY(lMargin, y+rowH) +} + +func safePlaceholder(s string) string { + if s == "" { + return "-" + } + return s +} + +// ------------------------- +// 各板块渲染(与 qiye.html 对齐) +// ------------------------- + +// 风险综合分析(只做风险点一览,与 qiye.html 中 renderRiskOverview 对齐的精简版) +func renderPDFRiskOverview(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + + pdf.SetFont(fontName, "", 11) + itemsRaw, ok := v["items"].([]interface{}) + if !ok || len(itemsRaw) == 0 { + pdf.MultiCell(0, 6, "暂无风险分析数据", "", "L", false) + return + } + + for _, it := range itemsRaw { + m, ok := it.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + continue + } + hit := false + if hv, ok := m["hit"]; ok { + if b, ok := hv.(bool); ok { + hit = b + } else { + hit = fmt.Sprint(hv) == "true" + } + } + status := "未命中" + if hit { + status = "命中" + } + pdf.MultiCell(0, 6, fmt.Sprintf("· %s:%s", name, status), "", "L", false) + } +} + +// 主体概览,对照 qiye.html renderBasic 中的字段顺序,做精简单列展示 +func renderPDFBasic(pdf *gofpdf.Fpdf, fontName, title string, basic map[string]interface{}, entName, creditCode string) { + if len(basic) == 0 && entName == "" && creditCode == "" { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 13) + + // 基本信息采用条目式行展示,隔行背景色,不使用表格框 + rowIndex := 0 + writeBasicRow := func(label, val string) { + if val == "" { + return + } + alt := rowIndex%2 == 0 + pdfWriteBasicRow(pdf, fontName, label, val, alt) + rowIndex++ + } + + writeBasicRow("企业名称", entName) + writeBasicRow("统一社会信用代码", safePlaceholder(creditCode)) + + keys := []string{ + "regNo", "orgCode", "entType", "entTypeCode", "entityTypeCode", + "establishDate", "registeredCapital", "regCap", "regCapCurrency", + "regCapCurrencyCode", "regOrg", "regOrgCode", "regProvince", "provinceCode", + "regCity", "regCityCode", "regDistrict", "districtCode", "address", + "postalCode", "legalRepresentative", "compositionForm", "approvedBusinessItem", + "status", "statusCode", "operationPeriodFrom", "operationPeriodTo", "approveDate", + "cancelDate", "revokeDate", "cancelReason", "revokeReason", + "oldNames", "businessScope", "lastAnnuReportYear", + } + + for _, k := range keys { + val := getString(basic, k) + if k == "oldNames" && val == "" { + continue + } + if val == "" { + continue + } + writeBasicRow(mapBasicLabel(k), val) + } +} + +func mapBasicLabel(k string) string { + switch k { + case "regNo": + return "注册号" + case "orgCode": + return "组织机构代码" + case "entType": + return "企业类型" + case "entTypeCode": + return "企业类型编码" + case "entityTypeCode": + return "实体类型编码" + case "establishDate": + return "成立日期" + case "registeredCapital", "regCap": + return "注册资本" + case "regCapCurrency": + return "注册资本币种" + case "regCapCurrencyCode": + return "注册资本币种代码" + case "regOrg": + return "登记机关" + case "regOrgCode": + return "注册地址行政编号" + case "regProvince": + return "所在省份" + case "provinceCode": + return "所在省份编码" + case "regCity": + return "所在城市" + case "regCityCode": + return "所在城市编码" + case "regDistrict": + return "所在区/县" + case "districtCode": + return "所在区/县编码" + case "address": + return "住址" + case "postalCode": + return "邮编" + case "legalRepresentative": + return "法定代表人" + case "compositionForm": + return "组成形式" + case "approvedBusinessItem": + return "许可经营项目" + case "status": + return "经营状态" + case "statusCode": + return "经营状态编码" + case "operationPeriodFrom": + return "经营期限自" + case "operationPeriodTo": + return "经营期限至" + case "approveDate": + return "核准日期" + case "cancelDate": + return "注销日期" + case "revokeDate": + return "吊销日期" + case "cancelReason": + return "注销原因" + case "revokeReason": + return "吊销原因" + case "oldNames": + return "曾用名" + case "businessScope": + return "经营业务范围" + case "lastAnnuReportYear": + return "最后年检年度" + default: + return k + } +} + +func renderPDFBranches(pdf *gofpdf.Fpdf, fontName, title string, branches []interface{}) { + if len(branches) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + for i, b := range branches { + m, ok := b.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + name = getString(m, "entName") + } + if name == "" { + continue + } + regNo := getString(m, "regNo") + credit := getString(m, "creditCode") + regOrg := getString(m, "regOrg") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, name), "", "L", false) + meta := fmt.Sprintf(" 注册号:%s;信用代码:%s;登记机关:%s", + safePlaceholder(regNo), + safePlaceholder(credit), + safePlaceholder(regOrg), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } +} + +func renderPDFShareholding(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + // 汇总(对齐前端 renderShareholding 的 summary 部分) + shareholderCount := getString(v, "shareholderCount") + registeredCapital := getString(v, "registeredCapital") + currency := getString(v, "currency") + topHolderName := getString(v, "topHolderName") + topHolderPercent := getString(v, "topHolderPercent") + top5TotalPercent := getString(v, "top5TotalPercent") + hasEquityPledges := boolToCN(getString(v, "hasEquityPledges")) + + if shareholderCount != "" || registeredCapital != "" || currency != "" || + topHolderName != "" || topHolderPercent != "" || top5TotalPercent != "" || hasEquityPledges != "" { + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "股权汇总", "", "L", false) + pdf.SetFont(fontName, "", 11) + if shareholderCount != "" { + writeKeyValue(pdf, fontName, "股东总数", shareholderCount) + } + if registeredCapital != "" { + writeKeyValue(pdf, fontName, "注册资本", registeredCapital+" 元") + } + if currency != "" { + writeKeyValue(pdf, fontName, "币种", currency) + } + if topHolderName != "" { + writeKeyValue(pdf, fontName, "第一大股东", topHolderName) + } + if topHolderPercent != "" { + writeKeyValue(pdf, fontName, "第一大股东持股", topHolderPercent) + } + if top5TotalPercent != "" { + writeKeyValue(pdf, fontName, "前五大合计", top5TotalPercent) + } + if hasEquityPledges != "" { + writeKeyValue(pdf, fontName, "存在股权出质", hasEquityPledges) + } + } + + // 股东明细(股东全量字段的精简版) + if shRaw, ok := v["shareholders"].([]interface{}); ok && len(shRaw) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "股东明细", "", "L", false) + pdf.SetFont(fontName, "", 11) + + for i, sh := range shRaw { + m, ok := sh.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + continue + } + pct := getString(m, "ownershipPercent") + if pct == "" { + pct = getString(m, "sharePercent") + } + titleLine := fmt.Sprintf("%d. %s", i+1, name) + if pct != "" { + titleLine = fmt.Sprintf("%s(持股:%s)", titleLine, pct) + } + pdf.MultiCell(0, 6, titleLine, "", "L", false) + // 认缴 / 实缴等关键信息 + subscribedAmount := getString(m, "subscribedAmount") + subscribedCurrency := getString(m, "subscribedCurrency") + subscribedCurrencyCode := getString(m, "subscribedCurrencyCode") + subscribedDate := getString(m, "subscribedDate") + subscribedMethod := getString(m, "subscribedMethod") + subscribedMethodCode := getString(m, "subscribedMethodCode") + paidAmount := getString(m, "paidAmount") + paidDate := getString(m, "paidDate") + paidMethod := getString(m, "paidMethod") + creditCode := getString(m, "creditCode") + regNo := getString(m, "regNo") + isHistory := boolToCN(getString(m, "isHistory")) + + metaParts := []string{} + if subscribedAmount != "" { + sub := "认缴:" + subscribedAmount + if subscribedCurrency != "" || subscribedCurrencyCode != "" { + sub += " " + subscribedCurrency + if subscribedCurrencyCode != "" { + sub += "(" + subscribedCurrencyCode + ")" + } + } + metaParts = append(metaParts, sub) + } + if subscribedDate != "" { + metaParts = append(metaParts, "认缴日:"+subscribedDate) + } + if subscribedMethod != "" || subscribedMethodCode != "" { + subm := "认缴方式:" + subscribedMethod + if subscribedMethodCode != "" { + subm += "(" + subscribedMethodCode + ")" + } + metaParts = append(metaParts, subm) + } + if paidAmount != "" { + metaParts = append(metaParts, "实缴:"+paidAmount) + } + if paidDate != "" { + metaParts = append(metaParts, "实缴日:"+paidDate) + } + if paidMethod != "" { + metaParts = append(metaParts, "实缴方式:"+paidMethod) + } + if creditCode != "" { + metaParts = append(metaParts, "股东信用代码:"+creditCode) + } + if regNo != "" { + metaParts = append(metaParts, "股东注册号:"+regNo) + } + if isHistory != "" { + metaParts = append(metaParts, "是否历史:"+isHistory) + } + if len(metaParts) > 0 { + pdf.MultiCell(0, 5, " "+joinWithChineseSemicolon(metaParts), "", "L", false) + } + pdf.Ln(1) + } + } + + // 股权变更记录 + if changes, ok := v["equityChanges"].([]interface{}); ok && len(changes) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "股权变更记录", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, e := range changes { + m, ok := e.(map[string]interface{}) + if !ok { + continue + } + changeDate := getString(m, "changeDate") + shareholderName := getString(m, "shareholderName") + titleLine := fmt.Sprintf("%d. %s %s", i+1, safePlaceholder(changeDate), shareholderName) + pdf.MultiCell(0, 6, titleLine, "", "L", false) + before := getString(m, "percentBefore") + after := getString(m, "percentAfter") + if before != "" || after != "" { + pdf.MultiCell(0, 5, fmt.Sprintf(" 变更前:%s → 变更后:%s", safePlaceholder(before), safePlaceholder(after)), "", "L", false) + } + pdf.Ln(1) + } + } + + // 股权出质 + if pledges, ok := v["equityPledges"].([]interface{}); ok && len(pledges) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "股权出质", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, e := range pledges { + m, ok := e.(map[string]interface{}) + if !ok { + continue + } + regNo := getString(m, "regNo") + pledgor := getString(m, "pledgor") + pledgee := getString(m, "pledgee") + titleLine := fmt.Sprintf("%d. %s %s → %s", i+1, safePlaceholder(regNo), safePlaceholder(pledgor), safePlaceholder(pledgee)) + pdf.MultiCell(0, 6, titleLine, "", "L", false) + amount := getString(m, "pledgedAmount") + regDate := getString(m, "regDate") + status := getString(m, "status") + meta := fmt.Sprintf(" 出质数额:%s 元;登记日:%s;状态:%s", + safePlaceholder(amount), + safePlaceholder(regDate), + safePlaceholder(status), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 实缴出资明细 + if paidDetails, ok := v["paidInDetails"].([]interface{}); ok && len(paidDetails) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "实缴出资明细", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, e := range paidDetails { + m, ok := e.(map[string]interface{}) + if !ok { + continue + } + investor := getString(m, "investor") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(investor)), "", "L", false) + paidDate := getString(m, "paidDate") + paidMethod := getString(m, "paidMethod") + accumulatedPaid := getString(m, "accumulatedPaid") + meta := fmt.Sprintf(" 日期:%s;方式:%s;累计实缴:%s 万元", + safePlaceholder(paidDate), + safePlaceholder(paidMethod), + safePlaceholder(accumulatedPaid), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 认缴出资明细 + if subDetails, ok := v["subscribedCapitalDetails"].([]interface{}); ok && len(subDetails) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "认缴出资明细", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, e := range subDetails { + m, ok := e.(map[string]interface{}) + if !ok { + continue + } + investor := getString(m, "investor") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(investor)), "", "L", false) + subscribedDate := getString(m, "subscribedDate") + subscribedMethod := getString(m, "subscribedMethod") + accSubscribed := getString(m, "accumulatedSubscribed") + meta := fmt.Sprintf(" 认缴日:%s;方式:%s;累计认缴:%s 元", + safePlaceholder(subscribedDate), + safePlaceholder(subscribedMethod), + safePlaceholder(accSubscribed), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } +} + +func renderPDFController(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + writeKeyValue(pdf, fontName, "标识", getString(v, "id")) + writeKeyValue(pdf, fontName, "姓名/名称", getString(v, "name")) + writeKeyValue(pdf, fontName, "类型", getString(v, "type")) + writeKeyValue(pdf, fontName, "持股比例", getString(v, "percent")) + writeKeyValue(pdf, fontName, "判定依据", getString(v, "reason")) + writeKeyValue(pdf, fontName, "来源", getString(v, "source")) +} + +func renderPDFBeneficiaries(pdf *gofpdf.Fpdf, fontName, title string, arr []interface{}) { + if len(arr) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + for i, b := range arr { + m, ok := b.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + continue + } + pct := getString(m, "percent") + line := fmt.Sprintf("%d. %s", i+1, name) + if pct != "" { + line = fmt.Sprintf("%s(持股:%s)", line, pct) + } + pdf.MultiCell(0, 6, line, "", "L", false) + reason := getString(m, "reason") + if reason != "" { + pdf.MultiCell(0, 5, fmt.Sprintf(" 原因:%s", reason), "", "L", false) + } + pdf.Ln(1) + } +} + +func renderPDFInvestments(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + // 汇总 + totalCount := getString(v, "totalCount") + totalAmount := getString(v, "totalAmount") + if totalCount != "" || totalAmount != "" { + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "汇总", "", "L", false) + pdf.SetFont(fontName, "", 11) + if totalCount != "" { + writeKeyValue(pdf, fontName, "对外投资数量", totalCount) + } + if totalAmount != "" { + writeKeyValue(pdf, fontName, "投资总额(万)", totalAmount) + } + } + + // 对外投资列表 + list := []interface{}{} + if arr, ok := v["list"].([]interface{}); ok { + list = arr + } else if arr, ok := v["entities"].([]interface{}); ok { + list = arr + } + if len(list) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "对外投资列表", "", "L", false) + pdf.SetFont(fontName, "", 11) + + for i, inv := range list { + m, ok := inv.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "entName") + if name == "" { + name = getString(m, "name") + } + if name == "" { + continue + } + pct := getString(m, "investPercent") + titleLine := fmt.Sprintf("%d. %s", i+1, name) + if pct != "" { + titleLine = fmt.Sprintf("%s(持股:%s)", titleLine, pct) + } + pdf.MultiCell(0, 6, titleLine, "", "L", false) + } + } +} + +func renderPDFGuarantees(pdf *gofpdf.Fpdf, fontName, title string, arr []interface{}) { + if len(arr) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + for i, it := range arr { + m, ok := it.(map[string]interface{}) + if !ok { + continue + } + mortgagor := getString(m, "mortgagor") + creditor := getString(m, "creditor") + if mortgagor == "" && creditor == "" { + continue + } + titleLine := fmt.Sprintf("%d. %s → %s", i+1, safePlaceholder(mortgagor), safePlaceholder(creditor)) + pdf.MultiCell(0, 6, titleLine, "", "L", false) + amount := getString(m, "principalAmount") + kind := getString(m, "principalKind") + guaranteeType := getString(m, "guaranteeType") + periodFrom := getString(m, "periodFrom") + periodTo := getString(m, "periodTo") + meta := fmt.Sprintf(" 主债权数额:%s;种类:%s;保证方式:%s;期限:%s 至 %s", + safePlaceholder(amount), + safePlaceholder(kind), + safePlaceholder(guaranteeType), + safePlaceholder(periodFrom), + safePlaceholder(periodTo), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } +} + +func renderPDFManagement(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + // 高管列表 + if execs, ok := v["executives"].([]interface{}); ok && len(execs) > 0 { + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "高管列表", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, e := range execs { + m, ok := e.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + continue + } + position := getString(m, "position") + line := fmt.Sprintf("%d. %s", i+1, name) + if position != "" { + line = fmt.Sprintf("%s(%s)", line, position) + } + pdf.MultiCell(0, 6, line, "", "L", false) + } + } + + // 从业与社保 + employeeCount := getString(v, "employeeCount") + femaleEmployeeCount := getString(v, "femaleEmployeeCount") + if employeeCount != "" || femaleEmployeeCount != "" { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "从业与社保", "", "L", false) + pdf.SetFont(fontName, "", 11) + if employeeCount != "" { + writeKeyValue(pdf, fontName, "从业人数", employeeCount) + } + if femaleEmployeeCount != "" { + writeKeyValue(pdf, fontName, "女性从业人数", femaleEmployeeCount) + } + } + + // 法定代表人其他任职 + var others []interface{} + if arr, ok := v["legalRepresentativeOtherPositions"].([]interface{}); ok { + others = arr + } else if arr, ok := v["legalPersonOtherPositions"].([]interface{}); ok { + others = arr + } + if len(others) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "法定代表人其他任职", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, f := range others { + m, ok := f.(map[string]interface{}) + if !ok { + continue + } + entName := getString(m, "entName") + if entName == "" { + continue + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, entName), "", "L", false) + parts := []string{} + if pos := getString(m, "position"); pos != "" { + parts = append(parts, "职务:"+pos) + } + if name := getString(m, "name"); name != "" { + parts = append(parts, "姓名:"+name) + } + if st := getString(m, "entStatus"); st != "" { + parts = append(parts, "企业状态:"+st) + } + if cc := getString(m, "creditCode"); cc != "" { + parts = append(parts, "信用代码:"+cc) + } + if rn := getString(m, "regNo"); rn != "" { + parts = append(parts, "注册号:"+rn) + } + if len(parts) > 0 { + pdf.MultiCell(0, 5, " "+joinWithChineseSemicolon(parts), "", "L", false) + } + pdf.Ln(1) + } + } +} + +func renderPDFAssets(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + yearsRaw, ok := v["years"].([]interface{}) + if !ok || len(yearsRaw) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + for _, y := range yearsRaw { + m, ok := y.(map[string]interface{}) + if !ok { + continue + } + year := getString(m, "year") + reportDate := getString(m, "reportDate") + titleLine := year + if reportDate != "" { + titleLine = fmt.Sprintf("%s(%s)", year, reportDate) + } + pdf.MultiCell(0, 6, titleLine, "", "L", false) + meta := fmt.Sprintf(" 资产总额:%s;营收:%s;净利润:%s;负债:%s", + safePlaceholder(getString(m, "assetTotal")), + safePlaceholder(getString(m, "revenueTotal")), + safePlaceholder(getString(m, "netProfit")), + safePlaceholder(getString(m, "liabilityTotal")), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } +} + +func renderPDFLicenses(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + // 行政许可列表 + if permits, ok := v["permits"].([]interface{}); ok && len(permits) > 0 { + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "行政许可列表", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, p := range permits { + m, ok := p.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + continue + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, name), "", "L", false) + meta := fmt.Sprintf(" 有效期:%s ~ %s;许可机关:%s;%s", + safePlaceholder(getString(m, "valFrom")), + safePlaceholder(getString(m, "valTo")), + safePlaceholder(getString(m, "licAnth")), + safePlaceholder(getString(m, "licItem")), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 许可变更记录 + if changes, ok := v["permitChanges"].([]interface{}); ok && len(changes) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "许可变更记录", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, p := range changes { + m, ok := p.(map[string]interface{}) + if !ok { + continue + } + changeDate := getString(m, "changeDate") + changeType := getString(m, "changeType") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s %s", i+1, safePlaceholder(changeDate), safePlaceholder(changeType)), "", "L", false) + before := getString(m, "detailBefore") + after := getString(m, "detailAfter") + if before != "" || after != "" { + pdf.MultiCell(0, 5, " 变更前:"+before, "", "L", false) + pdf.MultiCell(0, 5, " 变更后:"+after, "", "L", false) + } + pdf.Ln(1) + } + } + + // 知识产权出质(如有则原样列出) + if ipPledges, ok := v["ipPledges"].([]interface{}); ok && len(ipPledges) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "知识产权出质", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, it := range ipPledges { + pdf.MultiCell(0, 5, fmt.Sprintf("%d. %v", i+1, it), "", "L", false) + } + } +} + +func renderPDFActivities(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + // 招投标 + if bids, ok := v["bids"].([]interface{}); ok && len(bids) > 0 { + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "招投标", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, b := range bids { + m, ok := b.(map[string]interface{}) + if !ok { + continue + } + titleText := getString(m, "announcetitle") + if titleText == "" { + titleText = getString(m, "ANNOUNCETITLE") + } + if titleText == "" { + titleText = getString(m, "title") + } + if titleText == "" { + continue + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, titleText), "", "L", false) + } + } + + // 网站 / 网店 + if websites, ok := v["websites"].([]interface{}); ok && len(websites) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "网站 / 网店", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, w := range websites { + m, ok := w.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "websitname") + if name == "" { + name = getString(m, "WEBSITNAME") + } + if name == "" { + name = getString(m, "name") + } + if name == "" { + continue + } + webType := getString(m, "webtype") + if webType == "" { + webType = getString(m, "WEBTYPE") + } + domain := getString(m, "domain") + if domain == "" { + domain = getString(m, "DOMAIN") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, name), "", "L", false) + meta := fmt.Sprintf(" 类型:%s;域名:%s", safePlaceholder(webType), safePlaceholder(domain)) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } +} + +func renderPDFInspections(pdf *gofpdf.Fpdf, fontName, title string, arr []interface{}) { + if len(arr) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + for i, it := range arr { + m, ok := it.(map[string]interface{}) + if !ok { + continue + } + date := getString(m, "inspectDate") + result := getString(m, "result") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s %s", i+1, safePlaceholder(date), safePlaceholder(result)), "", "L", false) + } +} + +func renderPDFRisks(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + // has* 风险项一览(有 / 无) + type item struct { + Key string + Label string + } + items := []item{ + {"hasCourtJudgments", "裁判文书"}, + {"hasJudicialAssists", "司法协助"}, + {"hasDishonestDebtors", "失信被执行人"}, + {"hasLimitHighDebtors", "限高被执行人"}, + {"hasAdminPenalty", "行政处罚"}, + {"hasException", "经营异常"}, + {"hasSeriousIllegal", "严重违法"}, + {"hasTaxOwing", "欠税"}, + {"hasSeriousTaxIllegal", "重大税收违法"}, + {"hasMortgage", "动产抵押"}, + {"hasEquityPledges", "股权出质"}, + {"hasQuickCancel", "简易注销"}, + } + + for _, it := range items { + val := getString(v, it.Key) + status := "未发现" + if val == "true" || val == "1" { + status = "存在" + } + pdf.MultiCell(0, 6, fmt.Sprintf("· %s:%s", it.Label, status), "", "L", false) + } + + // 裁判文书 + if arr, ok := v["courtJudgments"].([]interface{}); !ok || len(arr) == 0 { + // 兼容 judicialCases 字段 + if alt, ok2 := v["judicialCases"].([]interface{}); ok2 { + arr = alt + ok = len(arr) > 0 + } + } + if arr, ok := v["courtJudgments"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "裁判文书", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, c := range arr { + m, ok := c.(map[string]interface{}) + if !ok { + continue + } + caseNo := getString(m, "caseNumber") + if caseNo == "" { + caseNo = getString(m, "CASENUMBER") + } + if caseNo == "" { + caseNo = getString(m, "judicialDocumentId") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. 案号:%s", i+1, safePlaceholder(caseNo)), "", "L", false) + } + } + + // 司法协助 + if arr, ok := v["judicialAssists"].([]interface{}); !ok || len(arr) == 0 { + if alt, ok2 := v["judicialAids"].([]interface{}); ok2 { + arr = alt + ok = len(arr) > 0 + } + if ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "司法协助", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, a := range arr { + m, ok := a.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "iname") + if name == "" { + name = getString(m, "INAME") + } + if name == "" { + name = getString(m, "marketName") + } + court := getString(m, "courtName") + if court == "" { + court = getString(m, "COURTNAME") + } + share := getString(m, "shaream") + if share == "" { + share = getString(m, "SHAREAM") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(name)), "", "L", false) + meta := fmt.Sprintf(" 法院:%s;股权数额:%s 元", safePlaceholder(court), safePlaceholder(share)) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + } + + // 失信被执行人 + if arr, ok := v["dishonestDebtors"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "失信被执行人", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, d := range arr { + m, ok := d.(map[string]interface{}) + if !ok { + continue + } + caseNo := getString(m, "caseNo") + execCourt := getString(m, "execCourt") + performance := getString(m, "performanceStatus") + publishDate := getString(m, "publishDate") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. 案号 %s", i+1, safePlaceholder(caseNo)), "", "L", false) + meta := fmt.Sprintf(" 执行法院:%s;履行情况:%s;发布时间:%s", + safePlaceholder(execCourt), + safePlaceholder(performance), + safePlaceholder(publishDate), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 限高被执行人 + if arr, ok := v["limitHighDebtors"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "限高被执行人", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, x := range arr { + m, ok := x.(map[string]interface{}) + if !ok { + continue + } + ah := getString(m, "ah") + if ah == "" { + ah = getString(m, "caseNo") + } + zxfy := getString(m, "zxfy") + if zxfy == "" { + zxfy = getString(m, "execCourt") + } + fbrq := getString(m, "fbrq") + if fbrq == "" { + fbrq = getString(m, "publishDate") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(ah)), "", "L", false) + meta := fmt.Sprintf(" 执行法院:%s;发布日期:%s", safePlaceholder(zxfy), safePlaceholder(fbrq)) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 行政处罚 + if arr, ok := v["adminPenalties"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "行政处罚", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, p := range arr { + m, ok := p.(map[string]interface{}) + if !ok { + continue + } + pendecno := getString(m, "pendecno") + if pendecno == "" { + pendecno = getString(m, "PENDECNO") + } + illeg := getString(m, "illegacttype") + if illeg == "" { + illeg = getString(m, "ILLEGACTTYPE") + } + auth := getString(m, "penauth") + if auth == "" { + auth = getString(m, "PENAUTH") + } + date := getString(m, "pendecissdate") + if date == "" { + date = getString(m, "PENDECISSDATE") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(pendecno)), "", "L", false) + meta := fmt.Sprintf(" 违法类型:%s;机关:%s;日期:%s", safePlaceholder(illeg), safePlaceholder(auth), safePlaceholder(date)) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 行政处罚变更 + if arr, ok := v["adminPenaltyUpdates"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "行政处罚变更记录", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, p := range arr { + m, ok := p.(map[string]interface{}) + if !ok { + continue + } + date := getString(m, "updateDate") + content := getString(m, "updateContent") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(date)), "", "L", false) + if content != "" { + pdf.MultiCell(0, 5, " "+content, "", "L", false) + } + pdf.Ln(1) + } + } + + // 经营异常 + if arr, ok := v["exceptions"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "经营异常", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, e := range arr { + m, ok := e.(map[string]interface{}) + if !ok { + continue + } + indate := getString(m, "indate") + if indate == "" { + indate = getString(m, "INDATE") + } + inreason := getString(m, "inreason") + if inreason == "" { + inreason = getString(m, "INREASON") + } + outdate := getString(m, "outdate") + if outdate == "" { + outdate = getString(m, "OUTDATE") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(indate)), "", "L", false) + meta := fmt.Sprintf(" 原因:%s;移出:%s", safePlaceholder(inreason), safePlaceholder(outdate)) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 动产抵押 + if arr, ok := v["mortgages"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "动产抵押", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, m := range arr { + mm, ok := m.(map[string]interface{}) + if !ok { + continue + } + regNo := getString(mm, "regNo") + amount := getString(mm, "guaranteedAmount") + regDate := getString(mm, "regDate") + regOrg := getString(mm, "regOrg") + status := getString(mm, "status") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s %s", i+1, safePlaceholder(regNo), safePlaceholder(amount)), "", "L", false) + meta := fmt.Sprintf(" 登记日:%s;机关:%s;状态:%s", safePlaceholder(regDate), safePlaceholder(regOrg), safePlaceholder(status)) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + + // 简易注销 + if qc, ok := v["quickCancel"].(map[string]interface{}); ok && len(qc) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "简易注销", "", "L", false) + pdf.SetFont(fontName, "", 11) + writeKeyValue(pdf, fontName, "企业名称", getString(qc, "entName")) + writeKeyValue(pdf, fontName, "统一社会信用代码", getString(qc, "creditCode")) + writeKeyValue(pdf, fontName, "注册号", getString(qc, "regNo")) + writeKeyValue(pdf, fontName, "登记机关", getString(qc, "regOrg")) + writeKeyValue(pdf, fontName, "公告起始日期", getString(qc, "noticeFromDate")) + writeKeyValue(pdf, fontName, "公告结束日期", getString(qc, "noticeToDate")) + writeKeyValue(pdf, fontName, "注销结果", getString(qc, "cancelResult")) + } + + // 清算信息 + if liq, ok := v["liquidation"].(map[string]interface{}); ok && len(liq) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "清算信息", "", "L", false) + pdf.SetFont(fontName, "", 11) + writeKeyValue(pdf, fontName, "负责人", getString(liq, "principal")) + writeKeyValue(pdf, fontName, "成员", getString(liq, "members")) + } + + // 纳税与欠税(taxRecords) + if tax, ok := v["taxRecords"].(map[string]interface{}); ok && len(tax) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "纳税与欠税", "", "L", false) + pdf.SetFont(fontName, "", 11) + + ayears := getSlice(tax, "taxLevelAYears") + owings := getSlice(tax, "taxOwings") + hint := fmt.Sprintf("纳税A级年度:%s;欠税:%s", + formatCountHint(len(ayears)), + formatCountHint(len(owings)), + ) + pdf.MultiCell(0, 5, hint, "", "L", false) + + if len(owings) > 0 { + pdf.Ln(1) + for i, t := range owings { + m, ok := t.(map[string]interface{}) + if !ok { + continue + } + taxType := getString(m, "taxOwedType") + if taxType == "" { + taxType = getString(m, "taxowedtype") + } + total := getString(m, "totalOwedAmount") + if total == "" { + total = getString(m, "totalowedamount") + } + pub := getString(m, "publishDate") + if pub == "" { + pub = getString(m, "publishdate") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(taxType)), "", "L", false) + meta := fmt.Sprintf(" 合计:%s;公示日:%s", safePlaceholder(total), safePlaceholder(pub)) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + } else { + // 完全无 taxRecords 时,与 HTML 行为一致给一个“无”的提示 + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "纳税与欠税", "", "L", false) + pdf.SetFont(fontName, "", 11) + pdf.MultiCell(0, 5, "无", "", "L", false) + } + + // 涉诉案件(litigation)—— 按案件类型分类 + if lit, ok := v["litigation"].(map[string]interface{}); ok && len(lit) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "涉诉案件汇总", "", "L", false) + pdf.SetFont(fontName, "", 11) + + typeLabels := map[string]string{ + "administrative": "行政案件", + "implement": "执行案件", + "preservation": "非诉保全审查", + "civil": "民事案件", + "criminal": "刑事案件", + "bankrupt": "强制清算与破产案件", + "jurisdict": "管辖案件", + "compensate": "赔偿案件", + } + + // 汇总表 + for key, label := range typeLabels { + if sec, ok := lit[key].(map[string]interface{}); ok { + count := getString(sec, "count") + if count != "" && count != "0" { + pdf.MultiCell(0, 5, fmt.Sprintf("· %s:%s 件", label, count), "", "L", false) + } + } + } + + // 各类案件明细 + for key, label := range typeLabels { + sec, ok := lit[key].(map[string]interface{}) + if !ok { + continue + } + cases, ok := sec["cases"].([]interface{}) + if !ok || len(cases) == 0 { + continue + } + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, label, "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, c := range cases { + m, ok := c.(map[string]interface{}) + if !ok { + continue + } + caseNo := getString(m, "caseNo") + court := getString(m, "court") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s(%s)", i+1, safePlaceholder(caseNo), safePlaceholder(court)), "", "L", false) + region := getString(m, "region") + trialLevel := getString(m, "trialLevel") + caseType := getString(m, "caseType") + cause := getString(m, "cause") + amount := getString(m, "amount") + filing := getString(m, "filingDate") + judgment := getString(m, "judgmentDate") + victory := getString(m, "victoryResult") + meta := fmt.Sprintf(" 地区:%s;审级:%s;案件类型:%s;案由:%s;标的金额:%s;立案日期:%s;裁判日期:%s;胜败结果:%s", + safePlaceholder(region), + safePlaceholder(trialLevel), + safePlaceholder(caseType), + safePlaceholder(cause), + safePlaceholder(amount), + safePlaceholder(filing), + safePlaceholder(judgment), + safePlaceholder(victory), + ) + pdf.MultiCell(0, 5, meta, "", "L", false) + pdf.Ln(1) + } + } + } +} + +func renderPDFTimeline(pdf *gofpdf.Fpdf, fontName, title string, arr []interface{}) { + if len(arr) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + for i, it := range arr { + m, ok := it.(map[string]interface{}) + if !ok { + continue + } + date := getString(m, "date") + tp := getString(m, "type") + desc := getString(m, "title") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s %s", i+1, safePlaceholder(date), safePlaceholder(tp)), "", "L", false) + if desc != "" { + pdf.MultiCell(0, 5, " "+desc, "", "L", false) + } + pdf.Ln(1) + } +} + +func renderPDFListed(pdf *gofpdf.Fpdf, fontName, title string, v map[string]interface{}) { + if len(v) == 0 { + return + } + pdf.AddPage() + pdfSectionTitle(pdf, fontName, title) + pdf.Ln(1) + pdf.SetFont(fontName, "", 11) + + isListed := boolToCN(getString(v, "isListed")) + if isListed != "" { + writeKeyValue(pdf, fontName, "是否上市", isListed) + } + + // 上市公司信息 + if company, ok := v["company"].(map[string]interface{}); ok && len(company) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "上市公司信息", "", "L", false) + pdf.SetFont(fontName, "", 11) + writeKeyValue(pdf, fontName, "经营范围", getString(company, "bizScope")) + writeKeyValue(pdf, fontName, "信用代码", getString(company, "creditCode")) + writeKeyValue(pdf, fontName, "注册地址", getString(company, "regAddr")) + writeKeyValue(pdf, fontName, "注册资本", getString(company, "regCapital")) + writeKeyValue(pdf, fontName, "组织机构代码", getString(company, "orgCode")) + writeKeyValue(pdf, fontName, "币种", getString(company, "cur")) + writeKeyValue(pdf, fontName, "币种名称", getString(company, "curName")) + } + + // 股票信息 + if stock, ok := v["stock"]; ok && stock != nil { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "股票信息", "", "L", false) + pdf.SetFont(fontName, "", 11) + switch s := stock.(type) { + case string: + if s != "" { + pdf.MultiCell(0, 5, s, "", "L", false) + } + case map[string]interface{}: + // 简单将几个常见字段以表格形式展示 + code := getString(s, "code") + name := getString(s, "name") + market := getString(s, "market") + if code != "" { + writeKeyValue(pdf, fontName, "代码", code) + } + if name != "" { + writeKeyValue(pdf, fontName, "名称", name) + } + if market != "" { + writeKeyValue(pdf, fontName, "市场", market) + } + default: + // 其他类型非空时,用字符串方式输出 + txt := fmt.Sprint(stock) + if txt != "" && txt != "" { + pdf.MultiCell(0, 5, txt, "", "L", false) + } + } + } + + // 十大股东 + if arr, ok := v["topShareholders"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "十大股东", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, s := range arr { + m, ok := s.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + name = getString(m, "shaname") + } + pct := getString(m, "percent") + if pct == "" { + pct = getString(m, "fundedratio") + } + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(name)), "", "L", false) + if pct != "" { + pdf.MultiCell(0, 5, " 持股比例:"+pct, "", "L", false) + } + pdf.Ln(1) + } + } + + // 上市高管 + if arr, ok := v["listedManagers"].([]interface{}); ok && len(arr) > 0 { + pdf.Ln(2) + pdf.SetFont(fontName, "B", 12) + pdf.MultiCell(0, 6, "上市高管", "", "L", false) + pdf.SetFont(fontName, "", 11) + for i, mng := range arr { + m, ok := mng.(map[string]interface{}) + if !ok { + continue + } + name := getString(m, "name") + if name == "" { + name = getString(m, "perName") + } + position := getString(m, "position") + pdf.MultiCell(0, 6, fmt.Sprintf("%d. %s", i+1, safePlaceholder(name)), "", "L", false) + if position != "" { + pdf.MultiCell(0, 5, " 职务:"+position, "", "L", false) + } + pdf.Ln(1) + } + } +} + +// joinWithChineseSemicolon 使用中文分号连接字符串 +func joinWithChineseSemicolon(parts []string) string { + if len(parts) == 0 { + return "" + } + res := parts[0] + for i := 1; i < len(parts); i++ { + res += ";" + parts[i] + } + return res +} + +// pdfSectionTitle 渲染模块主标题(对应 HTML h2) +func pdfSectionTitle(pdf *gofpdf.Fpdf, fontName, title string) { + pdf.SetFont(fontName, "B", 17) + // 模块标题前加蓝色点缀方块 + pdf.SetTextColor(37, 99, 235) + pdf.CellFormat(0, 10, "■ "+title, "", 1, "L", false, 0, "") + pdf.SetTextColor(0, 0, 0) +} + +// pdfSubTitle 渲染模块内小节标题(对应 HTML h3) +func pdfSubTitle(pdf *gofpdf.Fpdf, fontName, title string) { + pdf.SetFont(fontName, "B", 14) + // 小节标题用圆点前缀,略微缩进 + pdf.SetTextColor(37, 99, 235) + pdf.CellFormat(0, 8, "● "+title, "", 1, "L", false, 0, "") + pdf.SetTextColor(0, 0, 0) +} + +// pdfWriteBasicRow 渲染基础信息的一行,使用隔行背景色而不加表格框 +func pdfWriteBasicRow(pdf *gofpdf.Fpdf, fontName, label, value string, alt bool) { + if value == "" { + return + } + pageW, pageH := pdf.GetPageSize() + lMargin, _, rMargin, bMargin := pdf.GetMargins() + x := lMargin + y := pdf.GetY() + w := pageW - lMargin - rMargin + labelW := 40.0 + valueW := w - labelW + // 行高整体拉宽一点 + lineH := 10.0 + + // 预先根据内容拆行,计算本行整体高度(用于背景块) + lines := pdf.SplitLines([]byte(value), valueW) + rowH := float64(len(lines)) * lineH + if rowH < lineH { + rowH = lineH + } + rowH += 2 + + // 如当前页剩余空间不足,先分页再画整行,避免内容与下一页重叠 + if y+rowH > pageH-bMargin { + pdf.AddPage() + y = pdf.GetY() + pdf.SetXY(lMargin, y) + } + + // 隔行底色 + if alt { + pdf.SetFillColor(242, 242, 242) + } else { + pdf.SetFillColor(255, 255, 255) + } + pdf.Rect(x, y, w, rowH, "F") + + // 标签列 + pdf.SetXY(x, y) + pdf.SetFont(fontName, "B", 14) + pdf.CellFormat(labelW, rowH, fmt.Sprintf("%s:", label), "", 0, "L", false, 0, "") + + // 值列自动换行 + pdf.SetXY(x+labelW, y) + pdf.SetFont(fontName, "", 14) + pdf.MultiCell(valueW, lineH, value, "", "L", false) + + // 移到下一行起始位置 + pdf.SetXY(lMargin, y+rowH) +} + +// formatCountHint 将条数格式化为“X 条”或“无” +func formatCountHint(n int) string { + if n <= 0 { + return "无" + } + return fmt.Sprintf("%d 条", n) +} + +// boolToCN 将 true/false/1/0/yes/no 映射成 “是/否” +func boolToCN(s string) string { + if s == "" { + return s + } + low := strings.ToLower(strings.TrimSpace(s)) + switch low { + case "true", "1", "yes", "y": + return "是" + case "false", "0", "no", "n": + return "否" + default: + return s + } +} + diff --git a/resources/qiye.html b/resources/qiye.html index bd839c6..047bbc9 100644 --- a/resources/qiye.html +++ b/resources/qiye.html @@ -474,15 +474,17 @@ .section-card { box-shadow: none; border-radius: 0; - page-break-inside: avoid; - break-inside: avoid; + /* 允许在模块内部分页,减少页面底部的空白 */ + page-break-inside: auto; + break-inside: auto; } .section-card h2 { page-break-after: avoid; } .item-list li { - page-break-inside: avoid; - break-inside: avoid; + /* 列表项允许在需要时被分页拆分 */ + page-break-inside: auto; + break-inside: auto; } /* 原始 JSON 展示在 PDF 中通常不需要过高容器限制 */ .raw-section pre { @@ -661,6 +663,12 @@ isListed: "是否上市", company: "上市公司信息", stock: "股票信息", + // 上市公司信息字段 + bizScope: "经营范围", + regAddr: "注册地址", + regCapital: "注册资本", + cur: "币种", + curName: "币种名称", }; function label(key) {