This commit is contained in:
2026-05-01 11:28:46 +08:00
parent 98a78c3170
commit 40f4fab1d4
4 changed files with 280 additions and 9 deletions

View File

@@ -23,6 +23,7 @@ type rawBundle struct {
AnnualReport map[string]interface{} `json:"annualReport"`
TaxViolation map[string]interface{} `json:"taxViolation"`
TaxArrears map[string]interface{} `json:"taxArrears"`
CustomsCredit map[string]interface{} `json:"customsCredit"`
}
func main() {
@@ -57,6 +58,7 @@ func main() {
b.AnnualReport,
b.TaxViolation,
b.TaxArrears,
b.CustomsCredit,
)
out, err := json.MarshalIndent(report, "", " ")

View File

@@ -18,7 +18,7 @@ import (
)
// ProcessQYGLJ1U9Request 企业全景报告处理器:并发调用企业全量(QYGLUY3S)、股权全景(QYGLJ0Q1)、司法涉诉(QYGL5S1I)、
// 企业年报(QYGLDJ12)、税收违法(QYGL8848)、欠税公告(QYGL7D9A)。
// 企业年报(QYGLDJ12)、税收违法(QYGL8848)、欠税公告(QYGL7D9A)、进出口信用(QYGLDJ33)
// 单路失败、查无、解析失败时该路按空数据处理并继续合并;仅当合并后的报告仍无任何可展示的企业要素时返回查询为空。
func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
// 复用 QYGLUY3S 的入参结构:企业名称/注册号/统一社会信用代码
@@ -36,7 +36,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
data map[string]interface{}
err error
}
resultsCh := make(chan apiResult, 6)
resultsCh := make(chan apiResult, 7)
var wg sync.WaitGroup
call := func(key string, req interface{}, fn func(context.Context, []byte, *processors.ProcessorDependencies) ([]byte, error)) {
@@ -56,7 +56,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
var m map[string]interface{}
var uerr error
// 根节点可能是数组或非对象,与欠税接口一致用宽松解析
if key == "taxArrears" || key == "annualReport" || key == "taxViolation" {
if key == "taxArrears" || key == "annualReport" || key == "taxViolation" || key == "customsCredit" {
m, uerr = unmarshalToReportMap(resp)
} else {
uerr = json.Unmarshal(resp, &m)
@@ -105,6 +105,12 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
"page_num": 1,
}, ProcessQYGL7D9ARequest)
// 企业进出口信用核查QYGLDJ33
call("customsCredit", map[string]interface{}{
"ent_name": p.EntName,
"ent_code": p.EntCode,
}, ProcessQYGLDJ33Request)
wg.Wait()
close(resultsCh)
@@ -114,6 +120,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
annualReport := map[string]interface{}{}
taxViolation := map[string]interface{}{}
taxArrears := map[string]interface{}{}
customsCredit := map[string]interface{}{}
for r := range resultsCh {
if r.err != nil || r.data == nil {
continue
@@ -131,11 +138,13 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
taxViolation = r.data
case "taxArrears":
taxArrears = r.data
case "customsCredit":
customsCredit = r.data
}
}
// 复用构建逻辑生成企业报告结构(含年报 / 税收违法 / 欠税公告的转化结果)
report := buildReport(jiguang, judicial, equity, annualReport, taxViolation, taxArrears)
// 复用构建逻辑生成企业报告结构(含年报 / 税收违法 / 欠税公告 / 进出口信用的转化结果)
report := buildReport(jiguang, judicial, equity, annualReport, taxViolation, taxArrears, customsCredit)
if !qyglJ1U9ReportHasSubstantiveData(report) {
return nil, errors.Join(processors.ErrNotFound, errors.New("未查询到可用于生成报告的企业数据"))
}

View File

@@ -1,6 +1,7 @@
package qygl
import (
"encoding/json"
"fmt"
"sort"
"strconv"
@@ -8,7 +9,7 @@ import (
"time"
)
func buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw map[string]interface{}) map[string]interface{} {
func buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw, customsCreditRaw map[string]interface{}) map[string]interface{} {
report := make(map[string]interface{})
report["reportTime"] = time.Now().Format("2006-01-02 15:04:05")
@@ -54,10 +55,11 @@ func buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrea
report["basicList"] = bl
}
// QYGLDJ12 企业年报 / QYGL8848 税收违法 / QYGL7D9A 欠税公告(转化后的前端友好结构)
// QYGLDJ12 企业年报 / QYGL8848 税收违法 / QYGL7D9A 欠税公告 / QYGLDJ33 进出口信用(转化后的前端友好结构)
report["annualReports"] = annualReports
report["taxViolations"] = mapTaxViolations(taxViolationRaw)
report["ownTaxNotices"] = mapOwnTaxNotices(taxArrearsRaw)
report["customsCredit"] = mapCustomsCredit(customsCreditRaw)
applyQYGLJ1U9ReportFieldDefaults(report)
return report
}
@@ -105,6 +107,7 @@ func applyQYGLJ1U9ReportFieldDefaults(report map[string]interface{}) {
report["taxViolations"] = mergeTaxViolationsDefaults(report["taxViolations"])
report["ownTaxNotices"] = mergeOwnTaxNoticesDefaults(report["ownTaxNotices"])
report["customsCredit"] = mergeCustomsCreditDefaults(report["customsCredit"])
if ro, ok := report["riskOverview"].(map[string]interface{}); ok {
report["riskOverview"] = mergeRiskOverviewDefaults(ro)
@@ -651,6 +654,14 @@ func qyglJ1U9ReportHasSubstantiveData(report map[string]interface{}) bool {
return true
}
}
if cc, ok := report["customsCredit"].(map[string]interface{}); ok {
if intFromAny(cc["total"]) > 0 {
return true
}
if items, ok := cc["items"].([]interface{}); ok && len(items) > 0 {
return true
}
}
if br, ok := report["branches"].([]interface{}); ok && len(br) > 0 {
return true
}
@@ -733,7 +744,7 @@ func jiguangWithoutYearReportTables(jiguang map[string]interface{}) map[string]i
// BuildReportFromRawSources 供开发/测试:将各处理器原始 JSON与 QYGLJ1U9 并发结果形态一致)走与线上一致的 buildReport 转化。
// 任一路传入 nil 时按空 map 处理。
func BuildReportFromRawSources(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw map[string]interface{}) map[string]interface{} {
func BuildReportFromRawSources(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw, customsCreditRaw map[string]interface{}) map[string]interface{} {
if jiguang == nil {
jiguang = map[string]interface{}{}
}
@@ -752,7 +763,10 @@ func BuildReportFromRawSources(jiguang, judicial, equity, annualRaw, taxViolatio
if taxArrearsRaw == nil {
taxArrearsRaw = map[string]interface{}{}
}
return buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw)
if customsCreditRaw == nil {
customsCreditRaw = map[string]interface{}{}
}
return buildReport(jiguang, judicial, equity, annualRaw, taxViolationRaw, taxArrearsRaw, customsCreditRaw)
}
// extractJSONArrayFromEnterpriseAPI 从数据宝/天眼查类响应中提取数组主体data/list/result 等)。
@@ -2050,6 +2064,32 @@ func mapRiskOverview(report map[string]interface{}, risks map[string]interface{}
tags = append(tags, "存在股权出质")
}
// 进出口信用风险:基于 customsCredit 汇总
var customsCredit map[string]interface{}
if cc, ok := report["customsCredit"].(map[string]interface{}); ok {
customsCredit = cc
}
hasCustomsAbnormal := false
hasCustomsPunish := false
hasCustomsCancel := false
if customsCredit != nil {
hasCustomsAbnormal = getBool(customsCredit, "hasAbnormal")
hasCustomsPunish = getBool(customsCredit, "hasPunish")
hasCustomsCancel = getBool(customsCredit, "isCancelled")
}
addItem("海关信用异常", hasCustomsAbnormal)
if hasCustomsAbnormal {
tags = append(tags, "存在海关信用异常")
}
addItem("海关行政处罚", hasCustomsPunish)
if hasCustomsPunish {
tags = append(tags, "存在海关行政处罚")
}
addItem("海关注销或状态异常", hasCustomsCancel)
if hasCustomsCancel {
tags = append(tags, "海关注销或状态异常")
}
// 股权结构风险:基于 shareholding 汇总
var shareholding map[string]interface{}
if s, ok := report["shareholding"].(map[string]interface{}); ok {
@@ -2085,6 +2125,11 @@ func mapRiskOverview(report map[string]interface{}, risks map[string]interface{}
penalize(getBool(risks, "hasMortgage"), 5)
penalize(getBool(risks, "hasEquityPledges"), 5)
// 进出口信用风险扣分
penalize(hasCustomsPunish, 15)
penalize(hasCustomsAbnormal, 10)
penalize(hasCustomsCancel, 10)
if shareholding != nil {
if n, _ := shareholding["shareholderCount"].(int); n > 0 && n <= 3 {
penalize(true, 5)
@@ -2261,3 +2306,128 @@ func num(v interface{}) float64 {
return 0
}
}
// mapCustomsCredit QYGLDJ33 企业进出口信用核查 → { total, items, hasAbnormal, hasPunish, isCancelled }
// 字段保留接口原始驼峰命名,补充风险标志布尔值供 riskOverview 使用。
func mapCustomsCredit(raw map[string]interface{}) map[string]interface{} {
out := map[string]interface{}{
"total": 0,
"items": []interface{}{},
"hasAbnormal": false,
"hasPunish": false,
"isCancelled": false,
}
if raw == nil {
return out
}
items := sliceOrEmpty(raw["items"])
total := intFromAny(raw["total"])
if total == 0 {
total = len(items)
}
hasAbnormal := false
hasPunish := false
isCancelled := false
mapped := make([]interface{}, 0, len(items))
for _, it := range items {
row, _ := it.(map[string]interface{})
if row == nil {
continue
}
abnormalInfo := str(row["abnormalInfo"])
punishInfo := normalizePunishInfo(row["punishInfo"])
cancelFlag := str(row["customsCancelFlag"])
if abnormalInfo != "" && abnormalInfo != "无信用信息异常情况" {
hasAbnormal = true
}
if punishInfo != "" && punishInfo != "[]" && punishInfo != "无" {
hasPunish = true
}
if cancelFlag != "" && cancelFlag != "正常" {
isCancelled = true
}
mapped = append(mapped, map[string]interface{}{
"entityName": str(row["entityName"]),
"entityNameEn": str(row["entityNameEn"]),
"customsRegisterCode": str(row["customsRegisterCode"]),
"customsRegisterDate": str(row["customsRegisterDate"]),
"reportingAddress": str(row["reportingAddress"]),
"reportingAddressEn": str(row["reportingAddressEn"]),
"customsName": str(row["customsName"]),
"customsBusinessType": str(row["customsBusinessType"]),
"importExportEntityCode": str(row["importExportEntityCode"]),
"district": str(row["district"]),
"economicDistrict": str(row["economicDistrict"]),
"specialArea": str(row["specialArea"]),
"industryType": str(row["industryType"]),
"tradeType": str(row["tradeType"]),
"validDate": str(row["validDate"]),
"firstRecordDate": str(row["firstRecordDate"]),
"lastRecordDate": str(row["lastRecordDate"]),
"customsCancelFlag": cancelFlag,
"ifAnnualReport": str(row["ifAnnualReport"]),
"abnormalInfo": abnormalInfo,
"punishInfo": punishInfo,
"customsLevel": str(row["customsLevel"]),
"creditLevel": str(row["creditLevel"]),
})
}
out["total"] = total
out["items"] = mapped
out["hasAbnormal"] = hasAbnormal
out["hasPunish"] = hasPunish
out["isCancelled"] = isCancelled
return out
}
// mergeCustomsCreditDefaults 补全进出口信用字段默认值。
func mergeCustomsCreditDefaults(v interface{}) map[string]interface{} {
out, _ := v.(map[string]interface{})
if out == nil {
out = map[string]interface{}{}
}
if _, ok := out["total"]; !ok {
out["total"] = 0
}
if _, ok := out["items"]; !ok {
out["items"] = []interface{}{}
} else {
out["items"] = ensureSlice(out["items"])
}
if _, ok := out["hasAbnormal"]; !ok {
out["hasAbnormal"] = false
}
if _, ok := out["hasPunish"]; !ok {
out["hasPunish"] = false
}
if _, ok := out["isCancelled"]; !ok {
out["isCancelled"] = false
}
return out
}
// normalizePunishInfo 将 punishInfo 统一为 JSON 字符串。
// RecursiveParse 可能已将嵌套的 JSON 数组字符串解析为 []interface{}
// 此处将其序列化回 JSON 字符串,确保前端能正确 JSON.parse。
func normalizePunishInfo(v interface{}) string {
if v == nil {
return ""
}
switch p := v.(type) {
case string:
return p
case []interface{}:
b, err := json.Marshal(p)
if err != nil {
return fmt.Sprint(v)
}
return string(b)
default:
return fmt.Sprint(v)
}
}

View File

@@ -1094,6 +1094,7 @@
"annualReports",
"taxViolations",
"ownTaxNotices",
"customsCredit",
];
var sectionTitles = {
riskOverview: "风险情况(综合分析)",
@@ -1115,6 +1116,7 @@
annualReports: "十六、企业年报(公示)",
taxViolations: "十七、税收违法",
ownTaxNotices: "十八、欠税公告",
customsCredit: "十九、进出口信用(海关)",
};
// 保持所有板块单列纵向排布
var keyLabels = {
@@ -1225,6 +1227,28 @@
illegalFact: "具体违法事实描述",
caseType: "案件性质",
police: "案件移送公安机关情况",
// 进出口信用QYGLDJ33
entityNameEn: "企业英文名称",
customsRegisterCode: "海关注册编码",
customsRegisterDate: "海关注册日期",
reportingAddress: "海关报备地址",
reportingAddressEn: "海关报备地址(英文)",
customsName: "海关注册",
customsBusinessType: "经营类别",
importExportEntityCode: "进出口企业代码",
economicDistrict: "经济区划",
specialArea: "特殊贸易区域",
industryType: "行业种类",
tradeType: "跨境贸易电子商务类型",
validDate: "报关有效期",
firstRecordDate: "最早备案日期",
lastRecordDate: "最新备案日期",
customsCancelFlag: "海关注销标志",
ifAnnualReport: "年报情况",
abnormalInfo: "异常信息",
punishInfo: "行政处罚信息",
customsLevel: "海关评级",
creditLevel: "信用等级",
// 企业年报QYGLDJ12
registerCode: "注册号",
organizationCode: "组织机构代码",
@@ -3209,6 +3233,71 @@
});
}
function renderCustomsCredit(v) {
if (!v || !Array.isArray(v.items) || !v.items.length)
return "<p class='empty-hint'>暂无进出口信用记录</p>";
var html = "<p class='count-hint'>共 " + v.items.length + " 条</p>";
v.items.forEach(function (it, idx) {
html += "<div class='item-block' style='margin-bottom:16px'>";
html += "<h3 style='margin:0 0 8px;font-size:15px'>" + esc(it.entityName || "—") + "</h3>";
// 基本信息表
html += "<table class='kv-table'><tbody>";
html += "<tr><td class='k'>" + label("entityNameEn") + "</td><td>" + esc(it.entityNameEn) + "</td></tr>";
html += "<tr><td class='k'>" + label("customsRegisterCode") + "</td><td>" + esc(it.customsRegisterCode) + "</td></tr>";
html += "<tr><td class='k'>" + label("customsBusinessType") + "</td><td>" + esc(it.customsBusinessType) + "</td></tr>";
html += "<tr><td class='k'>" + label("importExportEntityCode") + "</td><td>" + esc(it.importExportEntityCode) + "</td></tr>";
html += "<tr><td class='k'>" + label("customsName") + "</td><td>" + esc(it.customsName) + "</td></tr>";
html += "<tr><td class='k'>" + label("customsRegisterDate") + "</td><td>" + esc(it.customsRegisterDate) + "</td></tr>";
html += "<tr><td class='k'>" + label("validDate") + "</td><td>" + esc(it.validDate) + "</td></tr>";
html += "<tr><td class='k'>" + label("district") + "</td><td>" + esc(it.district) + "</td></tr>";
html += "<tr><td class='k'>" + label("reportingAddress") + "</td><td>" + esc(it.reportingAddress || it.reportingAddressEn) + "</td></tr>";
html += "</tbody></table>";
// 信用状态
html += "<h4 style='margin:12px 0 6px;font-size:14px'>信用状态</h4>";
html += "<table class='kv-table'><tbody>";
html += "<tr><td class='k'>" + label("creditLevel") + "</td><td>" + esc(it.creditLevel) + "</td></tr>";
html += "<tr><td class='k'>" + label("customsLevel") + "</td><td>" + esc(it.customsLevel || "—") + "</td></tr>";
html += "<tr><td class='k'>" + label("customsCancelFlag") + "</td><td>" + esc(it.customsCancelFlag) + "</td></tr>";
html += "<tr><td class='k'>" + label("ifAnnualReport") + "</td><td>" + esc(it.ifAnnualReport) + "</td></tr>";
html += "<tr><td class='k'>" + label("abnormalInfo") + "</td><td>" + esc(it.abnormalInfo) + "</td></tr>";
html += "</tbody></table>";
// 行政处罚
if (it.punishInfo && it.punishInfo !== "" && it.punishInfo !== "[]" && it.punishInfo !== "无") {
html += "<h4 style='margin:12px 0 6px;font-size:14px'>行政处罚记录</h4>";
try {
var punishArr = JSON.parse(it.punishInfo);
if (Array.isArray(punishArr) && punishArr.length > 0) {
punishArr.forEach(function (p, pi) {
html += "<div class='item-meta' style='margin-bottom:6px;padding:6px 8px;background:var(--surface-raised);border-radius:var(--radius-sm)'>";
html += "<strong>处罚" + (punishArr.length > 1 ? "" + (pi + 1) + "" : "") + "</strong>";
html += "案件性质:" + esc(p.case_type || "—") + "";
html += "当事人:" + esc(p.case_litigant || "—") + "";
html += "决定书编号:" + esc(p.case_no || "—") + "";
html += "处罚日期:" + esc(p.case_date || "—");
html += "</div>";
});
} else {
html += "<div class='item-meta'>" + esc(it.punishInfo) + "</div>";
}
} catch (e) {
html += "<div class='item-meta'>" + esc(it.punishInfo) + "</div>";
}
}
// 其他备案信息
html += "<h4 style='margin:12px 0 6px;font-size:14px'>备案信息</h4>";
html += "<table class='kv-table'><tbody>";
html += "<tr><td class='k'>" + label("economicDistrict") + "</td><td>" + esc(it.economicDistrict) + "</td></tr>";
html += "<tr><td class='k'>" + label("specialArea") + "</td><td>" + esc(it.specialArea) + "</td></tr>";
html += "<tr><td class='k'>" + label("industryType") + "</td><td>" + esc(it.industryType) + "</td></tr>";
html += "<tr><td class='k'>" + label("tradeType") + "</td><td>" + esc(it.tradeType || "—") + "</td></tr>";
html += "<tr><td class='k'>" + label("firstRecordDate") + "</td><td>" + esc(it.firstRecordDate) + "</td></tr>";
html += "<tr><td class='k'>" + label("lastRecordDate") + "</td><td>" + esc(it.lastRecordDate) + "</td></tr>";
html += "</tbody></table>";
html += "</div>";
});
return html;
}
var sectionRenderers = {
riskOverview: renderRiskOverview,
basic: renderBasic,
@@ -3229,6 +3318,7 @@
annualReports: renderAnnualReports,
taxViolations: renderTaxViolationsSection,
ownTaxNotices: renderOwnTaxNotices,
customsCredit: renderCustomsCredit,
};
function renderSectionContent(key, value) {