f
This commit is contained in:
20
config.yaml
20
config.yaml
@@ -531,6 +531,26 @@ jiguang:
|
|||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 📄 PDF生成服务配置
|
||||||
|
# ===========================================
|
||||||
|
pdfgen:
|
||||||
|
# 服务地址配置
|
||||||
|
development_url: "http://pdfg.tianyuanapi.com" # 开发环境服务地址
|
||||||
|
production_url: "http://localhost:15990" # 生产环境服务地址
|
||||||
|
|
||||||
|
# API路径配置
|
||||||
|
api_path: "/api/v1/generate/guangzhou" # PDF生成API路径
|
||||||
|
|
||||||
|
# 超时配置
|
||||||
|
timeout: 120s # 请求超时时间(120秒)
|
||||||
|
|
||||||
|
# 缓存配置
|
||||||
|
cache:
|
||||||
|
ttl: 24h # 缓存过期时间(24小时)
|
||||||
|
cache_dir: "" # 缓存目录(空则使用默认目录)
|
||||||
|
max_size: 0 # 最大缓存大小(0表示不限制,单位:字节)
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# ✨ 数脉配置走实时接口
|
# ✨ 数脉配置走实时接口
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ type CertificationApplicationService interface {
|
|||||||
// 获取认证列表(管理员)
|
// 获取认证列表(管理员)
|
||||||
ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error)
|
ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error)
|
||||||
|
|
||||||
|
// ================ 管理员后台操作用例 ================
|
||||||
|
|
||||||
|
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
|
||||||
|
AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error)
|
||||||
|
|
||||||
// ================ e签宝回调处理 ================
|
// ================ e签宝回调处理 ================
|
||||||
|
|
||||||
// 处理e签宝回调
|
// 处理e签宝回调
|
||||||
|
|||||||
@@ -581,6 +581,110 @@ func (s *CertificationApplicationServiceImpl) HandleEsignCallback(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ 管理员后台操作用例 ================
|
||||||
|
|
||||||
|
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
|
||||||
|
func (s *CertificationApplicationServiceImpl) AdminCompleteCertificationWithoutContract(
|
||||||
|
ctx context.Context,
|
||||||
|
cmd *commands.AdminCompleteCertificationCommand,
|
||||||
|
) (*responses.CertificationResponse, error) {
|
||||||
|
s.logger.Info("管理员代用户完成认证(不关联合同)",
|
||||||
|
zap.String("admin_id", cmd.AdminID),
|
||||||
|
zap.String("user_id", cmd.UserID),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1. 基础参数及企业信息校验
|
||||||
|
enterpriseInfo := &certification_value_objects.EnterpriseInfo{
|
||||||
|
CompanyName: cmd.CompanyName,
|
||||||
|
UnifiedSocialCode: cmd.UnifiedSocialCode,
|
||||||
|
LegalPersonName: cmd.LegalPersonName,
|
||||||
|
LegalPersonID: cmd.LegalPersonID,
|
||||||
|
LegalPersonPhone: cmd.LegalPersonPhone,
|
||||||
|
EnterpriseAddress: cmd.EnterpriseAddress,
|
||||||
|
}
|
||||||
|
if err := enterpriseInfo.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("企业信息验证失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查统一社会信用代码唯一性(排除当前用户)
|
||||||
|
exists, err := s.userAggregateService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, cmd.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("检查企业信息失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("统一社会信用代码已被其他用户使用")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cert *entities.Certification
|
||||||
|
|
||||||
|
// 2. 事务内:创建/加载认证、写入企业信息、直接完成认证、创建钱包和API用户
|
||||||
|
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||||
|
// 2.1 检查并创建认证记录
|
||||||
|
existsCert, err := s.aggregateService.ExistsByUserID(txCtx, cmd.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("检查用户认证是否存在失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
if !existsCert {
|
||||||
|
cert, err = s.aggregateService.CreateCertification(txCtx, cmd.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建认证信息失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cert, err = s.aggregateService.LoadCertificationByUserID(txCtx, cmd.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加载认证信息失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.2 写入/覆盖用户域企业信息
|
||||||
|
if err := s.userAggregateService.CreateOrUpdateEnterpriseInfo(
|
||||||
|
txCtx,
|
||||||
|
cmd.UserID,
|
||||||
|
cmd.CompanyName,
|
||||||
|
cmd.UnifiedSocialCode,
|
||||||
|
cmd.LegalPersonName,
|
||||||
|
cmd.LegalPersonID,
|
||||||
|
cmd.LegalPersonPhone,
|
||||||
|
cmd.EnterpriseAddress,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("保存企业信息失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.3 直接将认证状态设置为完成(管理员操作,暂不校验合同信息)
|
||||||
|
if err := cert.TransitionTo(
|
||||||
|
enums.StatusCompleted,
|
||||||
|
enums.ActorTypeAdmin,
|
||||||
|
cmd.AdminID,
|
||||||
|
fmt.Sprintf("管理员代用户完成认证:%s", cmd.Reason),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("更新认证状态失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.4 基础激活:创建钱包、API用户并在用户域标记完成认证
|
||||||
|
if err := s.completeUserActivationWithoutContract(txCtx, cert); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.5 保存认证信息
|
||||||
|
if err := s.aggregateService.SaveCertification(txCtx, cert); err != nil {
|
||||||
|
return fmt.Errorf("保存认证信息失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := s.convertToResponse(cert)
|
||||||
|
s.logger.Info("管理员代用户完成认证成功(不关联合同)",
|
||||||
|
zap.String("admin_id", cmd.AdminID),
|
||||||
|
zap.String("user_id", cmd.UserID),
|
||||||
|
zap.String("certification_id", cert.ID),
|
||||||
|
)
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ================ 辅助方法 ================
|
// ================ 辅助方法 ================
|
||||||
|
|
||||||
// convertToResponse 转换实体为响应DTO
|
// convertToResponse 转换实体为响应DTO
|
||||||
@@ -932,21 +1036,8 @@ func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ct
|
|||||||
s.logger.Info("合同信息已保存到聚合根", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL))
|
s.logger.Info("合同信息已保存到聚合根", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.walletAggregateService.CreateWallet(ctx, cert.UserID)
|
// 合同签署完成后的基础激活流程
|
||||||
if err != nil {
|
return s.completeUserActivationWithoutContract(ctx, cert)
|
||||||
s.logger.Error("创建钱包失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 创建API用户
|
|
||||||
err = s.apiUserAggregateService.CreateApiUser(ctx, cert.UserID)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("创建API用户失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
|
||||||
}
|
|
||||||
err = s.userAggregateService.CompleteCertification(ctx, cert.UserID)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("用户域完成认证失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadFileContent 通过URL下载文件内容
|
// downloadFileContent 通过URL下载文件内容
|
||||||
@@ -995,6 +1086,27 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
|
|||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// completeUserActivationWithoutContract 创建钱包、API用户并在用户域标记完成认证(不依赖合同信息)
|
||||||
|
func (s *CertificationApplicationServiceImpl) completeUserActivationWithoutContract(ctx context.Context, cert *entities.Certification) error {
|
||||||
|
// 创建钱包
|
||||||
|
if _, err := s.walletAggregateService.CreateWallet(ctx, cert.UserID); err != nil {
|
||||||
|
s.logger.Error("创建钱包失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建API用户
|
||||||
|
if err := s.apiUserAggregateService.CreateApiUser(ctx, cert.UserID); err != nil {
|
||||||
|
s.logger.Error("创建API用户失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记用户域完成认证
|
||||||
|
if err := s.userAggregateService.CompleteCertification(ctx, cert.UserID); err != nil {
|
||||||
|
s.logger.Error("用户域完成认证失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RecognizeBusinessLicense OCR识别营业执照
|
// RecognizeBusinessLicense OCR识别营业执照
|
||||||
func (s *CertificationApplicationServiceImpl) RecognizeBusinessLicense(
|
func (s *CertificationApplicationServiceImpl) RecognizeBusinessLicense(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@@ -71,7 +71,19 @@ type EsignOrganization struct {
|
|||||||
// 可以根据需要添加更多企业信息字段
|
// 可以根据需要添加更多企业信息字段
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminCompleteCertificationCommand 管理员代用户完成认证命令(可不关联合同)
|
||||||
|
type AdminCompleteCertificationCommand struct {
|
||||||
|
AdminID string `json:"-" validate:"required"`
|
||||||
|
UserID string `json:"user_id" validate:"required"`
|
||||||
|
CompanyName string `json:"company_name" validate:"required,min=2,max=100"`
|
||||||
|
UnifiedSocialCode string `json:"unified_social_code" validate:"required"`
|
||||||
|
LegalPersonName string `json:"legal_person_name" validate:"required,min=2,max=20"`
|
||||||
|
LegalPersonID string `json:"legal_person_id" validate:"required"`
|
||||||
|
LegalPersonPhone string `json:"legal_person_phone" validate:"required"`
|
||||||
|
EnterpriseAddress string `json:"enterprise_address" validate:"required"`
|
||||||
|
// 备注信息,用于记录后台操作原因
|
||||||
|
Reason string `json:"reason" validate:"required"`
|
||||||
|
}
|
||||||
// ForceTransitionStatusCommand 强制状态转换命令(管理员)
|
// ForceTransitionStatusCommand 强制状态转换命令(管理员)
|
||||||
type ForceTransitionStatusCommand struct {
|
type ForceTransitionStatusCommand struct {
|
||||||
CertificationID string `json:"certification_id" validate:"required"`
|
CertificationID string `json:"certification_id" validate:"required"`
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type Config struct {
|
|||||||
Xingwei XingweiConfig `mapstructure:"xingwei"`
|
Xingwei XingweiConfig `mapstructure:"xingwei"`
|
||||||
Jiguang JiguangConfig `mapstructure:"jiguang"`
|
Jiguang JiguangConfig `mapstructure:"jiguang"`
|
||||||
Shumai ShumaiConfig `mapstructure:"shumai"`
|
Shumai ShumaiConfig `mapstructure:"shumai"`
|
||||||
|
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig HTTP服务器配置
|
// ServerConfig HTTP服务器配置
|
||||||
@@ -581,6 +582,22 @@ type ShumaiLevelFileConfig struct {
|
|||||||
Compress bool `mapstructure:"compress"`
|
Compress bool `mapstructure:"compress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PDFGenConfig PDF生成服务配置
|
||||||
|
type PDFGenConfig struct {
|
||||||
|
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
||||||
|
ProductionURL string `mapstructure:"production_url"` // 生产环境服务地址
|
||||||
|
APIPath string `mapstructure:"api_path"` // API路径
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"` // 请求超时时间
|
||||||
|
Cache PDFGenCacheConfig `mapstructure:"cache"` // 缓存配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDFGenCacheConfig PDF生成缓存配置
|
||||||
|
type PDFGenCacheConfig struct {
|
||||||
|
TTL time.Duration `mapstructure:"ttl"` // 缓存过期时间
|
||||||
|
CacheDir string `mapstructure:"cache_dir"` // 缓存目录(空则使用默认目录)
|
||||||
|
MaxSize int64 `mapstructure:"max_size"` // 最大缓存大小(0表示不限制,单位:字节)
|
||||||
|
}
|
||||||
|
|
||||||
// DomainConfig 域名配置
|
// DomainConfig 域名配置
|
||||||
type DomainConfig struct {
|
type DomainConfig struct {
|
||||||
API string `mapstructure:"api"` // API域名
|
API string `mapstructure:"api"` // API域名
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
@@ -1145,36 +1144,33 @@ func NewContainer() *Container {
|
|||||||
return pdf.NewPDFGenerator(logger)
|
return pdf.NewPDFGenerator(logger)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// PDF缓存管理器
|
// PDF缓存管理器(用于PDFG)
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
func(logger *zap.Logger) (*pdf.PDFCacheManager, error) {
|
func(cfg *config.Config, logger *zap.Logger) (*pdf.PDFCacheManager, error) {
|
||||||
// 使用默认配置:缓存目录在临时目录,TTL为24小时,最大缓存大小为500MB
|
cacheDir := cfg.PDFGen.Cache.CacheDir
|
||||||
cacheDir := "" // 使用默认目录(临时目录下的tyapi_pdf_cache)
|
ttl := cfg.PDFGen.Cache.TTL
|
||||||
ttl := 24 * time.Hour
|
if ttl == 0 {
|
||||||
maxSize := int64(500 * 1024 * 1024) // 500MB
|
ttl = 24 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
// 可以通过环境变量覆盖
|
// 环境变量可以覆盖配置
|
||||||
if envCacheDir := os.Getenv("PDF_CACHE_DIR"); envCacheDir != "" {
|
if envCacheDir := os.Getenv("PDFG_CACHE_DIR"); envCacheDir != "" {
|
||||||
cacheDir = envCacheDir
|
cacheDir = envCacheDir
|
||||||
}
|
}
|
||||||
if envTTL := os.Getenv("PDF_CACHE_TTL"); envTTL != "" {
|
if envTTL := os.Getenv("PDFG_CACHE_TTL"); envTTL != "" {
|
||||||
if parsedTTL, err := time.ParseDuration(envTTL); err == nil {
|
if parsedTTL, err := time.ParseDuration(envTTL); err == nil {
|
||||||
ttl = parsedTTL
|
ttl = parsedTTL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if envMaxSize := os.Getenv("PDF_CACHE_MAX_SIZE"); envMaxSize != "" {
|
|
||||||
if parsedMaxSize, err := strconv.ParseInt(envMaxSize, 10, 64); err == nil {
|
|
||||||
maxSize = parsedMaxSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
maxSize := cfg.PDFGen.Cache.MaxSize
|
||||||
cacheManager, err := pdf.NewPDFCacheManager(logger, cacheDir, ttl, maxSize)
|
cacheManager, err := pdf.NewPDFCacheManager(logger, cacheDir, ttl, maxSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("PDF缓存管理器初始化失败,将禁用缓存功能", zap.Error(err))
|
logger.Warn("PDFG缓存管理器初始化失败", zap.Error(err))
|
||||||
return nil, nil // 返回nil,handler中会检查
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("PDF缓存管理器已初始化",
|
logger.Info("PDFG缓存管理器已初始化",
|
||||||
zap.String("cache_dir", cacheDir),
|
zap.String("cache_dir", cacheDir),
|
||||||
zap.Duration("ttl", ttl),
|
zap.Duration("ttl", ttl),
|
||||||
zap.Int64("max_size", maxSize),
|
zap.Int64("max_size", maxSize),
|
||||||
@@ -1237,6 +1233,8 @@ func NewContainer() *Container {
|
|||||||
) *handlers.AnnouncementHandler {
|
) *handlers.AnnouncementHandler {
|
||||||
return handlers.NewAnnouncementHandler(appService, responseBuilder, validator, logger)
|
return handlers.NewAnnouncementHandler(appService, responseBuilder, validator, logger)
|
||||||
},
|
},
|
||||||
|
// PDFG HTTP处理器
|
||||||
|
handlers.NewPDFGHandler,
|
||||||
// 组件报告处理器
|
// 组件报告处理器
|
||||||
func(
|
func(
|
||||||
productRepo domain_product_repo.ProductRepository,
|
productRepo domain_product_repo.ProductRepository,
|
||||||
@@ -1299,6 +1297,8 @@ func NewContainer() *Container {
|
|||||||
routes.NewApiRoutes,
|
routes.NewApiRoutes,
|
||||||
// 统计路由
|
// 统计路由
|
||||||
routes.NewStatisticsRoutes,
|
routes.NewStatisticsRoutes,
|
||||||
|
// PDFG路由
|
||||||
|
routes.NewPDFGRoutes,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 应用生命周期
|
// 应用生命周期
|
||||||
@@ -1412,6 +1412,7 @@ func RegisterRoutes(
|
|||||||
announcementRoutes *routes.AnnouncementRoutes,
|
announcementRoutes *routes.AnnouncementRoutes,
|
||||||
apiRoutes *routes.ApiRoutes,
|
apiRoutes *routes.ApiRoutes,
|
||||||
statisticsRoutes *routes.StatisticsRoutes,
|
statisticsRoutes *routes.StatisticsRoutes,
|
||||||
|
pdfgRoutes *routes.PDFGRoutes,
|
||||||
jwtAuth *middleware.JWTAuthMiddleware,
|
jwtAuth *middleware.JWTAuthMiddleware,
|
||||||
adminAuth *middleware.AdminAuthMiddleware,
|
adminAuth *middleware.AdminAuthMiddleware,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
@@ -1435,6 +1436,7 @@ func RegisterRoutes(
|
|||||||
articleRoutes.Register(router)
|
articleRoutes.Register(router)
|
||||||
announcementRoutes.Register(router)
|
announcementRoutes.Register(router)
|
||||||
statisticsRoutes.Register(router)
|
statisticsRoutes.Register(router)
|
||||||
|
pdfgRoutes.Register(router)
|
||||||
|
|
||||||
// 打印注册的路由信息
|
// 打印注册的路由信息
|
||||||
router.PrintRoutes()
|
router.PrintRoutes()
|
||||||
|
|||||||
11
internal/domains/api/dto/pdfg_dto.go
Normal file
11
internal/domains/api/dto/pdfg_dto.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// PDFG01GZReq PDFG01GZ 请求参数
|
||||||
|
type PDFG01GZReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"` // IVYZ5A9O需要
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"` // 授权标识,0或1
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"tyapi-server/internal/application/api/commands"
|
"tyapi-server/internal/application/api/commands"
|
||||||
|
"tyapi-server/internal/config"
|
||||||
"tyapi-server/internal/domains/api/services/processors"
|
"tyapi-server/internal/domains/api/services/processors"
|
||||||
"tyapi-server/internal/domains/api/services/processors/comb"
|
"tyapi-server/internal/domains/api/services/processors/comb"
|
||||||
"tyapi-server/internal/domains/api/services/processors/dwbg"
|
"tyapi-server/internal/domains/api/services/processors/dwbg"
|
||||||
"tyapi-server/internal/domains/api/services/processors/flxg"
|
"tyapi-server/internal/domains/api/services/processors/flxg"
|
||||||
"tyapi-server/internal/domains/api/services/processors/ivyz"
|
"tyapi-server/internal/domains/api/services/processors/ivyz"
|
||||||
"tyapi-server/internal/domains/api/services/processors/jrzq"
|
"tyapi-server/internal/domains/api/services/processors/jrzq"
|
||||||
|
"tyapi-server/internal/domains/api/services/processors/pdfg"
|
||||||
"tyapi-server/internal/domains/api/services/processors/qcxg"
|
"tyapi-server/internal/domains/api/services/processors/qcxg"
|
||||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||||
"tyapi-server/internal/domains/api/services/processors/test"
|
"tyapi-server/internal/domains/api/services/processors/test"
|
||||||
@@ -46,6 +48,7 @@ type ApiRequestService struct {
|
|||||||
validator interfaces.RequestValidator
|
validator interfaces.RequestValidator
|
||||||
processorDeps *processors.ProcessorDependencies
|
processorDeps *processors.ProcessorDependencies
|
||||||
combService *comb.CombService
|
combService *comb.CombService
|
||||||
|
config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApiRequestService(
|
func NewApiRequestService(
|
||||||
@@ -60,6 +63,7 @@ func NewApiRequestService(
|
|||||||
shumaiService *shumai.ShumaiService,
|
shumaiService *shumai.ShumaiService,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
productManagementService *services.ProductManagementService,
|
productManagementService *services.ProductManagementService,
|
||||||
|
cfg *config.Config,
|
||||||
) *ApiRequestService {
|
) *ApiRequestService {
|
||||||
// 创建组合包服务
|
// 创建组合包服务
|
||||||
combService := comb.NewCombService(productManagementService)
|
combService := comb.NewCombService(productManagementService)
|
||||||
@@ -79,6 +83,7 @@ func NewApiRequestService(
|
|||||||
validator: validator,
|
validator: validator,
|
||||||
processorDeps: processorDeps,
|
processorDeps: processorDeps,
|
||||||
combService: combService,
|
combService: combService,
|
||||||
|
config: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +279,9 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"TEST001": test.ProcessTestRequest,
|
"TEST001": test.ProcessTestRequest,
|
||||||
"TEST002": test.ProcessTestErrorRequest,
|
"TEST002": test.ProcessTestErrorRequest,
|
||||||
"TEST003": test.ProcessTestTimeoutRequest,
|
"TEST003": test.ProcessTestTimeoutRequest,
|
||||||
|
|
||||||
|
// PDFG系列处理器 - PDF生成
|
||||||
|
"PDFG01GZ": pdfg.ProcessPDFG01GZRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量注册到组合包服务
|
// 批量注册到组合包服务
|
||||||
@@ -295,6 +303,8 @@ func (a *ApiRequestService) PreprocessRequestApi(ctx context.Context, apiCode st
|
|||||||
|
|
||||||
// 将apiCode放入context,供外部服务使用
|
// 将apiCode放入context,供外部服务使用
|
||||||
ctx = context.WithValue(ctx, "api_code", apiCode)
|
ctx = context.WithValue(ctx, "api_code", apiCode)
|
||||||
|
// 将config放入context,供处理器使用
|
||||||
|
ctx = context.WithValue(ctx, "config", a.config)
|
||||||
|
|
||||||
// 1. 优先查找已注册的自定义处理器
|
// 1. 优先查找已注册的自定义处理器
|
||||||
if processor, exists := RequestProcessors[apiCode]; exists {
|
if processor, exists := RequestProcessors[apiCode]; exists {
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
|||||||
"IVYZN2P8": &dto.IVYZ9K7FReq{}, //身份证实名认证政务版
|
"IVYZN2P8": &dto.IVYZ9K7FReq{}, //身份证实名认证政务版
|
||||||
"YYSYH6F3": &dto.YYSYH6F3Req{}, //运营商三要素简版即时版查询
|
"YYSYH6F3": &dto.YYSYH6F3Req{}, //运营商三要素简版即时版查询
|
||||||
"IVYZX5Q2": &dto.IVYZX5Q2Req{}, //活体识别步骤二
|
"IVYZX5Q2": &dto.IVYZX5Q2Req{}, //活体识别步骤二
|
||||||
|
"PDFG01GZ": &dto.PDFG01GZReq{}, //
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先返回已配置的DTO
|
// 优先返回已配置的DTO
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ func (cs *CombService) RegisterProcessor(apiCode string, processor processors.Pr
|
|||||||
cs.processorRegistry[apiCode] = processor
|
cs.processorRegistry[apiCode] = processor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProcessor 获取处理器(用于内部调用)
|
||||||
|
func (cs *CombService) GetProcessor(apiCode string) (processors.ProcessorFunc, bool) {
|
||||||
|
processor, exists := cs.processorRegistry[apiCode]
|
||||||
|
return processor, exists
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessCombRequest 处理组合包请求 - 实现 CombServiceInterface
|
// ProcessCombRequest 处理组合包请求 - 实现 CombServiceInterface
|
||||||
func (cs *CombService) ProcessCombRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies, packageCode string) (*processors.CombinedResult, error) {
|
func (cs *CombService) ProcessCombRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies, packageCode string) (*processors.CombinedResult, error) {
|
||||||
// 1. 根据组合包code获取产品信息
|
// 1. 根据组合包code获取产品信息
|
||||||
|
|||||||
@@ -0,0 +1,466 @@
|
|||||||
|
package pdfg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tyapi-server/internal/config"
|
||||||
|
"tyapi-server/internal/domains/api/dto"
|
||||||
|
"tyapi-server/internal/domains/api/services/processors"
|
||||||
|
"tyapi-server/internal/infrastructure/external/pdfgen"
|
||||||
|
"tyapi-server/internal/shared/logger"
|
||||||
|
"tyapi-server/internal/shared/pdf"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessPDFG01GZRequest PDFG01GZ 处理器 - 大数据租赁风险PDF报告
|
||||||
|
func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
|
var paramsDto dto.PDFG01GZReq
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取全局logger
|
||||||
|
zapLogger := logger.GetGlobalLogger()
|
||||||
|
|
||||||
|
// 从context获取config(如果存在)
|
||||||
|
var cacheTTL time.Duration = 24 * time.Hour
|
||||||
|
var cacheDir string
|
||||||
|
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
||||||
|
cacheTTL = cfg.PDFGen.Cache.TTL
|
||||||
|
if cacheTTL == 0 {
|
||||||
|
cacheTTL = 24 * time.Hour
|
||||||
|
}
|
||||||
|
cacheDir = cfg.PDFGen.Cache.CacheDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最大缓存大小
|
||||||
|
var maxSize int64
|
||||||
|
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
||||||
|
maxSize = cfg.PDFGen.Cache.MaxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建PDF缓存管理器
|
||||||
|
cacheManager, err := pdf.NewPDFCacheManager(zapLogger, cacheDir, cacheTTL, maxSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("创建PDF缓存管理器失败: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从context获取config创建PDF生成服务
|
||||||
|
var pdfGenService *pdfgen.PDFGenService
|
||||||
|
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
||||||
|
pdfGenService = pdfgen.NewPDFGenService(cfg, zapLogger)
|
||||||
|
} else {
|
||||||
|
// 如果无法获取config,使用默认配置
|
||||||
|
defaultCfg := &config.Config{
|
||||||
|
App: config.AppConfig{Env: "development"},
|
||||||
|
PDFGen: config.PDFGenConfig{
|
||||||
|
DevelopmentURL: "http://pdfg.tianyuanapi.com",
|
||||||
|
ProductionURL: "http://localhost:15990",
|
||||||
|
APIPath: "/api/v1/generate/guangzhou",
|
||||||
|
Timeout: 120 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pdfGenService = pdfgen.NewPDFGenService(defaultCfg, zapLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
_, hit, createdAt, err := cacheManager.Get(paramsDto.Name, paramsDto.IDCard)
|
||||||
|
if err != nil {
|
||||||
|
zapLogger.Warn("检查缓存失败,继续生成PDF", zap.Error(err))
|
||||||
|
} else if hit {
|
||||||
|
// 缓存命中,模拟慢几秒
|
||||||
|
zapLogger.Info("PDF缓存命中,返回缓存文件",
|
||||||
|
zap.String("name", paramsDto.Name),
|
||||||
|
zap.String("id_card", paramsDto.IDCard),
|
||||||
|
zap.Time("created_at", createdAt),
|
||||||
|
)
|
||||||
|
// 模拟慢几秒(2-4秒)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// 生成下载链接
|
||||||
|
downloadURL := generateDownloadURL(paramsDto.Name, paramsDto.IDCard)
|
||||||
|
return json.Marshal(map[string]interface{}{
|
||||||
|
"download_url": downloadURL,
|
||||||
|
"cached": true,
|
||||||
|
"created_at": createdAt.Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中,需要生成PDF
|
||||||
|
zapLogger.Info("PDF缓存未命中,开始生成PDF",
|
||||||
|
zap.String("name", paramsDto.Name),
|
||||||
|
zap.String("id_card", paramsDto.IDCard),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 调用多个处理器获取数据(即使部分失败也继续)
|
||||||
|
apiData := collectAPIData(ctx, paramsDto, deps, zapLogger)
|
||||||
|
|
||||||
|
// 格式化数据为PDF生成服务需要的格式(为缺失的数据提供默认值)
|
||||||
|
formattedData := formatDataForPDF(apiData, paramsDto, zapLogger)
|
||||||
|
|
||||||
|
// 从APPLICANT_BASIC_INFO中提取报告编号(如果存在)
|
||||||
|
var reportNumber string
|
||||||
|
if len(formattedData) > 0 {
|
||||||
|
if basicInfo, ok := formattedData[0]["data"].(map[string]interface{}); ok {
|
||||||
|
if rn, ok := basicInfo["report_number"].(string); ok {
|
||||||
|
reportNumber = rn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果没有提取到,生成新的报告编号
|
||||||
|
if reportNumber == "" {
|
||||||
|
reportNumber = generateReportNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建PDF生成请求
|
||||||
|
pdfReq := &pdfgen.GeneratePDFRequest{
|
||||||
|
Data: formattedData,
|
||||||
|
ReportNumber: reportNumber,
|
||||||
|
GenerateTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用PDF生成服务
|
||||||
|
// 即使部分子处理器失败,只要有APPLICANT_BASIC_INFO就可以生成PDF
|
||||||
|
pdfResp, err := pdfGenService.GenerateGuangzhouPDF(ctx, pdfReq)
|
||||||
|
if err != nil {
|
||||||
|
zapLogger.Error("生成PDF失败",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Int("data_items", len(formattedData)),
|
||||||
|
)
|
||||||
|
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("生成PDF失败: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到缓存
|
||||||
|
if err := cacheManager.Set(paramsDto.Name, paramsDto.IDCard, pdfResp.PDFBytes); err != nil {
|
||||||
|
zapLogger.Warn("保存PDF到缓存失败", zap.Error(err))
|
||||||
|
// 不影响返回结果,只记录警告
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成下载链接
|
||||||
|
downloadURL := generateDownloadURL(paramsDto.Name, paramsDto.IDCard)
|
||||||
|
|
||||||
|
zapLogger.Info("PDF生成成功",
|
||||||
|
zap.String("name", paramsDto.Name),
|
||||||
|
zap.String("id_card", paramsDto.IDCard),
|
||||||
|
zap.String("report_number", reportNumber),
|
||||||
|
zap.String("download_url", downloadURL),
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.Marshal(map[string]interface{}{
|
||||||
|
"download_url": downloadURL,
|
||||||
|
"report_number": reportNumber,
|
||||||
|
"cached": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectAPIData 收集所有需要的API数据
|
||||||
|
// 即使部分或全部子处理器失败,也会返回结果(失败的设为nil),确保流程继续
|
||||||
|
func collectAPIData(ctx context.Context, params dto.PDFG01GZReq, deps *processors.ProcessorDependencies, logger *zap.Logger) map[string]interface{} {
|
||||||
|
apiData := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 并发调用多个处理器
|
||||||
|
type processorResult struct {
|
||||||
|
apiCode string
|
||||||
|
data interface{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(chan processorResult, 5)
|
||||||
|
|
||||||
|
// 调用IVYZ5A9O - 需要: name, id_card, auth_authorize_file_code
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Error("调用IVYZ5A9O处理器时发生panic",
|
||||||
|
zap.Any("panic", r),
|
||||||
|
)
|
||||||
|
results <- processorResult{"IVYZ5A9O", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// 检查必需字段
|
||||||
|
if params.AuthAuthorizeFileCode == "" {
|
||||||
|
logger.Warn("IVYZ5A9O缺少auth_authorize_file_code字段,跳过调用")
|
||||||
|
results <- processorResult{"IVYZ5A9O", nil, fmt.Errorf("缺少必需字段: auth_authorize_file_code")}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ivyzParams := map[string]interface{}{
|
||||||
|
"name": params.Name,
|
||||||
|
"id_card": params.IDCard,
|
||||||
|
"auth_authorize_file_code": params.AuthAuthorizeFileCode,
|
||||||
|
}
|
||||||
|
paramsBytes, err := json.Marshal(ivyzParams)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("序列化IVYZ5A9O参数失败", zap.Error(err))
|
||||||
|
results <- processorResult{"IVYZ5A9O", nil, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := callProcessor(ctx, "IVYZ5A9O", paramsBytes, deps)
|
||||||
|
results <- processorResult{"IVYZ5A9O", data, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 调用JRZQ8A2D - 需要: name, id_card, mobile_no, authorized
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Error("调用JRZQ8A2D处理器时发生panic",
|
||||||
|
zap.Any("panic", r),
|
||||||
|
)
|
||||||
|
results <- processorResult{"JRZQ8A2D", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
jrzq8a2dParams := map[string]interface{}{
|
||||||
|
"name": params.Name,
|
||||||
|
"id_card": params.IDCard,
|
||||||
|
"mobile_no": params.MobileNo,
|
||||||
|
"authorized": params.Authorized,
|
||||||
|
}
|
||||||
|
paramsBytes, err := json.Marshal(jrzq8a2dParams)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("序列化JRZQ8A2D参数失败", zap.Error(err))
|
||||||
|
results <- processorResult{"JRZQ8A2D", nil, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := callProcessor(ctx, "JRZQ8A2D", paramsBytes, deps)
|
||||||
|
results <- processorResult{"JRZQ8A2D", data, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 调用FLXGDEA9 - 需要: name, id_card, authorized
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Error("调用FLXGDEA9处理器时发生panic",
|
||||||
|
zap.Any("panic", r),
|
||||||
|
)
|
||||||
|
results <- processorResult{"FLXGDEA9", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
flxgParams := map[string]interface{}{
|
||||||
|
"name": params.Name,
|
||||||
|
"id_card": params.IDCard,
|
||||||
|
"authorized": params.Authorized,
|
||||||
|
}
|
||||||
|
paramsBytes, err := json.Marshal(flxgParams)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("序列化FLXGDEA9参数失败", zap.Error(err))
|
||||||
|
results <- processorResult{"FLXGDEA9", nil, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := callProcessor(ctx, "FLXGDEA9", paramsBytes, deps)
|
||||||
|
results <- processorResult{"FLXGDEA9", data, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 调用JRZQ1D09 - 需要: name, id_card, mobile_no, authorized
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Error("调用JRZQ1D09处理器时发生panic",
|
||||||
|
zap.Any("panic", r),
|
||||||
|
)
|
||||||
|
results <- processorResult{"JRZQ1D09", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
jrzq1d09Params := map[string]interface{}{
|
||||||
|
"name": params.Name,
|
||||||
|
"id_card": params.IDCard,
|
||||||
|
"mobile_no": params.MobileNo,
|
||||||
|
"authorized": params.Authorized,
|
||||||
|
}
|
||||||
|
paramsBytes, err := json.Marshal(jrzq1d09Params)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("序列化JRZQ1D09参数失败", zap.Error(err))
|
||||||
|
results <- processorResult{"JRZQ1D09", nil, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := callProcessor(ctx, "JRZQ1D09", paramsBytes, deps)
|
||||||
|
results <- processorResult{"JRZQ1D09", data, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 调用JRZQ8B3C - 需要: name, id_card, mobile_no (不需要authorized)
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Error("调用JRZQ8B3C处理器时发生panic",
|
||||||
|
zap.Any("panic", r),
|
||||||
|
)
|
||||||
|
results <- processorResult{"JRZQ8B3C", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
jrzq8b3cParams := map[string]interface{}{
|
||||||
|
"name": params.Name,
|
||||||
|
"id_card": params.IDCard,
|
||||||
|
"mobile_no": params.MobileNo,
|
||||||
|
}
|
||||||
|
paramsBytes, err := json.Marshal(jrzq8b3cParams)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("序列化JRZQ8B3C参数失败", zap.Error(err))
|
||||||
|
results <- processorResult{"JRZQ8B3C", nil, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := callProcessor(ctx, "JRZQ8B3C", paramsBytes, deps)
|
||||||
|
results <- processorResult{"JRZQ8B3C", data, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 收集结果,即使所有处理器都失败也继续
|
||||||
|
successCount := 0
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
result := <-results
|
||||||
|
if result.err != nil {
|
||||||
|
// 记录错误但不中断流程,允许部分数据缺失
|
||||||
|
logger.Warn("调用处理器失败,将使用默认值",
|
||||||
|
zap.String("api_code", result.apiCode),
|
||||||
|
zap.Error(result.err),
|
||||||
|
)
|
||||||
|
apiData[result.apiCode] = nil
|
||||||
|
} else {
|
||||||
|
apiData[result.apiCode] = result.data
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("子处理器调用完成",
|
||||||
|
zap.Int("total", 5),
|
||||||
|
zap.Int("success", successCount),
|
||||||
|
zap.Int("failed", 5-successCount),
|
||||||
|
)
|
||||||
|
|
||||||
|
return apiData
|
||||||
|
}
|
||||||
|
|
||||||
|
// callProcessor 调用指定的处理器
|
||||||
|
func callProcessor(ctx context.Context, apiCode string, params []byte, deps *processors.ProcessorDependencies) (interface{}, error) {
|
||||||
|
// 通过CombService获取处理器
|
||||||
|
if combSvc, ok := deps.CombService.(interface {
|
||||||
|
GetProcessor(apiCode string) (processors.ProcessorFunc, bool)
|
||||||
|
}); ok {
|
||||||
|
processor, exists := combSvc.GetProcessor(apiCode)
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("未找到处理器: %s", apiCode)
|
||||||
|
}
|
||||||
|
respBytes, err := processor(ctx, params, deps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data interface{}
|
||||||
|
if err := json.Unmarshal(respBytes, &data); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果无法通过CombService获取,返回错误
|
||||||
|
return nil, fmt.Errorf("无法获取处理器: %s,CombService不支持GetProcessor方法", apiCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDataForPDF 格式化数据为PDF生成服务需要的格式
|
||||||
|
// 为所有子处理器提供数据,即使失败也提供默认值,确保PDF生成服务能收到完整结构
|
||||||
|
func formatDataForPDF(apiData map[string]interface{}, params dto.PDFG01GZReq, logger *zap.Logger) []map[string]interface{} {
|
||||||
|
result := make([]map[string]interface{}, 0)
|
||||||
|
|
||||||
|
// 1. APPLICANT_BASIC_INFO - 申请人基本信息(始终存在)
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "APPLICANT_BASIC_INFO",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"name": params.Name,
|
||||||
|
"id_card": params.IDCard,
|
||||||
|
"mobile": params.MobileNo,
|
||||||
|
"query_time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
"report_number": generateReportNumber(),
|
||||||
|
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. IVYZ5A9O - 自然人综合风险智能评估模型
|
||||||
|
if data, ok := apiData["IVYZ5A9O"]; ok && data != nil {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "IVYZ5A9O",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 子处理器失败或无数据时,返回空对象 {}
|
||||||
|
logger.Debug("IVYZ5A9O数据缺失,使用空对象")
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "IVYZ5A9O",
|
||||||
|
"data": map[string]interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. JRZQ8A2D - 特殊名单验证B
|
||||||
|
if data, ok := apiData["JRZQ8A2D"]; ok && data != nil {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "JRZQ8A2D",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.Debug("JRZQ8A2D数据缺失,使用空对象")
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "JRZQ8A2D",
|
||||||
|
"data": map[string]interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. FLXGDEA9 - 公安不良人员名单
|
||||||
|
if data, ok := apiData["FLXGDEA9"]; ok && data != nil {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "FLXGDEA9",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.Debug("FLXGDEA9数据缺失,使用空对象")
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "FLXGDEA9",
|
||||||
|
"data": map[string]interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. JRZQ1D09 - 3C租赁申请意向
|
||||||
|
if data, ok := apiData["JRZQ1D09"]; ok && data != nil {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "JRZQ1D09",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.Debug("JRZQ1D09数据缺失,使用空对象")
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "JRZQ1D09",
|
||||||
|
"data": map[string]interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. JRZQ8B3C - 个人消费能力等级
|
||||||
|
if data, ok := apiData["JRZQ8B3C"]; ok && data != nil {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "JRZQ8B3C",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.Debug("JRZQ8B3C数据缺失,使用空对象")
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"apiID": "JRZQ8B3C",
|
||||||
|
"data": map[string]interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateReportNumber 生成报告编号
|
||||||
|
func generateReportNumber() string {
|
||||||
|
return fmt.Sprintf("RPT%s", time.Now().Format("20060102150405"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDownloadURL 生成下载链接
|
||||||
|
func generateDownloadURL(name, idCard string) string {
|
||||||
|
// 这里应该生成实际的下载URL
|
||||||
|
// 暂时返回一个占位符,实际应该根据服务器配置生成
|
||||||
|
return fmt.Sprintf("/api/v1/pdfg/download?name=%s&id_card=%s", name, idCard)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -428,11 +428,6 @@ func (c *Certification) CompleteCertification() error {
|
|||||||
return fmt.Errorf("当前状态 %s 不允许完成认证", enums.GetStatusName(c.Status))
|
return fmt.Errorf("当前状态 %s 不允许完成认证", enums.GetStatusName(c.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证合同信息完整性
|
|
||||||
if c.ContractFileID == "" || c.EsignFlowID == "" || c.ContractURL == "" {
|
|
||||||
return errors.New("合同信息不完整,无法完成认证")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态转换
|
// 状态转换
|
||||||
if err := c.TransitionTo(enums.StatusCompleted, enums.ActorTypeSystem, "system", "系统处理完成,认证成功"); err != nil {
|
if err := c.TransitionTo(enums.StatusCompleted, enums.ActorTypeSystem, "system", "系统处理完成,认证成功"); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -567,18 +562,11 @@ func (c *Certification) ValidateBusinessRules() error {
|
|||||||
|
|
||||||
// 状态相关验证
|
// 状态相关验证
|
||||||
switch c.Status {
|
switch c.Status {
|
||||||
case enums.StatusEnterpriseVerified:
|
|
||||||
if c.ContractURL == "" {
|
|
||||||
return errors.New("企业认证成功后,合同文件ID和合同URL不能为空")
|
|
||||||
}
|
|
||||||
case enums.StatusContractSigned:
|
case enums.StatusContractSigned:
|
||||||
if c.ContractFileID == "" || c.EsignFlowID == "" {
|
if c.ContractFileID == "" || c.EsignFlowID == "" {
|
||||||
return errors.New("合同签署状态下必须有完整的合同信息")
|
return errors.New("合同签署状态下必须有完整的合同信息")
|
||||||
}
|
}
|
||||||
case enums.StatusCompleted:
|
case enums.StatusCompleted:
|
||||||
if c.ContractFileID == "" || c.EsignFlowID == "" || c.ContractURL == "" {
|
|
||||||
return errors.New("认证完成状态下必须有完整的合同信息")
|
|
||||||
}
|
|
||||||
if c.CompletedAt == nil {
|
if c.CompletedAt == nil {
|
||||||
return errors.New("认证完成状态下必须有完成时间")
|
return errors.New("认证完成状态下必须有完成时间")
|
||||||
}
|
}
|
||||||
|
|||||||
154
internal/infrastructure/external/pdfgen/pdfgen_service.go
vendored
Normal file
154
internal/infrastructure/external/pdfgen/pdfgen_service.go
vendored
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package pdfgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tyapi-server/internal/config"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PDFGenService PDF生成服务客户端
|
||||||
|
type PDFGenService struct {
|
||||||
|
baseURL string
|
||||||
|
apiPath string
|
||||||
|
logger *zap.Logger
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPDFGenService 创建PDF生成服务客户端
|
||||||
|
func NewPDFGenService(cfg *config.Config, logger *zap.Logger) *PDFGenService {
|
||||||
|
// 根据环境选择服务地址
|
||||||
|
var baseURL string
|
||||||
|
if cfg.App.IsProduction() {
|
||||||
|
baseURL = cfg.PDFGen.ProductionURL
|
||||||
|
} else {
|
||||||
|
baseURL = cfg.PDFGen.DevelopmentURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果配置为空,使用默认值
|
||||||
|
if baseURL == "" {
|
||||||
|
if cfg.App.IsProduction() {
|
||||||
|
baseURL = "http://localhost:15990"
|
||||||
|
} else {
|
||||||
|
baseURL = "http://1.117.67.95:15990"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取API路径,如果为空使用默认值
|
||||||
|
apiPath := cfg.PDFGen.APIPath
|
||||||
|
if apiPath == "" {
|
||||||
|
apiPath = "/api/v1/generate/guangzhou"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取超时时间,如果为0使用默认值
|
||||||
|
timeout := cfg.PDFGen.Timeout
|
||||||
|
if timeout == 0 {
|
||||||
|
timeout = 120 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("PDF生成服务已初始化",
|
||||||
|
zap.String("base_url", baseURL),
|
||||||
|
zap.String("api_path", apiPath),
|
||||||
|
zap.Duration("timeout", timeout),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &PDFGenService{
|
||||||
|
baseURL: baseURL,
|
||||||
|
apiPath: apiPath,
|
||||||
|
logger: logger,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePDFRequest PDF生成请求
|
||||||
|
type GeneratePDFRequest struct {
|
||||||
|
Data []map[string]interface{} `json:"data"`
|
||||||
|
ReportNumber string `json:"report_number,omitempty"`
|
||||||
|
GenerateTime string `json:"generate_time,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePDFResponse PDF生成响应
|
||||||
|
type GeneratePDFResponse struct {
|
||||||
|
PDFBytes []byte
|
||||||
|
FileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateGuangzhouPDF 生成广州大数据租赁风险PDF报告
|
||||||
|
func (s *PDFGenService) GenerateGuangzhouPDF(ctx context.Context, req *GeneratePDFRequest) (*GeneratePDFResponse, error) {
|
||||||
|
// 构建请求体
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求URL
|
||||||
|
url := fmt.Sprintf("%s%s", s.baseURL, s.apiPath)
|
||||||
|
|
||||||
|
// 创建HTTP请求
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
s.logger.Info("开始调用PDF生成服务",
|
||||||
|
zap.String("url", url),
|
||||||
|
zap.Int("data_count", len(req.Data)),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := s.client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("调用PDF生成服务失败",
|
||||||
|
zap.String("url", url),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil, fmt.Errorf("调用PDF生成服务失败: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 检查HTTP状态码
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// 尝试读取错误信息
|
||||||
|
errorBody, _ := io.ReadAll(resp.Body)
|
||||||
|
s.logger.Error("PDF生成服务返回错误",
|
||||||
|
zap.Int("status_code", resp.StatusCode),
|
||||||
|
zap.String("error_body", string(errorBody)),
|
||||||
|
)
|
||||||
|
return nil, fmt.Errorf("PDF生成失败,状态码: %d, 错误: %s", resp.StatusCode, string(errorBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取PDF文件
|
||||||
|
pdfBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取PDF文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
fileName := "大数据租赁风险报告.pdf"
|
||||||
|
if req.ReportNumber != "" {
|
||||||
|
fileName = fmt.Sprintf("%s.pdf", req.ReportNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("PDF生成成功",
|
||||||
|
zap.String("file_name", fileName),
|
||||||
|
zap.Int("file_size", len(pdfBytes)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &GeneratePDFResponse{
|
||||||
|
PDFBytes: pdfBytes,
|
||||||
|
FileName: fileName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
@@ -338,6 +338,43 @@ func (h *CertificationHandler) ListCertifications(c *gin.Context) {
|
|||||||
h.response.Success(c, result, "获取认证列表成功")
|
h.response.Success(c, result, "获取认证列表成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
|
||||||
|
// @Summary 管理员代用户完成认证(暂不关联合同)
|
||||||
|
// @Description 后台补充企业信息并直接完成认证,暂时不要求上传合同
|
||||||
|
// @Tags 认证管理
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body commands.AdminCompleteCertificationCommand true "管理员代用户完成认证请求"
|
||||||
|
// @Success 200 {object} responses.CertificationResponse "代用户完成认证成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 403 {object} map[string]interface{} "权限不足"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/certifications/admin/complete-without-contract [post]
|
||||||
|
func (h *CertificationHandler) AdminCompleteCertificationWithoutContract(c *gin.Context) {
|
||||||
|
adminID := h.getCurrentUserID(c)
|
||||||
|
if adminID == "" {
|
||||||
|
h.response.Unauthorized(c, "用户未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd commands.AdminCompleteCertificationCommand
|
||||||
|
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.AdminID = adminID
|
||||||
|
|
||||||
|
result, err := h.appService.AdminCompleteCertificationWithoutContract(c.Request.Context(), &cmd)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("管理员代用户完成认证失败", zap.Error(err), zap.String("admin_id", adminID), zap.String("user_id", cmd.UserID))
|
||||||
|
h.response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.response.Success(c, result, "代用户完成认证成功")
|
||||||
|
}
|
||||||
|
|
||||||
// ================ 回调处理 ================
|
// ================ 回调处理 ================
|
||||||
|
|
||||||
// HandleEsignCallback 处理e签宝回调
|
// HandleEsignCallback 处理e签宝回调
|
||||||
|
|||||||
93
internal/infrastructure/http/handlers/pdfg_handler.go
Normal file
93
internal/infrastructure/http/handlers/pdfg_handler.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"tyapi-server/internal/shared/interfaces"
|
||||||
|
"tyapi-server/internal/shared/pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PDFGHandler PDFG处理器
|
||||||
|
type PDFGHandler struct {
|
||||||
|
cacheManager *pdf.PDFCacheManager
|
||||||
|
responseBuilder interfaces.ResponseBuilder
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPDFGHandler 创建PDFG处理器
|
||||||
|
func NewPDFGHandler(
|
||||||
|
cacheManager *pdf.PDFCacheManager,
|
||||||
|
responseBuilder interfaces.ResponseBuilder,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *PDFGHandler {
|
||||||
|
return &PDFGHandler{
|
||||||
|
cacheManager: cacheManager,
|
||||||
|
responseBuilder: responseBuilder,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadPDF 下载PDF文件
|
||||||
|
// GET /api/v1/pdfg/download?name=xxx&id_card=xxx
|
||||||
|
func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
||||||
|
name := c.Query("name")
|
||||||
|
idCard := c.Query("id_card")
|
||||||
|
|
||||||
|
if name == "" || idCard == "" {
|
||||||
|
h.responseBuilder.BadRequest(c, "姓名和身份证号不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从缓存获取PDF
|
||||||
|
pdfBytes, hit, createdAt, err := h.cacheManager.Get(name, idCard)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取PDF缓存失败",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.String("id_card", idCard),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
h.responseBuilder.InternalError(c, "获取PDF文件失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hit {
|
||||||
|
h.logger.Warn("PDF文件不存在或已过期",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.String("id_card", idCard),
|
||||||
|
)
|
||||||
|
h.responseBuilder.NotFound(c, "PDF文件不存在或已过期,请重新生成")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期(从文件生成时间开始算24小时)
|
||||||
|
expiresAt := createdAt.Add(24 * time.Hour)
|
||||||
|
if time.Now().After(expiresAt) {
|
||||||
|
h.logger.Warn("PDF文件已过期",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.String("id_card", idCard),
|
||||||
|
zap.Time("expires_at", expiresAt),
|
||||||
|
)
|
||||||
|
h.responseBuilder.NotFound(c, "PDF文件已过期,请重新生成")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
c.Header("Content-Type", "application/pdf")
|
||||||
|
c.Header("Content-Disposition", `attachment; filename="大数据租赁风险报告.pdf"`)
|
||||||
|
c.Header("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
|
||||||
|
|
||||||
|
// 发送PDF文件
|
||||||
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||||
|
|
||||||
|
h.logger.Info("PDF文件下载成功",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.String("id_card", idCard),
|
||||||
|
zap.Int("file_size", len(pdfBytes)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -826,7 +826,7 @@ func (h *ProductHandler) DownloadProductDocumentation(c *gin.Context) {
|
|||||||
|
|
||||||
if h.pdfCacheManager != nil {
|
if h.pdfCacheManager != nil {
|
||||||
var cacheErr error
|
var cacheErr error
|
||||||
pdfBytes, cacheHit, cacheErr = h.pdfCacheManager.Get(productID, docVersion)
|
pdfBytes, cacheHit, cacheErr = h.pdfCacheManager.GetByProduct(productID, docVersion)
|
||||||
if cacheErr != nil {
|
if cacheErr != nil {
|
||||||
h.logger.Warn("从缓存获取PDF失败,将重新生成",
|
h.logger.Warn("从缓存获取PDF失败,将重新生成",
|
||||||
zap.String("product_id", productID),
|
zap.String("product_id", productID),
|
||||||
@@ -972,7 +972,7 @@ func (h *ProductHandler) DownloadProductDocumentation(c *gin.Context) {
|
|||||||
// 保存到缓存(异步,不阻塞响应)
|
// 保存到缓存(异步,不阻塞响应)
|
||||||
if h.pdfCacheManager != nil {
|
if h.pdfCacheManager != nil {
|
||||||
go func() {
|
go func() {
|
||||||
if err := h.pdfCacheManager.Set(productID, docVersion, pdfBytes); err != nil {
|
if err := h.pdfCacheManager.SetByProduct(productID, docVersion, pdfBytes); err != nil {
|
||||||
h.logger.Warn("保存PDF到缓存失败",
|
h.logger.Warn("保存PDF到缓存失败",
|
||||||
zap.String("product_id", productID),
|
zap.String("product_id", productID),
|
||||||
zap.String("version", docVersion),
|
zap.String("version", docVersion),
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
|
|||||||
// 前端确认是否完成签署
|
// 前端确认是否完成签署
|
||||||
authGroup.POST("/confirm-sign", r.handler.ConfirmSign)
|
authGroup.POST("/confirm-sign", r.handler.ConfirmSign)
|
||||||
|
|
||||||
|
// 管理员代用户完成认证(暂不关联合同)
|
||||||
|
authGroup.POST("/admin/complete-without-contract", r.handler.AdminCompleteCertificationWithoutContract)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回调路由(不需要认证,但需要验证签名)
|
// 回调路由(不需要认证,但需要验证签名)
|
||||||
|
|||||||
39
internal/infrastructure/http/routes/pdfg_routes.go
Normal file
39
internal/infrastructure/http/routes/pdfg_routes.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
sharedhttp "tyapi-server/internal/shared/http"
|
||||||
|
"tyapi-server/internal/infrastructure/http/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PDFGRoutes PDFG路由
|
||||||
|
type PDFGRoutes struct {
|
||||||
|
pdfgHandler *handlers.PDFGHandler
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPDFGRoutes 创建PDFG路由
|
||||||
|
func NewPDFGRoutes(
|
||||||
|
pdfgHandler *handlers.PDFGHandler,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *PDFGRoutes {
|
||||||
|
return &PDFGRoutes{
|
||||||
|
pdfgHandler: pdfgHandler,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 注册相关路由
|
||||||
|
func (r *PDFGRoutes) Register(router *sharedhttp.GinRouter) {
|
||||||
|
engine := router.GetEngine()
|
||||||
|
apiGroup := engine.Group("/api/v1")
|
||||||
|
|
||||||
|
{
|
||||||
|
// PDF下载接口 - 不需要认证(因为下载链接已经包含了验证信息)
|
||||||
|
apiGroup.GET("/pdfg/download", r.pdfgHandler.DownloadPDF)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("PDFG路由注册完成")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,39 +12,31 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PDFCacheManager PDF缓存管理器
|
// PDFCacheManager PDF缓存管理器(统一实现)
|
||||||
// 负责管理PDF文件的本地缓存,提高下载性能
|
// 支持两种缓存键生成方式:
|
||||||
|
// 1. 基于姓名+身份证(用于PDFG报告)
|
||||||
|
// 2. 基于产品ID+版本(用于产品文档)
|
||||||
type PDFCacheManager struct {
|
type PDFCacheManager struct {
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
cacheDir string
|
cacheDir string
|
||||||
ttl time.Duration // 缓存过期时间
|
ttl time.Duration // 缓存过期时间
|
||||||
maxSize int64 // 最大缓存大小(字节)
|
maxSize int64 // 最大缓存大小(字节,0表示不限制)
|
||||||
mu sync.RWMutex // 保护并发访问
|
mu sync.RWMutex // 保护并发访问
|
||||||
cleanupOnce sync.Once // 确保清理任务只启动一次
|
cleanupOnce sync.Once // 确保清理任务只启动一次
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheInfo 缓存信息
|
|
||||||
type CacheInfo struct {
|
|
||||||
FilePath string
|
|
||||||
Size int64
|
|
||||||
CreatedAt time.Time
|
|
||||||
ExpiresAt time.Time
|
|
||||||
ProductID string
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPDFCacheManager 创建PDF缓存管理器
|
// NewPDFCacheManager 创建PDF缓存管理器
|
||||||
|
// cacheDir: 缓存目录(空则使用默认目录)
|
||||||
|
// ttl: 缓存过期时间
|
||||||
|
// maxSize: 最大缓存大小(字节,0表示不限制)
|
||||||
func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration, maxSize int64) (*PDFCacheManager, error) {
|
func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration, maxSize int64) (*PDFCacheManager, error) {
|
||||||
// 如果缓存目录为空,使用项目根目录的storage/component-reports/cache目录
|
// 如果缓存目录为空,使用项目根目录的storage/pdfg-cache目录
|
||||||
if cacheDir == "" {
|
if cacheDir == "" {
|
||||||
// 获取当前工作目录并构建项目根目录路径
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 如果获取工作目录失败,回退到临时目录
|
cacheDir = filepath.Join(os.TempDir(), "tyapi_pdfg_cache")
|
||||||
cacheDir = filepath.Join(os.TempDir(), "tyapi_pdf_cache")
|
|
||||||
} else {
|
} else {
|
||||||
// 构建项目根目录下的storage/component-reports/cache路径
|
cacheDir = filepath.Join(wd, "storage", "pdfg-cache")
|
||||||
cacheDir = filepath.Join(wd, "storage", "component-reports", "cache")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +60,19 @@ func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration,
|
|||||||
zap.Duration("ttl", ttl),
|
zap.Duration("ttl", ttl),
|
||||||
zap.Int64("max_size", maxSize),
|
zap.Int64("max_size", maxSize),
|
||||||
)
|
)
|
||||||
|
|
||||||
return manager, nil
|
return manager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCacheKey 生成缓存键
|
// GetCacheKey 生成缓存键(基于姓名+身份证)
|
||||||
// 基于产品ID和文档版本号生成唯一的缓存键
|
func (m *PDFCacheManager) GetCacheKey(name, idCard string) string {
|
||||||
func (m *PDFCacheManager) GetCacheKey(productID string, version string) string {
|
key := fmt.Sprintf("%s:%s", name, idCard)
|
||||||
// 使用MD5哈希生成短键名
|
hash := md5.Sum([]byte(key))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheKeyByProduct 生成缓存键(基于产品ID+版本)
|
||||||
|
func (m *PDFCacheManager) GetCacheKeyByProduct(productID, version string) string {
|
||||||
key := fmt.Sprintf("%s:%s", productID, version)
|
key := fmt.Sprintf("%s:%s", productID, version)
|
||||||
hash := md5.Sum([]byte(key))
|
hash := md5.Sum([]byte(key))
|
||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
@@ -85,10 +83,23 @@ func (m *PDFCacheManager) GetCachePath(cacheKey string) string {
|
|||||||
return filepath.Join(m.cacheDir, fmt.Sprintf("%s.pdf", cacheKey))
|
return filepath.Join(m.cacheDir, fmt.Sprintf("%s.pdf", cacheKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 从缓存获取PDF文件
|
// Get 从缓存获取PDF文件(基于姓名+身份证)
|
||||||
// 返回PDF字节流和是否命中缓存
|
// 返回PDF字节流、是否命中缓存、文件创建时间、错误
|
||||||
func (m *PDFCacheManager) Get(productID string, version string) ([]byte, bool, error) {
|
func (m *PDFCacheManager) Get(name, idCard string) ([]byte, bool, time.Time, error) {
|
||||||
cacheKey := m.GetCacheKey(productID, version)
|
cacheKey := m.GetCacheKey(name, idCard)
|
||||||
|
return m.getByKey(cacheKey, name, idCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByProduct 从缓存获取PDF文件(基于产品ID+版本)
|
||||||
|
// 返回PDF字节流、是否命中缓存、错误
|
||||||
|
func (m *PDFCacheManager) GetByProduct(productID, version string) ([]byte, bool, error) {
|
||||||
|
cacheKey := m.GetCacheKeyByProduct(productID, version)
|
||||||
|
pdfBytes, hit, _, err := m.getByKey(cacheKey, productID, version)
|
||||||
|
return pdfBytes, hit, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getByKey 内部方法:根据缓存键获取文件
|
||||||
|
func (m *PDFCacheManager) getByKey(cacheKey string, key1, key2 string) ([]byte, bool, time.Time, error) {
|
||||||
cachePath := m.GetCachePath(cacheKey)
|
cachePath := m.GetCachePath(cacheKey)
|
||||||
|
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
@@ -98,85 +109,74 @@ func (m *PDFCacheManager) Get(productID string, version string) ([]byte, bool, e
|
|||||||
info, err := os.Stat(cachePath)
|
info, err := os.Stat(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, false, nil // 缓存未命中
|
return nil, false, time.Time{}, nil // 缓存未命中
|
||||||
}
|
}
|
||||||
return nil, false, fmt.Errorf("检查缓存文件失败: %w", err)
|
return nil, false, time.Time{}, fmt.Errorf("检查缓存文件失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查文件是否过期
|
// 检查文件是否过期(从文件生成时间开始算24小时)
|
||||||
// 使用文件的修改时间作为创建时间
|
|
||||||
createdAt := info.ModTime()
|
createdAt := info.ModTime()
|
||||||
expiresAt := createdAt.Add(m.ttl)
|
expiresAt := createdAt.Add(m.ttl)
|
||||||
if time.Now().After(expiresAt) {
|
if time.Now().After(expiresAt) {
|
||||||
// 缓存已过期,删除文件
|
// 缓存已过期,删除文件
|
||||||
m.logger.Debug("缓存已过期,删除文件",
|
m.logger.Debug("缓存已过期,删除文件",
|
||||||
zap.String("product_id", productID),
|
zap.String("key1", key1),
|
||||||
|
zap.String("key2", key2),
|
||||||
zap.String("cache_key", cacheKey),
|
zap.String("cache_key", cacheKey),
|
||||||
zap.Time("expires_at", expiresAt),
|
zap.Time("expires_at", expiresAt),
|
||||||
)
|
)
|
||||||
_ = os.Remove(cachePath)
|
_ = os.Remove(cachePath)
|
||||||
return nil, false, nil
|
return nil, false, time.Time{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取缓存文件
|
// 读取缓存文件
|
||||||
pdfBytes, err := os.ReadFile(cachePath)
|
pdfBytes, err := os.ReadFile(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("读取缓存文件失败: %w", err)
|
return nil, false, time.Time{}, fmt.Errorf("读取缓存文件失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.logger.Debug("缓存命中",
|
m.logger.Debug("缓存命中",
|
||||||
zap.String("product_id", productID),
|
zap.String("key1", key1),
|
||||||
|
zap.String("key2", key2),
|
||||||
zap.String("cache_key", cacheKey),
|
zap.String("cache_key", cacheKey),
|
||||||
zap.Int64("file_size", int64(len(pdfBytes))),
|
zap.Int64("file_size", int64(len(pdfBytes))),
|
||||||
zap.Time("expires_at", expiresAt),
|
zap.Time("expires_at", expiresAt),
|
||||||
)
|
)
|
||||||
|
|
||||||
return pdfBytes, true, nil
|
return pdfBytes, true, createdAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set 将PDF文件保存到缓存
|
// Set 将PDF文件保存到缓存(基于姓名+身份证)
|
||||||
func (m *PDFCacheManager) Set(productID string, version string, pdfBytes []byte) error {
|
func (m *PDFCacheManager) Set(name, idCard string, pdfBytes []byte) error {
|
||||||
cacheKey := m.GetCacheKey(productID, version)
|
cacheKey := m.GetCacheKey(name, idCard)
|
||||||
|
return m.setByKey(cacheKey, pdfBytes, name, idCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetByProduct 将PDF文件保存到缓存(基于产品ID+版本)
|
||||||
|
func (m *PDFCacheManager) SetByProduct(productID, version string, pdfBytes []byte) error {
|
||||||
|
cacheKey := m.GetCacheKeyByProduct(productID, version)
|
||||||
|
return m.setByKey(cacheKey, pdfBytes, productID, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setByKey 内部方法:根据缓存键保存文件
|
||||||
|
func (m *PDFCacheManager) setByKey(cacheKey string, pdfBytes []byte, key1, key2 string) error {
|
||||||
cachePath := m.GetCachePath(cacheKey)
|
cachePath := m.GetCachePath(cacheKey)
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
// 检查是否已存在缓存文件(用于判断是新建还是更新)
|
|
||||||
fileExists := false
|
|
||||||
var oldFileSize int64
|
|
||||||
if info, err := os.Stat(cachePath); err == nil {
|
|
||||||
fileExists = true
|
|
||||||
oldFileSize = info.Size()
|
|
||||||
m.logger.Info("检测到已存在的缓存文件,将更新缓存",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("version", version),
|
|
||||||
zap.String("cache_key", cacheKey),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Int64("old_file_size", oldFileSize),
|
|
||||||
zap.Int64("new_file_size", int64(len(pdfBytes))),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
m.logger.Info("开始创建新的PDF缓存",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("version", version),
|
|
||||||
zap.String("cache_key", cacheKey),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Int64("file_size", int64(len(pdfBytes))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查缓存大小限制
|
// 检查缓存大小限制
|
||||||
if m.maxSize > 0 {
|
if m.maxSize > 0 {
|
||||||
currentSize, err := m.getCacheDirSize()
|
currentSize, err := m.getCacheDirSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("获取缓存目录大小失败", zap.Error(err))
|
m.logger.Warn("获取缓存目录大小失败", zap.Error(err))
|
||||||
} else {
|
} else {
|
||||||
// 如果更新已存在的文件,需要减去旧文件的大小
|
// 检查是否已存在文件
|
||||||
sizeToAdd := int64(len(pdfBytes))
|
var oldFileSize int64
|
||||||
if fileExists {
|
if info, err := os.Stat(cachePath); err == nil {
|
||||||
sizeToAdd = int64(len(pdfBytes)) - oldFileSize
|
oldFileSize = info.Size()
|
||||||
}
|
}
|
||||||
|
sizeToAdd := int64(len(pdfBytes)) - oldFileSize
|
||||||
|
|
||||||
if currentSize+sizeToAdd > m.maxSize {
|
if currentSize+sizeToAdd > m.maxSize {
|
||||||
// 缓存空间不足,清理过期文件
|
// 缓存空间不足,清理过期文件
|
||||||
@@ -184,9 +184,8 @@ func (m *PDFCacheManager) Set(productID string, version string, pdfBytes []byte)
|
|||||||
zap.Int64("current_size", currentSize),
|
zap.Int64("current_size", currentSize),
|
||||||
zap.Int64("max_size", m.maxSize),
|
zap.Int64("max_size", m.maxSize),
|
||||||
zap.Int64("required_size", sizeToAdd),
|
zap.Int64("required_size", sizeToAdd),
|
||||||
zap.Bool("is_update", fileExists),
|
|
||||||
)
|
)
|
||||||
if err := m.cleanupExpiredFiles(); err != nil {
|
if err := m.cleanExpiredFiles(); err != nil {
|
||||||
m.logger.Warn("清理过期文件失败", zap.Error(err))
|
m.logger.Warn("清理过期文件失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
// 再次检查
|
// 再次检查
|
||||||
@@ -203,249 +202,17 @@ func (m *PDFCacheManager) Set(productID string, version string, pdfBytes []byte)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入缓存文件
|
// 写入文件
|
||||||
if err := os.WriteFile(cachePath, pdfBytes, 0644); err != nil {
|
if err := os.WriteFile(cachePath, pdfBytes, 0644); err != nil {
|
||||||
m.logger.Error("写入缓存文件失败",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
return fmt.Errorf("写入缓存文件失败: %w", err)
|
return fmt.Errorf("写入缓存文件失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算过期时间
|
m.logger.Debug("PDF已保存到缓存",
|
||||||
expiresAt := time.Now().Add(m.ttl)
|
zap.String("key1", key1),
|
||||||
|
zap.String("key2", key2),
|
||||||
// 根据是新建还是更新,记录不同的日志
|
|
||||||
if fileExists {
|
|
||||||
m.logger.Info("PDF缓存已更新",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("version", version),
|
|
||||||
zap.String("cache_key", cacheKey),
|
zap.String("cache_key", cacheKey),
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Int64("old_file_size", oldFileSize),
|
|
||||||
zap.Int64("new_file_size", int64(len(pdfBytes))),
|
|
||||||
zap.Int64("size_change", int64(len(pdfBytes))-oldFileSize),
|
|
||||||
zap.Duration("ttl", m.ttl),
|
|
||||||
zap.Time("expires_at", expiresAt),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
m.logger.Info("PDF缓存已创建",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("version", version),
|
|
||||||
zap.String("cache_key", cacheKey),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Int64("file_size", int64(len(pdfBytes))),
|
zap.Int64("file_size", int64(len(pdfBytes))),
|
||||||
zap.Duration("ttl", m.ttl),
|
|
||||||
zap.Time("expires_at", expiresAt),
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate 使缓存失效
|
|
||||||
func (m *PDFCacheManager) Invalidate(productID string, version string) error {
|
|
||||||
cacheKey := m.GetCacheKey(productID, version)
|
|
||||||
cachePath := m.GetCachePath(cacheKey)
|
|
||||||
|
|
||||||
m.logger.Info("开始使缓存失效",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("version", version),
|
|
||||||
zap.String("cache_key", cacheKey),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
)
|
|
||||||
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
// 检查文件是否存在,记录文件信息
|
|
||||||
fileInfo, err := os.Stat(cachePath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
m.logger.Info("缓存文件不存在,视为已失效",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("cache_key", cacheKey),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
)
|
|
||||||
return nil // 文件不存在,视为已失效
|
|
||||||
}
|
|
||||||
m.logger.Error("检查缓存文件失败",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
return fmt.Errorf("检查缓存文件失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录文件信息
|
|
||||||
fileSize := fileInfo.Size()
|
|
||||||
fileModTime := fileInfo.ModTime()
|
|
||||||
|
|
||||||
// 删除缓存文件
|
|
||||||
if err := os.Remove(cachePath); err != nil {
|
|
||||||
m.logger.Error("删除缓存文件失败",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
return fmt.Errorf("删除缓存文件失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger.Info("缓存已成功失效",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.String("version", version),
|
|
||||||
zap.String("cache_key", cacheKey),
|
|
||||||
zap.String("cache_path", cachePath),
|
|
||||||
zap.Int64("deleted_file_size", fileSize),
|
|
||||||
zap.Time("file_created_at", fileModTime),
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidateByProductID 使指定产品的所有缓存失效
|
|
||||||
func (m *PDFCacheManager) InvalidateByProductID(productID string) error {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
// 遍历缓存目录,查找匹配的文件
|
|
||||||
files, err := os.ReadDir(m.cacheDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取缓存目录失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 读取文件内容,检查是否匹配产品ID
|
|
||||||
// 由于我们使用MD5哈希,无法直接匹配,需要读取文件元数据
|
|
||||||
// 这里简化处理:删除所有PDF文件(实际应该存储元数据)
|
|
||||||
// 更好的方案是使用数据库或JSON文件存储元数据
|
|
||||||
if filepath.Ext(file.Name()) == ".pdf" {
|
|
||||||
filePath := filepath.Join(m.cacheDir, file.Name())
|
|
||||||
if err := os.Remove(filePath); err == nil {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger.Info("已清理产品缓存",
|
|
||||||
zap.String("product_id", productID),
|
|
||||||
zap.Int("deleted_count", count),
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear 清空所有缓存
|
|
||||||
func (m *PDFCacheManager) Clear() error {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
files, err := os.ReadDir(m.cacheDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取缓存目录失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
for _, file := range files {
|
|
||||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".pdf" {
|
|
||||||
filePath := filepath.Join(m.cacheDir, file.Name())
|
|
||||||
if err := os.Remove(filePath); err == nil {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger.Info("已清空所有缓存", zap.Int("deleted_count", count))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCacheStats 获取缓存统计信息
|
|
||||||
func (m *PDFCacheManager) GetCacheStats() (map[string]interface{}, error) {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
files, err := os.ReadDir(m.cacheDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("读取缓存目录失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalSize int64
|
|
||||||
var fileCount int
|
|
||||||
var expiredCount int
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if filepath.Ext(file.Name()) == ".pdf" {
|
|
||||||
filePath := filepath.Join(m.cacheDir, file.Name())
|
|
||||||
info, err := os.Stat(filePath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
totalSize += info.Size()
|
|
||||||
fileCount++
|
|
||||||
// 检查是否过期
|
|
||||||
expiresAt := info.ModTime().Add(m.ttl)
|
|
||||||
if now.After(expiresAt) {
|
|
||||||
expiredCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]interface{}{
|
|
||||||
"total_size": totalSize,
|
|
||||||
"file_count": fileCount,
|
|
||||||
"expired_count": expiredCount,
|
|
||||||
"cache_dir": m.cacheDir,
|
|
||||||
"ttl": m.ttl.String(),
|
|
||||||
"max_size": m.maxSize,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupExpiredFiles 清理过期的缓存文件
|
|
||||||
func (m *PDFCacheManager) cleanupExpiredFiles() error {
|
|
||||||
files, err := os.ReadDir(m.cacheDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取缓存目录失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
count := 0
|
|
||||||
var totalFreed int64
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if filepath.Ext(file.Name()) == ".pdf" {
|
|
||||||
filePath := filepath.Join(m.cacheDir, file.Name())
|
|
||||||
info, err := os.Stat(filePath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
expiresAt := info.ModTime().Add(m.ttl)
|
|
||||||
if now.After(expiresAt) {
|
|
||||||
if err := os.Remove(filePath); err == nil {
|
|
||||||
count++
|
|
||||||
totalFreed += info.Size()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
m.logger.Info("已清理过期缓存文件",
|
|
||||||
zap.Int("count", count),
|
|
||||||
zap.Int64("freed_size", totalFreed),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -473,10 +240,169 @@ func (m *PDFCacheManager) startCleanupTask() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
if err := m.cleanupExpiredFiles(); err != nil {
|
if err := m.cleanExpiredFiles(); err != nil {
|
||||||
m.logger.Warn("定期清理缓存失败", zap.Error(err))
|
m.logger.Warn("清理过期缓存文件失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanExpiredFiles 清理过期的缓存文件
|
||||||
|
func (m *PDFCacheManager) cleanExpiredFiles() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(m.cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取缓存目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
cleanedCount := 0
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理PDF文件
|
||||||
|
if filepath.Ext(entry.Name()) != ".pdf" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(m.cacheDir, entry.Name())
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否过期
|
||||||
|
createdAt := info.ModTime()
|
||||||
|
expiresAt := createdAt.Add(m.ttl)
|
||||||
|
if now.After(expiresAt) {
|
||||||
|
if err := os.Remove(filePath); err != nil {
|
||||||
|
m.logger.Warn("删除过期缓存文件失败",
|
||||||
|
zap.String("file_path", filePath),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
cleanedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cleanedCount > 0 {
|
||||||
|
m.logger.Info("清理过期缓存文件完成",
|
||||||
|
zap.Int("cleaned_count", cleanedCount),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate 使缓存失效(基于产品ID+版本)
|
||||||
|
func (m *PDFCacheManager) Invalidate(productID, version string) error {
|
||||||
|
cacheKey := m.GetCacheKeyByProduct(productID, version)
|
||||||
|
cachePath := m.GetCachePath(cacheKey)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if err := os.Remove(cachePath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil // 文件不存在,视为已失效
|
||||||
|
}
|
||||||
|
return fmt.Errorf("删除缓存文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateByNameIDCard 使缓存失效(基于姓名+身份证)
|
||||||
|
func (m *PDFCacheManager) InvalidateByNameIDCard(name, idCard string) error {
|
||||||
|
cacheKey := m.GetCacheKey(name, idCard)
|
||||||
|
cachePath := m.GetCachePath(cacheKey)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if err := os.Remove(cachePath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil // 文件不存在,视为已失效
|
||||||
|
}
|
||||||
|
return fmt.Errorf("删除缓存文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空所有缓存
|
||||||
|
func (m *PDFCacheManager) Clear() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(m.cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取缓存目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() && filepath.Ext(entry.Name()) == ".pdf" {
|
||||||
|
filePath := filepath.Join(m.cacheDir, entry.Name())
|
||||||
|
if err := os.Remove(filePath); err == nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("已清空所有缓存", zap.Int("deleted_count", count))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheStats 获取缓存统计信息
|
||||||
|
func (m *PDFCacheManager) GetCacheStats() (map[string]interface{}, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(m.cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取缓存目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalSize int64
|
||||||
|
var fileCount int
|
||||||
|
var expiredCount int
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if filepath.Ext(entry.Name()) == ".pdf" {
|
||||||
|
filePath := filepath.Join(m.cacheDir, entry.Name())
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
totalSize += info.Size()
|
||||||
|
fileCount++
|
||||||
|
// 检查是否过期
|
||||||
|
expiresAt := info.ModTime().Add(m.ttl)
|
||||||
|
if now.After(expiresAt) {
|
||||||
|
expiredCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"total_size": totalSize,
|
||||||
|
"file_count": fileCount,
|
||||||
|
"expired_count": expiredCount,
|
||||||
|
"cache_dir": m.cacheDir,
|
||||||
|
"ttl": m.ttl.String(),
|
||||||
|
"max_size": m.maxSize,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user