f
This commit is contained in:
39
config.yaml
39
config.yaml
@@ -686,3 +686,42 @@ nuoer:
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ✨ 汇博(BHSC)配置
|
||||
# ===========================================
|
||||
huibo:
|
||||
url: "http://47.111.187.101:12654/api/v1/project-api/bg_check_ssw"
|
||||
app_id: "db0029527bb4558c"
|
||||
app_key: "a6c9935e967894e731c62ecfcd9b7c95"
|
||||
x_order_code: "cpdd219219725093"
|
||||
secret_id: "cf581fe84aaf46ca"
|
||||
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
|
||||
log_dir: "logs/external_services"
|
||||
service_name: "huibo"
|
||||
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
|
||||
@@ -42,6 +42,7 @@ type Config struct {
|
||||
Shumai ShumaiConfig `mapstructure:"shumai"`
|
||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||
Nuoer NuoerConfig `mapstructure:"nuoer"`
|
||||
Huibo HuiboConfig `mapstructure:"huibo"`
|
||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||
}
|
||||
|
||||
@@ -709,6 +710,40 @@ type NuoerLevelFileConfig struct {
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// HuiboConfig 汇博配置
|
||||
type HuiboConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
AppID string `mapstructure:"app_id"`
|
||||
AppKey string `mapstructure:"app_key"`
|
||||
XOrderCode string `mapstructure:"x_order_code"`
|
||||
SecretID string `mapstructure:"secret_id"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// HuiboLoggingConfig 汇博日志配置
|
||||
type HuiboLoggingConfig 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]HuiboLevelFileConfig `mapstructure:"level_configs"`
|
||||
}
|
||||
|
||||
// HuiboLevelFileConfig 汇博日志级别配置
|
||||
type HuiboLevelFileConfig struct {
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// PDFGenConfig PDF生成服务配置
|
||||
type PDFGenConfig struct {
|
||||
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
||||
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"hyapi-server/internal/infrastructure/external/alicloud"
|
||||
"hyapi-server/internal/infrastructure/external/captcha"
|
||||
"hyapi-server/internal/infrastructure/external/email"
|
||||
"hyapi-server/internal/infrastructure/external/huibo"
|
||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||
"hyapi-server/internal/infrastructure/external/muzi"
|
||||
"hyapi-server/internal/infrastructure/external/nuoer"
|
||||
@@ -408,6 +409,10 @@ func NewContainer() *Container {
|
||||
func(cfg *config.Config) (*nuoer.NuoerService, error) {
|
||||
return nuoer.NewNuoerServiceWithConfig(cfg)
|
||||
},
|
||||
// HuiboService - 汇博服务
|
||||
func(cfg *config.Config) (*huibo.HuiboService, error) {
|
||||
return huibo.NewHuiboServiceWithConfig(cfg)
|
||||
},
|
||||
func(cfg *config.Config) *yushan.YushanService {
|
||||
return yushan.NewYushanService(
|
||||
cfg.Yushan.URL,
|
||||
|
||||
@@ -1231,3 +1231,12 @@ type JRZQVKK6Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type FLXGHB4FReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type QYGLBH7YReq struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"hyapi-server/internal/domains/api/services/processors/yysy"
|
||||
"hyapi-server/internal/domains/product/services"
|
||||
"hyapi-server/internal/infrastructure/external/alicloud"
|
||||
"hyapi-server/internal/infrastructure/external/huibo"
|
||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||
"hyapi-server/internal/infrastructure/external/muzi"
|
||||
nuoerext "hyapi-server/internal/infrastructure/external/nuoer"
|
||||
@@ -68,6 +69,7 @@ func NewApiRequestService(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
shumaiService *shumai.ShumaiService,
|
||||
nuoerService *nuoerext.NuoerService,
|
||||
huiboService *huibo.HuiboService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *appconfig.Config,
|
||||
@@ -84,6 +86,7 @@ func NewApiRequestService(
|
||||
jiguangService,
|
||||
shumaiService,
|
||||
nuoerService,
|
||||
huiboService,
|
||||
validator,
|
||||
productManagementService,
|
||||
cfg,
|
||||
@@ -105,6 +108,7 @@ func NewApiRequestServiceWithRepos(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
shumaiService *shumai.ShumaiService,
|
||||
nuoerService *nuoerext.NuoerService,
|
||||
huiboService *huibo.HuiboService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *appconfig.Config,
|
||||
@@ -132,6 +136,7 @@ func NewApiRequestServiceWithRepos(
|
||||
jiguangService,
|
||||
shumaiService,
|
||||
nuoerService,
|
||||
huiboService,
|
||||
validator,
|
||||
combService,
|
||||
reportRepo,
|
||||
@@ -187,6 +192,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"FLXGK5D2": flxg.ProcessFLXGK5D2Request,
|
||||
"FLXGDJG3": flxg.ProcessFLXGDJG3Request, //董监高司法综合信息核验
|
||||
"FLXGG0S4": flxg.ProcessFLXGG0S4Request, //个人诉讼定制版
|
||||
"FLXGHB4F": flxg.ProcessFLXGHB4FRequest, //个人涉诉案件查询汇博
|
||||
// JRZQ系列处理器
|
||||
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
|
||||
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
|
||||
@@ -274,6 +280,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查
|
||||
"QYGLVR76": qygl.ProcessQYGLVR76Request, //人企关联
|
||||
"QYGLV4S6": qygl.ProcessQYGLV4S6Request, //企业诉讼定制版
|
||||
"QYGLBH7Y": qygl.ProcessQYGLBH7YRequest, //企业案件查询汇博
|
||||
|
||||
// YYSY系列处理器
|
||||
"YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖
|
||||
|
||||
@@ -301,6 +301,9 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"JRZQVZTF": &dto.JRZQVZTFReq{}, //智瞳-通用版
|
||||
"JRZQV4TF": &dto.JRZQV4TFReq{}, //智瞳分尊享版
|
||||
"JRZQVKK6": &dto.JRZQVKK6Req{}, //坤羽模型V3-标签版
|
||||
"FLXGHB4F": &dto.FLXGHB4FReq{}, //汇博-个人涉诉
|
||||
"QYGLBH7Y": &dto.QYGLBH7YReq{}, //汇博-企业涉诉
|
||||
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"hyapi-server/internal/application/api/commands"
|
||||
"hyapi-server/internal/domains/api/repositories"
|
||||
"hyapi-server/internal/infrastructure/external/alicloud"
|
||||
"hyapi-server/internal/infrastructure/external/huibo"
|
||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||
"hyapi-server/internal/infrastructure/external/muzi"
|
||||
"hyapi-server/internal/infrastructure/external/nuoer"
|
||||
@@ -42,6 +43,7 @@ type ProcessorDependencies struct {
|
||||
JiguangService *jiguang.JiguangService
|
||||
ShumaiService *shumai.ShumaiService
|
||||
NuoerService *nuoer.NuoerService
|
||||
HuiboService *huibo.HuiboService
|
||||
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,
|
||||
nuoerService *nuoer.NuoerService,
|
||||
huiboService *huibo.HuiboService,
|
||||
validator interfaces.RequestValidator,
|
||||
combService CombServiceInterface, // Changed to interface
|
||||
reportRepo repositories.ReportRepository,
|
||||
@@ -88,6 +91,7 @@ func NewProcessorDependencies(
|
||||
JiguangService: jiguangService,
|
||||
ShumaiService: shumaiService,
|
||||
NuoerService: nuoerService,
|
||||
HuiboService: huiboService,
|
||||
Validator: validator,
|
||||
CombService: combService,
|
||||
Options: nil, // 初始化为nil,在调用时设置
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package flxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-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("汇博服务未初始化"))
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 使用 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-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("汇博服务未初始化"))
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
47
internal/infrastructure/external/huibo/huibo_factory.go
vendored
Normal file
47
internal/infrastructure/external/huibo/huibo_factory.go
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package huibo
|
||||
|
||||
import (
|
||||
"hyapi-server/internal/config"
|
||||
"hyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewHuiboServiceWithConfig 使用配置创建汇博服务
|
||||
func NewHuiboServiceWithConfig(cfg *config.Config) (*HuiboService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Huibo.Logging.Enabled,
|
||||
LogDir: cfg.Huibo.Logging.LogDir,
|
||||
ServiceName: "huibo",
|
||||
UseDaily: cfg.Huibo.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Huibo.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
for key, value := range cfg.Huibo.Logging.LevelConfigs {
|
||||
loggingConfig.LevelConfigs[key] = external_logger.ExternalServiceLevelFileConfig{
|
||||
MaxSize: value.MaxSize,
|
||||
MaxBackups: value.MaxBackups,
|
||||
MaxAge: value.MaxAge,
|
||||
Compress: value.Compress,
|
||||
}
|
||||
}
|
||||
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := NewHuiboService(HuiboConfig{
|
||||
URL: cfg.Huibo.URL,
|
||||
AppID: cfg.Huibo.AppID,
|
||||
AppKey: cfg.Huibo.AppKey,
|
||||
XOrderCode: cfg.Huibo.XOrderCode,
|
||||
SecretID: cfg.Huibo.SecretID,
|
||||
AESKey: cfg.Huibo.AESKey,
|
||||
WorkOrderCode: cfg.Huibo.WorkOrderCode,
|
||||
ProductCode: cfg.Huibo.ProductCode,
|
||||
BaseURL2: cfg.Huibo.BaseURL2,
|
||||
AppCode2: cfg.Huibo.AppCode2,
|
||||
}, logger)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
612
internal/infrastructure/external/huibo/huibo_service.go
vendored
Normal file
612
internal/infrastructure/external/huibo/huibo_service.go
vendored
Normal file
@@ -0,0 +1,612 @@
|
||||
package huibo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/shared/external_logger"
|
||||
"hyapi-server/internal/shared/pdfvalidate"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
)
|
||||
|
||||
const (
|
||||
headerAuthorization = "Authorization"
|
||||
headerWorkOrderCode = "workOrderCode"
|
||||
headerOrderCode = "X-ORDER-CODE"
|
||||
headerSecretIDHdr = "secretId"
|
||||
headerAESKeyHdr = "aesKey"
|
||||
)
|
||||
|
||||
// 汇博常见状态码
|
||||
const (
|
||||
HuiboStatusSuccess = "0"
|
||||
HuiboStatusException = "1"
|
||||
HuiboStatusNoData = "2"
|
||||
HuiboStatusDataEmpty = "6010001"
|
||||
HuiboStatusSignFailed = "6010002"
|
||||
HuiboStatusDecryptFailed = "6010003"
|
||||
HuiboStatusAppIDEmpty = "6010004"
|
||||
HuiboStatusEncryptedEmpty = "6010005"
|
||||
HuiboStatusRandomKeyEmpty = "6010006"
|
||||
HuiboStatusTimestampEmpty = "6010007"
|
||||
HuiboStatusProductCodeEmpty = "6010008"
|
||||
HuiboStatusProductNotFound = "6010010"
|
||||
HuiboStatusProductNotEnabled = "6010013"
|
||||
HuiboStatusBalanceNotEnough = "6010020"
|
||||
HuiboStatusUsageLimitReached = "6010021"
|
||||
)
|
||||
|
||||
var huiboStatusMessage = map[string]string{
|
||||
HuiboStatusSuccess: "操作成功",
|
||||
HuiboStatusException: "异常",
|
||||
HuiboStatusNoData: "数据未查得",
|
||||
HuiboStatusDataEmpty: "请求体 data 为空",
|
||||
HuiboStatusSignFailed: "验证签名失败",
|
||||
HuiboStatusDecryptFailed: "使用 AES/SM4 加解密失败",
|
||||
HuiboStatusAppIDEmpty: "appId 不能为空",
|
||||
HuiboStatusEncryptedEmpty: "AES/SM4 加密后的内容不可为空",
|
||||
HuiboStatusRandomKeyEmpty: "随机 AES/SM4 加密密钥不可为空",
|
||||
HuiboStatusTimestampEmpty: "请求时间戳不可为空",
|
||||
HuiboStatusProductCodeEmpty: "产品 code 不能为空",
|
||||
HuiboStatusProductNotFound: "产品不存在",
|
||||
HuiboStatusProductNotEnabled: "企业未开通产品",
|
||||
HuiboStatusBalanceNotEnough: "企业账户余额不足",
|
||||
HuiboStatusUsageLimitReached: "产品使用次数到达限制",
|
||||
}
|
||||
|
||||
type HuiboConfig struct {
|
||||
URL string
|
||||
AppID string
|
||||
AppKey string
|
||||
XOrderCode string
|
||||
SecretID string
|
||||
AESKey string
|
||||
WorkOrderCode string
|
||||
ProductCode string
|
||||
BaseURL2 string // CallAPI2 使用的 URL
|
||||
AppCode2 string // CallAPI2 使用的 AppCode
|
||||
}
|
||||
|
||||
type HuiboService struct {
|
||||
config HuiboConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
type responseWrapper struct {
|
||||
Code json.RawMessage `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
Status json.RawMessage `json:"status"`
|
||||
Data string `json:"data"`
|
||||
} `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()
|
||||
startTime := time.Now()
|
||||
transactionID := ""
|
||||
if v, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = v
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.LogRequest(requestID, transactionID, "huibo_bg_check_ssw", s.config.URL)
|
||||
}
|
||||
|
||||
if err := s.validateConfig(); err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
|
||||
pdfBytes, err := decodeAndValidatePDF(authPDFBase64)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrDatasource, err)
|
||||
}
|
||||
|
||||
bizParam := map[string]string{
|
||||
"productCode": s.getProductCode(),
|
||||
"name": name,
|
||||
"idCard": idCard,
|
||||
}
|
||||
|
||||
rawJSON, err := json.Marshal(bizParam)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedData, err := encryptAESGCMBase64(string(rawJSON), s.config.AESKey)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrSystem, fmt.Errorf("AES-GCM加密失败: %w", err))
|
||||
}
|
||||
|
||||
sortedParam := generateSortedParam(bizParam)
|
||||
signature := hmacSHA256Base64(sortedParam, s.config.AESKey)
|
||||
|
||||
reqInner := map[string]string{
|
||||
"data": encryptedData,
|
||||
"requestId": requestID,
|
||||
"secretId": s.config.SecretID,
|
||||
"signature": signature,
|
||||
}
|
||||
reqInnerBytes, err := json.Marshal(reqInner)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
reqOuter := map[string]string{"data": string(reqInnerBytes)}
|
||||
reqOuterBytes, err := json.Marshal(reqOuter)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
|
||||
respBody, err := s.callAPI(ctx, reqOuterBytes, pdfBytes)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, "huibo_bg_check_ssw", err, map[string]interface{}{"name": name, "id_card": idCard})
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.LogResponse(requestID, transactionID, "huibo_bg_check_ssw", http.StatusOK, time.Since(startTime))
|
||||
}
|
||||
|
||||
var wrapper responseWrapper
|
||||
if err = json.Unmarshal(respBody, &wrapper); err != nil {
|
||||
return nil, errors.Join(ErrDatasource, fmt.Errorf("响应解析失败: %w", err))
|
||||
}
|
||||
|
||||
outerCode := normalizeStatus(wrapper.Code)
|
||||
outerMsg := strings.TrimSpace(wrapper.Msg)
|
||||
if outerCode != "" && outerCode != "200" {
|
||||
return nil, errors.Join(ErrDatasource, fmt.Errorf("汇博外层响应异常(code=%s,msg=%s)", outerCode, outerMsg))
|
||||
}
|
||||
|
||||
status := normalizeStatus(wrapper.Data.Status)
|
||||
// status=2「数据未查得」:产品约定按调用成功计费,对外返回 {}(与外层 code=200 成功一致,走应用层异步扣款)
|
||||
if status == HuiboStatusNoData {
|
||||
if s.logger != nil {
|
||||
s.logger.LogInfo(
|
||||
"汇博教育背景:数据未查得(status=2),返回空 JSON 并按成功计费",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("name", name),
|
||||
zap.String("id_card", idCard),
|
||||
)
|
||||
}
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
if status != HuiboStatusSuccess {
|
||||
msg := wrapper.Data.Data
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
msg = getHuiboStatusMessage(status)
|
||||
}
|
||||
if outerMsg != "" && !strings.Contains(msg, outerMsg) {
|
||||
msg = msg + " | 外层消息: " + outerMsg
|
||||
}
|
||||
return nil, errors.Join(ErrDatasource, fmt.Errorf("汇博业务状态异常(status=%s,msg=%s)", status, msg))
|
||||
}
|
||||
|
||||
if wrapper.Data.Data == "" {
|
||||
return nil, errors.Join(ErrDatasource, errors.New("响应缺少加密数据"))
|
||||
}
|
||||
|
||||
decrypted, err := decryptAESGCMBase64(wrapper.Data.Data, s.config.AESKey)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrDatasource, fmt.Errorf("响应解密失败: %w", err))
|
||||
}
|
||||
return []byte(decrypted), nil
|
||||
}
|
||||
|
||||
func (s *HuiboService) callAPI(ctx context.Context, reqOuterJSON []byte, pdfBytes []byte) ([]byte, error) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
// 与对接示例一致:先 file 再 req
|
||||
part, err := writer.CreateFormFile("file", "authorization.pdf")
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
if _, err = part.Write(pdfBytes); err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
if err := writer.WriteField("req", string(reqOuterJSON)); err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.config.URL, &body)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrSystem, 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())
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.LogErrorWithFields("汇博 HTTP 请求失败",
|
||||
zap.String("url", s.config.URL),
|
||||
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("汇博 读取响应体失败",
|
||||
zap.String("url", s.config.URL),
|
||||
zap.Int("http_status", resp.StatusCode),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
return nil, errors.Join(ErrSystem, err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
if s.logger != nil {
|
||||
bodySnippet := string(respBody)
|
||||
const maxLog = 1024
|
||||
if len(bodySnippet) > maxLog {
|
||||
bodySnippet = bodySnippet[:maxLog] + "...(truncated)"
|
||||
}
|
||||
s.logger.LogErrorWithFields("汇博 HTTP 状态异常",
|
||||
zap.String("url", s.config.URL),
|
||||
zap.Int("http_status", resp.StatusCode),
|
||||
zap.String("response_body", bodySnippet),
|
||||
)
|
||||
}
|
||||
return nil, errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码异常: %d, body: %s", resp.StatusCode, string(respBody)))
|
||||
}
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func (s *HuiboService) validateConfig() error {
|
||||
if strings.TrimSpace(s.config.URL) == "" ||
|
||||
strings.TrimSpace(s.config.AppID) == "" ||
|
||||
strings.TrimSpace(s.config.AppKey) == "" ||
|
||||
strings.TrimSpace(s.config.SecretID) == "" ||
|
||||
strings.TrimSpace(s.config.AESKey) == "" ||
|
||||
strings.TrimSpace(s.config.WorkOrderCode) == "" ||
|
||||
strings.TrimSpace(s.config.XOrderCode) == "" {
|
||||
return errors.New("汇博配置不完整")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HuiboService) getProductCode() string {
|
||||
pc := strings.TrimSpace(s.config.ProductCode)
|
||||
if pc == "" {
|
||||
return "22089"
|
||||
}
|
||||
return pc
|
||||
}
|
||||
|
||||
func (s *HuiboService) generateRequestID() string {
|
||||
return "ssw_" + time.Now().Format("060102150405000") + randomDigits(6)
|
||||
}
|
||||
|
||||
func decodeAndValidatePDF(base64PDF string) ([]byte, error) {
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(base64PDF))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("授权书文件base64格式错误: %w", err)
|
||||
}
|
||||
if err := pdfvalidate.ValidateDecodedPDFBinary(raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func generateSortedParam(m map[string]string) string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+"="+m[k])
|
||||
}
|
||||
return strings.Join(parts, "&")
|
||||
}
|
||||
|
||||
func hmacSHA256Base64(data, secret string) string {
|
||||
m := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = m.Write([]byte(data))
|
||||
return base64.StdEncoding.EncodeToString(m.Sum(nil))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func normalizeStatus(raw json.RawMessage) string {
|
||||
s := strings.TrimSpace(string(raw))
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") {
|
||||
return strings.Trim(s, "\"")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getHuiboStatusMessage(status string) string {
|
||||
if msg, ok := huiboStatusMessage[status]; ok {
|
||||
return msg
|
||||
}
|
||||
if status == "" {
|
||||
return "数据源返回失败"
|
||||
}
|
||||
return "未知状态码: " + status
|
||||
}
|
||||
|
||||
func randomDigits(n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
raw := make([]byte, n)
|
||||
if _, err := io.ReadFull(rand.Reader, raw); err != nil {
|
||||
return strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
}
|
||||
b := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
b[i] = byte('0' + int(raw[i])%10)
|
||||
}
|
||||
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
|
||||
}
|
||||
26
internal/shared/pdfvalidate/pdfvalidate.go
Normal file
26
internal/shared/pdfvalidate/pdfvalidate.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package pdfvalidate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MaxAuthorizePDFBytes 授权类 PDF 大小上限(与汇博等对接约定一致)
|
||||
const MaxAuthorizePDFBytes = 500 * 1024
|
||||
|
||||
var pdfMagic = []byte("%PDF-")
|
||||
|
||||
// ValidateDecodedPDFBinary 仅校验已通过 Base64 解码得到的原始字节:非空、长度、PDF 魔数头部。
|
||||
func ValidateDecodedPDFBinary(raw []byte) error {
|
||||
if len(raw) == 0 {
|
||||
return errors.New("授权书文件不能为空")
|
||||
}
|
||||
if len(raw) > MaxAuthorizePDFBytes {
|
||||
return fmt.Errorf("授权书文件不能超过500KB,当前大小: %d字节", len(raw))
|
||||
}
|
||||
if len(raw) < len(pdfMagic) || !bytes.Equal(raw[:len(pdfMagic)], pdfMagic) {
|
||||
return errors.New("授权书文件必须为PDF格式")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user