Compare commits

...

18 Commits

Author SHA1 Message Date
c5970da195 f 2026-03-10 19:15:54 +08:00
1bcb4a9c2e f 2026-03-10 19:12:35 +08:00
8877cf9691 f 2026-03-10 19:07:29 +08:00
f63e6df9f9 f 2026-03-10 19:03:42 +08:00
a00fe12141 f 2026-03-10 17:56:31 +08:00
6b80182986 f 2026-03-10 17:49:53 +08:00
bf4c114ee2 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-10 17:28:22 +08:00
4cd3954574 f 2026-03-10 17:28:04 +08:00
Mrx
d7a5589873 f 2026-03-09 11:31:40 +08:00
Mrx
b0ec75d1af f 2026-03-06 16:39:00 +08:00
Mrx
57d18be972 f 2026-03-06 16:28:25 +08:00
Mrx
3d8775b6dc f 2026-03-06 15:20:27 +08:00
Mrx
f40950f890 f 2026-03-06 15:12:58 +08:00
Mrx
ba21a8f965 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-06 10:56:37 +08:00
Mrx
7d2716da7a f 2026-03-06 10:56:36 +08:00
9a7bda9527 f 2026-03-05 19:03:18 +08:00
abdae033f0 f 2026-03-05 18:44:17 +08:00
96abacd392 f 2026-03-05 18:41:00 +08:00
25 changed files with 4281 additions and 158 deletions

View File

@@ -53,9 +53,8 @@ COPY --from=builder /app/tyapi-server .
COPY config.yaml . COPY config.yaml .
COPY configs/ ./configs/ COPY configs/ ./configs/
# 复制资源文件(直接从构建上下文复制,与配置文件一致 # 复制资源文件(报告模板、PDF、组件等
COPY resources/etc ./resources/etc COPY resources ./resources
COPY resources/pdf ./resources/pdf
# 暴露端口 # 暴露端口
EXPOSE 8080 EXPOSE 8080

View File

@@ -89,7 +89,8 @@ services:
- "25000:8080" - "25000:8080"
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
- ./resources/Pure_Component:/app/resources/Pure_Component # 挂载完整 resources 目录(包含 qiye.html、Pure_Component、pdf 等)
- ./resources:/app/resources
# 持久化PDF缓存目录确保生成的PDF在容器重启后仍然存在 # 持久化PDF缓存目录确保生成的PDF在容器重启后仍然存在
- ./storage/pdfg-cache:/app/storage/pdfg-cache - ./storage/pdfg-cache:/app/storage/pdfg-cache
# user: "1001:1001" # 注释掉使用root权限运行 # user: "1001:1001" # 注释掉使用root权限运行

View File

@@ -260,6 +260,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
// api // api
&apiEntities.ApiUser{}, &apiEntities.ApiUser{},
&apiEntities.ApiCall{}, &apiEntities.ApiCall{},
&apiEntities.Report{},
// 任务域 // 任务域
&taskEntities.AsyncTask{}, &taskEntities.AsyncTask{},

View File

