This commit is contained in:
liangzai
2026-01-27 16:26:48 +08:00
parent 3ef7b7d1fb
commit f8806eb71c
19 changed files with 1260 additions and 358 deletions

View File

@@ -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表示不限制单位字节
# =========================================== # ===========================================
# ✨ 数脉配置走实时接口 # ✨ 数脉配置走实时接口
# =========================================== # ===========================================

View File

@@ -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签宝回调

View File

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

View File

@@ -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"`

View File

@@ -38,8 +38,9 @@ type Config struct {
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"` TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Alicloud AlicloudConfig `mapstructure:"alicloud"` Alicloud AlicloudConfig `mapstructure:"alicloud"`
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域名

View File

@@ -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 // 返回nilhandler中会检查 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()

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

View File

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

View File

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

View File

@@ -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获取产品信息

View File

@@ -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, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 获取全局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("无法获取处理器: %sCombService不支持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)
}

View File

@@ -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("认证完成状态下必须有完成时间")
} }

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

View File

@@ -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签宝回调

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

View File

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

View File

@@ -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)
} }
// 回调路由(不需要认证,但需要验证签名) // 回调路由(不需要认证,但需要验证签名)

View 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路由注册完成")
}

View File

@@ -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,250 +202,18 @@ 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_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.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_key", cacheKey),
zap.String("cache_path", cachePath), zap.Int64("file_size", int64(len(pdfBytes))),
) )
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
}