f
This commit is contained in:
77
cmd/qygl_report_build/main.go
Normal file
77
cmd/qygl_report_build/main.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// 将 raw fixture(各处理器原始 JSON)经 BuildReportFromRawSources 转化为与线上一致的完整报告 JSON。
|
||||||
|
//
|
||||||
|
// go run ./cmd/qygl_report_build -in resources/dev-report/fixture.raw.example.json -out resources/dev-report/built.json
|
||||||
|
// go run ./cmd/qygl_report_build -in raw.json -out - # 输出到 stdout
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rawBundle struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
|
||||||
|
JiguangFull map[string]interface{} `json:"jiguangFull"`
|
||||||
|
JudicialCertFull map[string]interface{} `json:"judicialCertFull"`
|
||||||
|
EquityPanorama map[string]interface{} `json:"equityPanorama"`
|
||||||
|
AnnualReport map[string]interface{} `json:"annualReport"`
|
||||||
|
TaxViolation map[string]interface{} `json:"taxViolation"`
|
||||||
|
TaxArrears map[string]interface{} `json:"taxArrears"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
inPath := flag.String("in", "", "raw fixture JSON 路径(含 jiguangFull 等字段,可参考 fixture.raw.example.json)")
|
||||||
|
outPath := flag.String("out", "", "输出文件;- 或留空表示输出到 stdout")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *inPath == "" {
|
||||||
|
log.Fatal("请指定 -in <raw.json>")
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(*inPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("读取输入失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b rawBundle
|
||||||
|
if err := json.Unmarshal(raw, &b); err != nil {
|
||||||
|
log.Fatalf("解析 JSON 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Kind == "full" {
|
||||||
|
log.Fatal("输入为 kind=full(已是 build 结果),无需再转化;预览请用: go run ./cmd/qygl_report_preview")
|
||||||
|
}
|
||||||
|
if b.Kind != "" && b.Kind != "raw" {
|
||||||
|
log.Fatalf("若填写 kind,仅支持 raw,当前: %q", b.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
report := qygl.BuildReportFromRawSources(
|
||||||
|
b.JiguangFull,
|
||||||
|
b.JudicialCertFull,
|
||||||
|
b.EquityPanorama,
|
||||||
|
b.AnnualReport,
|
||||||
|
b.TaxViolation,
|
||||||
|
b.TaxArrears,
|
||||||
|
)
|
||||||
|
|
||||||
|
out, err := json.MarshalIndent(report, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("序列化报告失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *outPath == "" || *outPath == "-" {
|
||||||
|
if _, err := os.Stdout.Write(append(out, '\n')); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(*outPath, append(out, '\n'), 0644); err != nil {
|
||||||
|
log.Fatalf("写入失败: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "已写入 %s\n", *outPath)
|
||||||
|
}
|
||||||
159
cmd/qygl_report_preview/main.go
Normal file
159
cmd/qygl_report_preview/main.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// 仅读取 build 后的报告 JSON,本地渲染 qiye.html(不执行 BuildReportFromRawSources)。
|
||||||
|
//
|
||||||
|
// go run ./cmd/qygl_report_preview -in resources/dev-report/built.json
|
||||||
|
// go run ./cmd/qygl_report_preview -in built.json -addr :8899 -watch
|
||||||
|
//
|
||||||
|
// 每次打开/刷新页面都会重新读取 -in 文件;加 -watch 后保存 JSON 会自动刷新浏览器。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseBuiltReport(data []byte) (map[string]interface{}, error) {
|
||||||
|
var root map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &root); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := root["jiguangFull"]; ok {
|
||||||
|
return nil, fmt.Errorf("检测到 raw 字段 jiguangFull,请先执行: go run ./cmd/qygl_report_build -in <raw.json> -out built.json")
|
||||||
|
}
|
||||||
|
if k, _ := root["kind"].(string); k == "full" {
|
||||||
|
r, ok := root["report"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("kind=full 时缺少 report 对象")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
if r, ok := root["report"].(map[string]interface{}); ok {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
if root["entName"] != nil || root["basic"] != nil || root["reportTime"] != nil {
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("不是有效的 build 后报告(根级应有 entName、basic、reportTime 之一,或 {\"report\":{...}} / kind=full)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileVersionTag(path string) (string, error) {
|
||||||
|
st, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d-%d", st.ModTime().UnixNano(), st.Size()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPage(tmpl *template.Template, report map[string]interface{}, injectLive bool) ([]byte, error) {
|
||||||
|
reportBytes, err := json.Marshal(report)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, map[string]interface{}{
|
||||||
|
"ReportJSON": template.JS(reportBytes),
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b := buf.Bytes()
|
||||||
|
if !injectLive {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
script := `<script>(function(){var v0=null;function tick(){fetch("/__version?="+Date.now(),{cache:"no-store"}).then(function(r){return r.text();}).then(function(v){if(v==="")return;if(v0===null)v0=v;else if(v0!==v){v0=v;location.reload();}}).catch(function(){});}setInterval(tick,600);tick();})();</script>`
|
||||||
|
closing := []byte("</body>")
|
||||||
|
idx := bytes.LastIndex(b, closing)
|
||||||
|
if idx < 0 {
|
||||||
|
return append(b, []byte(script)...), nil
|
||||||
|
}
|
||||||
|
out := make([]byte, 0, len(b)+len(script))
|
||||||
|
out = append(out, b[:idx]...)
|
||||||
|
out = append(out, script...)
|
||||||
|
out = append(out, b[idx:]...)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
addr := flag.String("addr", ":8899", "监听地址")
|
||||||
|
root := flag.String("root", ".", "项目根目录(含 resources/qiye.html)")
|
||||||
|
inPath := flag.String("in", "", "build 后的 JSON(由 qygl_report_build 生成,或 fixture.full 中的 report 形态)")
|
||||||
|
watch := flag.Bool("watch", false, "监听 -in 文件变化并自动刷新浏览器(轮询)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *inPath == "" {
|
||||||
|
log.Fatal("请指定 -in <built.json>")
|
||||||
|
}
|
||||||
|
|
||||||
|
rootAbs, err := filepath.Abs(*root)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("解析 root: %v", err)
|
||||||
|
}
|
||||||
|
tplPath := filepath.Join(rootAbs, "resources", "qiye.html")
|
||||||
|
if _, err := os.Stat(tplPath); err != nil {
|
||||||
|
log.Fatalf("未找到模板 %s: %v", tplPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var inAbs string
|
||||||
|
if filepath.IsAbs(*inPath) {
|
||||||
|
inAbs = *inPath
|
||||||
|
} else {
|
||||||
|
inAbs = filepath.Join(rootAbs, *inPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(inAbs); err != nil {
|
||||||
|
log.Fatalf("读取 %s: %v", inAbs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.ParseFiles(tplPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("解析模板: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/__version", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tag, err := fileVersionTag(inAbs)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(tag))
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(inAbs)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "读取报告文件失败: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
report, err := parseBuiltReport(raw)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "解析 JSON 失败: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
html, err := renderPage(tmpl, report, *watch)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "渲染失败: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = w.Write(html)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("报告预览: http://127.0.0.1%s/ (每请求重读 %s)", *addr, inAbs)
|
||||||
|
if *watch {
|
||||||
|
log.Printf("已启用 -watch:保存 JSON 后约 0.6s 内自动刷新页面")
|
||||||
|
}
|
||||||
|
if err := http.ListenAndServe(*addr, nil); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,6 +119,7 @@ jwt:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
domain: "api.tianyuanapi.com"
|
domain: "api.tianyuanapi.com"
|
||||||
|
# public_base_url: "" # 可选,无尾斜杠;空则按 https://{domain} 推导;环境变量 API_PUBLIC_BASE_URL 优先
|
||||||
|
|
||||||
sms:
|
sms:
|
||||||
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ database:
|
|||||||
jwt:
|
jwt:
|
||||||
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
|
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
|
||||||
|
|
||||||
|
# 本地联调:企业报告链接与 headless PDF 需能访问到本机服务;端口与 server 监听一致。环境变量 API_PUBLIC_BASE_URL 可覆盖。
|
||||||
|
api:
|
||||||
|
public_base_url: "http://127.0.0.1:8080"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 📁 存储服务配置 - 七牛云
|
# 📁 存储服务配置 - 七牛云
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ jwt:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
domain: "api.tianyuanapi.com"
|
domain: "api.tianyuanapi.com"
|
||||||
|
# 可选:对外可访问的 API 完整基址(无尾斜杠),用于企业报告 reportUrl、PDF 预生成等;不设则按 https://{domain} 推导。环境变量 API_PUBLIC_BASE_URL 优先于本项。
|
||||||
|
# public_base_url: "https://api.tianyuanapi.com"
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 📁 存储服务配置 - 七牛云
|
# 📁 存储服务配置 - 七牛云
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -200,6 +202,37 @@ type AppConfig struct {
|
|||||||
// APIConfig API配置
|
// APIConfig API配置
|
||||||
type APIConfig struct {
|
type APIConfig struct {
|
||||||
Domain string `mapstructure:"domain"`
|
Domain string `mapstructure:"domain"`
|
||||||
|
// PublicBaseURL 浏览器/第三方访问本 API 服务的完整基址(如 https://api.example.com 或 http://127.0.0.1:8080),无尾斜杠。
|
||||||
|
// 用于企业全景报告 reportUrl、headless PDF 预生成等。为空时由 Domain 推导为 https://{Domain}(Domain 若已含 scheme 则沿用)。
|
||||||
|
PublicBaseURL string `mapstructure:"public_base_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvedPublicBaseURL 由配置推导对外基址(不读环境变量)。
|
||||||
|
func (c *APIConfig) ResolvedPublicBaseURL() string {
|
||||||
|
u := strings.TrimSpace(c.PublicBaseURL)
|
||||||
|
if u != "" {
|
||||||
|
return strings.TrimRight(u, "/")
|
||||||
|
}
|
||||||
|
d := strings.TrimSpace(c.Domain)
|
||||||
|
if d == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
lo := strings.ToLower(d)
|
||||||
|
if strings.HasPrefix(lo, "http://") || strings.HasPrefix(lo, "https://") {
|
||||||
|
return strings.TrimRight(d, "/")
|
||||||
|
}
|
||||||
|
return "https://" + strings.TrimRight(d, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveAPIPublicBaseURL 对外 API 基址。优先环境变量 API_PUBLIC_BASE_URL,否则使用 API 配置。
|
||||||
|
func ResolveAPIPublicBaseURL(cfg *APIConfig) string {
|
||||||
|
if s := strings.TrimSpace(os.Getenv("API_PUBLIC_BASE_URL")); s != "" {
|
||||||
|
return strings.TrimRight(s, "/")
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return cfg.ResolvedPublicBaseURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMSConfig 短信配置
|
// SMSConfig 短信配置
|
||||||
|
|||||||
@@ -1210,6 +1210,13 @@ func NewContainer() *Container {
|
|||||||
return cacheManager, nil
|
return cacheManager, nil
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// 企业全景报告 PDF 异步预生成(依赖 PDF 缓存目录与公网可访问基址)
|
||||||
|
fx.Provide(
|
||||||
|
func(cfg *config.Config, logger *zap.Logger, cache *pdf.PDFCacheManager) *pdf.QYGLReportPDFPregen {
|
||||||
|
base := config.ResolveAPIPublicBaseURL(&cfg.API)
|
||||||
|
return pdf.NewQYGLReportPDFPregen(logger, cache, base)
|
||||||
|
},
|
||||||
|
),
|
||||||
// 本地文件存储服务
|
// 本地文件存储服务
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
func(logger *zap.Logger) *storage.LocalFileStorageService {
|
func(logger *zap.Logger) *storage.LocalFileStorageService {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"tyapi-server/internal/application/api/commands"
|
"tyapi-server/internal/application/api/commands"
|
||||||
"tyapi-server/internal/config"
|
appconfig "tyapi-server/internal/config"
|
||||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||||
"tyapi-server/internal/domains/api/services/processors"
|
"tyapi-server/internal/domains/api/services/processors"
|
||||||
"tyapi-server/internal/domains/api/services/processors/comb"
|
"tyapi-server/internal/domains/api/services/processors/comb"
|
||||||
@@ -50,7 +50,7 @@ type ApiRequestService struct {
|
|||||||
validator interfaces.RequestValidator
|
validator interfaces.RequestValidator
|
||||||
processorDeps *processors.ProcessorDependencies
|
processorDeps *processors.ProcessorDependencies
|
||||||
combService *comb.CombService
|
combService *comb.CombService
|
||||||
config *config.Config
|
config *appconfig.Config
|
||||||
|
|
||||||
reportRepo api_repositories.ReportRepository
|
reportRepo api_repositories.ReportRepository
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ func NewApiRequestService(
|
|||||||
shumaiService *shumai.ShumaiService,
|
shumaiService *shumai.ShumaiService,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
productManagementService *services.ProductManagementService,
|
productManagementService *services.ProductManagementService,
|
||||||
cfg *config.Config,
|
cfg *appconfig.Config,
|
||||||
) *ApiRequestService {
|
) *ApiRequestService {
|
||||||
return NewApiRequestServiceWithRepos(
|
return NewApiRequestServiceWithRepos(
|
||||||
westDexService,
|
westDexService,
|
||||||
@@ -85,6 +85,7 @@ func NewApiRequestService(
|
|||||||
productManagementService,
|
productManagementService,
|
||||||
cfg,
|
cfg,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,12 +103,18 @@ func NewApiRequestServiceWithRepos(
|
|||||||
shumaiService *shumai.ShumaiService,
|
shumaiService *shumai.ShumaiService,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
productManagementService *services.ProductManagementService,
|
productManagementService *services.ProductManagementService,
|
||||||
cfg *config.Config,
|
cfg *appconfig.Config,
|
||||||
reportRepo api_repositories.ReportRepository,
|
reportRepo api_repositories.ReportRepository,
|
||||||
|
qyglReportPDFScheduler processors.QYGLReportPDFScheduler,
|
||||||
) *ApiRequestService {
|
) *ApiRequestService {
|
||||||
// 创建组合包服务
|
// 创建组合包服务
|
||||||
combService := comb.NewCombService(productManagementService)
|
combService := comb.NewCombService(productManagementService)
|
||||||
|
|
||||||
|
apiPublicBase := ""
|
||||||
|
if cfg != nil {
|
||||||
|
apiPublicBase = appconfig.ResolveAPIPublicBaseURL(&cfg.API)
|
||||||
|
}
|
||||||
|
|
||||||
// 创建处理器依赖容器
|
// 创建处理器依赖容器
|
||||||
processorDeps := processors.NewProcessorDependencies(
|
processorDeps := processors.NewProcessorDependencies(
|
||||||
westDexService,
|
westDexService,
|
||||||
@@ -123,6 +130,8 @@ func NewApiRequestServiceWithRepos(
|
|||||||
validator,
|
validator,
|
||||||
combService,
|
combService,
|
||||||
reportRepo,
|
reportRepo,
|
||||||
|
qyglReportPDFScheduler,
|
||||||
|
apiPublicBase,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 统一注册所有处理器
|
// 统一注册所有处理器
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||||
"tyapi-server/internal/infrastructure/external/muzi"
|
"tyapi-server/internal/infrastructure/external/muzi"
|
||||||
|
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||||
"tyapi-server/internal/infrastructure/external/shumai"
|
"tyapi-server/internal/infrastructure/external/shumai"
|
||||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||||
"tyapi-server/internal/infrastructure/external/westdex"
|
"tyapi-server/internal/infrastructure/external/westdex"
|
||||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
|
||||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||||
"tyapi-server/internal/infrastructure/external/yushan"
|
"tyapi-server/internal/infrastructure/external/yushan"
|
||||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||||
@@ -47,6 +47,12 @@ type ProcessorDependencies struct {
|
|||||||
|
|
||||||
// 企业报告记录仓储,用于持久化 QYGLJ1U9 生成的企业报告
|
// 企业报告记录仓储,用于持久化 QYGLJ1U9 生成的企业报告
|
||||||
ReportRepo repositories.ReportRepository
|
ReportRepo repositories.ReportRepository
|
||||||
|
|
||||||
|
// 企业报告 PDF 异步预生成(可为 nil)
|
||||||
|
ReportPDFScheduler QYGLReportPDFScheduler
|
||||||
|
|
||||||
|
// APIPublicBaseURL 对外 API 根地址(无尾斜杠),用于 QYGL reportUrl 等
|
||||||
|
APIPublicBaseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessorDependencies 创建处理器依赖容器
|
// NewProcessorDependencies 创建处理器依赖容器
|
||||||
@@ -64,23 +70,27 @@ func NewProcessorDependencies(
|
|||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
combService CombServiceInterface, // Changed to interface
|
combService CombServiceInterface, // Changed to interface
|
||||||
reportRepo repositories.ReportRepository,
|
reportRepo repositories.ReportRepository,
|
||||||
|
reportPDFScheduler QYGLReportPDFScheduler,
|
||||||
|
apiPublicBaseURL string,
|
||||||
) *ProcessorDependencies {
|
) *ProcessorDependencies {
|
||||||
return &ProcessorDependencies{
|
return &ProcessorDependencies{
|
||||||
WestDexService: westDexService,
|
WestDexService: westDexService,
|
||||||
ShujubaoService: shujubaoService,
|
ShujubaoService: shujubaoService,
|
||||||
MuziService: muziService,
|
MuziService: muziService,
|
||||||
YushanService: yushanService,
|
YushanService: yushanService,
|
||||||
TianYanChaService: tianYanChaService,
|
TianYanChaService: tianYanChaService,
|
||||||
AlicloudService: alicloudService,
|
AlicloudService: alicloudService,
|
||||||
ZhichaService: zhichaService,
|
ZhichaService: zhichaService,
|
||||||
XingweiService: xingweiService,
|
XingweiService: xingweiService,
|
||||||
JiguangService: jiguangService,
|
JiguangService: jiguangService,
|
||||||
ShumaiService: shumaiService,
|
ShumaiService: shumaiService,
|
||||||
Validator: validator,
|
Validator: validator,
|
||||||
CombService: combService,
|
CombService: combService,
|
||||||
Options: nil, // 初始化为nil,在调用时设置
|
Options: nil, // 初始化为nil,在调用时设置
|
||||||
CallContext: nil, // 初始化为nil,在调用时设置
|
CallContext: nil, // 初始化为nil,在调用时设置
|
||||||
ReportRepo: reportRepo,
|
ReportRepo: reportRepo,
|
||||||
|
ReportPDFScheduler: reportPDFScheduler,
|
||||||
|
APIPublicBaseURL: apiPublicBaseURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ProcessQYGLJ1U9Request 企业全景报告处理器:并发调用企业全量(QYGLUY3S)、股权全景(QYGLJ0Q1)、司法涉诉(QYGL5S1I),
|
// ProcessQYGLJ1U9Request 企业全景报告处理器:并发调用企业全量(QYGLUY3S)、股权全景(QYGLJ0Q1)、司法涉诉(QYGL5S1I),
|
||||||
// 然后复用 qyglj1u9_processor_build.go 中的 buildReport / map* 逻辑生成企业报告结构
|
// 以及企业年报(QYGLDJ12)、税收违法(QYGL8848)、欠税公告(QYGL7D9A);
|
||||||
|
// 前三者走 buildReport / map*;后三者在 buildReport 中转为 annualReports、taxViolations、ownTaxNotices 供页面展示。
|
||||||
func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
// 复用 QYGLUY3S 的入参结构:企业名称/注册号/统一社会信用代码
|
// 复用 QYGLUY3S 的入参结构:企业名称/注册号/统一社会信用代码
|
||||||
var p dto.QYGLJ1U9Req
|
var p dto.QYGLJ1U9Req
|
||||||
@@ -34,7 +36,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
resultsCh := make(chan apiResult, 3)
|
resultsCh := make(chan apiResult, 6)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
call := func(key string, req interface{}, fn func(context.Context, []byte, *processors.ProcessorDependencies) ([]byte, error)) {
|
call := func(key string, req interface{}, fn func(context.Context, []byte, *processors.ProcessorDependencies) ([]byte, error)) {
|
||||||
@@ -52,8 +54,15 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var m map[string]interface{}
|
var m map[string]interface{}
|
||||||
if err := json.Unmarshal(resp, &m); err != nil {
|
var uerr error
|
||||||
resultsCh <- apiResult{key: key, err: err}
|
// 根节点可能是数组或非对象,与欠税接口一致用宽松解析
|
||||||
|
if key == "taxArrears" || key == "annualReport" || key == "taxViolation" {
|
||||||
|
m, uerr = unmarshalToReportMap(resp)
|
||||||
|
} else {
|
||||||
|
uerr = json.Unmarshal(resp, &m)
|
||||||
|
}
|
||||||
|
if uerr != nil {
|
||||||
|
resultsCh <- apiResult{key: key, err: uerr}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultsCh <- apiResult{key: key, data: m}
|
resultsCh <- apiResult{key: key, data: m}
|
||||||
@@ -62,8 +71,8 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
|
|
||||||
// 企业全量信息核验V2(QYGLUY3S)
|
// 企业全量信息核验V2(QYGLUY3S)
|
||||||
call("jiguangFull", map[string]interface{}{
|
call("jiguangFull", map[string]interface{}{
|
||||||
"ent_name": p.EntName,
|
"ent_name": p.EntName,
|
||||||
"ent_code": p.EntCode,
|
"ent_code": p.EntCode,
|
||||||
}, ProcessQYGLUY3SRequest)
|
}, ProcessQYGLUY3SRequest)
|
||||||
|
|
||||||
// 企业股权结构全景(QYGLJ0Q1)
|
// 企业股权结构全景(QYGLJ0Q1)
|
||||||
@@ -77,10 +86,30 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
"ent_code": p.EntCode,
|
"ent_code": p.EntCode,
|
||||||
}, ProcessQYGL5S1IRequest)
|
}, ProcessQYGL5S1IRequest)
|
||||||
|
|
||||||
|
// 企业年报信息核验(QYGLDJ12)
|
||||||
|
call("annualReport", map[string]interface{}{
|
||||||
|
"ent_name": p.EntName,
|
||||||
|
"ent_code": p.EntCode,
|
||||||
|
}, ProcessQYGLDJ12Request)
|
||||||
|
|
||||||
|
// 企业税收违法核查(QYGL8848)
|
||||||
|
call("taxViolation", map[string]interface{}{
|
||||||
|
"ent_name": p.EntName,
|
||||||
|
"ent_code": p.EntCode,
|
||||||
|
}, ProcessQYGL8848Request)
|
||||||
|
|
||||||
|
// 欠税公告(QYGL7D9A,天眼查 OwnTax,keyword 为统一社会信用代码)
|
||||||
|
call("taxArrears", map[string]interface{}{
|
||||||
|
"ent_code": p.EntCode,
|
||||||
|
"page_size": 20,
|
||||||
|
"page_num": 1,
|
||||||
|
}, ProcessQYGL7D9ARequest)
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(resultsCh)
|
close(resultsCh)
|
||||||
|
|
||||||
var jiguang, judicial, equity map[string]interface{}
|
var jiguang, judicial, equity map[string]interface{}
|
||||||
|
var annualReport, taxViolation, taxArrears map[string]interface{}
|
||||||
for r := range resultsCh {
|
for r := range resultsCh {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
// 任一关键数据源异常,则返回系统错误(也可以根据需求做降级)
|
// 任一关键数据源异常,则返回系统错误(也可以根据需求做降级)
|
||||||
@@ -93,6 +122,12 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
judicial = r.data
|
judicial = r.data
|
||||||
case "equityPanorama":
|
case "equityPanorama":
|
||||||
equity = r.data
|
equity = r.data
|
||||||
|
case "annualReport":
|
||||||
|
annualReport = r.data
|
||||||
|
case "taxViolation":
|
||||||
|
taxViolation = r.data
|
||||||
|
case "taxArrears":
|
||||||
|
taxArrears = r.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if jiguang == nil {
|
if jiguang == nil {
|
||||||
@@ -104,14 +139,28 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
if equity == nil {
|
if equity == nil {
|
||||||
equity = map[string]interface{}{}
|
equity = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
|
if annualReport == nil {
|
||||||
|
annualReport = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if taxViolation == nil {
|
||||||
|
taxViolation = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if taxArrears == nil {
|
||||||
|
taxArrears = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
// 复用构建逻辑生成企业报告结构
|
// 复用构建逻辑生成企业报告结构(含年报 / 税收违法 / 欠税公告的转化结果)
|
||||||
report := buildReport(jiguang, judicial, equity)
|
report := buildReport(jiguang, judicial, equity, annualReport, taxViolation, taxArrears)
|
||||||
|
|
||||||
// 为报告生成唯一编号并缓存,供后续通过编号查看
|
// 为报告生成唯一编号并缓存,供后续通过编号查看
|
||||||
reportID := saveQYGLReport(report)
|
reportID := saveQYGLReport(report)
|
||||||
report["reportId"] = reportID
|
report["reportId"] = reportID
|
||||||
|
|
||||||
|
// 异步预生成 PDF(写入磁盘缓存),用户点击「保存为 PDF」时可直读缓存
|
||||||
|
if deps.ReportPDFScheduler != nil {
|
||||||
|
deps.ReportPDFScheduler.ScheduleQYGLReportPDF(context.Background(), reportID)
|
||||||
|
}
|
||||||
|
|
||||||
// 持久化企业报告记录到数据库(忽略持久化失败,不影响接口主流程)
|
// 持久化企业报告记录到数据库(忽略持久化失败,不影响接口主流程)
|
||||||
if deps.ReportRepo != nil {
|
if deps.ReportRepo != nil {
|
||||||
reqJSON, _ := json.Marshal(p)
|
reqJSON, _ := json.Marshal(p)
|
||||||
@@ -127,7 +176,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 为报告补充前端查看链接,供调用方直接跳转到企业报告页面(通过编号访问)
|
// 为报告补充前端查看链接,供调用方直接跳转到企业报告页面(通过编号访问)
|
||||||
report["reportUrl"] = buildQYGLReportURLByID(reportID)
|
report["reportUrl"] = buildQYGLReportURLByID(deps.APIPublicBaseURL, reportID)
|
||||||
|
|
||||||
out, err := json.Marshal(report)
|
out, err := json.Marshal(report)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -136,6 +185,18 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unmarshalToReportMap 将 JSON 解析为报告用 map;根节点非对象时包在 data 下(兼容欠税等接口根为数组的情况)。
|
||||||
|
func unmarshalToReportMap(b []byte) (map[string]interface{}, error) {
|
||||||
|
var raw interface{}
|
||||||
|
if err := json.Unmarshal(b, &raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if m, ok := raw.(map[string]interface{}); ok {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return map[string]interface{}{"data": raw}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// 内存中的企业报告缓存(简单实现,进程重启后清空)
|
// 内存中的企业报告缓存(简单实现,进程重启后清空)
|
||||||
var qyglReportStore = struct {
|
var qyglReportStore = struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
@@ -171,8 +232,12 @@ func generateQYGLReportID() string {
|
|||||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildQYGLReportURLByID 构造企业报告前端查看链接(通过编号查看)
|
// buildQYGLReportURLByID 构造企业报告前端查看链接(通过编号查看)。
|
||||||
func buildQYGLReportURLByID(id string) string {
|
// publicBase 为对外 API 基址(如 https://api.example.com),空则返回站内相对路径。
|
||||||
return "https://api.tianyuanapi.com/reports/qygl/" + url.PathEscape(id)
|
func buildQYGLReportURLByID(publicBase, id string) string {
|
||||||
|
path := "/reports/qygl/" + url.PathEscape(id)
|
||||||
|
if publicBase == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return strings.TrimRight(publicBase, "/") + path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildReport(jiguang, judicial, equity map[string]interface{}) map[string]interface{} {
|
func buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw map[string]interface{}) map[string]interface{} {
|
||||||
report := make(map[string]interface{})
|
report := make(map[string]interface{})
|
||||||
report["reportTime"] = time.Now().Format("2006-01-02 15:04:05")
|
report["reportTime"] = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// 先转化年报接口数据;若有内容则以 QYGLDJ12 为准,不再使用全量 V2 中 YEARREPORT* 表(避免与「企业年报」板块重复)
|
||||||
|
annualReports := mapAnnualReports(annualRaw)
|
||||||
|
jgNoYearReport := jiguang
|
||||||
|
if len(annualReports) > 0 {
|
||||||
|
jgNoYearReport = jiguangWithoutYearReportTables(jiguang)
|
||||||
|
}
|
||||||
|
|
||||||
basic := mapFromBASIC(jiguang)
|
basic := mapFromBASIC(jiguang)
|
||||||
report["creditCode"] = str(basic["creditCode"])
|
report["creditCode"] = str(basic["creditCode"])
|
||||||
report["entName"] = str(basic["entName"])
|
report["entName"] = str(basic["entName"])
|
||||||
@@ -20,21 +27,22 @@ func buildReport(jiguang, judicial, equity map[string]interface{}) map[string]in
|
|||||||
report["branches"] = mapBranches(jiguang)
|
report["branches"] = mapBranches(jiguang)
|
||||||
// 股权/实控人/受益人/对外投资:有股权全景时以其为准,否则用全量信息
|
// 股权/实控人/受益人/对外投资:有股权全景时以其为准,否则用全量信息
|
||||||
if len(equity) > 0 {
|
if len(equity) > 0 {
|
||||||
report["shareholding"] = mapShareholdingWithEquity(jiguang, equity)
|
report["shareholding"] = mapShareholdingWithEquity(jgNoYearReport, equity)
|
||||||
report["controller"] = mapControllerFromEquity(equity)
|
report["controller"] = mapControllerFromEquity(equity)
|
||||||
report["beneficiaries"] = mapBeneficiariesFromEquity(equity)
|
report["beneficiaries"] = mapBeneficiariesFromEquity(equity)
|
||||||
report["investments"] = mapInvestmentsWithEquity(jiguang, equity)
|
report["investments"] = mapInvestmentsWithEquity(jiguang, equity)
|
||||||
} else {
|
} else {
|
||||||
report["shareholding"] = mapShareholding(jiguang)
|
report["shareholding"] = mapShareholding(jgNoYearReport)
|
||||||
report["controller"] = mapController(jiguang)
|
report["controller"] = mapController(jiguang)
|
||||||
report["beneficiaries"] = mapBeneficiaries()
|
report["beneficiaries"] = mapBeneficiaries()
|
||||||
report["investments"] = mapInvestments(jiguang)
|
report["investments"] = mapInvestments(jiguang)
|
||||||
}
|
}
|
||||||
report["guarantees"] = mapGuarantees(jiguang)
|
// 以下块在全量 V2 中依赖年报类表;接入 DJ12 后改从 jgNoYearReport 读取(已剔除 YEARREPORT*)
|
||||||
report["management"] = mapManagement(jiguang)
|
report["guarantees"] = mapGuarantees(jgNoYearReport)
|
||||||
report["assets"] = mapAssets(jiguang)
|
report["management"] = mapManagement(jgNoYearReport)
|
||||||
|
report["assets"] = mapAssets(jgNoYearReport)
|
||||||
report["licenses"] = mapLicenses(jiguang)
|
report["licenses"] = mapLicenses(jiguang)
|
||||||
report["activities"] = mapActivities(jiguang)
|
report["activities"] = mapActivities(jgNoYearReport)
|
||||||
report["inspections"] = mapInspections(jiguang)
|
report["inspections"] = mapInspections(jiguang)
|
||||||
risks := mapRisks(jiguang, judicial)
|
risks := mapRisks(jiguang, judicial)
|
||||||
report["risks"] = risks
|
report["risks"] = risks
|
||||||
@@ -45,9 +53,215 @@ func buildReport(jiguang, judicial, equity map[string]interface{}) map[string]in
|
|||||||
if bl, _ := jiguang["BASICLIST"].([]interface{}); len(bl) > 0 {
|
if bl, _ := jiguang["BASICLIST"].([]interface{}); len(bl) > 0 {
|
||||||
report["basicList"] = bl
|
report["basicList"] = bl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QYGLDJ12 企业年报 / QYGL8848 税收违法 / QYGL7D9A 欠税公告(转化后的前端友好结构)
|
||||||
|
report["annualReports"] = annualReports
|
||||||
|
report["taxViolations"] = mapTaxViolations(taxViolationRaw)
|
||||||
|
report["ownTaxNotices"] = mapOwnTaxNotices(taxArrearsRaw)
|
||||||
return report
|
return report
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jiguangWithoutYearReportTables 浅拷贝全量 map,并去掉企业全量 V2 中与「公示年报」对应的 YEARREPORT* 键。
|
||||||
|
// 在已接入 QYGLDJ12 且年报列表非空时使用,避免 build 与 HTML 中与「十六、企业年报」重复展示。
|
||||||
|
func jiguangWithoutYearReportTables(jiguang map[string]interface{}) map[string]interface{} {
|
||||||
|
if len(jiguang) == 0 {
|
||||||
|
return map[string]interface{}{}
|
||||||
|
}
|
||||||
|
strip := map[string]struct{}{
|
||||||
|
"YEARREPORTFORGUARANTEE": {},
|
||||||
|
"YEARREPORTPAIDUPCAPITAL": {},
|
||||||
|
"YEARREPORTSUBCAPITAL": {},
|
||||||
|
"YEARREPORTBASIC": {},
|
||||||
|
"YEARREPORTSOCSEC": {},
|
||||||
|
"YEARREPORTANASSETSINFO": {},
|
||||||
|
"YEARREPORTWEBSITEINFO": {},
|
||||||
|
}
|
||||||
|
out := make(map[string]interface{}, len(jiguang))
|
||||||
|
for k, v := range jiguang {
|
||||||
|
if _, drop := strip[k]; drop {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildReportFromRawSources 供开发/测试:将各处理器原始 JSON(与 QYGLJ1U9 并发结果形态一致)走与线上一致的 buildReport 转化。
|
||||||
|
// 任一路传入 nil 时按空 map 处理。
|
||||||
|
func BuildReportFromRawSources(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw map[string]interface{}) map[string]interface{} {
|
||||||
|
if jiguang == nil {
|
||||||
|
jiguang = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if judicial == nil {
|
||||||
|
judicial = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if equity == nil {
|
||||||
|
equity = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if annualRaw == nil {
|
||||||
|
annualRaw = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if taxViolationRaw == nil {
|
||||||
|
taxViolationRaw = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if taxArrearsRaw == nil {
|
||||||
|
taxArrearsRaw = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
return buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractJSONArrayFromEnterpriseAPI 从数据宝/天眼查类响应中提取数组主体(data/list/result 等)。
|
||||||
|
func extractJSONArrayFromEnterpriseAPI(m map[string]interface{}) []interface{} {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
priority := []string{"data", "list", "result", "records", "items", "body"}
|
||||||
|
for _, k := range priority {
|
||||||
|
if arr := sliceOrEmpty(m[k]); len(arr) > 0 {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m) == 1 {
|
||||||
|
for _, v := range m {
|
||||||
|
if arr, ok := v.([]interface{}); ok {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func intFromAny(v interface{}) int {
|
||||||
|
if v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return int(x)
|
||||||
|
case int:
|
||||||
|
return x
|
||||||
|
case int64:
|
||||||
|
return int(x)
|
||||||
|
default:
|
||||||
|
s := strings.TrimSpace(str(x))
|
||||||
|
if s == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if n, err := strconv.Atoi(s); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
|
return int(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapOwnTaxNotices QYGL7D9A 欠税公告 → { total, items }
|
||||||
|
func mapOwnTaxNotices(raw map[string]interface{}) map[string]interface{} {
|
||||||
|
out := map[string]interface{}{
|
||||||
|
"total": 0,
|
||||||
|
"items": []interface{}{},
|
||||||
|
}
|
||||||
|
if raw == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
items := sliceOrEmpty(raw["items"])
|
||||||
|
total := intFromAny(raw["total"])
|
||||||
|
if total == 0 {
|
||||||
|
total = len(items)
|
||||||
|
}
|
||||||
|
mapped := make([]interface{}, 0, len(items))
|
||||||
|
for _, it := range items {
|
||||||
|
row, _ := it.(map[string]interface{})
|
||||||
|
if row == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mapped = append(mapped, map[string]interface{}{
|
||||||
|
"taxIdNumber": str(row["taxIdNumber"]),
|
||||||
|
"taxpayerName": str(row["name"]),
|
||||||
|
"taxCategory": str(row["taxCategory"]),
|
||||||
|
"ownTaxBalance": str(row["ownTaxBalance"]),
|
||||||
|
"ownTaxAmount": str(row["ownTaxAmount"]),
|
||||||
|
"newOwnTaxBalance": str(row["newOwnTaxBalance"]),
|
||||||
|
"taxType": str(row["type"]),
|
||||||
|
"publishDate": str(row["publishDate"]),
|
||||||
|
"department": str(row["department"]),
|
||||||
|
"location": str(row["location"]),
|
||||||
|
"legalPersonName": str(row["legalpersonName"]),
|
||||||
|
"personIdNumber": str(row["personIdNumber"]),
|
||||||
|
"personIdName": str(row["personIdName"]),
|
||||||
|
"taxpayerType": str(row["taxpayerType"]),
|
||||||
|
"regType": str(row["regType"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
out["total"] = total
|
||||||
|
out["items"] = mapped
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapTaxViolations QYGL8848 税收违法 → { total, items }(字段驼峰化)
|
||||||
|
func mapTaxViolations(raw map[string]interface{}) map[string]interface{} {
|
||||||
|
out := map[string]interface{}{
|
||||||
|
"total": 0,
|
||||||
|
"items": []interface{}{},
|
||||||
|
}
|
||||||
|
if raw == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
items := sliceOrEmpty(raw["items"])
|
||||||
|
total := intFromAny(raw["total"])
|
||||||
|
if total == 0 {
|
||||||
|
total = len(items)
|
||||||
|
}
|
||||||
|
mapped := make([]interface{}, 0, len(items))
|
||||||
|
for _, it := range items {
|
||||||
|
row, _ := it.(map[string]interface{})
|
||||||
|
if row == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cm := convertReportKeysToCamel(row, true)
|
||||||
|
if mm, ok := cm.(map[string]interface{}); ok {
|
||||||
|
mapped = append(mapped, mm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out["total"] = total
|
||||||
|
out["items"] = mapped
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapAnnualReports QYGLDJ12 企业年报列表 → []年报对象(键名驼峰化,按 reportYear 降序)
|
||||||
|
func mapAnnualReports(raw map[string]interface{}) []interface{} {
|
||||||
|
rows := extractJSONArrayFromEnterpriseAPI(raw)
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []interface{}{}
|
||||||
|
}
|
||||||
|
out := make([]interface{}, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
m, ok := r.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v, ok := m["rpport_change_info"]; ok {
|
||||||
|
if _, has := m["report_change_info"]; !has {
|
||||||
|
m["report_change_info"] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cm := convertReportKeysToCamel(m, true)
|
||||||
|
mm, ok := cm.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, mm)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
ai, _ := out[i].(map[string]interface{})
|
||||||
|
bj, _ := out[j].(map[string]interface{})
|
||||||
|
return intFromAny(ai["reportYear"]) > intFromAny(bj["reportYear"])
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func mapFromBASIC(jiguang map[string]interface{}) map[string]interface{} {
|
func mapFromBASIC(jiguang map[string]interface{}) map[string]interface{} {
|
||||||
basic := make(map[string]interface{})
|
basic := make(map[string]interface{})
|
||||||
b, _ := jiguang["BASIC"].(map[string]interface{})
|
b, _ := jiguang["BASIC"].(map[string]interface{})
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package processors
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// QYGLReportPDFScheduler 企业全景报告 PDF 异步预生成调度器(可为 nil 表示禁用)
|
||||||
|
type QYGLReportPDFScheduler interface {
|
||||||
|
// ScheduleQYGLReportPDF 在报告数据就绪后异步生成 PDF 并写入缓存
|
||||||
|
ScheduleQYGLReportPDF(ctx context.Context, reportID string)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -27,6 +28,7 @@ type QYGLReportHandler struct {
|
|||||||
|
|
||||||
reportRepo api_repositories.ReportRepository
|
reportRepo api_repositories.ReportRepository
|
||||||
pdfCacheManager *pdf.PDFCacheManager
|
pdfCacheManager *pdf.PDFCacheManager
|
||||||
|
qyglPDFPregen *pdf.QYGLReportPDFPregen
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewQYGLReportHandler 创建企业报告页面处理器
|
// NewQYGLReportHandler 创建企业报告页面处理器
|
||||||
@@ -35,12 +37,14 @@ func NewQYGLReportHandler(
|
|||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
reportRepo api_repositories.ReportRepository,
|
reportRepo api_repositories.ReportRepository,
|
||||||
pdfCacheManager *pdf.PDFCacheManager,
|
pdfCacheManager *pdf.PDFCacheManager,
|
||||||
|
qyglPDFPregen *pdf.QYGLReportPDFPregen,
|
||||||
) *QYGLReportHandler {
|
) *QYGLReportHandler {
|
||||||
return &QYGLReportHandler{
|
return &QYGLReportHandler{
|
||||||
apiRequestService: apiRequestService,
|
apiRequestService: apiRequestService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
reportRepo: reportRepo,
|
reportRepo: reportRepo,
|
||||||
pdfCacheManager: pdfCacheManager,
|
pdfCacheManager: pdfCacheManager,
|
||||||
|
qyglPDFPregen: qyglPDFPregen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +145,49 @@ func (h *QYGLReportHandler) GetQYGLReportPageByID(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQYGLReportPDFByID 通过编号导出企业全景报告 PDF(基于 headless Chrome 渲染 HTML)
|
// 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)
|
||||||
|
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
|
// GET /reports/qygl/:id/pdf
|
||||||
func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
|
func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
@@ -150,7 +196,6 @@ func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选:从数据库查一次,用于生成更友好的文件名
|
|
||||||
var fileName = "企业全景报告.pdf"
|
var fileName = "企业全景报告.pdf"
|
||||||
if h.reportRepo != nil {
|
if h.reportRepo != nil {
|
||||||
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
|
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
|
||||||
@@ -160,46 +205,66 @@ func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 临时关闭企业全景报告 PDF 的读取缓存,强制每次都重新生成
|
var pdfBytes []byte
|
||||||
// 如果后续需要恢复缓存,可在此重新启用 pdfCacheManager.GetByReportID 的逻辑。
|
if h.pdfCacheManager != nil {
|
||||||
|
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
|
||||||
// 根据当前请求推断访问协议(支持通过反向代理的 X-Forwarded-Proto)
|
pdfBytes = b
|
||||||
scheme := "http"
|
}
|
||||||
if c.Request.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
} else if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
|
|
||||||
scheme = forwardedProto
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建用于 headless 浏览器访问的完整报告页面 URL
|
// 缓存未命中时:若正在预生成,短时等待(与前端轮询互补)
|
||||||
reportURL := fmt.Sprintf("%s://%s/reports/qygl/%s", scheme, c.Request.Host, id)
|
if len(pdfBytes) == 0 && h.qyglPDFPregen != nil && h.qyglPDFPregen.Enabled() && h.pdfCacheManager != nil {
|
||||||
|
deadline := time.Now().Add(90 * time.Second)
|
||||||
h.logger.Info("开始生成企业全景报告 PDF(headless Chrome)",
|
for time.Now().Before(deadline) {
|
||||||
zap.String("report_id", id),
|
st, _ := h.qyglPDFPregen.Status(id)
|
||||||
zap.String("url", reportURL),
|
if st == pdf.QYGLReportPDFStatusFailed {
|
||||||
)
|
break
|
||||||
|
}
|
||||||
pdfGen := pdf.NewHTMLPDFGenerator(h.logger)
|
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
|
||||||
pdfBytes, err := pdfGen.GenerateFromURL(c.Request.Context(), reportURL)
|
pdfBytes = b
|
||||||
if err != nil {
|
break
|
||||||
h.logger.Error("生成企业全景报告 PDF 失败", zap.String("report_id", id), zap.Error(err))
|
}
|
||||||
c.String(http.StatusInternalServerError, "生成企业报告 PDF 失败,请稍后重试")
|
time.Sleep(400 * time.Millisecond)
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(pdfBytes) == 0 {
|
if len(pdfBytes) == 0 {
|
||||||
h.logger.Error("生成的企业全景报告 PDF 为空", zap.String("report_id", id))
|
scheme := "http"
|
||||||
c.String(http.StatusInternalServerError, "生成的企业报告 PDF 为空,请稍后重试")
|
if c.Request.TLS != nil {
|
||||||
return
|
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)
|
||||||
|
|
||||||
// 临时关闭企业全景报告 PDF 的写入缓存,只返回最新生成的 PDF。
|
h.logger.Info("现场生成企业全景报告 PDF(headless Chrome)",
|
||||||
// 若后续需要重新启用缓存,可恢复对 pdfCacheManager.SetByReportID 的调用。
|
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)
|
encodedFileName := url.QueryEscape(fileName)
|
||||||
c.Header("Content-Type", "application/pdf")
|
c.Header("Content-Type", "application/pdf")
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", encodedFileName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", encodedFileName))
|
||||||
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ func (r *QYGLReportRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
// 企业全景报告页面(通过编号查看)
|
// 企业全景报告页面(通过编号查看)
|
||||||
engine.GET("/reports/qygl/:id", r.handler.GetQYGLReportPageByID)
|
engine.GET("/reports/qygl/:id", r.handler.GetQYGLReportPageByID)
|
||||||
|
|
||||||
|
// 企业全景报告 PDF 预生成状态(通过编号,供前端轮询)
|
||||||
|
engine.GET("/reports/qygl/:id/pdf/status", r.handler.GetQYGLReportPDFStatusByID)
|
||||||
|
|
||||||
// 企业全景报告 PDF 导出(通过编号)
|
// 企业全景报告 PDF 导出(通过编号)
|
||||||
engine.GET("/reports/qygl/:id/pdf", r.handler.GetQYGLReportPDFByID)
|
engine.GET("/reports/qygl/:id/pdf", r.handler.GetQYGLReportPDFByID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jung-kurt/gofpdf/v2"
|
"github.com/jung-kurt/gofpdf/v2"
|
||||||
@@ -106,15 +107,15 @@ func GenerateQYGLReportPDF(_ context.Context, logger *zap.Logger, report map[str
|
|||||||
{"riskOverview", "风险情况(综合分析)"},
|
{"riskOverview", "风险情况(综合分析)"},
|
||||||
{"basic", "一、主体概览(企业基础信息)"},
|
{"basic", "一、主体概览(企业基础信息)"},
|
||||||
{"branches", "二、分支机构"},
|
{"branches", "二、分支机构"},
|
||||||
{"shareholding", "三、股权与控制"},
|
{"shareholding", "三、股权与控制(持股结构;认缴与实缴公示见第十六节)"},
|
||||||
{"controller", "四、实际控制人"},
|
{"controller", "四、实际控制人"},
|
||||||
{"beneficiaries", "五、最终受益人"},
|
{"beneficiaries", "五、最终受益人"},
|
||||||
{"investments", "六、对外投资"},
|
{"investments", "六、对外投资"},
|
||||||
{"guarantees", "七、对外担保"},
|
{"guarantees", "七、对外担保(全量年报披露摘要;公示年报详版见第十六节)"},
|
||||||
{"management", "八、人员与组织"},
|
{"management", "八、人员与组织(高管与任职;年报从业与社保见第十六节)"},
|
||||||
{"assets", "九、资产与经营(年报)"},
|
{"assets", "九、资产与经营(全量年报财务摘要;公示年报详版见第十六节)"},
|
||||||
{"licenses", "十、行政许可与资质"},
|
{"licenses", "十、行政许可与资质"},
|
||||||
{"activities", "十一、经营活动"},
|
{"activities", "十一、经营活动(招投标;网站或网店公示见第十六节)"},
|
||||||
{"inspections", "十二、抽查检查"},
|
{"inspections", "十二、抽查检查"},
|
||||||
{"risks", "十三、风险与合规"},
|
{"risks", "十三、风险与合规"},
|
||||||
{"timeline", "十四、发展时间线"},
|
{"timeline", "十四、发展时间线"},
|
||||||
@@ -246,8 +247,8 @@ func writeKeyValue(pdf *gofpdf.Fpdf, fontName, label, value string) {
|
|||||||
// 使用黑色描边,左侧标签单元格填充标题蓝色背景
|
// 使用黑色描边,左侧标签单元格填充标题蓝色背景
|
||||||
pdf.SetDrawColor(0, 0, 0)
|
pdf.SetDrawColor(0, 0, 0)
|
||||||
pdf.SetFillColor(91, 155, 213)
|
pdf.SetFillColor(91, 155, 213)
|
||||||
pdf.Rect(x, y, labelW, rowH, "FD") // 标签:填充+描边
|
pdf.Rect(x, y, labelW, rowH, "FD") // 标签:填充+描边
|
||||||
pdf.Rect(x+labelW, y, valueW, rowH, "D") // 值:仅描边
|
pdf.Rect(x+labelW, y, valueW, rowH, "D") // 值:仅描边
|
||||||
|
|
||||||
// 写入标签单元格
|
// 写入标签单元格
|
||||||
pdf.SetXY(x, y)
|
pdf.SetXY(x, y)
|
||||||
@@ -891,7 +892,9 @@ func renderPDFManagement(pdf *gofpdf.Fpdf, fontName, title string, v map[string]
|
|||||||
// 从业与社保
|
// 从业与社保
|
||||||
employeeCount := getString(v, "employeeCount")
|
employeeCount := getString(v, "employeeCount")
|
||||||
femaleEmployeeCount := getString(v, "femaleEmployeeCount")
|
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.Ln(2)
|
||||||
pdf.SetFont(fontName, "B", 12)
|
pdf.SetFont(fontName, "B", 12)
|
||||||
pdf.MultiCell(0, 6, "从业与社保", "", "L", false)
|
pdf.MultiCell(0, 6, "从业与社保", "", "L", false)
|
||||||
@@ -902,6 +905,35 @@ func renderPDFManagement(pdf *gofpdf.Fpdf, fontName, title string, v map[string]
|
|||||||
if femaleEmployeeCount != "" {
|
if femaleEmployeeCount != "" {
|
||||||
writeKeyValue(pdf, fontName, "女性从业人数", 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
|
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 != ""
|
||||||
|
}
|
||||||
4
resources/dev-report/.gitignore
vendored
Normal file
4
resources/dev-report/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# 本地调试数据,勿提交敏感企业信息
|
||||||
|
fixture.json
|
||||||
|
built.json
|
||||||
|
*.local.json
|
||||||
330
resources/dev-report/BUILT_REPORT_FIELDS.md
Normal file
330
resources/dev-report/BUILT_REPORT_FIELDS.md
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
# 企业全景报告 · Build 后 JSON 字段说明
|
||||||
|
|
||||||
|
本文描述 **`BuildReportFromRawSources` / `buildReport` 产出的报告对象**(与 `cmd/qygl_report_build`、QYGLJ1U9 聚合逻辑一致)。
|
||||||
|
数值在 JSON 中可能为 **number**(`json.Unmarshal` 后常见 `float64`)或 **string**,以实际序列化结果为准。
|
||||||
|
|
||||||
|
## 数据来源概览
|
||||||
|
|
||||||
|
| 报告一级字段 | 主要来源(原始接口 / 表) |
|
||||||
|
|-------------|---------------------------|
|
||||||
|
| `basic`、`branches`、`guarantees`、`management`、`assets`、`activities`(部分)等 | 企业全量信息核验 V2(QYGLUY3S,`jiguang`) |
|
||||||
|
| 存在 `annualReports` 且非空时 | 上述块中与公示年报重复的 `YEARREPORT*` 表会在构建前剔除,改由年报接口数据支撑展示逻辑 |
|
||||||
|
| `shareholding`、`controller`、`beneficiaries`、`investments`(`list`) | 优先股权全景(QYGLJ0Q1,`equity`),否则回退全量 |
|
||||||
|
| `risks` 中司法部分 | 司法涉诉(QYGL5S1I,`judicial`)等 |
|
||||||
|
| `annualReports` | 企业年报(QYGLDJ12,`annualRaw`),键名统一转小驼峰 |
|
||||||
|
| `taxViolations` | 税收违法(QYGL8848),`items[]` 为驼峰化对象 |
|
||||||
|
| `ownTaxNotices` | 欠税公告(QYGL7D9A),`items[]` 为固定映射字段 |
|
||||||
|
|
||||||
|
## 线上接口额外字段
|
||||||
|
|
||||||
|
经 **QYGLJ1U9** 返回时,在 Build 结果上还会增加(不参与 `BuildReportFromRawSources` 纯构建):
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `reportId` | 报告编号,用于查看页 |
|
||||||
|
| `reportUrl` | 报告查看链接 |
|
||||||
|
|
||||||
|
## 根对象字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `reportTime` | string | 报告生成时间,`2006-01-02 15:04:05` |
|
||||||
|
| `entName` | string | 企业名称(与 `basic.entName` 一致,便于顶层取用) |
|
||||||
|
| `creditCode` | string | 统一社会信用代码 |
|
||||||
|
| `basic` | object | 主体登记信息,见下节 |
|
||||||
|
| `basicList` | array | **可选**。全量 `BASICLIST` 原始数组,有数据时存在 |
|
||||||
|
| `branches` | array | 分支机构列表 |
|
||||||
|
| `shareholding` | object | 股权与控制 |
|
||||||
|
| `controller` | object \| null | 实际控制人 |
|
||||||
|
| `beneficiaries` | array | 最终受益人 |
|
||||||
|
| `investments` | object | 对外投资汇总 |
|
||||||
|
| `guarantees` | array | 对外担保(年报披露摘要) |
|
||||||
|
| `management` | object | 人员与组织、从业与社保 |
|
||||||
|
| `assets` | object | 资产与经营(按年度摘要) |
|
||||||
|
| `licenses` | object | 行政许可、变更、知产出质等 |
|
||||||
|
| `activities` | object | 招投标、网站网店等 |
|
||||||
|
| `inspections` | array | 抽查检查 |
|
||||||
|
| `risks` | object | 风险与合规(含司法、处罚、抵押等) |
|
||||||
|
| `timeline` | array | 工商变更时间线 |
|
||||||
|
| `listed` | object \| null | 上市信息,无则 `null` |
|
||||||
|
| `riskOverview` | object | 综合风险评分与标签(由报告聚合结果计算) |
|
||||||
|
| `annualReports` | array | 企业年报(公示)列表,按 `reportYear` 降序 |
|
||||||
|
| `taxViolations` | object | `{ total, items[] }` 税收违法 |
|
||||||
|
| `ownTaxNotices` | object | `{ total, items[] }` 欠税公告 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `basic`(主体概览)
|
||||||
|
|
||||||
|
由全量 `BASIC` 映射,常见字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `entName`、`creditCode`、`regNo`、`orgCode` | 名称、统一码、注册号、组织机构代码 |
|
||||||
|
| `entType`、`entTypeCode`、`entityTypeCode` | 企业类型及编码 |
|
||||||
|
| `establishDate` | 成立日期 |
|
||||||
|
| `registeredCapital`、`regCapCurrency`、`regCapCurrencyCode` | 注册资本及币种 |
|
||||||
|
| `regOrg`、`regOrgCode`、`regProvince`、`provinceCode`、`regCity`、`regCityCode`、`regDistrict`、`districtCode` | 登记机关及行政区划 |
|
||||||
|
| `address`、`postalCode` | 住所、邮编 |
|
||||||
|
| `legalRepresentative` | 法定代表人 |
|
||||||
|
| `compositionForm` | 组成形式 |
|
||||||
|
| `approvedBusinessItem` | 许可经营项目 |
|
||||||
|
| `status`、`statusCode` | 经营状态(中文 / 代码) |
|
||||||
|
| `operationPeriodFrom`、`operationPeriodTo` | 营业期限 |
|
||||||
|
| `approveDate`、`cancelDate`、`revokeDate`、`cancelReason`、`revokeReason` | 核准、注销、吊销等 |
|
||||||
|
| `businessScope` | 经营范围 |
|
||||||
|
| `lastAnnuReportYear` | 最后年报年度 |
|
||||||
|
| `oldNames` | string[],曾用名;无则为 `[]` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `branches[]`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `name`、`regNo`、`creditCode`、`regOrg` | 分支机构名称、注册号、统一码、登记机关 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `shareholding`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `shareholders` | array | 股东及出资明细 |
|
||||||
|
| `shareholderCount` | int | 股东人数 |
|
||||||
|
| `registeredCapital`、`currency` | number / string | 注册资本与币种(来自 BASIC) |
|
||||||
|
| `topHolderName`、`topHolderPercent` | string / number | 第一大股东及持股比例 |
|
||||||
|
| `top5TotalPercent` | number | 前五大股东持股比例合计 |
|
||||||
|
| `equityChanges` | array | 股权变更记录 |
|
||||||
|
| `equityPledges` | array | 股权出质 |
|
||||||
|
| `paidInDetails` | array | 实缴出资明细(年报表) |
|
||||||
|
| `subscribedCapitalDetails` | array | 认缴出资明细(年报表) |
|
||||||
|
| `hasEquityPledges` | bool | 是否存在股权出质 |
|
||||||
|
|
||||||
|
### `shareholders[]` 常见子字段
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `name`、`type`、`typeCode` | 股东名称、类型 |
|
||||||
|
| `ownershipPercent` | 持股比例 |
|
||||||
|
| `subscribedAmount`、`paidAmount` | 认缴额、实缴额 |
|
||||||
|
| `subscribedCurrency`、`subscribedCurrencyCode`、`paidCurrency` | 币种 |
|
||||||
|
| `subscribedDate`、`paidDate` | 认缴/实缴日期 |
|
||||||
|
| `subscribedMethod`、`subscribedMethodCode`、`paidMethod` | 出资方式 |
|
||||||
|
| `creditCode`、`regNo` | 股东证件侧代码/注册号 |
|
||||||
|
| `isHistory` | 是否历史股东 |
|
||||||
|
| `source` | 数据来源说明,如「股权全景」 |
|
||||||
|
|
||||||
|
### `equityChanges[]`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `changeDate`、`shareholderName`、`percentBefore`、`percentAfter`、`source` | 变更日期、股东、变更前后比例、来源 |
|
||||||
|
|
||||||
|
### `equityPledges[]` / `paidInDetails[]` / `subscribedCapitalDetails[]`
|
||||||
|
|
||||||
|
结构与 `qyglj1u9_processor_build.go` 中 `mapEquityPledges`、`mapPaidInDetails`、`mapSubscribedCapitalDetails` 一致(含 `yearReportId`、`investor`、金额日期方式等字段)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `controller`(可为 `null`)
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `id`、`name`、`type`、`percent` | 实控人标识、名称、类型、比例 |
|
||||||
|
| `path` | object \| null,`nodes` / `links`,节点上可能含 `entityId`(由 `uid` 复制) |
|
||||||
|
| `reason`、`source` | 说明、数据来源 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `beneficiaries[]`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `id`、`name`、`type`、`typeCode`、`percent`、`path`、`reason`、`source` | 受益人标识、名称、类型、比例、路径、理由、来源 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `investments`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `totalCount`、`totalAmount` | 对外投资户数、认缴合计(全量路径下) |
|
||||||
|
| `list` | 对外投资企业列表 |
|
||||||
|
| `legalRepresentativeInvestments` | 法定代表人对外投资(FRINV) |
|
||||||
|
|
||||||
|
### `list[]` 常见子字段
|
||||||
|
|
||||||
|
`entName`、`creditCode`、`regNo`、`entType`、`regCap`、`regCapCurrency`、`entStatus`、`regOrg`、`establishDate`、`investAmount`、`investCurrency`、`investPercent`、`investMethod`、`isListed`、`source`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `guarantees[]`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `yearReportId`、`mortgagor`、`creditor`、`principalAmount`、`principalKind`、`guaranteeType`、`periodFrom`、`periodTo`、`guaranteePeriod` | 年报担保摘要 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `management`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `executives` | array,`name`、`position` |
|
||||||
|
| `legalRepresentativeOtherPositions` | array,法人对外任职:`entName`、`position`、`name`、`regNo`、`creditCode`、`entStatus` |
|
||||||
|
| `employeeCount`、`femaleEmployeeCount` | 从业人数、女性从业人数(来自最新年报 BASIC 摘要) |
|
||||||
|
| `socialSecurity` | object | 社会保险分项参保人数等,**一般为全量 `YEARREPORTSOCSEC` 首条原始对象**(键名多为大写,与数据源一致);无数据时可能为空对象 `{}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `assets`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `years` | array,按年度的资产经营摘要 |
|
||||||
|
|
||||||
|
### `assets.years[]`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `year`、`reportDate` | 年度、关联年报标识(实现上取自 `ANCHEID`) |
|
||||||
|
| `assetTotal`、`revenueTotal`、`mainBusinessRevenue`、`taxTotal`、`equityTotal`、`profitTotal`、`netProfit`、`liabilityTotal` | 资产、收入、税费、权益、利润、负债等 |
|
||||||
|
| `businessStatus`、`mainBusiness` | 经营状态、主营业务 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `licenses`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `permits` | array,`name`、`valFrom`、`valTo`、`licAnth`、`licItem` |
|
||||||
|
| `permitChanges` | array,`changeDate`、`detailBefore`、`detailAfter`、`changeType` |
|
||||||
|
| `ipPledges` | array,原始知产出质结构(与全量一致) |
|
||||||
|
| `otherLicenses` | array,当前固定为空数组 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `activities`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `bids` | array,招投标原始项 |
|
||||||
|
| `websites` | array,网站或网店(年报表 `YEARREPORTWEBSITEINFO`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `inspections[]`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `dataType`、`regOrg`、`inspectDate`、`result` | 抽查类型、机关、日期、结果 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `risks`
|
||||||
|
|
||||||
|
### 标量与汇总
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `riskLevel`、`riskScore` | 内部粗算风险等级/分数(与 `riskOverview` 计算方式不同,以 `riskOverview` 为准做展示) |
|
||||||
|
| `hasCourtJudgments`、`hasJudicialAssists`、`hasDishonestDebtors`、`hasLimitHighDebtors` | 布尔标志 |
|
||||||
|
| `hasAdminPenalty`、`hasException`、`hasSeriousIllegal` | 行政处罚、经营异常、严重违法 |
|
||||||
|
| `hasTaxOwing`、`hasSeriousTaxIllegal`、`hasMortgage`、`hasEquityPledges` | 欠税、重大税收违法、动产抵押、股权出质 |
|
||||||
|
| `hasQuickCancel` | 简易注销公告 |
|
||||||
|
| `dishonestDebtorCount`、`limitHighDebtorCount` | 失信、限高条数 |
|
||||||
|
|
||||||
|
### 主要数组 / 对象
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `dishonestDebtors` | 失信被执行人(映射后子字段含 `id`、`obligation`、`caseNo`、`execCourt` 等) |
|
||||||
|
| `limitHighDebtors` | 限高名单(原始结构数组) |
|
||||||
|
| `litigation` | 涉诉汇总,`administrative` / `civil` / … 各类下为 `{ count, cases[] }`,`cases[]` 含 `caseNo`、`court`、`filingDate` 等 |
|
||||||
|
| `adminPenalties`、`adminPenaltyUpdates` | 行政处罚及变更 |
|
||||||
|
| `exceptions` | 经营异常原始列表 |
|
||||||
|
| `seriousIllegals` | 严重违法原始列表 |
|
||||||
|
| `mortgages` | 动产抵押(含子数组 `mortgagees`、`collaterals` 等) |
|
||||||
|
| `quickCancel`、`liquidation` | 简易注销、清算信息,无则 `null` |
|
||||||
|
| `taxRecords` | `{ taxLevelAYears[], seriousTaxIllegal[], taxOwings[] }`(全量税务相关原始切片) |
|
||||||
|
| `courtJudgments`、`judicialAssists` | 裁判文书、司法协助(原始结构) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `timeline[]`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `date`、`type`、`title`、`detailBefore`、`detailAfter`、`source` | 变更日期、事项类型、标题、变更前后、来源 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `listed`(可为 `null`)
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `isListed` | bool |
|
||||||
|
| `company` | object,上市主体工商摘要片段 |
|
||||||
|
| `stock`、`topShareholders`、`listedManagers` | 股票信息、十大股东、高管(多为原始结构) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `riskOverview`(综合风险,供页眉/总览)
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `riskLevel` | string,`低` / `中` / `高` |
|
||||||
|
| `riskScore` | int,0–100 |
|
||||||
|
| `tags` | string[],命中风险点的简短标签 |
|
||||||
|
| `items` | array,`{ name, hit }`,各维度是否命中 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `annualReports[]`
|
||||||
|
|
||||||
|
- 每条为 **QYGLDJ12 单条年报**经 **`convertReportKeysToCamel` 递归转小驼峰** 后的对象。
|
||||||
|
- 除汇总字段外,常见还包含(名称以驼峰为准):网站 `reportWebsiteInfo`、股东 `reportShareholderInfo`、对外投资 `reportInvestInfo` 与 `investInfo`、社保 `reportSocialSecurityInfo`、担保 `reportGuaranteeInfo`、股权变更 `reportEquityChangeInfo`、变更 `reportChangeInfo` 等,**具体键集合以接口返回为准**。
|
||||||
|
- 页面汇总网格展示的字段集合与 `qyglj1u9_processor_build.go` 中 `mapAnnualReports` 之后、`qiye.html` 内 `sumKeys` 对齐(`investInfo` 仅在详情列表展示,不在顶层网格重复)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `taxViolations`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "total": 0, "items": [] }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `total`:条数,缺省时与 `items.length` 一致。
|
||||||
|
- `items[]`:**QYGL8848 每条记录键名转驼峰后的对象**。展示层常用字段包括(以实际数据为准):`entityName`、`taxpayerCode`、`caseType`、`entityCategory`、`illegalFact`、`punishBasis`、`illegalStartDate`、`illegalEndDate`、`illegalTime`、`publishDepartment`、`checkDepartment`、`belongDepartment`、`police`、`agencyPersonInfo` 等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `ownTaxNotices`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "total": 0, "items": [] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `items[]` 固定映射字段
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `taxIdNumber` | 纳税人识别号 |
|
||||||
|
| `taxpayerName` | 纳税人名称 |
|
||||||
|
| `taxCategory` | 欠税税种 |
|
||||||
|
| `ownTaxBalance`、`ownTaxAmount`、`newOwnTaxBalance` | 欠税余额、欠税金额、当前新发生欠税余额 |
|
||||||
|
| `taxType` | 税务类型(来自原始 `type`) |
|
||||||
|
| `publishDate` | 发布日期 |
|
||||||
|
| `department` | 主管税务机关 |
|
||||||
|
| `location` | 地点 |
|
||||||
|
| `legalPersonName` | 法定代表人 |
|
||||||
|
| `personIdNumber`、`personIdName` | 证件号码及证件名称字段名 |
|
||||||
|
| `taxpayerType`、`regType` | 纳税人类型、登记类型 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 维护说明
|
||||||
|
|
||||||
|
- **字段增删**以 `internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go` 为准;年报、税收违法条目的细键若接口升级,可能随 `convertReportKeysToCamel` 自动变为新驼峰键。
|
||||||
|
- 前端展示标签中文名见 `resources/qiye.html` 中 `keyLabels`(与 Build 字段名对应)。
|
||||||
57879
resources/dev-report/raw.bundle.template.json
Normal file
57879
resources/dev-report/raw.bundle.template.json
Normal file
File diff suppressed because one or more lines are too long
1896
resources/qiye.html
1896
resources/qiye.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user