@@ -660,6 +660,10 @@ func NewContainer() *Container {
api_repo.NewGormApiCallRepository, api_repo.NewGormApiCallRepository,
fx.As(new(domain_api_repo.ApiCallRepository)), 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.NewApiUserAggregateService,
), ),
api_services.NewApiCallAggregateService, api_services.NewApiCallAggregateService,
api_services.NewApiRequestService, // 使用带仓储注入的构造函数,支持企业报告记录持久化
api_services.NewApiRequestServiceWithRepos,
api_services.NewFormConfigService, api_services.NewFormConfigService,
), ),
@@ -1284,6 +1289,8 @@ func NewContainer() *Container {
) *handlers.ComponentReportOrderHandler { ) *handlers.ComponentReportOrderHandler {
return handlers.NewComponentReportOrderHandler(componentReportOrderService, purchaseOrderRepo, config, logger) return handlers.NewComponentReportOrderHandler(componentReportOrderService, purchaseOrderRepo, config, logger)
}, },
// 企业全景报告页面处理器
handlers.NewQYGLReportHandler,
// UI组件HTTP处理器 // UI组件HTTP处理器
func( func(
uiComponentAppService product.UIComponentApplicationService, uiComponentAppService product.UIComponentApplicationService,
@@ -1330,6 +1337,8 @@ func NewContainer() *Container {
routes.NewStatisticsRoutes, routes.NewStatisticsRoutes,
// PDFG路由 // PDFG路由
routes.NewPDFGRoutes, routes.NewPDFGRoutes,
// 企业报告页面路由
routes.NewQYGLReportRoutes,
), ),
// 应用生命周期 // 应用生命周期
@@ -1445,6 +1454,7 @@ func RegisterRoutes(
apiRoutes *routes.ApiRoutes, apiRoutes *routes.ApiRoutes,
statisticsRoutes *routes.StatisticsRoutes, statisticsRoutes *routes.StatisticsRoutes,
pdfgRoutes *routes.PDFGRoutes, pdfgRoutes *routes.PDFGRoutes,
qyglReportRoutes *routes.QYGLReportRoutes,
jwtAuth *middleware.JWTAuthMiddleware, jwtAuth *middleware.JWTAuthMiddleware,
adminAuth *middleware.AdminAuthMiddleware, adminAuth *middleware.AdminAuthMiddleware,
cfg *config.Config, cfg *config.Config,
@@ -1470,6 +1480,7 @@ func RegisterRoutes(
announcementRoutes.Register(router) announcementRoutes.Register(router)
statisticsRoutes.Register(router) statisticsRoutes.Register(router)
pdfgRoutes.Register(router) pdfgRoutes.Register(router)
qyglReportRoutes.Register(router)
// 打印注册的路由信息 // 打印注册的路由信息
router.PrintRoutes() router.PrintRoutes()

View File

@@ -118,7 +118,10 @@ type QYGLUY3SReq struct {
EntRegno string `json:"ent_reg_no" validate:"omitempty"` EntRegno string `json:"ent_reg_no" validate:"omitempty"`
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"` 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 { type JRZQOCRYReq struct {
PhotoData string `json:"photo_data" validate:"required,validBase64Image"` PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
} }
@@ -420,7 +423,7 @@ type COMENT01Req struct {
} }
type JRZQ09J8Req 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"` IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"` Name string `json:"name" validate:"required,min=1,validName"`
Authorized string `json:"authorized" validate:"required,oneof=0 1"` Authorized string `json:"authorized" validate:"required,oneof=0 1"`

View 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"
}

View File

@@ -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)
}

View File

@@ -7,6 +7,7 @@ import (
"tyapi-server/internal/application/api/commands" "tyapi-server/internal/application/api/commands"
"tyapi-server/internal/config" "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"
"tyapi-server/internal/domains/api/services/processors/comb" "tyapi-server/internal/domains/api/services/processors/comb"
"tyapi-server/internal/domains/api/services/processors/dwbg" "tyapi-server/internal/domains/api/services/processors/dwbg"
@@ -50,6 +51,8 @@ type ApiRequestService struct {
processorDeps *processors.ProcessorDependencies processorDeps *processors.ProcessorDependencies
combService *comb.CombService combService *comb.CombService
config *config.Config config *config.Config
reportRepo api_repositories.ReportRepository
} }
func NewApiRequestService( func NewApiRequestService(
@@ -66,26 +69,76 @@ func NewApiRequestService(
validator interfaces.RequestValidator, validator interfaces.RequestValidator,
productManagementService *services.ProductManagementService, productManagementService *services.ProductManagementService,
cfg *config.Config, 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 { ) *ApiRequestService {
// 创建组合包服务 // 创建组合包服务
combService := comb.NewCombService(productManagementService) 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) registerAllProcessors(combService)
return &ApiRequestService{ return &ApiRequestService{
westDexService: westDexService, westDexService: westDexService,
muziService: muziService, muziService: muziService,
yushanService: yushanService, yushanService: yushanService,
tianYanChaService: tianYanChaService, tianYanChaService: tianYanChaService,
alicloudService: alicloudService, alicloudService: alicloudService,
validator: validator, validator: validator,
processorDeps: processorDeps, processorDeps: processorDeps,
combService: combService, combService: combService,
config: cfg, config: cfg,
reportRepo: reportRepo,
} }
} }
@@ -180,6 +233,7 @@ func registerAllProcessors(combService *comb.CombService) {
"QYGLNIO8": qygl.ProcessQYGLNIO8Request, //企业基本信息 "QYGLNIO8": qygl.ProcessQYGLNIO8Request, //企业基本信息
"QYGLP0HT": qygl.ProcessQYGLP0HTRequest, //股权穿透 "QYGLP0HT": qygl.ProcessQYGLP0HTRequest, //股权穿透
"QYGL5S1I": qygl.ProcessQYGL5S1IRequest, //企业司法涉诉I "QYGL5S1I": qygl.ProcessQYGL5S1IRequest, //企业司法涉诉I
"QYGLJ1U9": qygl.ProcessQYGLJ1U9Request, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I
"QYGLJ0Q1": qygl.ProcessQYGLJ0Q1Request, //企业股权结构全景查询 "QYGLJ0Q1": qygl.ProcessQYGLJ0Q1Request, //企业股权结构全景查询
"QYGLUY3S": qygl.ProcessQYGLUY3SRequest, //企业经营状态全景查询 "QYGLUY3S": qygl.ProcessQYGLUY3SRequest, //企业经营状态全景查询
"YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖 "YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖

View File

@@ -176,6 +176,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
"QCXG8A3D": &dto.QCXG8A3DReq{}, "QCXG8A3D": &dto.QCXG8A3DReq{},
"QCXG6B4E": &dto.QCXG6B4EReq{}, "QCXG6B4E": &dto.QCXG6B4EReq{},
"QYGL2B5C": &dto.QYGL2B5CReq{}, "QYGL2B5C": &dto.QYGL2B5CReq{},
"QYGLJ1U9": &dto.QYGLJ1U9Req{},
"JRZQ2F8A": &dto.JRZQ2F8AReq{}, "JRZQ2F8A": &dto.JRZQ2F8AReq{},
"JRZQ1E7B": &dto.JRZQ1E7BReq{}, "JRZQ1E7B": &dto.JRZQ1E7BReq{},
"JRZQ3C9R": &dto.JRZQ3C9RReq{}, "JRZQ3C9R": &dto.JRZQ3C9RReq{},

View File

@@ -2,7 +2,9 @@ package processors
import ( import (
"context" "context"
"tyapi-server/internal/application/api/commands" "tyapi-server/internal/application/api/commands"
"tyapi-server/internal/domains/api/repositories"
"tyapi-server/internal/infrastructure/external/alicloud" "tyapi-server/internal/infrastructure/external/alicloud"
"tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/jiguang"
"tyapi-server/internal/infrastructure/external/muzi" "tyapi-server/internal/infrastructure/external/muzi"
@@ -42,6 +44,9 @@ type ProcessorDependencies struct {
CombService CombServiceInterface // Changed to interface to break import cycle CombService CombServiceInterface // Changed to interface to break import cycle
Options *commands.ApiCallOptions // 添加Options支持 Options *commands.ApiCallOptions // 添加Options支持
CallContext *CallContext // 添加CallApi调用上下文 CallContext *CallContext // 添加CallApi调用上下文
// 企业报告记录仓储,用于持久化 QYGLJ1U9 生成的企业报告
ReportRepo repositories.ReportRepository
} }
// NewProcessorDependencies 创建处理器依赖容器 // NewProcessorDependencies 创建处理器依赖容器
@@ -58,6 +63,7 @@ func NewProcessorDependencies(
shumaiService *shumai.ShumaiService, shumaiService *shumai.ShumaiService,
validator interfaces.RequestValidator, validator interfaces.RequestValidator,
combService CombServiceInterface, // Changed to interface combService CombServiceInterface, // Changed to interface
reportRepo repositories.ReportRepository,
) *ProcessorDependencies { ) *ProcessorDependencies {
return &ProcessorDependencies{ return &ProcessorDependencies{
WestDexService: westDexService, WestDexService: westDexService,
@@ -74,6 +80,7 @@ func NewProcessorDependencies(
CombService: combService, CombService: combService,
Options: nil, // 初始化为nil在调用时设置 Options: nil, // 初始化为nil在调用时设置
CallContext: nil, // 初始化为nil在调用时设置 CallContext: nil, // 初始化为nil在调用时设置
ReportRepo: reportRepo,
} }
} }

View File

@@ -4011,8 +4011,10 @@ func calculateAgeAndSex(idCard string) (int, string) {
return 0, "" return 0, ""
} }
now := time.Now()
// 计算年龄(简化处理,使用当前年份) // 计算年龄(简化处理,使用当前年份)
age := 2024 - year age := now.Year() - year
// 提取性别第17位奇数为男偶数为女 // 提取性别第17位奇数为男偶数为女
sexCode := idCard[16:17] sexCode := idCard[16:17]

View File

@@ -7,7 +7,7 @@ import (
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/zhicha"
) )
// ProcessIVYZ5E3FRequest IVYZ5E3F API处理方法 - 婚姻评估查询 // ProcessIVYZ5E3FRequest IVYZ5E3F API处理方法 - 婚姻评估查询
@@ -21,57 +21,36 @@ func ProcessIVYZ5E3FRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
// 构建极光请求参数(使用原始身份证和姓名) encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
reqData := map[string]interface{}{ if err != nil {
// 与 IVYZ9H2M 一致,极光使用 id_no 作为身份证字段 return nil, errors.Join(processors.ErrSystem, err)
"id_card": paramsDto.IDCard,
"name": paramsDto.Name,
} }
// 调用极光婚姻查询接口 encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
// apiCode: marriage-single用于请求头
// apiPath: marriage/single用于URL路径
respBytes, err := deps.JiguangService.CallAPI(ctx, "marriage-single", "marriage/single", reqData)
if err != nil { if err != nil {
// 根据错误类型返回相应的错误 return nil, errors.Join(processors.ErrSystem, err)
if errors.Is(err, jiguang.ErrNotFound) { }
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) { reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI029", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err) return nil, errors.Join(processors.ErrDatasource, err)
} else { } else {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
} }
// 解析极光返回的数据 // 将响应数据转换为JSON字节
// 现在的婚姻状态编码为state respBytes, err := json.Marshal(respData)
// 0-结婚 1-离婚 2-未匹配(均为字符串) if err != nil {
var jgResp struct {
State string `json:"state"`
}
if err := json.Unmarshal(respBytes, &jgResp); err != nil {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
// 将极光的编码转换为旧的 state 定义: return respBytes, nil
// 1:已婚,2:未婚/未在民政局登记,3:离异
var state string
switch jgResp.State {
case "0": // 结婚
state = "1"
case "1": // 离婚
state = "3"
case "2": // 未匹配
state = "2"
default:
// 未知状态按未匹配处理
state = "2"
}
// 返回与之前一致的结构:{"state": "1/2/3"}
result := map[string]string{
"state": state,
}
return json.Marshal(result)
} }

View File

@@ -7,7 +7,7 @@ import (
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/zhicha"
) )
// ProcessIVYZ81NCRequest IVYZ81NC API处理方法 // ProcessIVYZ81NCRequest IVYZ81NC API处理方法
@@ -21,55 +21,49 @@ func ProcessIVYZ81NCRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
// 构建极光请求参数(使用原始身份证和姓名) encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
reqData := map[string]interface{}{ if err != nil {
// 与 IVYZ9H2M 一致,极光使用 id_no 作为身份证字段 return nil, errors.Join(processors.ErrSystem, err)
"id_card": paramsDto.IDCard,
"name": paramsDto.Name,
} }
// 调用极光婚姻查询接口 encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
// apiCode: marriage-single用于请求头
// apiPath: marriage/single用于URL路径
respBytes, err := deps.JiguangService.CallAPI(ctx, "marriage-single", "marriage/single", reqData)
if err != nil { if err != nil {
// 根据错误类型返回相应的错误 return nil, errors.Join(processors.ErrSystem, err)
if errors.Is(err, jiguang.ErrNotFound) { }
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) { reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"authorized": "1", // 默认值
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI029", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err) return nil, errors.Join(processors.ErrDatasource, err)
} else { } else {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
} }
// 解析极光返回的数据,期望格式为 {"state": "0"|"1"|"2"} // 解析响应数据,期望格式为 {"state": "1"}
var jgResp struct { var stateResp struct {
State string `json:"state"` State string `json:"state"`
} }
if err := json.Unmarshal(respBytes, &jgResp); err != nil {
// 将 respData 转换为 JSON 字节再解析
respDataBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := json.Unmarshal(respDataBytes, &stateResp); err != nil {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
// 将极光的 state 编码转换为旧的 state 语义: // 根据 state 转换为 81nc 格式
// 极光0-结婚 1-离婚 2-未匹配
// 旧定义1:已婚,2:未婚/未在民政局登记,3:离异
var oldState string
switch jgResp.State {
case "0": // 结婚
oldState = "1"
case "1": // 离婚
oldState = "3"
case "2": // 未匹配
oldState = "2"
default:
// 未知状态按未匹配处理
oldState = "2"
}
// 根据旧的 state 值转换为 81nc 格式
var opType, opTypeDesc string var opType, opTypeDesc string
switch oldState { switch stateResp.State {
case "1": // 已婚 case "1": // 已婚
opType = "IA" opType = "IA"
opTypeDesc = "结婚" opTypeDesc = "结婚"

View File

@@ -8,7 +8,7 @@ import (
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/westdex"
) )
// ProcessIVYZ9363Request IVYZ9363 API处理方法 // ProcessIVYZ9363Request IVYZ9363 API处理方法
@@ -17,6 +17,7 @@ func ProcessIVYZ9363Request(ctx context.Context, params []byte, deps *processors
if err := json.Unmarshal(params, &paramsDto); err != nil { if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
if err := deps.Validator.ValidateStruct(paramsDto); err != nil { if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
@@ -25,72 +26,43 @@ func ProcessIVYZ9363Request(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, errors.New("请正确填写身份信息")) return nil, errors.Join(processors.ErrInvalidParam, errors.New("请正确填写身份信息"))
} }
// 构建极光双人婚姻查询请求参数 encryptedManName, err := deps.WestDexService.Encrypt(paramsDto.ManName)
// 极光入参id_card_m / name_m / id_card_w / name_w / authCode if err != nil {
reqData := map[string]interface{}{ return nil, errors.Join(processors.ErrSystem, err)
"id_card_m": paramsDto.ManIDCard,
"name_m": paramsDto.ManName,
"id_card_w": paramsDto.WomanIDCard,
"name_w": paramsDto.WomanName,
"authCode": deps.CallContext.ContractCode,
} }
// 调用极光双人婚姻查询接口 encryptedManIDCard, err := deps.WestDexService.Encrypt(paramsDto.ManIDCard)
// apiCode: marriage-double-v2用于请求头
// apiPath: marriage/double-v2用于URL路径
respBytes, err := deps.JiguangService.CallAPI(ctx, "marriage-double-v2", "marriage/double-v2", reqData)
if err != nil { if err != nil {
// 根据错误类型返回相应的错误 return nil, errors.Join(processors.ErrSystem, err)
if errors.Is(err, jiguang.ErrNotFound) { }
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) { encryptedWomanName, err := deps.WestDexService.Encrypt(paramsDto.WomanName)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedWomanIDCard, err := deps.WestDexService.Encrypt(paramsDto.WomanIDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"data": map[string]interface{}{
"name_man": encryptedManName,
"idcard_man": encryptedManIDCard,
"name_woman": encryptedWomanName,
"idcard_woman": encryptedWomanIDCard,
},
}
respBytes, err := deps.WestDexService.CallAPI(ctx, "G10XM02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err) return nil, errors.Join(processors.ErrDatasource, err)
} else { } else {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
} }
// 解析极光返回的数据,示例:"result": "0" return respBytes, nil
// 0 结婚 1 离婚 2 未匹配(均为字符串)
var jgResp struct {
Result string `json:"result"`
}
if err := json.Unmarshal(respBytes, &jgResp); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
// 将极光 result 映射为 81NC 业务类型
// 0: 结婚 -> IA / 结婚
// 1: 离婚 -> IB / 离婚
// 2: 未匹配 -> INR / 匹配不成功
var opType, opTypeDesc string
switch jgResp.Result {
case "0":
opType = "IA"
opTypeDesc = "结婚"
case "1":
opType = "IB"
opTypeDesc = "离婚"
case "2":
opType = "INR"
opTypeDesc = "匹配不成功"
default:
// 未知状态按未匹配处理
opType = "INR"
opTypeDesc = "匹配不成功"
}
// 构建当前接口对外约定的返回结构
result := map[string]interface{}{
"msg": "操作成功",
"code": 200,
"data": map[string]interface{}{
"op_date": "", // 极光未提供日期,这里保持空串
"op_type": opType, // IA / IB / INR
"op_type_desc": opTypeDesc, // 结婚 / 离婚 / 匹配不成功
},
"success": true,
}
return json.Marshal(result)
} }

View File

@@ -51,7 +51,13 @@ func ProcessQYGLJ0Q1Request(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err) 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 { if err != nil {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }

View File

@@ -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}
}()
}
// 企业全量信息核验V2QYGLUY3S
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)
// 企业司法涉诉V2QYGL5S1I
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

View File

@@ -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)
}
})
}

View File

@@ -42,7 +42,13 @@ func ProcessQYGLUY3SRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err) 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 { if err != nil {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }

View File

@@ -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
}

View File

@@ -210,8 +210,13 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st
return nil, ErrDatasource return nil, ErrDatasource
} }
// 201 表示查询为空,返回空对象 // 201 表示查询为空,兼容其它情况如果data也为空返回空对象
if zhichaResp.Code == "201" { if zhichaResp.Code == "201" {
// 先做类型断言
dataMap, ok := zhichaResp.Data.(map[string]interface{})
if ok && len(dataMap) > 0 {
return dataMap, nil
}
return map[string]interface{}{}, nil return map[string]interface{}{}, nil
} }

View 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,
})
}

View 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)
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"os"
"sort" "sort"
"time" "time"
@@ -37,6 +38,18 @@ func NewGinRouter(cfg *config.Config, logger *zap.Logger) *GinRouter {
// 创建Gin引擎 // 创建Gin引擎
engine := gin.New() 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{ return &GinRouter{
engine: engine, engine: engine,
config: cfg, config: cfg,

2186
resources/qiye.html Normal file

File diff suppressed because it is too large Load Diff