This commit is contained in:
2026-05-28 12:23:05 +08:00
66 changed files with 4954 additions and 430 deletions

View File

@@ -255,7 +255,7 @@ esign:
app_id: "7439073138"
app_secret: "d76e27fdd169b391e09262a0959dac5c"
server_url: "https://smlopenapi.esign.cn"
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
template_id: "6c91bfd5b1bb48c585f5eaceeea893d4"
contract:
name: "天远数据API合作协议"
expire_days: 7
@@ -654,6 +654,8 @@ huibo:
aes_key: "NQYN3YO+pb/GEcCBNX0ptMb7cUlnXSPvcX7VvNofBkc="
work_order_code: "gd219219725093"
product_code: "22089"
baseUrl2: "https://napi.zhixin.net:9000/api/data"
app_code2: "1508795945301708800"
logging:
enabled: true
@@ -677,3 +679,37 @@ huibo:
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🌐 诺尔智汇配置
# ===========================================
nuoer:
url: "https://api.enolfax.com/enol/api"
app_id: "t4qO2mR3"
app_secret: "d1515bf9ed2f2fe063b5f4f7e2c50f0ec65bfd58"
timeout: 4s
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "nuoer"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true

View File

@@ -46,10 +46,10 @@ ocr:
# 📝 e签宝服务配置
# ===========================================
esign:
app_id: "7439073713"
app_secret: "c7d8cb0d701f7890601d221e9b6edfef"
server_url: "https://smlopenapi.esign.cn"
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
app_id: "5112008003"
app_secret: "d487672273e7aa70c800804a1d9499b9"
server_url: "https://openapi.esign.cn"
template_id: "6c91bfd5b1bb48c585f5eaceeea893d4"
contract:
name: "天远数据API合作协议"
expire_days: 7

View File

@@ -79,7 +79,7 @@ esign:
app_id: "5112008003"
app_secret: "d487672273e7aa70c800804a1d9499b9"
server_url: "https://openapi.esign.cn"
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
template_id: "6c91bfd5b1bb48c585f5eaceeea893d4"
contract:
name: "天远数据API合作协议"
expire_days: 7

8
go.mod
View File

