diff --git a/internal/application/certification/certification_application_service_impl.go b/internal/application/certification/certification_application_service_impl.go index 37aaf51..9c748d4 100644 --- a/internal/application/certification/certification_application_service_impl.go +++ b/internal/application/certification/certification_application_service_impl.go @@ -1029,44 +1029,19 @@ func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx conte // 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.Status != "verified" { - return fmt.Errorf("该条提交记录未通过前置校验或已失败,无法从后台拒绝(请查看历史失败原因)") - } cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID) if err != nil { return fmt.Errorf("加载认证信息失败: %w", err) } - - // 幂等:认证已处于拒绝或后续状态,无需重复拒绝 - switch cert.Status { - case enums.StatusInfoRejected, - enums.StatusEnterpriseVerified, - enums.StatusContractApplied, - enums.StatusContractSigned, - enums.StatusCompleted, - enums.StatusContractRejected, - enums.StatusContractExpired: - return nil + if cert.UserID != record.UserID { + return fmt.Errorf("提交记录与认证用户不匹配,无法拒绝") } - if cert.Status != enums.StatusInfoPendingReview { - return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) - } - 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) - } - record.MarkManualRejected(adminID, remark) - if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { - return fmt.Errorf("保存企业信息提交记录失败: %w", err) + if err := s.rejectEnterpriseInfoWithCleanup(ctx, cert, record, adminID, remark); err != nil { + return err } s.logger.Info("管理员审核拒绝企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID)) return nil @@ -1137,23 +1112,19 @@ func (s *CertificationApplicationServiceImpl) AdminTransitionCertificationStatus s.logger.Info("管理端变更认证状态为通过", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID)) return nil case string(enums.StatusInfoRejected): - // 审核拒绝 + if cmd.Remark == "" { + return fmt.Errorf("拒绝时必须填写审核备注") + } if cert.Status == enums.StatusInfoRejected || cert.Status == enums.StatusEnterpriseVerified || cert.Status == enums.StatusContractApplied || cert.Status == enums.StatusContractSigned || cert.Status == enums.StatusCompleted { return nil } - if cert.Status != enums.StatusInfoPendingReview { - return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) + verifiedRecord, verr := s.enterpriseInfoSubmitRecordRepo.FindLatestVerifiedByUserID(ctx, cert.UserID) + if verr != nil { + return fmt.Errorf("查找该用户有效的企业信息提交记录失败: %w", verr) } - if err := cert.RejectEnterpriseInfoReview(cmd.AdminID, cmd.Remark); err != nil { - return fmt.Errorf("更新认证状态失败: %w", err) - } - if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { - return fmt.Errorf("保存认证信息失败: %w", err) - } - record.MarkManualRejected(cmd.AdminID, cmd.Remark) - if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { - return fmt.Errorf("保存企业信息提交记录失败: %w", err) + if err := s.rejectEnterpriseInfoWithCleanup(ctx, cert, verifiedRecord, cmd.AdminID, cmd.Remark); err != nil { + return err } s.logger.Info("管理端变更认证状态为拒绝", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID)) return nil @@ -1164,6 +1135,51 @@ func (s *CertificationApplicationServiceImpl) AdminTransitionCertificationStatus // ================ 辅助方法 ================ +// rejectEnterpriseInfoWithCleanup 管理员拒绝企业信息:同步更新认证状态与提交记录(仅释放该用户自己的 USCC 占用) +func (s *CertificationApplicationServiceImpl) rejectEnterpriseInfoWithCleanup( + ctx context.Context, + cert *entities.Certification, + record *entities.EnterpriseInfoSubmitRecord, + adminID, remark string, +) error { + if remark == "" { + return fmt.Errorf("拒绝时必须填写审核备注") + } + if record.UserID != cert.UserID { + return fmt.Errorf("提交记录与认证用户不匹配,无法拒绝") + } + if record.Status != "verified" { + return fmt.Errorf("该条提交记录未通过前置校验或已失败,无法从后台拒绝(请查看历史失败原因)") + } + switch cert.Status { + case enums.StatusInfoRejected, + enums.StatusEnterpriseVerified, + enums.StatusContractApplied, + enums.StatusContractSigned, + enums.StatusCompleted, + enums.StatusContractRejected, + enums.StatusContractExpired: + return nil + } + if !enums.CanAdminRejectEnterpriseInfoPhase(cert.Status) { + return fmt.Errorf("当前认证已进入企业认证/合同阶段,不可拒绝企业信息") + } + 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) + } + updated, err := s.enterpriseInfoSubmitRecordRepo.MarkRejectedAndFailedForUser(ctx, record.ID, cert.UserID, adminID, remark) + if err != nil { + return fmt.Errorf("保存企业信息提交记录失败: %w", err) + } + if !updated { + return fmt.Errorf("未找到该用户对应的有效提交记录,USCC 占用未释放") + } + return nil +} + // convertToResponse 转换实体为响应DTO func (s *CertificationApplicationServiceImpl) convertToResponse(cert *entities.Certification) *responses.CertificationResponse { response := &responses.CertificationResponse{ diff --git a/internal/domains/certification/entities/certification.go b/internal/domains/certification/entities/certification.go index e78b7a8..570c681 100644 --- a/internal/domains/certification/entities/certification.go +++ b/internal/domains/certification/entities/certification.go @@ -227,12 +227,20 @@ func (c *Certification) ApproveEnterpriseInfoReview(authURL, authFlowID string, return nil } -// RejectEnterpriseInfoReview 管理员审核拒绝 +// RejectEnterpriseInfoReview 管理员拒绝企业信息(info_pending_review 或 info_submitted) func (c *Certification) RejectEnterpriseInfoReview(actorID, message string) error { - if c.Status != enums.StatusInfoPendingReview { - return fmt.Errorf("当前状态 %s 不允许执行审核拒绝", enums.GetStatusName(c.Status)) + if !enums.CanAdminRejectEnterpriseInfoPhase(c.Status) { + return fmt.Errorf("当前认证已进入企业认证/合同阶段,不可拒绝企业信息") + } + failureReason := enums.FailureReasonManualReviewRejected + if c.Status == enums.StatusInfoSubmitted { + failureReason = enums.FailureReasonEsignVerificationFailed + } + c.setFailureInfo(failureReason, message) + if c.Status == enums.StatusInfoSubmitted { + c.AuthURL = "" + c.AuthFlowID = "" } - c.setFailureInfo(enums.FailureReasonManualReviewRejected, message) if err := c.TransitionTo(enums.StatusInfoRejected, enums.ActorTypeAdmin, actorID, "管理员审核拒绝"); err != nil { return err } diff --git a/internal/domains/certification/enums/certification_status.go b/internal/domains/certification/enums/certification_status.go index 3410d69..fa36f87 100644 --- a/internal/domains/certification/enums/certification_status.go +++ b/internal/domains/certification/enums/certification_status.go @@ -242,7 +242,8 @@ func GetNextValidStatuses(currentStatus CertificationStatus) []CertificationStat // 最终状态,无后续状态 }, StatusInfoRejected: { - StatusInfoSubmitted, // 可以重新提交 + StatusInfoPendingReview, // 用户修正后重新提交,进入人工审核 + StatusInfoSubmitted, // 兼容旧路径:直接重新提交 // 管理员/系统可直接标记为完成 StatusCompleted, }, @@ -289,6 +290,7 @@ func GetTransitionReason(from, to CertificationStatus) string { string(StatusContractSigned) + "->" + string(StatusCompleted): "系统处理完成,认证成功", string(StatusContractApplied) + "->" + string(StatusContractRejected): "用户拒绝签署合同", string(StatusContractApplied) + "->" + string(StatusContractExpired): "合同签署超时", + string(StatusInfoRejected) + "->" + string(StatusInfoPendingReview): "用户修正后重新提交企业信息", string(StatusInfoRejected) + "->" + string(StatusInfoSubmitted): "用户重新提交企业信息", string(StatusContractRejected) + "->" + string(StatusEnterpriseVerified): "重置状态,准备重新申请", string(StatusContractExpired) + "->" + string(StatusEnterpriseVerified): "重置状态,准备重新申请", @@ -300,3 +302,8 @@ func GetTransitionReason(from, to CertificationStatus) string { } return "未知转换" } + +// CanAdminRejectEnterpriseInfoPhase 是否允许管理员拒绝企业信息(仅 early phase) +func CanAdminRejectEnterpriseInfoPhase(status CertificationStatus) bool { + return status == StatusInfoPendingReview || status == StatusInfoSubmitted +} 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 97c06a1..6e93caa 100644 --- a/internal/domains/certification/repositories/enterprise_info_submit_record_repository.go +++ b/internal/domains/certification/repositories/enterprise_info_submit_record_repository.go @@ -30,5 +30,7 @@ type EnterpriseInfoSubmitRecordRepository interface { FindLatestVerifiedByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) // ExistsByUnifiedSocialCodeExcludeUser 检查该统一社会信用代码是否已被其他用户提交(已提交/已通过验证,排除指定用户) ExistsByUnifiedSocialCodeExcludeUser(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error) + // MarkRejectedAndFailedForUser 仅当提交记录属于指定用户且为 verified 时,标记人工拒绝并 failed(释放该用户占用的 USCC) + MarkRejectedAndFailedForUser(ctx context.Context, recordID, userID, reviewerID, remark string) (updated bool, err 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 1156965..1b96a47 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 @@ -2,6 +2,8 @@ package certification import ( "context" + "time" + "tyapi-server/internal/domains/certification/entities" "tyapi-server/internal/domains/certification/repositories" "tyapi-server/internal/shared/database" @@ -90,6 +92,33 @@ func (r *GormEnterpriseInfoSubmitRecordRepository) ExistsByUnifiedSocialCodeExcl return count > 0, nil } +// MarkRejectedAndFailedForUser 仅当 id 与 user_id 均匹配且 status=verified 时更新,避免误释放其他用户的 USCC 占用 +func (r *GormEnterpriseInfoSubmitRecordRepository) MarkRejectedAndFailedForUser( + ctx context.Context, + recordID, userID, reviewerID, remark string, +) (bool, error) { + if recordID == "" || userID == "" { + return false, nil + } + now := time.Now() + result := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{}). + Where("id = ? AND user_id = ? AND status = ?", recordID, userID, "verified"). + Updates(map[string]interface{}{ + "status": "failed", + "failed_at": now, + "failure_reason": remark, + "manual_review_status": "rejected", + "manual_reviewed_at": now, + "manual_reviewer_id": reviewerID, + "manual_review_remark": remark, + "updated_at": now, + }) + if result.Error != nil { + return false, result.Error + } + return result.RowsAffected > 0, nil +} + func (r *GormEnterpriseInfoSubmitRecordRepository) List(ctx context.Context, filter repositories.ListSubmitRecordsFilter) (*repositories.ListSubmitRecordsResult, error) { base := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{}) if filter.CertificationStatus != "" {