diff --git a/cmd/qygl_report_build/main.go b/cmd/qygl_report_build/main.go index 7f26c39..658646f 100644 --- a/cmd/qygl_report_build/main.go +++ b/cmd/qygl_report_build/main.go @@ -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, "", " ") diff --git a/internal/domains/api/services/processors/qygl/qyglj1u9_processor.go b/internal/domains/api/services/processors/qygl/qyglj1u9_processor.go index d7758f2..0c5d3f6 100644 --- a/internal/domains/api/services/processors/qygl/qyglj1u9_processor.go +++ b/internal/domains/api/services/processors/qygl/qyglj1u9_processor.go @@ -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("未查询到可用于生成报告的企业数据")) } diff --git a/internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go b/internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go index 3c71d67..ec82ef3 100644 --- a/internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go +++ b/internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go @@ -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) + } +} diff --git a/resources/qiye.html b/resources/qiye.html index 403fcf3..dd23eb0 100644 --- a/resources/qiye.html +++ b/resources/qiye.html @@ -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 "
暂无进出口信用记录
"; + var html = "共 " + v.items.length + " 条
"; + v.items.forEach(function (it, idx) { + html += "| " + label("entityNameEn") + " | " + esc(it.entityNameEn) + " |
| " + label("customsRegisterCode") + " | " + esc(it.customsRegisterCode) + " |
| " + label("customsBusinessType") + " | " + esc(it.customsBusinessType) + " |
| " + label("importExportEntityCode") + " | " + esc(it.importExportEntityCode) + " |
| " + label("customsName") + " | " + esc(it.customsName) + " |
| " + label("customsRegisterDate") + " | " + esc(it.customsRegisterDate) + " |
| " + label("validDate") + " | " + esc(it.validDate) + " |
| " + label("district") + " | " + esc(it.district) + " |
| " + label("reportingAddress") + " | " + esc(it.reportingAddress || it.reportingAddressEn) + " |
| " + label("creditLevel") + " | " + esc(it.creditLevel) + " |
| " + label("customsLevel") + " | " + esc(it.customsLevel || "—") + " |
| " + label("customsCancelFlag") + " | " + esc(it.customsCancelFlag) + " |
| " + label("ifAnnualReport") + " | " + esc(it.ifAnnualReport) + " |
| " + label("abnormalInfo") + " | " + esc(it.abnormalInfo) + " |
| " + label("economicDistrict") + " | " + esc(it.economicDistrict) + " |
| " + label("specialArea") + " | " + esc(it.specialArea) + " |
| " + label("industryType") + " | " + esc(it.industryType) + " |
| " + label("tradeType") + " | " + esc(it.tradeType || "—") + " |
| " + label("firstRecordDate") + " | " + esc(it.firstRecordDate) + " |
| " + label("lastRecordDate") + " | " + esc(it.lastRecordDate) + " |