@@ -7,6 +7,8 @@ require (
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
github.com/alibabacloud-go/tea v1.3.13
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8
github.com/chromedp/chromedp v0.13.2
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/go-playground/locales v0.14.1
@@ -16,6 +18,7 @@ require (
github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.25.1
github.com/jung-kurt/gofpdf/v2 v2.17.3
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220
github.com/prometheus/client_golang v1.22.0
github.com/qiniu/go-sdk/v7 v7.25.4
github.com/redis/go-redis/v9 v9.11.0
@@ -58,8 +61,6 @@ require (
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect
github.com/chromedp/chromedp v0.13.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
@@ -94,15 +95,12 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/oschwald/geoip2-golang v1.13.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect

6
go.sum
View File

@@ -244,6 +244,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -268,10 +269,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=

View File

@@ -9,7 +9,6 @@ import (
"strings"
"time"
"github.com/shopspring/decimal"
"tyapi-server/internal/application/certification/dto/commands"
"tyapi-server/internal/application/certification/dto/queries"
"tyapi-server/internal/application/certification/dto/responses"
@@ -19,11 +18,11 @@ import (
certification_value_objects "tyapi-server/internal/domains/certification/entities/value_objects"
"tyapi-server/internal/domains/certification/enums"
"tyapi-server/internal/domains/certification/repositories"
"tyapi-server/internal/domains/certification/services"
finance_entities "tyapi-server/internal/domains/finance/entities"
finance_repositories "tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/domains/certification/services"
subordinate_repositories "tyapi-server/internal/domains/subordinate/repositories"
finance_service "tyapi-server/internal/domains/finance/services"
subordinate_repositories "tyapi-server/internal/domains/subordinate/repositories"
user_entities "tyapi-server/internal/domains/user/entities"
user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/infrastructure/external/notification"
@@ -32,6 +31,8 @@ import (
"tyapi-server/internal/shared/esign"
sharedOCR "tyapi-server/internal/shared/ocr"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
@@ -726,7 +727,7 @@ func (s *CertificationApplicationServiceImpl) HandleEsignCallback(
}
// 生成合同
err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID)
err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.UnifiedSocialCode, record.EnterpriseAddress, pickAuthorizedRepName(record, record.LegalPersonName))
if err != nil {
return err
}
@@ -1287,6 +1288,25 @@ func (s *CertificationApplicationServiceImpl) validateContractApplicationPrecond
return nil
}
// pickAuthorizedRepName 合同模板「客户授权代表」: 优先企业提交记录中的授权代表, 否则为法定代表人
func pickAuthorizedRepName(record *entities.EnterpriseInfoSubmitRecord, legalPersonName string) string {
if record != nil && strings.TrimSpace(record.AuthorizedRepName) != "" {
return strings.TrimSpace(record.AuthorizedRepName)
}
return legalPersonName
}
// pickEnterpriseString 优先用户域企业表字段,为空则用最近一次认证提交记录(避免 enterprise_infos 未同步导致合同控件无值)
func pickEnterpriseString(primary string, record *entities.EnterpriseInfoSubmitRecord, fromRecord func(*entities.EnterpriseInfoSubmitRecord) string) string {
if strings.TrimSpace(primary) != "" {
return strings.TrimSpace(primary)
}
if record == nil {
return ""
}
return strings.TrimSpace(fromRecord(record))
}
// generateContractAndSignURL 生成合同和签署链接
func (s *CertificationApplicationServiceImpl) generateContractAndSignURL(ctx context.Context, cert *entities.Certification, enterpriseInfo *user_entities.EnterpriseInfo) (*certification_value_objects.ContractInfo, error) {
// 发起签署流程
@@ -1361,7 +1381,7 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification(
}
// 生成合同
err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID)
err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.UnifiedSocialCode, record.EnterpriseAddress, pickAuthorizedRepName(record, record.LegalPersonName))
if err != nil {
return err
}
@@ -1383,27 +1403,41 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile(
ctx context.Context,
cert *entities.Certification,
companyName string,
legalPersonName string,
unifiedSocialCode string,
enterpriseAddress string,
legalPersonPhone string,
legalPersonID string,
authorizedRepName string,
) error {
s.logger.Info("合同生成-步骤1-开始填充合同模板",
zap.String("user_id", cert.UserID),
zap.String("company_name", companyName))
// 协议编号:已有则复用,否则新生成
if cert.ContractCode == "" {
cert.SetContractCode(user_entities.GenerateContractCode(user_entities.ContractTypeCooperation))
}
agreementNo := cert.ContractCode
// e签宝日期控件格式必须与模板预设一致本模板为 yyyy年MM月dd日
signDate := time.Now().Format("2006年01月02日")
if strings.TrimSpace(unifiedSocialCode) == "" {
s.logger.Warn("合同模板控件 jftyshxydm统一社会信用代码为空若 PDF 上该处空白,请核对 enterprise_infos.unified_social_code、提交记录或 e签宝模板控件 componentKey 是否与代码键名一致",
zap.String("user_id", cert.UserID))
}
// 控件 key 须与 e签宝控制台该控件「控件编码/componentKey」完全一致区分大小写
fileComponent := map[string]string{
"YFCompanyName": companyName,
"YFCompanyName2": companyName,
"YFLegalPersonName": legalPersonName,
"YFLegalPersonName2": legalPersonName,
"YFUnifiedSocialCode": unifiedSocialCode,
"YFEnterpriseAddress": enterpriseAddress,
"YFContactPerson": legalPersonName,
"YFMobile": legalPersonPhone,
"SignDate": time.Now().Format("2006年01月02日"),
"SignDate2": time.Now().Format("2006年01月02日"),
"SignDate3": time.Now().Format("2006年01月02日"),
"jfqym": companyName,
"jfqym2": companyName,
"jfsqdb": authorizedRepName,
"jftyshxydm": unifiedSocialCode,
"jflxdz": enterpriseAddress,
// 甲方
"xybh": agreementNo,
"qsrq1": signDate,
"qsrq3": signDate,
// 乙方
"qsrq2": signDate,
}
fillTemplateResp, err := s.esignClient.FillTemplate(fileComponent)
if err != nil {
@@ -1412,7 +1446,8 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile(
}
s.logger.Info("合同生成-步骤1-模板填充成功",
zap.String("user_id", cert.UserID),
zap.String("file_id", fillTemplateResp.FileID))
zap.String("file_id", fillTemplateResp.FileID),
zap.String("contract_code", agreementNo))
err = cert.AddContractFileID(fillTemplateResp.FileID, fillTemplateResp.FileDownloadUrl)
if err != nil {
s.logger.Error("加入合同文件ID链接失败", zap.Error(err))
@@ -1432,9 +1467,26 @@ func (s *CertificationApplicationServiceImpl) updateContractFile(ctx context.Con
s.logger.Error("获取企业信息失败", zap.Error(err))
return fmt.Errorf("获取企业信息失败: %w", err)
}
if enterpriseInfo.EnterpriseInfo == nil {
return fmt.Errorf("用户企业信息不存在")
}
ei := enterpriseInfo.EnterpriseInfo
submitRec, recErr := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID)
if recErr != nil {
s.logger.Warn("更新合同时加载企业提交记录失败,统一社会信用代码等仅以用户域为准",
zap.String("user_id", cert.UserID),
zap.Error(recErr))
submitRec = nil
}
authRep := pickAuthorizedRepName(submitRec, ei.LegalPersonName)
company := pickEnterpriseString(ei.CompanyName, submitRec, func(r *entities.EnterpriseInfoSubmitRecord) string { return r.CompanyName })
uscc := pickEnterpriseString(ei.UnifiedSocialCode, submitRec, func(r *entities.EnterpriseInfoSubmitRecord) string { return r.UnifiedSocialCode })
addr := pickEnterpriseString(ei.EnterpriseAddress, submitRec, func(r *entities.EnterpriseInfoSubmitRecord) string { return r.EnterpriseAddress })
// 生成合同
err = s.generateAndAddContractFile(ctx, cert, enterpriseInfo.EnterpriseInfo.CompanyName, enterpriseInfo.EnterpriseInfo.LegalPersonName, enterpriseInfo.EnterpriseInfo.UnifiedSocialCode, enterpriseInfo.EnterpriseInfo.EnterpriseAddress, enterpriseInfo.EnterpriseInfo.LegalPersonPhone, enterpriseInfo.EnterpriseInfo.LegalPersonID)
err = s.generateAndAddContractFile(ctx, cert, company, uscc, addr, authRep)
if err != nil {
return err
}
@@ -1571,16 +1623,29 @@ func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ct
s.logger.Info("合同文件已上传七牛云", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL))
// 4. 保存到合同聚合根
_, err = s.contractAggregateService.CreateContract(
ctx,
user.EnterpriseInfo.ID,
cert.UserID,
fileName,
user_entities.ContractTypeCooperation,
fileId,
qiniuURL,
)
// 4. 保存到合同聚合根(复用认证阶段生成的合同编号;旧数据无编号时退回自动生成)
if strings.TrimSpace(cert.ContractCode) != "" {
_, err = s.contractAggregateService.CreateContractWithCode(
ctx,
user.EnterpriseInfo.ID,
cert.UserID,
fileName,
user_entities.ContractTypeCooperation,
fileId,
qiniuURL,
strings.TrimSpace(cert.ContractCode),
)
} else {
_, err = s.contractAggregateService.CreateContract(
ctx,
user.EnterpriseInfo.ID,
cert.UserID,
fileName,
user_entities.ContractTypeCooperation,
fileId,
qiniuURL,
)
}
if err != nil {
s.logger.Error("保存合同信息到聚合根失败", zap.String("file_name", fileName), zap.Error(err))
continue

File diff suppressed because it is too large Load Diff

View File

@@ -47,4 +47,7 @@ type ProductApplicationService interface {
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
DeleteProductApiConfig(ctx context.Context, configID string) error
// 产品字典导出
ExportProductDictionary(ctx context.Context, format string) ([]byte, error)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"reflect"
"sort"
"strings"
"github.com/shopspring/decimal"
@@ -16,6 +17,7 @@ import (
api_services "tyapi-server/internal/domains/api/services"
"tyapi-server/internal/domains/product/entities"
product_service "tyapi-server/internal/domains/product/services"
"tyapi-server/internal/shared/export"
"tyapi-server/internal/shared/interfaces"
)
@@ -28,6 +30,7 @@ type ProductApplicationServiceImpl struct {
documentationAppService DocumentationApplicationServiceInterface
formConfigService api_services.FormConfigService
logger *zap.Logger
exportManager *export.ExportManager
}
// NewProductApplicationService 创建产品应用服务
@@ -38,6 +41,7 @@ func NewProductApplicationService(
documentationAppService DocumentationApplicationServiceInterface,
formConfigService api_services.FormConfigService,
logger *zap.Logger,
exportManager *export.ExportManager,
) ProductApplicationService {
return &ProductApplicationServiceImpl{
productManagementService: productManagementService,
@@ -46,6 +50,7 @@ func NewProductApplicationService(
documentationAppService: documentationAppService,
formConfigService: formConfigService,
logger: logger,
exportManager: exportManager,
}
}
@@ -492,24 +497,24 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
// convertToProductInfoResponse 转换为产品信息响应
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
response := &responses.ProductInfoResponse{
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Content: product.Content,
CategoryID: product.CategoryID,
SubCategoryID: product.SubCategoryID,
Price: product.Price.InexactFloat64(),
IsEnabled: product.IsEnabled,
IsPackage: product.IsPackage,
SellUIComponent: product.SellUIComponent,
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Content: product.Content,
CategoryID: product.CategoryID,
SubCategoryID: product.SubCategoryID,
Price: product.Price.InexactFloat64(),
IsEnabled: product.IsEnabled,
IsPackage: product.IsPackage,
SellUIComponent: product.SellUIComponent,
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
SEOTitle: product.SEOTitle,
SEODescription: product.SEODescription,
SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
SEOTitle: product.SEOTitle,
SEODescription: product.SEODescription,
SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
}
// 添加一级分类信息
@@ -544,27 +549,27 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
// convertToProductAdminInfoResponse 转换为管理员产品信息响应
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
response := &responses.ProductAdminInfoResponse{
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Content: product.Content,
CategoryID: product.CategoryID,
SubCategoryID: product.SubCategoryID,
Price: product.Price.InexactFloat64(),
CostPrice: product.CostPrice.InexactFloat64(),
Remark: product.Remark,
IsEnabled: product.IsEnabled,
IsVisible: product.IsVisible, // 管理员可以看到可见状态
IsPackage: product.IsPackage,
SellUIComponent: product.SellUIComponent,
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Content: product.Content,
CategoryID: product.CategoryID,
SubCategoryID: product.SubCategoryID,
Price: product.Price.InexactFloat64(),
CostPrice: product.CostPrice.InexactFloat64(),
Remark: product.Remark,
IsEnabled: product.IsEnabled,
IsVisible: product.IsVisible, // 管理员可以看到可见状态
IsPackage: product.IsPackage,
SellUIComponent: product.SellUIComponent,
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
SEOTitle: product.SEOTitle,
SEODescription: product.SEODescription,
SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
SEOTitle: product.SEOTitle,
SEODescription: product.SEODescription,
SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
}
// 添加一级分类信息
@@ -994,103 +999,103 @@ func (s *ProductApplicationServiceImpl) mergeRequestParamsFromDTOs(ctx context.C
// getDTOMap 获取API代码到DTO结构体的映射复用form_config_service的逻辑
func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} {
return map[string]interface{}{
"IVYZ9363": &dto.IVYZ9363Req{},
"IVYZ385E": &dto.IVYZ385EReq{},
"IVYZ5733": &dto.IVYZ5733Req{},
"FLXG3D56": &dto.FLXG3D56Req{},
"FLXG75FE": &dto.FLXG75FEReq{},
"FLXG0V3B": &dto.FLXG0V3BReq{},
"FLXG0V4B": &dto.FLXG0V4BReq{},
"FLXG54F5": &dto.FLXG54F5Req{},
"FLXG162A": &dto.FLXG162AReq{},
"FLXG0687": &dto.FLXG0687Req{},
"FLXGBC21": &dto.FLXGBC21Req{},
"FLXG970F": &dto.FLXG970FReq{},
"FLXG5876": &dto.FLXG5876Req{},
"FLXG9687": &dto.FLXG9687Req{},
"FLXGC9D1": &dto.FLXGC9D1Req{},
"FLXGCA3D": &dto.FLXGCA3DReq{},
"FLXGDEC7": &dto.FLXGDEC7Req{},
"JRZQ0A03": &dto.JRZQ0A03Req{},
"JRZQ4AA8": &dto.JRZQ4AA8Req{},
"JRZQ8203": &dto.JRZQ8203Req{},
"JRZQDCBE": &dto.JRZQDCBEReq{},
"QYGL2ACD": &dto.QYGL2ACDReq{},
"QYGL6F2D": &dto.QYGL6F2DReq{},
"QYGL45BD": &dto.QYGL45BDReq{},
"QYGL8261": &dto.QYGL8261Req{},
"QYGL8271": &dto.QYGL8271Req{},
"QYGLB4C0": &dto.QYGLB4C0Req{},
"QYGL23T7": &dto.QYGL23T7Req{},
"QYGL5A3C": &dto.QYGL5A3CReq{},
"QYGL8B4D": &dto.QYGL8B4DReq{},
"QYGL9E2F": &dto.QYGL9E2FReq{},
"QYGL7C1A": &dto.QYGL7C1AReq{},
"QYGL3F8E": &dto.QYGL3F8EReq{},
"YYSY4B37": &dto.YYSY4B37Req{},
"YYSY4B21": &dto.YYSY4B21Req{},
"YYSY6F2E": &dto.YYSY6F2EReq{},
"YYSY09CD": &dto.YYSY09CDReq{},
"IVYZ0B03": &dto.IVYZ0B03Req{},
"IVYZ9363": &dto.IVYZ9363Req{},
"IVYZ385E": &dto.IVYZ385EReq{},
"IVYZ5733": &dto.IVYZ5733Req{},
"FLXG3D56": &dto.FLXG3D56Req{},
"FLXG75FE": &dto.FLXG75FEReq{},
"FLXG0V3B": &dto.FLXG0V3BReq{},
"FLXG0V4B": &dto.FLXG0V4BReq{},
"FLXG54F5": &dto.FLXG54F5Req{},
"FLXG162A": &dto.FLXG162AReq{},
"FLXG0687": &dto.FLXG0687Req{},
"FLXGBC21": &dto.FLXGBC21Req{},
"FLXG970F": &dto.FLXG970FReq{},
"FLXG5876": &dto.FLXG5876Req{},
"FLXG9687": &dto.FLXG9687Req{},
"FLXGC9D1": &dto.FLXGC9D1Req{},
"FLXGCA3D": &dto.FLXGCA3DReq{},
"FLXGDEC7": &dto.FLXGDEC7Req{},
"JRZQ0A03": &dto.JRZQ0A03Req{},
"JRZQ4AA8": &dto.JRZQ4AA8Req{},
"JRZQ8203": &dto.JRZQ8203Req{},
"JRZQDCBE": &dto.JRZQDCBEReq{},
"QYGL2ACD": &dto.QYGL2ACDReq{},
"QYGL6F2D": &dto.QYGL6F2DReq{},
"QYGL45BD": &dto.QYGL45BDReq{},
"QYGL8261": &dto.QYGL8261Req{},
"QYGL8271": &dto.QYGL8271Req{},
"QYGLB4C0": &dto.QYGLB4C0Req{},
"QYGL23T7": &dto.QYGL23T7Req{},
"QYGL5A3C": &dto.QYGL5A3CReq{},
"QYGL8B4D": &dto.QYGL8B4DReq{},
"QYGL9E2F": &dto.QYGL9E2FReq{},
"QYGL7C1A": &dto.QYGL7C1AReq{},
"QYGL3F8E": &dto.QYGL3F8EReq{},
"YYSY4B37": &dto.YYSY4B37Req{},
"YYSY4B21": &dto.YYSY4B21Req{},
"YYSY6F2E": &dto.YYSY6F2EReq{},
"YYSY09CD": &dto.YYSY09CDReq{},
"IVYZ0B03": &dto.IVYZ0B03Req{},
"YYSYBE08": &dto.YYSYBE08Req{},
"YYSYBE08TEST": &dto.YYSYBE08Req{},
"YYSYD50F": &dto.YYSYD50FReq{},
"YYSYF7DB": &dto.YYSYF7DBReq{},
"IVYZ9A2B": &dto.IVYZ9A2BReq{},
"IVYZ7F2A": &dto.IVYZ7F2AReq{},
"IVYZ4E8B": &dto.IVYZ4E8BReq{},
"IVYZ1C9D": &dto.IVYZ1C9DReq{},
"IVYZGZ08": &dto.IVYZGZ08Req{},
"FLXG8A3F": &dto.FLXG8A3FReq{},
"FLXG5B2E": &dto.FLXG5B2EReq{},
"COMB298Y": &dto.COMB298YReq{},
"COMB86PM": &dto.COMB86PMReq{},
"QCXG7A2B": &dto.QCXG7A2BReq{},
"COMENT01": &dto.COMENT01Req{},
"JRZQ09J8": &dto.JRZQ09J8Req{},
"FLXGDEA8": &dto.FLXGDEA8Req{},
"FLXGDEA9": &dto.FLXGDEA9Req{},
"JRZQ1D09": &dto.JRZQ1D09Req{},
"IVYZ2A8B": &dto.IVYZ2A8BReq{},
"IVYZ7C9D": &dto.IVYZ7C9DReq{},
"IVYZ5E3F": &dto.IVYZ5E3FReq{},
"YYSY4F2E": &dto.YYSY4F2EReq{},
"YYSY8B1C": &dto.YYSY8B1CReq{},
"YYSY6D9A": &dto.YYSY6D9AReq{},
"YYSY3E7F": &dto.YYSY3E7FReq{},
"FLXG5A3B": &dto.FLXG5A3BReq{},
"FLXG9C1D": &dto.FLXG9C1DReq{},
"FLXG2E8F": &dto.FLXG2E8FReq{},
"JRZQ3C7B": &dto.JRZQ3C7BReq{},
"JRZQ8A2D": &dto.JRZQ8A2DReq{},
"JRZQ5E9F": &dto.JRZQ5E9FReq{},
"JRZQ4B6C": &dto.JRZQ4B6CReq{},
"JRZQ7F1A": &dto.JRZQ7F1AReq{},
"DWBG6A2C": &dto.DWBG6A2CReq{},
"DWBG8B4D": &dto.DWBG8B4DReq{},
"FLXG8B4D": &dto.FLXG8B4DReq{},
"IVYZ81NC": &dto.IVYZ81NCReq{},
"IVYZ2MN6": &dto.IVYZ2MN6Req{},
"IVYZ7F3A": &dto.IVYZ7F3AReq{},
"IVYZ3P9M": &dto.IVYZ3P9MReq{},
"IVYZ3A7F": &dto.IVYZ3A7FReq{},
"IVYZ9D2E": &dto.IVYZ9D2EReq{},
"DWBG7F3A": &dto.DWBG7F3AReq{},
"YYSY8F3A": &dto.YYSY8F3AReq{},
"QCXG9P1C": &dto.QCXG9P1CReq{},
"JRZQ9E2A": &dto.JRZQ9E2AReq{},
"YYSY9A1B": &dto.YYSY9A1BReq{},
"YYSY8C2D": &dto.YYSY8C2DReq{},
"YYSY7D3E": &dto.YYSY7D3EReq{},
"YYSY9E4A": &dto.YYSY9E4AReq{},
"JRZQ6F2A": &dto.JRZQ6F2AReq{},
"JRZQ8B3C": &dto.JRZQ8B3CReq{},
"JRZQ9D4E": &dto.JRZQ9D4EReq{},
"FLXG7E8F": &dto.FLXG7E8FReq{},
"QYGL5F6A": &dto.QYGL5F6AReq{},
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
"JRZQ0L85": &dto.JRZQ0L85Req{},
"YYSYD50F": &dto.YYSYD50FReq{},
"YYSYF7DB": &dto.YYSYF7DBReq{},
"IVYZ9A2B": &dto.IVYZ9A2BReq{},
"IVYZ7F2A": &dto.IVYZ7F2AReq{},
"IVYZ4E8B": &dto.IVYZ4E8BReq{},
"IVYZ1C9D": &dto.IVYZ1C9DReq{},
"IVYZGZ08": &dto.IVYZGZ08Req{},
"FLXG8A3F": &dto.FLXG8A3FReq{},
"FLXG5B2E": &dto.FLXG5B2EReq{},
"COMB298Y": &dto.COMB298YReq{},
"COMB86PM": &dto.COMB86PMReq{},
"QCXG7A2B": &dto.QCXG7A2BReq{},
"COMENT01": &dto.COMENT01Req{},
"JRZQ09J8": &dto.JRZQ09J8Req{},
"FLXGDEA8": &dto.FLXGDEA8Req{},
"FLXGDEA9": &dto.FLXGDEA9Req{},
"JRZQ1D09": &dto.JRZQ1D09Req{},
"IVYZ2A8B": &dto.IVYZ2A8BReq{},
"IVYZ7C9D": &dto.IVYZ7C9DReq{},
"IVYZ5E3F": &dto.IVYZ5E3FReq{},
"YYSY4F2E": &dto.YYSY4F2EReq{},
"YYSY8B1C": &dto.YYSY8B1CReq{},
"YYSY6D9A": &dto.YYSY6D9AReq{},
"YYSY3E7F": &dto.YYSY3E7FReq{},
"FLXG5A3B": &dto.FLXG5A3BReq{},
"FLXG9C1D": &dto.FLXG9C1DReq{},
"FLXG2E8F": &dto.FLXG2E8FReq{},
"JRZQ3C7B": &dto.JRZQ3C7BReq{},
"JRZQ8A2D": &dto.JRZQ8A2DReq{},
"JRZQ5E9F": &dto.JRZQ5E9FReq{},
"JRZQ4B6C": &dto.JRZQ4B6CReq{},
"JRZQ7F1A": &dto.JRZQ7F1AReq{},
"DWBG6A2C": &dto.DWBG6A2CReq{},
"DWBG8B4D": &dto.DWBG8B4DReq{},
"FLXG8B4D": &dto.FLXG8B4DReq{},
"IVYZ81NC": &dto.IVYZ81NCReq{},
"IVYZ2MN6": &dto.IVYZ2MN6Req{},
"IVYZ7F3A": &dto.IVYZ7F3AReq{},
"IVYZ3P9M": &dto.IVYZ3P9MReq{},
"IVYZ3A7F": &dto.IVYZ3A7FReq{},
"IVYZ9D2E": &dto.IVYZ9D2EReq{},
"DWBG7F3A": &dto.DWBG7F3AReq{},
"YYSY8F3A": &dto.YYSY8F3AReq{},
"QCXG9P1C": &dto.QCXG9P1CReq{},
"JRZQ9E2A": &dto.JRZQ9E2AReq{},
"YYSY9A1B": &dto.YYSY9A1BReq{},
"YYSY8C2D": &dto.YYSY8C2DReq{},
"YYSY7D3E": &dto.YYSY7D3EReq{},
"YYSY9E4A": &dto.YYSY9E4AReq{},
"JRZQ6F2A": &dto.JRZQ6F2AReq{},
"JRZQ8B3C": &dto.JRZQ8B3CReq{},
"JRZQ9D4E": &dto.JRZQ9D4EReq{},
"FLXG7E8F": &dto.FLXG7E8FReq{},
"QYGL5F6A": &dto.QYGL5F6AReq{},
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
"JRZQ0L85": &dto.JRZQ0L85Req{},
}
}
@@ -1240,3 +1245,120 @@ func (s *ProductApplicationServiceImpl) mapFieldTypeToDocType(frontendType strin
return "string"
}
}
// ExportProductDictionary 导出产品字典
func (s *ProductApplicationServiceImpl) ExportProductDictionary(ctx context.Context, format string) ([]byte, error) {
// 查询所有启用且可见的产品及其分类信息
products, err := s.productManagementService.GetAllProductsForDictionary(ctx)
if err != nil {
s.logger.Error("获取产品字典数据失败", zap.Error(err))
return nil, err
}
if len(products) == 0 {
return nil, fmt.Errorf("没有找到符合条件的产品数据")
}
// 按分类名称分组整理数据
type categoryInfo struct {
name string
sort int
}
categoryGroups := make(map[string][]map[string]interface{}) // 使用分类名称作为key
categorySortMap := make(map[string]int) // 分类名称到sort值的映射
var categoryNames []string // 保持分类顺序
for _, product := range products {
// 获取分类信息
categoryName := "未分类"
categorySort := 999999 // 默认排序值,确保未分类的产品排在最后
if product.Category != nil {
categoryName = product.Category.Name
categorySort = product.Category.Sort
// 如果有二级分类,添加到分类名称中
if product.SubCategory != nil && product.SubCategory.Name != "" {
categoryName = categoryName + " / " + product.SubCategory.Name
}
}
// 如果分类不存在,初始化并添加到顺序列表
if _, exists := categoryGroups[categoryName]; !exists {
categoryGroups[categoryName] = []map[string]interface{}{}
categoryNames = append(categoryNames, categoryName)
categorySortMap[categoryName] = categorySort
}
// 添加产品到对应分类
productInfo := map[string]interface{}{
"category": categoryName,
"product_code": product.Code,
"product_name": product.Name,
"description": product.Description,
}
categoryGroups[categoryName] = append(categoryGroups[categoryName], productInfo)
}
// 按分类的sort值对分类名称进行排序
sort.Slice(categoryNames, func(i, j int) bool {
return categorySortMap[categoryNames[i]] < categorySortMap[categoryNames[j]]
})
// 准备导出数据
headers := []string{"分类", "产品编码", "产品名称", "产品简介"}
columnWidths := []float64{25, 15, 20, 40}
// 构建数据行
var data [][]interface{}
for _, categoryName := range categoryNames {
productsInCategory := categoryGroups[categoryName]
for i, product := range productsInCategory {
// 只有每个分类的第一个产品才显示分类名称
var categoryNameForRow interface{}
if i == 0 {
categoryNameForRow = categoryName
} else {
categoryNameForRow = ""
}
row := []interface{}{
categoryNameForRow,
product["product_code"],
product["product_name"],
product["description"],
}
data = append(data, row)
}
}
// 计算需要合并的行
mergedRegions := [][]int{}
currentRow := 1 // 从第1行开始第0行是表头
for _, categoryName := range categoryNames {
productsCount := len(categoryGroups[categoryName])
if productsCount > 1 {
// 合并相同分类的单元格从当前行开始合并productsCount行第0列
// Excel格式[startRow, startCol, endRow, endCol]
mergedRegions = append(mergedRegions, []int{
currentRow, // startRow
0, // startCol (分类列)
currentRow + productsCount - 1, // endRow
0, // endCol
})
}
currentRow += productsCount
}
// 创建导出配置
config := &export.ExportConfig{
SheetName: "产品字典",
Headers: headers,
Data: data,
ColumnWidths: columnWidths,
MergedRegions: mergedRegions,
}
// 使用导出管理器生成文件
return s.exportManager.Export(ctx, config, format)
}

View File

@@ -45,6 +45,7 @@ type Config struct {
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
Huibo HuiboConfig `mapstructure:"huibo"`
Nuoer NuoerConfig `mapstructure:"nuoer"`
}
// ServerConfig HTTP服务器配置
@@ -682,6 +683,8 @@ type HuiboConfig struct {
AESKey string `mapstructure:"aes_key"`
WorkOrderCode string `mapstructure:"work_order_code"`
ProductCode string `mapstructure:"product_code"`
BaseURL2 string `mapstructure:"baseUrl2"`
AppCode2 string `mapstructure:"app_code2"`
Logging HuiboLoggingConfig `mapstructure:"logging"`
}
@@ -703,6 +706,34 @@ type HuiboLevelFileConfig struct {
Compress bool `mapstructure:"compress"`
}
// NuoerConfig 诺尔智汇配置
type NuoerConfig struct {
URL string `mapstructure:"url"`
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
Timeout time.Duration `mapstructure:"timeout"`
Logging NuoerLoggingConfig `mapstructure:"logging"`
}
// NuoerLoggingConfig 诺尔智汇日志配置
type NuoerLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
ServiceName string `mapstructure:"service_name"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]NuoerLevelFileConfig `mapstructure:"level_configs"`
}
// NuoerLevelFileConfig 诺尔智汇级别文件配置
type NuoerLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// DomainConfig 域名配置
type DomainConfig struct {
API string `mapstructure:"api"` // API域名

View File

@@ -45,6 +45,7 @@ import (
"tyapi-server/internal/infrastructure/external/huibo"
"tyapi-server/internal/infrastructure/external/jiguang"
"tyapi-server/internal/infrastructure/external/muzi"
"tyapi-server/internal/infrastructure/external/nuoer"
"tyapi-server/internal/infrastructure/external/ocr"
"tyapi-server/internal/infrastructure/external/shujubao"
"tyapi-server/internal/infrastructure/external/shumai"
@@ -405,6 +406,10 @@ func NewContainer() *Container {
func(cfg *config.Config) (*shujubao.ShujubaoService, error) {
return shujubao.NewShujubaoServiceWithConfig(cfg)
},
// NuoerService - 诺尔智汇服务
func(cfg *config.Config) (*nuoer.NuoerService, error) {
return nuoer.NewNuoerServiceWithConfig(cfg)
},
func(cfg *config.Config) *yushan.YushanService {
return yushan.NewYushanService(
cfg.Yushan.URL,
@@ -1026,6 +1031,7 @@ func NewContainer() *Container {
documentationAppService product.DocumentationApplicationServiceInterface,
formConfigService api_services.FormConfigService,
logger *zap.Logger,
exportManager *export.ExportManager,
) product.ProductApplicationService {
return product.NewProductApplicationService(
productManagementService,
@@ -1034,6 +1040,7 @@ func NewContainer() *Container {
documentationAppService,
formConfigService,
logger,
exportManager,
)
},
fx.As(new(product.ProductApplicationService)),

View File

@@ -249,7 +249,8 @@ type IVYZZQT3Req struct {
type IVYZZQ3BReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
ImageUrl string `json:"image_url" validate:"required,url"`
PhotoData string `json:"photo_data" validate:"omitempty,validBase64Image"`
ImageUrl string `json:"image_url" validate:"omitempty,url"`
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}
@@ -357,6 +358,11 @@ type QCXGGB2QReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
CarPlateType string `json:"carplate_type" validate:"required"`
}
type QCXGM4CLReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type QCXGJJ2AReq struct {
VinCode string `json:"vin_code" validate:"required"`
EngineNumber string `json:"engine_number" validate:"omitempty"`
@@ -487,6 +493,30 @@ type IVYZ2A8BReq struct {
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}
type QYGL4YABReq struct {
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type QYGL3YSBReq struct {
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
}
type QYGL2YSBReq struct {
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
}
type QYGLDG77Req struct {
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
AccountNo string `json:"account_no" validate:"required,min=1,"`
AccountBank string `json:"account_bank" validate:"required,min=1,"`
}
type IVYZ7C9DReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
@@ -533,6 +563,12 @@ type IVYZ4Y27Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
AuthAuthorizeFileBase64 string `json:"auth_authorize_file_base64" validate:"required,validBase64PDF"`
}
type FLXGHB4FReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type IVYZP2Q6Req struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
@@ -571,6 +607,10 @@ type QYGL5A3CReq struct {
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
}
type QYGLBH7YReq struct {
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
}
type QYGL2naoReq struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"`
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`

View File

@@ -24,6 +24,7 @@ import (
"tyapi-server/internal/infrastructure/external/huibo"
"tyapi-server/internal/infrastructure/external/jiguang"
"tyapi-server/internal/infrastructure/external/muzi"
"tyapi-server/internal/infrastructure/external/nuoer"
"tyapi-server/internal/infrastructure/external/shujubao"
"tyapi-server/internal/infrastructure/external/shumai"
"tyapi-server/internal/infrastructure/external/tianyancha"
@@ -68,6 +69,7 @@ func NewApiRequestService(
jiguangService *jiguang.JiguangService,
shumaiService *shumai.ShumaiService,
huiboService *huibo.HuiboService,
nuoerService *nuoer.NuoerService,
validator interfaces.RequestValidator,
productManagementService *services.ProductManagementService,
cfg *appconfig.Config,
@@ -84,6 +86,7 @@ func NewApiRequestService(
jiguangService,
shumaiService,
huiboService,
nuoerService,
validator,
productManagementService,
cfg,
@@ -105,6 +108,7 @@ func NewApiRequestServiceWithRepos(
jiguangService *jiguang.JiguangService,
shumaiService *shumai.ShumaiService,
huiboService *huibo.HuiboService,
nuoerService *nuoer.NuoerService,
validator interfaces.RequestValidator,
productManagementService *services.ProductManagementService,
cfg *appconfig.Config,
@@ -132,6 +136,7 @@ func NewApiRequestServiceWithRepos(
jiguangService,
shumaiService,
huiboService,
nuoerService,
validator,
combService,
reportRepo,
@@ -186,6 +191,7 @@ func registerAllProcessors(combService *comb.CombService) {
"FLXG3A9B": flxg.ProcessFLXG3A9BRequest,
"FLXGK5D2": flxg.ProcessFLXGK5D2Request,
"FLXGDJG3": flxg.ProcessFLXGDJG3Request, //董监高司法综合信息核验
"FLXGHB4F": flxg.ProcessFLXGHB4FRequest, //个人涉诉案件查询汇博
// JRZQ系列处理器
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
@@ -254,7 +260,11 @@ func registerAllProcessors(combService *comb.CombService) {
"QYGLDJ12": qygl.ProcessQYGLDJ12Request, //企业年报信息核验
"QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查
"QYGLDJ33": qygl.ProcessQYGLDJ33Request, //企业年报信息核验
"QYGLBH7Y": qygl.ProcessQYGLBH7YRequest, //企业涉诉案件查询汇博
"QYGL4YAB": qygl.ProcessQYGL4YABRequest, //企业四要素认证shumai
"QYGL3YSB": qygl.ProcessQYGL3YSBRequest, //企业三要素认证shumai
"QYGL2YSB": qygl.ProcessQYGL2YSBRequest, //企业二要素认证shumai
"QYGLDG77": qygl.ProcessQYGLDG77Request, //企业对公打款认证shumai
// YYSY系列处理器
"YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
@@ -373,7 +383,7 @@ func registerAllProcessors(combService *comb.CombService) {
"QCXG5U0Z": qcxg.ProcessQCXG5U0ZRequest, // 车辆静态信息查询 10479
"QCXGY7F2": qcxg.ProcessQCXGY7F2Request, // 二手车VIN估值 10443
"QCXG3M7Z": qcxg.ProcessQCXG3M7ZRequest, //人车关系核验ETC10093 月更
"QCXGM4CL": qcxg.ProcessQCXGM4CLRequest, //名下车辆诺尔
// DWBG系列处理器 - 多维报告
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
"DWBG8B4D": dwbg.ProcessDWBG8B4DRequest,

View File

@@ -281,6 +281,13 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
"IVYZRAX1": &dto.IVYZRAX1Req{}, //融安信用分
"IVYZRAX2": &dto.IVYZRAX1Req{}, //融御反欺诈
"IVYZ2MN7": &dto.IVYZ2MN6Req{}, //学历Bzhicha
"FLXGHB4F": &dto.FLXGHB4FReq{}, //个人涉诉案件查询汇博
"QYGLBH7Y": &dto.QYGLBH7YReq{}, //企业涉诉案件查询汇博
"QYGL4YAB": &dto.QYGL4YABReq{}, //企业四要素认证shumai
"QYGL3YSB": &dto.QYGL3YSBReq{}, //企业三要素认证shumai
"QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai
"QYGLDG77": &dto.QYGLDG77Req{}, //企业对公打款认证shumai
"QCXGM4CL": &dto.QCXGM4CLReq{}, //名下车辆诺尔
}
// 优先返回已配置的DTO
@@ -513,6 +520,8 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
"plate_color": "车牌颜色",
"marital_type": "婚姻状况类型",
"auth_authorize_file_base64": "PDF授权文件Base64编码≤500KB仅PDF",
"account_no": "企业账户",
"account_bank": "开户行",
}
if label, exists := labelMap[jsonTag]; exists {
@@ -579,6 +588,8 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
"plate_color": "0",
"marital_type": "10",
"auth_authorize_file_base64": "JVBERi0xLjQKJcTl8uXr...示例PDF的Base64编码",
"account_no": "6222021234567890123",
"account_bank": "中国工商银行",
}
if example, exists := exampleMap[jsonTag]; exists {
@@ -654,6 +665,8 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
"plate_color": "请输入车牌颜色",
"marital_type": "请选择婚姻状况类型",
"auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串",
"account_no": "请输入企业账户",
"account_bank": "请输入开户行",
}
if placeholder, exists := placeholderMap[jsonTag]; exists {
@@ -731,6 +744,8 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s
"plate_color": "车牌颜色",
"marital_type": "婚姻状况类型10-未登记无登记记录20-已婚30-丧偶40-离异",
"auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串",
"account_no": "请输入企业账户",
"account_bank": "请输入开户行",
}
if desc, exists := descMap[jsonTag]; exists {

View File

@@ -9,6 +9,7 @@ import (
"tyapi-server/internal/infrastructure/external/huibo"
"tyapi-server/internal/infrastructure/external/jiguang"
"tyapi-server/internal/infrastructure/external/muzi"
"tyapi-server/internal/infrastructure/external/nuoer"
"tyapi-server/internal/infrastructure/external/shujubao"
"tyapi-server/internal/infrastructure/external/shumai"
"tyapi-server/internal/infrastructure/external/tianyancha"
@@ -42,6 +43,7 @@ type ProcessorDependencies struct {
JiguangService *jiguang.JiguangService
ShumaiService *shumai.ShumaiService
HuiboService *huibo.HuiboService
NuoerService *nuoer.NuoerService
Validator interfaces.RequestValidator
CombService CombServiceInterface // Changed to interface to break import cycle
Options *commands.ApiCallOptions // 添加Options支持
@@ -70,6 +72,7 @@ func NewProcessorDependencies(
jiguangService *jiguang.JiguangService,
shumaiService *shumai.ShumaiService,
huiboService *huibo.HuiboService,
nuoerService *nuoer.NuoerService,
validator interfaces.RequestValidator,
combService CombServiceInterface, // Changed to interface
reportRepo repositories.ReportRepository,
@@ -88,6 +91,7 @@ func NewProcessorDependencies(
JiguangService: jiguangService,
ShumaiService: shumaiService,
HuiboService: huiboService,
NuoerService: nuoerService,
Validator: validator,
CombService: combService,
Options: nil, // 初始化为nil在调用时设置

View File

@@ -25,7 +25,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 去掉司法案件案件去掉身份证号码
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" {
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)

View File

@@ -20,9 +20,10 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" {
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)

View File

@@ -20,7 +20,7 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" {
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
}

View File

@@ -20,7 +20,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" {
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)

View File

@@ -20,6 +20,10 @@ func ProcessFLXGDEA8Request(ctx context.Context, params []byte, deps *processors
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 去掉司法案件案件去掉身份证号码
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" {
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {

View File

@@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" {
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" {
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)

View File

@@ -0,0 +1,60 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/huibo"
)
// ProcessFLXGHB4FRequest FLXGHB4F API处理方法 - 个人涉诉案件查询汇博
func ProcessFLXGHB4FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXGHB4FReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if deps.HuiboService == nil {
return nil, errors.Join(processors.ErrSystem, errors.New("汇博服务未初始化"))
}
// 使用 MD5 加密 name 和 idCard
// encryptedName := "MD5:" + huibo.MD5Encrypt(paramsDto.Name, deps.HuiboService.GetConfig().AppKey)
// encryptedIDCard := "MD5:" + huibo.MD5Encrypt(paramsDto.IDCard, deps.HuiboService.GetConfig().AppKey)
reqdata := map[string]interface{}{
"name": paramsDto.Name,
"idCard": paramsDto.IDCard,
}
respBytes, err := deps.HuiboService.CallAPI2(ctx, "P_004_0271", reqdata)
if err != nil {
return nil, errors.Join(processors.ErrDatasource, err)
}
// 解析响应
var response huibo.CallAPI2Response
if err := json.Unmarshal(respBytes, &response); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
// 处理状态码
switch response.Code {
case huibo.CallAPI2StatusSuccess:
// 查询成功
if response.Data == nil {
return []byte("{}"), nil
}
return respBytes, nil
case huibo.CallAPI2StatusNoData:
// 查询成功,无数据 - 按产品约定按调用成功计费
return []byte("{}"), nil
default:
// 其他错误状态码
message := huibo.GetCallAPI2StatusMessage(response.Code)
return nil, errors.Join(processors.ErrDatasource, errors.New(message))
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
@@ -30,19 +31,20 @@ func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedImageUrl, err := deps.ZhichaService.Encrypt(paramsDto.ImageUrl)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"idCard": encryptedIDCard,
"name": encryptedName,
"imageId": encryptedImageUrl,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI062", reqData)
if paramsDto.ImageUrl != "" {
reqData["url"] = paramsDto.ImageUrl
} else {
reqData["image"] = paramsDto.PhotoData
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI1001", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
@@ -56,7 +58,7 @@ func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err)
}
outBytes, err := mapZCI062RespToIVYZZQ3B(respBytes)
outBytes, err := mapZCI1001RespToIVYZZQ3B(respBytes)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
@@ -69,44 +71,118 @@ type IVYZZQ3BOut struct {
}
type IVYZZQ3BOutResultData struct {
// VerificationResult 审核校验结果valid 身份审核通过invalid 身份审核不通过(与 similarity 区间联动,见 mapVerificationResultFromSimilarity
// VerificationCode 审核校验编码,透传上游 incorrect
VerificationCode string `json:"verification_code"`
// VerificationResult 审核校验结果valid 身份审核通过invalid 身份审核不通过score >= 0.45 为 valid
VerificationResult string `json:"verification_result"`
// Similarity 照片相似度分数字符串01000。区间说明0600不同人600700不能确定是否同人7001000同人。数值为上游 score(0~1)×1000。
// VerificationMessage 审核校验信息,透传上游 msg
VerificationMessage string `json:"verification_message"`
// Similarity 照片相似度分数字符串01000。区间说明0600不同人600700不能确定是否同人7001000同人。
Similarity string `json:"similarity"`
}
// zci062UpstreamResp 智查 ZCI062 成功返回体中的分数字段(分值越大相似度越高)
type zci062UpstreamResp struct {
Score interface{} `json:"score"`
// zci1001UpstreamResp 智查 ZCI1001 成功返回体中的字段
type zci1001UpstreamResp struct {
Score interface{} `json:"score"`
Msg string `json:"msg"`
Incorrect interface{} `json:"incorrect"`
}
func mapZCI062RespToIVYZZQ3B(respBytes []byte) ([]byte, error) {
var r zci062UpstreamResp
func mapZCI1001RespToIVYZZQ3B(respBytes []byte) ([]byte, error) {
var r zci1001UpstreamResp
if err := json.Unmarshal(respBytes, &r); err != nil {
return nil, err
}
score := parseScoreToFloat64(r.Score)
similarityVal := score * 1000
score := parseScoreZCI1001ToFloat64(r.Score)
similarityVal := mapScoreToZCI001Similarity(score)
similarity := strconv.FormatFloat(similarityVal, 'f', 2, 64)
verificationResult := mapVerificationResultFromSimilarity(similarityVal)
verificationResult := buildToStringmapZCI1001RespToIVYZZQ3B(score)
out := IVYZZQ3BOut{
HandleTime: time.Now().Format("2006-01-02 15:04:05"),
ResultData: IVYZZQ3BOutResultData{
VerificationResult: verificationResult,
Similarity: similarity,
VerificationCode: valueZCI001ToString(r.Incorrect),
VerificationResult: verificationResult,
VerificationMessage: r.Msg,
Similarity: similarity,
},
}
return json.Marshal(out)
}
// mapVerificationResultFromSimilarity 与 similarity01000区间说明对齐
// 7001000】系统判断为同一人 → 身份审核通过 valid其余 → invalid。
func mapVerificationResultFromSimilarity(similarity float64) string {
if similarity >= 700 {
// buildToStringmapZCI1001RespToIVYZZQ3B 审核判定逻辑score >= 0.45 为 valid否则为 invalid
func buildToStringmapZCI1001RespToIVYZZQ3B(score float64) string {
if score >= 0.45 {
return "valid"
}
return "invalid"
}
// mapScoreToZCI001Similarity 将 score(0~1) 分段映射到 similarity(0~1000)
// 0.40 -> 6000.45 -> 700
func mapScoreToZCI001Similarity(score float64) float64 {
if score <= 0 {
return 0
}
if score >= 1 {
return 1000
}
if score < 0.40 {
// [0, 0.40) -> [0, 600)
return (score / 0.40) * 600
}
if score < 0.45 {
// [0.40, 0.45) -> [600, 700)
return 600 + ((score-0.40)/0.05)*100
}
// [0.45, 1] -> [700, 1000]
return 700 + ((score-0.45)/0.55)*300
}
func parseScoreZCI1001ToFloat64(v interface{}) float64 {
switch t := v.(type) {
case float64:
return t
case float32:
return float64(t)
case int:
return float64(t)
case int32:
return float64(t)
case int64:
return float64(t)
case json.Number:
if f, err := t.Float64(); err == nil {
return f
}
case string:
if f, err := strconv.ParseFloat(t, 64); err == nil {
return f
}
}
return 0
}
func valueZCI001ToString(v interface{}) string {
if v == nil {
return ""
}
switch t := v.(type) {
case string:
return t
case int:
return strconv.Itoa(t)
case int32:
return strconv.FormatInt(int64(t), 10)
case int64:
return strconv.FormatInt(t, 10)
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case float32:
return strconv.FormatFloat(float64(t), 'f', -1, 64)
default:
return fmt.Sprintf("%v", v)
}
}

View File

@@ -37,9 +37,9 @@ func ProcessJRZQ1E7BRequest(ctx context.Context, params []byte, deps *processors
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"authorized": paramsDto.Authorized,
}
@@ -55,9 +55,9 @@ func ProcessJRZQ1E7BRequest(ctx context.Context, params []byte, deps *processors
// 将响应数据转换为 JSON 字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
// return nil, errors.Join(processors.ErrSystem, err)
return json.Marshal(map[string]interface{}{})
}
return respBytes, nil
}

View File

@@ -37,9 +37,9 @@ func ProcessJRZQ2F8ARequest(ctx context.Context, params []byte, deps *processors
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"authorized": paramsDto.Authorized,
}
@@ -60,4 +60,3 @@ func ProcessJRZQ2F8ARequest(ctx context.Context, params []byte, deps *processors
return respBytes, nil
}

View File

@@ -40,6 +40,7 @@ func ProcessJRZQ3P01Request(ctx context.Context, params []byte, deps *processors
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI109", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)

View File

@@ -7,10 +7,9 @@ import (
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG4D2ERequest QCXG4D2E API处理方法 - 极光名下车辆数量查询
// ProcessQCXG4D2ERequest QCXG4D2E API处理方法 - 名下车辆数量(委托诺尔 QCXGM4CL
func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG4D2EReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
@@ -21,27 +20,15 @@ func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"idNum": paramsDto.IDCard,
"userType": paramsDto.UserType,
}
// 调用极光API
// apiCode: vehicle-inquiry-under-name (用于请求头)
// apiPath: vehicle/inquiry-under-name (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData)
m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard})
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
if err != nil {
return nil, err
}
return transformQCXG5F3AResponse(raw)
}

View File

@@ -4,13 +4,15 @@ import (
"context"
"encoding/json"
"errors"
"strconv"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
"github.com/tidwall/gjson"
)
// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 极光名下车辆车牌查询
// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 名下车辆(委托诺尔 QCXGM4CL响应格式兼容极光
func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG5F3AReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
@@ -21,32 +23,53 @@ func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err)
}
null := ""
// 构建请求参数
reqData := map[string]interface{}{
"id_card": paramsDto.IDCard,
"name": paramsDto.Name,
"userType": null,
"vehicleType": null,
"encryptionType": null,
"encryptionContent": null,
}
// 调用极光API
// apiCode: vehicle-person-vehicles (用于请求头)
// apiPath: vehicle/person-vehicles (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-person-vehicles", "vehicle/person-vehicles", reqData)
m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard})
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
if err != nil {
return nil, err
}
return transformQCXG5F3AResponse(raw)
}
// transformQCXG5F3AResponse 将诺尔响应转为 QCXG5F3A 对外格式:去掉 busiCode/busiMsg展开 resultvehicleCount 为字符串
func transformQCXG5F3AResponse(raw []byte) ([]byte, error) {
base := gjson.GetBytes(raw, "result")
if !base.Exists() {
base = gjson.ParseBytes(raw)
}
list := base.Get("list").Value()
if list == nil {
list = []interface{}{}
}
countStr, err := formatVehicleCountAsString(base.Get("vehicleCount"))
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
out := map[string]interface{}{
"vehicleCount": countStr,
"list": list,
}
return json.Marshal(out)
}
func formatVehicleCountAsString(v gjson.Result) (string, error) {
if !v.Exists() {
return "0", nil
}
switch v.Type {
case gjson.String:
return v.String(), nil
case gjson.Number:
return strconv.FormatInt(v.Int(), 10), nil
default:
return "", errors.New("vehicleCount 类型无效")
}
}

View File

@@ -8,12 +8,11 @@ import (
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
"github.com/tidwall/gjson"
)
// ProcessQCXG9P1CRequest QCXG9P1C API处理方法 兼容旧版 极光名下车牌查询数量
// ProcessQCXG9P1CRequest QCXG9P1C API处理方法 - 名下车辆详版(委托诺尔 QCXGM4CL
func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG9P1CReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
@@ -24,54 +23,53 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err)
}
null := ""
// 构建请求参数
reqData := map[string]interface{}{
"id_card": paramsDto.IDCard,
"name": null,
"userType": null,
"vehicleType": null,
"encryptionType": null,
"encryptionContent": null,
}
// 调用极光API
// apiCode: vehicle-person-vehicles (用于请求头)
// apiPath: vehicle/person-vehicles (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-person-vehicles", "vehicle/person-vehicles", reqData)
m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard})
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
// 使用 gjson 检查并转换 vehicleCount 字段
vehicleCountResult := gjson.GetBytes(respBytes, "vehicleCount")
if vehicleCountResult.Exists() && vehicleCountResult.Type == gjson.String {
// 如果是字符串类型,转换为整数
vehicleCountInt, err := strconv.Atoi(vehicleCountResult.String())
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
// 解析 JSON 并修改 vehicleCount 字段
var respData map[string]interface{}
if err := json.Unmarshal(respBytes, &respData); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
respData["vehicleCount"] = vehicleCountInt
// 重新序列化为JSON并返回
resultBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return resultBytes, nil
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
if err != nil {
return nil, err
}
// 如果 vehicleCount 不存在或不是字符串,直接返回原始响应
return respBytes, nil
return transformQCXG9P1CResponse(raw)
}
// transformQCXG9P1CResponse 将诺尔响应转为 QCXG9P1C 对外格式:去掉 busiCode/busiMsg展开 resultvehicleCount 为整数
func transformQCXG9P1CResponse(raw []byte) ([]byte, error) {
base := gjson.GetBytes(raw, "result")
if !base.Exists() {
base = gjson.ParseBytes(raw)
}
list := base.Get("list").Value()
if list == nil {
list = []interface{}{}
}
countInt, err := formatVehicleCountAsInt(base.Get("vehicleCount"))
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
out := map[string]interface{}{
"vehicleCount": countInt,
"list": list,
}
return json.Marshal(out)
}
func formatVehicleCountAsInt(v gjson.Result) (int, error) {
if !v.Exists() {
return 0, nil
}
switch v.Type {
case gjson.String:
return strconv.Atoi(v.String())
case gjson.Number:
return int(v.Int()), nil
default:
return 0, errors.New("vehicleCount 类型无效")
}
}

View File

@@ -0,0 +1,48 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/nuoer"
)
// ProcessQCXGM4CLRequest QCXGM4CL API处理方法 - 名下车辆诺尔
func ProcessQCXGM4CLRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGM4CLReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
body := map[string]string{
"idCard": paramsDto.IDCard,
}
nuoerDoCheckAPIKey := "id_vehicle_query_102"
ApiPath := "/v1/doCheck"
resp, err := deps.NuoerService.CallAPI(ctx, nuoerDoCheckAPIKey, ApiPath, body)
if err != nil {
if errors.Is(err, nuoer.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
if errors.Is(err, nuoer.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,47 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/shumai"
)
// ProcessQYGL2YSBRequest QYGL2YSB API处理方法 - 企业二要素认证
func ProcessQYGL2YSBRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGL2YSBReq
if err := json.Unmarshal(params, &paramsDto); 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{}{
"legalPerson": paramsDto.LegalPerson,
"creditNo": paramsDto.EntCode,
}
apiPath := "/v4/company/two/check" // 接口路径,根据数脉文档填写(如 v4/xxx
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
if err != nil {
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 if errors.Is(err, shumai.ErrNotFound) {
// 查无记录
return nil, errors.Join(processors.ErrNotFound, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,48 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/shumai"
)
// ProcessQYGL3YSBRequest QYGL3YSB API处理方法 - 企业三要素认证
func ProcessQYGL3YSBRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGL3YSBReq
if err := json.Unmarshal(params, &paramsDto); 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{}{
"legalPerson": paramsDto.LegalPerson,
"companyName": paramsDto.EntName,
"creditNo": paramsDto.EntCode,
}
apiPath := "/v4/company-three/check" // 接口路径,根据数脉文档填写(如 v4/xxx
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
if err != nil {
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 if errors.Is(err, shumai.ErrNotFound) {
// 查无记录
return nil, errors.Join(processors.ErrNotFound, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -8,10 +8,31 @@ import (
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/shujubao"
)
// QYGL4B2EItem 返回列表项
type QYGL4B2EItem struct {
PublishTime string `json:"publish_time"`
CaseType string `json:"case_type"`
ID int64 `json:"id"`
Department string `json:"department"`
TaxpayerName string `json:"taxpayer_name"`
}
// QYGL4B2EResponse 最终返回结构
type QYGL4B2EResponse struct {
Total int64 `json:"total"`
Items []QYGL4B2EItem `json:"items"`
}
// ProcessQYGL4B2ERequest QYGL4B2E API处理方法 - 税收违法
func ProcessQYGL4B2ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
func ProcessQYGL4B2ERequest(
ctx context.Context,
params []byte,
deps *processors.ProcessorDependencies,
) ([]byte, error) {
var paramsDto dto.QYGL5A3CReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
@@ -21,39 +42,90 @@ func ProcessQYGL4B2ERequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 设置默认值
// 默认值
pageSize := paramsDto.PageSize
if pageSize == 0 {
pageSize = int64(20)
pageSize = 20
}
pageNum := paramsDto.PageNum
if pageNum == 0 {
pageNum = int64(1)
pageNum = 1
}
// 构建API调用参数
apiParams := map[string]string{
"keyword": paramsDto.EntCode,
"pageSize": strconv.FormatInt(pageSize, 10),
"pageNum": strconv.FormatInt(pageNum, 10),
// 调用外部接口
reqParams := map[string]interface{}{
"key": "c67673dd2e92deb2d2ec91b87bb0a81c",
"creditCode": paramsDto.EntCode,
}
// 调用天眼查API - 税收违法
response, err := deps.TianYanChaService.CallAPI(ctx, "TaxContravention", apiParams)
apiPath := "/communication/personal/10233"
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
if err != nil {
return nil, convertTianYanChaError(err)
if errors.Is(err, shujubao.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, shujubao.ErrQueryEmpty) {
data = map[string]interface{}{}
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 检查天眼查API调用是否成功
if !response.Success {
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
// 原始返回结构处理 - data 是 map[string]interface{} 类型
dataMap, ok := data.(map[string]interface{})
if !ok {
return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型"))
}
// 返回天眼查响应数据
respBytes, err := json.Marshal(response.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
// 提取 total
total := int64(0)
if v, ok := dataMap["total"]; ok {
switch val := v.(type) {
case float64:
total = int64(val)
case int:
total = int64(val)
case string:
total, _ = strconv.ParseInt(val, 10, 64)
}
}
return respBytes, nil
// 提取 items 数组
srcItems := make([]map[string]interface{}, 0)
if v, ok := dataMap["items"]; ok {
if arr, ok := v.([]interface{}); ok {
for _, item := range arr {
if itemMap, ok := item.(map[string]interface{}); ok {
srcItems = append(srcItems, itemMap)
}
}
}
}
// 构造返回数据
resp := QYGL4B2EResponse{
Total: total,
Items: make([]QYGL4B2EItem, 0, len(srcItems)),
}
getString := func(item map[string]interface{}, key string) string {
if v, ok := item[key]; ok {
if str, ok := v.(string); ok {
return str
}
}
return ""
}
for i, v := range srcItems {
resp.Items = append(resp.Items, QYGL4B2EItem{
ID: int64(i + 1),
TaxpayerName: getString(v, "entityName"),
Department: getString(v, "belongDepartment"),
CaseType: getString(v, "caseType"),
PublishTime: getString(v, "illegalTime"),
})
}
return json.Marshal(resp)
}

View File

@@ -0,0 +1,49 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/shumai"
)
// ProcessQYGL4YABRequest QYGL4YAB API处理方法 - 企业四要素认证
func ProcessQYGL4YABRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGL4YABReq
if err := json.Unmarshal(params, &paramsDto); 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{}{
"idCard": paramsDto.IDCard,
"legalPerson": paramsDto.LegalPerson,
"companyName": paramsDto.EntName,
"creditNo": paramsDto.EntCode,
}
apiPath := "/v4/company-four/check" // 接口路径,根据数脉文档填写(如 v4/xxx
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
if err != nil {
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 if errors.Is(err, shumai.ErrNotFound) {
// 查无记录
return nil, errors.Join(processors.ErrNotFound, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -38,8 +38,10 @@ func ProcessQYGL6S1BRequest(ctx context.Context, params []byte, deps *processors
}
if errors.Is(err, shujubao.ErrQueryEmpty) {
return nil, errors.Join(processors.ErrNotFound, err)
// data = map[string]interface{}{}
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
// 解析响应中的 JSON 字符串(使用 RecursiveParse

View File

@@ -8,8 +8,59 @@ import (
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/shujubao"
)
/*
## 返回字段说明
| 返回值字段 | 字段类型 | 字段说明 | 备注 |
|------------|----------|----------|------|
| total | Number | int(11) | 总数 |
| items | Array | | |
| _child | Object | | |
| taxIdNumber | String | varchar(150) | 纳税人识别号 |
| newOwnTaxBalance | String | varchar(20) | 当前新发生欠税余额 |
| ownTaxAmount | String | varchar(50) | 欠税金额 |
| publishDate | String | 日期 | 发布时间 |
| ownTaxBalance | String | varchar(20) | 欠税余额 |
| type | String | varchar(10) | 税务类型 |
| personIdNumber | String | varchar(150) | 证件号码 |
| taxCategory | String | varchar(255) | 欠税税种 |
| taxpayerType | String | varchar(10) | 纳税人类型 |
| personIdName | String | varchar(50) | 法人证件名称 |
| name | String | varchar(255) | 纳税人名称 |
| location | String | varchar(150) | 经营地点 |
| department | String | varchar(200) | 税务机关 |
| regType | String | varchar(50) | 注册类型 |
| legalpersonName | String | varchar(150) | 法人或负责人名称 |
*/
// QYGL7D9AItem 返回列表项
type QYGL7D9AItem struct {
TaxIdNumber string `json:"taxIdNumber"`
NewOwnTaxBalance string `json:"newOwnTaxBalance"`
OwnTaxAmount string `json:"ownTaxAmount"`
PublishDate string `json:"publishDate"`
OwnTaxBalance string `json:"ownTaxBalance"`
Type string `json:"type"`
PersonIdNumber string `json:"personIdNumber"`
TaxCategory string `json:"taxCategory"`
TaxpayerType string `json:"taxpayerType"`
PersonIdName string `json:"personIdName"`
Name string `json:"name"`
Location string `json:"location"`
Department string `json:"department"`
RegType string `json:"regType"`
LegalpersonName string `json:"legalpersonName"`
}
// QYGL7D9AResponse 最终返回结构
type QYGL7D9AResponse struct {
Total int64 `json:"total"`
Items []QYGL7D9AItem `json:"items"`
}
// ProcessQYGL7D9ARequest QYGL7D9A API处理方法 - 欠税公告
func ProcessQYGL7D9ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGL5A3CReq
@@ -21,39 +72,100 @@ func ProcessQYGL7D9ARequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 设置默认值
// 默认值
pageSize := paramsDto.PageSize
if pageSize == 0 {
pageSize = int64(20)
pageSize = 20
}
pageNum := paramsDto.PageNum
if pageNum == 0 {
pageNum = int64(1)
pageNum = 1
}
// 构建API调用参数
apiParams := map[string]string{
"keyword": paramsDto.EntCode,
"pageSize": strconv.FormatInt(pageSize, 10),
"pageNum": strconv.FormatInt(pageNum, 10),
// 调用外部接口
reqParams := map[string]interface{}{
"key": "9ad1365cb0580863a239b0255649fb1a",
"creditCode": paramsDto.EntCode,
}
// 调用天眼查API - 欠税公告
response, err := deps.TianYanChaService.CallAPI(ctx, "OwnTax", apiParams)
apiPath := "/communication/personal/10235"
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
if err != nil {
return nil, convertTianYanChaError(err)
if errors.Is(err, shujubao.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, shujubao.ErrQueryEmpty) {
data = map[string]interface{}{}
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 检查天眼查API调用是否成功
if !response.Success {
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
// 原始返回结构处理 - data 是 map[string]interface{} 类型
dataMap, ok := data.(map[string]interface{})
if !ok {
return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型"))
}
// 返回天眼查响应数据
respBytes, err := json.Marshal(response.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
// 提取 total
totalStr := ""
if v, ok := dataMap["total"]; ok {
if str, ok := v.(string); ok {
totalStr = str
}
}
return respBytes, nil
// 提取 items 数组
srcItems := make([]map[string]interface{}, 0)
if v, ok := dataMap["items"]; ok {
if arr, ok := v.([]interface{}); ok {
for _, item := range arr {
if itemMap, ok := item.(map[string]interface{}); ok {
srcItems = append(srcItems, itemMap)
}
}
}
}
// 构造返回数据(缺失字段留空字符串)
resp := QYGL7D9AResponse{
Total: 0,
Items: make([]QYGL7D9AItem, 0, len(srcItems)),
}
getString := func(item map[string]interface{}, key string) string {
if v, ok := item[key]; ok {
if str, ok := v.(string); ok {
return str
}
}
return ""
}
for _, v := range srcItems {
resp.Items = append(resp.Items, QYGL7D9AItem{
TaxIdNumber: getString(v, "taxpayerCode"),
NewOwnTaxBalance: getString(v, "thisOwedAmount"),
OwnTaxAmount: getString(v, "totalOwedAmount"),
PublishDate: getString(v, "publishDate"),
OwnTaxBalance: getString(v, "beforeOwedAmount"),
Type: getString(v, "taxBureauType"),
PersonIdNumber: "",
TaxCategory: getString(v, "taxOwedType"),
TaxpayerType: "",
PersonIdName: "",
Name: getString(v, "entityName"),
Location: getString(v, "businessAddress"),
Department: getString(v, "taxBureauName"),
RegType: "",
LegalpersonName: getString(v, "legalPerson"),
})
}
// total 转 int64
if totalStr != "" {
resp.Total, _ = strconv.ParseInt(totalStr, 10, 64)
}
return json.Marshal(resp)
}

View File

@@ -8,8 +8,50 @@ import (
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/shujubao"
)
/*
## 返回字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| items | array | 融资历史记录列表,无记录时为空数组 |
| items[].round | string | 融资轮次 |
| items[].amount | string | 融资金额 |
| items[].date | string | 融资日期格式YYYY-MM-DD |
| items[].investors | array of string | 投资方列表 |
| items[].postValuation | string | 融资后估值 |
| items[].preValuation | string | 融资前估值 |
| items[].currency | string | 币种 |
| items[].intro | string | 项目简介 |
| items[].shareRatio | string | 持股比例 |
| total | integer | 符合条件的融资记录总数,无记录时为 0 |
| pageNum | integer | 当前页码 |
| pageSize | integer | 每页记录条数 |
*/
// QYGL8B4DItem 融资历史单项
type QYGL8B4DItem struct {
Round string `json:"round"`
Amount string `json:"amount"`
Date string `json:"date"`
Investors []string `json:"investors"`
PostValuation string `json:"postValuation"`
PreValuation string `json:"preValuation"`
Currency string `json:"currency"`
Intro string `json:"intro"`
ShareRatio string `json:"shareRatio"`
}
// QYGL8B4DResponse 融资历史返回结构
type QYGL8B4DResponse struct {
Items []QYGL8B4DItem `json:"items"`
Total int64 `json:"total"`
PageNum int64 `json:"pageNum"`
PageSize int64 `json:"pageSize"`
}
// ProcessQYGL8B4DRequest QYGL8B4D API处理方法 - 融资历史
func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGL8B4DReq
@@ -21,7 +63,7 @@ func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 设置默认值
// 默认值
pageSize := paramsDto.PageSize
if pageSize == 0 {
pageSize = int64(20)
@@ -31,29 +73,100 @@ func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors
pageNum = int64(1)
}
// 构建API调用参数
apiParams := map[string]string{
"keyword": paramsDto.EntCode,
"pageSize": strconv.FormatInt(pageSize, 10),
"pageNum": strconv.FormatInt(pageNum, 10),
// 调用外部接口
reqParams := map[string]interface{}{
"key": "8887a17748d767b0a1f417171108873f",
"creditCode": paramsDto.EntCode,
}
// 调用天眼查API - 融资历史
response, err := deps.TianYanChaService.CallAPI(ctx, "FinancingHistory", apiParams)
apiPath := "/communication/personal/10435"
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
if err != nil {
return nil, convertTianYanChaError(err)
if errors.Is(err, shujubao.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, shujubao.ErrQueryEmpty) {
data = map[string]interface{}{}
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 检查天眼查API调用是否成功
if !response.Success {
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
// 原始返回结构处理 - data 是 map[string]interface{} 类型
dataMap, ok := data.(map[string]interface{})
if !ok {
return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型"))
}
// 返回天眼查响应数据
respBytes, err := json.Marshal(response.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
// 提取 page 对象中的 totalRecords
totalRecords := ""
if page, ok := dataMap["page"].(map[string]interface{}); ok {
if v, ok := page["totalRecords"]; ok {
if str, ok := v.(string); ok {
totalRecords = str
}
}
}
return respBytes, nil
// 提取 records 数组(不是 items
records := make([]map[string]interface{}, 0)
if v, ok := dataMap["records"]; ok {
if arr, ok := v.([]interface{}); ok {
for _, item := range arr {
if itemMap, ok := item.(map[string]interface{}); ok {
records = append(records, itemMap)
}
}
}
}
// 构造返回数据
resp := QYGL8B4DResponse{
Items: make([]QYGL8B4DItem, 0, len(records)),
PageNum: pageNum,
PageSize: pageSize,
}
for _, item := range records {
// 提取字段,使用类型断言
getString := func(key string) string {
if v, ok := item[key]; ok {
if str, ok := v.(string); ok {
return str
}
}
return ""
}
investAmount := getString("investAmount")
afterValuation := getString("afterInvestValuation")
// 计算持股比例:持股比例 = 本轮投资金额 / 投后估值 × 100%
shareRatio := ""
investAmountFloat, err1 := strconv.ParseFloat(investAmount, 64)
afterValuationFloat, err2 := strconv.ParseFloat(afterValuation, 64)
if err1 == nil && err2 == nil && afterValuationFloat > 0 {
ratio := investAmountFloat / afterValuationFloat * 100
shareRatio = strconv.FormatFloat(ratio, 'f', 2, 64) // 保留两位小数
}
resp.Items = append(resp.Items, QYGL8B4DItem{
Round: getString("financingRound"),
Amount: investAmount,
Date: getString("financingDate"),
Investors: []string{getString("financingRoundInvestor")},
PostValuation: afterValuation,
PreValuation: getString("beforeInvestValuation"),
Currency: getString("currency"),
Intro: "",
ShareRatio: shareRatio,
})
}
// total
if totalRecords != "" {
resp.Total, _ = strconv.ParseInt(totalRecords, 10, 64)
}
return json.Marshal(resp)
}

View File

@@ -0,0 +1,55 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/huibo"
)
// ProcessQYGLBH7YRequest QYGLBH7Y API处理方法 - 企业案件查询汇博
func ProcessQYGLBH7YRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGLBH7YReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if deps.HuiboService == nil {
return nil, errors.Join(processors.ErrSystem, errors.New("汇博服务未初始化"))
}
reqdata := map[string]interface{}{
"companyName": paramsDto.EntName,
}
respBytes, err := deps.HuiboService.CallAPI2(ctx, "E_004_0261", reqdata)
if err != nil {
return nil, errors.Join(processors.ErrDatasource, err)
}
// 解析响应
var response huibo.CallAPI2Response
if err := json.Unmarshal(respBytes, &response); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
// 处理状态码
switch response.Code {
case huibo.CallAPI2StatusSuccess:
// 查询成功
if response.Data == nil {
return []byte("{}"), nil
}
return respBytes, nil
case huibo.CallAPI2StatusNoData:
// 查询成功,无数据 - 按产品约定按调用成功计费
return []byte("{}"), nil
default:
// 其他错误状态码
message := huibo.GetCallAPI2StatusMessage(response.Code)
return nil, errors.Join(processors.ErrDatasource, errors.New(message))
}
}

View File

@@ -0,0 +1,48 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/shumai"
)
// ProcessQYGLDG77Request QYGLDG77 API处理方法 - 企业打款认证
func ProcessQYGLDG77Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGLDG77Req
if err := json.Unmarshal(params, &paramsDto); 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{}{
"companyName": paramsDto.EntName,
"accountNo": paramsDto.AccountNo,
"accountBank": paramsDto.AccountBank,
}
apiPath := "/v2/company/dkrz/check" // 接口路径,根据数脉文档填写(如 v4/xxx
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
if err != nil {
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 if errors.Is(err, shumai.ErrNotFound) {
// 查无记录
return nil, errors.Join(processors.ErrNotFound, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -35,6 +35,8 @@ type Certification struct {
EsignFlowID string `gorm:"type:varchar(500)" json:"esign_flow_id,omitempty" comment:"签署流程ID"`
ContractURL string `gorm:"type:varchar(500)" json:"contract_url,omitempty" comment:"合同文件访问链接"`
ContractSignURL string `gorm:"type:varchar(500)" json:"contract_sign_url,omitempty" comment:"合同签署链接"`
// ContractCode 合作协议编号(与电子合同模板控件 xybh 一致,签署完成后写入用户域合同)
ContractCode string `gorm:"type:varchar(255)" json:"contract_code,omitempty" comment:"合作协议编号"`
// === 失败信息 ===
FailureReason enums.FailureReason `gorm:"type:varchar(100)" json:"failure_reason,omitempty" comment:"失败原因"`
@@ -323,6 +325,11 @@ func (c *Certification) ApplyContract(EsignFlowID string, ContractSignURL string
return nil
}
// SetContractCode 设置合作协议编号(首次生成合同时写入,后续换文件复用)
func (c *Certification) SetContractCode(code string) {
c.ContractCode = code
}
// AddContractFileID 生成合同文件
func (c *Certification) AddContractFileID(contractFileID string, contractURL string) error {
c.ContractFileID = contractFileID

View File

@@ -435,3 +435,31 @@ func (s *ProductManagementService) ListProductsWithSubscriptionStatus(ctx contex
return products, subscriptionStatusMap, total, nil
}
// GetAllProductsForDictionary 获取所有启用且可见的产品(用于导出产品字典)
func (s *ProductManagementService) GetAllProductsForDictionary(ctx context.Context) ([]*entities.Product, error) {
// 构建查询条件:启用且可见
isEnabled := true
isVisible := true
filters := map[string]interface{}{
"is_enabled": isEnabled,
"is_visible": isVisible,
}
options := interfaces.ListOptions{
Page: 1,
PageSize: 1000, // 获取所有产品
Sort: "id", // 按产品ID排序避免JOIN问题
Order: "asc",
}
// 获取产品列表
products, _, err := s.ListProducts(ctx, filters, options)
if err != nil {
s.logger.Error("获取产品字典数据失败", zap.Error(err))
return nil, fmt.Errorf("获取产品字典数据失败: %w", err)
}
return products, nil
}

View File

@@ -119,6 +119,58 @@ func NewContractInfo(enterpriseInfoID, userID, contractName string, contractType
return contractInfo, nil
}
// NewContractInfoWithContractCode 使用指定合同编号创建合同信息(与认证阶段生成的编号一致)
func NewContractInfoWithContractCode(enterpriseInfoID, userID, contractName string, contractType ContractType, contractFileID, contractFileURL, contractCode string) (*ContractInfo, error) {
if enterpriseInfoID == "" {
return nil, fmt.Errorf("企业信息ID不能为空")
}
if userID == "" {
return nil, fmt.Errorf("用户ID不能为空")
}
if contractName == "" {
return nil, fmt.Errorf("合同名称不能为空")
}
if contractType == "" {
return nil, fmt.Errorf("合同类型不能为空")
}
if contractFileID == "" {
return nil, fmt.Errorf("合同文件ID不能为空")
}
if contractFileURL == "" {
return nil, fmt.Errorf("合同文件URL不能为空")
}
if contractCode == "" {
return nil, fmt.Errorf("合同编号不能为空")
}
if !isValidContractType(contractType) {
return nil, fmt.Errorf("无效的合同类型: %s", contractType)
}
contractInfo := &ContractInfo{
ID: uuid.New().String(),
EnterpriseInfoID: enterpriseInfoID,
UserID: userID,
ContractCode: contractCode,
ContractName: contractName,
ContractType: contractType,
ContractFileID: contractFileID,
ContractFileURL: contractFileURL,
domainEvents: make([]interface{}, 0),
}
contractInfo.addDomainEvent(&ContractInfoCreatedEvent{
ContractInfoID: contractInfo.ID,
EnterpriseInfoID: enterpriseInfoID,
UserID: userID,
ContractCode: contractCode,
ContractName: contractName,
ContractType: string(contractType),
CreatedAt: time.Now(),
})
return contractInfo, nil
}
// ================ 聚合根核心方法 ================
// UpdateContractInfo 更新合同信息

View File

@@ -14,6 +14,7 @@ import (
type ContractAggregateService interface {
// 聚合根生命周期管理
CreateContract(ctx context.Context, enterpriseInfoID, userID, contractName string, contractType entities.ContractType, contractFileID, contractFileURL string) (*entities.ContractInfo, error)
CreateContractWithCode(ctx context.Context, enterpriseInfoID, userID, contractName string, contractType entities.ContractType, contractFileID, contractFileURL, contractCode string) (*entities.ContractInfo, error)
LoadContract(ctx context.Context, contractID string) (*entities.ContractInfo, error)
SaveContract(ctx context.Context, contract *entities.ContractInfo) error
DeleteContract(ctx context.Context, contractID string) error
@@ -94,6 +95,51 @@ func (s *ContractAggregateServiceImpl) CreateContract(
return contract, nil
}
// CreateContractWithCode 使用指定合同编号创建合同信息(与认证合同模板上的编号一致)
func (s *ContractAggregateServiceImpl) CreateContractWithCode(
ctx context.Context,
enterpriseInfoID, userID, contractName string,
contractType entities.ContractType,
contractFileID, contractFileURL, contractCode string,
) (*entities.ContractInfo, error) {
s.logger.Debug("创建合同信息(指定编号)",
zap.String("enterprise_info_id", enterpriseInfoID),
zap.String("user_id", userID),
zap.String("contract_name", contractName),
zap.String("contract_code", contractCode),
zap.String("contract_type", string(contractType)))
exists, err := s.ExistsByContractFileID(ctx, contractFileID)
if err != nil {
return nil, fmt.Errorf("检查合同文件ID失败: %w", err)
}
if exists {
return nil, fmt.Errorf("合同文件ID已存在")
}
contract, err := entities.NewContractInfoWithContractCode(enterpriseInfoID, userID, contractName, contractType, contractFileID, contractFileURL, contractCode)
if err != nil {
return nil, fmt.Errorf("创建合同信息失败: %w", err)
}
if err := s.ValidateBusinessRules(ctx, contract); err != nil {
return nil, fmt.Errorf("业务规则验证失败: %w", err)
}
err = s.SaveContract(ctx, contract)
if err != nil {
return nil, fmt.Errorf("保存合同信息失败: %w", err)
}
s.logger.Info("合同信息创建成功",
zap.String("contract_id", contract.ID),
zap.String("enterprise_info_id", enterpriseInfoID),
zap.String("contract_code", contractCode),
zap.String("contract_name", contractName))
return contract, nil
}
// LoadContract 加载合同信息
func (s *ContractAggregateServiceImpl) LoadContract(ctx context.Context, contractID string) (*entities.ContractInfo, error) {
s.logger.Debug("加载合同信息", zap.String("contract_id", contractID))

View File

@@ -149,6 +149,28 @@ func (r *GormApiCallRepository) ListByUserIdWithFiltersAndProductName(ctx contex
// 应用筛选条件
if filters != nil {
// 产品ID筛选支持多个
if productIds, ok := filters["product_ids"].(string); ok && productIds != "" {
// 多个产品ID逗号分隔
productIdsList := strings.Split(productIds, ",")
// 去除空白字符
var cleanProductIds []string
for _, id := range productIdsList {
id = strings.TrimSpace(id)
if id != "" {
cleanProductIds = append(cleanProductIds, id)
}
}
if len(cleanProductIds) > 0 {
placeholders := strings.Repeat("?,", len(cleanProductIds))
placeholders = placeholders[:len(placeholders)-1] // 移除最后一个逗号
whereCondition += " AND ac.product_id IN (" + placeholders + ")"
for _, id := range cleanProductIds {
whereArgs = append(whereArgs, id)
}
}
}
// 时间范围筛选
if startTime, ok := filters["start_time"].(time.Time); ok {
whereCondition += " AND ac.created_at >= ?"
@@ -337,6 +359,28 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
whereArgs = append(whereArgs, userId)
}
// 产品ID筛选支持多个
if productIds, ok := filters["product_ids"].(string); ok && productIds != "" {
// 多个产品ID逗号分隔
productIdsList := strings.Split(productIds, ",")
// 去除空白字符
var cleanProductIds []string
for _, id := range productIdsList {
id = strings.TrimSpace(id)
if id != "" {
cleanProductIds = append(cleanProductIds, id)
}
}
if len(cleanProductIds) > 0 {
placeholders := strings.Repeat("?,", len(cleanProductIds))
placeholders = placeholders[:len(placeholders)-1] // 移除最后一个逗号
whereCondition += " AND ac.product_id IN (" + placeholders + ")"
for _, id := range cleanProductIds {
whereArgs = append(whereArgs, id)
}
}
}
// 时间范围筛选
if startTime, ok := filters["start_time"].(time.Time); ok {
whereCondition += " AND ac.created_at >= ?"

View File

@@ -3,6 +3,8 @@ package repositories
import (
"context"
"errors"
"strings"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/domains/product/repositories/queries"
@@ -165,13 +167,33 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries
// 应用排序
if query.SortBy != "" {
order := query.SortBy
if query.SortOrder == "desc" {
order += " DESC"
// 检查是否是关联表字段排序
if strings.Contains(query.SortBy, ".") {
parts := strings.Split(query.SortBy, ".")
if len(parts) == 2 {
// 关联表字段排序需要JOIN表
joinTable := parts[0]
sortField := parts[1]
dbQuery = dbQuery.Joins("JOIN "+joinTable+" ON products.category_id = "+joinTable+".id")
order := joinTable + "." + sortField
if query.SortOrder == "desc" {
order += " DESC"
} else {
order += " ASC"
}
dbQuery = dbQuery.Order(order)
}
} else {
order += " ASC"
// 本表字段排序
order := query.SortBy
if query.SortOrder == "desc" {
order += " DESC"
} else {
order += " ASC"
}
dbQuery = dbQuery.Order(order)
}
dbQuery = dbQuery.Order(order)
} else {
dbQuery = dbQuery.Order("created_at DESC")
}
@@ -183,7 +205,7 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries
}
// 预加载分类信息并获取数据
if err := dbQuery.Preload("Category").Find(&productEntities).Error; err != nil {
if err := dbQuery.Preload("Category").Preload("SubCategory").Find(&productEntities).Error; err != nil {
return nil, 0, err
}

View File

@@ -0,0 +1,90 @@
自然人公开涉诉信息查询接口文档
接口编码BHSC-P_004_0271
版本V1.0
实施日期2026-04-13
适用场景:合作厂家接入中胜信用平台,用于个人涉诉案件查询(含失信、限高)
1. 通信说明
请求方式HTTP POST
数据格式JSON
编码格式UTF-8
安全要求:请求 IP 需提前绑定
2. 请求信息
请求地址
http://host:port/api/data
请求头Header
表格
参数名 含义 必填 类型 备注
AppCode 接口授权码 Y String 接口服务商提供
pcode 产品编码 Y String 固定值P_004_0271
请求体Body
表格
参数名 含义 必填 类型 说明
name 姓名 Y String 支持明文 / MD5 加密
idCard 身份证号 Y String 支持明文 / MD5 加密
请求示例(明文)
json
{
"idCard": "2103111*****0",
"name": "*****"
}
请求示例(加密)
json
{
"name": "MD5:3e29aae20b7b92775*****",
"idCard": "MD5:a0c28f3a5a*****14"
}
3. 响应信息
表格
字段名 含义 类型 备注
code 状态码 String 参考状态码说明
message 描述信息 String -
orderNo 订单号 String -
pcode 产品编码 String 与请求一致
param 请求参数 Object 原样返回
charge 是否收费 Boolean true = 收费false = 不收费
time 响应时间戳 String 13 位毫秒
data 业务数据 Object 涉诉 / 失信 / 限高数据
响应示例
json
{
"code": "100",
"orderNo": "1361269246899077120",
"charge": true,
"data": {
"ss": {
"preservation": { "count": {} },
"crc": 35****4186,
"cases_tree": {},
"administrative": {},
"civil": {},
"count": {},
"implement": {},
"criminal": {},
"bankrupt": {}
},
"sxbzxr": [{}],
"xgbzxr": []
},
"pcode": "P_004_0271",
"param": null,
"time": "1744593480538",
"message": "查询成功"
}
data 节点说明
ss涉诉案件民事 / 刑事 / 执行 / 行政 / 破产等)
sxbzxr失信被执行人
xgbzxr限制高消费
4. 状态码说明
表格
状态码 描述
100 查询成功
101 参数错误
103 账户不存在
104 IP 限制
105 账号已过期
107 服务不存在
108 产品通道已关闭
109 账户资金不足
110 查询成功,无数据
500 未知请求错误
要不要我帮你把这份接口直接写成可上线的小程序请求代码含加密、header、异常捕获、状态码统一处理

View File

@@ -0,0 +1,83 @@
package huibo
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
)
// MD5Encrypt 使用 AppKey 进行 MD5 加密
func MD5Encrypt(data, appKey string) string {
h := md5.New()
h.Write([]byte(data + appKey))
return fmt.Sprintf("%x", h.Sum(nil))
}
// HMACSHA256Base64 使用 HMAC-SHA256 算法生成签名
func HMACSHA256Base64(data, secret string) string {
m := hmac.New(sha256.New, []byte(secret))
_, _ = m.Write([]byte(data))
return base64.StdEncoding.EncodeToString(m.Sum(nil))
}
// EncryptAESGCMBase64 使用 AES-GCM 算法加密数据
func EncryptAESGCMBase64(plainText, base64Key string) (string, error) {
key, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
iv := make([]byte, 12)
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
ciphertext := gcm.Seal(nil, iv, []byte(plainText), nil)
out := append(iv, ciphertext...)
return base64.StdEncoding.EncodeToString(out), nil
}
// DecryptAESGCMBase64 使用 AES-GCM 算法解密数据
func DecryptAESGCMBase64(encryptedBase64, base64Key string) (string, error) {
key, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return "", err
}
raw, err := base64.StdEncoding.DecodeString(encryptedBase64)
if err != nil {
return "", err
}
if len(raw) < 13 {
return "", errors.New("密文长度非法")
}
iv := raw[:12]
ciphertext := raw[12:]
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
plain, err := gcm.Open(nil, iv, ciphertext, nil)
if err != nil {
return "", err
}
return string(plain), nil
}

View File

@@ -0,0 +1,432 @@
package huibo
import (
"encoding/base64"
"testing"
)
// 测试 MD5 加密(固定密文对比验证)
func TestMD5Encrypt(t *testing.T) {
appKey := "a6c9935e967894e731c62ecfcd9b7c95"
testCases := []struct {
name string
data string
expected string // 固定密文
}{
{
name: "姓名",
data: "何志勇",
expected: "64d4d5c6457026117a4911acf189e269", // 固定密文
},
{
name: "身份证号",
data: "452528197907133014",
expected: "7c6cc77dabb83d95948904dba5a7219d", // 固定密文
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 加密
result := MD5Encrypt(tc.data, appKey)
// 最核心:对比是否和固定密文一致
if result != tc.expected {
t.Errorf("加密不匹配!\n明文%s\n期望密文%s\n实际密文%s",
tc.data, tc.expected, result)
return
}
// 打印成功日志
t.Logf("✅ 校验成功\n明文%s\n密文%s", tc.data, result)
})
}
}
// 测试 HMAC-SHA256 签名
func TestHMACSHA256Base64(t *testing.T) {
secret := "test_secret_key"
testCases := []struct {
name string
data string
secret string
}{
{
name: "简单字符串",
data: "hello world",
secret: secret,
},
{
name: "JSON数据",
data: `{"name":"张三","idCard":"110101199003072345"}`,
secret: secret,
},
{
name: "URL参数",
data: "idCard=110101199003072345&name=张三&productCode=22089",
secret: secret,
},
{
name: "空字符串",
data: "",
secret: secret,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := HMACSHA256Base64(tc.data, tc.secret)
// 验证结果不为空
if result == "" {
t.Error("HMAC-SHA256签名结果为空")
}
// 验证结果是有效的Base64
_, err := base64.StdEncoding.DecodeString(result)
if err != nil {
t.Errorf("HMAC-SHA256结果不是有效的Base64: %v", err)
}
// 验证相同输入产生相同输出
result2 := HMACSHA256Base64(tc.data, tc.secret)
if result != result2 {
t.Error("相同输入产生的签名不一致")
}
// 验证不同输入产生不同输出
result3 := HMACSHA256Base64(tc.data+"x", tc.secret)
if result == result3 {
t.Error("不同输入产生的签名相同")
}
t.Logf("数据: %s, 签名: %s", tc.data, result)
})
}
}
// 测试 AES-GCM 加密解密
func TestEncryptDecryptAESGCMBase64(t *testing.T) {
// 生成一个有效的 Base64 编密的 AES 密钥
key := make([]byte, 32) // AES-256
for i := range key {
key[i] = byte(i)
}
base64Key := base64.StdEncoding.EncodeToString(key)
testCases := []struct {
name string
data string
}{
{
name: "简单文本",
data: "hello world",
},
{
name: "中文文本",
data: "你好世界",
},
{
name: "JSON数据",
data: `{"name":"张三","idCard":"110101199003072345"}`,
},
{
name: "长文本",
data: "这是一个很长的文本用来测试加密解密功能是否正常工作。包含各种字符123456789!@#$%^&*()_+-=[]{}|;':\",./<>?",
},
{
name: "空字符串",
data: "",
},
{
name: "特殊字符",
data: "\n\t\r\x00",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 加密
encrypted, err := EncryptAESGCMBase64(tc.data, base64Key)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
// 验证加密结果不为空
if encrypted == "" {
t.Error("加密结果为空")
}
// 验证加密结果是有效的Base64
_, err = base64.StdEncoding.DecodeString(encrypted)
if err != nil {
t.Errorf("加密结果不是有效的Base64: %v", err)
}
// 验证加密结果与原文不同
if encrypted == tc.data {
t.Error("加密结果与原文相同")
}
// 解密
decrypted, err := DecryptAESGCMBase64(encrypted, base64Key)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
// 验证解密结果与原文一致
if decrypted != tc.data {
t.Errorf("解密结果不匹配,期望: %s, 实际: %s", tc.data, decrypted)
}
t.Logf("原文: %s", tc.data)
t.Logf("密文: %s", encrypted)
t.Logf("解密: %s", decrypted)
})
}
}
// 测试错误的密钥
func TestEncryptDecryptWithWrongKey(t *testing.T) {
// 生成两个不同的密钥
key1 := make([]byte, 32)
for i := range key1 {
key1[i] = byte(i)
}
base64Key1 := base64.StdEncoding.EncodeToString(key1)
key2 := make([]byte, 32)
for i := range key2 {
key2[i] = byte(i + 1)
}
base64Key2 := base64.StdEncoding.EncodeToString(key2)
data := "sensitive data"
// 用密钥1加密
encrypted, err := EncryptAESGCMBase64(data, base64Key1)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
// 用密钥2解密
decrypted, err := DecryptAESGCMBase64(encrypted, base64Key2)
if err == nil {
t.Error("用错误密钥解密应该返回错误")
}
// 验证解密结果与原文不同(如果解密成功的话)
if decrypted == data {
t.Error("用错误密钥解密不应该得到正确结果")
}
t.Logf("用错误密钥解密预期失败: %v", err)
}
// 测试无效的 Base64 密钥
func TestEncryptWithInvalidBase64Key(t *testing.T) {
data := "test data"
invalidKeys := []string{
"", // 空字符串
"not_base64", // 非Base64
"abc", // 解码后太短
}
for _, invalidKey := range invalidKeys {
_, err := EncryptAESGCMBase64(data, invalidKey)
if err == nil {
t.Errorf("使用无效密钥 %s 应该返回错误", invalidKey)
}
t.Logf("无效密钥 %s 预期失败: %v", invalidKey, err)
}
}
// 测试解密无效数据
func TestDecryptWithInvalidData(t *testing.T) {
// 生成一个有效的密钥
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
base64Key := base64.StdEncoding.EncodeToString(key)
invalidData := []string{
"", // 空字符串
"invalid_base64", // 非Base64
"dGVzdA==", // 有效的Base64但不是AES-GCM数据
"short", // 太短
}
for _, data := range invalidData {
_, err := DecryptAESGCMBase64(data, base64Key)
if err == nil {
t.Errorf("使用无效数据 %s 应该返回错误", data)
}
t.Logf("无效数据 %s 预期失败: %v", data, err)
}
}
// 测试加密结果的唯一性
func TestEncryptionUniqueness(t *testing.T) {
// 生成一个有效的密钥
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
base64Key := base64.StdEncoding.EncodeToString(key)
data := "same data"
// 加密多次
results := make([]string, 10)
for i := 0; i < 10; i++ {
encrypted, err := EncryptAESGCMBase64(data, base64Key)
if err != nil {
t.Fatalf("第%d次加密失败: %v", i, err)
}
results[i] = encrypted
}
// 验证每次加密结果都不同因为包含随机IV
uniqueCount := 0
for i := 0; i < len(results); i++ {
isUnique := true
for j := 0; j < len(results); j++ {
if i != j && results[i] == results[j] {
isUnique = false
break
}
}
if isUnique {
uniqueCount++
}
}
if uniqueCount != len(results) {
t.Errorf("加密结果应该唯一,实际上只有%d个唯一结果期望%d个", uniqueCount, len(results))
}
t.Logf("生成了%d个不同的加密结果", uniqueCount)
}
// 测试使用真实配置的加密解密
func TestEncryptionWithRealConfig(t *testing.T) {
// 使用配置文件中的真实AES密钥
aesKey := "NQYN3YO+pb/GEcCBNX0ptMb7cUlnXSPvcX7VvNofBkc="
appKey := "a6c9935e967894e731c62ecfcd9b7c95"
// 测试数据
testData := `{"name":"张三","idCard":"110101199003072345","productCode":"22089"}`
t.Run("MD5加密", func(t *testing.T) {
// 测试 MD5 加密
md5Result := MD5Encrypt("张三", appKey)
t.Logf("姓名 MD5: %s", md5Result)
md5Result2 := MD5Encrypt("110101199003072345", appKey)
t.Logf("身份证号 MD5: %s", md5Result2)
// 验证格式
if len(md5Result) != 32 {
t.Errorf("MD5结果长度错误期望32位实际%d位", len(md5Result))
}
})
t.Run("HMAC-SHA256签名", func(t *testing.T) {
// 生成排序后的参数
sortedParam := "idCard=110101199003072345&name=张三&productCode=22089"
signature := HMACSHA256Base64(sortedParam, aesKey)
t.Logf("签名参数: %s", sortedParam)
t.Logf("HMAC-SHA256签名: %s", signature)
// 验证格式
_, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
t.Errorf("签名不是有效的Base64: %v", err)
}
})
t.Run("AES-GCM加密解密", func(t *testing.T) {
// 测试 AES-GCM 加密
encrypted, err := EncryptAESGCMBase64(testData, aesKey)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
t.Logf("原始数据: %s", testData)
t.Logf("加密结果: %s", encrypted)
// 测试解密
decrypted, err := DecryptAESGCMBase64(encrypted, aesKey)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
t.Logf("解密结果: %s", decrypted)
// 验证结果
if decrypted != testData {
t.Errorf("解密结果不匹配")
}
})
}
// 基准测试
func BenchmarkMD5Encrypt(b *testing.B) {
data := "张三"
appKey := "a6c9935e967894e731c62ecfcd9b7c95"
b.ResetTimer()
for i := 0; i < b.N; i++ {
MD5Encrypt(data, appKey)
}
}
func BenchmarkHMACSHA256Base64(b *testing.B) {
data := "idCard=110101199003072345&name=张三"
secret := "a6c9935e967894e731c62ecfcd9b7c95"
b.ResetTimer()
for i := 0; i < b.N; i++ {
HMACSHA256Base64(data, secret)
}
}
func BenchmarkEncryptAESGCMBase64(b *testing.B) {
data := `{"name":"张三","idCard":"110101199003072345","productCode":"22089"}`
// 生成密钥
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
base64Key := base64.StdEncoding.EncodeToString(key)
b.ResetTimer()
for i := 0; i < b.N; i++ {
EncryptAESGCMBase64(data, base64Key)
}
}
func BenchmarkDecryptAESGCMBase64(b *testing.B) {
data := `{"name":"张三","idCard":"110101199003072345","productCode":"22089"}`
// 生成密钥
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
base64Key := base64.StdEncoding.EncodeToString(key)
// 先加密一次
encrypted, err := EncryptAESGCMBase64(data, base64Key)
if err != nil {
b.Fatalf("预加密失败: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
DecryptAESGCMBase64(encrypted, base64Key)
}
}

View File

@@ -0,0 +1,59 @@
package huibo
import (
"net/http"
"strings"
)
// generateCurlCommand 从 HTTP 请求生成 curl 命令
func generateCurlCommand(req *http.Request) string {
var cmd strings.Builder
cmd.WriteString("curl -X ")
cmd.WriteString(req.Method)
cmd.WriteString(" '")
cmd.WriteString(req.URL.String())
cmd.WriteString("'")
// 添加请求头
for key, values := range req.Header {
for _, value := range values {
cmd.WriteString(" \\\n -H '")
cmd.WriteString(key)
cmd.WriteString(": ")
cmd.WriteString(value)
cmd.WriteString("'")
}
}
return cmd.String()
}
// generateCurlCommandWithBody 生成包含请求体的 curl 命令
func generateCurlCommandWithBody(method, url string, headers map[string]string, body string) string {
var cmd strings.Builder
cmd.WriteString("curl -X ")
cmd.WriteString(method)
cmd.WriteString(" '")
cmd.WriteString(url)
cmd.WriteString("'")
// 添加请求头
for key, value := range headers {
cmd.WriteString(" \\\n -H '")
cmd.WriteString(key)
cmd.WriteString(": ")
cmd.WriteString(value)
cmd.WriteString("'")
}
// 添加请求体
if body != "" {
cmd.WriteString(" \\\n -d '")
cmd.WriteString(body)
cmd.WriteString("'")
}
cmd.WriteString(" \\\n --compressed")
return cmd.String()
}

View File

@@ -39,6 +39,8 @@ func NewHuiboServiceWithConfig(cfg *config.Config) (*HuiboService, error) {
AESKey: cfg.Huibo.AESKey,
WorkOrderCode: cfg.Huibo.WorkOrderCode,
ProductCode: cfg.Huibo.ProductCode,
BaseURL2: cfg.Huibo.BaseURL2,
AppCode2: cfg.Huibo.AppCode2,
}, logger)
return service, nil

View File

@@ -6,6 +6,7 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
@@ -85,6 +86,8 @@ type HuiboConfig struct {
AESKey string
WorkOrderCode string
ProductCode string
BaseURL2 string // CallAPI2 使用的 URL
AppCode2 string // CallAPI2 使用的 AppCode
}
type HuiboService struct {
@@ -101,10 +104,22 @@ type responseWrapper struct {
} `json:"data"`
}
// CallAPI2Response CallAPI2 的响应结构体
type CallAPI2Response struct {
Code string `json:"code"`
Data map[string]interface{} `json:"data"`
Msg string `json:"msg"`
}
func NewHuiboService(config HuiboConfig, logger *external_logger.ExternalServiceLogger) *HuiboService {
return &HuiboService{config: config, logger: logger}
}
// GetConfig 获取汇博配置
func (s *HuiboService) GetConfig() HuiboConfig {
return s.config
}
// CallEducationBackgroundDetailed 教育背景(详细)查询
func (s *HuiboService) CallEducationBackgroundDetailed(ctx context.Context, name, idCard, authPDFBase64 string) ([]byte, error) {
requestID := s.generateRequestID()
@@ -440,3 +455,158 @@ func randomDigits(n int) string {
}
return string(b)
}
// MD5Encrypt 使用配置的 AppKey 进行 MD5 加密
func (s *HuiboService) MD5Encrypt(data string) string {
h := md5.New()
h.Write([]byte(data + s.config.AppKey))
return fmt.Sprintf("%x", h.Sum(nil))
}
// CallAPI2 通用 HTTP 调用方法,返回原始响应 JSON
func (s *HuiboService) CallAPI2(ctx context.Context, pcode string, requestData map[string]interface{}) ([]byte, error) {
startTime := time.Now()
transactionID := ""
if v, ok := ctx.Value("transaction_id").(string); ok {
transactionID = v
}
if s.logger != nil {
s.logger.LogRequest("", transactionID, "huibo_callapi2", s.config.BaseURL2)
}
if strings.TrimSpace(s.config.BaseURL2) == "" {
return nil, errors.Join(ErrSystem, errors.New("汇博配置不完整BaseURL2为空"))
}
if strings.TrimSpace(s.config.AppCode2) == "" {
return nil, errors.Join(ErrSystem, errors.New("汇博配置不完整AppCode2为空"))
}
reqJSON, err := json.Marshal(requestData)
if err != nil {
return nil, errors.Join(ErrSystem, fmt.Errorf("请求参数序列化失败: %w", err))
}
// 构建 curl 命令的 headers
headers := map[string]string{
"AppCode": s.config.AppCode2,
"pcode": pcode,
"Content-Type": "application/json",
"X-ORDER-CODE": s.config.XOrderCode,
}
// 生成包含请求体的 curl 命令用于日志记录
curlCmd := generateCurlCommandWithBody("POST", s.config.BaseURL2, headers, string(reqJSON))
// 创建 HTTP 请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.config.BaseURL2, bytes.NewBuffer(reqJSON))
if err != nil {
return nil, errors.Join(ErrSystem, fmt.Errorf("创建HTTP请求失败: %w", err))
}
// req.Header.Set(headerAuthorization, s.config.AppID+"::"+s.config.AppKey)
// req.Header.Set(headerWorkOrderCode, s.config.WorkOrderCode)
// req.Header.Set(headerOrderCode, s.config.XOrderCode)
// req.Header.Set(headerSecretIDHdr, s.config.SecretID)
// req.Header.Set(headerAESKeyHdr, s.config.AESKey)
// req.Header.Set("Content-Type", writer.FormDataContentType())
// 设置请求头
req.Header.Set("AppCode", s.config.AppCode2)
req.Header.Set("pcode", pcode)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ORDER-CODE", s.config.XOrderCode)
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
if s.logger != nil {
s.logger.LogErrorWithFields("汇博 CallAPI2 HTTP 请求失败",
zap.String("url", s.config.BaseURL2),
zap.String("pcode", pcode),
zap.String("curl", curlCmd),
zap.Error(err),
)
}
return nil, errors.Join(ErrDatasource, err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
if s.logger != nil {
s.logger.LogErrorWithFields("汇博 CallAPI2 读取响应体失败",
zap.String("url", s.config.BaseURL2),
zap.Int("http_status", resp.StatusCode),
zap.Error(err),
)
}
return nil, errors.Join(ErrSystem, fmt.Errorf("读取响应体失败: %w", err))
}
// 解析响应以检查业务状态码
var response CallAPI2Response
if err := json.Unmarshal(respBody, &response); err != nil {
if s.logger != nil {
s.logger.LogErrorWithFields("汇博 CallAPI2 响应解析失败",
zap.String("url", s.config.BaseURL2),
zap.String("pcode", pcode),
zap.Error(err),
)
}
return nil, errors.Join(ErrDatasource, fmt.Errorf("响应解析失败: %w", err))
}
// 根据业务状态码进行处理
switch response.Code {
case CallAPI2StatusSuccess:
// 查询成功
if s.logger != nil {
s.logger.LogInfo(
"汇博 CallAPI2 查询成功",
zap.String("pcode", pcode),
zap.String("code", response.Code),
zap.String("transaction_id", transactionID),
)
}
case CallAPI2StatusNoData:
// 查询成功,无数据
if s.logger != nil {
s.logger.LogInfo(
"汇博 CallAPI2 查询成功但无数据",
zap.String("pcode", pcode),
zap.String("code", response.Code),
zap.String("transaction_id", transactionID),
)
}
default:
// 其他错误状态码
message := GetCallAPI2StatusMessage(response.Code)
if s.logger != nil {
s.logger.LogErrorWithFields("汇博 CallAPI2 业务状态异常",
zap.String("url", s.config.BaseURL2),
zap.String("pcode", pcode),
zap.String("code", response.Code),
zap.String("message", message),
)
}
return nil, errors.Join(ErrDatasource, fmt.Errorf("业务状态异常(code=%s,msg=%s)", response.Code, message))
}
// 记录 curl 命令和响应
if s.logger != nil {
s.logger.LogInfo(
"汇博 CallAPI2 请求响应",
zap.String("curl", curlCmd),
zap.String("response_body", string(respBody)),
zap.String("transaction_id", transactionID),
)
}
if s.logger != nil {
s.logger.LogResponse("", transactionID, "huibo_callapi2", http.StatusOK, time.Since(startTime))
}
return respBody, nil
}

View File

@@ -0,0 +1,52 @@
package huibo
// CallAPI2 状态码常量
const (
CallAPI2StatusSuccess = "100" // 查询成功
CallAPI2StatusNoData = "110" // 查询成功,无数据
CallAPI2StatusParamError = "101" // 参数错误
CallAPI2StatusAccountError = "103" // 账户不存在
CallAPI2StatusIPError = "104" // IP 限制
CallAPI2StatusExpired = "105" // 账号已过期
CallAPI2StatusServiceError = "107" // 服务不存在
CallAPI2StatusChannelError = "108" // 产品通道已关闭
CallAPI2StatusBalanceError = "109" // 账户资金不足
CallAPI2StatusUnknownError = "500" // 未知请求错误
)
// CallAPI2 状态码对应的错误信息
var CallAPI2StatusMessages = map[string]string{
CallAPI2StatusSuccess: "查询成功",
CallAPI2StatusNoData: "查询成功,无数据",
CallAPI2StatusParamError: "参数错误",
CallAPI2StatusAccountError: "账户不存在",
CallAPI2StatusIPError: "IP 限制",
CallAPI2StatusExpired: "账号已过期",
CallAPI2StatusServiceError: "服务不存在",
CallAPI2StatusChannelError: "产品通道已关闭",
CallAPI2StatusBalanceError: "账户资金不足",
CallAPI2StatusUnknownError: "未知请求错误",
}
// IsCallAPI2Success 判断 CallAPI2 状态码是否为成功(需要扣费)
func IsCallAPI2Success(code string) bool {
return code == CallAPI2StatusSuccess
}
// IsCallAPI2NoData 判断 CallAPI2 状态码是否为无数据(需要扣费)
func IsCallAPI2NoData(code string) bool {
return code == CallAPI2StatusNoData
}
// IsCallAPI2Billable 判断 CallAPI2 状态码是否需要扣费
func IsCallAPI2Billable(code string) bool {
return IsCallAPI2Success(code) || IsCallAPI2NoData(code)
}
// GetCallAPI2StatusMessage 获取 CallAPI2 状态码对应的错误信息
func GetCallAPI2StatusMessage(code string) string {
if msg, ok := CallAPI2StatusMessages[code]; ok {
return msg
}
return "未知状态码: " + code
}

View File

@@ -0,0 +1,38 @@
package nuoer
import (
"crypto/md5"
"encoding/hex"
"sort"
"strings"
)
// Sign 根据 body 业务参数与 secret 生成 MD5 签名。
// 规则:排除空值参数,按 key 的 ASCII 升序排序,拼接「参数名+参数值」后追加 secret再 MD5小写十六进制
func Sign(body map[string]string, secret string) string {
if len(body) == 0 {
return genMD5(secret)
}
keys := make([]string, 0, len(body))
for k, v := range body {
if strings.TrimSpace(v) == "" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
for _, k := range keys {
sb.WriteString(k)
sb.WriteString(body[k])
}
sb.WriteString(secret)
return genMD5(sb.String())
}
func genMD5(s string) string {
sum := md5.Sum([]byte(s))
return hex.EncodeToString(sum[:])
}

View File

@@ -0,0 +1,21 @@
package nuoer
import "testing"
func TestSign(t *testing.T) {
body := map[string]string{
"name": "张三",
"mobile": "13290879000",
"idCard": "330129199511153412",
}
secret := "secret"
got := Sign(body, secret)
if got == "" {
t.Fatal("sign should not be empty")
}
// 文档示例name张三mobile13290879000idCard330129199511153412secret
want := genMD5("idCard330129199511153412mobile13290879000name张三secret")
if got != want {
t.Fatalf("sign mismatch: got %s want %s", got, want)
}
}

View File

@@ -0,0 +1,141 @@
package nuoer
import (
"errors"
"fmt"
)
// 平台层 code 返回码见文档2
const (
CodeSuccess = 0 // 成功
CodeResponseError = -1 // 响应异常
)
// 业务层 busiCode 返回码见文档2
const (
BusiCodeSuccess = 10 // 查询成功【计费】
BusiCodeNotFound = 1000 // 数据未查得
BusiCodeInsufficientFund = 1001 // 账户余额不足
BusiCodeAccountNotFound = 1002 // 账户信息不存在
BusiCodeAppIDError = 1003 // appId异常
BusiCodeProductError = 1004 // 产品编号异常
BusiCodeAccountError = 1005 // 账号信息异常
BusiCodeOverdraftLimit = 1006 // 透支余额已达上限
BusiCodeDataRequestError = 1007 // 数据请求异常
BusiCodeServiceNotOpen = 1009 // 服务尚未开通
)
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
ErrNotFound = errors.New("查询为空")
)
// platformCodeDesc 平台层 code -> 描述
var platformCodeDesc = map[int]string{
CodeSuccess: "成功",
CodeResponseError: "响应异常",
}
// busiCodeDesc 业务层 busiCode -> 描述
var busiCodeDesc = map[int]string{
BusiCodeSuccess: "查询成功【计费】",
BusiCodeNotFound: "数据未查得",
BusiCodeInsufficientFund: "账户余额不足",
BusiCodeAccountNotFound: "账户信息不存在",
BusiCodeAppIDError: "appId异常",
BusiCodeProductError: "产品编号异常",
BusiCodeAccountError: "账号信息异常",
BusiCodeOverdraftLimit: "透支余额已达上限",
BusiCodeDataRequestError: "数据请求异常",
BusiCodeServiceNotOpen: "服务尚未开通",
}
// GetPlatformCodeDesc 根据平台 code 获取描述
func GetPlatformCodeDesc(code int) string {
if desc, ok := platformCodeDesc[code]; ok {
return desc
}
return ""
}
// GetBusiCodeDesc 根据 busiCode 获取描述
func GetBusiCodeDesc(busiCode int) string {
if desc, ok := busiCodeDesc[busiCode]; ok {
return desc
}
return ""
}
// nuoerError 诺尔智汇平台层错误(响应 code 字段)
type nuoerError struct {
Code int
Message string
}
func (e *nuoerError) Error() string {
return fmt.Sprintf("诺尔智汇返回错误code: %dmsg: %s", e.Code, e.Message)
}
// NewNuoerError 创建平台层错误
func NewNuoerError(code int, message string) *nuoerError {
if message == "" {
if desc := GetPlatformCodeDesc(code); desc != "" {
message = desc
} else {
message = "诺尔智汇返回未知错误"
}
}
return &nuoerError{Code: code, Message: message}
}
// nuoerBusiError 诺尔智汇业务层错误data.busiCode 字段)
type nuoerBusiError struct {
BusiCode int
BusiMsg string
}
func (e *nuoerBusiError) Error() string {
return fmt.Sprintf("诺尔智汇业务错误busiCode: %dbusiMsg: %s", e.BusiCode, e.BusiMsg)
}
// NewNuoerBusiError 创建业务层错误
func NewNuoerBusiError(busiCode int, busiMsg string) *nuoerBusiError {
if busiMsg == "" {
if desc := GetBusiCodeDesc(busiCode); desc != "" {
busiMsg = desc
} else {
busiMsg = "诺尔智汇业务返回未知错误"
}
}
return &nuoerBusiError{BusiCode: busiCode, BusiMsg: busiMsg}
}
// GetNotFoundErrByBusiCode 将 busiCode 映射为「查询为空」类错误(不扣费场景)
func GetNotFoundErrByBusiCode(busiCode int) error {
switch busiCode {
case BusiCodeNotFound:
return ErrNotFound
default:
return nil
}
}
// GetErrByBusiCode 将 busiCode 映射为内部哨兵错误,供处理器 errors.Is 判断
func GetErrByBusiCode(busiCode int) error {
if busiCode == BusiCodeSuccess {
return nil
}
if notFound := GetNotFoundErrByBusiCode(busiCode); notFound != nil {
return notFound
}
return ErrDatasource
}
// GetErrByPlatformCode 将平台 code 映射为内部哨兵错误
func GetErrByPlatformCode(code int) error {
if code == CodeSuccess {
return nil
}
return ErrDatasource
}

View File

@@ -0,0 +1,64 @@
package nuoer
import (
"time"
"tyapi-server/internal/config"
"tyapi-server/internal/shared/external_logger"
)
// NewNuoerServiceWithConfig 使用配置创建诺尔智汇服务
func NewNuoerServiceWithConfig(cfg *config.Config) (*NuoerService, error) {
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: cfg.Nuoer.Logging.Enabled,
LogDir: cfg.Nuoer.Logging.LogDir,
ServiceName: "nuoer",
UseDaily: cfg.Nuoer.Logging.UseDaily,
EnableLevelSeparation: cfg.Nuoer.Logging.EnableLevelSeparation,
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
}
for level, levelCfg := range cfg.Nuoer.Logging.LevelConfigs {
loggingConfig.LevelConfigs[level] = external_logger.ExternalServiceLevelFileConfig{
MaxSize: levelCfg.MaxSize,
MaxBackups: levelCfg.MaxBackups,
MaxAge: levelCfg.MaxAge,
Compress: levelCfg.Compress,
}
}
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
timeout := cfg.Nuoer.Timeout
if timeout <= 0 {
timeout = defaultRequestTimeout
}
return NewNuoerService(
cfg.Nuoer.URL,
cfg.Nuoer.AppID,
cfg.Nuoer.AppSecret,
timeout,
logger,
), nil
}
// NewNuoerServiceWithLogging 使用自定义日志配置创建诺尔智汇服务
func NewNuoerServiceWithLogging(url, appID, appSecret string, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*NuoerService, error) {
loggingConfig.ServiceName = "nuoer"
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
return NewNuoerService(url, appID, appSecret, timeout, logger), nil
}
// NewNuoerServiceSimple 创建无日志的诺尔智汇服务
func NewNuoerServiceSimple(url, appID, appSecret string, timeout time.Duration) *NuoerService {
return NewNuoerService(url, appID, appSecret, timeout, nil)
}

View File

@@ -0,0 +1,253 @@
package nuoer
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"tyapi-server/internal/shared/external_logger"
)
const defaultRequestTimeout = 4 * time.Second
// nuoerResponse 诺尔智汇通用响应
type nuoerResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
SeqNo string `json:"seqNo"`
Data interface{} `json:"data"`
}
// serviceConfig 诺尔智汇服务运行时配置
type serviceConfig struct {
URL string
AppID string
AppSecret string
Timeout time.Duration
}
// NuoerService 诺尔智汇服务
type NuoerService struct {
config serviceConfig
logger *external_logger.ExternalServiceLogger
}
// NewNuoerService 创建诺尔智汇服务实例
func NewNuoerService(url, appID, appSecret string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *NuoerService {
if timeout <= 0 {
timeout = defaultRequestTimeout
}
return &NuoerService{
config: serviceConfig{
URL: url,
AppID: appID,
AppSecret: appSecret,
Timeout: timeout,
},
logger: logger,
}
}
func (s *NuoerService) generateRequestID() string {
timestamp := time.Now().UnixNano()
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AppID)))
return fmt.Sprintf("nuoer_%x", hash[:8])
}
func (s *NuoerService) CallAPI(ctx context.Context, apiKey, apiPath string, body map[string]string) (*nuoerResponse, error) {
requestURL := strings.TrimSuffix(s.config.URL, "/")
if apiPath != "" {
if !strings.HasPrefix(apiPath, "/") {
apiPath = "/" + apiPath
}
requestURL += apiPath
}
requestID := s.generateRequestID()
startTime := time.Now()
var transactionID string
if id, ok := ctx.Value("transaction_id").(string); ok {
transactionID = id
}
// 对调用方传入的 body 全量参与加签(排除空值,按 key 升序,见 Sign
sign := Sign(body, s.config.AppSecret)
requestPayload := map[string]interface{}{
"appId": s.config.AppID,
"sign": sign,
"apiKey": apiKey,
"body": body,
}
if s.logger != nil {
s.logger.LogRequest(requestID, transactionID, apiKey, requestURL)
}
bodyBytes, err := json.Marshal(requestPayload)
if err != nil {
err = errors.Join(ErrSystem, err)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
if err != nil {
err = errors.Join(ErrSystem, err)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: s.config.Timeout}
resp, err := client.Do(req)
if err != nil {
err = wrapHTTPError(err)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
err = errors.Join(ErrSystem, err)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
if s.logger != nil {
s.logger.LogResponse(requestID, transactionID, apiKey, resp.StatusCode, time.Since(startTime))
}
if resp.StatusCode != http.StatusOK {
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode))
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
var nuoerResp nuoerResponse
if err := json.Unmarshal(respBody, &nuoerResp); err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
if nuoerResp.Code != CodeSuccess {
nuoerErr := NewNuoerError(nuoerResp.Code, nuoerResp.Msg)
err = errors.Join(GetErrByPlatformCode(nuoerResp.Code), nuoerErr)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, nuoerErr, requestPayload)
}
return nil, err
}
if nuoerResp.Data == nil {
err = errors.Join(ErrSystem, errors.New("响应 data 为空"))
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
busiCode, busiMsg, ok := parseDataBusiInfo(nuoerResp.Data)
if !ok {
err = errors.Join(ErrSystem, errors.New("响应 data 无法解析 busiCode"))
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
if busiCode != BusiCodeSuccess {
busiErr := NewNuoerBusiError(busiCode, busiMsg)
err = errors.Join(GetErrByBusiCode(busiCode), busiErr)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, busiErr, requestPayload)
}
return nil, err
}
cleanedData, err := stripBusiMetaFromData(nuoerResp.Data)
if err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应 data 清理失败: %w", err))
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
}
return nil, err
}
nuoerResp.Data = cleanedData
return &nuoerResp, nil
}
// nuoerDataBusiMeta 业务层状态字段,仅用于解析校验,不对外返回
type nuoerDataBusiMeta struct {
BusiCode int `json:"busiCode"`
BusiMsg string `json:"busiMsg"`
}
// parseDataBusiInfo 从各接口不同的 data 结构中解析 busiCode、busiMsg
func parseDataBusiInfo(data interface{}) (busiCode int, busiMsg string, ok bool) {
if data == nil {
return 0, "", false
}
raw, err := json.Marshal(data)
if err != nil {
return 0, "", false
}
var meta nuoerDataBusiMeta
if err := json.Unmarshal(raw, &meta); err != nil {
return 0, "", false
}
return meta.BusiCode, meta.BusiMsg, true
}
// stripBusiMetaFromData 去掉 data 中的 busiCode、busiMsg仅保留业务载荷
func stripBusiMetaFromData(data interface{}) (interface{}, error) {
raw, err := json.Marshal(data)
if err != nil {
return nil, err
}
var payload map[string]interface{}
if err := json.Unmarshal(raw, &payload); err != nil {
return nil, err
}
delete(payload, "busiCode")
delete(payload, "busiMsg")
return payload, nil
}
func wrapHTTPError(err error) error {
if err == context.DeadlineExceeded {
return errors.Join(ErrDatasource, err)
}
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
return errors.Join(ErrDatasource, err)
}
switch err.Error() {
case "context deadline exceeded", "timeout", "Client.Timeout exceeded", "net/http: request canceled":
return errors.Join(ErrDatasource, err)
default:
return errors.Join(ErrSystem, err)
}
}

View File

@@ -426,6 +426,16 @@ func (h *ApiHandler) GetAdminApiCalls(c *gin.Context) {
filters["user_id"] = userId
}
// 用户ID列表筛选
if userIds := c.Query("user_ids"); userIds != "" {
filters["user_ids"] = userIds
}
// 产品ID列表筛选
if productIds := c.Query("product_ids"); productIds != "" {
filters["product_ids"] = productIds
}
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {

View File

@@ -1659,3 +1659,54 @@ func (h *ProductAdminHandler) ExportAdminApiCalls(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Data(200, contentType, fileData)
}
// ExportProductDictionary 导出产品字典
// @Summary 导出产品字典
// @Description 导出所有启用且可见的产品字典,按分类分组,左侧分类列合并单元格
// @Tags 产品管理
// @Accept json
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
// @Security Bearer
// @Param format query string false "导出格式" Enums(excel, csv) default(excel)
// @Success 200 {file} file "导出文件"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/products/export-dictionary [get]
func (h *ProductAdminHandler) ExportProductDictionary(c *gin.Context) {
// 获取导出格式默认为excel
format := c.DefaultQuery("format", "excel")
if format != "excel" && format != "csv" {
h.responseBuilder.BadRequest(c, "不支持的导出格式")
return
}
// 调用应用服务导出数据
fileData, err := h.productAppService.ExportProductDictionary(c.Request.Context(), format)
if err != nil {
h.logger.Error("导出产品字典失败", zap.Error(err))
// 根据错误信息返回具体的提示
errMsg := err.Error()
if strings.Contains(errMsg, "没有找到符合条件的产品数据") || strings.Contains(errMsg, "没有数据") {
h.responseBuilder.NotFound(c, "没有找到符合条件的产品数据,请确保有启用且可见的产品")
} else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") {
h.responseBuilder.BadRequest(c, errMsg)
} else {
h.responseBuilder.BadRequest(c, "导出产品字典失败:"+errMsg)
}
return
}
// 设置响应头
contentType := "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
filename := "产品字典.xlsx"
if format == "csv" {
contentType = "text/csv;charset=utf-8"
filename = "产品字典.csv"
}
c.Header("Content-Type", contentType)
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Data(200, contentType, fileData)
}

View File

@@ -60,6 +60,9 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
products.GET("/:id/documentation", r.handler.GetProductDocumentation)
products.POST("/:id/documentation", r.handler.CreateOrUpdateProductDocumentation)
products.DELETE("/:id/documentation", r.handler.DeleteProductDocumentation)
// 产品字典导出
products.GET("/export-dictionary", r.handler.ExportProductDictionary)
}
// 分类管理

View File

@@ -28,13 +28,13 @@ func (s *SignFlowService) UpdateConfig(config *Config) {
// 创建包含多个签署人的签署流程,支持自动盖章和手动签署
func (s *SignFlowService) Create(req *CreateSignFlowRequest) (string, error) {
fmt.Println("开始创建签署流程...")
fmt.Println("(将创建包含甲方自动盖章和乙方手动签署的流程)")
fmt.Println("(将创建包含甲方手动签署和乙方自动盖章的流程)")
// 构建甲方签署人信息(自动盖章
partyASigner := s.buildPartyASigner(req.FileID)
// 构建甲方签署人信息(手动签署
partyASigner := s.buildPartyASigner(req.FileID, req.SignerAccount, req.SignerName, req.TransactorPhone, req.TransactorName, req.TransactorIDCardNum)
// 构建乙方签署人信息(手动签署
partyBSigner := s.buildPartyBSigner(req.FileID, req.SignerAccount, req.SignerName, req.TransactorPhone, req.TransactorName, req.TransactorIDCardNum)
// 构建乙方签署人信息(自动盖章
partyBSigner := s.buildPartyBSigner(req.FileID)
signers := []SignerInfo{partyASigner, partyBSigner}
@@ -128,34 +128,11 @@ func (s *SignFlowService) GetSignURL(signFlowID, psnAccount, orgName string) (st
return response.Data.Url, response.Data.ShortUrl, nil
}
// buildPartyASigner 构建甲方签署人信息(自动盖章
func (s *SignFlowService) buildPartyASigner(fileID string) SignerInfo {
return SignerInfo{
SignConfig: &SignConfig{SignOrder: 1},
SignerType: SignerTypeOrg,
SignFields: []SignField{
{
CustomBizNum: "甲方签章",
FileId: fileID,
NormalSignFieldConfig: &NormalSignFieldConfig{
AutoSign: true,
SignFieldStyle: SignFieldStyleNormal,
SignFieldPosition: &SignFieldPosition{
PositionPage: "8",
PositionX: 200,
PositionY: 430,
},
},
},
},
}
}
// buildPartyBSigner 构建乙方签署人信息(手动签署)
func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, transactorPhone, transactorName, transactorIDCardNum string) SignerInfo {
// buildPartyASigner 构建甲方签署人信息(手动签署
func (s *SignFlowService) buildPartyASigner(fileID, signerAccount, signerName, transactorPhone, transactorName, transactorIDCardNum string) SignerInfo {
return SignerInfo{
SignConfig: &SignConfig{
SignOrder: 2,
SignOrder: 1,
},
AuthConfig: &AuthConfig{
PsnAvailableAuthModes: []string{AuthModeMobile3},
@@ -182,19 +159,66 @@ func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, t
},
SignFields: []SignField{
{
CustomBizNum: "方签章",
CustomBizNum: "方签章",
FileId: fileID,
NormalSignFieldConfig: &NormalSignFieldConfig{
AutoSign: false,
SignFieldStyle: SignFieldStyleNormal,
SignFieldPosition: &SignFieldPosition{
PositionPage: "8",
PositionX: 450,
PositionY: 430,
PositionPage: "10",
PositionX: 165,
PositionY: 197,
},
OrgSealBizTypes: "PUBLIC",
},
},
{
CustomBizNum: "甲方骑缝章", // 建议设唯一标识,便于调试
FileId: fileID,
NormalSignFieldConfig: &NormalSignFieldConfig{
AutoSign: false,
SignFieldStyle: SignFieldStyleSeam, // 必须为 2Edges
SignFieldPosition: &SignFieldPosition{
AcrossPageMode: "ALL", // 覆盖全部页面(推荐)
PositionY: 694.0, // 您指定的 Y 坐标float64
},
},
},
},
}
}
// buildPartyBSigner 构建乙方签署人信息(自动盖章)
func (s *SignFlowService) buildPartyBSigner(fileID string) SignerInfo {
return SignerInfo{
SignConfig: &SignConfig{SignOrder: 2},
SignerType: SignerTypeOrg,
SignFields: []SignField{
{
CustomBizNum: "乙方签章",
FileId: fileID,
NormalSignFieldConfig: &NormalSignFieldConfig{
AutoSign: true,
SignFieldStyle: SignFieldStyleNormal,
SignFieldPosition: &SignFieldPosition{
PositionPage: "10",
PositionX: 403,
PositionY: 197,
},
},
},
{
CustomBizNum: "乙方骑缝章", // 建议设唯一标识,便于调试
FileId: fileID,
NormalSignFieldConfig: &NormalSignFieldConfig{
AutoSign: true, // 骑缝章也支持自动签署
SignFieldStyle: SignFieldStyleSeam, // 必须为 2Edges
SignFieldPosition: &SignFieldPosition{
AcrossPageMode: "ALL", // 覆盖全部页面(推荐)
PositionY: 554.0, // 您指定的 Y 坐标float64
},
},
},
},
}
}
@@ -216,4 +240,4 @@ func (s *SignFlowService) buildSignFlowConfig() SignFlowConfig {
RedirectUrl: s.config.Sign.RedirectUrl,
},
}
}
}

View File

@@ -161,7 +161,7 @@ func CreateDefaultComponents() []Component {
},
{
ComponentKey: "QDRQ",
ComponentValue: time.Now().Format("20060102"),
ComponentValue: time.Now().Format("2006-01-02"),
},
}
}

View File

@@ -75,8 +75,8 @@ type SignFlowConfig struct {
// RedirectConfig 重定向配置
type RedirectConfig struct {
RedirectUrl string `json:"redirectUrl"` // 重定向URL
RedirectDelayTime int64 `json:"redirectDelayTime"` //重定向时间
RedirectUrl string `json:"redirectUrl"` // 重定向URL
RedirectDelayTime int64 `json:"redirectDelayTime"` //重定向时间
}
// AuthConfig 认证配置
@@ -170,9 +170,10 @@ type NormalSignFieldConfig struct {
// SignFieldPosition 签署区位置
type SignFieldPosition struct {
PositionPage string `json:"positionPage"` // 页码
PositionX float64 `json:"positionX"` // X坐标
PositionY float64 `json:"positionY"` // Y坐标
PositionPage string `json:"positionPage,omitempty"` // 页码(骑缝章可与 acrossPageMode 组合)
PositionX float64 `json:"positionX,omitempty"` // X坐标
PositionY float64 `json:"positionY,omitempty"` // Y坐标
AcrossPageMode string `json:"acrossPageMode,omitempty"` // 骑缝章跨页:如 ALL 表示全部页面
}
// ==================== 签署页面链接相关结构体 ====================

View File

@@ -104,11 +104,11 @@ func getCurrentDate() string {
}
// formatDateForTemplate 格式化日期用于模板填写
// 格式: "2006年01月02日"
// e签宝日期控件通常预设为 yyyy-MM-dd与中文年月日格式不兼容时需用本格式。
//
// 返回: 中文格式的日期字符串
// 返回: yyyy-MM-dd
func formatDateForTemplate() string {
return time.Now().Format("20060102")
return time.Now().Format("2006-01-02")
}
// generateFileName 生成带时间戳的文件名

View File

@@ -11,10 +11,11 @@ import (
// ExportConfig 定义了导出所需的配置
type ExportConfig struct {
SheetName string // 工作表名称
Headers []string // 表头
Data [][]interface{} // 导出数据
ColumnWidths []float64 // 列宽
SheetName string // 工作表名称
Headers []string // 表头
Data [][]interface{} // 导出数据
ColumnWidths []float64 // 列宽
MergedRegions [][]int // 合并单元格配置 [[startRow, startCol, endRow, endCol], ...]
}
// ExportManager 负责管理不同格式的导出
@@ -95,6 +96,30 @@ func (m *ExportManager) generateExcel(ctx context.Context, config *ExportConfig)
}
}
// 合并单元格
if len(config.MergedRegions) > 0 {
for _, region := range config.MergedRegions {
startRow := region[0]
startCol := region[1]
endRow := region[2]
endCol := region[3]
startCell, err := excelize.CoordinatesToCellName(startCol+1, startRow+1)
if err != nil {
return nil, fmt.Errorf("生成合并区域起始单元格坐标失败: %v", err)
}
endCell, err := excelize.CoordinatesToCellName(endCol+1, endRow+1)
if err != nil {
return nil, fmt.Errorf("生成合并区域结束单元格坐标失败: %v", err)
}
err = f.MergeCell(sheetName, startCell, endCell)
if err != nil {
return nil, fmt.Errorf("合并单元格失败: %v", err)
}
}
}
// 设置列宽
for i, width := range config.ColumnWidths {
col, err := excelize.ColumnNumberToName(i + 1)

View File

@@ -11,10 +11,11 @@ import (
// ExportConfig 定义了导出所需的配置
type ExportConfig struct {
SheetName string // 工作表名称
Headers []string // 表头
Data [][]interface{} // 导出数据
ColumnWidths []float64 // 列宽
SheetName string // 工作表名称
Headers []string // 表头
Data [][]interface{} // 导出数据
ColumnWidths []float64 // 列宽
MergedRegions [][]int // 合并单元格配置 [[startRow, startCol, endRow, endCol], ...]
}
// ExportManager 负责管理不同格式的导出