Compare commits
25 Commits
019e47896d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c5970da195 | |||
| 1bcb4a9c2e | |||
| 8877cf9691 | |||
| f63e6df9f9 | |||
| a00fe12141 | |||
| 6b80182986 | |||
| bf4c114ee2 | |||
| 4cd3954574 | |||
| d7a5589873 | |||
| b0ec75d1af | |||
| 57d18be972 | |||
| 3d8775b6dc | |||
| f40950f890 | |||
| ba21a8f965 | |||
| 7d2716da7a | |||
| 9a7bda9527 | |||
| abdae033f0 | |||
| 96abacd392 | |||
| 4e6c93413e | |||
| 9c8dbd458f | |||
| 9e9cee02f5 | |||
| 360bd579ce | |||
| db889ccba0 | |||
| 25a4961328 | |||
| 578e68a76b |
@@ -53,9 +53,8 @@ COPY --from=builder /app/tyapi-server .
|
||||
COPY config.yaml .
|
||||
COPY configs/ ./configs/
|
||||
|
||||
# 复制资源文件(直接从构建上下文复制,与配置文件一致)
|
||||
COPY resources/etc ./resources/etc
|
||||
COPY resources/pdf ./resources/pdf
|
||||
# 复制资源文件(报告模板、PDF、组件等)
|
||||
COPY resources ./resources
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -89,7 +89,8 @@ services:
|
||||
- "25000:8080"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./resources/Pure_Component:/app/resources/Pure_Component
|
||||
# 挂载完整 resources 目录(包含 qiye.html、Pure_Component、pdf 等)
|
||||
- ./resources:/app/resources
|
||||
# 持久化PDF缓存目录,确保生成的PDF在容器重启后仍然存在
|
||||
- ./storage/pdfg-cache:/app/storage/pdfg-cache
|
||||
# user: "1001:1001" # 注释掉,使用root权限运行
|
||||
|
||||
@@ -260,6 +260,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
// api
|
||||
&apiEntities.ApiUser{},
|
||||
&apiEntities.ApiCall{},
|
||||
&apiEntities.Report{},
|
||||
|
||||
// 任务域
|
||||
&taskEntities.AsyncTask{},
|
||||
|
||||
@@ -609,8 +609,6 @@ func (s *ApiApplicationServiceImpl) GetUserApiCalls(ctx context.Context, userID
|
||||
// 转换为响应DTO
|
||||
var items []dto.ApiCallRecordResponse
|
||||
for _, call := range calls {
|
||||
// 出于安全考虑,不再在数据库中存储或解密真实请求参数
|
||||
// 这里只保留数据库中的原始占位值(通常为空字符串)
|
||||
requestParamsStr := call.RequestParams
|
||||
|
||||
item := dto.ApiCallRecordResponse{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -118,7 +118,10 @@ type QYGLUY3SReq struct {
|
||||
EntRegno string `json:"ent_reg_no" validate:"omitempty"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
}
|
||||
|
||||
type QYGLJ1U9Req struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
}
|
||||
type JRZQOCRYReq struct {
|
||||
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
|
||||
}
|
||||
@@ -133,6 +136,10 @@ type YYSYK9R4Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type YYSY35TAReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type QCXG9F5CReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
@@ -416,7 +423,7 @@ type COMENT01Req struct {
|
||||
}
|
||||
|
||||
type JRZQ09J8Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"omitempty,min=11,max=11,validMobileNo"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
|
||||
@@ -80,7 +80,7 @@ func NewApiCall(accessId, requestParams, clientIp string) (*ApiCall, error) {
|
||||
AccessId: accessId,
|
||||
TransactionId: GenerateTransactionID(),
|
||||
ClientIp: clientIp,
|
||||
RequestParams: "",
|
||||
RequestParams: requestParams,
|
||||
Status: ApiCallStatusPending,
|
||||
StartAt: time.Now(),
|
||||
}, nil
|
||||
|
||||
35
internal/domains/api/entities/enterprise_report.go
Normal file
35
internal/domains/api/entities/enterprise_report.go
Normal file
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,8 +233,10 @@ 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, //运营商归属地数卖
|
||||
|
||||
// YYSY系列处理器
|
||||
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
||||
|
||||
@@ -176,6 +176,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"QCXG8A3D": &dto.QCXG8A3DReq{},
|
||||
"QCXG6B4E": &dto.QCXG6B4EReq{},
|
||||
"QYGL2B5C": &dto.QYGL2B5CReq{},
|
||||
"QYGLJ1U9": &dto.QYGLJ1U9Req{},
|
||||
"JRZQ2F8A": &dto.JRZQ2F8AReq{},
|
||||
"JRZQ1E7B": &dto.JRZQ1E7BReq{},
|
||||
"JRZQ3C9R": &dto.JRZQ3C9RReq{},
|
||||
@@ -261,6 +262,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"QYGLUY3S": &dto.QYGLUY3SReq{}, //企业全量信息核验V2 可用
|
||||
"JRZQOCRE": &dto.JRZQOCREReq{}, //银行卡OCR数卖
|
||||
"JRZQOCRY": &dto.JRZQOCRYReq{}, //银行卡OCR数据宝
|
||||
"YYSY35TA": &dto.YYSY35TAReq{}, //运营商归属地数卖
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4011,8 +4011,10 @@ func calculateAgeAndSex(idCard string) (int, string) {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 计算年龄(简化处理,使用当前年份)
|
||||
age := 2024 - year
|
||||
age := now.Year() - year
|
||||
|
||||
// 提取性别(第17位,奇数为男,偶数为女)
|
||||
sexCode := idCard[16:17]
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
# 谛听多维报告(DWBG8B4D)风险标识判断标准
|
||||
|
||||
本文档汇总 `dwbg8b4d_processor.go` 中所有风险标识(*Flag / riskFlag)的判定规则与数据来源,便于查阅和评审。
|
||||
|
||||
---
|
||||
|
||||
## 一、elementVerificationDetail(要素核验产品)内风险标识
|
||||
|
||||
### 1. sjsysFlag(手机三要素简版风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 YYSYH6D2 数据或无法解析 |
|
||||
| 1 | 高风险 | YYSYH6D2.result ≠ "0"(身份证+手机号+姓名 不一致) |
|
||||
| 2 | 低风险 | YYSYH6D2.result == "0"(三要素一致) |
|
||||
|
||||
**数据源**:YYSYH6D2(手机三要素简版)
|
||||
|
||||
---
|
||||
|
||||
### 2. sfzeysFlag(身份证二要素风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 IVYZ9K7F 数据或 status/desc/result 无法解析 |
|
||||
| 1 | 高风险 | 核验结果不为「一致」 |
|
||||
| 2 | 低风险 | 核验结果为「一致」 |
|
||||
|
||||
**数据源**:IVYZ9K7F(身份证二要素)。status 来自 data.status、desc 或 result(0=一致,非0=不一致)。
|
||||
|
||||
---
|
||||
|
||||
### 3. onlineRiskFlag(手机在网时长风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 YYSY8B1C 数据或 inTime 为空 |
|
||||
| 1 | 高风险 | inTime == "0" 或 "3"(在网时长极短) |
|
||||
| 2 | 低风险 | inTime 为其他非空值 |
|
||||
|
||||
**数据源**:YYSY8B1C(手机在网时长)
|
||||
|
||||
---
|
||||
|
||||
### 4. phoneVailRiskFlag(手机信息验证风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 YYSYE7V5 数据或 status 无法解析 |
|
||||
| 1 | 高风险 | status == 1(不在网) |
|
||||
| 2 | 低风险 | status == 0(在网/实号) |
|
||||
|
||||
**数据源**:YYSYE7V5(手机在网状态等)
|
||||
|
||||
---
|
||||
|
||||
### 5. belongRiskFlag(身份证号与手机号归属地风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 YYSY9E4A 或 YYSYH6D2,或户籍/归属地任一方为空 |
|
||||
| 1 | 高风险 | 户籍所在地与号码归属地**不一致**(经智能比较) |
|
||||
| 2 | 低风险 | 户籍所在地与号码归属地**一致** |
|
||||
|
||||
**数据源**:YYSY9E4A(号码归属地)、YYSYH6D2(address 作户籍)。使用 `compareLocation` 做省/市归一化后比较。
|
||||
|
||||
---
|
||||
|
||||
### 6. highRiskFlag(公安重点人员核验风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 无风险 | keyPersonCheckList 五项(fontFlag、jingJiFontFlag、fangAiFlag、zhongDianFlag、sheJiaoTongFlag)**均为 0** |
|
||||
| 1 | 高风险 | **任一项** fontFlag / jingJiFontFlag / fangAiFlag / zhongDianFlag 为 1(涉刑/经侦/妨害/重点/涉交通以外) |
|
||||
| 2 | 低风险 | **仅** sheJiaoTongFlag 为 1(仅涉交通命中),其余为 0 |
|
||||
|
||||
**数据源**:FLXGDEA9 的 level 解析后填充 keyPersonCheckList(A→fontFlag, B→jingJiFontFlag, C→fangAiFlag, D→zhongDianFlag, E→sheJiaoTongFlag)。**不看是否有数据**,仅根据五项当前值判定。
|
||||
|
||||
---
|
||||
|
||||
## 二、overdueRiskProduct(逾期风险产品)内风险标识
|
||||
|
||||
### 7. lyjlhyFlag(履约/借贷逾期类风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 JRZQ5E9F 数据(默认) |
|
||||
| 1 | 高风险 | 当前存在未结清逾期 **或** 近 7/14/30 天有逾期(hasUnsettledOverdue=="逾期" 或 overdueLast7Days/14Days/30Days 任一为「逾期」) |
|
||||
| 2 | 低风险 | 有 JRZQ5E9F 数据且无上述逾期情况 |
|
||||
|
||||
**数据源**:JRZQ5E9F(借选指数评估),字段:xyp_cpl0044、xyp_cpl0029、xyp_cpl0030、xyp_cpl0031 等。
|
||||
|
||||
---
|
||||
|
||||
### 8. dkzhktjFlag(贷款综合情况风险标识)
|
||||
|
||||
与 **lyjlhyFlag** 使用同一套逻辑,同时赋值:有逾期或近期逾期则为 1,否则为 2。
|
||||
|
||||
**数据源**:JRZQ5E9F。
|
||||
|
||||
---
|
||||
|
||||
### 9. tsmdyzFlag(特殊名单验证风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 JRZQ8A2D 数据(默认) |
|
||||
| 1 | 高风险 | specialListVerification 列表**非空**(身份证或手机号任一侧命中:法院失信人/被执行人、银行/非银中风险/一般风险/高风险等任一) |
|
||||
| 2 | 低风险 | 有 JRZQ8A2D 数据且 specialListVerification 为空 |
|
||||
|
||||
**数据源**:JRZQ8A2D(特殊名单验证 B)。命中规则:id/cell 下对应字段值为 "0" 表示命中(如 court_bad、court_executed、bank_lost 等)。
|
||||
|
||||
---
|
||||
|
||||
## 三、multCourtInfo(司法风险核验产品)内风险标识
|
||||
|
||||
### 10. legalCasesFlag(涉案公告案件风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 无 | 无 FLXG7E8F 或 lawsuitStat 下无民事/刑事/行政/保全/破产案件 |
|
||||
| 1 | 高风险 | legalCases 列表**非空**(民事、刑事、行政、保全、破产任一类 cases 非空) |
|
||||
|
||||
**数据源**:FLXG7E8F(个人司法涉诉查询)→ judicial_data.lawsuitStat → civil/criminal/administrative/preservation/bankrupt.cases。
|
||||
|
||||
---
|
||||
|
||||
### 11. executionCasesFlag(执行案件风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 无 | 无 FLXG7E8F 或 implement.cases 为空 |
|
||||
| 1 | 高风险 | executionCases 列表**非空** |
|
||||
|
||||
**数据源**:FLXG7E8F → judicial_data.lawsuitStat.implement.cases。
|
||||
|
||||
---
|
||||
|
||||
### 12. disinCasesFlag(失信案件风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 无 | 无 FLXG7E8F 或 breachCaseList 为空 |
|
||||
| 1 | 高风险 | disinCases 列表**非空** |
|
||||
|
||||
**数据源**:FLXG7E8F → judicial_data.breachCaseList。
|
||||
|
||||
---
|
||||
|
||||
### 13. limitCasesFlag(限高案件风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 无 | 无 FLXG7E8F 或 consumptionRestrictionList 为空 |
|
||||
| 1 | 高风险 | limitCases 列表**非空** |
|
||||
|
||||
**数据源**:FLXG7E8F → judicial_data.consumptionRestrictionList。
|
||||
|
||||
---
|
||||
|
||||
## 四、loanEvaluationVerificationDetail(借贷评估产品)内风险标识
|
||||
|
||||
### 14. riskFlag(借贷评估风险标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 未查得 | 无 JRZQ6F2A 或 risk_screen_v2.variables[0].variableValue 无法解析 |
|
||||
| 1 | 高风险 | checkLoanRisk 为 true:**任一时间段**(d7/d15/m1/m3)下,银行或非银申请次数 **≥ 10**(als_{period}_id_bank_allnum / als_{period}_id_nbank_allnum) |
|
||||
| 2 | 低风险 | 有数据且 checkLoanRisk 为 false |
|
||||
|
||||
**数据源**:JRZQ6F2A(借贷意向验证 A)→ risk_screen_v2.variables[0].variableValue。
|
||||
|
||||
---
|
||||
|
||||
## 五、leasingRiskAssessment(租赁风险评估产品)内风险标识
|
||||
|
||||
### 15. riskFlag(租赁风险评估标识)
|
||||
|
||||
| 取值 | 含义 | 判断条件 |
|
||||
|------|--------|----------|
|
||||
| 0 | 无风险 | 近 12 月(m12)**总次数为 0**(身份证维度 alc_m12_id_allnum 与手机号维度 alc_m12_cell_allnum 取**较大值**后为 0) |
|
||||
| 1 | 高风险 | 近 12 月总次数 **> 10** |
|
||||
| 2 | 低风险 | 近 12 月总次数 **1~10**(含 10) |
|
||||
|
||||
**数据源**:JRZQ1D09(3C 租赁申请意向)。**仅用近 12 月(Last12 / m12)**,totalCount = max(idCount, cellCount)。
|
||||
|
||||
---
|
||||
|
||||
## 六、riskWarning(规则风险提示)内各项命中规则摘要
|
||||
|
||||
以下为「命中即置 1」的规则,未命中或未查得均为 0。不改变上述各产品内 *Flag 的取值含义。
|
||||
|
||||
| 类别 | 字段示例 | 命中条件(简要) |
|
||||
|----------------|----------|------------------|
|
||||
| 要素核查 | idCardTwoElementMismatch | IVYZ9K7F 二要素结果非「一致」 |
|
||||
| | phoneThreeElementMismatch | YYSYH6D2.result ≠ "0" |
|
||||
| 运营商/在网 | shortPhoneDuration | YYSY8B1C.inTime == "0" |
|
||||
| | shortPhoneDurationSlight | YYSY8B1C.inTime == "3" |
|
||||
| | noPhoneDuration | YYSYE7V5.status==1 且 desc 含「风险」 |
|
||||
| 归属地 | idCardPhoneProvinceMismatch | 户籍与号码归属地智能比较不一致 |
|
||||
| 公安重点人员 | hasCriminalRecord / isEconomyFront / isDisrupSocial / isKeyPerson / isTrafficRelated | FLXGDEA9.level 解析后 A/B/C/D/E 前缀 |
|
||||
| 涉赌涉诈 | isAntiFraudInfo | FLXG8B4D 中 moneyLaundering/deceiver/gamblerPlayer/gamblerBanker 任一项非空且非"0" |
|
||||
| 逾期/名单 | hitHighRiskBankLastTwoYears / hitHighRiskNonBankLastTwoYears / hitCurrentOverdue | JRZQ8A2D.id 下 bank_lost/nbank_lost/bank_overdue/nbank_overdue 等字段值为 "0"(命中) |
|
||||
| 司法 | hitCivilCase / hitCriminalRisk / hitExecutionCase / hitAdministrativeCase / hitPreservationReview / hitBankruptcyAndLiquidation | FLXG7E8F 对应案件类型 **cases 数组非空**(不依 count) |
|
||||
| 借贷意向 | frequentApplicationRecent | 近 d7/d15/m1 银行或非银申请次数 **≥ 10**(JRZQ6F2A) |
|
||||
| | frequentBankApplications / moreFrequentBankApplications | 近 m6+m12 银行申请总次数 **≥ 20** / **≥ 15** |
|
||||
| | frequentNonBankApplications / moreFrequentNonBankApplications | 近 m6+m12 非银申请总次数 **≥ 20** / **≥ 15** |
|
||||
| 租赁申请 | veryFrequentRentalApplications / frequentRentalApplications | JRZQ1D09 近 m3+m6+m12(每期取 id/cell 较大值再累加)总次数 **≥ 20** / **≥ 15** |
|
||||
| 偿债压力 | highDebtPressure | JRZQ5E9F 当前逾期金额或当前逾期机构数非空且非 "0"/"1" |
|
||||
|
||||
---
|
||||
|
||||
## 七、风险取值约定(通用)
|
||||
|
||||
- **0**:未查得 / 无风险 / 无命中(依字段语义)
|
||||
- **1**:高风险 / 命中
|
||||
- **2**:低风险(仅部分 *Flag 使用,如 sjsysFlag、sfzeysFlag、onlineRiskFlag、phoneVailRiskFlag、belongRiskFlag、highRiskFlag、lyjlhyFlag、dkzhktjFlag、tsmdyzFlag、loanEvaluation riskFlag、leasingRiskAssessment riskFlag)
|
||||
|
||||
文档版本:根据当前 `dwbg8b4d_processor.go` 逻辑整理,若有代码变更请同步更新本文档。
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessIVYZOCR2Request IVYZOCR2 OCR识别API处理方法
|
||||
// ProcessIVYZOCR2Request IVYZOCR2 OCR识别API处理方法数卖
|
||||
func ProcessIVYZOCR2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZOCR1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -20,7 +20,7 @@ func ProcessIVYZOCR2Request(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
|
||||
if paramsDto.PhotoData == "" && paramsDto.ImageUrl == "" {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("photo_data or image_url is required"))
|
||||
}
|
||||
|
||||
@@ -51,7 +51,13 @@ func ProcessQYGLJ0Q1Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
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.QYGLJ1U9Req
|
||||
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_code": p.EntCode,
|
||||
}, ProcessQYGLUY3SRequest)
|
||||
|
||||
// 企业股权结构全景(QYGLJ0Q1)
|
||||
call("equityPanorama", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
}, 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 "https://api.tianyuanapi.com/reports/qygl/" + url.PathEscape(id)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
sharedvalidator "tyapi-server/internal/shared/validator"
|
||||
)
|
||||
|
||||
// TestQYGLJ1U9Req_ValidateParams 仅验证 QYGLJ1U9 入参的校验规则(特别是 validUSCI)。
|
||||
func TestQYGLJ1U9Req_ValidateParams(t *testing.T) {
|
||||
// 使用全局业务校验器
|
||||
bv := sharedvalidator.NewBusinessValidator()
|
||||
|
||||
t.Run("invalid_usci_should_fail", func(t *testing.T) {
|
||||
req := dto.QYGLJ1U9Req{
|
||||
EntName: "测试企业有限公司",
|
||||
EntCode: "123", // 明显不符合 validUSCI
|
||||
}
|
||||
if err := bv.ValidateStruct(req); err == nil {
|
||||
t.Fatalf("expected validation error for invalid ent_code, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid_usci_should_pass", func(t *testing.T) {
|
||||
req := dto.QYGLJ1U9Req{
|
||||
EntName: "杭州娃哈哈集团有限公司",
|
||||
EntCode: "91330000142916567N", // 符合 validUSCI 正则的示例
|
||||
}
|
||||
if err := bv.ValidateStruct(req); err != nil {
|
||||
t.Fatalf("expected no validation error for valid ent_code, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +42,13 @@ func ProcessQYGLUY3SRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package yysy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessYYSY35TARequest YYSY35TA API 运营商归属地数卖处理方法数脉
|
||||
func ProcessYYSY35TARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSY35TAReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/phone/number" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
// 查无记录情况
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -39,6 +39,14 @@ func ProcessYYSY9E4ARequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容上游有时返回 JSON 字符串的情况:如果是字符串则尝试再反序列化一次
|
||||
if str, ok := respData.(string); ok && str != "" {
|
||||
var parsed interface{}
|
||||
if err := json.Unmarshal([]byte(str), &parsed); err == nil {
|
||||
respData = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -17,12 +17,50 @@ import (
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// 错误日志中单条入参值的最大长度,避免 base64 等长内容打满日志
|
||||
maxLogParamValueLen = 300
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrQueryEmpty = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// truncateForLog 将字符串截断到指定长度,用于错误日志,避免 base64 等过长内容
|
||||
func truncateForLog(s string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return s
|
||||
}
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "...[truncated, total " + strconv.Itoa(len(s)) + " chars]"
|
||||
}
|
||||
|
||||
// paramsForLog 返回适合写入错误日志的入参副本(长字符串会被截断)
|
||||
func paramsForLog(params map[string]interface{}) map[string]interface{} {
|
||||
if params == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]interface{}, len(params))
|
||||
for k, v := range params {
|
||||
if v == nil {
|
||||
out[k] = nil
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
out[k] = truncateForLog(val, maxLogParamValueLen)
|
||||
default:
|
||||
s := fmt.Sprint(v)
|
||||
out[k] = truncateForLog(s, maxLogParamValueLen)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ShujubaoResp 数据宝 API 通用响应(按实际文档调整)
|
||||
type ShujubaoResp struct {
|
||||
Code string `json:"code"`
|
||||
@@ -187,7 +225,7 @@ func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params ma
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -216,7 +254,7 @@ func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params ma
|
||||
err = errors.Join(ErrSystem, err)
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -226,7 +264,7 @@ func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params ma
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -239,7 +277,7 @@ func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params ma
|
||||
if response.StatusCode != http.StatusOK {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", response.StatusCode))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -248,7 +286,7 @@ func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params ma
|
||||
if err := json.Unmarshal(respBody, &shujubaoResp); err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -261,7 +299,7 @@ func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params ma
|
||||
if code != "10000" {
|
||||
shujubaoErr := NewShujubaoErrorFromCode(code, shujubaoResp.Message)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shujubaoErr, params)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shujubaoErr, paramsForLog(params))
|
||||
}
|
||||
return nil, errors.Join(ErrDatasource, shujubaoErr)
|
||||
}
|
||||
|
||||
@@ -16,12 +16,52 @@ import (
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// 错误日志中单条入参值的最大长度,避免 base64 等长内容打满日志
|
||||
maxLogParamValueLen = 300
|
||||
// 错误日志中 response_body 的最大长度
|
||||
maxLogResponseBodyLen = 500
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrNotFound = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// truncateForLog 将字符串截断到指定长度,用于错误日志,避免 base64 等过长内容
|
||||
func truncateForLog(s string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return s
|
||||
}
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "...[truncated, total " + strconv.Itoa(len(s)) + " chars]"
|
||||
}
|
||||
|
||||
// requestParamsForLog 返回适合写入错误日志的入参副本(长字符串会被截断)
|
||||
func requestParamsForLog(reqFormData map[string]interface{}) map[string]interface{} {
|
||||
if reqFormData == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]interface{}, len(reqFormData))
|
||||
for k, v := range reqFormData {
|
||||
if v == nil {
|
||||
out[k] = nil
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
out[k] = truncateForLog(val, maxLogParamValueLen)
|
||||
default:
|
||||
s := fmt.Sprint(v)
|
||||
out[k] = truncateForLog(s, maxLogParamValueLen)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ShumaiResponse 数脉 API 通用响应(占位,按实际文档调整)
|
||||
type ShumaiResponse struct {
|
||||
Code int `json:"code"` // 状态码
|
||||
@@ -188,7 +228,7 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": reqFormData})
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": requestParamsForLog(reqFormData)})
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -216,7 +256,7 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
err = errors.Join(ErrSystem, err)
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": reqFormData})
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": requestParamsForLog(reqFormData)})
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -227,7 +267,7 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": reqFormData})
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": requestParamsForLog(reqFormData)})
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,8 +276,8 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP %d", resp.StatusCode))
|
||||
if s.logger != nil {
|
||||
errorPayload := map[string]interface{}{
|
||||
"request_params": reqFormData,
|
||||
"response_body": string(raw),
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
}
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, errorPayload)
|
||||
}
|
||||
@@ -253,8 +293,8 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
parseErr := errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, parseErr, map[string]interface{}{
|
||||
"request_params": reqFormData,
|
||||
"response_body": string(raw),
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
})
|
||||
}
|
||||
return nil, parseErr
|
||||
@@ -273,8 +313,8 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shumaiErr, map[string]interface{}{
|
||||
"request_params": reqFormData,
|
||||
"response_body": string(raw),
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
})
|
||||
}
|
||||
if shumaiErr.IsNoRecord() {
|
||||
@@ -292,8 +332,8 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
marshalErr := errors.Join(ErrSystem, fmt.Errorf("data 序列化失败: %w", err))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, marshalErr, map[string]interface{}{
|
||||
"request_params": reqFormData,
|
||||
"response_body": string(raw),
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
})
|
||||
}
|
||||
return nil, marshalErr
|
||||
|
||||
@@ -210,8 +210,13 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st
|
||||
return nil, ErrDatasource
|
||||
}
|
||||
|
||||
// 201 表示查询为空,返回空对象
|
||||
// 201 表示查询为空,兼容其它情况如果data也为空,则返回空对象
|
||||
if zhichaResp.Code == "201" {
|
||||
// 先做类型断言
|
||||
dataMap, ok := zhichaResp.Data.(map[string]interface{})
|
||||
if ok && len(dataMap) > 0 {
|
||||
return dataMap, nil
|
||||
}
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
|
||||
137
internal/infrastructure/http/handlers/qygl_report_handler.go
Normal file
137
internal/infrastructure/http/handlers/qygl_report_handler.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
32
internal/infrastructure/http/routes/qygl_report_routes.go
Normal file
32
internal/infrastructure/http/routes/qygl_report_routes.go
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
@@ -37,6 +38,18 @@ func NewGinRouter(cfg *config.Config, logger *zap.Logger) *GinRouter {
|
||||
// 创建Gin引擎
|
||||
engine := gin.New()
|
||||
|
||||
// 加载HTML模板(企业报告等页面)
|
||||
// 为避免生产环境文件不存在导致panic,这里先检查文件是否存在
|
||||
const reportTemplatePath = "resources/qiye.html"
|
||||
if _, err := os.Stat(reportTemplatePath); err == nil {
|
||||
engine.LoadHTMLFiles(reportTemplatePath)
|
||||
logger.Info("已加载企业报告模板文件", zap.String("template", reportTemplatePath))
|
||||
} else {
|
||||
logger.Warn("未找到企业报告模板文件,将跳过模板加载(请确认部署时包含 resources/qiye.html)",
|
||||
zap.String("template", reportTemplatePath),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
return &GinRouter{
|
||||
engine: engine,
|
||||
config: cfg,
|
||||
|
||||
2186
resources/qiye.html
Normal file
2186
resources/qiye.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user