From 4cd3954574bd6e8de5708c9282b9f57f7c806e24 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Tue, 10 Mar 2026 17:28:04 +0800 Subject: [PATCH] f --- internal/app/app.go | 1 + internal/container/container.go | 13 +- .../domains/api/entities/enterprise_report.go | 35 + .../enterprise_report_repository.go | 17 + .../api/services/api_request_service.go | 74 +- .../api/services/processors/dependencies.go | 7 + .../processors/qygl/qyglj1u9_processor.go | 180 +++ .../qygl/qyglj1u9_processor_build.go | 1404 +++++++++++++++++ .../api/gorm_enterprise_report_repository.go | 45 + .../http/handlers/qygl_report_handler.go | 137 ++ .../http/routes/qygl_report_routes.go | 32 + internal/shared/http/router.go | 4 + 12 files changed, 1938 insertions(+), 11 deletions(-) create mode 100644 internal/domains/api/entities/enterprise_report.go create mode 100644 internal/domains/api/repositories/enterprise_report_repository.go create mode 100644 internal/domains/api/services/processors/qygl/qyglj1u9_processor.go create mode 100644 internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go create mode 100644 internal/infrastructure/database/repositories/api/gorm_enterprise_report_repository.go create mode 100644 internal/infrastructure/http/handlers/qygl_report_handler.go create mode 100644 internal/infrastructure/http/routes/qygl_report_routes.go diff --git a/internal/app/app.go b/internal/app/app.go index a666a8b..5e9c6c2 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -260,6 +260,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error { // api &apiEntities.ApiUser{}, &apiEntities.ApiCall{}, + &apiEntities.Report{}, // 任务域 &taskEntities.AsyncTask{}, diff --git a/internal/container/container.go b/internal/container/container.go index d1f7c50..7648d8b 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -660,6 +660,10 @@ func NewContainer() *Container { api_repo.NewGormApiCallRepository, fx.As(new(domain_api_repo.ApiCallRepository)), ), + fx.Annotate( + api_repo.NewGormReportRepository, + fx.As(new(domain_api_repo.ReportRepository)), + ), ), // 统计域仓储层 @@ -769,7 +773,8 @@ func NewContainer() *Container { api_services.NewApiUserAggregateService, ), api_services.NewApiCallAggregateService, - api_services.NewApiRequestService, + // 使用带仓储注入的构造函数,支持企业报告记录持久化 + api_services.NewApiRequestServiceWithRepos, api_services.NewFormConfigService, ), @@ -1284,6 +1289,8 @@ func NewContainer() *Container { ) *handlers.ComponentReportOrderHandler { return handlers.NewComponentReportOrderHandler(componentReportOrderService, purchaseOrderRepo, config, logger) }, + // 企业全景报告页面处理器 + handlers.NewQYGLReportHandler, // UI组件HTTP处理器 func( uiComponentAppService product.UIComponentApplicationService, @@ -1330,6 +1337,8 @@ func NewContainer() *Container { routes.NewStatisticsRoutes, // PDFG路由 routes.NewPDFGRoutes, + // 企业报告页面路由 + routes.NewQYGLReportRoutes, ), // 应用生命周期 @@ -1445,6 +1454,7 @@ func RegisterRoutes( apiRoutes *routes.ApiRoutes, statisticsRoutes *routes.StatisticsRoutes, pdfgRoutes *routes.PDFGRoutes, + qyglReportRoutes *routes.QYGLReportRoutes, jwtAuth *middleware.JWTAuthMiddleware, adminAuth *middleware.AdminAuthMiddleware, cfg *config.Config, @@ -1470,6 +1480,7 @@ func RegisterRoutes( announcementRoutes.Register(router) statisticsRoutes.Register(router) pdfgRoutes.Register(router) + qyglReportRoutes.Register(router) // 打印注册的路由信息 router.PrintRoutes() diff --git a/internal/domains/api/entities/enterprise_report.go b/internal/domains/api/entities/enterprise_report.go new file mode 100644 index 0000000..1a2edcf --- /dev/null +++ b/internal/domains/api/entities/enterprise_report.go @@ -0,0 +1,35 @@ +package entities + +import "time" + +// Report 报告记录实体 +// 用于持久化存储各类报告(企业报告等),通过编号和类型区分 +type Report struct { + // 报告编号,直接使用业务生成的 reportId 作为主键 + ReportID string `gorm:"primaryKey;type:varchar(64)" json:"report_id"` + + // 报告类型,例如 enterprise(企业报告)、personal 等 + Type string `gorm:"type:varchar(32);not null;index" json:"type"` + + // 调用来源API编码,例如 QYGLJ1U9 + ApiCode string `gorm:"type:varchar(32);not null;index" json:"api_code"` + + // 企业名称和统一社会信用代码,便于后续检索 + EntName string `gorm:"type:varchar(255);index" json:"ent_name"` + EntCode string `gorm:"type:varchar(64);index" json:"ent_code"` + + // 原始请求参数(JSON字符串),用于审计和排错 + RequestParams string `gorm:"type:text" json:"request_params"` + + // 报告完整JSON内容 + ReportData string `gorm:"type:text" json:"report_data"` + + // 创建时间 + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` +} + +// TableName 指定数据库表名 +func (Report) TableName() string { + return "reports" +} + diff --git a/internal/domains/api/repositories/enterprise_report_repository.go b/internal/domains/api/repositories/enterprise_report_repository.go new file mode 100644 index 0000000..346621c --- /dev/null +++ b/internal/domains/api/repositories/enterprise_report_repository.go @@ -0,0 +1,17 @@ +package repositories + +import ( + "context" + + "tyapi-server/internal/domains/api/entities" +) + +// ReportRepository 报告记录仓储接口 +type ReportRepository interface { + // Create 创建报告记录 + Create(ctx context.Context, report *entities.Report) error + + // FindByReportID 根据报告编号查询记录 + FindByReportID(ctx context.Context, reportID string) (*entities.Report, error) +} + diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 17e314c..73f1998 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -7,6 +7,7 @@ import ( "tyapi-server/internal/application/api/commands" "tyapi-server/internal/config" + api_repositories "tyapi-server/internal/domains/api/repositories" "tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors/comb" "tyapi-server/internal/domains/api/services/processors/dwbg" @@ -50,6 +51,8 @@ type ApiRequestService struct { processorDeps *processors.ProcessorDependencies combService *comb.CombService config *config.Config + + reportRepo api_repositories.ReportRepository } func NewApiRequestService( @@ -66,26 +69,76 @@ func NewApiRequestService( validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, cfg *config.Config, +) *ApiRequestService { + return NewApiRequestServiceWithRepos( + westDexService, + shujubaoService, + muziService, + yushanService, + tianYanChaService, + alicloudService, + zhichaService, + xingweiService, + jiguangService, + shumaiService, + validator, + productManagementService, + cfg, + nil, + ) +} + +// NewApiRequestServiceWithRepos 带自定义仓储的构造函数,便于扩展(例如企业报告记录) +func NewApiRequestServiceWithRepos( + westDexService *westdex.WestDexService, + shujubaoService *shujubao.ShujubaoService, + muziService *muzi.MuziService, + yushanService *yushan.YushanService, + tianYanChaService *tianyancha.TianYanChaService, + alicloudService *alicloud.AlicloudService, + zhichaService *zhicha.ZhichaService, + xingweiService *xingwei.XingweiService, + jiguangService *jiguang.JiguangService, + shumaiService *shumai.ShumaiService, + validator interfaces.RequestValidator, + productManagementService *services.ProductManagementService, + cfg *config.Config, + reportRepo api_repositories.ReportRepository, ) *ApiRequestService { // 创建组合包服务 combService := comb.NewCombService(productManagementService) // 创建处理器依赖容器 - processorDeps := processors.NewProcessorDependencies(westDexService, shujubaoService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, shumaiService, validator, combService) + processorDeps := processors.NewProcessorDependencies( + westDexService, + shujubaoService, + muziService, + yushanService, + tianYanChaService, + alicloudService, + zhichaService, + xingweiService, + jiguangService, + shumaiService, + validator, + combService, + reportRepo, + ) // 统一注册所有处理器 registerAllProcessors(combService) return &ApiRequestService{ - westDexService: westDexService, - muziService: muziService, - yushanService: yushanService, - tianYanChaService: tianYanChaService, - alicloudService: alicloudService, - validator: validator, - processorDeps: processorDeps, - combService: combService, - config: cfg, + westDexService: westDexService, + muziService: muziService, + yushanService: yushanService, + tianYanChaService: tianYanChaService, + alicloudService: alicloudService, + validator: validator, + processorDeps: processorDeps, + combService: combService, + config: cfg, + reportRepo: reportRepo, } } @@ -180,6 +233,7 @@ func registerAllProcessors(combService *comb.CombService) { "QYGLNIO8": qygl.ProcessQYGLNIO8Request, //企业基本信息 "QYGLP0HT": qygl.ProcessQYGLP0HTRequest, //股权穿透 "QYGL5S1I": qygl.ProcessQYGL5S1IRequest, //企业司法涉诉I + "QYGLJ1U9": qygl.ProcessQYGLJ1U9Request, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I) "QYGLJ0Q1": qygl.ProcessQYGLJ0Q1Request, //企业股权结构全景查询 "QYGLUY3S": qygl.ProcessQYGLUY3SRequest, //企业经营状态全景查询 "YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖 diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 07798ca..a56560c 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -2,7 +2,9 @@ package processors import ( "context" + "tyapi-server/internal/application/api/commands" + "tyapi-server/internal/domains/api/repositories" "tyapi-server/internal/infrastructure/external/alicloud" "tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/muzi" @@ -42,6 +44,9 @@ type ProcessorDependencies struct { CombService CombServiceInterface // Changed to interface to break import cycle Options *commands.ApiCallOptions // 添加Options支持 CallContext *CallContext // 添加CallApi调用上下文 + + // 企业报告记录仓储,用于持久化 QYGLJ1U9 生成的企业报告 + ReportRepo repositories.ReportRepository } // NewProcessorDependencies 创建处理器依赖容器 @@ -58,6 +63,7 @@ func NewProcessorDependencies( shumaiService *shumai.ShumaiService, validator interfaces.RequestValidator, combService CombServiceInterface, // Changed to interface + reportRepo repositories.ReportRepository, ) *ProcessorDependencies { return &ProcessorDependencies{ WestDexService: westDexService, @@ -74,6 +80,7 @@ func NewProcessorDependencies( CombService: combService, Options: nil, // 初始化为nil,在调用时设置 CallContext: nil, // 初始化为nil,在调用时设置 + ReportRepo: reportRepo, } } diff --git a/internal/domains/api/services/processors/qygl/qyglj1u9_processor.go b/internal/domains/api/services/processors/qygl/qyglj1u9_processor.go new file mode 100644 index 0000000..a08fbd3 --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qyglj1u9_processor.go @@ -0,0 +1,180 @@ +package qygl + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/url" + "sync" + "time" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/entities" + "tyapi-server/internal/domains/api/services/processors" +) + +// ProcessQYGLJ1U9Request 企业全景报告处理器:并发调用企业全量(QYGLUY3S)、股权全景(QYGLJ0Q1)、司法涉诉(QYGL5S1I), +// 然后复用 qyglj1u9_processor_build.go 中的 buildReport / map* 逻辑生成企业报告结构 +func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + // 复用 QYGLUY3S 的入参结构:企业名称/注册号/统一社会信用代码 + var p dto.QYGLUY3SReq + if err := json.Unmarshal(params, &p); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + if err := deps.Validator.ValidateStruct(p); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + + // 并发调用三个已有处理器 + type apiResult struct { + key string + data map[string]interface{} + err error + } + resultsCh := make(chan apiResult, 3) + var wg sync.WaitGroup + + call := func(key string, req interface{}, fn func(context.Context, []byte, *processors.ProcessorDependencies) ([]byte, error)) { + wg.Add(1) + go func() { + defer wg.Done() + b, err := json.Marshal(req) + if err != nil { + resultsCh <- apiResult{key: key, err: err} + return + } + resp, err := fn(ctx, b, deps) + if err != nil { + resultsCh <- apiResult{key: key, err: err} + return + } + var m map[string]interface{} + if err := json.Unmarshal(resp, &m); err != nil { + resultsCh <- apiResult{key: key, err: err} + return + } + resultsCh <- apiResult{key: key, data: m} + }() + } + + // 企业全量信息核验V2(QYGLUY3S) + call("jiguangFull", map[string]interface{}{ + "ent_name": p.EntName, + "ent_reg_no": p.EntRegno, + "ent_code": p.EntCode, + }, ProcessQYGLUY3SRequest) + + // 企业股权结构全景(QYGLJ0Q1) + call("equityPanorama", map[string]interface{}{ + "ent_name": p.EntName, + "ent_code": p.EntCode, + }, ProcessQYGLJ0Q1Request) + + // 企业司法涉诉V2(QYGL5S1I) + call("judicialCertFull", map[string]interface{}{ + "ent_name": p.EntName, + "ent_code": p.EntCode, + }, ProcessQYGL5S1IRequest) + + wg.Wait() + close(resultsCh) + + var jiguang, judicial, equity map[string]interface{} + for r := range resultsCh { + if r.err != nil { + // 任一关键数据源异常,则返回系统错误(也可以根据需求做降级) + return nil, errors.Join(processors.ErrSystem, fmt.Errorf("%s 调用失败: %w", r.key, r.err)) + } + switch r.key { + case "jiguangFull": + jiguang = r.data + case "judicialCertFull": + judicial = r.data + case "equityPanorama": + equity = r.data + } + } + if jiguang == nil { + jiguang = map[string]interface{}{} + } + if judicial == nil { + judicial = map[string]interface{}{} + } + if equity == nil { + equity = map[string]interface{}{} + } + + // 复用构建逻辑生成企业报告结构 + report := buildReport(jiguang, judicial, equity) + + // 为报告生成唯一编号并缓存,供后续通过编号查看 + reportID := saveQYGLReport(report) + report["reportId"] = reportID + + // 持久化企业报告记录到数据库(忽略持久化失败,不影响接口主流程) + if deps.ReportRepo != nil { + reqJSON, _ := json.Marshal(p) + reportJSON, _ := json.Marshal(report) + _ = deps.ReportRepo.Create(ctx, &entities.Report{ + ReportID: reportID, + Type: "enterprise", + ApiCode: "QYGLJ1U9", + EntName: p.EntName, + EntCode: p.EntCode, + RequestParams: string(reqJSON), + ReportData: string(reportJSON), + }) + } + // 为报告补充前端查看链接,供调用方直接跳转到企业报告页面(通过编号访问) + report["reportUrl"] = buildQYGLReportURLByID(reportID) + + out, err := json.Marshal(report) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + return out, nil +} + +// 内存中的企业报告缓存(简单实现,进程重启后清空) +var qyglReportStore = struct { + sync.RWMutex + data map[string]map[string]interface{} +}{ + data: make(map[string]map[string]interface{}), +} + +// saveQYGLReport 保存报告并返回生成的编号 +func saveQYGLReport(report map[string]interface{}) string { + id := generateQYGLReportID() + qyglReportStore.Lock() + qyglReportStore.data[id] = report + qyglReportStore.Unlock() + return id +} + +// GetQYGLReport 根据编号获取报告(供页面渲染使用) +func GetQYGLReport(id string) (map[string]interface{}, bool) { + qyglReportStore.RLock() + defer qyglReportStore.RUnlock() + r, ok := qyglReportStore.data[id] + return r, ok +} + +// generateQYGLReportID 生成短编号 +func generateQYGLReportID() string { + b := make([]byte, 8) + if _, err := rand.Read(b); err == nil { + return hex.EncodeToString(b) + } + // 随机数失败时退化为时间戳 + return fmt.Sprintf("%d", time.Now().UnixNano()) +} + +// buildQYGLReportURLByID 构造企业报告前端查看链接(通过编号查看) +func buildQYGLReportURLByID(id string) string { + return "/reports/qygl/" + url.PathEscape(id) +} + diff --git a/internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go b/internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go new file mode 100644 index 0000000..e54abd9 --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qyglj1u9_processor_build.go @@ -0,0 +1,1404 @@ +package qygl + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" +) + +func buildReport(jiguang, judicial, equity map[string]interface{}) map[string]interface{} { + report := make(map[string]interface{}) + report["reportTime"] = time.Now().Format("2006-01-02 15:04:05") + + basic := mapFromBASIC(jiguang) + report["creditCode"] = str(basic["creditCode"]) + report["entName"] = str(basic["entName"]) + report["basic"] = basic + + report["branches"] = mapBranches(jiguang) + // 股权/实控人/受益人/对外投资:有股权全景时以其为准,否则用全量信息 + if len(equity) > 0 { + report["shareholding"] = mapShareholdingWithEquity(jiguang, equity) + report["controller"] = mapControllerFromEquity(equity) + report["beneficiaries"] = mapBeneficiariesFromEquity(equity) + report["investments"] = mapInvestmentsWithEquity(jiguang, equity) + } else { + report["shareholding"] = mapShareholding(jiguang) + report["controller"] = mapController(jiguang) + report["beneficiaries"] = mapBeneficiaries() + report["investments"] = mapInvestments(jiguang) + } + report["guarantees"] = mapGuarantees(jiguang) + report["management"] = mapManagement(jiguang) + report["assets"] = mapAssets(jiguang) + report["licenses"] = mapLicenses(jiguang) + report["activities"] = mapActivities(jiguang) + report["inspections"] = mapInspections(jiguang) + risks := mapRisks(jiguang, judicial) + report["risks"] = risks + report["timeline"] = mapTimeline(jiguang) + report["listed"] = mapListed(jiguang) + // 基于汇总后的报告数据生成「风险情况」,不再直接从原始数据源计算综合评分 + report["riskOverview"] = mapRiskOverview(report, risks) + if bl, _ := jiguang["BASICLIST"].([]interface{}); len(bl) > 0 { + report["basicList"] = bl + } + return report +} + +func mapFromBASIC(jiguang map[string]interface{}) map[string]interface{} { + basic := make(map[string]interface{}) + b, _ := jiguang["BASIC"].(map[string]interface{}) + if b == nil { + return basic + } + // 与《企业全量信息核验V2_返回字段说明》BASIC 一一对应 + basic["entName"] = str(b["ENTNAME"]) + basic["creditCode"] = str(b["CREDITCODE"]) + basic["regNo"] = str(b["REGNO"]) + basic["orgCode"] = str(b["ORGCODES"]) + basic["entType"] = str(b["ENTTYPE"]) + basic["entTypeCode"] = str(b["ENTTYPECODE"]) + basic["entityTypeCode"] = str(b["ENTITYTYPE"]) + basic["establishDate"] = str(b["ESDATE"]) + basic["registeredCapital"] = num(b["REGCAP"]) + basic["regCapCurrency"] = str(b["REGCAPCUR"]) + basic["regCapCurrencyCode"] = str(b["REGCAPCURCODE"]) + basic["regOrg"] = str(b["REGORG"]) + basic["regOrgCode"] = str(b["REGORGCODE"]) + basic["regProvince"] = str(b["REGORGPROVINCE"]) + basic["provinceCode"] = str(b["S_EXT_NODENUM"]) + basic["regCity"] = str(b["REGORGCITY"]) + basic["regCityCode"] = str(b["REGCITY"]) + basic["regDistrict"] = str(b["REGORGDISTRICT"]) + basic["districtCode"] = str(b["DISTRICTCODE"]) + basic["address"] = str(b["DOM"]) + basic["postalCode"] = str(b["POSTALCODE"]) + basic["legalRepresentative"] = str(b["FRNAME"]) + basic["compositionForm"] = str(b["FROM"]) + basic["approvedBusinessItem"] = str(b["ABUITEM"]) + basic["status"] = entStatusText(str(b["ENTSTATUS"])) + basic["statusCode"] = str(b["ENTSTATUS"]) + basic["operationPeriodFrom"] = str(b["OPFROM"]) + basic["operationPeriodTo"] = str(b["OPTO"]) + basic["approveDate"] = str(b["APPRDATE"]) + basic["cancelDate"] = str(b["CANDATE"]) + basic["revokeDate"] = str(b["REVDATE"]) + basic["cancelReason"] = str(b["CANREASON"]) + basic["revokeReason"] = str(b["REVREASON"]) + basic["businessScope"] = str(b["ZSOPSCOPE"]) + basic["lastAnnuReportYear"] = str(b["ANCHEYEAR"]) + // 曾用名:始终写入该键,无数据时为空数组,便于前端固定展示「曾用名」行 + if n := str(b["ENTNAME_OLD"]); n != "" { + basic["oldNames"] = strings.Split(n, ";") + } else { + basic["oldNames"] = []interface{}{} + } + return basic +} + +func entStatusText(code string) string { + m := map[string]string{"1": "存续", "2": "注销", "3": "吊销", "4": "撤销", "5": "迁出", "6": "设立中", "7": "清算中", "8": "停业", "9": "其他"} + if t, ok := m[code]; ok { + return t + } + return code +} + +func mapBranches(jiguang map[string]interface{}) []interface{} { + arr, _ := jiguang["FILIATION"].([]interface{}) + out := make([]interface{}, 0, len(arr)) + for _, v := range arr { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + br := map[string]interface{}{ + "name": str(m["BRNAME"]), + "regNo": str(m["BRREGNO"]), + "creditCode": str(m["BRN_CREDIT_CODE"]), + "regOrg": str(m["BRN_REG_ORG"]), + } + out = append(out, br) + } + return out +} + +// mapGuarantees 按文档 §4.2:yearReportId, mortgagor, creditor, principalAmount, principalKind, guaranteeType, periodFrom, periodTo, guaranteePeriod +func mapGuarantees(jiguang map[string]interface{}) []interface{} { + var out []interface{} + for _, v := range sliceOrEmpty(jiguang["YEARREPORTFORGUARANTEE"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + out = append(out, map[string]interface{}{ + "yearReportId": str(m["ANCHEID"]), + "mortgagor": str(m["MORTGAGOR"]), + "creditor": str(m["MORE"]), + "principalAmount": str(m["PRICLASECAM"]), + "principalKind": str(m["PRICLASECKIND"]), + "guaranteeType": str(m["GATYPE"]), + "periodFrom": str(m["PEFPERFORM"]), + "periodTo": str(m["PEFPERTO"]), + "guaranteePeriod": str(m["GUARAPPERIOD"]), + }) + } + return out +} + +// mapInspections 按文档 §4.7:dataType, regOrg, inspectDate, result +func mapInspections(jiguang map[string]interface{}) []interface{} { + var out []interface{} + for _, v := range sliceOrEmpty(jiguang["INSPECT"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + out = append(out, map[string]interface{}{ + "dataType": str(m["ISP_TYPE"]), + "regOrg": str(m["ISP_REGORG"]), + "inspectDate": str(m["ISP_DATE"]), + "result": str(m["ISP_RESULT"]), + }) + } + return out +} + +// mapPaidInDetails 按文档 §3.1 paidInDetails 子项:yearReportId, investor, paidDate, paidMethod, accumulatedPaid +func mapPaidInDetails(jiguang map[string]interface{}) []interface{} { + var out []interface{} + for _, v := range sliceOrEmpty(jiguang["YEARREPORTPAIDUPCAPITAL"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + out = append(out, map[string]interface{}{ + "yearReportId": str(m["ANCHEID"]), + "investor": str(m["INV"]), + "paidDate": str(m["CONDATE"]), + "paidMethod": str(m["CONFORM"]), + "accumulatedPaid": str(m["LIACCONAM"]), + }) + } + return out +} + +// mapSubscribedCapitalDetails 按文档 §3.1 subscribedCapitalDetails 子项:yearReportId, investor, subscribedDate, subscribedMethod, accumulatedSubscribed +func mapSubscribedCapitalDetails(jiguang map[string]interface{}) []interface{} { + var out []interface{} + for _, v := range sliceOrEmpty(jiguang["YEARREPORTSUBCAPITAL"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + out = append(out, map[string]interface{}{ + "yearReportId": str(m["ANCHEID"]), + "investor": str(m["INV"]), + "subscribedDate": str(m["CONDATE"]), + "subscribedMethod": str(m["CONFORM"]), + "accumulatedSubscribed": str(m["LISUBCONAM"]), + }) + } + return out +} + +// mapEquityPledges 按文档 §3.1 equityPledges 子项:regNo, pledgor, pledgorIdNo, pledgedAmount, pledgee, pledgeeIdNo, regDate, status, publicDate, changeContent, changeDate, cancelDate, cancelReason +func mapEquityPledges(jiguang map[string]interface{}) []interface{} { + var out []interface{} + for _, v := range sliceOrEmpty(jiguang["STOCKPAWN"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + out = append(out, map[string]interface{}{ + "regNo": str(m["STK_PAWN_REGNO"]), + "pledgor": str(m["STK_PAWN_CZPER"]), + "pledgorIdNo": str(m["STK_PAWN_CZCERNO"]), + "pledgedAmount": str(m["STK_PAWN_CZAMT"]), + "pledgee": str(m["STK_PAWN_ZQPER"]), + "pledgeeIdNo": str(m["STK_PAWN_ZQCERNO"]), + "regDate": str(m["STK_PAWN_REGDATE"]), + "status": str(m["STK_PAWN_STATUS"]), + "publicDate": str(m["STK_PAWN_DATE"]), + }) + } + return out +} + +func mapShareholding(jiguang map[string]interface{}) map[string]interface{} { + stockPawn := sliceOrEmpty(jiguang["STOCKPAWN"]) + out := map[string]interface{}{ + "shareholders": []interface{}{}, + "equityChanges": []interface{}{}, + "equityPledges": mapEquityPledges(jiguang), + "paidInDetails": mapPaidInDetails(jiguang), + "subscribedCapitalDetails": mapSubscribedCapitalDetails(jiguang), + "hasEquityPledges": len(stockPawn) > 0, + } + // 股东明细:企业全量信息核验V2 SHAREHOLDER(《企业全量信息核验V2_返回字段说明》股东及出资信息) + // 接口字段 -> 报告 shareholding.shareholders[] 子项: + // CONDATE->subscribedDate, SUBCONAM->subscribedAmount, FUNDEDRATIO->ownershipPercent, + // INVTYPECODE->typeCode, INVTYPE->type, CONFORMCODE->subscribedMethodCode, CONFORM->subscribedMethod, + // CURRENCYCODE->subscribedCurrencyCode, REGCAPCUR->subscribedCurrency/paidCurrency, + // SHANAME->name, CREDITCODE->creditCode(股东), REGNO->regNo(股东), ACCONAM->paidAmount, + // ACCONDATE->paidDate, ACCONFORM_CN->paidMethod, ISHISTORY->isHistory;BLICTYPE/BLICNO/ZSBLICTYPE_* 暂不支持 + shList, _ := jiguang["SHAREHOLDER"].([]interface{}) + holders, _ := jiguang["holders"].([]interface{}) + var list []interface{} + for _, v := range shList { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + pct := num(m["FUNDEDRATIO"]) + list = append(list, map[string]interface{}{ + "name": str(m["SHANAME"]), + "type": str(m["INVTYPE"]), + "typeCode": str(m["INVTYPECODE"]), + "ownershipPercent": pct, + "subscribedAmount": num(m["SUBCONAM"]), + "paidAmount": num(m["ACCONAM"]), + "subscribedCurrency": str(m["REGCAPCUR"]), + "subscribedCurrencyCode": str(m["CURRENCYCODE"]), + "paidCurrency": str(m["REGCAPCUR"]), + "subscribedDate": str(m["CONDATE"]), + "paidDate": str(m["ACCONDATE"]), + "subscribedMethod": str(m["CONFORM"]), + "subscribedMethodCode": str(m["CONFORMCODE"]), + "paidMethod": str(m["ACCONFORM_CN"]), + "creditCode": str(m["CREDITCODE"]), + "regNo": str(m["REGNO"]), + "isHistory": str(m["ISHISTORY"]) == "1", + }) + } + if len(list) == 0 { + for _, v := range holders { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + list = append(list, map[string]interface{}{ + "name": str(m["shareholderName"]), + "ownershipPercent": num(m["shareholderPercent"]), + "subscribedAmount": num(m["subscribedCapital"]), + }) + } + } + out["shareholders"] = list + out["shareholderCount"] = len(list) + if basic, _ := jiguang["BASIC"].(map[string]interface{}); basic != nil { + out["registeredCapital"] = num(basic["REGCAP"]) + out["currency"] = str(basic["REGCAPCUR"]) + } + // 根据股东持股比例计算第一大股东与前五大合计 + computeShareholdingSummary(out) + // 股权变更 + for _, v := range sliceOrEmpty(jiguang["ENTPUBINVALTERINFO"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + out["equityChanges"] = append(out["equityChanges"].([]interface{}), map[string]interface{}{ + "changeDate": str(m["ALTDATE"]), + "shareholderName": str(m["INV"]), + "percentBefore": str(m["TRANSAMPR"]), + "percentAfter": str(m["TRANSAMAFT"]), + "source": "自主公示", + }) + } + return out +} + +func mapController(jiguang map[string]interface{}) map[string]interface{} { + arr, _ := jiguang["ACTUALCONTROLLER"].([]interface{}) + if len(arr) == 0 { + return nil + } + first, _ := arr[0].(map[string]interface{}) + if first == nil { + return nil + } + return map[string]interface{}{ + "id": "", + "name": str(first["CONTROLLERNAME"]), + "type": str(first["CONTROLLERTYPE"]), + "percent": num(first["CONTROLLERPERCENT"]), + "path": nil, + "reason": "", + "source": "企业全量信息核验V2", + } +} + +func mapBeneficiaries() []interface{} { + return []interface{}{} +} + +func mapInvestments(jiguang map[string]interface{}) map[string]interface{} { + list := sliceOrEmpty(jiguang["ENTINV"]) + var entities []interface{} + var totalAmount float64 + for _, v := range list { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + amt := num(m["SUBCONAM"]) + totalAmount += amt + entities = append(entities, map[string]interface{}{ + "entName": str(m["ENTJGNAME"]), + "creditCode": str(m["CREDITCODE"]), + "regNo": str(m["REGNO"]), + "entType": str(m["ENTTYPE"]), + "regCap": num(m["REGCAP"]), + "regCapCurrency": str(m["REGCAPCUR"]), + "entStatus": entStatusText(str(m["ENTSTATUS"])), + "regOrg": str(m["REGORG"]), + "establishDate": str(m["ESDATE"]), + "investAmount": num(m["SUBCONAM"]), + "investCurrency": str(m["CONGROCUR"]), + "investPercent": num(m["FUNDEDRATIO"]), + "investMethod": str(m["CONFORM"]), + "isListed": str(m["ISLISTED"]) == "1" || strings.ToUpper(str(m["ISLISTED"])) == "Y", + "source": "企业全量信息核验V2", + }) + } + frinv := sliceOrEmpty(jiguang["FRINV"]) + var legalRepInvest []interface{} + for _, v := range frinv { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + // FRINV 中没有明确的个人投资金额字段,使用 REGCAP 或 PPVAMOUNT 时需谨慎; + // 当前数据中投资额不可直接确定,这里保持 0,仅做结构展示。 + legalRepInvest = append(legalRepInvest, map[string]interface{}{ + "entName": str(m["ENTNAME"]), + "creditCode": str(m["CREDITCODE"]), + "regNo": str(m["REGNO"]), + "entType": str(m["ENTTYPE"]), + "regCap": num(m["REGCAP"]), + "entStatus": entStatusText(str(m["ENTSTATUS"])), + "regOrg": str(m["REGORG"]), + "establishDate": str(m["ESDATE"]), + "investAmount": num(m["SUBCONAM"]), + "investPercent": num(m["FUNDEDRATIO"]), + "investMethod": str(m["CONFORM"]), + }) + } + return map[string]interface{}{ + "totalCount": len(entities), + "totalAmount": totalAmount, + "list": entities, + "legalRepresentativeInvestments": legalRepInvest, + } +} + +// ---- 股权全景:与全量信息重合时以股权全景为准 ---- + +func equityTypeLabel(t string) string { + switch strings.TrimSpace(strings.ToUpper(t)) { + case "P": + return "自然人" + case "E": + return "企业" + case "UE": + return "其他" + default: + return t + } +} + +// flattenEquityTreeItems 递归展开树形 items,仅收集有 name 的子节点(level>=1) +func flattenEquityTreeItems(items []interface{}, out *[]interface{}, level int) { + for _, v := range items { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + name := str(m["name"]) + if name == "" { + continue + } + childLevel, _ := m["level"].(float64) + if int(childLevel) == 0 && level > 0 { + continue + } + *out = append(*out, m) + if sub, _ := m["items"].([]interface{}); len(sub) > 0 { + flattenEquityTreeItems(sub, out, level+1) + } + } +} + +// computeShareholdingSummary 计算股权汇总:第一大股东、前五大合计 +func computeShareholdingSummary(m map[string]interface{}) { + list, _ := m["shareholders"].([]interface{}) + if len(list) == 0 { + return + } + type holder struct { + name string + pct float64 + } + var hs []holder + for _, v := range list { + if mp, ok := v.(map[string]interface{}); ok { + p := num(mp["ownershipPercent"]) + if p <= 0 { + continue + } + hs = append(hs, holder{name: str(mp["name"]), pct: p}) + } + } + if len(hs) == 0 { + return + } + sort.Slice(hs, func(i, j int) bool { return hs[i].pct > hs[j].pct }) + m["topHolderName"] = hs[0].name + m["topHolderPercent"] = hs[0].pct + var sum float64 + for i := 0; i < len(hs) && i < 5; i++ { + sum += hs[i].pct + } + m["top5TotalPercent"] = sum +} + +// mapShareholdingWithEquity 股权全景 shareholderDetail 为主,全量信息补 equityPledges/paidIn/subscribed/equityChanges +func mapShareholdingWithEquity(jiguang, equity map[string]interface{}) map[string]interface{} { + base := mapShareholding(jiguang) + if base == nil { + base = map[string]interface{}{ + "shareholders": []interface{}{}, + "equityChanges": []interface{}{}, + "equityPledges": []interface{}{}, + "paidInDetails": []interface{}{}, + "subscribedCapitalDetails": []interface{}{}, + "hasEquityPledges": false, + "shareholderCount": 0, + "registeredCapital": nil, + "currency": "", + } + } + shDetail, _ := equity["shareholderDetail"].(map[string]interface{}) + if shDetail == nil { + return base + } + items, _ := shDetail["items"].([]interface{}) + var shareholders []interface{} + for _, v := range items { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + name := str(m["name"]) + if name == "" { + continue + } + typ := str(m["type"]) + percent := num(m["percent"]) + amount := num(m["amount"]) + shareholders = append(shareholders, map[string]interface{}{ + "name": name, + "type": equityTypeLabel(typ), + "typeCode": typ, + "ownershipPercent": percent, + "subscribedAmount": amount, + "paidAmount": nil, + "subscribedCurrency": "", + "subscribedCurrencyCode": "", + "paidCurrency": "", + "subscribedDate": "", + "paidDate": "", + "subscribedMethod": "", + "subscribedMethodCode": "", + "paidMethod": "", + "creditCode": "", + "regNo": "", + "isHistory": false, + "source": "股权全景", + }) + } + base["shareholders"] = shareholders + base["shareholderCount"] = len(shareholders) + // 重新根据股权全景股东数据计算第一大股东与前五大合计 + computeShareholdingSummary(base) + return base +} + +// mapControllerFromEquity 从股权全景 actualController* 生成实控人 +func mapControllerFromEquity(equity map[string]interface{}) map[string]interface{} { + name := str(equity["actualControllerName"]) + if name == "" { + return nil + } + pathRaw := equity["actualControllerPath"] + var pathMap map[string]interface{} + if p, ok := pathRaw.(map[string]interface{}); ok { + nodes := sliceOrEmpty(p["nodes"]) + links := sliceOrEmpty(p["links"]) + if len(nodes) > 0 || len(links) > 0 { + pathMap = deepCopyPathForReport(p) + } + } + return map[string]interface{}{ + "id": str(equity["actualControllerId"]), + "name": name, + "type": str(equity["actualControllerType"]), + "percent": num(equity["actualControllerPercent"]), + "path": pathMap, + "reason": "", + "source": "股权全景", + } +} + +// deepCopyPathForReport 复制 path,并将 nodes 中 uid 转为 entityId 以兼容前端 +func deepCopyPathForReport(p map[string]interface{}) map[string]interface{} { + out := map[string]interface{}{"links": p["links"], "nodes": nil} + nodes := sliceOrEmpty(p["nodes"]) + if len(nodes) == 0 { + return out + } + newNodes := make([]interface{}, 0, len(nodes)) + for _, n := range nodes { + nm, _ := n.(map[string]interface{}) + if nm == nil { + continue + } + dup := make(map[string]interface{}) + for k, v := range nm { + dup[k] = v + } + if uid, has := dup["uid"]; has && uid != nil { + dup["entityId"] = uid + } + newNodes = append(newNodes, dup) + } + out["nodes"] = newNodes + return out +} + +// mapBeneficiariesFromEquity 从股权全景 beneficiary[] 生成最终受益人列表 +func mapBeneficiariesFromEquity(equity map[string]interface{}) []interface{} { + benList, _ := equity["beneficiary"].([]interface{}) + var out []interface{} + for _, v := range benList { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + tcode := str(m["beneficiaryType"]) + out = append(out, map[string]interface{}{ + "id": str(m["beneficiaryId"]), + "name": str(m["beneficiaryName"]), + "type": equityTypeLabel(tcode), + "typeCode": tcode, + "percent": num(m["beneficiaryPercent"]), + "path": m["beneficiaryPath"], + "reason": str(m["reason"]), + "source": "股权全景", + }) + } + return out +} + +// mapInvestmentsWithEquity 股权全景 investmentDetail 为主,全量信息补 legalRepresentativeInvestments +func mapInvestmentsWithEquity(jiguang, equity map[string]interface{}) map[string]interface{} { + base := mapInvestments(jiguang) + if base == nil { + base = map[string]interface{}{ + "totalCount": 0, "list": []interface{}{}, + "legalRepresentativeInvestments": []interface{}{}, + } + } + invDetail, _ := equity["investmentDetail"].(map[string]interface{}) + if invDetail == nil { + return base + } + var list []interface{} + flattenEquityTreeItems(sliceOrEmpty(invDetail["items"]), &list, 0) + var entities []interface{} + for _, v := range list { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + // 跳过根节点(level 0),只保留对外投资主体 + if num(m["level"]) == 0 { + continue + } + name := str(m["name"]) + if name == "" { + continue + } + entities = append(entities, map[string]interface{}{ + "entName": name, + "creditCode": "", + "regNo": "", + "entType": str(m["sh_type"]), + "regCap": nil, + "regCapCurrency": "", + "entStatus": "", + "regOrg": "", + "establishDate": "", + "investAmount": num(m["amount"]), + "investCurrency": "", + "investPercent": num(m["percent"]), + "investMethod": "", + "isListed": false, + "source": "股权全景", + }) + } + base["list"] = entities + base["totalCount"] = len(entities) + return base +} + +func mapManagement(jiguang map[string]interface{}) map[string]interface{} { + person := sliceOrEmpty(jiguang["PERSON"]) + var executives []interface{} + for _, v := range person { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + executives = append(executives, map[string]interface{}{ + "name": str(m["PERNAME"]), + "position": str(m["POSITION"]), + }) + } + frPos := sliceOrEmpty(jiguang["FRPOSITION"]) + var otherPos []interface{} + for _, v := range frPos { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + otherPos = append(otherPos, map[string]interface{}{ + "entName": str(m["ENTNAME"]), + "position": str(m["POSITION"]), + "name": str(m["NAME"]), + "regNo": str(m["REGNO"]), + "creditCode": str(m["CREDITCODE"]), + "entStatus": str(m["ENTSTATUS"]), + }) + } + // 文档 §4.3:employeeCount, femaleEmployeeCount 来自 YEARREPORTBASIC;socialSecurity 来自 YEARREPORTSOCSEC + var employeeCount, femaleEmployeeCount interface{} + if yb := sliceOrEmpty(jiguang["YEARREPORTBASIC"]); len(yb) > 0 { + if b, _ := yb[0].(map[string]interface{}); b != nil { + if n := num(b["EMPNUM"]); n > 0 { + employeeCount = n + } + if n := num(b["WOMEMPNUM"]); n > 0 { + femaleEmployeeCount = n + } + } + } + var socialSecurity interface{} = map[string]interface{}{} + if soc := sliceOrEmpty(jiguang["YEARREPORTSOCSEC"]); len(soc) > 0 { + socialSecurity = soc[0] + } + return map[string]interface{}{ + "executives": executives, + "legalRepresentativeOtherPositions": otherPos, + "employeeCount": employeeCount, + "femaleEmployeeCount": femaleEmployeeCount, + "socialSecurity": socialSecurity, + } +} + +// mapAssets 按文档 §4.4:years[]. year, reportDate, assetTotal, revenueTotal, mainBusinessRevenue, taxTotal, equityTotal, profitTotal, netProfit, liabilityTotal, businessStatus, mainBusiness +func mapAssets(jiguang map[string]interface{}) map[string]interface{} { + years := sliceOrEmpty(jiguang["YEARREPORTANASSETSINFO"]) + list := make([]interface{}, 0, len(years)) + for _, v := range years { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + list = append(list, map[string]interface{}{ + "year": str(m["ANCHEYEAR"]), + "reportDate": str(m["ANCHEID"]), + "assetTotal": str(m["ASSGRO"]), + "revenueTotal": str(m["VENDINC"]), + "mainBusinessRevenue": str(m["MAIBUSINC"]), + "taxTotal": str(m["RATGRO"]), + "equityTotal": str(m["TOTEQU"]), + "profitTotal": str(m["PROGRO"]), + "netProfit": str(m["NETINC"]), + "liabilityTotal": str(m["LIAGRO"]), + "businessStatus": str(m["BUSST"]), + "mainBusiness": str(m["MAIBUS"]), + }) + } + return map[string]interface{}{"years": list} +} + +func mapLicenses(jiguang map[string]interface{}) map[string]interface{} { + permits := sliceOrEmpty(jiguang["ENTPUBPERMITINFO"]) + var list []interface{} + for _, v := range permits { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + list = append(list, map[string]interface{}{ + "name": str(m["LICNAME_CN"]), + "valFrom": str(m["VALFROM"]), + "valTo": str(m["VALTO"]), + "licAnth": str(m["LICANTH"]), + "licItem": str(m["LICITEM"]), + }) + } + // permitChanges:按文档用 changeDate/detailBefore/detailAfter/changeType(驼峰) + var permitChangeList []interface{} + for _, v := range sliceOrEmpty(jiguang["ENTPUBPERMITUPDINFO"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + permitChangeList = append(permitChangeList, map[string]interface{}{ + "changeDate": str(m["ALTDATE"]), + "detailBefore": str(m["ALTBE"]), + "detailAfter": str(m["ALTAF"]), + "changeType": str(m["ALTITEM"]), + }) + } + return map[string]interface{}{ + "permits": list, + "permitChanges": permitChangeList, + "ipPledges": sliceOrEmpty(jiguang["ENTPUBIPINFO"]), + "otherLicenses": []interface{}{}, + } +} + +func mapActivities(jiguang map[string]interface{}) map[string]interface{} { + return map[string]interface{}{ + "bids": sliceOrEmpty(jiguang["BIDINFO"]), + "websites": sliceOrEmpty(jiguang["YEARREPORTWEBSITEINFO"]), + } +} + +func mapAdminPenaltyUpdates(jiguang map[string]interface{}) []interface{} { + var out []interface{} + for _, v := range sliceOrEmpty(jiguang["ENTPUBCASEUPDINFO"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + updateContent := str(m["ALTBE"]) + if updateContent == "" { + updateContent = str(m["ALTAF"]) + } + out = append(out, map[string]interface{}{ + "updateDate": str(m["ALTDATE"]), + "updateContent": updateContent, + }) + } + return out +} + +// mapMortgages 按文档 §5.1 risks.mortgages 子项:regNo, regDate, regOrg, guaranteedAmount, status, publicDate, details, mortgagees, collaterals, debts, alterations, cancellations +func mapMortgages(jiguang map[string]interface{}) []interface{} { + var out []interface{} + for _, v := range sliceOrEmpty(jiguang["MORTGAGEBASIC"]) { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + regNo := str(m["MAB_REGNO"]) + out = append(out, map[string]interface{}{ + "regNo": regNo, + "regDate": str(m["MAB_REG_DATE"]), + "regOrg": str(m["MAB_REG_ORG"]), + "guaranteedAmount": str(m["MAB_GUAR_AMT"]), + "status": str(m["MAB_STATUS"]), + "publicDate": str(m["MAB_GS_DATE"]), + "details": str(m["MAB_DETAILS"]), + "mortgagees": filterByRegNo(sliceOrEmpty(jiguang["MORTGAGEPER"]), regNo, "MAB_REGNO"), + "collaterals": filterByRegNo(sliceOrEmpty(jiguang["MORTGAGEPAWN"]), regNo, "MAB_REGNO"), + "debts": filterByRegNo(sliceOrEmpty(jiguang["MORTGAGEDEBT"]), regNo, "MAB_REGNO"), + "alterations": filterByRegNo(sliceOrEmpty(jiguang["MORTGAGEALT"]), regNo, "MAB_REGNO"), + "cancellations": filterByRegNo(sliceOrEmpty(jiguang["MORTGAGECAN"]), regNo, "MAB_REGNO"), + }) + } + return out +} + +func filterByRegNo(arr []interface{}, regNo, key string) []interface{} { + if regNo == "" { + return arr + } + var out []interface{} + for _, v := range arr { + m, _ := v.(map[string]interface{}) + if m != nil && str(m[key]) == regNo { + out = append(out, v) + } + } + return out +} + +// mapQuickCancel 按文档 §5.1:entName, creditCode, regNo, regOrg, noticeFromDate, noticeToDate, cancelResult, dissents +func mapQuickCancel(jiguang map[string]interface{}) interface{} { + qc, _ := jiguang["QUICKCANCELBASIC"].([]interface{}) + if len(qc) == 0 { + return nil + } + m, _ := qc[0].(map[string]interface{}) + if m == nil { + return nil + } + var dissents []interface{} + for _, v := range sliceOrEmpty(jiguang["QUICKCANCELDISSENT"]) { + d, _ := v.(map[string]interface{}) + if d == nil { + continue + } + dissents = append(dissents, map[string]interface{}{ + "dissentOrg": str(d["DISSENT_ORG"]), + "dissentDes": str(d["DISSENT_DES"]), + "dissentDate": str(d["DISSENT_DATE"]), + }) + } + return map[string]interface{}{ + "entName": str(m["ENTNAME"]), + "creditCode": str(m["CREDITCODE"]), + "regNo": str(m["REGNO"]), + "regOrg": str(m["REGORG"]), + "noticeFromDate": str(m["NOTICE_FROMDATE"]), + "noticeToDate": str(m["NOTICE_TODATE"]), + "cancelResult": str(m["CANCELRESULT"]), + "dissents": dissents, + } +} + +// mapLiquidation 按文档 §5.1:principal, members +func mapLiquidation(jiguang map[string]interface{}) interface{} { + liq, _ := jiguang["LIQUIDATION"].([]interface{}) + if len(liq) == 0 { + return nil + } + m, _ := liq[0].(map[string]interface{}) + if m == nil { + return nil + } + var members []string + switch v := m["LIQMEN"].(type) { + case string: + if v != "" { + members = []string{v} + } + case []interface{}: + for _, item := range v { + members = append(members, str(item)) + } + } + return map[string]interface{}{ + "principal": str(m["LIGPRINCIPAL"]), + "members": members, + } +} + +func mapRisks(jiguang, judicial map[string]interface{}) map[string]interface{} { + exceptions := sliceOrEmpty(jiguang["EXCEPTIONLIST"]) + adminPenalties := sliceOrEmpty(jiguang["ENTPUBCASEINFO"]) + if len(adminPenalties) == 0 { + adminPenalties = sliceOrEmpty(jiguang["ENTCASEBASEINFO"]) + } + taxOwing := sliceOrEmpty(jiguang["TAXOWING"]) + seriousTax := sliceOrEmpty(jiguang["TAXSERIOUSILLEGAL"]) + mortgages := mapMortgages(jiguang) + stockPawn := sliceOrEmpty(jiguang["STOCKPAWN"]) + quickCancel := mapQuickCancel(jiguang) + liquidation := mapLiquidation(jiguang) + + // 司法认证V2:失信、限高、涉诉 + sxbzxr := sliceOrEmpty(judicial["sxbzxr"]) + xgbzxr := sliceOrEmpty(judicial["xgbzxr"]) + litigation := mapLitigation(judicial) + var dishonestDebtors []interface{} + for _, v := range sxbzxr { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + dishonestDebtors = append(dishonestDebtors, map[string]interface{}{ + "id": str(m["id"]), + "obligation": str(m["yw"]), + "judgmentAmountEst": str(m["pjje_gj"]), + "discreditDetail": str(m["xwqx"]), + "execCourt": str(m["zxfy"]), + "caseNo": str(m["ah"]), + "execBasisNo": str(m["zxyjwh"]), + "performanceStatus": str(m["lxqk"]), + "execBasisOrg": str(m["zxyjdw"]), + "publishDate": str(m["fbrq"]), + "gender": str(m["xb"]), + "filingDate": str(m["larq"]), + "province": str(m["sf"]), + }) + } + + judicialCases := sliceOrEmpty(jiguang["JUDLAWSUIT"]) + judicialAids := sliceOrEmpty(jiguang["JUDICIALAID"]) + + riskLevel := "低" + riskScore := 80 + if len(exceptions) > 0 || len(adminPenalties) > 0 || len(taxOwing) > 0 || len(dishonestDebtors) > 0 || len(xgbzxr) > 0 { + riskLevel = "中" + riskScore = 55 + } + if len(adminPenalties) > 2 || len(taxOwing) > 3 || len(dishonestDebtors) > 0 { + riskLevel = "高" + riskScore = 35 + } + + taxRecords := map[string]interface{}{ + "taxLevelAYears": sliceOrEmpty(jiguang["TAXLEVELATAXPAYER"]), + "seriousTaxIllegal": seriousTax, + "taxOwings": taxOwing, + } + + return map[string]interface{}{ + "riskLevel": riskLevel, + "riskScore": riskScore, + "hasCourtJudgments": len(judicialCases) > 0, + "hasJudicialAssists": len(judicialAids) > 0, + "hasDishonestDebtors": len(dishonestDebtors) > 0, + "hasLimitHighDebtors": len(xgbzxr) > 0, + "hasAdminPenalty": len(adminPenalties) > 0, + "hasException": len(exceptions) > 0, + "hasSeriousIllegal": len(sliceOrEmpty(jiguang["BREAKLAW"])) > 0, + "hasTaxOwing": len(taxOwing) > 0, + "hasSeriousTaxIllegal": len(seriousTax) > 0, + "hasMortgage": len(mortgages) > 0, + "hasEquityPledges": len(stockPawn) > 0, + "hasQuickCancel": quickCancel != nil, + "courtJudgments": judicialCases, + "judicialAssists": judicialAids, + "dishonestDebtors": dishonestDebtors, + "dishonestDebtorCount": len(dishonestDebtors), + "limitHighDebtors": xgbzxr, + "limitHighDebtorCount": len(xgbzxr), + "litigation": litigation, + "adminPenalties": adminPenalties, + "adminPenaltyUpdates": mapAdminPenaltyUpdates(jiguang), + "exceptions": exceptions, + "seriousIllegals": sliceOrEmpty(jiguang["BREAKLAW"]), + "mortgages": mortgages, + "quickCancel": quickCancel, // object | null,文档 §5.1 + "liquidation": liquidation, // object | null,文档 §5.1 + "taxRecords": taxRecords, + } +} + +// mapLitigation 将企业司法认证 entout 中的各类涉诉案件按类型规范为统一结构 +// 类型包括:administrative/implement/preservation/civil/criminal/bankrupt/jurisdict/compensate +func mapLitigation(judicial map[string]interface{}) map[string]interface{} { + if judicial == nil { + return nil + } + entout, _ := judicial["entout"].(map[string]interface{}) + if entout == nil { + return nil + } + typeKeys := []string{ + "administrative", + "implement", + "preservation", + "civil", + "criminal", + "bankrupt", + "jurisdict", + "compensate", + } + out := make(map[string]interface{}) + total := 0 + for _, key := range typeKeys { + sec, _ := entout[key].(map[string]interface{}) + if sec == nil { + continue + } + rawCases := sliceOrEmpty(sec["cases"]) + var cases []interface{} + for _, v := range rawCases { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + // 统一的案件结构 + cases = append(cases, map[string]interface{}{ + "caseNo": str(m["c_ah"]), + "court": str(m["n_jbfy"]), + "region": str(m["c_ssdy"]), + "filingDate": str(m["d_larq"]), + "judgmentDate": str(m["d_jarq"]), + "trialLevel": str(m["n_slcx"]), + "caseType": str(m["n_ajlx"]), + "status": str(m["n_ajjzjd"]), + "cause": str(m["n_laay"]), + "amount": str(m["n_qsbdje"]), + "victoryResult": str(m["n_pj_victory"]), + }) + } + if len(cases) == 0 { + continue + } + out[key] = map[string]interface{}{ + "count": len(cases), + "cases": cases, + } + total += len(cases) + } + if total == 0 { + return nil + } + out["totalCases"] = total + return out +} + +func mapTimeline(jiguang map[string]interface{}) []interface{} { + alter := sliceOrEmpty(jiguang["ALTER"]) + var out []interface{} + for _, v := range alter { + m, _ := v.(map[string]interface{}) + if m == nil { + continue + } + out = append(out, map[string]interface{}{ + "date": str(m["ALTDATE"]), + "type": str(m["ALTITEM"]), + "title": str(m["ZSALTITEM"]), + "detailBefore": str(m["ALTBE"]), + "detailAfter": str(m["ALTAF"]), + "source": "工商变更", + }) + } + return out +} + +func mapListed(jiguang map[string]interface{}) interface{} { + listedInfo := sliceOrEmpty(jiguang["LISTEDINFO"]) + listedComp := sliceOrEmpty(jiguang["LISTEDCOMPINFO"]) + if len(listedInfo) == 0 && len(listedComp) == 0 { + return nil + } + var company map[string]interface{} + if c, _ := first(listedComp).(map[string]interface{}); c != nil { + company = map[string]interface{}{ + "bizScope": str(c["BIZSCOPE"]), + "creditCode": str(c["CREDITCODE"]), + "regAddr": str(c["REGADDR"]), + "regCapital": str(c["REGCAPITAL"]), + "orgCode": str(c["ORGCODE"]), + "cur": str(c["CUR"]), + "curName": str(c["CURNAME"]), + } + } + return map[string]interface{}{ + "isListed": true, + "company": company, + "stock": first(listedInfo), + "topShareholders": sliceOrEmpty(jiguang["LISTEDSHAREHOLDER"]), + "listedManagers": sliceOrEmpty(jiguang["LISTEDMANAGER"]), + } +} + +// mapRiskOverview 基于已汇总的报告结构(而不是底层接口原始字段)生成「风险情况」模块 +// 仅依赖 report["risks"]、report["shareholding"] 等聚合结果,便于后续由企业报告.json 直接使用 +func mapRiskOverview(report map[string]interface{}, risks map[string]interface{}) map[string]interface{} { + tags := []string{} + items := []map[string]interface{}{} + + addItem := func(name string, hit bool) { + items = append(items, map[string]interface{}{ + "name": name, + "hit": hit, + }) + } + + // 司法相关 + hasCourt := getBool(risks, "hasCourtJudgments") + addItem("裁判文书记录", hasCourt) + if hasCourt { + tags = append(tags, "存在裁判文书记录") + } + + hasAssist := getBool(risks, "hasJudicialAssists") + addItem("司法协助记录", hasAssist) + if hasAssist { + tags = append(tags, "存在司法协助记录") + } + + hasDishonest := getBool(risks, "hasDishonestDebtors") + addItem("失信被执行人", hasDishonest) + if hasDishonest { + tags = append(tags, "存在失信被执行人") + } + + hasLimitHigh := getBool(risks, "hasLimitHighDebtors") + addItem("限高被执行人", hasLimitHigh) + if hasLimitHigh { + tags = append(tags, "存在限高被执行人") + } + + // 行政与合规 + hasAdmin := getBool(risks, "hasAdminPenalty") + addItem("行政处罚", hasAdmin) + if hasAdmin { + tags = append(tags, "存在行政处罚记录") + } + + hasException := getBool(risks, "hasException") + addItem("经营异常名录", hasException) + if hasException { + tags = append(tags, "曾列入经营异常名录") + } + + hasSeriousIllegal := getBool(risks, "hasSeriousIllegal") + addItem("严重违法", hasSeriousIllegal) + if hasSeriousIllegal { + tags = append(tags, "存在严重违法记录") + } + + // 税务 + hasTaxOwing := getBool(risks, "hasTaxOwing") + addItem("欠税记录", hasTaxOwing) + if hasTaxOwing { + tags = append(tags, "存在欠税记录") + } + + hasSeriousTax := getBool(risks, "hasSeriousTaxIllegal") + addItem("重大税收违法案件", hasSeriousTax) + if hasSeriousTax { + tags = append(tags, "存在重大税收违法案件") + } + + // 资产与担保 + hasMortgage := getBool(risks, "hasMortgage") + addItem("动产抵押", hasMortgage) + if hasMortgage { + tags = append(tags, "存在动产抵押") + } + + hasEquityPledge := getBool(risks, "hasEquityPledges") + addItem("股权出质", hasEquityPledge) + if hasEquityPledge { + tags = append(tags, "存在股权出质") + } + + // 股权结构风险:基于 shareholding 汇总 + var shareholding map[string]interface{} + if s, ok := report["shareholding"].(map[string]interface{}); ok { + shareholding = s + } + if shareholding != nil { + if n, _ := shareholding["shareholderCount"].(int); n > 0 { + isConcentrated := n <= 3 + addItem("股东人数偏少(股权集中)", isConcentrated) + if isConcentrated { + tags = append(tags, "股权结构偏集中") + } + } + } + + // 基于各类风险标志重新综合计算风险得分(0-100),而不是直接沿用底层接口评分 + score := 100 + penalize := func(cond bool, pts int) { + if cond { + score -= pts + } + } + + penalize(getBool(risks, "hasDishonestDebtors"), 30) + penalize(getBool(risks, "hasLimitHighDebtors"), 20) + penalize(getBool(risks, "hasSeriousIllegal"), 25) + penalize(getBool(risks, "hasSeriousTaxIllegal"), 20) + penalize(getBool(risks, "hasAdminPenalty"), 15) + penalize(getBool(risks, "hasException"), 10) + penalize(getBool(risks, "hasTaxOwing"), 10) + penalize(getBool(risks, "hasCourtJudgments"), 10) + penalize(getBool(risks, "hasJudicialAssists"), 5) + penalize(getBool(risks, "hasMortgage"), 5) + penalize(getBool(risks, "hasEquityPledges"), 5) + + if shareholding != nil { + if n, _ := shareholding["shareholderCount"].(int); n > 0 && n <= 3 { + penalize(true, 5) + } + } + + if score < 0 { + score = 0 + } + if score > 100 { + score = 100 + } + + level := "低" + switch { + case score < 60: + level = "高" + case score < 80: + level = "中" + } + + return map[string]interface{}{ + "riskLevel": level, + "riskScore": score, + "tags": tags, + "items": items, + } +} + +// getBool 安全读取 map 中的布尔字段 +func getBool(m map[string]interface{}, key string) bool { + if m == nil { + return false + } + b, ok := m[key].(bool) + return ok && b +} + +func first(arr []interface{}) interface{} { + if len(arr) > 0 { + return arr[0] + } + return nil +} + +func sliceOrEmpty(v interface{}) []interface{} { + if v == nil { + return []interface{}{} + } + arr, ok := v.([]interface{}) + if !ok { + return []interface{}{} + } + return arr +} + +// knownCamelSuffixes 全大写键转驼峰时识别的后缀(按长度降序,优先长匹配) +var knownCamelSuffixes = []struct { + lower string + camel string +}{ + {"controller", "Controller"}, {"number", "Number"}, {"amount", "Amount"}, {"before", "Before"}, {"after", "After"}, + {"person", "Person"}, {"percent", "Percent"}, {"period", "Period"}, {"reason", "Reason"}, {"invalid", "Invalid"}, + {"status", "Status"}, {"source", "Source"}, {"target", "Target"}, {"detail", "Detail"}, {"alter", "Alter"}, + {"name", "Name"}, {"date", "Date"}, {"type", "Type"}, {"code", "Code"}, {"list", "List"}, {"basic", "Basic"}, {"item", "Item"}, + {"info", "Info"}, {"desc", "Desc"}, {"time", "Time"}, {"from", "From"}, {"org", "Org"}, {"upd", "Upd"}, + {"no", "No"}, {"id", "Id"}, {"cn", "Cn"}, {"en", "En"}, {"cur", "Cur"}, {"be", "Be"}, {"af", "Af"}, {"to", "To"}, +} + +// toCamelKey 将键名转为小写驼峰:下划线拆分、全大写按已知后缀转驼峰、全小写已知键补全 +func toCamelKey(key string) string { + if key == "" { + return key + } + // 含下划线:按 _ 拆分,每段首字母大写(首段小写) + if strings.Contains(key, "_") { + parts := strings.Split(key, "_") + for i, p := range parts { + p = strings.ToLower(p) + if p == "" { + continue + } + if i > 0 && len(p) > 0 { + parts[i] = strings.ToUpper(p[:1]) + strings.ToLower(p[1:]) + } else { + parts[i] = p + } + } + return strings.Join(parts, "") + } + // 全大写:先整键转小写,再按已知后缀转驼峰(如 ENTNAME->entName, ALTBE->altBe) + if key == strings.ToUpper(key) { + lower := strings.ToLower(key) + for _, s := range knownCamelSuffixes { + if len(lower) > len(s.lower) && strings.HasSuffix(lower, s.lower) { + base := lower[:len(lower)-len(s.lower)] + if len(base) > 0 { + return base + s.camel + } + } + } + return strings.ToLower(key) + } + // 全小写但应为驼峰的常见键 + if key == strings.ToLower(key) && len(key) > 2 { + switch key { + case "iskey": + return "isKey" + case "ishistory": + return "isHistory" + case "hasproblem": + return "hasProblem" + case "shortname": + return "shortName" + case "shtype": + return "shType" + case "entityid": + return "entityId" + } + } + return key +} + +// convertReportKeysToCamel 递归将 map 中所有键转为驼峰 +func convertReportKeysToCamel(v interface{}, root bool) interface{} { + switch m := v.(type) { + case map[string]interface{}: + out := make(map[string]interface{}, len(m)) + for k, val := range m { + newK := toCamelKey(k) + out[newK] = convertReportKeysToCamel(val, false) + } + return out + case []interface{}: + out := make([]interface{}, len(m)) + for i, item := range m { + out[i] = convertReportKeysToCamel(item, false) + } + return out + default: + return v + } +} + +func str(v interface{}) string { + if v == nil { + return "" + } + switch s := v.(type) { + case string: + return s + case float64: + return strconv.FormatFloat(s, 'f', -1, 64) + case int: + return strconv.Itoa(s) + default: + return fmt.Sprint(v) + } +} + +func num(v interface{}) float64 { + if v == nil { + return 0 + } + switch n := v.(type) { + case float64: + return n + case int: + return float64(n) + case string: + f, _ := strconv.ParseFloat(strings.TrimSpace(n), 64) + return f + default: + return 0 + } +} diff --git a/internal/infrastructure/database/repositories/api/gorm_enterprise_report_repository.go b/internal/infrastructure/database/repositories/api/gorm_enterprise_report_repository.go new file mode 100644 index 0000000..5be10d6 --- /dev/null +++ b/internal/infrastructure/database/repositories/api/gorm_enterprise_report_repository.go @@ -0,0 +1,45 @@ +package api + +import ( + "context" + + "tyapi-server/internal/domains/api/entities" + "tyapi-server/internal/domains/api/repositories" + "tyapi-server/internal/shared/database" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +const ( + ReportsTable = "reports" +) + +// GormReportRepository 报告记录 GORM 仓储实现 +type GormReportRepository struct { + *database.BaseRepositoryImpl +} + +var _ repositories.ReportRepository = (*GormReportRepository)(nil) + +// NewGormReportRepository 创建报告记录仓储实现 +func NewGormReportRepository(db *gorm.DB, logger *zap.Logger) repositories.ReportRepository { + return &GormReportRepository{ + BaseRepositoryImpl: database.NewBaseRepositoryImpl(db, logger), + } +} + +// Create 创建报告记录 +func (r *GormReportRepository) Create(ctx context.Context, report *entities.Report) error { + return r.CreateEntity(ctx, report) +} + +// FindByReportID 根据报告编号查询记录 +func (r *GormReportRepository) FindByReportID(ctx context.Context, reportID string) (*entities.Report, error) { + var report entities.Report + if err := r.FindOneByField(ctx, &report, "report_id", reportID); err != nil { + return nil, err + } + return &report, nil +} + diff --git a/internal/infrastructure/http/handlers/qygl_report_handler.go b/internal/infrastructure/http/handlers/qygl_report_handler.go new file mode 100644 index 0000000..177e2a3 --- /dev/null +++ b/internal/infrastructure/http/handlers/qygl_report_handler.go @@ -0,0 +1,137 @@ +package handlers + +import ( + "encoding/json" + "html/template" + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "tyapi-server/internal/application/api/commands" + "tyapi-server/internal/domains/api/dto" + api_repositories "tyapi-server/internal/domains/api/repositories" + api_services "tyapi-server/internal/domains/api/services" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/domains/api/services/processors/qygl" +) + +// QYGLReportHandler 企业全景报告页面渲染处理器 +// 使用 QYGLJ1U9 聚合接口生成企业报告数据,并通过模板引擎渲染 qiye.html +type QYGLReportHandler struct { + apiRequestService *api_services.ApiRequestService + logger *zap.Logger + + reportRepo api_repositories.ReportRepository +} + +// NewQYGLReportHandler 创建企业报告页面处理器 +func NewQYGLReportHandler( + apiRequestService *api_services.ApiRequestService, + logger *zap.Logger, + reportRepo api_repositories.ReportRepository, +) *QYGLReportHandler { + return &QYGLReportHandler{ + apiRequestService: apiRequestService, + logger: logger, + reportRepo: reportRepo, + } +} + +// GetQYGLReportPage 企业全景报告页面 +// GET /reports/qygl?ent_code=xxx&ent_name=yyy&ent_reg_no=zzz +func (h *QYGLReportHandler) GetQYGLReportPage(c *gin.Context) { + // 读取查询参数 + entCode := c.Query("ent_code") + entName := c.Query("ent_name") + entRegNo := c.Query("ent_reg_no") + + // 组装 QYGLUY3S 入参 + req := dto.QYGLUY3SReq{ + EntName: entName, + EntRegno: entRegNo, + EntCode: entCode, + } + + params, err := json.Marshal(req) + if err != nil { + h.logger.Error("序列化企业全景报告入参失败", zap.Error(err)) + c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试") + return + } + + // 通过 ApiRequestService 调用 QYGLJ1U9 聚合处理器 + options := &commands.ApiCallOptions{} + callCtx := &processors.CallContext{} + + ctx := c.Request.Context() + respBytes, err := h.apiRequestService.PreprocessRequestApi(ctx, "QYGLJ1U9", params, options, callCtx) + if err != nil { + h.logger.Error("调用企业全景报告处理器失败", zap.Error(err)) + c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试") + return + } + + var report map[string]interface{} + if err := json.Unmarshal(respBytes, &report); err != nil { + h.logger.Error("解析企业全景报告结果失败", zap.Error(err)) + c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试") + return + } + + reportJSONBytes, err := json.Marshal(report) + if err != nil { + h.logger.Error("序列化企业全景报告结果失败", zap.Error(err)) + c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试") + return + } + + // 使用 template.JS 避免在脚本中被转义,直接作为 JS 对象字面量注入 + reportJSON := template.JS(reportJSONBytes) + + c.HTML(http.StatusOK, "qiye.html", gin.H{ + "ReportJSON": reportJSON, + }) +} + +// GetQYGLReportPageByID 通过编号查看企业全景报告页面 +// GET /reports/qygl/:id +func (h *QYGLReportHandler) GetQYGLReportPageByID(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.String(http.StatusBadRequest, "报告编号不能为空") + return + } + + // 优先从数据库中查询报告记录 + if h.reportRepo != nil { + if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil { + reportJSON := template.JS(entity.ReportData) + c.HTML(http.StatusOK, "qiye.html", gin.H{ + "ReportJSON": reportJSON, + }) + return + } + } + + // 回退到进程内存缓存(兼容老的访问方式) + report, ok := qygl.GetQYGLReport(id) + if !ok { + c.String(http.StatusNotFound, "报告不存在或已过期") + return + } + + reportJSONBytes, err := json.Marshal(report) + if err != nil { + h.logger.Error("序列化企业全景报告结果失败", zap.Error(err)) + c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后再试") + return + } + + reportJSON := template.JS(reportJSONBytes) + + c.HTML(http.StatusOK, "qiye.html", gin.H{ + "ReportJSON": reportJSON, + }) +} + diff --git a/internal/infrastructure/http/routes/qygl_report_routes.go b/internal/infrastructure/http/routes/qygl_report_routes.go new file mode 100644 index 0000000..fa4512f --- /dev/null +++ b/internal/infrastructure/http/routes/qygl_report_routes.go @@ -0,0 +1,32 @@ +package routes + +import ( + "tyapi-server/internal/infrastructure/http/handlers" + sharedhttp "tyapi-server/internal/shared/http" +) + +// QYGLReportRoutes 企业报告页面路由注册器 +type QYGLReportRoutes struct { + handler *handlers.QYGLReportHandler +} + +// NewQYGLReportRoutes 创建企业报告页面路由注册器 +func NewQYGLReportRoutes( + handler *handlers.QYGLReportHandler, +) *QYGLReportRoutes { + return &QYGLReportRoutes{ + handler: handler, + } +} + +// Register 注册企业报告页面路由 +func (r *QYGLReportRoutes) Register(router *sharedhttp.GinRouter) { + engine := router.GetEngine() + + // 企业全景报告页面(实时生成) + engine.GET("/reports/qygl", r.handler.GetQYGLReportPage) + + // 企业全景报告页面(通过编号查看) + engine.GET("/reports/qygl/:id", r.handler.GetQYGLReportPageByID) +} + diff --git a/internal/shared/http/router.go b/internal/shared/http/router.go index 7fbe543..26367ac 100644 --- a/internal/shared/http/router.go +++ b/internal/shared/http/router.go @@ -37,6 +37,10 @@ func NewGinRouter(cfg *config.Config, logger *zap.Logger) *GinRouter { // 创建Gin引擎 engine := gin.New() + // 加载HTML模板(企业报告等页面) + // 这里直接加载项目根目录下的 qiye.html,后续如有更多模板可改为 LoadHTMLGlob + engine.LoadHTMLFiles("qiye.html") + return &GinRouter{ engine: engine, config: cfg,