Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server
This commit is contained in:
38
config.yaml
38
config.yaml
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
8
go.mod
@@ -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
6
go.sum
@@ -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=
|
||||
|
||||
@@ -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,7 +1623,19 @@ func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ct
|
||||
|
||||
s.logger.Info("合同文件已上传七牛云", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL))
|
||||
|
||||
// 4. 保存到合同聚合根
|
||||
// 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,
|
||||
@@ -1581,6 +1645,7 @@ func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ct
|
||||
fileId,
|
||||
qiniuURL,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("保存合同信息到聚合根失败", zap.String("file_name", fileName), zap.Error(err))
|
||||
continue
|
||||
|
||||
1733
internal/application/certification/new.md
Normal file
1733
internal/application/certification/new.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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域名
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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, //人车关系核验(ETC)10093 月更
|
||||
|
||||
"QCXGM4CL": qcxg.ProcessQCXGM4CLRequest, //名下车辆诺尔
|
||||
// DWBG系列处理器 - 多维报告
|
||||
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
|
||||
"DWBG8B4D": dwbg.ProcessDWBG8B4DRequest,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,在调用时设置
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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("查询为空"))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ¶msDto); 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))
|
||||
}
|
||||
}
|
||||
@@ -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,32 +71,40 @@ 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 照片相似度分数字符串(0–1000)。区间说明:(0,600)不同人;(600,700)不能确定是否同人;(700,1000)同人。数值为上游 score(0~1)×1000。
|
||||
// VerificationMessage 审核校验信息,透传上游 msg
|
||||
VerificationMessage string `json:"verification_message"`
|
||||
// Similarity 照片相似度分数字符串(0–1000)。区间说明:(0,600)不同人;(600,700)不能确定是否同人;(700,1000)同人。
|
||||
Similarity string `json:"similarity"`
|
||||
}
|
||||
|
||||
// zci062UpstreamResp 智查 ZCI062 成功返回体中的分数字段(分值越大相似度越高)
|
||||
type zci062UpstreamResp struct {
|
||||
// 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{
|
||||
VerificationCode: valueZCI001ToString(r.Incorrect),
|
||||
VerificationResult: verificationResult,
|
||||
VerificationMessage: r.Msg,
|
||||
Similarity: similarity,
|
||||
},
|
||||
}
|
||||
@@ -102,11 +112,77 @@ func mapZCI062RespToIVYZZQ3B(respBytes []byte) ([]byte, error) {
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// mapVerificationResultFromSimilarity 与 similarity(0–1000)区间说明对齐:
|
||||
// (700,1000】系统判断为同一人 → 身份审核通过 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 -> 600,0.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -60,4 +60,3 @@ func ProcessJRZQ2F8ARequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ¶msDto); 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)
|
||||
}
|
||||
|
||||
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 极光服务已经返回了 data 字段的 JSON,直接返回即可
|
||||
return respBytes, nil
|
||||
return transformQCXG5F3AResponse(raw)
|
||||
}
|
||||
|
||||
@@ -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, ¶msDto); 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)
|
||||
}
|
||||
|
||||
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 极光服务已经返回了 data 字段的 JSON,直接返回即可
|
||||
return respBytes, nil
|
||||
return transformQCXG5F3AResponse(raw)
|
||||
}
|
||||
|
||||
// transformQCXG5F3AResponse 将诺尔响应转为 QCXG5F3A 对外格式:去掉 busiCode/busiMsg,展开 result,vehicleCount 为字符串
|
||||
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 类型无效")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¶msDto); 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 gjson 检查并转换 vehicleCount 字段
|
||||
vehicleCountResult := gjson.GetBytes(respBytes, "vehicleCount")
|
||||
if vehicleCountResult.Exists() && vehicleCountResult.Type == gjson.String {
|
||||
// 如果是字符串类型,转换为整数
|
||||
vehicleCountInt, err := strconv.Atoi(vehicleCountResult.String())
|
||||
m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard})
|
||||
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)
|
||||
|
||||
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respData["vehicleCount"] = vehicleCountInt
|
||||
// 重新序列化为JSON并返回
|
||||
resultBytes, err := json.Marshal(respData)
|
||||
|
||||
return transformQCXG9P1CResponse(raw)
|
||||
}
|
||||
|
||||
// transformQCXG9P1CResponse 将诺尔响应转为 QCXG9P1C 对外格式:去掉 busiCode/busiMsg,展开 result,vehicleCount 为整数
|
||||
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)
|
||||
}
|
||||
return resultBytes, nil
|
||||
|
||||
out := map[string]interface{}{
|
||||
"vehicleCount": countInt,
|
||||
"list": list,
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// 如果 vehicleCount 不存在或不是字符串,直接返回原始响应
|
||||
return respBytes, nil
|
||||
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 类型无效")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
reqFormData := map[string]interface{}{
|
||||
"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
|
||||
|
||||
}
|
||||
@@ -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, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
reqFormData := map[string]interface{}{
|
||||
"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
|
||||
|
||||
}
|
||||
@@ -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, ¶msDto); 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)
|
||||
}
|
||||
|
||||
// 检查天眼查API调用是否成功
|
||||
if !response.Success {
|
||||
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
|
||||
}
|
||||
|
||||
// 返回天眼查响应数据
|
||||
respBytes, err := json.Marshal(response.Data)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
data = map[string]interface{}{}
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
// 原始返回结构处理 - data 是 map[string]interface{} 类型
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型"))
|
||||
}
|
||||
|
||||
// 提取 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 提取 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)
|
||||
}
|
||||
|
||||
@@ -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, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
reqFormData := map[string]interface{}{
|
||||
"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
|
||||
|
||||
}
|
||||
@@ -38,9 +38,11 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 检查天眼查API调用是否成功
|
||||
if !response.Success {
|
||||
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
|
||||
}
|
||||
|
||||
// 返回天眼查响应数据
|
||||
respBytes, err := json.Marshal(response.Data)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
data = map[string]interface{}{}
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
// 原始返回结构处理 - data 是 map[string]interface{} 类型
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型"))
|
||||
}
|
||||
|
||||
// 提取 total
|
||||
totalStr := ""
|
||||
if v, ok := dataMap["total"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
totalStr = str
|
||||
}
|
||||
}
|
||||
|
||||
// 提取 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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 检查天眼查API调用是否成功
|
||||
if !response.Success {
|
||||
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
|
||||
}
|
||||
|
||||
// 返回天眼查响应数据
|
||||
respBytes, err := json.Marshal(response.Data)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
data = map[string]interface{}{}
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
// 原始返回结构处理 - data 是 map[string]interface{} 类型
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型"))
|
||||
}
|
||||
|
||||
// 提取 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提取 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)
|
||||
}
|
||||
|
||||
@@ -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, ¶msDto); 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))
|
||||
}
|
||||
}
|
||||
@@ -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, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
reqFormData := map[string]interface{}{
|
||||
"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
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 更新合同信息
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 >= ?"
|
||||
|
||||
@@ -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,6 +167,25 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries
|
||||
|
||||
// 应用排序
|
||||
if query.SortBy != "" {
|
||||
// 检查是否是关联表字段排序
|
||||
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 := query.SortBy
|
||||
if query.SortOrder == "desc" {
|
||||
order += " DESC"
|
||||
@@ -172,6 +193,7 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries
|
||||
order += " ASC"
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
90
internal/infrastructure/external/huibo/2.md
vendored
Normal file
90
internal/infrastructure/external/huibo/2.md
vendored
Normal 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、异常捕获、状态码统一处理)?
|
||||
83
internal/infrastructure/external/huibo/crypto.go
vendored
Normal file
83
internal/infrastructure/external/huibo/crypto.go
vendored
Normal 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
|
||||
}
|
||||
432
internal/infrastructure/external/huibo/crypto_test.go
vendored
Normal file
432
internal/infrastructure/external/huibo/crypto_test.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
59
internal/infrastructure/external/huibo/curl_helper.go
vendored
Normal file
59
internal/infrastructure/external/huibo/curl_helper.go
vendored
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
52
internal/infrastructure/external/huibo/status_codes.go
vendored
Normal file
52
internal/infrastructure/external/huibo/status_codes.go
vendored
Normal 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
|
||||
}
|
||||
38
internal/infrastructure/external/nuoer/crypto.go
vendored
Normal file
38
internal/infrastructure/external/nuoer/crypto.go
vendored
Normal 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[:])
|
||||
}
|
||||
21
internal/infrastructure/external/nuoer/crypto_test.go
vendored
Normal file
21
internal/infrastructure/external/nuoer/crypto_test.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
141
internal/infrastructure/external/nuoer/nuoer_errors.go
vendored
Normal file
141
internal/infrastructure/external/nuoer/nuoer_errors.go
vendored
Normal 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: %d,msg: %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: %d,busiMsg: %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
|
||||
}
|
||||
64
internal/infrastructure/external/nuoer/nuoer_factory.go
vendored
Normal file
64
internal/infrastructure/external/nuoer/nuoer_factory.go
vendored
Normal 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)
|
||||
}
|
||||
253
internal/infrastructure/external/nuoer/nuoer_service.go
vendored
Normal file
253
internal/infrastructure/external/nuoer/nuoer_service.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 分类管理
|
||||
|
||||
@@ -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, // 必须为 2(Edges)
|
||||
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, // 必须为 2(Edges)
|
||||
SignFieldPosition: &SignFieldPosition{
|
||||
AcrossPageMode: "ALL", // 覆盖全部页面(推荐)
|
||||
PositionY: 554.0, // 您指定的 Y 坐标(float64)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ func CreateDefaultComponents() []Component {
|
||||
},
|
||||
{
|
||||
ComponentKey: "QDRQ",
|
||||
ComponentValue: time.Now().Format("2006年01月02日"),
|
||||
ComponentValue: time.Now().Format("2006-01-02"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 表示全部页面
|
||||
}
|
||||
|
||||
// ==================== 签署页面链接相关结构体 ====================
|
||||
|
||||
@@ -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("2006年01月02日")
|
||||
return time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
// generateFileName 生成带时间戳的文件名
|
||||
|
||||
@@ -15,6 +15,7 @@ type ExportConfig struct {
|
||||
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)
|
||||
|
||||
@@ -15,6 +15,7 @@ type ExportConfig struct {
|
||||
Headers []string // 表头
|
||||
Data [][]interface{} // 导出数据
|
||||
ColumnWidths []float64 // 列宽
|
||||
MergedRegions [][]int // 合并单元格配置 [[startRow, startCol, endRow, endCol], ...]
|
||||
}
|
||||
|
||||
// ExportManager 负责管理不同格式的导出
|
||||
|
||||
Reference in New Issue
Block a user