From 12ed1c81e313706e87b5000e42be532bdc13535b Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 17 Mar 2026 17:18:54 +0800 Subject: [PATCH] add --- .../certification_application_service.go | 9 + .../certification_application_service_impl.go | 282 ++++++++++++++---- .../dto/commands/certification_commands.go | 15 + .../dto/queries/certification_queries.go | 7 + .../dto/responses/certification_responses.go | 62 +++- .../certification/entities/certification.go | 60 +++- .../entities/enterprise_info_submit_record.go | 66 +++- .../enums/certification_status.go | 67 +++-- .../certification/enums/failure_reason.go | 12 +- ...nterprise_info_submit_record_repository.go | 15 + ...nterprise_info_submit_record_repository.go | 38 +++ .../http/handlers/certification_handler.go | 209 ++++++++++++- .../http/routes/certification_routes.go | 17 ++ .../shared/middleware/comprehensive_logger.go | 20 +- .../shared/middleware/daily_rate_limit.go | 5 +- internal/shared/pdf/page_builder.go | 2 +- 16 files changed, 763 insertions(+), 123 deletions(-) diff --git a/internal/application/certification/certification_application_service.go b/internal/application/certification/certification_application_service.go index 8e42854..67fc0be 100644 --- a/internal/application/certification/certification_application_service.go +++ b/internal/application/certification/certification_application_service.go @@ -37,6 +37,15 @@ type CertificationApplicationService interface { // AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同) AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error) + // AdminListSubmitRecords 管理端分页查询企业信息提交记录 + AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error) + // AdminGetSubmitRecordByID 管理端获取单条提交记录详情 + AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error) + // AdminApproveSubmitRecord 管理端审核通过 + AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error + // AdminRejectSubmitRecord 管理端审核拒绝 + AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error + // ================ e签宝回调处理 ================ // 处理e签宝回调 diff --git a/internal/application/certification/certification_application_service_impl.go b/internal/application/certification/certification_application_service_impl.go index f38f456..5566772 100644 --- a/internal/application/certification/certification_application_service_impl.go +++ b/internal/application/certification/certification_application_service_impl.go @@ -2,6 +2,7 @@ package certification import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -10,6 +11,7 @@ import ( "tyapi-server/internal/application/certification/dto/commands" "tyapi-server/internal/application/certification/dto/queries" "tyapi-server/internal/application/certification/dto/responses" + "tyapi-server/internal/config" api_service "tyapi-server/internal/domains/api/services" "tyapi-server/internal/domains/certification/entities" certification_value_objects "tyapi-server/internal/domains/certification/entities/value_objects" @@ -18,8 +20,7 @@ import ( "tyapi-server/internal/domains/certification/services" finance_service "tyapi-server/internal/domains/finance/services" user_entities "tyapi-server/internal/domains/user/entities" - user_service "tyapi-server/internal/domains/user/services" - "tyapi-server/internal/config" + user_service "tyapi-server/internal/domains/user/services" "tyapi-server/internal/infrastructure/external/notification" "tyapi-server/internal/infrastructure/external/storage" "tyapi-server/internal/shared/database" @@ -51,6 +52,7 @@ type CertificationApplicationServiceImpl struct { wechatWorkService *notification.WeChatWorkService logger *zap.Logger + config *config.Config } // NewCertificationApplicationService 创建认证应用服务 @@ -93,6 +95,7 @@ func NewCertificationApplicationService( txManager: txManager, wechatWorkService: wechatSvc, logger: logger, + config: cfg, } } @@ -106,7 +109,7 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( s.logger.Info("开始提交企业信息", zap.String("user_id", cmd.UserID)) - // 1.5 插入企业信息提交记录 + // 1.5 插入企业信息提交记录(包含扩展字段) record := entities.NewEnterpriseInfoSubmitRecord( cmd.UserID, cmd.CompanyName, @@ -117,6 +120,36 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( cmd.EnterpriseAddress, ) + // 扩展字段赋值 + record.BusinessLicenseImageURL = cmd.BusinessLicenseImageURL + if len(cmd.OfficePlaceImageURLs) > 0 { + if data, mErr := json.Marshal(cmd.OfficePlaceImageURLs); mErr == nil { + record.OfficePlaceImageURLs = string(data) + } else { + s.logger.Warn("序列化办公场地图片URL失败", zap.Error(mErr)) + } + } + + record.APIUsage = cmd.APIUsage + if len(cmd.ScenarioAttachmentURLs) > 0 { + if data, mErr := json.Marshal(cmd.ScenarioAttachmentURLs); mErr == nil { + record.ScenarioAttachmentURLs = string(data) + } else { + s.logger.Warn("序列化场景附件图片URL失败", zap.Error(mErr)) + } + } + // 授权代表信息落库 + record.AuthorizedRepName = cmd.AuthorizedRepName + record.AuthorizedRepID = cmd.AuthorizedRepID + record.AuthorizedRepPhone = cmd.AuthorizedRepPhone + if len(cmd.AuthorizedRepIDImageURLs) > 0 { + if data, mErr := json.Marshal(cmd.AuthorizedRepIDImageURLs); mErr == nil { + record.AuthorizedRepIDImageURLs = string(data) + } else { + s.logger.Warn("序列化授权代表身份证图片URL失败", zap.Error(mErr)) + } + } + // 验证验证码 // 特殊验证码"768005"直接跳过验证环节 if cmd.VerificationCode != "768005" { @@ -141,6 +174,7 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( } return nil, fmt.Errorf("检查企业信息失败: %s", err.Error()) } + if exists { record.MarkAsFailed("该企业信息已被其他用户使用,请确认企业信息是否正确") saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) @@ -148,6 +182,7 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("该企业信息已被其他用户使用,请确认企业信息是否正确") + } enterpriseInfo := &certification_value_objects.EnterpriseInfo{ @@ -205,56 +240,24 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( return fmt.Errorf("加载认证信息失败: %s", err.Error()) } - // 3. 调用e签宝看是否进行过认证 - respMeta := map[string]interface{}{} + // 已是「已提交企业信息」:说明已通过人工审核,直接返回当前认证数据,前端可刷新到企业认证步骤 + if cert.Status == enums.StatusInfoSubmitted { + response = s.convertToResponse(cert) + response.Metadata = map[string]interface{}{ + "next_action": "您已通过审核,请完成企业认证步骤", + } + return nil + } - identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{ - OrgName: cmd.CompanyName, - }) - if identity != nil && identity.Data.RealnameStatus == 1 { - // 已提交 - err = cert.SubmitEnterpriseInfo(enterpriseInfo, "", "") - if err != nil { - return fmt.Errorf("提交企业认证信息失败: %s", err.Error()) - } - s.logger.Info("企业认证成功", zap.Any("identity", identity)) + // 4. 提交企业信息进入人工审核(不调用 e签宝,不生成认证链接) + err = cert.SubmitEnterpriseInfoForReview(enterpriseInfo) + if err != nil { + return fmt.Errorf("提交企业信息失败: %s", err.Error()) + } - // 完成企业认证流程 - err = s.completeEnterpriseVerification(txCtx, cert, cmd.UserID, cmd.CompanyName, cmd.LegalPersonName) - if err != nil { - return err - } - - respMeta = map[string]interface{}{ - "enterprise_info": enterpriseInfo, - "next_action": "企业已认证,可进行后续操作", - } - } else { - if err != nil { - s.logger.Error("e签宝查询企业认证信息失败或未进行企业认证", zap.Error(err)) - } - authURL, err := s.esignClient.GenerateEnterpriseAuth(&esign.EnterpriseAuthRequest{ - CompanyName: enterpriseInfo.CompanyName, - UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode, - LegalPersonName: enterpriseInfo.LegalPersonName, - LegalPersonID: enterpriseInfo.LegalPersonID, - TransactorName: enterpriseInfo.LegalPersonName, - TransactorMobile: enterpriseInfo.LegalPersonPhone, - TransactorID: enterpriseInfo.LegalPersonID, - }) - if err != nil { - s.logger.Error("生成企业认证链接失败", zap.Error(err)) - return fmt.Errorf("生成企业认证链接失败: %s", err.Error()) - } - err = cert.SubmitEnterpriseInfo(enterpriseInfo, authURL.AuthShortURL, authURL.AuthFlowID) - if err != nil { - return fmt.Errorf("提交企业认证信息失败: %s", err.Error()) - } - respMeta = map[string]interface{}{ - "enterprise_info": enterpriseInfo, - "authUrl": authURL.AuthURL, - "next_action": "请完成企业认证", - } + respMeta := map[string]interface{}{ + "enterprise_info": enterpriseInfo, + "next_action": "请等待管理员审核企业信息", } err = s.aggregateService.SaveCertification(txCtx, cert) @@ -694,6 +697,183 @@ func (s *CertificationApplicationServiceImpl) AdminCompleteCertificationWithoutC return response, nil } +// AdminListSubmitRecords 管理端分页查询企业信息提交记录 +func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords( + ctx context.Context, + query *queries.AdminListSubmitRecordsQuery, +) (*responses.AdminSubmitRecordsListResponse, error) { + if query.PageSize <= 0 { + query.PageSize = 10 + } + if query.Page <= 0 { + query.Page = 1 + } + filter := repositories.ListSubmitRecordsFilter{ + Page: query.Page, + PageSize: query.PageSize, + ManualReviewStatus: query.ManualReviewStatus, + } + result, err := s.enterpriseInfoSubmitRecordRepo.List(ctx, filter) + if err != nil { + return nil, fmt.Errorf("查询提交记录失败: %w", err) + } + items := make([]*responses.AdminSubmitRecordItem, 0, len(result.Records)) + for _, r := range result.Records { + certStatus := "" + if cert, err := s.aggregateService.LoadCertificationByUserID(ctx, r.UserID); err == nil && cert != nil { + certStatus = string(cert.Status) + } + items = append(items, &responses.AdminSubmitRecordItem{ + ID: r.ID, + UserID: r.UserID, + CompanyName: r.CompanyName, + UnifiedSocialCode: r.UnifiedSocialCode, + LegalPersonName: r.LegalPersonName, + SubmitAt: r.SubmitAt, + Status: r.Status, + ManualReviewStatus: r.ManualReviewStatus, + ManualReviewedAt: r.ManualReviewedAt, + CertificationStatus: certStatus, + }) + } + totalPages := int((result.Total + int64(query.PageSize) - 1) / int64(query.PageSize)) + if totalPages == 0 { + totalPages = 1 + } + return &responses.AdminSubmitRecordsListResponse{ + Items: items, + Total: result.Total, + Page: query.Page, + PageSize: query.PageSize, + TotalPages: totalPages, + }, nil +} + +// AdminGetSubmitRecordByID 管理端获取单条提交记录详情 +func (s *CertificationApplicationServiceImpl) AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error) { + record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID) + if err != nil { + return nil, fmt.Errorf("获取提交记录失败: %w", err) + } + certStatus := "" + if cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID); loadErr == nil && cert != nil { + certStatus = string(cert.Status) + } + return &responses.AdminSubmitRecordDetail{ + ID: record.ID, + UserID: record.UserID, + CompanyName: record.CompanyName, + UnifiedSocialCode: record.UnifiedSocialCode, + LegalPersonName: record.LegalPersonName, + LegalPersonID: record.LegalPersonID, + LegalPersonPhone: record.LegalPersonPhone, + EnterpriseAddress: record.EnterpriseAddress, + AuthorizedRepName: record.AuthorizedRepName, + AuthorizedRepID: record.AuthorizedRepID, + AuthorizedRepPhone: record.AuthorizedRepPhone, + AuthorizedRepIDImageURLs: record.AuthorizedRepIDImageURLs, + BusinessLicenseImageURL: record.BusinessLicenseImageURL, + OfficePlaceImageURLs: record.OfficePlaceImageURLs, + APIUsage: record.APIUsage, + ScenarioAttachmentURLs: record.ScenarioAttachmentURLs, + Status: record.Status, + SubmitAt: record.SubmitAt, + VerifiedAt: record.VerifiedAt, + FailedAt: record.FailedAt, + FailureReason: record.FailureReason, + ManualReviewStatus: record.ManualReviewStatus, + ManualReviewRemark: record.ManualReviewRemark, + ManualReviewedAt: record.ManualReviewedAt, + ManualReviewerID: record.ManualReviewerID, + CertificationStatus: certStatus, + CreatedAt: record.CreatedAt, + UpdatedAt: record.UpdatedAt, + }, nil +} + +// AdminApproveSubmitRecord 管理端审核通过 +func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error { + record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID) + if err != nil { + return fmt.Errorf("获取提交记录失败: %w", err) + } + if record.ManualReviewStatus != "pending" { + return fmt.Errorf("该记录已审核,当前状态: %s", record.ManualReviewStatus) + } + cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID) + if err != nil { + return fmt.Errorf("加载认证信息失败: %w", err) + } + if cert.Status != enums.StatusInfoPendingReview { + return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) + } + enterpriseInfo := &certification_value_objects.EnterpriseInfo{ + CompanyName: record.CompanyName, + UnifiedSocialCode: record.UnifiedSocialCode, + LegalPersonName: record.LegalPersonName, + LegalPersonID: record.LegalPersonID, + LegalPersonPhone: record.LegalPersonPhone, + EnterpriseAddress: record.EnterpriseAddress, + } + authURL, err := s.esignClient.GenerateEnterpriseAuth(&esign.EnterpriseAuthRequest{ + CompanyName: enterpriseInfo.CompanyName, + UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode, + LegalPersonName: enterpriseInfo.LegalPersonName, + LegalPersonID: enterpriseInfo.LegalPersonID, + TransactorName: enterpriseInfo.LegalPersonName, + TransactorMobile: enterpriseInfo.LegalPersonPhone, + TransactorID: enterpriseInfo.LegalPersonID, + }) + if err != nil { + return fmt.Errorf("生成企业认证链接失败: %w", err) + } + record.MarkManualApproved(adminID, remark) + if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { + return fmt.Errorf("保存提交记录失败: %w", err) + } + if err := cert.ApproveEnterpriseInfoReview(authURL.AuthShortURL, authURL.AuthFlowID, adminID); err != nil { + return fmt.Errorf("更新认证状态失败: %w", err) + } + if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { + return fmt.Errorf("保存认证信息失败: %w", err) + } + s.logger.Info("管理员审核通过企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID)) + return nil +} + +// AdminRejectSubmitRecord 管理端审核拒绝 +func (s *CertificationApplicationServiceImpl) AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error { + if remark == "" { + return fmt.Errorf("拒绝时必须填写审核备注") + } + record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID) + if err != nil { + return fmt.Errorf("获取提交记录失败: %w", err) + } + if record.ManualReviewStatus != "pending" { + return fmt.Errorf("该记录已审核,当前状态: %s", record.ManualReviewStatus) + } + cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID) + if err != nil { + return fmt.Errorf("加载认证信息失败: %w", err) + } + if cert.Status != enums.StatusInfoPendingReview { + return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) + } + record.MarkManualRejected(adminID, remark) + if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { + return fmt.Errorf("保存提交记录失败: %w", err) + } + if err := cert.RejectEnterpriseInfoReview(adminID, remark); err != nil { + return fmt.Errorf("更新认证状态失败: %w", err) + } + if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { + return fmt.Errorf("保存认证信息失败: %w", err) + } + s.logger.Info("管理员审核拒绝企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID)) + return nil +} + // ================ 辅助方法 ================ // convertToResponse 转换实体为响应DTO diff --git a/internal/application/certification/dto/commands/certification_commands.go b/internal/application/certification/dto/commands/certification_commands.go index 3f73759..a1175cf 100644 --- a/internal/application/certification/dto/commands/certification_commands.go +++ b/internal/application/certification/dto/commands/certification_commands.go @@ -104,4 +104,19 @@ type SubmitEnterpriseInfoCommand struct { LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号,11位,如:13800138000"` EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"` VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"` + + // 营业执照图片 URL(单张) + BusinessLicenseImageURL string `json:"business_license_image_url" binding:"omitempty,url" comment:"营业执照图片URL"` + // 办公场地图片 URL 列表(前端传 string 数组) + OfficePlaceImageURLs []string `json:"office_place_image_urls" binding:"omitempty,dive,url" comment:"办公场地图片URL列表"` + + // 授权代表信息(与前端 authorized_rep_* 及表字段一致) + AuthorizedRepName string `json:"authorized_rep_name" binding:"omitempty,min=2,max=20" comment:"授权代表姓名"` + AuthorizedRepID string `json:"authorized_rep_id" binding:"omitempty,id_card" comment:"授权代表身份证号"` + AuthorizedRepPhone string `json:"authorized_rep_phone" binding:"omitempty,phone" comment:"授权代表手机号"` + AuthorizedRepIDImageURLs []string `json:"authorized_rep_id_image_urls" binding:"omitempty,dive,url" comment:"授权代表身份证正反面图片URL"` + + // 应用场景 + APIUsage string `json:"api_usage" binding:"omitempty,min=5,max=500" comment:"接口用途及业务场景说明"` + ScenarioAttachmentURLs []string `json:"scenario_attachment_urls" binding:"omitempty,dive,url" comment:"场景附件图片URL列表"` } diff --git a/internal/application/certification/dto/queries/certification_queries.go b/internal/application/certification/dto/queries/certification_queries.go index 5afa2c2..0840371 100644 --- a/internal/application/certification/dto/queries/certification_queries.go +++ b/internal/application/certification/dto/queries/certification_queries.go @@ -192,3 +192,10 @@ func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool { } return false } + +// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询 +type AdminListSubmitRecordsQuery struct { + Page int `json:"page" form:"page"` + PageSize int `json:"page_size" form:"page_size"` + ManualReviewStatus string `json:"manual_review_status" form:"manual_review_status"` // pending, approved, rejected,空为全部 +} diff --git a/internal/application/certification/dto/responses/certification_responses.go b/internal/application/certification/dto/responses/certification_responses.go index 2ba58c4..672d2b8 100644 --- a/internal/application/certification/dto/responses/certification_responses.go +++ b/internal/application/certification/dto/responses/certification_responses.go @@ -53,13 +53,13 @@ type CertificationResponse struct { // ConfirmAuthResponse 确认认证状态响应 type ConfirmAuthResponse struct { Status enums.CertificationStatus `json:"status"` - Reason string `json:"reason"` + Reason string `json:"reason"` } // ConfirmSignResponse 确认签署状态响应 type ConfirmSignResponse struct { Status enums.CertificationStatus `json:"status"` - Reason string `json:"reason"` + Reason string `json:"reason"` } // CertificationListResponse 认证列表响应 @@ -81,7 +81,6 @@ type ContractSignUrlResponse struct { Message string `json:"message"` } - // SystemMonitoringResponse 系统监控响应 type SystemMonitoringResponse struct { TimeRange string `json:"time_range"` @@ -111,6 +110,61 @@ type SystemHealthStatus struct { Details map[string]interface{} `json:"details,omitempty"` } +// AdminSubmitRecordItem 管理端提交记录列表项 +type AdminSubmitRecordItem struct { + ID string `json:"id"` + UserID string `json:"user_id"` + CompanyName string `json:"company_name"` + UnifiedSocialCode string `json:"unified_social_code"` + LegalPersonName string `json:"legal_person_name"` + SubmitAt time.Time `json:"submit_at"` + Status string `json:"status"` + ManualReviewStatus string `json:"manual_review_status"` + ManualReviewedAt *time.Time `json:"manual_reviewed_at,omitempty"` + CertificationStatus string `json:"certification_status,omitempty"` // 该用户当前认证状态,用于前端判断是否已完成企业认证并显示「已审核」 +} + +// AdminSubmitRecordDetail 管理端提交记录详情(含完整信息与图片 URL) +type AdminSubmitRecordDetail struct { + ID string `json:"id"` + UserID string `json:"user_id"` + CompanyName string `json:"company_name"` + UnifiedSocialCode string `json:"unified_social_code"` + LegalPersonName string `json:"legal_person_name"` + LegalPersonID string `json:"legal_person_id"` + LegalPersonPhone string `json:"legal_person_phone"` + EnterpriseAddress string `json:"enterprise_address"` + AuthorizedRepName string `json:"authorized_rep_name"` + AuthorizedRepID string `json:"authorized_rep_id"` + AuthorizedRepPhone string `json:"authorized_rep_phone"` + AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls"` // JSON 字符串或解析后数组 + BusinessLicenseImageURL string `json:"business_license_image_url"` + OfficePlaceImageURLs string `json:"office_place_image_urls"` // JSON 数组字符串 + APIUsage string `json:"api_usage"` + ScenarioAttachmentURLs string `json:"scenario_attachment_urls"` + Status string `json:"status"` + SubmitAt time.Time `json:"submit_at"` + VerifiedAt *time.Time `json:"verified_at,omitempty"` + FailedAt *time.Time `json:"failed_at,omitempty"` + FailureReason string `json:"failure_reason,omitempty"` + ManualReviewStatus string `json:"manual_review_status"` + ManualReviewRemark string `json:"manual_review_remark,omitempty"` + ManualReviewedAt *time.Time `json:"manual_reviewed_at,omitempty"` + ManualReviewerID string `json:"manual_reviewer_id,omitempty"` + CertificationStatus string `json:"certification_status,omitempty"` // 该用户当前认证状态,用于前端显示「已审核」 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AdminSubmitRecordsListResponse 管理端提交记录列表响应 +type AdminSubmitRecordsListResponse struct { + Items []*AdminSubmitRecordItem `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + // ================ 响应构建辅助方法 ================ // NewCertificationListResponse 创建认证列表响应 @@ -146,7 +200,6 @@ func NewContractSignUrlResponse(certificationID, signURL, contractURL, nextActio return response } - // NewSystemAlert 创建系统警告 func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert { return &SystemAlert{ @@ -161,7 +214,6 @@ func NewSystemAlert(level, alertType, message, metric string, value, threshold i } } - // IsHealthy 检查系统是否健康 func (r *SystemMonitoringResponse) IsHealthy() bool { return r.SystemHealth.Overall == "healthy" diff --git a/internal/domains/certification/entities/certification.go b/internal/domains/certification/entities/certification.go index edf3d22..58adecd 100644 --- a/internal/domains/certification/entities/certification.go +++ b/internal/domains/certification/entities/certification.go @@ -153,7 +153,31 @@ func (c *Certification) TransitionTo(targetStatus enums.CertificationStatus, act // ================ 业务操作方法 ================ -// SubmitEnterpriseInfo 提交企业信息 +// SubmitEnterpriseInfoForReview 提交企业信息进入人工审核(不调用 e签宝,不生成认证链接) +func (c *Certification) SubmitEnterpriseInfoForReview(enterpriseInfo *value_objects.EnterpriseInfo) error { + // 已处于待审核:幂等,直接成功 + if c.Status == enums.StatusInfoPendingReview { + return nil + } + if c.Status != enums.StatusPending && c.Status != enums.StatusInfoRejected { + return fmt.Errorf("当前状态 %s 不允许提交企业信息", enums.GetStatusName(c.Status)) + } + if err := enterpriseInfo.Validate(); err != nil { + return fmt.Errorf("企业信息验证失败: %w", err) + } + if err := c.TransitionTo(enums.StatusInfoPendingReview, enums.ActorTypeUser, c.UserID, "用户提交企业信息,等待人工审核"); err != nil { + return err + } + c.addDomainEvent(&EnterpriseInfoSubmittedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + EnterpriseInfo: enterpriseInfo, + SubmittedAt: time.Now(), + }) + return nil +} + +// SubmitEnterpriseInfo 提交企业信息(直接进入已提交,含认证链接;用于无审核或管理员审核通过后补链) func (c *Certification) SubmitEnterpriseInfo(enterpriseInfo *value_objects.EnterpriseInfo, authURL string, authFlowID string) error { // 验证当前状态 if c.Status != enums.StatusPending && c.Status != enums.StatusInfoRejected { @@ -186,6 +210,33 @@ func (c *Certification) SubmitEnterpriseInfo(enterpriseInfo *value_objects.Enter return nil } +// ApproveEnterpriseInfoReview 管理员审核通过:从待审核转为已提交,并写入企业认证链接 +func (c *Certification) ApproveEnterpriseInfoReview(authURL, authFlowID string, actorID string) error { + if c.Status != enums.StatusInfoPendingReview { + return fmt.Errorf("当前状态 %s 不允许执行审核通过", enums.GetStatusName(c.Status)) + } + c.AuthURL = authURL + c.AuthFlowID = authFlowID + if err := c.TransitionTo(enums.StatusInfoSubmitted, enums.ActorTypeAdmin, actorID, "管理员审核通过"); err != nil { + return err + } + now := time.Now() + c.InfoSubmittedAt = &now + return nil +} + +// RejectEnterpriseInfoReview 管理员审核拒绝 +func (c *Certification) RejectEnterpriseInfoReview(actorID, message string) error { + if c.Status != enums.StatusInfoPendingReview { + return fmt.Errorf("当前状态 %s 不允许执行审核拒绝", enums.GetStatusName(c.Status)) + } + c.setFailureInfo(enums.FailureReasonManualReviewRejected, message) + if err := c.TransitionTo(enums.StatusInfoRejected, enums.ActorTypeAdmin, actorID, "管理员审核拒绝"); err != nil { + return err + } + return nil +} + // 完成企业认证 func (c *Certification) CompleteEnterpriseVerification() error { if c.Status != enums.StatusInfoSubmitted { @@ -448,6 +499,8 @@ func (c *Certification) CompleteCertification() error { func (c *Certification) GetDataByStatus() map[string]interface{} { data := map[string]interface{}{} switch c.Status { + case enums.StatusInfoPendingReview: + // 待审核,无额外数据 case enums.StatusInfoSubmitted: data["auth_url"] = c.AuthURL case enums.StatusInfoRejected: @@ -494,6 +547,8 @@ func (c *Certification) GetAvailableActions() []string { switch c.Status { case enums.StatusPending: actions = append(actions, "submit_enterprise_info") + case enums.StatusInfoPendingReview: + // 等待人工审核,无用户操作 case enums.StatusEnterpriseVerified: actions = append(actions, "apply_contract") case enums.StatusInfoRejected, enums.StatusContractRejected, enums.StatusContractExpired: @@ -587,8 +642,9 @@ func (c *Certification) ValidateBusinessRules() error { // validateActorPermission 验证操作者权限 func (c *Certification) validateActorPermission(targetStatus enums.CertificationStatus, actor enums.ActorType) bool { - // 定义状态转换的权限规则 + // 定义状态转换的权限规则(目标状态 -> 允许的操作者) permissions := map[enums.CertificationStatus][]enums.ActorType{ + enums.StatusInfoPendingReview: {enums.ActorTypeUser}, enums.StatusInfoSubmitted: {enums.ActorTypeUser, enums.ActorTypeAdmin}, enums.StatusEnterpriseVerified: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin}, enums.StatusInfoRejected: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin}, diff --git a/internal/domains/certification/entities/enterprise_info_submit_record.go b/internal/domains/certification/entities/enterprise_info_submit_record.go index 0f0d249..1d6202a 100644 --- a/internal/domains/certification/entities/enterprise_info_submit_record.go +++ b/internal/domains/certification/entities/enterprise_info_submit_record.go @@ -19,6 +19,21 @@ type EnterpriseInfoSubmitRecord struct { LegalPersonID string `json:"legal_person_id" gorm:"type:varchar(50);not null"` LegalPersonPhone string `json:"legal_person_phone" gorm:"type:varchar(50);not null"` EnterpriseAddress string `json:"enterprise_address" gorm:"type:varchar(200);not null"` // 新增企业地址 + + // 授权代表信息(gorm 指定列名,确保与表 enterprise_info_submit_records 列一致并正确读入) + AuthorizedRepName string `json:"authorized_rep_name" gorm:"column:authorized_rep_name;type:varchar(50);comment:授权代表姓名"` + AuthorizedRepID string `json:"authorized_rep_id" gorm:"column:authorized_rep_id;type:varchar(50);comment:授权代表身份证号"` + AuthorizedRepPhone string `json:"authorized_rep_phone" gorm:"column:authorized_rep_phone;type:varchar(50);comment:授权代表手机号"` + // 授权代表身份证正反面图片URL列表(JSON字符串),按顺序存储[人像面, 国徽面] + AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls" gorm:"column:authorized_rep_id_image_urls;type:text;comment:授权代表身份证正反面图片URL列表(JSON字符串)"` + + // 企业资质与场地材料 + BusinessLicenseImageURL string `json:"business_license_image_url" gorm:"type:varchar(500);comment:营业执照图片URL"` + OfficePlaceImageURLs string `json:"office_place_image_urls" gorm:"type:text;comment:办公场地图片URL列表(JSON字符串)"` + // 应用场景 + APIUsage string `json:"api_usage" gorm:"type:text;comment:接口用途及业务场景说明"` + ScenarioAttachmentURLs string `json:"scenario_attachment_urls" gorm:"type:text;comment:场景附件图片URL列表(JSON字符串)"` + // 提交状态 Status string `json:"status" gorm:"type:varchar(20);not null;default:'submitted'"` // submitted, verified, failed SubmitAt time.Time `json:"submit_at" gorm:"not null"` @@ -26,6 +41,12 @@ type EnterpriseInfoSubmitRecord struct { FailedAt *time.Time `json:"failed_at"` FailureReason string `json:"failure_reason" gorm:"type:text"` + // 人工审核信息 + ManualReviewStatus string `json:"manual_review_status" gorm:"type:varchar(20);not null;default:'pending';comment:人工审核状态(pending,approved,rejected)"` + ManualReviewRemark string `json:"manual_review_remark" gorm:"type:text;comment:人工审核备注"` + ManualReviewedAt *time.Time `json:"manual_reviewed_at" gorm:"comment:人工审核时间"` + ManualReviewerID string `json:"manual_reviewer_id" gorm:"type:varchar(36);comment:人工审核人ID"` + // 系统字段 CreatedAt time.Time `json:"created_at" gorm:"not null"` UpdatedAt time.Time `json:"updated_at" gorm:"not null"` @@ -42,18 +63,19 @@ func NewEnterpriseInfoSubmitRecord( userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string, ) *EnterpriseInfoSubmitRecord { return &EnterpriseInfoSubmitRecord{ - ID: uuid.New().String(), - UserID: userID, - CompanyName: companyName, - UnifiedSocialCode: unifiedSocialCode, - LegalPersonName: legalPersonName, - LegalPersonID: legalPersonID, - LegalPersonPhone: legalPersonPhone, - EnterpriseAddress: enterpriseAddress, - Status: "submitted", - SubmitAt: time.Now(), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + ID: uuid.New().String(), + UserID: userID, + CompanyName: companyName, + UnifiedSocialCode: unifiedSocialCode, + LegalPersonName: legalPersonName, + LegalPersonID: legalPersonID, + LegalPersonPhone: legalPersonPhone, + EnterpriseAddress: enterpriseAddress, + Status: "submitted", + ManualReviewStatus: "pending", + SubmitAt: time.Now(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } } @@ -74,6 +96,26 @@ func (r *EnterpriseInfoSubmitRecord) MarkAsFailed(reason string) { r.UpdatedAt = now } +// MarkManualApproved 标记人工审核通过 +func (r *EnterpriseInfoSubmitRecord) MarkManualApproved(reviewerID, remark string) { + now := time.Now() + r.ManualReviewStatus = "approved" + r.ManualReviewedAt = &now + r.ManualReviewerID = reviewerID + r.ManualReviewRemark = remark + r.UpdatedAt = now +} + +// MarkManualRejected 标记人工审核拒绝 +func (r *EnterpriseInfoSubmitRecord) MarkManualRejected(reviewerID, remark string) { + now := time.Now() + r.ManualReviewStatus = "rejected" + r.ManualReviewedAt = &now + r.ManualReviewerID = reviewerID + r.ManualReviewRemark = remark + r.UpdatedAt = now +} + // IsVerified 检查是否已验证 func (r *EnterpriseInfoSubmitRecord) IsVerified() bool { return r.Status == "verified" diff --git a/internal/domains/certification/enums/certification_status.go b/internal/domains/certification/enums/certification_status.go index 0711e8f..048b4c9 100644 --- a/internal/domains/certification/enums/certification_status.go +++ b/internal/domains/certification/enums/certification_status.go @@ -5,22 +5,24 @@ type CertificationStatus string const ( // === 主流程状态 === - StatusPending CertificationStatus = "pending" // 待认证 - StatusInfoSubmitted CertificationStatus = "info_submitted" // 已提交企业信息 - StatusEnterpriseVerified CertificationStatus = "enterprise_verified" // 已企业认证 - StatusContractApplied CertificationStatus = "contract_applied" // 已申请签署合同 - StatusContractSigned CertificationStatus = "contract_signed" // 已签署合同 - StatusCompleted CertificationStatus = "completed" // 认证完成 + StatusPending CertificationStatus = "pending" // 待认证 + StatusInfoPendingReview CertificationStatus = "info_pending_review" // 企业信息待人工审核 + StatusInfoSubmitted CertificationStatus = "info_submitted" // 已提交企业信息(审核通过) + StatusEnterpriseVerified CertificationStatus = "enterprise_verified" // 已企业认证 + StatusContractApplied CertificationStatus = "contract_applied" // 已申请签署合同 + StatusContractSigned CertificationStatus = "contract_signed" // 已签署合同 + StatusCompleted CertificationStatus = "completed" // 认证完成 // === 失败状态 === StatusInfoRejected CertificationStatus = "info_rejected" // 企业信息被拒绝 StatusContractRejected CertificationStatus = "contract_rejected" // 合同被拒签 - StatusContractExpired CertificationStatus = "contract_expired" // 合同签署超时 + StatusContractExpired CertificationStatus = "contract_expired" // 合同签署超时 ) // AllStatuses 所有有效状态列表 var AllStatuses = []CertificationStatus{ StatusPending, + StatusInfoPendingReview, StatusInfoSubmitted, StatusEnterpriseVerified, StatusContractApplied, @@ -34,6 +36,7 @@ var AllStatuses = []CertificationStatus{ // MainFlowStatuses 主流程状态列表 var MainFlowStatuses = []CertificationStatus{ StatusPending, + StatusInfoPendingReview, StatusInfoSubmitted, StatusEnterpriseVerified, StatusContractApplied, @@ -61,6 +64,7 @@ func IsValidStatus(status CertificationStatus) bool { func GetStatusName(status CertificationStatus) string { statusNames := map[CertificationStatus]string{ StatusPending: "待认证", + StatusInfoPendingReview: "企业信息待审核", StatusInfoSubmitted: "已提交企业信息", StatusEnterpriseVerified: "已企业认证", StatusContractApplied: "已申请签署合同", @@ -120,14 +124,15 @@ func GetStatusCategory(status CertificationStatus) string { func GetStatusPriority(status CertificationStatus) int { priorities := map[CertificationStatus]int{ StatusPending: 1, - StatusInfoSubmitted: 2, - StatusEnterpriseVerified: 3, - StatusContractApplied: 4, - StatusContractSigned: 5, - StatusCompleted: 6, - StatusInfoRejected: 7, - StatusContractRejected: 8, - StatusContractExpired: 9, + StatusInfoPendingReview: 2, + StatusInfoSubmitted: 3, + StatusEnterpriseVerified: 4, + StatusContractApplied: 5, + StatusContractSigned: 6, + StatusCompleted: 7, + StatusInfoRejected: 8, + StatusContractRejected: 9, + StatusContractExpired: 10, } if priority, exists := priorities[status]; exists { @@ -140,14 +145,15 @@ func GetStatusPriority(status CertificationStatus) int { func GetProgressPercentage(status CertificationStatus) int { progressMap := map[CertificationStatus]int{ StatusPending: 0, - StatusInfoSubmitted: 25, - StatusEnterpriseVerified: 50, - StatusContractApplied: 75, - StatusContractSigned: 100, + StatusInfoPendingReview: 15, + StatusInfoSubmitted: 25, + StatusEnterpriseVerified: 50, + StatusContractApplied: 75, + StatusContractSigned: 100, StatusCompleted: 100, - StatusInfoRejected: 25, - StatusContractRejected: 75, - StatusContractExpired: 75, + StatusInfoRejected: 25, + StatusContractRejected: 75, + StatusContractExpired: 75, } if progress, exists := progressMap[status]; exists { @@ -160,7 +166,8 @@ func GetProgressPercentage(status CertificationStatus) int { func IsUserActionRequired(status CertificationStatus) bool { userActionRequired := map[CertificationStatus]bool{ StatusPending: true, // 需要提交企业信息 - StatusInfoSubmitted: false, // 等待系统验证 + StatusInfoPendingReview: false, // 等待人工审核 + StatusInfoSubmitted: false, // 等待完成企业认证 StatusEnterpriseVerified: true, // 需要申请合同 StatusContractApplied: true, // 需要签署合同 StatusContractSigned: false, // 合同已签署,等待系统处理 @@ -180,6 +187,7 @@ func IsUserActionRequired(status CertificationStatus) bool { func GetUserActionHint(status CertificationStatus) string { hints := map[CertificationStatus]string{ StatusPending: "请提交企业信息", + StatusInfoPendingReview: "企业信息已提交,请等待管理员审核", StatusInfoSubmitted: "请完成企业认证", StatusEnterpriseVerified: "企业认证完成,请申请签署合同", StatusContractApplied: "请在规定时间内完成合同签署", @@ -200,8 +208,12 @@ func GetUserActionHint(status CertificationStatus) string { func GetNextValidStatuses(currentStatus CertificationStatus) []CertificationStatus { nextStatusMap := map[CertificationStatus][]CertificationStatus{ StatusPending: { + StatusInfoPendingReview, // 用户提交企业信息,进入待审核 + StatusCompleted, + }, + StatusInfoPendingReview: { StatusInfoSubmitted, - // 管理员/系统可直接完成认证 + StatusInfoRejected, StatusCompleted, }, StatusInfoSubmitted: { @@ -265,8 +277,11 @@ func CanTransitionTo(currentStatus, targetStatus CertificationStatus) bool { // GetTransitionReason 获取状态转换的原因描述 func GetTransitionReason(from, to CertificationStatus) string { transitionReasons := map[string]string{ - string(StatusPending) + "->" + string(StatusInfoSubmitted): "用户提交企业信息", - string(StatusInfoSubmitted) + "->" + string(StatusEnterpriseVerified): "e签宝企业认证成功", + string(StatusPending) + "->" + string(StatusInfoPendingReview): "用户提交企业信息,等待人工审核", + string(StatusInfoPendingReview) + "->" + string(StatusInfoSubmitted): "管理员审核通过", + string(StatusInfoPendingReview) + "->" + string(StatusInfoRejected): "管理员审核拒绝", + string(StatusPending) + "->" + string(StatusInfoSubmitted): "用户提交企业信息", + string(StatusInfoSubmitted) + "->" + string(StatusEnterpriseVerified): "e签宝企业认证成功", string(StatusInfoSubmitted) + "->" + string(StatusInfoRejected): "e签宝企业认证失败", string(StatusEnterpriseVerified) + "->" + string(StatusContractApplied): "用户申请签署合同", string(StatusContractApplied) + "->" + string(StatusContractSigned): "e签宝合同签署成功", diff --git a/internal/domains/certification/enums/failure_reason.go b/internal/domains/certification/enums/failure_reason.go index c593a3d..0cfac32 100644 --- a/internal/domains/certification/enums/failure_reason.go +++ b/internal/domains/certification/enums/failure_reason.go @@ -11,6 +11,7 @@ const ( FailureReasonLegalPersonMismatch FailureReason = "legal_person_mismatch" // 法定代表人信息不匹配 FailureReasonEsignVerificationFailed FailureReason = "esign_verification_failed" // e签宝验证失败 FailureReasonInvalidDocument FailureReason = "invalid_document" // 证件信息无效 + FailureReasonManualReviewRejected FailureReason = "manual_review_rejected" // 人工审核拒绝 // === 合同签署失败原因 === FailureReasonContractRejectedByUser FailureReason = "contract_rejected_by_user" // 用户拒绝签署 @@ -35,7 +36,7 @@ var AllFailureReasons = []FailureReason{ FailureReasonLegalPersonMismatch, FailureReasonEsignVerificationFailed, FailureReasonInvalidDocument, - + FailureReasonManualReviewRejected, // 合同签署失败 FailureReasonContractRejectedByUser, FailureReasonContractExpired, @@ -97,7 +98,7 @@ func GetFailureReasonName(reason FailureReason) string { FailureReasonLegalPersonMismatch: "法定代表人信息不匹配", FailureReasonEsignVerificationFailed: "e签宝验证失败", FailureReasonInvalidDocument: "证件信息无效", - + FailureReasonManualReviewRejected: "人工审核拒绝", // 合同签署失败 FailureReasonContractRejectedByUser: "用户拒绝签署", FailureReasonContractExpired: "合同签署超时", @@ -128,7 +129,7 @@ func GetFailureReasonCategory(reason FailureReason) string { FailureReasonLegalPersonMismatch: "企业验证", FailureReasonEsignVerificationFailed: "企业验证", FailureReasonInvalidDocument: "企业验证", - + FailureReasonManualReviewRejected: "人工审核", // 合同签署失败 FailureReasonContractRejectedByUser: "合同签署", FailureReasonContractExpired: "合同签署", @@ -189,7 +190,7 @@ func GetSuggestedAction(reason FailureReason) string { FailureReasonLegalPersonMismatch: "请核对法定代表人信息是否正确", FailureReasonEsignVerificationFailed: "请稍后重试,如持续失败请联系客服", FailureReasonInvalidDocument: "请检查证件信息是否有效", - + FailureReasonManualReviewRejected: "请根据审核意见修正后重新提交,或联系客服", // 合同签署失败 FailureReasonContractRejectedByUser: "您可以重新申请签署合同", FailureReasonContractExpired: "请重新申请签署合同", @@ -220,7 +221,7 @@ func IsRetryable(reason FailureReason) bool { FailureReasonLegalPersonMismatch: true, FailureReasonEsignVerificationFailed: true, // 可能是临时问题 FailureReasonInvalidDocument: true, - + FailureReasonManualReviewRejected: true, // 用户可修正后重新提交 // 合同签署失败 FailureReasonContractRejectedByUser: true, // 用户可以改变主意 FailureReasonContractExpired: true, // 可以重新申请 @@ -253,6 +254,7 @@ func GetRetrySuggestion(reason FailureReason) string { FailureReasonLegalPersonMismatch: "请确认法定代表人信息后重新提交", FailureReasonEsignVerificationFailed: "请稍后重新尝试", FailureReasonInvalidDocument: "请检查证件信息后重新提交", + FailureReasonManualReviewRejected: "请根据审核意见修正企业信息后重新提交", FailureReasonContractRejectedByUser: "如需要可重新申请合同", FailureReasonContractExpired: "请重新申请合同签署", FailureReasonSignProcessFailed: "请重新尝试签署", diff --git a/internal/domains/certification/repositories/enterprise_info_submit_record_repository.go b/internal/domains/certification/repositories/enterprise_info_submit_record_repository.go index f481fa8..77bf909 100644 --- a/internal/domains/certification/repositories/enterprise_info_submit_record_repository.go +++ b/internal/domains/certification/repositories/enterprise_info_submit_record_repository.go @@ -5,10 +5,25 @@ import ( "tyapi-server/internal/domains/certification/entities" ) +// ListSubmitRecordsFilter 提交记录列表筛选 +type ListSubmitRecordsFilter struct { + ManualReviewStatus string // pending, approved, rejected,空表示全部 + Page int + PageSize int +} + +// ListSubmitRecordsResult 列表结果 +type ListSubmitRecordsResult struct { + Records []*entities.EnterpriseInfoSubmitRecord + Total int64 +} + type EnterpriseInfoSubmitRecordRepository interface { Create(ctx context.Context, record *entities.EnterpriseInfoSubmitRecord) error Update(ctx context.Context, record *entities.EnterpriseInfoSubmitRecord) error Exists(ctx context.Context, ID string) (bool, error) + FindByID(ctx context.Context, id string) (*entities.EnterpriseInfoSubmitRecord, error) FindLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) FindLatestVerifiedByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) + List(ctx context.Context, filter ListSubmitRecordsFilter) (*ListSubmitRecordsResult, error) } diff --git a/internal/infrastructure/database/repositories/certification/gorm_enterprise_info_submit_record_repository.go b/internal/infrastructure/database/repositories/certification/gorm_enterprise_info_submit_record_repository.go index a741e91..8dee97c 100644 --- a/internal/infrastructure/database/repositories/certification/gorm_enterprise_info_submit_record_repository.go +++ b/internal/infrastructure/database/repositories/certification/gorm_enterprise_info_submit_record_repository.go @@ -3,6 +3,7 @@ package certification import ( "context" "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/repositories" "tyapi-server/internal/shared/database" "go.uber.org/zap" @@ -39,6 +40,15 @@ func (r *GormEnterpriseInfoSubmitRecordRepository) Exists(ctx context.Context, I return r.ExistsEntity(ctx, ID, &entities.EnterpriseInfoSubmitRecord{}) } +func (r *GormEnterpriseInfoSubmitRecordRepository) FindByID(ctx context.Context, id string) (*entities.EnterpriseInfoSubmitRecord, error) { + var record entities.EnterpriseInfoSubmitRecord + err := r.GetDB(ctx).Where("id = ?", id).First(&record).Error + if err != nil { + return nil, err + } + return &record, nil +} + func (r *GormEnterpriseInfoSubmitRecordRepository) FindLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) { var record entities.EnterpriseInfoSubmitRecord err := r.GetDB(ctx). @@ -61,4 +71,32 @@ func (r *GormEnterpriseInfoSubmitRecordRepository) FindLatestVerifiedByUserID(ct return nil, err } return &record, nil +} + +func (r *GormEnterpriseInfoSubmitRecordRepository) List(ctx context.Context, filter repositories.ListSubmitRecordsFilter) (*repositories.ListSubmitRecordsResult, error) { + db := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{}) + if filter.ManualReviewStatus != "" { + db = db.Where("manual_review_status = ?", filter.ManualReviewStatus) + } + var total int64 + if err := db.Count(&total).Error; err != nil { + return nil, err + } + if filter.PageSize <= 0 { + filter.PageSize = 10 + } + if filter.Page <= 0 { + filter.Page = 1 + } + offset := (filter.Page - 1) * filter.PageSize + var records []*entities.EnterpriseInfoSubmitRecord + q := r.GetDB(ctx) + if filter.ManualReviewStatus != "" { + q = q.Where("manual_review_status = ?", filter.ManualReviewStatus) + } + err := q.Order("submit_at DESC").Offset(offset).Limit(filter.PageSize).Find(&records).Error + if err != nil { + return nil, err + } + return &repositories.ListSubmitRecordsResult{Records: records, Total: total}, nil } \ No newline at end of file diff --git a/internal/infrastructure/http/handlers/certification_handler.go b/internal/infrastructure/http/handlers/certification_handler.go index 48b5c46..0fa8089 100644 --- a/internal/infrastructure/http/handlers/certification_handler.go +++ b/internal/infrastructure/http/handlers/certification_handler.go @@ -14,17 +14,19 @@ import ( "tyapi-server/internal/application/certification/dto/commands" "tyapi-server/internal/application/certification/dto/queries" _ "tyapi-server/internal/application/certification/dto/responses" + "tyapi-server/internal/infrastructure/external/storage" "tyapi-server/internal/shared/interfaces" "tyapi-server/internal/shared/middleware" ) // CertificationHandler 认证HTTP处理器 type CertificationHandler struct { - appService certification.CertificationApplicationService - response interfaces.ResponseBuilder - validator interfaces.RequestValidator - logger *zap.Logger - jwtAuth *middleware.JWTAuthMiddleware + appService certification.CertificationApplicationService + response interfaces.ResponseBuilder + validator interfaces.RequestValidator + logger *zap.Logger + jwtAuth *middleware.JWTAuthMiddleware + storageService *storage.QiNiuStorageService } // NewCertificationHandler 创建认证处理器 @@ -34,13 +36,15 @@ func NewCertificationHandler( validator interfaces.RequestValidator, logger *zap.Logger, jwtAuth *middleware.JWTAuthMiddleware, + storageService *storage.QiNiuStorageService, ) *CertificationHandler { return &CertificationHandler{ - appService: appService, - response: response, - validator: validator, - logger: logger, - jwtAuth: jwtAuth, + appService: appService, + response: response, + validator: validator, + logger: logger, + jwtAuth: jwtAuth, + storageService: storageService, } } @@ -295,6 +299,78 @@ func (h *CertificationHandler) RecognizeBusinessLicense(c *gin.Context) { h.response.Success(c, result, "营业执照识别成功") } +// UploadCertificationFile 上传认证相关图片到七牛云(企业信息中的营业执照、办公场地、场景附件、授权代表身份证等) +// @Summary 上传认证图片 +// @Description 上传企业信息中使用的图片到七牛云,返回可访问的 URL +// @Tags 认证管理 +// @Accept multipart/form-data +// @Produce json +// @Security Bearer +// @Param file formData file true "图片文件" +// @Success 200 {object} map[string]string "上传成功,返回 url 与 key" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/certifications/upload [post] +func (h *CertificationHandler) UploadCertificationFile(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + file, err := c.FormFile("file") + if err != nil { + h.logger.Error("获取上传文件失败", zap.Error(err), zap.String("user_id", userID)) + h.response.BadRequest(c, "请选择要上传的图片文件") + return + } + + allowedTypes := map[string]bool{ + "image/jpeg": true, + "image/jpg": true, + "image/png": true, + "image/webp": true, + } + contentType := file.Header.Get("Content-Type") + if !allowedTypes[contentType] { + h.response.BadRequest(c, "只支持 JPG、PNG、WEBP 格式的图片") + return + } + + if file.Size > 5*1024*1024 { + h.response.BadRequest(c, "图片大小不能超过 5MB") + return + } + + src, err := file.Open() + if err != nil { + h.logger.Error("打开上传文件失败", zap.Error(err), zap.String("user_id", userID)) + h.response.BadRequest(c, "文件读取失败") + return + } + defer src.Close() + + fileBytes, err := io.ReadAll(src) + if err != nil { + h.logger.Error("读取文件内容失败", zap.Error(err), zap.String("user_id", userID)) + h.response.BadRequest(c, "文件读取失败") + return + } + + uploadResult, err := h.storageService.UploadFile(c.Request.Context(), fileBytes, file.Filename) + if err != nil { + h.logger.Error("上传文件到七牛云失败", zap.Error(err), zap.String("user_id", userID), zap.String("file_name", file.Filename)) + h.response.BadRequest(c, "图片上传失败,请稍后重试") + return + } + + h.response.Success(c, map[string]string{ + "url": uploadResult.URL, + "key": uploadResult.Key, + }, "上传成功") +} + // ListCertifications 获取认证列表(管理员) // @Summary 获取认证列表 // @Description 管理员获取认证申请列表 @@ -375,6 +451,119 @@ func (h *CertificationHandler) AdminCompleteCertificationWithoutContract(c *gin. h.response.Success(c, result, "代用户完成认证成功") } +// AdminListSubmitRecords 管理端分页查询企业信息提交记录 +// @Summary 管理端企业审核列表 +// @Tags 认证管理 +// @Produce json +// @Security Bearer +// @Param page query int false "页码" +// @Param page_size query int false "每页条数" +// @Param manual_review_status query string false "审核状态 pending/approved/rejected" +// @Success 200 {object} responses.AdminSubmitRecordsListResponse +// @Router /api/v1/certifications/admin/submit-records [get] +func (h *CertificationHandler) AdminListSubmitRecords(c *gin.Context) { + query := &queries.AdminListSubmitRecordsQuery{} + if err := c.ShouldBindQuery(query); err != nil { + h.response.BadRequest(c, "参数错误") + return + } + result, err := h.appService.AdminListSubmitRecords(c.Request.Context(), query) + if err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, result, "获取成功") +} + +// AdminGetSubmitRecordByID 管理端获取单条提交记录详情 +// @Summary 管理端企业审核详情 +// @Tags 认证管理 +// @Produce json +// @Security Bearer +// @Param id path string true "记录ID" +// @Success 200 {object} responses.AdminSubmitRecordDetail +// @Router /api/v1/certifications/admin/submit-records/{id} [get] +func (h *CertificationHandler) AdminGetSubmitRecordByID(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.response.BadRequest(c, "记录ID不能为空") + return + } + result, err := h.appService.AdminGetSubmitRecordByID(c.Request.Context(), id) + if err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, result, "获取成功") +} + +// AdminApproveSubmitRecord 管理端审核通过 +// @Summary 管理端企业审核通过 +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param id path string true "记录ID" +// @Param request body object true "可选 remark" +// @Success 200 {object} map[string]interface{} +// @Router /api/v1/certifications/admin/submit-records/{id}/approve [post] +func (h *CertificationHandler) AdminApproveSubmitRecord(c *gin.Context) { + adminID := h.getCurrentUserID(c) + if adminID == "" { + h.response.Unauthorized(c, "未登录") + return + } + id := c.Param("id") + if id == "" { + h.response.BadRequest(c, "记录ID不能为空") + return + } + var body struct { + Remark string `json:"remark"` + } + _ = c.ShouldBindJSON(&body) + if err := h.appService.AdminApproveSubmitRecord(c.Request.Context(), id, adminID, body.Remark); err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, nil, "审核通过") +} + +// AdminRejectSubmitRecord 管理端审核拒绝 +// @Summary 管理端企业审核拒绝 +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param id path string true "记录ID" +// @Param request body object true "remark 必填" +// @Success 200 {object} map[string]interface{} +// @Router /api/v1/certifications/admin/submit-records/{id}/reject [post] +func (h *CertificationHandler) AdminRejectSubmitRecord(c *gin.Context) { + adminID := h.getCurrentUserID(c) + if adminID == "" { + h.response.Unauthorized(c, "未登录") + return + } + id := c.Param("id") + if id == "" { + h.response.BadRequest(c, "记录ID不能为空") + return + } + var body struct { + Remark string `json:"remark" binding:"required"` + } + if err := c.ShouldBindJSON(&body); err != nil { + h.response.BadRequest(c, "请填写拒绝原因(remark)") + return + } + if err := h.appService.AdminRejectSubmitRecord(c.Request.Context(), id, adminID, body.Remark); err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, nil, "已拒绝") +} + // ================ 回调处理 ================ // HandleEsignCallback 处理e签宝回调 diff --git a/internal/infrastructure/http/routes/certification_routes.go b/internal/infrastructure/http/routes/certification_routes.go index 9a487e3..d1ac1a1 100644 --- a/internal/infrastructure/http/routes/certification_routes.go +++ b/internal/infrastructure/http/routes/certification_routes.go @@ -14,6 +14,7 @@ type CertificationRoutes struct { router *http.GinRouter logger *zap.Logger auth *middleware.JWTAuthMiddleware + admin *middleware.AdminAuthMiddleware optional *middleware.OptionalAuthMiddleware dailyRateLimit *middleware.DailyRateLimitMiddleware } @@ -24,6 +25,7 @@ func NewCertificationRoutes( router *http.GinRouter, logger *zap.Logger, auth *middleware.JWTAuthMiddleware, + admin *middleware.AdminAuthMiddleware, optional *middleware.OptionalAuthMiddleware, dailyRateLimit *middleware.DailyRateLimitMiddleware, ) *CertificationRoutes { @@ -32,6 +34,7 @@ func NewCertificationRoutes( router: router, logger: logger, auth: auth, + admin: admin, optional: optional, dailyRateLimit: dailyRateLimit, } @@ -57,6 +60,9 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) { // OCR营业执照识别接口 authGroup.POST("/ocr/business-license", r.handler.RecognizeBusinessLicense) + // 认证图片上传(七牛云,用于企业信息中的各类图片) + authGroup.POST("/upload", r.handler.UploadCertificationFile) + // 3. 申请合同签署 authGroup.POST("/apply-contract", r.handler.ApplyContract) @@ -71,6 +77,17 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) { } + // 管理端企业审核(需管理员权限) + adminCertGroup := certificationGroup.Group("/admin/submit-records") + adminCertGroup.Use(r.auth.Handle()) + adminCertGroup.Use(r.admin.Handle()) + { + adminCertGroup.GET("", r.handler.AdminListSubmitRecords) + adminCertGroup.GET("/:id", r.handler.AdminGetSubmitRecordByID) + adminCertGroup.POST("/:id/approve", r.handler.AdminApproveSubmitRecord) + adminCertGroup.POST("/:id/reject", r.handler.AdminRejectSubmitRecord) + } + // 回调路由(不需要认证,但需要验证签名) callbackGroup := certificationGroup.Group("/callbacks") { diff --git a/internal/shared/middleware/comprehensive_logger.go b/internal/shared/middleware/comprehensive_logger.go index 23f04ee..2668f3e 100644 --- a/internal/shared/middleware/comprehensive_logger.go +++ b/internal/shared/middleware/comprehensive_logger.go @@ -19,15 +19,15 @@ type ComprehensiveLoggerMiddleware struct { // ComprehensiveLoggerConfig 全面日志配置 type ComprehensiveLoggerConfig struct { - EnableRequestLogging bool // 是否记录请求日志 - EnableResponseLogging bool // 是否记录响应日志 - EnableRequestBodyLogging bool // 是否记录请求体 - EnableErrorLogging bool // 是否记录错误日志 - EnableBusinessLogging bool // 是否记录业务日志 - EnablePerformanceLogging bool // 是否记录性能日志 - MaxBodySize int64 // 最大记录体大小 - ExcludePaths []string // 排除的路径 - IncludePaths []string // 包含的路径 + EnableRequestLogging bool // 是否记录请求日志 + EnableResponseLogging bool // 是否记录响应日志 + EnableRequestBodyLogging bool // 是否记录请求体 + EnableErrorLogging bool // 是否记录错误日志 + EnableBusinessLogging bool // 是否记录业务日志 + EnablePerformanceLogging bool // 是否记录性能日志 + MaxBodySize int64 // 最大记录体大小 + ExcludePaths []string // 排除的路径 + IncludePaths []string // 包含的路径 } // NewComprehensiveLoggerMiddleware 创建全面日志中间件 @@ -439,4 +439,4 @@ func (bl *BusinessLogger) getUserIDFromContext(ctx context.Context) string { } } return "" -} \ No newline at end of file +} diff --git a/internal/shared/middleware/daily_rate_limit.go b/internal/shared/middleware/daily_rate_limit.go index a42220a..b1dc369 100644 --- a/internal/shared/middleware/daily_rate_limit.go +++ b/internal/shared/middleware/daily_rate_limit.go @@ -97,7 +97,10 @@ func (m *DailyRateLimitMiddleware) GetPriority() int { func (m *DailyRateLimitMiddleware) Handle() gin.HandlerFunc { return func(c *gin.Context) { ctx := c.Request.Context() - + if m.config.App.IsDevelopment() { + c.Next() + return + } // 检查是否在排除路径中 if m.isExcludedPath(c.Request.URL.Path) { c.Next() diff --git a/internal/shared/pdf/page_builder.go b/internal/shared/pdf/page_builder.go index 88a8f40..fe28878 100644 --- a/internal/shared/pdf/page_builder.go +++ b/internal/shared/pdf/page_builder.go @@ -1139,7 +1139,7 @@ func (pb *PageBuilder) addWatermark(pdf *gofpdf.Fpdf, chineseFontAvailable bool) // 段前缩进宽度(约两字符,mm) const paragraphIndentMM = 7.0 -// drawRichTextBlock 按段落与换行绘制文本块(还原 HTML 换行),超出 maxContentY 截断并显示 … +// drawRichTextBlock 按段落与换行绘制文本块(还原 HTML 换行) // align: "C" 居中;"L" 左对齐。firstLineIndent 为 true 时每段首行缩进(段前两空格效果)。 func (pb *PageBuilder) drawRichTextBlock(pdf *gofpdf.Fpdf, text string, contentWidth, lineHeight float64, maxContentY float64, align string, firstLineIndent bool, chineseFontAvailable bool) { pageWidth, _ := pdf.GetPageSize()